Skip to content

Commit f27557e

Browse files
committed
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
1 parent 1e1b59b commit f27557e

3 files changed

Lines changed: 280 additions & 0 deletions

File tree

packages/super-editor/src/extensions/linked-styles/linked-styles.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,22 @@ describe('LinkedStyles Extension', () => {
144144
expect(getParagraphProps(firstParagraph.node).styleId).toBe('Heading1');
145145
});
146146

147+
it('should toggle off style with a cursor (empty) selection', () => {
148+
// Apply style first
149+
setParagraphCursor(editor.view, 0);
150+
editor.commands.setLinkedStyle(headingStyle);
151+
let firstParagraph = findParagraphInfo(editor.state.doc, 0);
152+
expect(getParagraphProps(firstParagraph.node).styleId).toBe('Heading1');
153+
154+
// Toggle off with cursor
155+
setParagraphCursor(editor.view, 0);
156+
const result = editor.commands.toggleLinkedStyle(headingStyle, 'paragraph');
157+
158+
expect(result).toBe(true);
159+
firstParagraph = findParagraphInfo(editor.state.doc, 0);
160+
expect(getParagraphProps(firstParagraph.node).styleId).toBe(null);
161+
});
162+
147163
it('should apply style when no style is currently set', () => {
148164
selectParagraph(editor.view, 0); // Select "First paragraph"
149165
const applied = toggleLinkedStyleCommand(editor, headingStyle);

packages/super-editor/src/extensions/track-changes/trackChangesHelpers/replaceAroundStep.test.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,155 @@ describe('replaceAroundStep handler', () => {
148148
return newTr;
149149
};
150150

151+
describe('isNodeMarkupChange detection', () => {
152+
it('allows setNodeMarkup-style steps through (structure=true, insert=1, gap=±1)', () => {
153+
const doc = schema.nodes.doc.create(
154+
{},
155+
schema.nodes.paragraph.create(
156+
{ paragraphProperties: { styleId: 'Normal' } },
157+
schema.nodes.run.create({}, [schema.text('Hello')]),
158+
),
159+
);
160+
const state = createState(doc);
161+
162+
// Build a ReplaceAroundStep that matches setNodeMarkup: structure=true, insert=1,
163+
// gapFrom=from+1, gapTo=to-1 (wraps the same content in a new node with different attrs)
164+
let paraStart = null;
165+
let paraEnd = null;
166+
state.doc.forEach((node, offset) => {
167+
if (paraStart === null && node.type.name === 'paragraph') {
168+
paraStart = offset;
169+
paraEnd = offset + node.nodeSize;
170+
}
171+
});
172+
173+
const newParagraph = schema.nodes.paragraph.create({ paragraphProperties: { styleId: 'Heading1' } });
174+
const step = new ReplaceAroundStep(
175+
paraStart,
176+
paraEnd,
177+
paraStart + 1,
178+
paraEnd - 1,
179+
new Slice(Fragment.from(newParagraph), 0, 0),
180+
1,
181+
true,
182+
);
183+
184+
const tr = state.tr;
185+
tr.setMeta('inputType', 'insertParagraph'); // non-backspace — would normally be blocked
186+
const newTr = state.tr;
187+
const map = new Mapping();
188+
189+
replaceAroundStep({
190+
state,
191+
tr,
192+
step,
193+
newTr,
194+
map,
195+
doc: state.doc,
196+
user,
197+
date,
198+
originalStep: step,
199+
originalStepIndex: 0,
200+
});
201+
202+
// The step should be applied directly (not blocked)
203+
expect(newTr.steps.length).toBe(1);
204+
expect(newTr.steps[0]).toBe(step);
205+
});
206+
207+
it('blocks lift-style steps (structure=true, insert=0, gap=±1)', () => {
208+
const doc = schema.nodes.doc.create(
209+
{},
210+
schema.nodes.paragraph.create({}, schema.nodes.run.create({}, [schema.text('Hello')])),
211+
);
212+
const state = createState(doc);
213+
214+
let paraStart = null;
215+
let paraEnd = null;
216+
state.doc.forEach((node, offset) => {
217+
if (paraStart === null && node.type.name === 'paragraph') {
218+
paraStart = offset;
219+
paraEnd = offset + node.nodeSize;
220+
}
221+
});
222+
223+
// lift-style step: insert=0, structure=true, gap=±1
224+
const step = new ReplaceAroundStep(paraStart, paraEnd, paraStart + 1, paraEnd - 1, Slice.empty, 0, true);
225+
226+
const tr = state.tr;
227+
tr.setMeta('inputType', 'insertParagraph');
228+
const newTr = state.tr;
229+
const map = new Mapping();
230+
231+
replaceAroundStep({
232+
state,
233+
tr,
234+
step,
235+
newTr,
236+
map,
237+
doc: state.doc,
238+
user,
239+
date,
240+
originalStep: step,
241+
originalStepIndex: 0,
242+
});
243+
244+
// Should be blocked — not a node markup change
245+
expect(newTr.steps.length).toBe(0);
246+
});
247+
248+
it('appends step mapping after applying node markup change', () => {
249+
const doc = schema.nodes.doc.create(
250+
{},
251+
schema.nodes.paragraph.create(
252+
{ paragraphProperties: { styleId: 'Normal' } },
253+
schema.nodes.run.create({}, [schema.text('Hello')]),
254+
),
255+
);
256+
const state = createState(doc);
257+
258+
let paraStart = null;
259+
let paraEnd = null;
260+
state.doc.forEach((node, offset) => {
261+
if (paraStart === null && node.type.name === 'paragraph') {
262+
paraStart = offset;
263+
paraEnd = offset + node.nodeSize;
264+
}
265+
});
266+
267+
const newParagraph = schema.nodes.paragraph.create({ paragraphProperties: { styleId: 'Heading1' } });
268+
const step = new ReplaceAroundStep(
269+
paraStart,
270+
paraEnd,
271+
paraStart + 1,
272+
paraEnd - 1,
273+
new Slice(Fragment.from(newParagraph), 0, 0),
274+
1,
275+
true,
276+
);
277+
278+
const tr = state.tr;
279+
const newTr = state.tr;
280+
const map = new Mapping();
281+
282+
replaceAroundStep({
283+
state,
284+
tr,
285+
step,
286+
newTr,
287+
map,
288+
doc: state.doc,
289+
user,
290+
date,
291+
originalStep: step,
292+
originalStepIndex: 0,
293+
});
294+
295+
// map should have been updated
296+
expect(map.maps.length).toBe(1);
297+
});
298+
});
299+
151300
describe('non-backspace blocking', () => {
152301
it('blocks non-backspace ReplaceAroundStep (no steps added to newTr)', () => {
153302
const doc = schema.nodes.doc.create(
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { test, expect } from '../../fixtures/superdoc.js';
2+
3+
test.use({ config: { toolbar: 'full', comments: 'on', trackChanges: true } });
4+
5+
test.describe('SD-2182 heading style changes in suggesting mode', () => {
6+
test('applying heading style via setStyleById works in suggesting mode', async ({ superdoc }) => {
7+
// Type text in editing mode
8+
await superdoc.type('Hello world');
9+
await superdoc.waitForStable();
10+
11+
// Switch to suggesting mode
12+
await superdoc.setDocumentMode('suggesting');
13+
await superdoc.waitForStable();
14+
15+
// Select all and apply Heading1 style
16+
await superdoc.selectAll();
17+
await superdoc.page.evaluate(() => {
18+
(window as any).editor.commands.setStyleById('Heading1');
19+
});
20+
await superdoc.waitForStable();
21+
22+
// Verify the paragraph now has styleId 'Heading1'
23+
const styleId = await superdoc.page.evaluate(() => {
24+
const editor = (window as any).editor;
25+
let result: string | null = null;
26+
editor.state.doc.descendants((node: any) => {
27+
if (node.type.name === 'paragraph' && node.attrs?.paragraphProperties?.styleId) {
28+
result = node.attrs.paragraphProperties.styleId;
29+
return false;
30+
}
31+
return true;
32+
});
33+
return result;
34+
});
35+
36+
expect(styleId).toBe('Heading1');
37+
});
38+
39+
test('toggling heading style with cursor works in suggesting mode', async ({ superdoc }) => {
40+
// Type text in editing mode
41+
await superdoc.type('Hello world');
42+
await superdoc.waitForStable();
43+
44+
// Switch to suggesting mode (cursor is at end of text — empty selection)
45+
await superdoc.setDocumentMode('suggesting');
46+
await superdoc.waitForStable();
47+
48+
// Apply Heading1 via toggleLinkedStyle with cursor (no selection)
49+
const result = await superdoc.page.evaluate(() => {
50+
const editor = (window as any).editor;
51+
const style = editor.helpers.linkedStyles.getStyleById('Heading1');
52+
return editor.commands.toggleLinkedStyle(style);
53+
});
54+
await superdoc.waitForStable();
55+
56+
expect(result).toBe(true);
57+
58+
// Verify the paragraph now has styleId 'Heading1'
59+
const styleId = await superdoc.page.evaluate(() => {
60+
const editor = (window as any).editor;
61+
let result: string | null = null;
62+
editor.state.doc.descendants((node: any) => {
63+
if (node.type.name === 'paragraph' && node.attrs?.paragraphProperties?.styleId) {
64+
result = node.attrs.paragraphProperties.styleId;
65+
return false;
66+
}
67+
return true;
68+
});
69+
return result;
70+
});
71+
72+
expect(styleId).toBe('Heading1');
73+
});
74+
75+
test('toggling heading style off with cursor works in suggesting mode', async ({ superdoc }) => {
76+
// Type text and apply heading in editing mode
77+
await superdoc.type('Hello world');
78+
await superdoc.waitForStable();
79+
await superdoc.selectAll();
80+
await superdoc.page.evaluate(() => {
81+
(window as any).editor.commands.setStyleById('Heading1');
82+
});
83+
await superdoc.waitForStable();
84+
85+
// Switch to suggesting mode
86+
await superdoc.setDocumentMode('suggesting');
87+
await superdoc.waitForStable();
88+
89+
// Toggle off Heading1 with cursor (no selection)
90+
const result = await superdoc.page.evaluate(() => {
91+
const editor = (window as any).editor;
92+
const style = editor.helpers.linkedStyles.getStyleById('Heading1');
93+
return editor.commands.toggleLinkedStyle(style);
94+
});
95+
await superdoc.waitForStable();
96+
97+
expect(result).toBe(true);
98+
99+
// Verify the paragraph no longer has Heading1 styleId
100+
const styleId = await superdoc.page.evaluate(() => {
101+
const editor = (window as any).editor;
102+
let result: string | null = null;
103+
editor.state.doc.descendants((node: any) => {
104+
if (node.type.name === 'paragraph') {
105+
result = node.attrs?.paragraphProperties?.styleId ?? null;
106+
return false;
107+
}
108+
return true;
109+
});
110+
return result;
111+
});
112+
113+
expect(styleId).toBeNull();
114+
});
115+
});

0 commit comments

Comments
 (0)