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 = () => ;
+
+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'],