Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/markdown toolbar #1013

Merged
merged 7 commits into from
Oct 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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