Files
gongxue-base/apps/admin/src/components/EditableCell/editable-cell.integration.test.ts

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();
});
});