Skip to content

Commit d3fc8fe

Browse files
authored
chore: test fixes (#2427)
* chore: test fixes * fix(content-controls): make plain-text SDT no-op checks structure-aware
1 parent d9465aa commit d3fc8fe

4 files changed

Lines changed: 293 additions & 61 deletions

File tree

packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2536,8 +2536,8 @@ const MISSING_SDT_TARGET = { kind: 'block' as const, nodeType: 'sdt' as const, n
25362536
const RS_TARGET = { kind: 'block' as const, nodeType: 'sdt' as const, nodeId: 'rs-1' };
25372537

25382538
/** Create an SDT editor whose commands return false — triggers NO_OP failure. */
2539-
function makeNoOpSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
2540-
const editor = makeSdtEditor(overrideAttrs);
2539+
function makeNoOpSdtEditor(overrideAttrs: Record<string, unknown> = {}, textContent = 'SDT content'): Editor {
2540+
const editor = makeSdtEditor(overrideAttrs, textContent);
25412541
(editor.commands as any).updateStructuredContentById = vi.fn(() => false);
25422542
(editor.commands as any).deleteStructuredContentById = vi.fn(() => false);
25432543
(editor.commands as any).insertStructuredContentBlock = vi.fn(() => false);
@@ -2554,7 +2554,7 @@ function makeNoOpSdtEditorWithRepeatingSectionItems(): Editor {
25542554
return editor;
25552555
}
25562556

2557-
function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
2557+
function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}, textContent = 'SDT content'): Editor {
25582558
const sdtAttrs = {
25592559
id: 'sdt-1',
25602560
tag: 'test-tag',
@@ -2566,7 +2566,7 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
25662566
...overrideAttrs,
25672567
};
25682568

2569-
const textNode = createNode('text', [], { text: 'SDT content' });
2569+
const textNode = createNode('text', [], { text: textContent });
25702570
const innerParagraph = createNode('paragraph', [textNode], {
25712571
attrs: { sdBlockId: 'inner-p' },
25722572
isBlock: true,
@@ -7151,8 +7151,8 @@ const mutationVectors: Partial<Record<OperationId, MutationVector>> = {
71517151
return adapter.appendContent({ target: MISSING_SDT_TARGET, content: 'appended' }, { changeMode: 'direct' });
71527152
},
71537153
failureCase: () => {
7154-
const adapter = createContentControlsAdapter(makeNoOpSdtEditor());
7155-
return adapter.appendContent({ target: SDT_TARGET, content: 'appended' }, { changeMode: 'direct' });
7154+
const adapter = createContentControlsAdapter(makeSdtEditor());
7155+
return adapter.appendContent({ target: SDT_TARGET, content: '' }, { changeMode: 'direct' });
71567156
},
71577157
applyCase: () => {
71587158
const adapter = createContentControlsAdapter(makeSdtEditor());
@@ -7369,7 +7369,7 @@ const mutationVectors: Partial<Record<OperationId, MutationVector>> = {
73697369
return adapter.clearContent({ target: MISSING_SDT_TARGET }, { changeMode: 'direct' });
73707370
},
73717371
failureCase: () => {
7372-
const adapter = createContentControlsAdapter(makeNoOpSdtEditor());
7372+
const adapter = createContentControlsAdapter(makeSdtEditor({}, ''));
73737373
return adapter.clearContent({ target: SDT_TARGET }, { changeMode: 'direct' });
73747374
},
73757375
applyCase: () => {
@@ -7627,8 +7627,8 @@ const mutationVectors: Partial<Record<OperationId, MutationVector>> = {
76277627
return adapter.prependContent({ target: MISSING_SDT_TARGET, content: 'prepended' }, { changeMode: 'direct' });
76287628
},
76297629
failureCase: () => {
7630-
const adapter = createContentControlsAdapter(makeNoOpSdtEditor());
7631-
return adapter.prependContent({ target: SDT_TARGET, content: 'prepended' }, { changeMode: 'direct' });
7630+
const adapter = createContentControlsAdapter(makeSdtEditor());
7631+
return adapter.prependContent({ target: SDT_TARGET, content: '' }, { changeMode: 'direct' });
76327632
},
76337633
applyCase: () => {
76347634
const adapter = createContentControlsAdapter(makeSdtEditor());
@@ -7720,7 +7720,7 @@ const mutationVectors: Partial<Record<OperationId, MutationVector>> = {
77207720
return adapter.replaceContent({ target: MISSING_SDT_TARGET, content: 'replaced' }, { changeMode: 'direct' });
77217721
},
77227722
failureCase: () => {
7723-
const adapter = createContentControlsAdapter(makeNoOpSdtEditor());
7723+
const adapter = createContentControlsAdapter(makeSdtEditor({}, 'replaced'));
77247724
return adapter.replaceContent({ target: SDT_TARGET, content: 'replaced' }, { changeMode: 'direct' });
77257725
},
77267726
applyCase: () => {
@@ -7785,7 +7785,7 @@ const mutationVectors: Partial<Record<OperationId, MutationVector>> = {
77857785
return adapter.text.clearValue({ target: MISSING_SDT_TARGET }, { changeMode: 'direct' });
77867786
},
77877787
failureCase: () => {
7788-
const adapter = createContentControlsAdapter(makeNoOpSdtEditor({ controlType: 'text', type: 'text' }));
7788+
const adapter = createContentControlsAdapter(makeSdtEditor({ controlType: 'text', type: 'text' }, ''));
77897789
return adapter.text.clearValue({ target: SDT_TARGET }, { changeMode: 'direct' });
77907790
},
77917791
applyCase: () => {
@@ -7813,7 +7813,7 @@ const mutationVectors: Partial<Record<OperationId, MutationVector>> = {
78137813
return adapter.text.setValue({ target: MISSING_SDT_TARGET, value: 'hello' }, { changeMode: 'direct' });
78147814
},
78157815
failureCase: () => {
7816-
const adapter = createContentControlsAdapter(makeNoOpSdtEditor({ controlType: 'text', type: 'text' }));
7816+
const adapter = createContentControlsAdapter(makeSdtEditor({ controlType: 'text', type: 'text' }, 'hello'));
78177817
return adapter.text.setValue({ target: SDT_TARGET, value: 'hello' }, { changeMode: 'direct' });
78187818
},
78197819
applyCase: () => {

packages/super-editor/src/document-api-adapters/plan-engine/content-controls-wrappers.test.ts

Lines changed: 171 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ type NodeOptions = {
2424
nodeSize?: number;
2525
};
2626

27+
function toChildArray(content: unknown): ProseMirrorNode[] {
28+
if (content == null) {
29+
return [];
30+
}
31+
32+
if (Array.isArray(content)) {
33+
return content as ProseMirrorNode[];
34+
}
35+
36+
return [content as ProseMirrorNode];
37+
}
38+
2739
function createNode(typeName: string, children: ProseMirrorNode[] = [], options: NodeOptions = {}): ProseMirrorNode {
2840
const attrs = options.attrs ?? {};
2941
const text = options.text ?? '';
@@ -40,13 +52,14 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
4052
type: {
4153
name: typeName,
4254
create(newAttrs: Record<string, unknown>, newContent: unknown) {
43-
return createNode(typeName, [], { attrs: newAttrs, isBlock, inlineContent });
55+
return createNode(typeName, toChildArray(newContent), { attrs: newAttrs, isInline, isBlock, inlineContent });
4456
},
4557
createAndFill() {
46-
return createNode(typeName, [], { attrs: {}, isBlock, inlineContent });
58+
return createNode(typeName, [], { attrs: {}, isInline, isBlock, inlineContent });
4759
},
4860
},
4961
attrs,
62+
marks: [],
5063
text: isText ? text : undefined,
5164
content: { size: contentSize },
5265
nodeSize,
@@ -112,8 +125,48 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
112125
// ---------------------------------------------------------------------------
113126

114127
const SDT_TARGET = { kind: 'block' as const, nodeType: 'sdt' as const, nodeId: 'sdt-1' };
128+
const INLINE_SDT_TARGET = { kind: 'inline' as const, nodeType: 'sdt' as const, nodeId: 'sdt-inline-1' };
129+
130+
function createParagraphNode(
131+
text = 'SDT content',
132+
attrs: Record<string, unknown> = { sdBlockId: 'inner-p' },
133+
): ProseMirrorNode {
134+
const children = text.length > 0 ? [createNode('text', [], { text })] : [];
135+
return createNode('paragraph', children, {
136+
attrs,
137+
isBlock: true,
138+
inlineContent: true,
139+
});
140+
}
141+
142+
function createRunNode(text: string, attrs: Record<string, unknown> = {}): ProseMirrorNode {
143+
return createNode('run', [createNode('text', [], { text })], {
144+
attrs,
145+
isInline: true,
146+
isBlock: false,
147+
inlineContent: true,
148+
});
149+
}
150+
151+
function createSingleCellTableNode(cellBlocks: ProseMirrorNode[]): ProseMirrorNode {
152+
const cell = createNode('tableCell', cellBlocks, {
153+
isInline: false,
154+
isBlock: false,
155+
inlineContent: false,
156+
});
157+
const row = createNode('tableRow', [cell], {
158+
isInline: false,
159+
isBlock: false,
160+
inlineContent: false,
161+
});
162+
163+
return createNode('table', [row], {
164+
isBlock: true,
165+
inlineContent: false,
166+
});
167+
}
115168

116-
function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
169+
function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}, sdtChildren?: ProseMirrorNode[]): Editor {
117170
const sdtAttrs = {
118171
id: 'sdt-1',
119172
tag: 'test-tag',
@@ -125,13 +178,9 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
125178
...overrideAttrs,
126179
};
127180

128-
const textNode = createNode('text', [], { text: 'SDT content' });
129-
const innerParagraph = createNode('paragraph', [textNode], {
130-
attrs: { sdBlockId: 'inner-p' },
131-
isBlock: true,
132-
inlineContent: true,
133-
});
134-
const sdtNode = createNode('structuredContentBlock', [innerParagraph], {
181+
const defaultParagraph = createParagraphNode();
182+
const blockChildren = sdtChildren ?? [defaultParagraph];
183+
const sdtNode = createNode('structuredContentBlock', blockChildren, {
135184
attrs: sdtAttrs,
136185
isBlock: true,
137186
});
@@ -162,12 +211,15 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
162211
text: (t: string) => createNode('text', [], { text: t }),
163212
nodes: {
164213
paragraph: {
165-
create: vi.fn(() => innerParagraph),
166-
createAndFill: vi.fn(() => innerParagraph),
214+
create: vi.fn(() => createParagraphNode('')),
215+
createAndFill: vi.fn(() => createParagraphNode('')),
167216
},
168217
structuredContentBlock: {
169218
create: vi.fn((attrs: unknown, content: unknown) =>
170-
createNode('structuredContentBlock', [], { attrs: attrs as Record<string, unknown>, isBlock: true }),
219+
createNode('structuredContentBlock', toChildArray(content), {
220+
attrs: attrs as Record<string, unknown>,
221+
isBlock: true,
222+
}),
171223
),
172224
},
173225
},
@@ -179,12 +231,15 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
179231
text: (t: string) => createNode('text', [], { text: t }),
180232
nodes: {
181233
paragraph: {
182-
create: vi.fn(() => innerParagraph),
183-
createAndFill: vi.fn(() => innerParagraph),
234+
create: vi.fn(() => createParagraphNode('')),
235+
createAndFill: vi.fn(() => createParagraphNode('')),
184236
},
185237
structuredContentBlock: {
186238
create: vi.fn((attrs: unknown, content: unknown) =>
187-
createNode('structuredContentBlock', [], { attrs: attrs as Record<string, unknown>, isBlock: true }),
239+
createNode('structuredContentBlock', toChildArray(content), {
240+
attrs: attrs as Record<string, unknown>,
241+
isBlock: true,
242+
}),
188243
),
189244
},
190245
},
@@ -202,7 +257,7 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
202257
return editor;
203258
}
204259

205-
function makeInlineSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
260+
function makeInlineSdtEditor(overrideAttrs: Record<string, unknown> = {}, sdtChildren?: ProseMirrorNode[]): Editor {
206261
const sdtAttrs = {
207262
id: 'sdt-inline-1',
208263
tag: 'inline-test-tag',
@@ -214,8 +269,8 @@ function makeInlineSdtEditor(overrideAttrs: Record<string, unknown> = {}): Edito
214269
...overrideAttrs,
215270
};
216271

217-
const textNode = createNode('text', [], { text: 'Inline SDT content' });
218-
const sdtNode = createNode('structuredContent', [textNode], {
272+
const inlineChildren = sdtChildren ?? [createNode('text', [], { text: 'Inline SDT content' })];
273+
const sdtNode = createNode('structuredContent', inlineChildren, {
219274
attrs: sdtAttrs,
220275
isInline: true,
221276
isBlock: false,
@@ -258,16 +313,12 @@ function makeInlineSdtEditor(overrideAttrs: Record<string, unknown> = {}): Edito
258313
},
259314
structuredContent: {
260315
create: vi.fn((attrs: unknown, content: unknown) =>
261-
createNode(
262-
'structuredContent',
263-
Array.isArray(content) ? content : content ? [content as ProseMirrorNode] : [],
264-
{
265-
attrs: attrs as Record<string, unknown>,
266-
isInline: true,
267-
isBlock: false,
268-
inlineContent: true,
269-
},
270-
),
316+
createNode('structuredContent', toChildArray(content), {
317+
attrs: attrs as Record<string, unknown>,
318+
isInline: true,
319+
isBlock: false,
320+
inlineContent: true,
321+
}),
271322
),
272323
},
273324
},
@@ -284,16 +335,12 @@ function makeInlineSdtEditor(overrideAttrs: Record<string, unknown> = {}): Edito
284335
},
285336
structuredContent: {
286337
create: vi.fn((attrs: unknown, content: unknown) =>
287-
createNode(
288-
'structuredContent',
289-
Array.isArray(content) ? content : content ? [content as ProseMirrorNode] : [],
290-
{
291-
attrs: attrs as Record<string, unknown>,
292-
isInline: true,
293-
isBlock: false,
294-
inlineContent: true,
295-
},
296-
),
338+
createNode('structuredContent', toChildArray(content), {
339+
attrs: attrs as Record<string, unknown>,
340+
isInline: true,
341+
isBlock: false,
342+
inlineContent: true,
343+
}),
297344
),
298345
},
299346
},
@@ -488,17 +535,96 @@ describe('contentControls text clearing', () => {
488535
const editor = makeInlineSdtEditor();
489536
const adapter = createContentControlsAdapter(editor);
490537

491-
const result = adapter.text.clearValue(
492-
{
493-
target: { kind: 'inline', nodeType: 'sdt', nodeId: 'sdt-inline-1' },
494-
},
495-
{ changeMode: 'direct' },
496-
);
538+
const result = adapter.text.clearValue({ target: INLINE_SDT_TARGET }, { changeMode: 'direct' });
497539

498540
expect(result.success).toBe(true);
499541
expect(editor.commands!.updateStructuredContentById).not.toHaveBeenCalled();
500542
expect((editor.state.tr as any).replaceWith).toHaveBeenCalledTimes(1);
501543
});
544+
545+
it('clearContent clears block SDTs that only contain non-text block content', () => {
546+
const editor = makeSdtEditor({}, [
547+
createSingleCellTableNode([createParagraphNode('', { sdBlockId: 'table-cell-p' })]),
548+
]);
549+
const adapter = createContentControlsAdapter(editor);
550+
551+
const result = adapter.clearContent({ target: SDT_TARGET }, { changeMode: 'direct' });
552+
553+
expect(result.success).toBe(true);
554+
expect(editor.commands!.updateStructuredContentById).not.toHaveBeenCalled();
555+
expect((editor.state.tr as any).replaceWith).toHaveBeenCalledTimes(1);
556+
});
557+
558+
it('text.clearValue clears block text controls with non-text block content', () => {
559+
const editor = makeSdtEditor({}, [
560+
createSingleCellTableNode([createParagraphNode('', { sdBlockId: 'table-cell-p' })]),
561+
]);
562+
const adapter = createContentControlsAdapter(editor);
563+
564+
const result = adapter.text.clearValue({ target: SDT_TARGET }, { changeMode: 'direct' });
565+
566+
expect(result.success).toBe(true);
567+
expect(editor.commands!.updateStructuredContentById).not.toHaveBeenCalled();
568+
expect((editor.state.tr as any).replaceWith).toHaveBeenCalledTimes(1);
569+
});
570+
});
571+
572+
describe('contentControls plain-text replacement no-op detection', () => {
573+
it('replaceContent rewrites block SDTs when matching text is split across multiple paragraphs', () => {
574+
const editor = makeSdtEditor({}, [
575+
createParagraphNode('Alpha', { sdBlockId: 'inner-p1' }),
576+
createParagraphNode('Beta', { sdBlockId: 'inner-p2' }),
577+
]);
578+
const adapter = createContentControlsAdapter(editor);
579+
580+
const result = adapter.replaceContent({ target: SDT_TARGET, content: 'AlphaBeta' }, { changeMode: 'direct' });
581+
582+
expect(result.success).toBe(true);
583+
expect((editor.state.tr as any).replaceWith).toHaveBeenCalledTimes(1);
584+
});
585+
586+
it('text.setValue rewrites block text controls when matching text is split across multiple paragraphs', () => {
587+
const editor = makeSdtEditor({}, [
588+
createParagraphNode('Alpha', { sdBlockId: 'inner-p1' }),
589+
createParagraphNode('Beta', { sdBlockId: 'inner-p2' }),
590+
]);
591+
const adapter = createContentControlsAdapter(editor);
592+
593+
const result = adapter.text.setValue({ target: SDT_TARGET, value: 'AlphaBeta' }, { changeMode: 'direct' });
594+
595+
expect(result.success).toBe(true);
596+
expect((editor.state.tr as any).replaceWith).toHaveBeenCalledTimes(1);
597+
});
598+
599+
it('replaceContent rewrites inline SDTs when matching text still carries run formatting', () => {
600+
const editor = makeInlineSdtEditor({}, [createRunNode('Inline SDT content', { runProperties: { bold: true } })]);
601+
const adapter = createContentControlsAdapter(editor);
602+
603+
const result = adapter.replaceContent(
604+
{ target: INLINE_SDT_TARGET, content: 'Inline SDT content' },
605+
{ changeMode: 'direct' },
606+
);
607+
608+
expect(result.success).toBe(true);
609+
expect(editor.commands!.updateStructuredContentById).toHaveBeenCalledWith('sdt-inline-1', {
610+
text: 'Inline SDT content',
611+
});
612+
});
613+
614+
it('text.setValue rewrites inline text controls when matching text still carries run formatting', () => {
615+
const editor = makeInlineSdtEditor({}, [createRunNode('Inline SDT content', { runProperties: { bold: true } })]);
616+
const adapter = createContentControlsAdapter(editor);
617+
618+
const result = adapter.text.setValue(
619+
{ target: INLINE_SDT_TARGET, value: 'Inline SDT content' },
620+
{ changeMode: 'direct' },
621+
);
622+
623+
expect(result.success).toBe(true);
624+
expect(editor.commands!.updateStructuredContentById).toHaveBeenCalledWith('sdt-inline-1', {
625+
text: 'Inline SDT content',
626+
});
627+
});
502628
});
503629

504630
describe('contentControls.setType OOXML element transitions', () => {

0 commit comments

Comments
 (0)