Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/superdoc/src/SuperDoc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,28 @@ describe('SuperDoc.vue', () => {
await nextTick();
expect(superdocStoreStub.isReady.value).toBe(true);

// SD-673: pin the list-definitions-change bridge using the actual
// production payload shape. Both producers in super-editor's
// numbering-part-descriptor emit `{ editor, numbering }` (see
// packages/super-editor/src/editors/v1/core/parts/adapters/
// numbering-part-descriptor.ts:222,242). ListDefinitionsPayload
// itself marks all three fields optional, so this test pins the
// current production numbering variant + the SuperDoc.vue
// pass-through, not every possible ListDefinitionsPayload shape.
//
// Reference equality (.toBe) pins verbatim pass-through; the
// separate Object.keys snapshot pins the key set in case the
// bridge ever mutates the payload in place before forwarding (a
// deep-equal assertion against the same reference would pass
// trivially).
const listDefsPayload = { editor: editorMock, numbering: { nums: [] } };
options.onListDefinitionsChange(listDefsPayload);
const listDefsCall = superdocStub.emit.mock.calls.find(([name]) => name === 'list-definitions-change');
expect(listDefsCall).toBeDefined();
const [, emittedListDefsPayload] = listDefsCall;
expect(emittedListDefsPayload).toBe(listDefsPayload);
expect(Object.keys(emittedListDefsPayload).sort()).toEqual(['editor', 'numbering']);

options.onDocumentLocked({ editor: editorMock, isLocked: true, lockedBy: { name: 'A' } });
expect(superdocStub.lockSuperdoc).toHaveBeenCalledWith(true, { name: 'A' });

Expand Down
103 changes: 103 additions & 0 deletions packages/superdoc/src/core/SuperDoc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2887,4 +2887,107 @@ describe('SuperDoc core', () => {
expect(instance.config.documentMode).toBe(before);
});
});

// ---------------------------------------------------------------------------
// SD-673: runtime event payload shapes
// ---------------------------------------------------------------------------
//
// Pin the exact key set the runtime emits for each public event whose
// Config callback has a named payload type (SuperDoc{Ready,Editor,Locked}
// Payload). Existing tests use objectContaining({...}) which would not
// catch a missing or extra field; these assertions catch the bug class
// from #3503 where typed payloads silently drifted from what the runtime
// emits.
//
// Each test:
// 1. Registers superdoc.on(event, listener).
// 2. Triggers the runtime emit path (the broadcast/lock method).
// 3. Asserts Object.keys(payload).sort() matches the named type's
// key set, and each value has the expected runtime type.

describe('SD-673: runtime event payload shapes', () => {
const baseConfig = () => ({
selector: '#host',
document: 'https://example.com/doc.docx',
documents: [],
modules: { comments: {}, toolbar: {} },
colors: ['red'],
user: { name: 'Jane', email: 'jane@example.com' },
onException: vi.fn(),
});

it("ready: payload key set is ['superdoc'] and value is the SuperDoc instance", async () => {
const { superdocStore } = createAppHarness();
superdocStore.documents = [{ type: DOCX, getEditor: vi.fn(() => ({})), setEditor: vi.fn() }];

const instance = new SuperDoc(baseConfig());
await flushMicrotasks();

const received = [];
instance.on('ready', (payload) => received.push(payload));

instance.broadcastEditorCreate({});

expect(received).toHaveLength(1);
expect(Object.keys(received[0]).sort()).toEqual(['superdoc']);
expect(received[0].superdoc).toBe(instance);
});

it("editorBeforeCreate: payload key set is ['editor']", async () => {
createAppHarness();
const instance = new SuperDoc(baseConfig());
await flushMicrotasks();

const received = [];
instance.on('editorBeforeCreate', (payload) => received.push(payload));

const editor = { id: 'editor-1' };
instance.broadcastEditorBeforeCreate(editor);

expect(received).toHaveLength(1);
expect(Object.keys(received[0]).sort()).toEqual(['editor']);
// editor is wrapped in createDeprecatedEditorProxy; the proxy is
// transparent for property access, so identity-by-property holds.
expect(received[0].editor.id).toBe('editor-1');
});

it("editorCreate: payload key set is ['editor']", async () => {
createAppHarness();
const instance = new SuperDoc(baseConfig());
await flushMicrotasks();

const received = [];
instance.on('editorCreate', (payload) => received.push(payload));

const editor = { id: 'editor-2' };
instance.broadcastEditorCreate(editor);

expect(received).toHaveLength(1);
expect(Object.keys(received[0]).sort()).toEqual(['editor']);
expect(received[0].editor.id).toBe('editor-2');
});

it("locked: payload key set is ['isLocked', 'lockedBy'] and lockedBy is User | null", async () => {
createAppHarness();
const instance = new SuperDoc(baseConfig());
await flushMicrotasks();
instance.config.documents = [];

const received = [];
instance.on('locked', (payload) => received.push(payload));

// Lock with a user.
instance.lockSuperdoc(true, { name: 'Admin', email: 'admin@x.com' });
// Unlock (lockedBy is the implicit `null` default).
instance.lockSuperdoc(false);

expect(received).toHaveLength(2);

expect(Object.keys(received[0]).sort()).toEqual(['isLocked', 'lockedBy']);
expect(received[0]).toEqual({ isLocked: true, lockedBy: { name: 'Admin', email: 'admin@x.com' } });

expect(Object.keys(received[1]).sort()).toEqual(['isLocked', 'lockedBy']);
expect(received[1]).toEqual({ isLocked: false, lockedBy: null });
});
});
});
26 changes: 26 additions & 0 deletions packages/superdoc/src/core/collaboration/collaboration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,32 @@ describe('collaboration.createProvider', () => {
);
});

// SD-673: pin the exact key set of the awareness-update payload.
// SuperDocAwarenessUpdatePayload declares { states, added, removed,
// superdoc }; the objectContaining assertion above would not catch a
// dropped field or a regression to the older { context } shape.
it('awareness-update payload has exactly { states, added, removed, superdoc } (SD-673)', () => {
const context = { emit: vi.fn() };
const result = collaborationModule.createProvider({
config: { url: 'ws://test' },
user: { name: 'Sam', email: 'sam@example.com' },
documentId: 'doc-1',
superdocInstance: context,
});

awarenessStatesToArrayMock.mockReturnValueOnce([{ name: 'A' }, { name: 'B' }]);
result.provider.emitAwareness({ added: [3], removed: [7] }, new Map());

expect(context.emit).toHaveBeenCalledTimes(1);
const [eventName, payload] = context.emit.mock.calls[0];
expect(eventName).toBe('awareness-update');
expect(Object.keys(payload).sort()).toEqual(['added', 'removed', 'states', 'superdoc']);
expect(payload.states).toEqual([{ name: 'A' }, { name: 'B' }]);
expect(payload.added).toEqual([3]);
expect(payload.removed).toEqual([7]);
expect(payload.superdoc).toBe(context);
});

it('creates hocuspocus provider and wires lifecycle callbacks', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const context = { emit: vi.fn() };
Expand Down
23 changes: 23 additions & 0 deletions packages/superdoc/src/stores/comments-store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,15 @@ describe('comments-store', () => {
comment: { commentId: 'change-1' },
}),
);

// SD-673: pin the exact key set of the comments-update payload.
// SuperDocCommentsUpdatePayload declares { type, comment?, changes? };
// the objectContaining assertion above would not catch a regression
// to the older { data: ... } shape or an extra field leaking in.
const [, payload] = superdoc.emit.mock.calls[0];
expect(Object.keys(payload).sort()).toEqual(['comment', 'type']);
expect(typeof payload.type).toBe('string');
expect(payload.comment).toEqual({ commentId: 'change-1' });
});

it('reopens resolved tracked change comments on update events', () => {
Expand Down Expand Up @@ -1382,6 +1391,20 @@ describe('comments-store', () => {
comment: expect.objectContaining({ commentId: 'tc-stale' }),
}),
);

// SD-673: pin the DELETED variant of SuperDocCommentsUpdatePayload.
// This emit path produces the three-key shape { type, comment, changes }
// (changes carries the [{ key: 'deleted', commentId, fileId }] tuple).
// The objectContaining assertion above does not require `changes` to be
// present, so a regression that dropped it would not be caught.
const deletedCall = superdoc.emit.mock.calls.find(
([name, payload]) => name === 'comments-update' && payload?.type === comments_module_events.DELETED,
);
expect(deletedCall).toBeDefined();
const [, deletedPayload] = deletedCall;
expect(Object.keys(deletedPayload).sort()).toEqual(['changes', 'comment', 'type']);
expect(Array.isArray(deletedPayload.changes)).toBe(true);
expect(deletedPayload.changes[0]).toMatchObject({ key: 'deleted' });
});

it('keeps tracked-change comments whose IDs are still present in marks', () => {
Expand Down
Loading