Skip to content

Commit

Permalink
Merge pull request #1013 from tomrule007/feature/markdown-toolbar
Browse files Browse the repository at this point in the history
WIP Feature/markdown toolbar
  • Loading branch information
songz authored Oct 17, 2021
2 parents 40e13f7 + 7fb41cb commit f06753b
Show file tree
Hide file tree
Showing 31 changed files with 19,252 additions and 84 deletions.
5 changes: 4 additions & 1 deletion __mocks__/useBreakpoint.mock.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export const mockUseBreakpoint = jest.fn(() => false)
jest.mock('../helpers/useBreakpoint', () => mockUseBreakpoint)
jest.mock('../helpers/useBreakpoint', () => ({
__esModule: true,
default: mockUseBreakpoint
}))
5 changes: 5 additions & 0 deletions __mocks__/useIsMac.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const mockUseIsMac = jest.fn(() => false)
jest.mock('../helpers/useIsMac', () => ({
__esModule: true,
default: mockUseIsMac
}))
4,029 changes: 3,972 additions & 57 deletions __tests__/__snapshots__/storyshots.test.js.snap

Large diffs are not rendered by default.

3,984 changes: 3,984 additions & 0 deletions __tests__/pages/admin/__snapshots__/lessons.test.js.snap

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions __tests__/pages/admin/lessons.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '../../../__mocks__/useIsMac.mock'
import '../../../__mocks__/useBreakpoint.mock'
import React from 'react'
import Lessons from '../../../pages/admin/lessons'
import dummyLessonData from '../../../__dummy__/lessonData'
Expand Down
2 changes: 2 additions & 0 deletions __tests__/pages/admin/users.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '../../../__mocks__/useIsMac.mock'
import '../../../__mocks__/useBreakpoint.mock'
import React from 'react'
import {
render,
Expand Down
498 changes: 498 additions & 0 deletions __tests__/pages/review/__snapshots__/[lesson].test.js.snap

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions components/CommentBox.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '../__mocks__/useIsMac.mock'
import '../__mocks__/useBreakpoint.mock'
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
Expand Down
2 changes: 2 additions & 0 deletions components/DiffView.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '../__mocks__/useIsMac.mock'
import '../__mocks__/useBreakpoint.mock'
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
Expand Down
2 changes: 2 additions & 0 deletions components/FormCard.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '../__mocks__/useIsMac.mock'
import '../__mocks__/useBreakpoint.mock'
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import { FormCard } from './FormCard'
Expand Down
111 changes: 111 additions & 0 deletions components/MarkdownToolbar.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import MarkdownToolbar from './MarkdownToolbar'
import { markdown } from '../helpers/textStylers'
jest.mock('../helpers/textStylers')
describe('MarkdownToolbar Component', () => {
const mockTextArea = {}
const mockRef = { current: mockTextArea }

test('Should match screenshot', () => {
const { container } = render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
expect(container).toMatchSnapshot()
})
test('Should render "cmd" based hotkey tooltips for Mac users', () => {
render(
<MarkdownToolbar isMac={true} inputRef={mockRef} onChange={() => {}} />
)
expect(screen.queryAllByLabelText(/ctrl/).length).toBe(0)
expect(screen.queryAllByLabelText(/cmd/).length).toBeGreaterThan(0)
})
test('Should render "ctrl" based hotkey tooltips for non Mac users', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
expect(screen.queryAllByLabelText(/ctrl/).length).toBeGreaterThan(0)
expect(screen.queryAllByLabelText(/cmd/).length).toBe(0)
})
test('does not call onChange if inputRef.current is null', () => {
const nullRef = { current: null }
const mockOnChange = jest.fn()
render(
<MarkdownToolbar
isMac={false}
inputRef={nullRef}
onChange={mockOnChange}
/>
)
userEvent.click(screen.getByRole('button', { name: /header/ }))
expect(mockOnChange).not.toBeCalled()
})

describe('Buttons should call markdown stylers when clicked', () => {
test('header calls -> markdown.header', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
userEvent.click(screen.getByRole('button', { name: /header/ }))
expect(markdown.header).toBeCalledWith(mockTextArea)
})

test('bold calls -> markdown.bold', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
userEvent.click(screen.getByRole('button', { name: /bold/ }))
expect(markdown.bold).toBeCalledWith(mockTextArea)
})

test('italic calls -> markdown.italic', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
userEvent.click(screen.getByRole('button', { name: /italic/ }))
expect(markdown.italic).toBeCalledWith(mockTextArea)
})

test('quote calls -> markdown.quote', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
userEvent.click(screen.getByRole('button', { name: /quote/ }))
expect(markdown.quote).toBeCalledWith(mockTextArea)
})

test('code calls -> markdown.code', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
userEvent.click(screen.getByRole('button', { name: /code/ }))
expect(markdown.code).toBeCalledWith(mockTextArea)
})

test('link calls -> markdown.link', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
userEvent.click(screen.getByRole('button', { name: /link/ }))
expect(markdown.link).toBeCalledWith(mockTextArea)
})

test('bulletList calls -> markdown.bulletList', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
userEvent.click(screen.getByRole('button', { name: /bulleted list/ }))
expect(markdown.bulletList).toBeCalledWith(mockTextArea)
})

test('orderedList calls -> markdown.orderedList', () => {
render(
<MarkdownToolbar isMac={false} inputRef={mockRef} onChange={() => {}} />
)
userEvent.click(screen.getByRole('button', { name: /numbered list/ }))
expect(markdown.orderedList).toBeCalledWith(mockTextArea)
})
})
})
134 changes: 134 additions & 0 deletions components/MarkdownToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React from 'react'
import {
HeadingIcon,
BoldIcon,
ItalicIcon,
QuoteIcon,
CodeIcon,
LinkIcon,
ListUnorderedIcon,
ListOrderedIcon,
Icon
} from '@primer/octicons-react'
import { ButtonToolbar, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { markdown, TextAreaState } from '../helpers/textStylers'

type ButtonProps = {
tooltipTitle: string
hotkey?: string
macHotkey?: string
handleMarkdown: (i: HTMLTextAreaElement) => TextAreaState
Icon: Icon
}

// The hotkeys are registered in the MdInput.tsx files hotkeyMap
// Any changes to the hotkeys here will need to match those hotkeys
const buttons: ButtonProps[] = [
{
tooltipTitle: 'Add header text',
handleMarkdown: markdown.header,
Icon: HeadingIcon
},
{
tooltipTitle: 'Add bold text',
hotkey: 'ctrl+b',
macHotkey: 'cmd+b',
handleMarkdown: markdown.bold,
Icon: BoldIcon
},
{
tooltipTitle: 'Add italic text',
hotkey: 'ctrl+i',
macHotkey: 'cmd+i',
handleMarkdown: markdown.italic,
Icon: ItalicIcon
},
{
tooltipTitle: 'Insert a quote',
hotkey: 'ctrl+shift+.',
macHotkey: 'cmd+shift+.',
handleMarkdown: markdown.quote,
Icon: QuoteIcon
},
{
tooltipTitle: 'Insert code',
hotkey: 'ctrl+e',
macHotkey: 'cmd+e',
handleMarkdown: markdown.code,
Icon: CodeIcon
},
{
tooltipTitle: 'Add a link',
hotkey: 'ctrl+k',
macHotkey: 'cmd+k',
handleMarkdown: markdown.link,
Icon: LinkIcon
},
{
tooltipTitle: 'Add a bulleted list',
hotkey: 'ctrl+shift+8',
macHotkey: 'cmd+shift+8',
handleMarkdown: markdown.bulletList,
Icon: ListUnorderedIcon
},
{
tooltipTitle: 'Add a numbered list',
hotkey: 'ctrl+shift+7',
macHotkey: 'cmd+shift+7',
handleMarkdown: markdown.orderedList,
Icon: ListOrderedIcon
}
]

type Props = {
isMac: boolean
className?: string
inputRef: React.RefObject<HTMLTextAreaElement>
onChange: (i: TextAreaState) => void
}

const MarkdownToolbar: React.FC<Props> = ({
isMac,
inputRef,
onChange,
className = ''
}) => {
return (
<ButtonToolbar className={className}>
{buttons.map(
({ tooltipTitle, handleMarkdown, Icon, macHotkey, hotkey }, i) => {
const hotkeyString = isMac ? macHotkey : hotkey
const tooltipWithHotkey =
tooltipTitle + (hotkeyString ? ` <${hotkeyString}>` : '')
return (
<OverlayTrigger
key={i}
placement="bottom-end"
delay={{ show: 200, hide: 100 }}
overlay={<Tooltip id="btn-tooltip">{tooltipWithHotkey}</Tooltip>}
>
{({ ref, ...triggerHandler }) => {
return (
<button
className="btn"
{...triggerHandler}
ref={ref}
aria-label={tooltipWithHotkey}
onClick={() => {
inputRef.current &&
onChange(handleMarkdown(inputRef.current))
}}
>
<Icon size={'small'} />
</button>
)
}}
</OverlayTrigger>
)
}
)}
</ButtonToolbar>
)
}

export default MarkdownToolbar
25 changes: 17 additions & 8 deletions components/MdInput.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { mockUseIsMac } from '../__mocks__/useIsMac.mock'
import '../__mocks__/useBreakpoint.mock'
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
Expand Down Expand Up @@ -69,16 +71,10 @@ describe('MdInput Component', () => {
})
test('Should automatically resize to fit content', () => {
render(<TestComponent />)
const textbox = screen.getByRole('textbox')
expect(textbox).not.toHaveStyle('height: 2px')
userEvent.type(
textbox,
'Hello,{enter}{enter}{enter}{enter}{enter}{enter}Tom!'
)

// autoSize function runs on first render with useEffect.
// Only picks up '+2px' additional padding as JSDOM doesnt properly mock styles
// but this proves the code was exercised and is good enough for unit test
expect(textbox).toHaveStyle('height: 2px')
expect(screen.getByRole('textbox')).toHaveStyle('height: 2px')
})
test('Show not auto resize after user sets height', () => {
render(<TestComponent />)
Expand Down Expand Up @@ -177,4 +173,17 @@ describe('MdInput Component', () => {
userEvent.type(textbox, '{ctrl}y')
expect(textbox).toHaveValue('Reset')
})
test('Should exercise handleMarkdown and insert bold markdown style ', () => {
render(<TestComponent />)
const textbox = screen.getByRole('textbox')
userEvent.type(textbox, '{ctrl}b')
expect(textbox).toHaveValue('****')
})
test('Should exercise handleMarkdown and insert bold markdown style with mac hotkey ', () => {
mockUseIsMac.mockImplementationOnce(() => true)
render(<TestComponent />)
const textbox = screen.getByRole('textbox')
userEvent.type(textbox, '{meta}b')
expect(textbox).toHaveValue('****')
})
})
Loading

1 comment on commit f06753b

@vercel
Copy link

@vercel vercel bot commented on f06753b Oct 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.