Operand

pro bo? no.

gram: docs

> ./packages/core/src/prosemirror/utils/extractTrackedChanges.test.ts

/**
* Regression: foreign editors (MS Word Online, in particular) emit a
* fresh `w:id` per atomic edit even when the edits share an author and
* timestamp. The extractor must coalesce by (author, date) so a single
* logical revision burst surfaces as ONE sidebar card, and the dropped
* ids must be tucked into `coalescedRevisionIds` so Accept/Reject still
* clear every site in one click.
*/
import { describe, test, expect } from 'bun:test';
import { Schema } from 'prosemirror-model';
import { EditorState } from 'prosemirror-state';
import { extractTrackedChanges } from './extractTrackedChanges';
const schema = new Schema({
nodes: {
doc: { content: 'block+' },
paragraph: {
group: 'block',
content: 'inline*',
attrs: { pPrIns: { default: null }, pPrDel: { default: null } },
toDOM: () => ['p', 0],
},
tableRow: {
content: 'tableCell+',
attrs: { trIns: { default: null }, trDel: { default: null } },
toDOM: () => ['tr', 0],
},
tableCell: {
content: 'paragraph+',
attrs: { cellMarker: { default: null } },
toDOM: () => ['td', 0],
},
table: { content: 'tableRow+', group: 'block', toDOM: () => ['table', 0] },
text: { group: 'inline' },
},
marks: {
insertion: {
attrs: { revisionId: { default: 0 }, author: { default: '' }, date: { default: null } },
toDOM: () => ['ins', 0],
},
deletion: {
attrs: { revisionId: { default: 0 }, author: { default: '' }, date: { default: null } },
toDOM: () => ['del', 0],
},
comment: {
attrs: { commentId: { default: 0 } },
toDOM: () => ['span', 0],
},
},
});
const AUTHOR = 'Docx Editor User 960';
const DATE = '2026-05-28T20:28:35.944Z';
function makeState(doc: ReturnType<typeof schema.node>): EditorState {
return EditorState.create({ doc });
}
describe('extractTrackedChanges: foreign-doc coalescing by (author, date)', () => {
test('5 insertions with distinct w:ids but same (author, date) collapse to ONE card', () => {
const ins = (id: number, text: string) =>
schema.text(text, [
schema.marks.insertion.create({ revisionId: id, author: AUTHOR, date: DATE }),
]);
const doc = schema.nodes.doc.create({}, [
schema.nodes.paragraph.create({}, [ins(1388975360, 'fdsfsd')]),
schema.nodes.paragraph.create({}, [ins(47262383, 'fdsfsdf')]),
schema.nodes.paragraph.create({}, [ins(1323221525, 'dsfsd')]),
schema.nodes.paragraph.create({}, [ins(737865714, 'fds')]),
schema.nodes.paragraph.create({}, [ins(186027604, 'fsd')]),
]);
const { entries } = extractTrackedChanges(makeState(doc));
expect(entries).toHaveLength(1);
const e = entries[0]!;
expect(e.type).toBe('insertion');
expect(e.author).toBe(AUTHOR);
expect(e.date).toBe(DATE);
// Coalesced ids cover the 4 absorbed entries (the primary lives on `revisionId`).
expect(new Set([e.revisionId, ...(e.coalescedRevisionIds ?? [])])).toEqual(
new Set([1388975360, 47262383, 1323221525, 737865714, 186027604])
);
});
test('paragraph-mark insertions with distinct ids but same (author, date) hide behind one inline card', () => {
const ins = (id: number, text: string) =>
schema.text(text, [
schema.marks.insertion.create({ revisionId: id, author: AUTHOR, date: DATE }),
]);
const pPrIns = (id: number) => ({ revisionId: id, author: AUTHOR, date: DATE });
const doc = schema.nodes.doc.create({}, [
schema.nodes.paragraph.create({ pPrIns: pPrIns(1694997150) }, [ins(1388975360, 'fdsfsd')]),
schema.nodes.paragraph.create({ pPrIns: pPrIns(1254058768) }, [ins(47262383, 'fdsfsdf')]),
]);
const { entries } = extractTrackedChanges(makeState(doc));
expect(entries).toHaveLength(1);
expect(entries[0]!.type).toBe('insertion');
});
test('whole-table insert: rows with distinct trIns ids but same (author, date) surface "Inserted table"', () => {
const trIns = (id: number) => ({ revisionId: id, author: AUTHOR, date: DATE });
const emptyCell = () =>
schema.nodes.tableCell.create({}, [schema.nodes.paragraph.create({}, [])]);
const row = (id: number) =>
schema.nodes.tableRow.create({ trIns: trIns(id) }, [emptyCell(), emptyCell()]);
const table = schema.nodes.table.create({}, [row(844706625), row(694611694)]);
const doc = schema.nodes.doc.create({}, [table]);
const { entries } = extractTrackedChanges(makeState(doc));
const tableEntry = entries.find((e) => e.type === 'tableInserted');
expect(tableEntry).toBeTruthy();
// The two row ids should both be reachable from the card.
const allIds = new Set([tableEntry!.revisionId, ...(tableEntry!.coalescedRevisionIds ?? [])]);
expect(allIds).toEqual(new Set([844706625, 694611694]));
});
test('distinct (author, date) bursts stay as separate cards (we are not over-coalescing)', () => {
const ins = (id: number, author: string, date: string, text: string) =>
schema.text(text, [schema.marks.insertion.create({ revisionId: id, author, date })]);
const doc = schema.nodes.doc.create({}, [
schema.nodes.paragraph.create({}, [ins(1, 'Jane', '2026-05-28T20:28:35.944Z', 'foo')]),
schema.nodes.paragraph.create({}, [ins(2, 'Jane', '2026-05-28T20:30:00.000Z', 'bar')]),
schema.nodes.paragraph.create({}, [ins(3, 'Bob', '2026-05-28T20:28:35.944Z', 'baz')]),
]);
const { entries } = extractTrackedChanges(makeState(doc));
expect(entries).toHaveLength(3);
});
});