Skip to content

Commit 846d4aa

Browse files
committed
chore: add behavior tests
1 parent d676a68 commit 846d4aa

23 files changed

Lines changed: 1400 additions & 94 deletions

packages/superdoc/src/SuperDoc.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,22 @@ const HrbrFieldsLayerStub = stubComponent('HrbrFieldsLayer');
104104
const AiLayerStub = stubComponent('AiLayer');
105105
const HtmlViewerStub = stubComponent('HtmlViewer');
106106

107+
const createTrackedChangeIndexStub = () => ({
108+
subscribe: vi.fn(() => () => {}),
109+
getAll: vi.fn(() => []),
110+
get: vi.fn(() => []),
111+
invalidate: vi.fn(),
112+
invalidateAll: vi.fn(),
113+
dispose: vi.fn(),
114+
});
115+
116+
const getTrackedChangeIndexMock = vi.fn(() => createTrackedChangeIndexStub());
117+
107118
// Mock @superdoc/super-editor with stubs and PresentationEditor class
108119
vi.mock('@superdoc/super-editor', () => ({
109120
SuperEditor: SuperEditorStub,
110121
AIWriter: AIWriterStub,
122+
getTrackedChangeIndex: getTrackedChangeIndexMock,
111123
PresentationEditor: class PresentationEditorMock {
112124
static getInstance(documentId) {
113125
return mockState.instances.get(documentId);
@@ -387,6 +399,8 @@ describe('SuperDoc.vue', () => {
387399
useSelectionMock.mockClear();
388400
useAiMock.mockClear();
389401
useSelectedTextMock.mockClear();
402+
getTrackedChangeIndexMock.mockClear();
403+
getTrackedChangeIndexMock.mockImplementation(() => createTrackedChangeIndexStub());
390404
mockState.instances.clear();
391405

392406
// Make RAF synchronous in tests — jsdom has no rendering loop, and
@@ -1285,6 +1299,7 @@ describe('SuperDoc.vue', () => {
12851299
expect(doc.setPresentationEditor).toHaveBeenCalledWith(presentationEditor);
12861300
expect(presentationEditor.setContextMenuDisabled).toHaveBeenCalledWith(true);
12871301
expect(presentationEditor.on).toHaveBeenCalledWith('commentPositions', expect.any(Function));
1302+
expect(getTrackedChangeIndexMock).toHaveBeenCalledWith(editor);
12881303
});
12891304

12901305
it('forwards header/footer presentation events through the public update callbacks', async () => {

packages/superdoc/src/components/CommentsLayer/commentsList/super-comments-list.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export class SuperComments extends EventEmitter {
2929

3030
createVueApp() {
3131
this.app = createApp(CommentsList);
32+
if (this.superdoc?.pinia) {
33+
this.app.use(this.superdoc.pinia);
34+
}
3235
this.app.directive('click-outside', vClickOutside);
3336
this.app.config.globalProperties.$superdoc = this.superdoc;
3437

packages/superdoc/src/components/CommentsLayer/commentsList/super-comments-list.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ describe('SuperComments', () => {
4444
expect(instance.app.config.globalProperties.$superdoc).toBe(superdocStub);
4545
});
4646

47+
it('reuses the parent SuperDoc pinia instance when available', () => {
48+
const pinia = { id: 'shared-pinia' };
49+
const instance = new SuperComments({ element }, { ...superdocStub, pinia });
50+
expect(instance.app._context.plugins).toContain(pinia);
51+
});
52+
4753
it('resolves element via selector when no element is provided', () => {
4854
const el = document.createElement('div');
4955
el.id = 'my-comments-host';

tests/behavior/harness/index.html

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,48 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>SuperDoc Behavior Test Harness</title>
7+
<style>
8+
body {
9+
margin: 0;
10+
font-family: Arial, Helvetica, sans-serif;
11+
}
12+
13+
#harness-root {
14+
display: flex;
15+
min-height: 100vh;
16+
align-items: flex-start;
17+
}
18+
19+
#harness-main {
20+
flex: 1 1 auto;
21+
min-width: 0;
22+
}
23+
24+
#comments-panel {
25+
display: none;
26+
width: 360px;
27+
min-height: 100vh;
28+
box-sizing: border-box;
29+
overflow: auto;
30+
padding: 12px;
31+
border-left: 1px solid #d8d8d8;
32+
background: #fafafa;
33+
}
34+
35+
#comments-panel.is-visible {
36+
display: block;
37+
}
38+
</style>
739
</head>
840
<body>
9-
<input type="file" accept=".docx" />
10-
<div id="toolbar"></div>
11-
<div id="editor"></div>
41+
<div id="harness-root">
42+
<div id="harness-main">
43+
<input type="file" accept=".docx" />
44+
<div id="toolbar"></div>
45+
<div id="editor"></div>
46+
</div>
47+
<div id="comments-panel"></div>
48+
</div>
1249
<script type="module" src="./main.ts"></script>
1350
</body>
1451
</html>

tests/behavior/harness/main.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,43 @@ type SuperDocConfig = ConstructorParameters<typeof SuperDoc>[0];
55
type SuperDocInstance = InstanceType<typeof SuperDoc>;
66
type SuperDocReadyPayload = Parameters<NonNullable<SuperDocConfig['onReady']>>[0];
77
type OverrideType = 'markdown' | 'html' | 'text';
8+
type StoryLocator =
9+
| { kind: 'story'; storyType: 'body' }
10+
| { kind: 'story'; storyType: 'headerFooterPart'; refId: string }
11+
| { kind: 'story'; storyType: 'footnote' | 'endnote'; noteId: string };
812
type ContentOverrideInput = {
913
contentOverride?: string;
1014
overrideType?: OverrideType;
1115
};
16+
type BehaviorHarnessCommentSnapshot = {
17+
commentId?: string;
18+
importedId?: string;
19+
trackedChange?: boolean;
20+
trackedChangeText?: string | null;
21+
trackedChangeType?: string | null;
22+
trackedChangeDisplayType?: string | null;
23+
trackedChangeStory?: StoryLocator | null;
24+
trackedChangeStoryKind?: string | null;
25+
trackedChangeStoryLabel?: string;
26+
trackedChangeAnchorKey?: string | null;
27+
deletedText?: string | null;
28+
resolvedTime?: number | null;
29+
};
30+
type BehaviorHarnessApi = {
31+
getActiveStorySession: () => StoryLocator | null;
32+
getActiveStoryText: () => string | null;
33+
getBodyStoryText: () => string | null;
34+
getCommentsSnapshot: () => BehaviorHarnessCommentSnapshot[];
35+
getEditorCommentPositions: () => Record<string, unknown>;
36+
getActiveCommentId: () => string | null;
37+
};
1238

1339
type HarnessWindow = Window &
1440
typeof globalThis & {
1541
superdocReady?: boolean;
1642
superdoc?: SuperDocInstance;
1743
editor?: unknown;
44+
behaviorHarness?: BehaviorHarnessApi;
1845
behaviorHarnessInit?: (input?: ContentOverrideInput) => void;
1946
};
2047

@@ -42,6 +69,63 @@ if (!showCaret) {
4269
}
4370

4471
let instance: SuperDocInstance | null = null;
72+
const commentsPanel = document.querySelector<HTMLElement>('#comments-panel');
73+
74+
function getEditorText(editor: any): string | null {
75+
const state = editor?.state;
76+
const doc = state?.doc;
77+
if (!doc || typeof doc.textBetween !== 'function' || typeof doc.content?.size !== 'number') return null;
78+
return doc.textBetween(0, doc.content.size, '\n', '\n');
79+
}
80+
81+
function cloneJson<T>(value: T): T {
82+
return JSON.parse(JSON.stringify(value)) as T;
83+
}
84+
85+
function buildBehaviorHarnessApi(): BehaviorHarnessApi {
86+
return {
87+
getActiveStorySession: () => {
88+
const session = (harnessWindow.editor as any)?.presentationEditor
89+
?.getStorySessionManager?.()
90+
?.getActiveSession?.();
91+
return session?.locator ?? null;
92+
},
93+
getActiveStoryText: () => {
94+
const activeEditor = (harnessWindow.editor as any)?.presentationEditor?.getActiveEditor?.();
95+
if (!activeEditor || activeEditor === harnessWindow.editor) return null;
96+
return getEditorText(activeEditor);
97+
},
98+
getBodyStoryText: () => getEditorText(harnessWindow.editor),
99+
getCommentsSnapshot: () => {
100+
const comments = (harnessWindow.superdoc as any)?.commentsStore?.commentsList ?? [];
101+
return comments.map((comment: any) => {
102+
const raw = typeof comment?.getValues === 'function' ? comment.getValues() : comment;
103+
return cloneJson({
104+
commentId: raw?.commentId,
105+
importedId: raw?.importedId,
106+
trackedChange: raw?.trackedChange === true,
107+
trackedChangeText: raw?.trackedChangeText ?? null,
108+
trackedChangeType: raw?.trackedChangeType ?? null,
109+
trackedChangeDisplayType: raw?.trackedChangeDisplayType ?? null,
110+
trackedChangeStory: raw?.trackedChangeStory ?? null,
111+
trackedChangeStoryKind: raw?.trackedChangeStoryKind ?? null,
112+
trackedChangeStoryLabel: raw?.trackedChangeStoryLabel ?? '',
113+
trackedChangeAnchorKey: raw?.trackedChangeAnchorKey ?? null,
114+
deletedText: raw?.deletedText ?? null,
115+
resolvedTime: raw?.resolvedTime ?? null,
116+
});
117+
});
118+
},
119+
getEditorCommentPositions: () => {
120+
const positions = (harnessWindow.superdoc as any)?.commentsStore?.editorCommentPositions ?? {};
121+
return cloneJson(positions);
122+
},
123+
getActiveCommentId: () => {
124+
const activeComment = (harnessWindow.superdoc as any)?.commentsStore?.activeComment;
125+
return activeComment == null ? null : String(activeComment);
126+
},
127+
};
128+
}
45129

46130
function applyContentOverride(config: SuperDocConfig, input?: ContentOverrideInput) {
47131
if (!input?.contentOverride || !input?.overrideType) return;
@@ -77,10 +161,15 @@ function init(file?: File, content?: ContentOverrideInput) {
77161
telemetry: { enabled: false },
78162
onReady: ({ superdoc }: SuperDocReadyPayload) => {
79163
harnessWindow.superdoc = superdoc;
164+
if (comments === 'panel' && commentsPanel) {
165+
commentsPanel.replaceChildren();
166+
superdoc.addCommentsList(commentsPanel);
167+
}
80168
superdoc.activeEditor.on('create', (payload: unknown) => {
81169
if (!payload || typeof payload !== 'object' || !('editor' in payload)) return;
82170
harnessWindow.editor = (payload as { editor: unknown }).editor;
83171
});
172+
harnessWindow.behaviorHarness = buildBehaviorHarnessApi();
84173
harnessWindow.superdocReady = true;
85174
},
86175
};
@@ -104,6 +193,14 @@ function init(file?: File, content?: ContentOverrideInput) {
104193
// Comments
105194
if (comments === 'on' || comments === 'panel') {
106195
config.comments = { visible: true };
196+
if (comments === 'panel') {
197+
config.modules = {
198+
...(config.modules ?? {}),
199+
comments: {
200+
...((config.modules as Record<string, unknown> | undefined)?.comments as Record<string, unknown> | undefined),
201+
},
202+
};
203+
}
107204
} else if (comments === 'readonly') {
108205
config.comments = { visible: true, readOnly: true };
109206
} else if (comments === 'disabled') {
@@ -135,6 +232,10 @@ function init(file?: File, content?: ContentOverrideInput) {
135232
}
136233

137234
instance = new SuperDoc(config);
235+
if (commentsPanel) {
236+
commentsPanel.classList.toggle('is-visible', comments === 'panel');
237+
if (comments !== 'panel') commentsPanel.replaceChildren();
238+
}
138239

139240
if (!showSelection) {
140241
const style = document.createElement('style');

tests/behavior/helpers/document-api.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import type {
33
TextAddress,
44
SelectionTarget,
55
MatchContext,
6+
StoryLocator,
67
TrackChangeType,
8+
TrackChangesAcceptInput,
9+
TrackChangesListInput,
10+
TrackChangesRejectInput,
711
CommentsListResult,
812
TrackChangesListResult,
913
TextMutationReceipt,
@@ -320,10 +324,7 @@ export async function deleteText(
320324
});
321325
}
322326

323-
export async function listTrackChanges(
324-
page: Page,
325-
query: { limit?: number; offset?: number; type?: TrackChangeType } = {},
326-
): Promise<TrackChangesListResult> {
327+
export async function listTrackChanges(page: Page, query: TrackChangesListInput = {}): Promise<TrackChangesListResult> {
327328
return page.evaluate((input) => {
328329
const result = (window as any).editor.doc.trackChanges.list(input);
329330
if (Array.isArray(result?.changes)) {
@@ -376,16 +377,24 @@ export async function listSeparate(
376377
return invokeListMutation(page, 'separate', input, options) as Promise<ListsSeparateResult>;
377378
}
378379

379-
export async function acceptTrackChange(page: Page, input: { id: string }): Promise<void> {
380+
export async function acceptTrackChange(page: Page, input: TrackChangesAcceptInput): Promise<void> {
380381
await page.evaluate(
381-
(payload) => (window as any).editor.doc.trackChanges.decide({ decision: 'accept', target: { id: payload.id } }),
382+
(payload) =>
383+
(window as any).editor.doc.trackChanges.decide({
384+
decision: 'accept',
385+
target: payload.story ? { id: payload.id, story: payload.story } : { id: payload.id },
386+
}),
382387
input,
383388
);
384389
}
385390

386-
export async function rejectTrackChange(page: Page, input: { id: string }): Promise<void> {
391+
export async function rejectTrackChange(page: Page, input: TrackChangesRejectInput): Promise<void> {
387392
await page.evaluate(
388-
(payload) => (window as any).editor.doc.trackChanges.decide({ decision: 'reject', target: { id: payload.id } }),
393+
(payload) =>
394+
(window as any).editor.doc.trackChanges.decide({
395+
decision: 'reject',
396+
target: payload.story ? { id: payload.id, story: payload.story } : { id: payload.id },
397+
}),
389398
input,
390399
);
391400
}

0 commit comments

Comments
 (0)