diff --git a/.dumi/tsconfig.json b/.dumi/tsconfig.json new file mode 100644 index 0000000..a32dd4f --- /dev/null +++ b/.dumi/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["**/*"] +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a158a38..42f6225 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,14 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Set up Cypress binary cache + uses: actions/cache@v3 + with: + path: ~/.cache/Cypress + key: ${{ runner.os }}-cypress-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-cypress- + - name: Install deps run: pnpm install diff --git a/packages/antd-record-hotkey-input/src/__tests__/__snapshots__/action-icon.spec.tsx.snap b/packages/antd-record-hotkey-input/src/__tests__/__snapshots__/action-icon.spec.tsx.snap new file mode 100644 index 0000000..c71023a --- /dev/null +++ b/packages/antd-record-hotkey-input/src/__tests__/__snapshots__/action-icon.spec.tsx.snap @@ -0,0 +1,19 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ActionIcon > should render correctly 1`] = ` +
+ +
+`; diff --git a/packages/antd-record-hotkey-input/src/__tests__/__snapshots__/input.spec.tsx.snap b/packages/antd-record-hotkey-input/src/__tests__/__snapshots__/input.spec.tsx.snap new file mode 100644 index 0000000..31f278e --- /dev/null +++ b/packages/antd-record-hotkey-input/src/__tests__/__snapshots__/input.spec.tsx.snap @@ -0,0 +1,50 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RecordShortcutInput > should render correctly 1`] = ` +
+ + + + + + +
+`; diff --git a/packages/antd-record-hotkey-input/src/__tests__/action-icon.spec.tsx b/packages/antd-record-hotkey-input/src/__tests__/action-icon.spec.tsx new file mode 100644 index 0000000..7d06991 --- /dev/null +++ b/packages/antd-record-hotkey-input/src/__tests__/action-icon.spec.tsx @@ -0,0 +1,78 @@ +import { fireEvent, render } from '@test/utils'; +import { ConfigProvider } from 'antd'; +import ActionIcon from '../ActionIcon'; + +const Icon = () => o; + +describe('ActionIcon', () => { + it('should render correctly', () => { + const { container } = render(} />); + expect(container).toMatchSnapshot(); + }); + + it('should render with className', () => { + const { getByRole } = render(} className="test" />); + expect(getByRole('button')).toHaveClass('test'); + }); + + it('should render with onClick', () => { + const onClick = vi.fn(); + const { getByRole } = render(} onClick={onClick} />); + fireEvent.click(getByRole('button')); + expect(onClick).toBeCalled(); + }); + + describe('disabled', () => { + it('should render with disabled', () => { + const onClick = vi.fn(); + const { getByRole } = render(} disabled onClick={onClick} />); + expect(getByRole('button')).toBeDisabled(); + fireEvent.click(getByRole('button')); + expect(onClick).not.toBeCalled(); + }); + + it('should render with disabled by antd ConfigProvider', () => { + const { getByRole } = render( + + } />, + , + ); + expect(getByRole('button')).toBeDisabled(); + }); + }); + + it('should render with style', () => { + const { getByRole } = render(} style={{ color: 'red' }} />); + expect(getComputedStyle(getByRole('button')).color).toBe('rgb(255, 0, 0)'); + }); + + describe('size', () => { + it('should render', () => { + const { getByRole } = render(} size="large" />); + expect(getByRole('button')).toHaveClass('ant-btn-lg'); + expect(getComputedStyle(getByRole('button')).maxHeight).toBeFalsy(); + }); + + it.each([void 0, 'small', 'middle'])('should render with size %s', (size: any) => { + const { getByRole } = render(} size={size} />); + expect(getComputedStyle(getByRole('button')).maxHeight).toBeTruthy(); + }); + + it('overwrites max-height', () => { + const { getByRole } = render( + } size="small" style={{ maxHeight: 100 }} />, + ); + expect(getComputedStyle(getByRole('button')).maxHeight).toBe('100px'); + }); + + it('should render with size by antd ConfigProvider', () => { + const { getByRole } = render( + + } />, + , + ); + expect(getByRole('button')).toHaveClass('ant-btn-sm'); + expect(getComputedStyle(getByRole('button')).maxHeight).toBeTruthy(); + }); + }); +}); diff --git a/packages/antd-record-hotkey-input/src/__tests__/input.spec.tsx b/packages/antd-record-hotkey-input/src/__tests__/input.spec.tsx new file mode 100644 index 0000000..39387f4 --- /dev/null +++ b/packages/antd-record-hotkey-input/src/__tests__/input.spec.tsx @@ -0,0 +1,137 @@ +import { fireEvent, render, waitFakeTimer } from '@test/utils'; +import { ConfigProvider } from 'antd'; +import en_US from 'antd/locale/en_US'; +import zh_CN from 'antd/locale/zh_CN'; +import RecordShortcutInput from '../RecordShortcutInput'; + +// 抽离一个录制快捷键的测试用例 +async function recordShortcut(input: HTMLElement, container: HTMLElement) { + fireEvent.dblClick(input); // 双击输入框进入编辑模式 + + await waitFakeTimer(); + + expect(container.querySelector('.ant-record-hotkey-input-recording')).toBeInTheDocument(); + + // 按下 shift + a + fireEvent.keyDown(input, { keyCode: 16, shiftKey: true, code: 'ShiftLeft' }); + fireEvent.keyDown(input, { keyCode: 65, shiftKey: true, code: 'KeyA' }); + + // 按下回车结束录制 + fireEvent.keyDown(input, { keyCode: 13, code: 'Enter' }); + + expect(input).toHaveValue('Shift + A'); +} + +describe('RecordShortcutInput', () => { + it('should render correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('正常录制快捷键', async () => { + const { getByRole, container } = render(); + + const input = getByRole('textbox'); + + expect(input).toBeInTheDocument(); + + await recordShortcut(input, container); + }); + + it('should clear value when click clear icon', async () => { + const { getByRole, getAllByRole, container } = render(); + const input = getByRole('textbox'); + + await recordShortcut(input, container); + + expect(getAllByRole('button')).toHaveLength(2); + + // 点击清除按钮 + fireEvent.click(getAllByRole('button').at(-1)!); + expect(input).toHaveValue(''); + }); + + it('should not record when disabled', async () => { + const { getByRole, container } = render(); + const input = getByRole('textbox'); + + fireEvent.dblClick(input); + + await waitFakeTimer(); + + // 断言输入框未进入录制状态 + expect(container.querySelector('.ant-record-hotkey-input-recording')).not.toBeInTheDocument(); + }); + + it('should render placeholder correctly', () => { + const { getByPlaceholderText } = render(); + expect(getByPlaceholderText('请输入快捷键')).toBeInTheDocument(); + }); + + it('should render placeholder function correctly', () => { + const placeholder = vi.fn((recording) => (recording ? 'foo' : 'bar')); + const { getByRole } = render(); + + const input = getByRole('textbox'); + + expect(input).toHaveAttribute('placeholder', 'bar'); + + fireEvent.dblClick(input); + + expect(input).toHaveAttribute('placeholder', 'foo'); + }); + + describe('模拟录制一半退出', () => { + it('should stop recording when blur', async () => { + const { getByRole, container } = render(); + const input = getByRole('textbox'); + fireEvent.dblClick(input); + + await waitFakeTimer(); + + expect(container.querySelector('.ant-record-hotkey-input-recording')).toBeInTheDocument(); + fireEvent.blur(input); + fireEvent.keyDown(input, { keyCode: 16, shiftKey: true, code: 'ShiftLeft' }); + expect(container.querySelector('.ant-record-hotkey-input-recording')).not.toBeInTheDocument(); + expect(input).toHaveValue(''); + }); + + it('should stop recording when press esc', async () => { + const { getByRole, container } = render(); + const input = getByRole('textbox'); + fireEvent.dblClick(input); + + await waitFakeTimer(); + + expect(container.querySelector('.ant-record-hotkey-input-recording')).toBeInTheDocument(); + fireEvent.keyDown(input, { keyCode: 27, code: 'Escape' }); + expect(container.querySelector('.ant-record-hotkey-input-recording')).not.toBeInTheDocument(); + expect(input).toHaveValue(''); + }); + }); + + describe('i18n', () => { + it('should render correct i18n text', () => { + const { getByRole } = render(); + expect(getByRole('textbox')).toHaveAttribute('placeholder', 'Double click to edit'); + }); + + it('should render correct i18n text with ConfigProvider', () => { + const { getByRole, rerender } = render( + + + , + ); + + expect(getByRole('textbox')).toHaveAttribute('placeholder', '双击以编辑'); + + rerender( + + + , + ); + + expect(getByRole('textbox')).toHaveAttribute('placeholder', 'Double click to edit'); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index eab3ff9..b09f51a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,7 @@ "types": ["vitest/globals"], "paths": { "@@/*": [".dumi/tmp/*"], - "@test/*": ["tests"], + "@test/*": ["tests/*"], "antd-record-hotkey-input": ["packages/antd-record-hotkey-input/src"], "react-use-record-hotkey": ["packages/react-use-record-hotkey/src"] } diff --git a/vitest.config.mts b/vitest.config.mts index 91936ba..2621b70 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -10,6 +10,7 @@ export default defineConfig({ alias: { 'antd-record-hotkey-input': resolve(__dirname, './packages/antd-record-hotkey-input/src'), 'react-use-record-hotkey': resolve(__dirname, './packages/react-use-record-hotkey/src'), + "@test": resolve(__dirname, './tests'), }, coverage: { reporter: ['text', 'text-summary', 'json', 'lcov'],