178 lines
5.9 KiB
TypeScript
178 lines
5.9 KiB
TypeScript
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));
|
|
|
|
const dispatchPointer = (
|
|
target: Element,
|
|
type: 'pointerdown' | 'pointerup',
|
|
init: { pointerType: 'touch' | 'mouse'; pointerId?: number; clientX?: number; clientY?: number },
|
|
) => {
|
|
const event = new Event(type, { bubbles: true });
|
|
Object.defineProperties(event, {
|
|
pointerType: { value: init.pointerType },
|
|
pointerId: { value: init.pointerId ?? 1 },
|
|
clientX: { value: init.clientX ?? 0 },
|
|
clientY: { value: init.clientY ?? 0 },
|
|
});
|
|
target.dispatchEvent(event);
|
|
};
|
|
|
|
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', () => {
|
|
const renderTextCell = 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: '张三', onSave }, '张三'),
|
|
);
|
|
});
|
|
return container.querySelector('.editable-cell') as HTMLElement;
|
|
};
|
|
|
|
it('keeps a single touch tap read-only', async () => {
|
|
const cell = await renderTextCell();
|
|
|
|
await act(async () => {
|
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'touch', clientX: 10, clientY: 10 });
|
|
dispatchPointer(cell, 'pointerup', { pointerType: 'touch', clientX: 12, clientY: 11 });
|
|
await flush();
|
|
});
|
|
|
|
expect(container?.querySelector('.editable-cell--editing')).toBeNull();
|
|
});
|
|
|
|
it('enters edit mode after two nearby touch taps', async () => {
|
|
const cell = await renderTextCell();
|
|
|
|
await act(async () => {
|
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'touch', clientX: 10, clientY: 10 });
|
|
dispatchPointer(cell, 'pointerup', { pointerType: 'touch', clientX: 11, clientY: 10 });
|
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'touch', clientX: 12, clientY: 11 });
|
|
dispatchPointer(cell, 'pointerup', { pointerType: 'touch', clientX: 12, clientY: 11 });
|
|
await flush();
|
|
});
|
|
|
|
expect(container?.querySelector('.editable-cell--editing')).toBeTruthy();
|
|
});
|
|
|
|
it('keeps mouse single-click behavior read-only', async () => {
|
|
const cell = await renderTextCell();
|
|
|
|
await act(async () => {
|
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'mouse' });
|
|
dispatchPointer(cell, 'pointerup', { pointerType: 'mouse' });
|
|
await flush();
|
|
});
|
|
|
|
expect(container?.querySelector('.editable-cell--editing')).toBeNull();
|
|
});
|
|
|
|
it('does not edit when a touch gesture scrolls the table', async () => {
|
|
const cell = await renderTextCell();
|
|
|
|
await act(async () => {
|
|
dispatchPointer(cell, 'pointerdown', { pointerType: 'touch', clientX: 10, clientY: 10 });
|
|
dispatchPointer(cell, 'pointerup', { pointerType: 'touch', clientX: 30, clientY: 10 });
|
|
await flush();
|
|
});
|
|
|
|
expect(container?.querySelector('.editable-cell--editing')).toBeNull();
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|