Skip to content

Commit

Permalink
FileMenu: rewrite using hooks, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
warpdesign committed Dec 8, 2022
1 parent 0a0f645 commit 6a1a28d
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 80 deletions.
118 changes: 45 additions & 73 deletions src/components/FileMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,53 @@
import * as React from 'react'
import { Menu, MenuItem, MenuDivider } from '@blueprintjs/core'
import { observer, inject } from 'mobx-react'
import { withTranslation, WithTranslation } from 'react-i18next'
import { observer } from 'mobx-react'
import { useTranslation } from 'react-i18next'

import { AppState } from '$src/state/appState'
import { File } from '$src/services/Fs'
import { useStores } from '$src/hooks/useStores'

interface FileMenuProps extends WithTranslation {
interface FileMenuProps {
onFileAction: (action: string) => void
selectedItems: File[]
selectedItemsLength: number
isDisabled: boolean
}

interface InjectedProps extends FileMenuProps {
appState: AppState
}

export const FileMenuClass = inject('appState')(
observer(
class FileMenuClass extends React.Component<FileMenuProps> {
constructor(props: FileMenuProps) {
super(props)
}

private get injected(): InjectedProps {
return this.props as InjectedProps
}

private onNewfolder = (): void => {
this.props.onFileAction('makedir')
}

private onPaste = (): void => {
this.props.onFileAction('paste')
}

private onDelete = (): void => {
this.props.onFileAction('delete')
}

public render(): React.ReactNode {
const { appState } = this.injected
const clipboardLength = appState.clipboard.files.length
const { selectedItems, t, isDisabled } = this.props

return (
<React.Fragment>
<Menu>
<MenuItem
disabled={isDisabled}
text={t('COMMON.MAKEDIR')}
icon="folder-new"
onClick={this.onNewfolder}
/>
<MenuDivider />
<MenuItem
text={t('FILEMENU.PASTE', { count: clipboardLength })}
icon="duplicate"
onClick={this.onPaste}
disabled={!clipboardLength || isDisabled}
/>
<MenuDivider />
<MenuItem
text={t('FILEMENU.DELETE', { count: selectedItems.length })}
onClick={this.onDelete}
intent={(selectedItems.length && 'danger') || 'none'}
icon="delete"
disabled={!selectedItems.length || isDisabled}
/>
</Menu>
</React.Fragment>
)
}
},
),
)

const FileMenu = withTranslation()(FileMenuClass)

export { FileMenu }
export const FileMenu = observer(({ onFileAction, selectedItemsLength, isDisabled }: FileMenuProps) => {
const onNewfolder = (): void => {
onFileAction('makedir')
}

const onPaste = (): void => {
onFileAction('paste')
}

const onDelete = (): void => {
onFileAction('delete')
}

const { t } = useTranslation()
const { appState } = useStores('appState')
const clipboardLength = appState.clipboard.files.length

return (
<>
<Menu>
<MenuItem disabled={isDisabled} text={t('COMMON.MAKEDIR')} icon="folder-new" onClick={onNewfolder} />
<MenuDivider />
<MenuItem
text={t('FILEMENU.PASTE', { count: clipboardLength })}
icon="duplicate"
onClick={onPaste}
disabled={!clipboardLength || isDisabled}
/>
<MenuDivider />
<MenuItem
text={t('FILEMENU.DELETE', { count: selectedItemsLength })}
onClick={onDelete}
intent={(selectedItemsLength && 'danger') || 'none'}
icon="delete"
disabled={!selectedItemsLength || isDisabled}
/>
</Menu>
</>
)
})
2 changes: 1 addition & 1 deletion src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ export const ToolbarClass = inject(
content={
<FileMenu
isDisabled={!fileCache || fileCache.error}
selectedItems={selected}
selectedItemsLength={selected.length}
onFileAction={this.onFileAction}
/>
}
Expand Down
107 changes: 107 additions & 0 deletions src/components/__tests__/FileMenu.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* @jest-environment jsdom
*/
import { AppState } from '$src/state/appState'
import React from 'react'
import { screen, render, setup, t } from 'rtl'
import { FileMenu } from '../FileMenu'

describe('FileMenu', () => {
const appState = {
clipboard: {
files: [''],
},
}

const OPTIONS = {
providerProps: {
appState: appState as unknown as AppState,
},
}

const PROPS = {
selectedItemsLength: 3,
isDisabled: false,
onFileAction: jest.fn(),
}

beforeEach(() => jest.clearAllMocks())

const items = [
{
label: t('COMMON.MAKEDIR'),
action: 'makedir',
},
{
label: t('FILEMENU.PASTE', { count: appState.clipboard.files.length }),
action: 'paste',
},
{
label: t('FILEMENU.DELETE', { count: PROPS.selectedItemsLength }),
action: 'delete',
},
]

const getMenuItem = (label: string) => screen.getByRole('menuitem', { name: label })

it('should display FileMenu', () => {
render(<FileMenu {...PROPS} />, OPTIONS)
items.forEach(({ label }) => {
expect(getMenuItem(label)).toBeInTheDocument()
})
})

it('should call item action when clicking on menu item', async () => {
const { user } = setup(<FileMenu {...PROPS} />, OPTIONS)
items.forEach(async ({ label, action }) => {
await user.click(getMenuItem(label))
expect(PROPS.onFileAction).toHaveBeenCalledWith(action)
})
})

it('should disable menu items when isDisabled prop is true', () => {
const props = { ...PROPS, isDisabled: true }

const { user } = setup(<FileMenu {...props} />, OPTIONS)

items.forEach(async ({ label, action }) => {
await user.click(getMenuItem(label))
expect(PROPS.onFileAction).not.toHaveBeenCalledWith(action)
})
})

it.only('should disable individual menu items when disable condition is met', () => {
const props = { ...PROPS, selectedItemsLength: 0 }
const options = {
providerProps: {
appState: {
clipboard: {
files: [],
},
} as unknown as AppState,
},
}

const items = [
{
label: t('COMMON.MAKEDIR'),
action: 'makedir',
},
{
label: t('FILEMENU.PASTE', { count: 0 }),
action: 'paste',
},
{
label: t('FILEMENU.DELETE', { count: 0 }),
action: 'delete',
},
]

const { user } = setup(<FileMenu {...props} />, options)

items.forEach(async ({ label, action }) => {
await user.click(getMenuItem(label))
expect(PROPS.onFileAction).not.toHaveBeenCalledWith(action)
})
})
})
18 changes: 12 additions & 6 deletions src/utils/test/rtl.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import type { ReactElement } from 'react'
import { render, screen, configure, waitForElementToBeRemoved } from '@testing-library/react'
import { render, screen, configure, waitForElementToBeRemoved, RenderOptions } from '@testing-library/react'
import { within } from '@testing-library/dom'
import type { MatcherFunction } from '@testing-library/react'
import { Provider } from 'mobx-react'
Expand Down Expand Up @@ -58,14 +58,18 @@ jest.mock('$src/utils/throttle', () => ({
throttle: (fn: any) => fn,
}))
import { SettingsState } from '$src/state/settingsState'
import { AppState } from '$src/state/appState'

type Query = (f: MatcherFunction) => HTMLElement

const LOCALE_EN = en.translations
interface ProvidersAndRenderOptions extends RenderOptions {
providerProps?: {
settingsState?: SettingsState
appState?: AppState
}
}

const customRender = (
ui: ReactElement,
{ providerProps = { settingsState: new SettingsState('2.31') }, ...renderOptions } = {},
{ providerProps = { settingsState: new SettingsState('2.31') }, ...renderOptions }: ProvidersAndRenderOptions = {},
) =>
render(
<DndProvider backend={HTML5Backend}>
Expand All @@ -75,9 +79,10 @@ const customRender = (
</I18nextProvider>
</Provider>
</DndProvider>,
renderOptions,
)

function withMarkup(query: Query) {
function withMarkup(query: (f: MatcherFunction) => HTMLElement) {
return (text: string | RegExp) =>
query((content: string, node: Element | null) => {
const didMatch = (node: Element) => {
Expand Down Expand Up @@ -117,6 +122,7 @@ const setup = (jsx: ReactElement, options = {}) => {
}

const t = i18n.i18next.t
const LOCALE_EN = en.translations

configure({
testIdAttribute: 'id',
Expand Down

0 comments on commit 6a1a28d

Please sign in to comment.