Skip to content

Commit 006066c

Browse files
committed
chore: add tests
1 parent 6adc025 commit 006066c

File tree

1 file changed

+255
-0
lines changed

1 file changed

+255
-0
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
2+
3+
import { resolvePointerPositionHit } from '../input/PositionHitResolver.js';
4+
5+
const { mockTextSelectionCreate, mockNodeSelectionCreate } = vi.hoisted(() => ({
6+
mockTextSelectionCreate: vi.fn(),
7+
mockNodeSelectionCreate: vi.fn(),
8+
}));
9+
10+
vi.mock('../input/PositionHitResolver.js', () => ({
11+
resolvePointerPositionHit: vi.fn(() => ({
12+
pos: 12,
13+
layoutEpoch: 1,
14+
pageIndex: 0,
15+
blockId: 'body-1',
16+
column: 0,
17+
lineIndex: 0,
18+
})),
19+
}));
20+
21+
vi.mock('@superdoc/layout-bridge', () => ({
22+
getFragmentAtPosition: vi.fn(() => null),
23+
}));
24+
25+
vi.mock('prosemirror-state', async (importOriginal) => {
26+
const original = await importOriginal<typeof import('prosemirror-state')>();
27+
return {
28+
...original,
29+
TextSelection: {
30+
...original.TextSelection,
31+
create: mockTextSelectionCreate,
32+
},
33+
NodeSelection: {
34+
...original.NodeSelection,
35+
create: mockNodeSelectionCreate,
36+
},
37+
Selection: {
38+
...original.Selection,
39+
near: vi.fn(() => ({
40+
empty: true,
41+
$from: { parent: { inlineContent: true } },
42+
})),
43+
},
44+
};
45+
});
46+
47+
function getPointerEventImpl(): typeof PointerEvent | typeof MouseEvent {
48+
return (
49+
(globalThis as unknown as { PointerEvent?: typeof PointerEvent; MouseEvent: typeof MouseEvent }).PointerEvent ??
50+
globalThis.MouseEvent
51+
);
52+
}
53+
54+
function createMockDoc(mode: 'tableInSdt' | 'plainSdt') {
55+
return {
56+
content: { size: 200 },
57+
nodeAt: vi.fn(() => ({ nodeSize: 20 })),
58+
resolve: vi.fn((_pos: number) => {
59+
if (mode === 'tableInSdt') {
60+
return {
61+
depth: 2,
62+
node: (depth: number) => {
63+
if (depth === 2) return { type: { name: 'table' } };
64+
if (depth === 1) return { type: { name: 'structuredContentBlock' } };
65+
return { type: { name: 'doc' } };
66+
},
67+
before: (depth: number) => (depth === 1 ? 10 : 11),
68+
start: (depth: number) => (depth === 1 ? 11 : 12),
69+
end: (depth: number) => (depth === 1 ? 30 : 29),
70+
};
71+
}
72+
return {
73+
depth: 1,
74+
node: (depth: number) => {
75+
if (depth === 1) return { type: { name: 'structuredContentBlock' } };
76+
return { type: { name: 'doc' } };
77+
},
78+
before: (_depth: number) => 10,
79+
start: (_depth: number) => 11,
80+
end: (_depth: number) => 30,
81+
};
82+
}),
83+
nodesBetween: vi.fn((_from: number, _to: number, cb: (node: unknown, pos: number) => void) => {
84+
cb({ isTextblock: true }, 0);
85+
}),
86+
};
87+
}
88+
89+
describe('EditorInputManager structuredContentBlock table exception', () => {
90+
let EditorInputManagerClass:
91+
| (new () => {
92+
setDependencies: (deps: unknown) => void;
93+
setCallbacks: (callbacks: unknown) => void;
94+
bind: () => void;
95+
destroy: () => void;
96+
})
97+
| null = null;
98+
let manager: InstanceType<NonNullable<typeof EditorInputManagerClass>>;
99+
let viewportHost: HTMLElement;
100+
let visibleHost: HTMLElement;
101+
let mountRoot: HTMLElement;
102+
let mockEditor: {
103+
isEditable: boolean;
104+
state: {
105+
doc: ReturnType<typeof createMockDoc>;
106+
tr: { setSelection: Mock; setStoredMarks: Mock };
107+
selection: { $anchor: null };
108+
storedMarks: null;
109+
};
110+
view: {
111+
dispatch: Mock;
112+
dom: HTMLElement;
113+
focus: Mock;
114+
hasFocus: Mock;
115+
};
116+
on: Mock;
117+
off: Mock;
118+
emit: Mock;
119+
};
120+
let mockHitTestTable: Mock;
121+
122+
function mountWithDoc(mode: 'tableInSdt' | 'plainSdt') {
123+
mockEditor.state.doc = createMockDoc(mode);
124+
}
125+
126+
beforeEach(async () => {
127+
mockTextSelectionCreate.mockReset();
128+
mockNodeSelectionCreate.mockReset();
129+
mockTextSelectionCreate.mockReturnValue({
130+
empty: true,
131+
$from: { parent: { inlineContent: true } },
132+
});
133+
mockNodeSelectionCreate.mockReturnValue({
134+
empty: false,
135+
});
136+
137+
viewportHost = document.createElement('div');
138+
visibleHost = document.createElement('div');
139+
visibleHost.appendChild(viewportHost);
140+
mountRoot = document.createElement('div');
141+
mountRoot.appendChild(visibleHost);
142+
document.body.appendChild(mountRoot);
143+
144+
mockEditor = {
145+
isEditable: true,
146+
state: {
147+
doc: createMockDoc('plainSdt'),
148+
tr: {
149+
setSelection: vi.fn().mockReturnThis(),
150+
setStoredMarks: vi.fn().mockReturnThis(),
151+
},
152+
selection: { $anchor: null },
153+
storedMarks: null,
154+
},
155+
view: {
156+
dispatch: vi.fn(),
157+
dom: document.createElement('div'),
158+
focus: vi.fn(),
159+
hasFocus: vi.fn(() => false),
160+
},
161+
on: vi.fn(),
162+
off: vi.fn(),
163+
emit: vi.fn(),
164+
};
165+
166+
if (!EditorInputManagerClass) {
167+
const mod = await import('../pointer-events/EditorInputManager.js');
168+
EditorInputManagerClass = mod.EditorInputManager as typeof EditorInputManagerClass;
169+
}
170+
171+
manager = new EditorInputManagerClass!();
172+
manager.setDependencies({
173+
getActiveEditor: vi.fn(() => mockEditor),
174+
getEditor: vi.fn(() => mockEditor),
175+
getLayoutState: vi.fn(() => ({ layout: {} as any, blocks: [], measures: [] })),
176+
getEpochMapper: vi.fn(() => ({
177+
mapPosFromLayoutToCurrentDetailed: vi.fn(() => ({ ok: true, pos: 12, toEpoch: 1 })),
178+
})),
179+
getViewportHost: vi.fn(() => viewportHost),
180+
getVisibleHost: vi.fn(() => visibleHost),
181+
getLayoutMode: vi.fn(() => 'vertical'),
182+
getHeaderFooterSession: vi.fn(() => null),
183+
getPageGeometryHelper: vi.fn(() => null),
184+
getZoom: vi.fn(() => 1),
185+
isViewLocked: vi.fn(() => false),
186+
getDocumentMode: vi.fn(() => 'editing'),
187+
getPageElement: vi.fn(() => null),
188+
isSelectionAwareVirtualizationEnabled: vi.fn(() => false),
189+
});
190+
manager.setCallbacks({
191+
normalizeClientPoint: vi.fn((clientX: number, clientY: number) => ({ x: clientX, y: clientY })),
192+
scheduleSelectionUpdate: vi.fn(),
193+
updateSelectionDebugHud: vi.fn(),
194+
hitTestTable: (mockHitTestTable = vi.fn(() => null)),
195+
});
196+
manager.bind();
197+
});
198+
199+
afterEach(() => {
200+
manager.destroy();
201+
mountRoot.remove();
202+
vi.clearAllMocks();
203+
});
204+
205+
it('uses TextSelection when click lands inside table within structuredContentBlock', () => {
206+
mountWithDoc('tableInSdt');
207+
mockHitTestTable.mockReturnValue({
208+
block: { id: 'table-1' },
209+
cellRowIndex: 0,
210+
cellColIndex: 0,
211+
});
212+
const tableFragment = document.createElement('div');
213+
tableFragment.className = 'superdoc-table-fragment';
214+
const target = document.createElement('span');
215+
tableFragment.appendChild(target);
216+
viewportHost.appendChild(tableFragment);
217+
218+
const PointerEventImpl = getPointerEventImpl();
219+
target.dispatchEvent(
220+
new PointerEventImpl('pointerdown', {
221+
bubbles: true,
222+
cancelable: true,
223+
button: 0,
224+
buttons: 1,
225+
clientX: 20,
226+
clientY: 20,
227+
} as PointerEventInit),
228+
);
229+
230+
expect(resolvePointerPositionHit as unknown as Mock).toHaveBeenCalled();
231+
expect(mockTextSelectionCreate).toHaveBeenCalled();
232+
expect(mockNodeSelectionCreate).not.toHaveBeenCalled();
233+
});
234+
235+
it('uses NodeSelection for plain structuredContentBlock click (non-table)', () => {
236+
mountWithDoc('plainSdt');
237+
const target = document.createElement('span');
238+
viewportHost.appendChild(target);
239+
240+
const PointerEventImpl = getPointerEventImpl();
241+
target.dispatchEvent(
242+
new PointerEventImpl('pointerdown', {
243+
bubbles: true,
244+
cancelable: true,
245+
button: 0,
246+
buttons: 1,
247+
clientX: 24,
248+
clientY: 24,
249+
} as PointerEventInit),
250+
);
251+
252+
expect(resolvePointerPositionHit as unknown as Mock).toHaveBeenCalled();
253+
expect(mockNodeSelectionCreate).toHaveBeenCalled();
254+
});
255+
});

0 commit comments

Comments
 (0)