import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
disableTrackRevisionsCommand,
|
insertTypedControlCommand,
|
listContentControlsCommand,
|
readControlTextCommand,
|
readTrackRevisionsStateCommand,
|
setFormulaResultLockCommand,
|
updateControlSpecCommand,
|
writeTextDefaultCommand,
|
} from '../../src/onlyoffice/commands';
|
import { installOnlyOfficeGlobals, makeApiFixture, makeInsertSpec, type FixtureApi, type InsertSpec, type TextDefaultSpec } from '../fixtures/onlyoffice';
|
|
interface CommandResult {
|
controlType?: string;
|
controls?: Array<{
|
Alias: string;
|
ControlConfig: Record<string, unknown>;
|
ControlRole: string;
|
ControlType: string;
|
InternalId: string;
|
Lock: string;
|
Tag: string;
|
}>;
|
error?: string;
|
ok: boolean;
|
}
|
|
function parseResult(value: string): CommandResult {
|
return JSON.parse(value) as CommandResult;
|
}
|
|
function setFixtureInsertScope(Api: FixtureApi, overrides: Partial<InsertSpec> & Pick<InsertSpec, 'controlType'>): void {
|
installOnlyOfficeGlobals(Api, {
|
elnTemplateInsert: makeInsertSpec(overrides),
|
});
|
}
|
|
function setTextDefaultScope(Api: FixtureApi, spec: TextDefaultSpec): void {
|
installOnlyOfficeGlobals(Api, { elnTemplateTextDefault: spec });
|
}
|
|
describe('insertTypedControlCommand', () => {
|
it('使用显示值、业务值和默认项创建下拉控件', () => {
|
const fixture = makeApiFixture();
|
const options = [
|
{ display: '合格', value: 'pass' },
|
{ display: '不合格', value: 'fail' },
|
];
|
setFixtureInsertScope(fixture.Api, { controlType: 'dropdown', options, selectedIndex: 0 });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result).toEqual({ ok: true, controlType: 'dropdown' });
|
expect(fixture.calls.combo).toEqual([[options, 0]]);
|
expect(fixture.calls.inserted[0]).toMatchObject({ block: false, replaceSelection: true });
|
expect(fixture.calls.inserted[0]?.control.tag).toBe('eln.field.result');
|
expect(fixture.calls.inserted[0]?.control.lock).toBe('unlocked');
|
});
|
|
it.each(['text', 'dropdown', 'checkbox', 'date', 'multiline'] as const)('带格式选区插入 %s 控件后保留完整文字格式', (controlType) => {
|
const fixture = makeApiFixture();
|
fixture.document.selectionTextPr = {
|
bold: true,
|
color: { r: 23, g: 109, b: 92 },
|
fontFamily: 'SimSun',
|
fontSize: 14,
|
italic: true,
|
strikeout: true,
|
underline: true,
|
};
|
setFixtureInsertScope(fixture.Api, {
|
controlType,
|
options: [
|
{ display: '合格', value: 'pass' },
|
{ display: '不合格', value: 'fail' },
|
],
|
selectedIndex: -1,
|
});
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result).toEqual({ ok: true, controlType });
|
expect(fixture.calls.inserted[0]?.control.GetRange?.().GetTextPr?.()).toEqual({
|
bold: true,
|
color: { r: 23, g: 109, b: 92 },
|
fontFamily: 'SimSun',
|
fontSize: 14,
|
italic: true,
|
strikeout: true,
|
underline: true,
|
});
|
});
|
|
it('光标位置插入复选框后继承当前字号和下划线', () => {
|
const fixture = makeApiFixture();
|
fixture.document.currentTextPr = { fontFamily: 'SimSun', fontSize: 18, underline: true };
|
setFixtureInsertScope(fixture.Api, { controlType: 'checkbox', defaultChecked: false });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result).toEqual({ ok: true, controlType: 'checkbox' });
|
expect(fixture.calls.inserted[0]?.control.GetRange?.().GetTextPr?.()).toEqual({
|
fontFamily: 'SimSun',
|
fontSize: 18,
|
underline: true,
|
});
|
});
|
|
it('插入后按 Tag 重新定位持久化控件并应用选区格式', () => {
|
const fixture = makeApiFixture();
|
fixture.document.selectionTextPr = { fontFamily: 'SimSun', fontSize: 14, underline: true };
|
let persistedControl: ReturnType<typeof fixture.makeControl> | undefined;
|
fixture.document.InsertContent = (items) => {
|
const transientControl = (items[0] as { inlineControl?: ReturnType<typeof fixture.makeControl> }).inlineControl;
|
if (!transientControl) return false;
|
persistedControl = fixture.makeControl('dropdown', transientControl.tag);
|
fixture.document.controls.push(persistedControl);
|
return true;
|
};
|
setFixtureInsertScope(fixture.Api, {
|
controlType: 'dropdown',
|
options: [
|
{ display: '合格', value: 'pass' },
|
{ display: '不合格', value: 'fail' },
|
],
|
selectedIndex: -1,
|
});
|
|
expect(parseResult(insertTypedControlCommand())).toEqual({ ok: true, controlType: 'dropdown' });
|
expect(persistedControl?.GetRange?.().GetTextPr?.()).toEqual({ fontFamily: 'SimSun', fontSize: 14, underline: true });
|
});
|
|
it('使用配置的默认状态创建复选框且不写占位文本', () => {
|
const fixture = makeApiFixture();
|
setFixtureInsertScope(fixture.Api, { controlType: 'checkbox', defaultChecked: true });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(true);
|
expect(fixture.calls.checkbox).toEqual([{ checked: true, checkedSymbol: 0x2611, uncheckedSymbol: 0x2610 }]);
|
expect(fixture.calls.inserted[0]?.control.placeholder).toBeUndefined();
|
});
|
|
it('使用内联内容控件写入单行文本默认值', () => {
|
const fixture = makeApiFixture();
|
setFixtureInsertScope(fixture.Api, {
|
controlType: 'text',
|
defaultValue: 'S-001',
|
placeholder: '请输入样品编号',
|
});
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result).toEqual({ ok: true, controlType: 'text' });
|
expect(fixture.calls.inserted[0]).toMatchObject({ block: false, replaceSelection: true });
|
expect(fixture.calls.inserted[0]?.control).toMatchObject({
|
kind: 'text',
|
elements: [{ text: 'S-001' }],
|
});
|
expect(fixture.calls.inserted[0]?.control.placeholder).toBeUndefined();
|
});
|
|
it('单行文本先挂入段落,再设置可回读的 Tag 属性', () => {
|
const fixture = makeApiFixture();
|
const control = fixture.makeControl('text');
|
const createParagraph = fixture.Api.CreateParagraph;
|
let attached = false;
|
fixture.Api.CreateInlineLvlSdt = () => control;
|
fixture.Api.CreateParagraph = () => {
|
const paragraph = createParagraph();
|
const addInlineLvlSdt = paragraph.AddInlineLvlSdt;
|
paragraph.AddInlineLvlSdt = (value) => {
|
attached = true;
|
addInlineLvlSdt(value);
|
};
|
return paragraph;
|
};
|
control.SetTag = (tag) => {
|
if (!attached) return false;
|
control.tag = tag;
|
return true;
|
};
|
setFixtureInsertScope(fixture.Api, { controlType: 'text' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result).toEqual({ ok: true, controlType: 'text' });
|
expect(fixture.document.controls).toEqual([control]);
|
});
|
|
it('单行文本没有默认值时设置占位文本', () => {
|
const fixture = makeApiFixture();
|
setFixtureInsertScope(fixture.Api, { controlType: 'text', defaultValue: '', placeholder: '请输入样品编号' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(true);
|
expect(fixture.calls.inserted[0]?.control.placeholder).toBe('请输入样品编号');
|
});
|
|
it('使用本地年月日创建日期默认值', () => {
|
const fixture = makeApiFixture();
|
setFixtureInsertScope(fixture.Api, { controlType: 'date', defaultValue: '2026-08-01' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(true);
|
expect(fixture.calls.dateProperties).toEqual({ format: 'yyyy-MM-dd', lang: 'zh-CN' });
|
expect(fixture.calls.date[0]?.getFullYear()).toBe(2026);
|
expect(fixture.calls.date[0]?.getMonth()).toBe(7);
|
expect(fixture.calls.date[0]?.getDate()).toBe(1);
|
});
|
|
it('使用字段格式和本地完整时间创建日期时间默认值', () => {
|
const fixture = makeApiFixture();
|
setFixtureInsertScope(fixture.Api, {
|
controlType: 'date',
|
dateFormat: 'yyyy-MM-dd HH:mm:ss',
|
defaultValue: '2026-08-01 14:35:27',
|
});
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(true);
|
expect(fixture.calls.dateProperties).toEqual({ format: 'yyyy-MM-dd HH:mm:ss', lang: 'zh-CN' });
|
expect(fixture.calls.date[0]?.getFullYear()).toBe(2026);
|
expect(fixture.calls.date[0]?.getMonth()).toBe(7);
|
expect(fixture.calls.date[0]?.getDate()).toBe(1);
|
expect(fixture.calls.date[0]?.getHours()).toBe(14);
|
expect(fixture.calls.date[0]?.getMinutes()).toBe(35);
|
expect(fixture.calls.date[0]?.getSeconds()).toBe(27);
|
});
|
|
it('把多行默认值拆成独立段落并作为块级内容插入', () => {
|
const fixture = makeApiFixture();
|
setFixtureInsertScope(fixture.Api, {
|
controlType: 'multiline',
|
tag: 'eln.field.note',
|
alias: '备注',
|
defaultValue: '第一行\n第二行',
|
placeholder: '请输入备注',
|
});
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(true);
|
expect(fixture.calls.paragraphTexts).toEqual(['第一行', '第二行']);
|
expect(fixture.calls.inserted[0]).toMatchObject({ block: true, replaceSelection: false });
|
expect(fixture.calls.inserted[0]?.control.elements).toHaveLength(2);
|
});
|
|
it('日期无默认值时不写入当前日期', () => {
|
const fixture = makeApiFixture();
|
const createDatePicker = fixture.Api.CreateDatePickerContentControl!;
|
fixture.Api.CreateDatePickerContentControl = (properties) => {
|
const control = createDatePicker(properties);
|
control.elements = [{ text: '2026-09-04' }];
|
return control;
|
};
|
setFixtureInsertScope(fixture.Api, { controlType: 'date', defaultValue: '', placeholder: '' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(true);
|
expect(fixture.calls.date).toEqual([]);
|
expect(fixture.calls.inserted[0]?.control.elements).toEqual([]);
|
expect(fixture.calls.inserted[0]?.control.placeholder).toBeFalsy();
|
});
|
|
it('构造器缺失时报告不支持且不插入', () => {
|
const fixture = makeApiFixture();
|
delete fixture.Api.CreateDatePickerContentControl;
|
setFixtureInsertScope(fixture.Api, { controlType: 'date' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(false);
|
expect(result.error).toMatch(/不支持日期控件/);
|
expect(fixture.calls.inserted).toHaveLength(0);
|
});
|
|
it('关键控件 setter 返回 false 时拒绝插入', () => {
|
const fixture = makeApiFixture();
|
const control = fixture.makeControl('date');
|
control.SetTag = () => false;
|
fixture.Api.CreateDatePickerContentControl = () => control;
|
setFixtureInsertScope(fixture.Api, { controlType: 'date' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(false);
|
expect(result.error).toMatch(/Tag/);
|
expect(fixture.calls.inserted).toHaveLength(0);
|
});
|
|
it.each([
|
{
|
type: 'date' as const,
|
defaultValue: '2026-08-01',
|
patch: (Api: FixtureApi, control: ReturnType<ReturnType<typeof makeApiFixture>['makeControl']>) => {
|
control.SetDate = () => false;
|
Api.CreateDatePickerContentControl = () => control;
|
},
|
},
|
{
|
type: 'multiline' as const,
|
defaultValue: '第一行\n第二行',
|
patch: (Api: FixtureApi, control: ReturnType<ReturnType<typeof makeApiFixture>['makeControl']>) => {
|
control.RemoveAllElements = () => false;
|
Api.CreateBlockLvlSdt = () => control;
|
},
|
},
|
{
|
type: 'multiline' as const,
|
defaultValue: '第一行\n第二行',
|
patch: (Api: FixtureApi, control: ReturnType<ReturnType<typeof makeApiFixture>['makeControl']>) => {
|
control.AddElement = () => false;
|
Api.CreateBlockLvlSdt = () => control;
|
},
|
},
|
])('$type 默认值写入返回 false 时拒绝插入', ({ type, defaultValue, patch }) => {
|
const fixture = makeApiFixture();
|
const control = fixture.makeControl(type);
|
patch(fixture.Api, control);
|
setFixtureInsertScope(fixture.Api, { controlType: type, defaultValue });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(false);
|
expect(result.error).toMatch(/失败/);
|
expect(fixture.calls.inserted).toHaveLength(0);
|
});
|
|
it.each(['SetPlaceholderText', 'SetColor'] as const)('配置相应值时要求 %s API 存在', (missingMethod) => {
|
const fixture = makeApiFixture();
|
const control = fixture.makeControl('date');
|
delete control[missingMethod];
|
fixture.Api.CreateDatePickerContentControl = () => control;
|
setFixtureInsertScope(fixture.Api, { controlType: 'date', defaultValue: '' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(false);
|
expect(result.error).toMatch(/不支持/);
|
expect(fixture.calls.inserted).toHaveLength(0);
|
});
|
|
it('文档 InsertContent 返回 false 时报告失败', () => {
|
const fixture = makeApiFixture();
|
fixture.document.InsertContent = () => false;
|
setFixtureInsertScope(fixture.Api, { controlType: 'checkbox' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(false);
|
expect(result.error).toMatch(/插入/);
|
});
|
});
|
|
describe('disableTrackRevisionsCommand', () => {
|
it('接受历史修订后关闭修订跟踪,并回读确认最终状态', () => {
|
const fixture = makeApiFixture();
|
const operations: string[] = [];
|
let enabled = true;
|
fixture.document.AcceptAllRevisionChanges = () => {
|
operations.push('accept');
|
return true;
|
};
|
fixture.document.Document = {
|
HaveRevisionChanges: (includeCurrentUser) => {
|
operations.push(`revisions:${includeCurrentUser}`);
|
return false;
|
},
|
SetTrackRevisions: (value) => {
|
operations.push(`local-track:${value}`);
|
enabled = value;
|
},
|
};
|
fixture.document.SetTrackRevisions = (value) => {
|
operations.push(`track:${value}`);
|
enabled = value;
|
return true;
|
};
|
fixture.document.IsTrackRevisions = () => enabled;
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateReview: {} });
|
|
expect(parseResult(disableTrackRevisionsCommand())).toEqual({ ok: true });
|
expect(operations).toEqual(['accept', 'revisions:true', 'track:false', 'local-track:false']);
|
expect(enabled).toBe(false);
|
});
|
|
it('修订跟踪回读仍开启时拒绝进入模板标注', () => {
|
const fixture = makeApiFixture();
|
fixture.document.AcceptAllRevisionChanges = () => true;
|
fixture.document.Document = { HaveRevisionChanges: () => false, SetTrackRevisions: () => undefined };
|
fixture.document.SetTrackRevisions = () => true;
|
fixture.document.IsTrackRevisions = () => true;
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateReview: {} });
|
|
expect(parseResult(disableTrackRevisionsCommand())).toEqual({ ok: false, error: '修订跟踪仍处于开启状态' });
|
});
|
|
it('接受后仍有历史修订时拒绝进入模板标注', () => {
|
const fixture = makeApiFixture();
|
fixture.document.AcceptAllRevisionChanges = () => true;
|
fixture.document.Document = { HaveRevisionChanges: () => true, SetTrackRevisions: () => undefined };
|
fixture.document.SetTrackRevisions = () => true;
|
fixture.document.IsTrackRevisions = () => false;
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateReview: {} });
|
|
expect(parseResult(disableTrackRevisionsCommand())).toEqual({ ok: false, error: '历史修订仍未全部接受' });
|
});
|
});
|
|
describe('readTrackRevisionsStateCommand', () => {
|
it('独立回读当前修订跟踪状态', () => {
|
const fixture = makeApiFixture();
|
fixture.document.Document = { HaveRevisionChanges: () => false };
|
fixture.document.IsTrackRevisions = () => false;
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateReviewState: {} });
|
|
expect(parseResult(readTrackRevisionsStateCommand())).toEqual({ hasRevisionChanges: false, ok: true, trackRevisions: false });
|
});
|
});
|
|
describe('listContentControlsCommand', () => {
|
it('回读内容控件属性并识别五种具体类型', () => {
|
const fixture = makeApiFixture();
|
const definitions = [
|
{ alias: '文本', id: 'text-1', tag: 'eln.field.text', type: 'text' },
|
{ alias: '下拉', id: 'dropdown-1', tag: 'eln.field.dropdown', type: 'dropdown' },
|
{ alias: '复选', id: 'checkbox-1', tag: 'eln.field.checkbox', type: 'checkbox' },
|
{ alias: '日期', id: 'date-1', tag: 'eln.field.date', type: 'date' },
|
{ alias: '多行', id: 'multiline-1', tag: 'eln.field.multiline', type: 'multiline' },
|
] as const;
|
|
fixture.document.controls = definitions.map(({ alias, id, tag, type }) => {
|
const control = Object.assign(fixture.makeControl(type, tag), {
|
GetAlias: () => alias,
|
GetInternalId: () => id,
|
IsBlockLevel: () => type === 'multiline',
|
IsCheckBox: () => type === 'checkbox',
|
IsComboBox: () => type === 'dropdown',
|
IsDatePicker: () => type === 'date',
|
IsDropDownList: () => false,
|
});
|
if (type === 'date') Object.assign(control, { ToJSON: () => JSON.stringify({ sdtPr: { date: { dateFormat: 'dd/MM/yyyy' } } }) });
|
return control;
|
});
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateList: {} });
|
|
expect(parseResult(listContentControlsCommand())).toEqual({
|
ok: true,
|
controls: [
|
{
|
Alias: '文本',
|
ControlConfig: { placeholder: '' },
|
ControlRole: 'PRIMARY',
|
ControlType: 'text',
|
InternalId: 'text-1',
|
Lock: 'unlocked',
|
Tag: 'eln.field.text',
|
},
|
{
|
Alias: '下拉',
|
ControlConfig: { defaultValue: '', options: [], placeholder: '' },
|
ControlRole: 'PRIMARY',
|
ControlType: 'dropdown',
|
InternalId: 'dropdown-1',
|
Lock: 'unlocked',
|
Tag: 'eln.field.dropdown',
|
},
|
{
|
Alias: '复选',
|
ControlConfig: { defaultChecked: false },
|
ControlRole: 'PRIMARY',
|
ControlType: 'checkbox',
|
InternalId: 'checkbox-1',
|
Lock: 'unlocked',
|
Tag: 'eln.field.checkbox',
|
},
|
{
|
Alias: '日期',
|
ControlConfig: { dateFormat: 'dd/MM/yyyy', placeholder: '' },
|
ControlRole: 'PRIMARY',
|
ControlType: 'date',
|
InternalId: 'date-1',
|
Lock: 'unlocked',
|
Tag: 'eln.field.date',
|
},
|
{
|
Alias: '多行',
|
ControlConfig: { placeholder: '' },
|
ControlRole: 'PRIMARY',
|
ControlType: 'multiline',
|
InternalId: 'multiline-1',
|
Lock: 'unlocked',
|
Tag: 'eln.field.multiline',
|
},
|
],
|
});
|
});
|
|
it('从文档实际控件枚举角色、锁和类型专用配置', () => {
|
const fixture = makeApiFixture();
|
const dropdown = fixture.makeControl('dropdown', 'eln.ref.result.abc');
|
dropdown.alias = '检测结论';
|
dropdown.lock = 'contentLocked';
|
dropdown.placeholder = '请选择';
|
dropdown.options = [
|
{ display: '合格', value: 'PASS' },
|
{ display: '不合格', value: 'FAIL' },
|
];
|
fixture.document.controls = [dropdown];
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateList: {} });
|
|
expect(parseResult(listContentControlsCommand()).controls).toEqual([
|
{
|
Alias: '检测结论',
|
ControlConfig: { options: dropdown.options, placeholder: '请选择' },
|
ControlRole: 'REFERENCE',
|
ControlType: 'dropdown',
|
InternalId: 'id-eln.ref.result.abc',
|
Lock: 'contentLocked',
|
Tag: 'eln.ref.result.abc',
|
},
|
]);
|
});
|
|
it('下拉主控件把实际选中项规范化为业务值,引用不把同步正文伪造为默认值', () => {
|
const fixture = makeApiFixture();
|
const primary = fixture.makeControl('dropdown', 'eln.field.result');
|
const reference = fixture.makeControl('dropdown', 'eln.ref.result.ref1');
|
for (const control of [primary, reference]) {
|
control.options = [
|
{ display: '合格', value: 'PASS' },
|
{ display: '不合格', value: 'FAIL' },
|
];
|
control.elements = [{ text: '不合格' }];
|
}
|
fixture.document.controls = [primary, reference];
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateList: {} });
|
|
expect(parseResult(listContentControlsCommand()).controls?.map((control) => control.ControlConfig)).toEqual([
|
{ defaultValue: 'FAIL', options: primary.options, placeholder: '' },
|
{ options: reference.options, placeholder: '' },
|
]);
|
});
|
|
it('文本、日期和多行控件不把当前正文伪造为字段默认值', () => {
|
const fixture = makeApiFixture();
|
const text = fixture.makeControl('text', 'eln.field.text');
|
const date = fixture.makeControl('date', 'eln.field.date');
|
const multiline = fixture.makeControl('multiline', 'eln.field.note');
|
text.elements = [{ text: '当前文本' }];
|
date.elements = [{ text: '2026-08-16' }];
|
multiline.elements = [{ text: '当前多行正文' }];
|
Object.assign(date, { ToJSON: () => JSON.stringify({ sdtPr: { date: { dateFormat: 'yyyy年M月d日' } } }) });
|
fixture.document.controls = [text, date, multiline];
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateList: {} });
|
|
expect(parseResult(listContentControlsCommand()).controls?.map((control) => control.ControlConfig)).toEqual([
|
{ placeholder: '' },
|
{ dateFormat: 'yyyy年M月d日', placeholder: '' },
|
{ placeholder: '' },
|
]);
|
});
|
|
it('多个类型特征同时存在时按复选、下拉、日期、多行的顺序判定', () => {
|
const fixture = makeApiFixture();
|
fixture.document.controls = [
|
Object.assign(fixture.makeControl('ambiguous', 'eln.field.ambiguous'), {
|
GetAlias: () => '歧义字段',
|
GetInternalId: () => 'ambiguous-1',
|
IsBlockLevel: () => true,
|
IsCheckBox: () => true,
|
IsComboBox: () => true,
|
IsDatePicker: () => true,
|
IsDropDownList: () => true,
|
}),
|
];
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateList: {} });
|
|
expect(parseResult(listContentControlsCommand()).controls?.[0]?.ControlType).toBe('checkbox');
|
});
|
});
|
|
describe('writeTextDefaultCommand', () => {
|
it('按唯一 Tag 成功写入文本默认值', () => {
|
const fixture = makeApiFixture();
|
const target = fixture.makeControl('text', 'eln.field.sample_id');
|
fixture.document.controls.push(target);
|
setTextDefaultScope(fixture.Api, { tag: 'eln.field.sample_id', value: 'S-001' });
|
|
const result = parseResult(writeTextDefaultCommand());
|
|
expect(result.ok).toBe(true);
|
expect(target.elements).toHaveLength(1);
|
expect(target.elements[0]?.text).toBe('S-001');
|
});
|
|
it('写入文本默认值后保留原控件字符格式', () => {
|
const fixture = makeApiFixture();
|
const target = fixture.makeControl('text', 'eln.field.sample_id');
|
target.textPr = {
|
bold: true,
|
color: { r: 23, g: 109, b: 92 },
|
fontFamily: 'SimSun',
|
fontSize: 14,
|
italic: true,
|
strikeout: true,
|
underline: true,
|
};
|
fixture.document.controls.push(target);
|
setTextDefaultScope(fixture.Api, { tag: 'eln.field.sample_id', value: 'S-001' });
|
|
const result = parseResult(writeTextDefaultCommand());
|
|
expect(result).toEqual({ ok: true });
|
expect(target.GetRange?.().GetTextPr?.()).toEqual({
|
bold: true,
|
color: { r: 23, g: 109, b: 92 },
|
fontFamily: 'SimSun',
|
fontSize: 14,
|
italic: true,
|
strikeout: true,
|
underline: true,
|
});
|
});
|
|
it.each(['AddText', 'AddElement'] as const)('%s 返回 false 时拒绝文本默认值写入', (failedStep) => {
|
const fixture = makeApiFixture();
|
const target = fixture.makeControl('text', 'eln.field.sample_id');
|
fixture.document.controls.push(target);
|
|
if (failedStep === 'AddText') {
|
fixture.Api.CreateRun = () => ({ text: '', AddText: () => false });
|
} else {
|
target.AddElement = () => false;
|
}
|
setTextDefaultScope(fixture.Api, { tag: 'eln.field.sample_id', value: 'S-001' });
|
|
const result = parseResult(writeTextDefaultCommand());
|
|
expect(result.ok).toBe(false);
|
expect(result.error).toMatch(/失败/);
|
});
|
|
it('清空返回 false 但最终回读一致时仍接受写入', () => {
|
const fixture = makeApiFixture();
|
const target = fixture.makeControl('text', 'eln.field.sample_id');
|
target.RemoveAllElements = () => false;
|
fixture.document.controls.push(target);
|
setTextDefaultScope(fixture.Api, { tag: 'eln.field.sample_id', value: 'S-001' });
|
|
const result = parseResult(writeTextDefaultCommand());
|
|
expect(result.ok).toBe(true);
|
expect(target.GetRange?.().GetText?.()).toBe('S-001');
|
});
|
|
it('文本默认值回读不一致时拒绝写入', () => {
|
const fixture = makeApiFixture();
|
const target = fixture.makeControl('text', 'eln.field.sample_id');
|
target.GetRange = () => ({ GetText: () => '旧值' });
|
fixture.document.controls.push(target);
|
setTextDefaultScope(fixture.Api, { tag: 'eln.field.sample_id', value: 'S-001' });
|
|
const result = parseResult(writeTextDefaultCommand());
|
|
expect(result.ok).toBe(false);
|
expect(result.error).toMatch(/回读/);
|
});
|
|
it.each([0, 2])('目标 Tag 匹配数量为 %i 时拒绝写入', (targetCount) => {
|
const fixture = makeApiFixture();
|
for (let index = 0; index < targetCount; index += 1) {
|
fixture.document.controls.push(fixture.makeControl('text', 'eln.field.sample_id'));
|
}
|
setTextDefaultScope(fixture.Api, { tag: 'eln.field.sample_id', value: 'S-001' });
|
|
const result = parseResult(writeTextDefaultCommand());
|
|
expect(result.ok).toBe(false);
|
expect(result.error).toMatch(/唯一控件/);
|
});
|
});
|
|
describe('command 自包含契约', () => {
|
it.each([
|
['disableTrackRevisionsCommand', disableTrackRevisionsCommand],
|
['insertTypedControlCommand', insertTypedControlCommand],
|
['listContentControlsCommand', listContentControlsCommand],
|
['readTrackRevisionsStateCommand', readTrackRevisionsStateCommand],
|
['writeTextDefaultCommand', writeTextDefaultCommand],
|
['readControlTextCommand', readControlTextCommand],
|
['setFormulaResultLockCommand', setFormulaResultLockCommand],
|
['updateControlSpecCommand', updateControlSpecCommand],
|
] as const)('%s 源码只依赖 Asc、Api 和标准全局', (_name, command) => {
|
const source = command.toString();
|
|
expect(source).not.toMatch(/\b(?:import|require|window|document)\b/);
|
expect(source).toContain('Asc.scope');
|
expect(source).toContain('Api');
|
});
|
|
it('insertTypedControlCommand 的源码可在隔离函数中执行', () => {
|
const fixture = makeApiFixture();
|
const execute = new Function('Asc', 'Api', `return (${insertTypedControlCommand.toString()})()`) as (
|
Asc: { scope: { elnTemplateInsert: InsertSpec } },
|
Api: FixtureApi,
|
) => string;
|
|
const result = parseResult(execute({ scope: { elnTemplateInsert: makeInsertSpec({ controlType: 'checkbox', defaultChecked: true }) } }, fixture.Api));
|
|
expect(result).toEqual({ ok: true, controlType: 'checkbox' });
|
expect(fixture.calls.inserted).toHaveLength(1);
|
});
|
|
it('listContentControlsCommand 的源码可在隔离函数中执行', () => {
|
const fixture = makeApiFixture();
|
const dateControl = fixture.makeControl('date', 'eln.field.date');
|
dateControl.dateFormat = 'yyyy-MM-dd';
|
fixture.document.controls = [
|
Object.assign(dateControl, {
|
GetAlias: () => '日期',
|
GetInternalId: () => 'date-1',
|
IsDatePicker: () => true,
|
}),
|
];
|
const execute = new Function('Asc', 'Api', `return (${listContentControlsCommand.toString()})()`) as (
|
Asc: { scope: { elnTemplateList: Record<string, never> } },
|
Api: FixtureApi,
|
) => string;
|
|
expect(parseResult(execute({ scope: { elnTemplateList: {} } }, fixture.Api))).toEqual({
|
ok: true,
|
controls: [
|
{
|
Alias: '日期',
|
ControlConfig: { dateFormat: 'yyyy-MM-dd', placeholder: '' },
|
ControlRole: 'PRIMARY',
|
ControlType: 'date',
|
InternalId: 'date-1',
|
Lock: 'unlocked',
|
Tag: 'eln.field.date',
|
},
|
],
|
});
|
});
|
|
it('writeTextDefaultCommand 的源码可在隔离函数中执行', () => {
|
const fixture = makeApiFixture();
|
fixture.document.controls.push(fixture.makeControl('text', 'eln.field.sample_id'));
|
const execute = new Function('Asc', 'Api', `return (${writeTextDefaultCommand.toString()})()`) as (
|
Asc: { scope: { elnTemplateTextDefault: TextDefaultSpec } },
|
Api: FixtureApi,
|
) => string;
|
|
const result = parseResult(execute({ scope: { elnTemplateTextDefault: { tag: 'eln.field.sample_id', value: 'S-001' } } }, fixture.Api));
|
|
expect(result).toEqual({ ok: true });
|
});
|
});
|
|
describe('setFormulaResultLockCommand', () => {
|
it('锁定结果主控件与其全部引用,并返回原锁快照', () => {
|
const fixture = makeApiFixture();
|
const primary = fixture.makeControl('text', 'eln.field.result');
|
const reference = fixture.makeControl('text', 'eln.ref.result.one');
|
const unrelated = fixture.makeControl('text', 'eln.field.other');
|
primary.lock = 'unlocked';
|
reference.lock = 'contentLocked';
|
unrelated.lock = 'unlocked';
|
fixture.document.controls.push(primary, reference, unrelated);
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateFormulaLocks: { mode: 'lock', primaryTag: primary.tag } });
|
|
expect(parseResult(setFormulaResultLockCommand())).toEqual({
|
ok: true,
|
snapshot: {
|
primaryTag: 'eln.field.result',
|
locks: [
|
{ tag: 'eln.field.result', lock: 'unlocked' },
|
{ tag: 'eln.ref.result.one', lock: 'contentLocked' },
|
],
|
},
|
});
|
expect(primary.lock).toBe('sdtContentLocked');
|
expect(reference.lock).toBe('sdtContentLocked');
|
expect(unrelated.lock).toBe('unlocked');
|
});
|
|
it('恢复快照后回读每个锁状态,失败时不伪装成功', () => {
|
const fixture = makeApiFixture();
|
const primary = fixture.makeControl('text', 'eln.field.result');
|
const reference = fixture.makeControl('text', 'eln.ref.result.one');
|
primary.lock = 'contentLocked';
|
reference.lock = 'contentLocked';
|
fixture.document.controls.push(primary, reference);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateFormulaLocks: {
|
mode: 'restore',
|
snapshot: {
|
primaryTag: primary.tag,
|
locks: [
|
{ tag: primary.tag, lock: 'unlocked' },
|
{ tag: reference.tag, lock: 'contentLocked' },
|
],
|
},
|
},
|
});
|
|
expect(parseResult(setFormulaResultLockCommand())).toMatchObject({ ok: true });
|
expect(primary.lock).toBe('unlocked');
|
expect(reference.lock).toBe('contentLocked');
|
});
|
});
|
|
describe('字段引用与版本更新命令', () => {
|
it('按主字段 Tag 读取唯一控件的当前文本', () => {
|
const fixture = makeApiFixture();
|
const primary = fixture.makeControl('text', 'eln.field.result');
|
primary.elements = [{ text: '12.3' }];
|
fixture.document.controls.push(primary);
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateReadText: { tag: 'eln.field.result' } });
|
|
expect(parseResult(readControlTextCommand())).toEqual({ ok: true, text: '12.3' });
|
});
|
|
it('批量更新主字段和全部引用的 Tag、Alias、配置和只读锁', () => {
|
const fixture = makeApiFixture();
|
const primary = fixture.makeControl('text', 'eln.field.result');
|
const reference = fixture.makeControl('text', 'eln.ref.result.abc');
|
primary.placeholder = '原提示';
|
reference.placeholder = '原提示';
|
fixture.document.controls.push(primary, reference);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: 'eln.field.result',
|
tag: 'eln.field.result',
|
alias: '新结果',
|
controlType: 'TEXT',
|
controlConfig: { placeholder: '请输入新结果' },
|
lock: 'unlocked',
|
},
|
{
|
currentTag: 'eln.ref.result.abc',
|
tag: 'eln.ref.result.abc',
|
alias: '新结果',
|
controlType: 'TEXT',
|
controlConfig: { placeholder: '请输入新结果' },
|
lock: 'contentLocked',
|
},
|
],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: true, updated: 2 });
|
expect(primary.alias).toBe('新结果');
|
expect(primary.placeholder).toBe('请输入新结果');
|
expect(primary.lock).toBe('unlocked');
|
expect(reference.alias).toBe('新结果');
|
expect(reference.lock).toBe('contentLocked');
|
});
|
|
it('目标 placeholder 为空但旧值非空时在任何 setter 前拒绝清空', () => {
|
const fixture = makeApiFixture();
|
const control = fixture.makeControl('text', 'eln.field.result');
|
control.alias = '原结果';
|
control.lock = 'contentLocked';
|
control.placeholder = '原提示';
|
fixture.document.controls.push(control);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: control.tag,
|
tag: 'eln.field.result_new',
|
alias: '新结果',
|
controlType: 'TEXT',
|
controlConfig: { placeholder: '' },
|
lock: 'unlocked',
|
},
|
],
|
removals: [],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: false, error: '当前编辑器不支持清空已有占位文本' });
|
expect(control).toMatchObject({ alias: '原结果', lock: 'contentLocked', placeholder: '原提示', tag: 'eln.field.result' });
|
});
|
|
it('已有控件 placeholder 为空但目标非空时在任何 setter 前拒绝更新', () => {
|
const fixture = makeApiFixture();
|
const control = fixture.makeControl('text', 'eln.field.result');
|
const setters = {
|
SetAlias: vi.fn(() => true),
|
SetLock: vi.fn(() => true),
|
SetPlaceholderText: vi.fn(() => true),
|
SetTag: vi.fn(() => true),
|
};
|
Object.assign(control, setters);
|
fixture.document.controls.push(control);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: control.tag,
|
tag: 'eln.field.result_new',
|
alias: '新结果',
|
controlType: 'TEXT',
|
controlConfig: { placeholder: '请输入结果' },
|
lock: 'unlocked',
|
},
|
],
|
removals: [],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: false, error: '当前编辑器不支持为已有控件设置占位文本' });
|
expect(setters.SetTag).not.toHaveBeenCalled();
|
expect(setters.SetAlias).not.toHaveBeenCalled();
|
expect(setters.SetLock).not.toHaveBeenCalled();
|
expect(setters.SetPlaceholderText).not.toHaveBeenCalled();
|
});
|
|
it('目标和旧 placeholder 均为空时允许更新', () => {
|
const fixture = makeApiFixture();
|
const control = fixture.makeControl('text', 'eln.field.result');
|
fixture.document.controls.push(control);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: control.tag,
|
tag: control.tag,
|
alias: '结果',
|
controlType: 'TEXT',
|
controlConfig: { placeholder: '' },
|
lock: 'unlocked',
|
},
|
],
|
removals: [],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: true, updated: 1 });
|
});
|
|
it('下拉当前值不在新选项中时在任何 setter 前拒绝更新', () => {
|
const fixture = makeApiFixture();
|
const dropdown = fixture.makeControl('dropdown', 'eln.field.conclusion');
|
dropdown.alias = '原结论';
|
dropdown.lock = 'contentLocked';
|
dropdown.placeholder = '请选择';
|
dropdown.options = [
|
{ display: '合格', value: 'PASS' },
|
{ display: '不合格', value: 'FAIL' },
|
];
|
dropdown.elements = [{ text: '合格' }];
|
fixture.document.controls.push(dropdown);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: dropdown.tag,
|
tag: 'eln.field.conclusion_new',
|
alias: '新结论',
|
controlType: 'DROPDOWN',
|
controlConfig: {
|
options: [
|
{ display: '待定', value: 'PENDING' },
|
{ display: '取消', value: 'CANCELLED' },
|
],
|
placeholder: '请选择',
|
},
|
lock: 'unlocked',
|
},
|
],
|
removals: [],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: false, error: '下拉控件当前值不在新选项中' });
|
expect(dropdown).toMatchObject({
|
alias: '原结论',
|
lock: 'contentLocked',
|
options: [
|
{ display: '合格', value: 'PASS' },
|
{ display: '不合格', value: 'FAIL' },
|
],
|
tag: 'eln.field.conclusion',
|
});
|
});
|
|
it.each([
|
['空文本', ''],
|
['占位文本', '请选择'],
|
])('下拉当前为%s时视为未选中并允许更新选项', (_label, currentText) => {
|
const fixture = makeApiFixture();
|
const dropdown = fixture.makeControl('dropdown', 'eln.field.conclusion');
|
dropdown.placeholder = '请选择';
|
dropdown.options = [
|
{ display: '合格', value: 'PASS' },
|
{ display: '不合格', value: 'FAIL' },
|
];
|
dropdown.elements = currentText ? [{ text: currentText }] : [];
|
fixture.document.controls.push(dropdown);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: dropdown.tag,
|
tag: dropdown.tag,
|
alias: '结论',
|
controlType: 'DROPDOWN',
|
controlConfig: {
|
options: [
|
{ display: '待定', value: 'PENDING' },
|
{ display: '取消', value: 'CANCELLED' },
|
],
|
placeholder: '请选择',
|
},
|
lock: 'unlocked',
|
},
|
],
|
removals: [],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: true, updated: 1 });
|
expect(dropdown.options).toEqual([
|
{ display: '待定', value: 'PENDING' },
|
{ display: '取消', value: 'CANCELLED' },
|
]);
|
});
|
|
it('原位更新下拉和日期配置且保留复选框当前状态', () => {
|
const fixture = makeApiFixture();
|
const dropdown = fixture.makeControl('dropdown', 'eln.field.conclusion');
|
const checkbox = fixture.makeControl('checkbox', 'eln.field.reviewed');
|
const date = fixture.makeControl('date', 'eln.field.test_date');
|
const setCheckBoxChecked = vi.fn((value: boolean) => {
|
checkbox.checked = value;
|
return true;
|
});
|
checkbox.SetCheckBoxChecked = setCheckBoxChecked;
|
dropdown.placeholder = '原提示';
|
date.placeholder = '原提示';
|
date.dateValue = new Date(2025, 5, 15);
|
fixture.document.controls.push(dropdown, checkbox, date);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: dropdown.tag,
|
tag: dropdown.tag,
|
alias: '结论',
|
controlType: 'DROPDOWN',
|
controlConfig: {
|
options: [
|
{ display: '合格', value: 'PASS' },
|
{ display: '不合格', value: 'FAIL' },
|
],
|
placeholder: '请选择',
|
},
|
lock: 'unlocked',
|
},
|
{
|
currentTag: checkbox.tag,
|
tag: checkbox.tag,
|
alias: '已复核',
|
controlType: 'CHECKBOX',
|
controlConfig: { defaultChecked: true },
|
lock: 'contentLocked',
|
},
|
{
|
currentTag: date.tag,
|
tag: date.tag,
|
alias: '检验日期',
|
controlType: 'DATE',
|
controlConfig: { defaultValue: '2026-08-16', dateFormat: 'yyyy-MM-dd', placeholder: '请选择日期' },
|
lock: 'unlocked',
|
},
|
],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: true, updated: 3 });
|
expect(dropdown.options).toEqual([
|
{ display: '合格', value: 'PASS' },
|
{ display: '不合格', value: 'FAIL' },
|
]);
|
expect(checkbox.checked).toBeFalsy();
|
expect(setCheckBoxChecked).not.toHaveBeenCalled();
|
expect(date.dateFormat).toBe('yyyy-MM-dd');
|
expect(date.dateValue).toEqual(new Date(2025, 5, 15));
|
});
|
|
it('类型变化时在修改文档前明确拒绝', () => {
|
const fixture = makeApiFixture();
|
const before = fixture.makeControl('text', 'eln.field.conclusion');
|
before.elements = [{ text: '合格' }];
|
fixture.document.controls.push(before);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: before.tag,
|
tag: before.tag,
|
alias: '结论',
|
controlType: 'DROPDOWN',
|
controlConfig: {
|
options: [
|
{ display: '合格', value: 'PASS' },
|
{ display: '不合格', value: 'FAIL' },
|
],
|
},
|
lock: 'unlocked',
|
},
|
],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: false, error: '当前阶段不支持修改已有控件类型' });
|
expect(fixture.document.controls).toHaveLength(1);
|
expect(fixture.document.controls[0]).toBe(before);
|
expect(fixture.calls.inserted).toHaveLength(0);
|
});
|
|
it('同类型日期升级成功时保留文档当前日期值', () => {
|
const fixture = makeApiFixture();
|
const date = fixture.makeControl('date', 'eln.field.test_date');
|
date.placeholder = '原提示';
|
date.dateValue = new Date(2025, 5, 15);
|
fixture.document.controls.push(date);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: date.tag,
|
tag: date.tag,
|
alias: '检验日期',
|
controlType: 'DATE',
|
controlConfig: { defaultValue: '2026-08-16', dateFormat: 'yyyy-MM-dd', placeholder: '请选择日期' },
|
lock: 'unlocked',
|
},
|
],
|
removals: [],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: true, updated: 1 });
|
expect(date.dateValue).toEqual(new Date(2025, 5, 15));
|
});
|
|
it('日期升级后后续控件更新失败时日期值保持不变', () => {
|
const fixture = makeApiFixture();
|
const date = fixture.makeControl('date', 'eln.field.test_date');
|
const text = fixture.makeControl('text', 'eln.field.result');
|
date.dateValue = new Date(2025, 5, 15);
|
text.SetAlias = () => false;
|
fixture.document.controls.push(date, text);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: date.tag,
|
tag: date.tag,
|
alias: '检验日期',
|
controlType: 'DATE',
|
controlConfig: { defaultValue: '2026-08-16', dateFormat: 'yyyy-MM-dd' },
|
lock: 'unlocked',
|
},
|
{ currentTag: text.tag, tag: text.tag, alias: '新结果', controlType: 'TEXT', controlConfig: {}, lock: 'unlocked' },
|
],
|
removals: [],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand()).ok).toBe(false);
|
expect(date.dateValue).toEqual(new Date(2025, 5, 15));
|
});
|
|
it('主字段提升的第二个更新失败时回滚已更新控件且不删除旧主字段', () => {
|
const fixture = makeApiFixture();
|
const oldPrimary = fixture.makeControl('text', 'eln.field.result');
|
const promoted = fixture.makeControl('text', 'eln.ref.result.promoted');
|
const remainingReference = fixture.makeControl('text', 'eln.ref.result.remaining');
|
promoted.alias = '原提升引用';
|
promoted.lock = 'contentLocked';
|
promoted.placeholder = '原提示';
|
remainingReference.alias = '原保留引用';
|
remainingReference.lock = 'contentLocked';
|
remainingReference.placeholder = '原提示';
|
remainingReference.SetTag = () => false;
|
fixture.document.controls.push(oldPrimary, promoted, remainingReference);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{ currentTag: promoted.tag, tag: oldPrimary.tag, alias: '结果', controlType: 'TEXT', controlConfig: { placeholder: '新提示' }, lock: 'unlocked' },
|
{
|
currentTag: remainingReference.tag,
|
tag: remainingReference.tag,
|
alias: '结果',
|
controlType: 'TEXT',
|
controlConfig: { placeholder: '新提示' },
|
lock: 'contentLocked',
|
},
|
],
|
removals: [oldPrimary.tag],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand()).ok).toBe(false);
|
expect(promoted).toMatchObject({ alias: '原提升引用', lock: 'contentLocked', tag: 'eln.ref.result.promoted' });
|
expect(remainingReference).toMatchObject({ alias: '原保留引用', lock: 'contentLocked', tag: 'eln.ref.result.remaining' });
|
expect(fixture.document.controls).toContain(oldPrimary);
|
});
|
|
it('删除旧控件失败时回滚全部更新', () => {
|
const fixture = makeApiFixture();
|
const oldPrimary = fixture.makeControl('text', 'eln.field.result');
|
const reference = fixture.makeControl('text', 'eln.ref.result.reference');
|
reference.alias = '原引用';
|
reference.lock = 'contentLocked';
|
reference.placeholder = '原提示';
|
oldPrimary.Delete = () => false;
|
fixture.document.controls.push(oldPrimary, reference);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: reference.tag,
|
tag: reference.tag,
|
alias: '新结果',
|
controlType: 'TEXT',
|
controlConfig: { placeholder: '新提示' },
|
lock: 'contentLocked',
|
},
|
],
|
removals: [oldPrimary.tag],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand()).ok).toBe(false);
|
expect(reference).toMatchObject({ alias: '原引用', lock: 'contentLocked', placeholder: '原提示', tag: 'eln.ref.result.reference' });
|
expect(fixture.document.controls).toContain(oldPrimary);
|
});
|
|
it('删除字段控件时同时删除占位内容', () => {
|
const fixture = makeApiFixture();
|
const reference = fixture.makeControl('text', 'eln.ref.result.reference');
|
const deleteControl = vi.fn(() => true);
|
reference.Delete = deleteControl;
|
fixture.document.controls.push(reference);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [],
|
removals: [reference.tag],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: true, updated: 0 });
|
expect(deleteControl).toHaveBeenCalledWith(false);
|
});
|
|
it('引用提升时先释放主 Tag,不向编辑器写入暂态重复 Tag', () => {
|
const fixture = makeApiFixture();
|
const oldPrimary = fixture.makeControl('text', 'eln.field.result');
|
const promoted = fixture.makeControl('text', 'eln.ref.result.promoted');
|
oldPrimary.SetTag = (value) => {
|
if (fixture.document.controls.some((control) => control !== oldPrimary && control.tag === value)) return false;
|
oldPrimary.tag = value;
|
return true;
|
};
|
promoted.SetTag = (value) => {
|
if (fixture.document.controls.some((control) => control !== promoted && control.tag === value)) return false;
|
promoted.tag = value;
|
return true;
|
};
|
fixture.document.controls.push(oldPrimary, promoted);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{
|
currentTag: promoted.tag,
|
tag: oldPrimary.tag,
|
alias: '结果',
|
controlType: 'TEXT',
|
controlConfig: {},
|
lock: 'unlocked',
|
},
|
],
|
removals: [oldPrimary.tag],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toEqual({ ok: true, updated: 1 });
|
expect(fixture.document.controls).toHaveLength(1);
|
expect(fixture.document.controls[0]).toBe(promoted);
|
expect(promoted.tag).toBe('eln.field.result');
|
});
|
|
it('补偿 setter 失败时明确报告文档回滚失败', () => {
|
const fixture = makeApiFixture();
|
const first = fixture.makeControl('text', 'eln.field.first');
|
const second = fixture.makeControl('text', 'eln.field.second');
|
let firstSetTagCalls = 0;
|
first.SetTag = (value) => {
|
firstSetTagCalls += 1;
|
if (firstSetTagCalls > 1) return false;
|
first.tag = value;
|
return true;
|
};
|
second.SetTag = () => false;
|
fixture.document.controls.push(first, second);
|
installOnlyOfficeGlobals(fixture.Api, {
|
elnTemplateUpdateControls: {
|
updates: [
|
{ currentTag: first.tag, tag: 'eln.field.first_new', alias: '新一', controlType: 'TEXT', controlConfig: {}, lock: 'unlocked' },
|
{ currentTag: second.tag, tag: 'eln.field.second_new', alias: '新二', controlType: 'TEXT', controlConfig: {}, lock: 'unlocked' },
|
],
|
removals: [],
|
},
|
});
|
|
expect(parseResult(updateControlSpecCommand())).toMatchObject({ ok: false, error: expect.stringContaining('回滚失败') });
|
});
|
});
|