From 707f39f9d44c442fef8670ce1077df3a85f671be Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 12 May 2026 15:55:08 -0300 Subject: [PATCH] fix(super-editor): null-safe getTrackChanges to avoid init-race crash (SD-2641) createCommentForTrackChanges is scheduled from processLoadedDocxComments via setTimeout(0). That only defers to the next tick; it does not wait for the editor's PM state to be initialized. When the bootstrap fires before editor.state is attached, getTrackChanges(editor.state) becomes getTrackChanges(undefined) and crashes reading state.doc. The same race affects all four call sites of getTrackChanges in comments-store.js (lines 294, 1143, 1453, 1545), so harden the helper once rather than guarding each caller. A pure helper returning [] when there is no state to inspect is the semantically correct null behaviour. Adds focused unit tests covering undefined / null / missing-doc inputs. --- .../trackChangesHelpers/getTrackChanges.js | 10 +++++-- .../getTrackChanges.test.js | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/getTrackChanges.test.js diff --git a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/getTrackChanges.js b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/getTrackChanges.js index aaaf6ea4da..4450620adc 100644 --- a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/getTrackChanges.js +++ b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/getTrackChanges.js @@ -3,12 +3,18 @@ import { findInlineNodes } from './documentHelpers.js'; /** * Get track changes marks. - * @param {import('prosemirror-state').EditorState} state - * @param {string} id + * + * Tolerates a missing or partially-initialized state and returns an empty array + * instead of throwing. Comment-import bootstrap can call this through a + * setTimeout(0) before the editor's PM state is attached (SD-2641). + * + * @param {import('prosemirror-state').EditorState | null | undefined} state + * @param {string} [id] * @returns {Array} Array with track changes marks. */ export const getTrackChanges = (state, id = null) => { const trackedChanges = []; + if (!state?.doc) return trackedChanges; const allInlineNodes = findInlineNodes(state.doc); if (!allInlineNodes.length) { diff --git a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/getTrackChanges.test.js b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/getTrackChanges.test.js new file mode 100644 index 0000000000..34d3805b47 --- /dev/null +++ b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/getTrackChanges.test.js @@ -0,0 +1,26 @@ +import { describe, test, expect } from 'vitest'; +import { getTrackChanges } from './getTrackChanges.js'; + +// SD-2641: The helper must not throw when called before the editor's PM state +// is initialized. During DOCX comment-import bootstrap, the orchestrator schedules +// the call via setTimeout(0) which only defers to the next tick — it does not wait +// for editor.state to be attached. The helper is reused from 4 call sites in +// comments-store.js, so we harden it once at the source rather than guarding each +// caller. +describe('getTrackChanges — null-safe input handling', () => { + test('returns [] when state is undefined', () => { + expect(getTrackChanges(undefined)).toEqual([]); + }); + + test('returns [] when state is null', () => { + expect(getTrackChanges(null)).toEqual([]); + }); + + test('returns [] when state has no doc property', () => { + expect(getTrackChanges({})).toEqual([]); + }); + + test('returns [] when state.doc is undefined', () => { + expect(getTrackChanges({ doc: undefined })).toEqual([]); + }); +});