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