import { describe, expect, it } from 'vitest';
|
|
import { disableTrackRevisionsCommand, insertTypedControlCommand, listContentControlsCommand, 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; ControlType: string; InternalId: 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('使用配置的默认状态创建复选框且不写占位文本', () => {
|
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: '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: '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();
|
setFixtureInsertScope(fixture.Api, { controlType: 'date', defaultValue: '', placeholder: '请选择日期' });
|
|
const result = parseResult(insertTypedControlCommand());
|
|
expect(result.ok).toBe(true);
|
expect(fixture.calls.inserted[0]?.control.placeholder).toBe('请选择日期');
|
});
|
|
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();
|
let enabled: boolean | undefined;
|
fixture.document.SetTrackRevisions = (value) => {
|
enabled = value;
|
return true;
|
};
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateReview: {} });
|
|
expect(parseResult(disableTrackRevisionsCommand())).toEqual({ ok: true });
|
expect(enabled).toBe(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 }) =>
|
Object.assign(fixture.makeControl(type, tag), {
|
GetAlias: () => alias,
|
GetInternalId: () => id,
|
IsBlockLevel: () => type === 'multiline',
|
IsCheckBox: () => type === 'checkbox',
|
IsComboBox: () => type === 'dropdown',
|
IsDatePicker: () => type === 'date',
|
IsDropDownList: () => false,
|
}),
|
);
|
installOnlyOfficeGlobals(fixture.Api, { elnTemplateList: {} });
|
|
expect(parseResult(listContentControlsCommand())).toEqual({
|
ok: true,
|
controls: [
|
{ Alias: '文本', ControlType: 'text', InternalId: 'text-1', Tag: 'eln.field.text' },
|
{ Alias: '下拉', ControlType: 'dropdown', InternalId: 'dropdown-1', Tag: 'eln.field.dropdown' },
|
{ Alias: '复选', ControlType: 'checkbox', InternalId: 'checkbox-1', Tag: 'eln.field.checkbox' },
|
{ Alias: '日期', ControlType: 'date', InternalId: 'date-1', Tag: 'eln.field.date' },
|
{ Alias: '多行', ControlType: 'multiline', InternalId: 'multiline-1', Tag: 'eln.field.multiline' },
|
],
|
});
|
});
|
|
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.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],
|
['writeTextDefaultCommand', writeTextDefaultCommand],
|
] 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();
|
fixture.document.controls = [
|
Object.assign(fixture.makeControl('date', 'eln.field.date'), {
|
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: '日期', ControlType: 'date', InternalId: 'date-1', 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 });
|
});
|
});
|