From d18953e2094b4db7804947e77715eff3312471c5 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 11 Mar 2026 17:20:00 -0300 Subject: [PATCH 1/5] fix(track-changes): allow linked style changes in suggesting mode (SD-2182) Heading/paragraph style changes were silently blocked in suggesting mode. tr.setNodeMarkup() produces a ReplaceAroundStep which was being dropped by the track changes handler. This detects node markup changes (same structure, different attrs) and allows them through. Also removes the empty selection guard from toggleLinkedStyle so keyboard shortcuts work. Note: style changes are applied directly without tracked change marks. Tracked change decoration for paragraph styles is a follow-up. --- .../src/extensions/linked-styles/linked-styles.js | 5 +---- .../trackChangesHelpers/replaceAroundStep.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/super-editor/src/extensions/linked-styles/linked-styles.js b/packages/super-editor/src/extensions/linked-styles/linked-styles.js index 837a93c42d..1ec1ef5f3f 100644 --- a/packages/super-editor/src/extensions/linked-styles/linked-styles.js +++ b/packages/super-editor/src/extensions/linked-styles/linked-styles.js @@ -61,14 +61,11 @@ export const LinkedStyles = Extension.create({ * @example * const style = editor.helpers.linkedStyles.getStyleById('Heading1'); * editor.commands.toggleLinkedStyle(style) - * @note If selection is empty, returns false + * @note Works with both cursor position and text selection * @note Removes style if already applied, applies it if not */ toggleLinkedStyle: (style) => (params) => { const { tr } = params; - if (tr.selection.empty) { - return false; - } let node = tr.doc.nodeAt(tr.selection.$from.pos); if (node && node.type.name !== 'paragraph') { diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js index 0eee67ccb8..1a6a837cc5 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js @@ -94,6 +94,16 @@ export const replaceAroundStep = ({ return; } + // Detect setNodeMarkup steps: they preserve the node type and content, + // only changing attributes (e.g. paragraph styleId for heading changes). + // These are safe to apply — they don't alter document structure. + const isNodeMarkupChange = step.structure && step.gapFrom === step.from + 1 && step.gapTo === step.to - 1; + + if (isNodeMarkupChange) { + newTr.step(step); + return; + } + const inputType = tr.getMeta('inputType'); const isBackspace = inputType === 'deleteContentBackward'; From 1e1b59b7b9eacad83f199e58656c6d0ce1f01961 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 11 Mar 2026 18:01:40 -0300 Subject: [PATCH 2/5] fix(track-changes): tighten setNodeMarkup heuristic and fix toggleLinkedStyle cursor handling - Add step.insert === 1 check to exclude lift() false positives - Add map.appendMap() for consistency with other step handlers - Fix nodeAt() null-return by always falling back to findParentNodeClosestToPos - Update stale test to reflect new cursor selection behavior --- .../src/extensions/linked-styles/linked-styles.js | 2 +- .../src/extensions/linked-styles/linked-styles.test.js | 6 +++--- .../track-changes/trackChangesHelpers/replaceAroundStep.js | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/super-editor/src/extensions/linked-styles/linked-styles.js b/packages/super-editor/src/extensions/linked-styles/linked-styles.js index 1ec1ef5f3f..7138f7e131 100644 --- a/packages/super-editor/src/extensions/linked-styles/linked-styles.js +++ b/packages/super-editor/src/extensions/linked-styles/linked-styles.js @@ -68,7 +68,7 @@ export const LinkedStyles = Extension.create({ const { tr } = params; let node = tr.doc.nodeAt(tr.selection.$from.pos); - if (node && node.type.name !== 'paragraph') { + if (!node || node.type.name !== 'paragraph') { node = findParentNodeClosestToPos(tr.selection.$from, (n) => { return n.type.name === 'paragraph'; })?.node; diff --git a/packages/super-editor/src/extensions/linked-styles/linked-styles.test.js b/packages/super-editor/src/extensions/linked-styles/linked-styles.test.js index 790bfb20be..1666a48702 100644 --- a/packages/super-editor/src/extensions/linked-styles/linked-styles.test.js +++ b/packages/super-editor/src/extensions/linked-styles/linked-styles.test.js @@ -135,13 +135,13 @@ describe('LinkedStyles Extension', () => { }); describe('toggleLinkedStyle', () => { - it('should return false for an empty selection', () => { + it('should apply style with a cursor (empty) selection', () => { setParagraphCursor(editor.view, 0); // Cursor selection at first paragraph const result = editor.commands.toggleLinkedStyle(headingStyle, 'paragraph'); - expect(result).toBe(false); + expect(result).toBe(true); const firstParagraph = findParagraphInfo(editor.state.doc, 0); - expect(getParagraphProps(firstParagraph.node).styleId).toBeUndefined(); + expect(getParagraphProps(firstParagraph.node).styleId).toBe('Heading1'); }); it('should apply style when no style is currently set', () => { diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js index 1a6a837cc5..a38ed21921 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js @@ -97,10 +97,13 @@ export const replaceAroundStep = ({ // Detect setNodeMarkup steps: they preserve the node type and content, // only changing attributes (e.g. paragraph styleId for heading changes). // These are safe to apply — they don't alter document structure. - const isNodeMarkupChange = step.structure && step.gapFrom === step.from + 1 && step.gapTo === step.to - 1; + // We also check step.insert === 1 to exclude lift() operations (insert === 0). + const isNodeMarkupChange = + step.structure && step.insert === 1 && step.gapFrom === step.from + 1 && step.gapTo === step.to - 1; if (isNodeMarkupChange) { newTr.step(step); + map.appendMap(step.getMap()); return; } From f27557e23a10d9e17316d11cb93cc19a74aab4f8 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 11 Mar 2026 18:27:09 -0300 Subject: [PATCH 3/5] test(track-changes): add tests for linked style changes in suggesting mode (SD-2182) - Unit: toggleLinkedStyle toggle-off with cursor selection - Unit: isNodeMarkupChange passthrough, lift blocking, map.appendMap - Behavior: heading style apply/toggle in suggesting mode --- .../linked-styles/linked-styles.test.js | 16 ++ .../replaceAroundStep.test.js | 149 ++++++++++++++++++ .../heading-style-suggesting-mode.spec.ts | 115 ++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 tests/behavior/tests/formatting/heading-style-suggesting-mode.spec.ts diff --git a/packages/super-editor/src/extensions/linked-styles/linked-styles.test.js b/packages/super-editor/src/extensions/linked-styles/linked-styles.test.js index 1666a48702..84c5568324 100644 --- a/packages/super-editor/src/extensions/linked-styles/linked-styles.test.js +++ b/packages/super-editor/src/extensions/linked-styles/linked-styles.test.js @@ -144,6 +144,22 @@ describe('LinkedStyles Extension', () => { expect(getParagraphProps(firstParagraph.node).styleId).toBe('Heading1'); }); + it('should toggle off style with a cursor (empty) selection', () => { + // Apply style first + setParagraphCursor(editor.view, 0); + editor.commands.setLinkedStyle(headingStyle); + let firstParagraph = findParagraphInfo(editor.state.doc, 0); + expect(getParagraphProps(firstParagraph.node).styleId).toBe('Heading1'); + + // Toggle off with cursor + setParagraphCursor(editor.view, 0); + const result = editor.commands.toggleLinkedStyle(headingStyle, 'paragraph'); + + expect(result).toBe(true); + firstParagraph = findParagraphInfo(editor.state.doc, 0); + expect(getParagraphProps(firstParagraph.node).styleId).toBe(null); + }); + it('should apply style when no style is currently set', () => { selectParagraph(editor.view, 0); // Select "First paragraph" const applied = toggleLinkedStyleCommand(editor, headingStyle); diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js index 5764beddea..bb8d0e74d4 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js @@ -148,6 +148,155 @@ describe('replaceAroundStep handler', () => { return newTr; }; + describe('isNodeMarkupChange detection', () => { + it('allows setNodeMarkup-style steps through (structure=true, insert=1, gap=±1)', () => { + const doc = schema.nodes.doc.create( + {}, + schema.nodes.paragraph.create( + { paragraphProperties: { styleId: 'Normal' } }, + schema.nodes.run.create({}, [schema.text('Hello')]), + ), + ); + const state = createState(doc); + + // Build a ReplaceAroundStep that matches setNodeMarkup: structure=true, insert=1, + // gapFrom=from+1, gapTo=to-1 (wraps the same content in a new node with different attrs) + let paraStart = null; + let paraEnd = null; + state.doc.forEach((node, offset) => { + if (paraStart === null && node.type.name === 'paragraph') { + paraStart = offset; + paraEnd = offset + node.nodeSize; + } + }); + + const newParagraph = schema.nodes.paragraph.create({ paragraphProperties: { styleId: 'Heading1' } }); + const step = new ReplaceAroundStep( + paraStart, + paraEnd, + paraStart + 1, + paraEnd - 1, + new Slice(Fragment.from(newParagraph), 0, 0), + 1, + true, + ); + + const tr = state.tr; + tr.setMeta('inputType', 'insertParagraph'); // non-backspace — would normally be blocked + const newTr = state.tr; + const map = new Mapping(); + + replaceAroundStep({ + state, + tr, + step, + newTr, + map, + doc: state.doc, + user, + date, + originalStep: step, + originalStepIndex: 0, + }); + + // The step should be applied directly (not blocked) + expect(newTr.steps.length).toBe(1); + expect(newTr.steps[0]).toBe(step); + }); + + it('blocks lift-style steps (structure=true, insert=0, gap=±1)', () => { + const doc = schema.nodes.doc.create( + {}, + schema.nodes.paragraph.create({}, schema.nodes.run.create({}, [schema.text('Hello')])), + ); + const state = createState(doc); + + let paraStart = null; + let paraEnd = null; + state.doc.forEach((node, offset) => { + if (paraStart === null && node.type.name === 'paragraph') { + paraStart = offset; + paraEnd = offset + node.nodeSize; + } + }); + + // lift-style step: insert=0, structure=true, gap=±1 + const step = new ReplaceAroundStep(paraStart, paraEnd, paraStart + 1, paraEnd - 1, Slice.empty, 0, true); + + const tr = state.tr; + tr.setMeta('inputType', 'insertParagraph'); + const newTr = state.tr; + const map = new Mapping(); + + replaceAroundStep({ + state, + tr, + step, + newTr, + map, + doc: state.doc, + user, + date, + originalStep: step, + originalStepIndex: 0, + }); + + // Should be blocked — not a node markup change + expect(newTr.steps.length).toBe(0); + }); + + it('appends step mapping after applying node markup change', () => { + const doc = schema.nodes.doc.create( + {}, + schema.nodes.paragraph.create( + { paragraphProperties: { styleId: 'Normal' } }, + schema.nodes.run.create({}, [schema.text('Hello')]), + ), + ); + const state = createState(doc); + + let paraStart = null; + let paraEnd = null; + state.doc.forEach((node, offset) => { + if (paraStart === null && node.type.name === 'paragraph') { + paraStart = offset; + paraEnd = offset + node.nodeSize; + } + }); + + const newParagraph = schema.nodes.paragraph.create({ paragraphProperties: { styleId: 'Heading1' } }); + const step = new ReplaceAroundStep( + paraStart, + paraEnd, + paraStart + 1, + paraEnd - 1, + new Slice(Fragment.from(newParagraph), 0, 0), + 1, + true, + ); + + const tr = state.tr; + const newTr = state.tr; + const map = new Mapping(); + + replaceAroundStep({ + state, + tr, + step, + newTr, + map, + doc: state.doc, + user, + date, + originalStep: step, + originalStepIndex: 0, + }); + + // map should have been updated + expect(map.maps.length).toBe(1); + }); + }); + describe('non-backspace blocking', () => { it('blocks non-backspace ReplaceAroundStep (no steps added to newTr)', () => { const doc = schema.nodes.doc.create( diff --git a/tests/behavior/tests/formatting/heading-style-suggesting-mode.spec.ts b/tests/behavior/tests/formatting/heading-style-suggesting-mode.spec.ts new file mode 100644 index 0000000000..e5d2948ca8 --- /dev/null +++ b/tests/behavior/tests/formatting/heading-style-suggesting-mode.spec.ts @@ -0,0 +1,115 @@ +import { test, expect } from '../../fixtures/superdoc.js'; + +test.use({ config: { toolbar: 'full', comments: 'on', trackChanges: true } }); + +test.describe('SD-2182 heading style changes in suggesting mode', () => { + test('applying heading style via setStyleById works in suggesting mode', async ({ superdoc }) => { + // Type text in editing mode + await superdoc.type('Hello world'); + await superdoc.waitForStable(); + + // Switch to suggesting mode + await superdoc.setDocumentMode('suggesting'); + await superdoc.waitForStable(); + + // Select all and apply Heading1 style + await superdoc.selectAll(); + await superdoc.page.evaluate(() => { + (window as any).editor.commands.setStyleById('Heading1'); + }); + await superdoc.waitForStable(); + + // Verify the paragraph now has styleId 'Heading1' + const styleId = await superdoc.page.evaluate(() => { + const editor = (window as any).editor; + let result: string | null = null; + editor.state.doc.descendants((node: any) => { + if (node.type.name === 'paragraph' && node.attrs?.paragraphProperties?.styleId) { + result = node.attrs.paragraphProperties.styleId; + return false; + } + return true; + }); + return result; + }); + + expect(styleId).toBe('Heading1'); + }); + + test('toggling heading style with cursor works in suggesting mode', async ({ superdoc }) => { + // Type text in editing mode + await superdoc.type('Hello world'); + await superdoc.waitForStable(); + + // Switch to suggesting mode (cursor is at end of text — empty selection) + await superdoc.setDocumentMode('suggesting'); + await superdoc.waitForStable(); + + // Apply Heading1 via toggleLinkedStyle with cursor (no selection) + const result = await superdoc.page.evaluate(() => { + const editor = (window as any).editor; + const style = editor.helpers.linkedStyles.getStyleById('Heading1'); + return editor.commands.toggleLinkedStyle(style); + }); + await superdoc.waitForStable(); + + expect(result).toBe(true); + + // Verify the paragraph now has styleId 'Heading1' + const styleId = await superdoc.page.evaluate(() => { + const editor = (window as any).editor; + let result: string | null = null; + editor.state.doc.descendants((node: any) => { + if (node.type.name === 'paragraph' && node.attrs?.paragraphProperties?.styleId) { + result = node.attrs.paragraphProperties.styleId; + return false; + } + return true; + }); + return result; + }); + + expect(styleId).toBe('Heading1'); + }); + + test('toggling heading style off with cursor works in suggesting mode', async ({ superdoc }) => { + // Type text and apply heading in editing mode + await superdoc.type('Hello world'); + await superdoc.waitForStable(); + await superdoc.selectAll(); + await superdoc.page.evaluate(() => { + (window as any).editor.commands.setStyleById('Heading1'); + }); + await superdoc.waitForStable(); + + // Switch to suggesting mode + await superdoc.setDocumentMode('suggesting'); + await superdoc.waitForStable(); + + // Toggle off Heading1 with cursor (no selection) + const result = await superdoc.page.evaluate(() => { + const editor = (window as any).editor; + const style = editor.helpers.linkedStyles.getStyleById('Heading1'); + return editor.commands.toggleLinkedStyle(style); + }); + await superdoc.waitForStable(); + + expect(result).toBe(true); + + // Verify the paragraph no longer has Heading1 styleId + const styleId = await superdoc.page.evaluate(() => { + const editor = (window as any).editor; + let result: string | null = null; + editor.state.doc.descendants((node: any) => { + if (node.type.name === 'paragraph') { + result = node.attrs?.paragraphProperties?.styleId ?? null; + return false; + } + return true; + }); + return result; + }); + + expect(styleId).toBeNull(); + }); +}); From 8a03c4686d5b1d44956d6c822bc656180883ef7b Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Wed, 18 Mar 2026 08:50:25 -0300 Subject: [PATCH 4/5] refactor(track-changes): improve comment accuracy and deduplicate test helpers - Update replaceAroundStep.js comment to note that the isNodeMarkupChange heuristic matches both setNodeMarkup and setBlockType (indistinguishable at the step level), with reference to SD-2191 follow-up - Extract findFirstParagraphRange() into testUtils.js, replacing 4 duplicated paragraph-finding loops in replaceAroundStep.test.js - Extract getFirstParagraphStyleId() helper in behavior test, replacing 3 duplicated page.evaluate blocks --- .../trackChangesHelpers/replaceAroundStep.js | 12 ++-- .../replaceAroundStep.test.js | 42 ++----------- .../trackChangesHelpers/testUtils.js | 18 ++++++ .../heading-style-suggesting-mode.spec.ts | 61 ++++++------------- 4 files changed, 50 insertions(+), 83 deletions(-) diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js index a38ed21921..52ab9724f5 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.js @@ -94,10 +94,14 @@ export const replaceAroundStep = ({ return; } - // Detect setNodeMarkup steps: they preserve the node type and content, - // only changing attributes (e.g. paragraph styleId for heading changes). - // These are safe to apply — they don't alter document structure. - // We also check step.insert === 1 to exclude lift() operations (insert === 0). + // Detect node-markup-change steps (setNodeMarkup and setBlockType both + // produce this same ReplaceAroundStep shape — they can't be distinguished + // at the step level). Used here to let paragraph style changes through in + // suggesting mode (e.g. Normal → Heading1 via setNodeMarkup). + // step.insert === 1 excludes lift() operations (insert === 0). + // Note: setBlockType is not triggered via UI in suggesting mode, but if + // it were, it would also bypass tracking. SD-2191 will add proper tracked + // change marks for these operations. const isNodeMarkupChange = step.structure && step.insert === 1 && step.gapFrom === step.from + 1 && step.gapTo === step.to - 1; diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js index bb8d0e74d4..9f4b501117 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js @@ -7,7 +7,7 @@ import { replaceAroundStep } from './replaceAroundStep.js'; import { TrackDeleteMarkName, TrackInsertMarkName } from '../constants.js'; import { TrackChangesBasePluginKey } from '../plugins/trackChangesBasePlugin.js'; import { initTestEditor } from '@tests/helpers/helpers.js'; -import { findTextPos } from './testUtils.js'; +import { findTextPos, findFirstParagraphRange } from './testUtils.js'; describe('replaceAroundStep handler', () => { let editor; @@ -53,16 +53,7 @@ describe('replaceAroundStep handler', () => { // We find the first paragraph and create a step that would "unwrap" it // by replacing the paragraph's opening and closing tokens while preserving // the content between them. - let paraStart = null; - let paraEnd = null; - doc.forEach((node, offset) => { - if (paraStart === null && node.type.name === 'paragraph') { - paraStart = offset; - paraEnd = offset + node.nodeSize; - } - }); - - if (paraStart === null) throw new Error('No paragraph found'); + const { paraStart, paraEnd } = findFirstParagraphRange(doc); // Build a transaction with a ReplaceAroundStep. // The step unwraps the paragraph: replaces the paragraph node but keeps its inline content. @@ -159,16 +150,7 @@ describe('replaceAroundStep handler', () => { ); const state = createState(doc); - // Build a ReplaceAroundStep that matches setNodeMarkup: structure=true, insert=1, - // gapFrom=from+1, gapTo=to-1 (wraps the same content in a new node with different attrs) - let paraStart = null; - let paraEnd = null; - state.doc.forEach((node, offset) => { - if (paraStart === null && node.type.name === 'paragraph') { - paraStart = offset; - paraEnd = offset + node.nodeSize; - } - }); + const { paraStart, paraEnd } = findFirstParagraphRange(state.doc); const newParagraph = schema.nodes.paragraph.create({ paragraphProperties: { styleId: 'Heading1' } }); const step = new ReplaceAroundStep( @@ -211,14 +193,7 @@ describe('replaceAroundStep handler', () => { ); const state = createState(doc); - let paraStart = null; - let paraEnd = null; - state.doc.forEach((node, offset) => { - if (paraStart === null && node.type.name === 'paragraph') { - paraStart = offset; - paraEnd = offset + node.nodeSize; - } - }); + const { paraStart, paraEnd } = findFirstParagraphRange(state.doc); // lift-style step: insert=0, structure=true, gap=±1 const step = new ReplaceAroundStep(paraStart, paraEnd, paraStart + 1, paraEnd - 1, Slice.empty, 0, true); @@ -255,14 +230,7 @@ describe('replaceAroundStep handler', () => { ); const state = createState(doc); - let paraStart = null; - let paraEnd = null; - state.doc.forEach((node, offset) => { - if (paraStart === null && node.type.name === 'paragraph') { - paraStart = offset; - paraEnd = offset + node.nodeSize; - } - }); + const { paraStart, paraEnd } = findFirstParagraphRange(state.doc); const newParagraph = schema.nodes.paragraph.create({ paragraphProperties: { styleId: 'Heading1' } }); const step = new ReplaceAroundStep( diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/testUtils.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/testUtils.js index 62d9928ba2..b17ee5b6ef 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/testUtils.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/testUtils.js @@ -17,3 +17,21 @@ export function findTextPos(docNode, exactText) { }); return found; } + +/** + * Find the start and end positions of the first paragraph node in a document. + * @param {import('prosemirror-model').Node} doc - Document node to search + * @returns {{ paraStart: number, paraEnd: number }} + */ +export function findFirstParagraphRange(doc) { + let paraStart = null; + let paraEnd = null; + doc.forEach((node, offset) => { + if (paraStart === null && node.type.name === 'paragraph') { + paraStart = offset; + paraEnd = offset + node.nodeSize; + } + }); + if (paraStart === null) throw new Error('No paragraph found'); + return { paraStart, paraEnd }; +} diff --git a/tests/behavior/tests/formatting/heading-style-suggesting-mode.spec.ts b/tests/behavior/tests/formatting/heading-style-suggesting-mode.spec.ts index e5d2948ca8..37367bdf08 100644 --- a/tests/behavior/tests/formatting/heading-style-suggesting-mode.spec.ts +++ b/tests/behavior/tests/formatting/heading-style-suggesting-mode.spec.ts @@ -1,4 +1,20 @@ import { test, expect } from '../../fixtures/superdoc.js'; +import type { Page } from '@playwright/test'; + +async function getFirstParagraphStyleId(page: Page): Promise { + return page.evaluate(() => { + const editor = (window as any).editor; + let result: string | null = null; + editor.state.doc.descendants((node: any) => { + if (node.type.name === 'paragraph') { + result = node.attrs?.paragraphProperties?.styleId ?? null; + return false; + } + return true; + }); + return result; + }); +} test.use({ config: { toolbar: 'full', comments: 'on', trackChanges: true } }); @@ -19,20 +35,7 @@ test.describe('SD-2182 heading style changes in suggesting mode', () => { }); await superdoc.waitForStable(); - // Verify the paragraph now has styleId 'Heading1' - const styleId = await superdoc.page.evaluate(() => { - const editor = (window as any).editor; - let result: string | null = null; - editor.state.doc.descendants((node: any) => { - if (node.type.name === 'paragraph' && node.attrs?.paragraphProperties?.styleId) { - result = node.attrs.paragraphProperties.styleId; - return false; - } - return true; - }); - return result; - }); - + const styleId = await getFirstParagraphStyleId(superdoc.page); expect(styleId).toBe('Heading1'); }); @@ -55,20 +58,7 @@ test.describe('SD-2182 heading style changes in suggesting mode', () => { expect(result).toBe(true); - // Verify the paragraph now has styleId 'Heading1' - const styleId = await superdoc.page.evaluate(() => { - const editor = (window as any).editor; - let result: string | null = null; - editor.state.doc.descendants((node: any) => { - if (node.type.name === 'paragraph' && node.attrs?.paragraphProperties?.styleId) { - result = node.attrs.paragraphProperties.styleId; - return false; - } - return true; - }); - return result; - }); - + const styleId = await getFirstParagraphStyleId(superdoc.page); expect(styleId).toBe('Heading1'); }); @@ -96,20 +86,7 @@ test.describe('SD-2182 heading style changes in suggesting mode', () => { expect(result).toBe(true); - // Verify the paragraph no longer has Heading1 styleId - const styleId = await superdoc.page.evaluate(() => { - const editor = (window as any).editor; - let result: string | null = null; - editor.state.doc.descendants((node: any) => { - if (node.type.name === 'paragraph') { - result = node.attrs?.paragraphProperties?.styleId ?? null; - return false; - } - return true; - }); - return result; - }); - + const styleId = await getFirstParagraphStyleId(superdoc.page); expect(styleId).toBeNull(); }); }); From 0290f8a996a4c98becb691e79cc0c858e27af97a Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Thu, 19 Mar 2026 19:48:00 -0300 Subject: [PATCH 5/5] fix(test): update toggleHeading test for cursor selection behavior toggleHeading uses toggleLinkedStyle, which now supports cursor selections. Update the test to expect true (style applied) instead of false (rejected). --- .../super-editor/src/extensions/heading/heading.test.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/super-editor/src/extensions/heading/heading.test.js b/packages/super-editor/src/extensions/heading/heading.test.js index b69e941d93..4d68619516 100644 --- a/packages/super-editor/src/extensions/heading/heading.test.js +++ b/packages/super-editor/src/extensions/heading/heading.test.js @@ -56,13 +56,12 @@ describe('Heading Extension', () => { }); describe('toggleHeading', () => { - it('should return false for an empty selection', () => { + it('should apply heading with a cursor (empty) selection', () => { tr.setSelection(TextSelection.create(tr.doc, 1)); // Cursor selection const result = editor.commands.toggleHeading({ level: 1 }); - expect(result).toBe(false); - const styleId = editor.state.doc.content.content[0].attrs.paragraphProperties?.styleId ?? null; - expect(styleId).toBeNull(); + expect(result).toBe(true); + expect(editor.state.doc.content.content[0].attrs.paragraphProperties?.styleId).toBe('Heading1'); }); it('should toggle heading on for a paragraph', () => {