feat: 支持表格单元格快捷编辑
Some checks failed
CI 检查 / lint (pull_request) Has been cancelled
CI 检查 / typecheck (pull_request) Has been cancelled
CI 检查 / test (pull_request) Has been cancelled

This commit is contained in:
2026-07-21 14:17:08 +08:00
parent fe0b71a0a9
commit a585fd42d5
17 changed files with 2197 additions and 303 deletions

View File

@@ -0,0 +1,98 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import dayjs from 'dayjs';
import EditableCell, {
editableValuesEqual,
normalizeEditableValue,
serializeEditableValue,
} from './index';
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
afterEach(async () => {
if (root) {
await act(async () => root?.unmount());
}
container?.remove();
root = null;
container = null;
});
describe('editable cell value mapping', () => {
it('normalizes and serializes date values', () => {
const value = normalizeEditableValue('2026-07-21', 'date');
expect(dayjs.isDayjs(value)).toBe(true);
expect(serializeEditableValue(value, 'date')).toBe('2026-07-21');
});
it('normalizes and serializes date ranges', () => {
const value = normalizeEditableValue(['2026-07-01', '2026-07-31'], 'date-range');
expect(serializeEditableValue(value, 'date-range')).toEqual(['2026-07-01', '2026-07-31']);
});
it('converts numeric input and preserves zero', () => {
expect(serializeEditableValue(normalizeEditableValue('12.50', 'money'), 'money')).toBe(12.5);
expect(serializeEditableValue(0, 'number')).toBe(0);
});
it('normalizes empty and multi-select values', () => {
expect(serializeEditableValue('', 'number')).toBeUndefined();
expect(normalizeEditableValue(undefined, 'multi-select')).toEqual([]);
});
it('compares structured values without reference equality', () => {
expect(editableValuesEqual([1, 2], [1, 2])).toBe(true);
expect(editableValuesEqual(' a ', 'a')).toBe(false);
});
});
describe('editable cell interactions', () => {
it('saves a single-select value immediately when an option is clicked', async () => {
const onSave = vi.fn(async () => undefined);
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root?.render(
React.createElement(
EditableCell,
{
value: 'active',
editor: 'select',
options: [
{ value: 'active', label: '启用' },
{ value: 'disabled', label: '停用' },
],
onSave,
},
'启用',
),
);
});
await act(async () => {
container?.querySelector('.editable-cell')?.dispatchEvent(
new MouseEvent('dblclick', { bubbles: true }),
);
await flush();
});
const options = Array.from(document.querySelectorAll<HTMLElement>('.ant-select-item-option'));
const disabledOption = options.find((option) => option.textContent?.includes('停用'));
expect(disabledOption).toBeTruthy();
await act(async () => {
disabledOption?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await flush();
});
expect(onSave).toHaveBeenCalledOnce();
expect(onSave).toHaveBeenCalledWith('disabled');
expect(container.querySelector('.editable-cell--editing')).toBeNull();
});
});