Operand

thee, sea, us.

gram: docs

> ./packages/core/src/prosemirror/__tests__/contentControls.test.ts

/**
* PM-level content-control addressing (live-editor path): discover by tag,
* fill content, and remove, against an EditorState built from the singleton
* schema.
*/
import { describe, expect, test } from 'bun:test';
import { EditorState } from 'prosemirror-state';
import { singletonManager } from '../schema';
import {
findContentControlsInPM,
findContentControlPos,
setContentControlContentTr,
removeContentControlTr,
setContentControlValueTr,
setContentControlValueAtPosTr,
} from '../contentControls';
import {
ContentControlNotFoundError,
ContentControlLockedError,
ContentControlTypeError,
} from '../../agent/contentControls';
const schema = singletonManager.getSchema();
function blockSdt(attrs: Record<string, unknown>, text: string) {
return schema.nodes.blockSdt.create(
attrs,
schema.nodes.paragraph.create(null, schema.text(text))
);
}
function inlineSdt(attrs: Record<string, unknown>, text: string) {
return schema.nodes.sdt.create(attrs, schema.text(text));
}
function makeState() {
const doc = schema.nodes.doc.create(null, [
schema.nodes.paragraph.create(null, schema.text('before')),
blockSdt({ sdtType: 'richText', tag: 'intro', alias: 'Intro' }, 'CONTROL #1'),
blockSdt({ sdtType: 'richText', tag: 'locked', lock: 'sdtContentLocked' }, 'do not edit'),
schema.nodes.paragraph.create(null, schema.text('after')),
]);
return EditorState.create({ schema, doc });
}
describe('PM content-control addressing', () => {
test('finds controls by tag and reads text', () => {
const state = makeState();
const all = findContentControlsInPM(state.doc);
expect(all.map((c) => c.tag)).toEqual(['intro', 'locked']);
expect(findContentControlsInPM(state.doc, { tag: 'intro' })[0].text).toBe('CONTROL #1');
});
test('finds inline content controls and preserves raw checkbox attrs', () => {
const rawPropertiesXml =
'<w:sdtPr><w:id w:val="77"/><w:tag w:val="option-alpha"/><w14:checkbox>' +
'<w14:checked w14:val="0"/><w14:checkedState w14:val="2612"/>' +
'<w14:uncheckedState w14:val="2610"/></w14:checkbox></w:sdtPr>';
const doc = schema.nodes.doc.create(null, [
schema.nodes.paragraph.create(null, [
schema.text('before '),
inlineSdt(
{
sdtType: 'checkbox',
id: 77,
tag: 'option-alpha',
alias: 'Option alpha',
checked: false,
dataBinding: JSON.stringify({ xpath: '/x', storeItemId: 'item' }),
rawPropertiesXml,
},
String.fromCodePoint(0x2610)
),
schema.text(' after'),
]),
]);
const state = EditorState.create({ schema, doc });
const control = findContentControlsInPM(state.doc, { tag: 'option-alpha' })[0];
expect(control.sdtType).toBe('checkbox');
expect(control.id).toBe(77);
expect(control.text).toBe(String.fromCodePoint(0x2610));
expect(control.checked).toBe(false);
expect(control.dataBinding?.xpath).toBe('/x');
expect(state.doc.nodeAt(control.pos)?.attrs.rawPropertiesXml).toBe(rawPropertiesXml);
});
test('findContentControlPos returns the node position', () => {
const state = makeState();
const pos = findContentControlPos(state.doc, { tag: 'intro' });
expect(pos).not.toBeNull();
expect(state.doc.nodeAt(pos!)?.type.name).toBe('blockSdt');
});
test('setContentControlContentTr replaces content, keeping the control', () => {
const state = makeState();
const tr = setContentControlContentTr(state, { tag: 'intro' }, 'Filled');
const next = state.apply(tr);
const ctrl = findContentControlsInPM(next.doc, { tag: 'intro' })[0];
expect(ctrl.text).toBe('Filled');
expect(ctrl.alias).toBe('Intro');
expect(findContentControlsInPM(next.doc).length).toBe(2); // none lost
});
test('locked control is refused unless forced', () => {
const state = makeState();
expect(() => setContentControlContentTr(state, { tag: 'locked' }, 'x')).toThrow(
ContentControlLockedError
);
const tr = setContentControlContentTr(state, { tag: 'locked' }, 'forced', { force: true });
expect(findContentControlsInPM(state.apply(tr).doc, { tag: 'locked' })[0].text).toBe('forced');
});
test('missing tag throws', () => {
const state = makeState();
expect(() => setContentControlContentTr(state, { tag: 'nope' }, 'x')).toThrow(
ContentControlNotFoundError
);
});
test('removeContentControlTr deletes; keepContent unwraps', () => {
const state = makeState();
const removed = state.apply(removeContentControlTr(state, { tag: 'intro' }));
expect(findContentControlsInPM(removed.doc, { tag: 'intro' })).toHaveLength(0);
const unwrapped = state.apply(
removeContentControlTr(state, { tag: 'intro' }, { keepContent: true })
);
expect(findContentControlsInPM(unwrapped.doc, { tag: 'intro' })).toHaveLength(0);
expect(unwrapped.doc.textContent).toContain('CONTROL #1'); // content kept
});
test('refuses free-text replacement of a typed (dropdown) control unless forced', () => {
const doc = schema.nodes.doc.create(null, [
blockSdt({ sdtType: 'dropDownList', tag: 'choice' }, 'Active'),
]);
const state = EditorState.create({ schema, doc });
expect(() => setContentControlContentTr(state, { tag: 'choice' }, 'x')).toThrow(
ContentControlTypeError
);
const tr = setContentControlContentTr(state, { tag: 'choice' }, 'Archived', { force: true });
expect(findContentControlsInPM(state.apply(tr).doc, { tag: 'choice' })[0].text).toBe(
'Archived'
);
});
test('setContentControlValueTr toggles a checkbox: content glyph + checked attr', () => {
const doc = schema.nodes.doc.create(null, [
blockSdt(
{
sdtType: 'checkbox',
tag: 'agree',
checked: false,
rawPropertiesXml:
'<w:sdtPr><w14:checkbox><w14:checked w14:val="0"/>' +
'<w14:checkedState w14:val="2612"/><w14:uncheckedState w14:val="2610"/>' +
'</w14:checkbox></w:sdtPr>',
},
String.fromCodePoint(0x2610)
),
]);
const state = EditorState.create({ schema, doc });
const next = state.apply(
setContentControlValueTr(
state,
{ tag: 'agree' },
{
kind: 'checkbox',
checked: true,
}
)
);
const node = next.doc.firstChild!;
expect(node.attrs.checked).toBe(true);
expect(node.textContent).toBe(String.fromCodePoint(0x2612));
expect(String(node.attrs.rawPropertiesXml)).toContain('w14:val="1"');
});
test('setContentControlValueAtPosTr toggles an inline checkbox in place', () => {
const rawPropertiesXml =
'<w:sdtPr><w:tag w:val="option-alpha"/><w14:checkbox>' +
'<w14:checked w14:val="0"/><w14:checkedState w14:val="2612"/>' +
'<w14:uncheckedState w14:val="2610"/></w14:checkbox></w:sdtPr>';
const doc = schema.nodes.doc.create(null, [
schema.nodes.paragraph.create(null, [
schema.text('before '),
inlineSdt(
{
sdtType: 'checkbox',
tag: 'option-alpha',
checked: false,
rawPropertiesXml,
},
String.fromCodePoint(0x2610)
),
schema.text(' after'),
]),
]);
const state = EditorState.create({ schema, doc });
const control = findContentControlsInPM(state.doc, { tag: 'option-alpha' })[0];
const next = state.apply(
setContentControlValueAtPosTr(state, control.pos, {
kind: 'checkbox',
checked: true,
})
);
const node = next.doc.nodeAt(control.pos)!;
expect(node.type.name).toBe('sdt');
expect(node.attrs.checked).toBe(true);
expect(node.textContent).toBe(String.fromCodePoint(0x2612));
expect(String(node.attrs.rawPropertiesXml)).toContain('w14:val="1"');
expect(next.doc.textContent).toBe(`before ${String.fromCodePoint(0x2612)} after`);
});
test('setContentControlValueTr selects a dropdown item by value', () => {
const doc = schema.nodes.doc.create(null, [
blockSdt(
{
sdtType: 'dropDownList',
tag: 'status',
listItems: JSON.stringify([
{ displayText: 'Draft', value: '1' },
{ displayText: 'Final', value: '2' },
]),
rawPropertiesXml: '<w:sdtPr><w:dropDownList w:lastValue="1"/></w:sdtPr>',
},
'Draft'
),
]);
const state = EditorState.create({ schema, doc });
const next = state.apply(
setContentControlValueTr(
state,
{ tag: 'status' },
{
kind: 'dropdown',
value: '2',
}
)
);
expect(next.doc.firstChild!.textContent).toBe('Final');
expect(String(next.doc.firstChild!.attrs.rawPropertiesXml)).toContain('w:lastValue="2"');
});
test('clears the showingPlaceholder attr when filling a placeholder control', () => {
const doc = schema.nodes.doc.create(null, [
blockSdt(
{
sdtType: 'richText',
tag: 'ph',
showingPlaceholder: true,
rawPropertiesXml: '<w:sdtPr><w:showingPlcHdr/></w:sdtPr>',
},
'Click to enter'
),
]);
const state = EditorState.create({ schema, doc });
const next = state.apply(setContentControlContentTr(state, { tag: 'ph' }, 'Real'));
expect(next.doc.firstChild!.attrs.showingPlaceholder).toBe(false);
expect(String(next.doc.firstChild!.attrs.rawPropertiesXml)).not.toContain('showingPlcHdr');
});
});