import type { OnlyOfficeBridge } from '../../src/onlyoffice/bridge';
|
|
import { createPinia, setActivePinia } from 'pinia';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
appendRepeatRow,
|
deleteRepeatRow as requestDeleteRepeatRow,
|
fetchFillData,
|
previewSourceSync as requestSourceSyncPreview,
|
restoreRepeatRow as requestRestoreRepeatRow,
|
synchronizeSource as requestSourceSync,
|
} from '../../src/api/record-fill';
|
import { useRecordFiller } from '../../src/composables/useRecordFiller';
|
|
vi.mock('../../src/api/record-fill', () => ({
|
appendRepeatRow: vi.fn(),
|
deleteRepeatRow: vi.fn(),
|
fetchFillData: vi.fn(),
|
previewSourceSync: vi.fn(),
|
restoreRepeatRow: vi.fn(),
|
synchronizeSource: vi.fn(),
|
}));
|
|
const context = {
|
apiBaseUrl: 'http://localhost:30000',
|
bizDataId: 'record-1',
|
bizModule: 'eln.record',
|
bizScene: 'eln.record.fill',
|
canFill: true,
|
documentId: 'file-1',
|
pluginVersion: '0.1.0',
|
scope: 'onlyoffice:eln-record-fill',
|
ticket: 'opaque-ticket',
|
};
|
|
function fixture(overrides: Partial<OnlyOfficeBridge> = {}) {
|
let ready = () => undefined;
|
let changed = (_control: { InternalId?: unknown; Tag?: unknown }) => undefined;
|
let focused = (_control: { InternalId?: unknown; Tag?: unknown }) => undefined;
|
const bridge: OnlyOfficeBridge = {
|
applyFields: vi.fn().mockResolvedValue([]),
|
appendRepeatRow: vi.fn().mockResolvedValue([]),
|
bindLifecycle: vi.fn((callbacks) => {
|
changed = callbacks.onControlChanged;
|
focused = callbacks.onControlFocused;
|
ready = callbacks.onReady;
|
return vi.fn();
|
}),
|
getRuntimeOptions: vi.fn(() => context),
|
inspectFields: vi.fn().mockResolvedValue([{ alias: '名称', currentValue: '', internalId: '1', options: [], tag: 'eln.field.name', type: 'text' }]),
|
removeRepeatRow: vi.fn().mockResolvedValue([]),
|
selectControl: vi.fn().mockResolvedValue(undefined),
|
setFillOnlyMode: vi.fn().mockResolvedValue(undefined),
|
...overrides,
|
};
|
const filler = useRecordFiller(bridge);
|
return {
|
bridge,
|
changed: (control: { InternalId?: unknown; Tag?: unknown }) => changed(control),
|
filler,
|
focused: (control: { InternalId?: unknown; Tag?: unknown }) => focused(control),
|
ready: () => ready(),
|
};
|
}
|
|
describe('useRecordFiller', () => {
|
beforeEach(() => {
|
setActivePinia(createPinia());
|
vi.mocked(fetchFillData).mockReset().mockResolvedValue({ fields: {}, repeatGroups: [] });
|
vi.mocked(appendRepeatRow).mockReset();
|
vi.mocked(requestDeleteRepeatRow).mockReset();
|
vi.mocked(requestRestoreRepeatRow).mockReset();
|
vi.mocked(requestSourceSyncPreview).mockReset();
|
vi.mocked(requestSourceSync).mockReset();
|
});
|
|
it('enables fill-only mode before inspecting a writable record document', async () => {
|
let fillOnlyMode = false;
|
const setFillOnlyMode = vi.fn(async (enabled: boolean) => {
|
fillOnlyMode = enabled;
|
});
|
const inspectFields = vi.fn(async () => {
|
expect(fillOnlyMode).toBe(true);
|
return [];
|
});
|
const { filler, ready } = fixture({
|
getRuntimeOptions: () => ({
|
...context,
|
bizModule: 'eln.record', bizScene: 'eln.record.fill', scope: 'onlyoffice:eln-record-fill',
|
}),
|
inspectFields,
|
setFillOnlyMode,
|
});
|
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(setFillOnlyMode).toHaveBeenCalledWith(true);
|
expect(inspectFields).toHaveBeenCalledOnce();
|
});
|
|
it('restores a persisted pending source synchronization and awaits document save', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.source': { readOnly: true, type: 'text', value: 'GYDW', valueSource: 'CROSS_TEMPLATE' },
|
},
|
pendingSourceSync: {
|
changedCount: 1, changedFieldCodes: ['source'], differences: [],
|
operationCode: 'source-sync-pending', recalculatedCount: 0,
|
recordId: 'record-1', recordVersion: 5, revisionId: 'revision-1',
|
},
|
repeatGroups: [],
|
});
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { filler, ready } = fixture({
|
applyFields,
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '来源', currentValue: '', internalId: '1', options: [], tag: 'eln.field.source', type: 'text' },
|
]),
|
});
|
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.field.source', type: 'text', value: 'GYDW' },
|
]);
|
expect(filler.store.message).toBe('来源同步已恢复,等待文档保存确认');
|
});
|
|
it('keeps a pending source synchronization when document recovery is partial', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.source': { readOnly: true, type: 'text', value: 'GYDW', valueSource: 'CROSS_TEMPLATE' },
|
},
|
pendingSourceSync: {
|
changedCount: 1, changedFieldCodes: ['source'], differences: [],
|
operationCode: 'source-sync-pending', recalculatedCount: 0,
|
recordId: 'record-1', recordVersion: 5, revisionId: 'revision-1',
|
},
|
repeatGroups: [],
|
});
|
const { filler, ready } = fixture({
|
applyFields: vi.fn().mockResolvedValue([{ ok: false, reason: '写入失败', tag: 'eln.field.source' }]),
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '来源', currentValue: '', internalId: '1', options: [], tag: 'eln.field.source', type: 'text' },
|
]),
|
});
|
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('error'));
|
|
expect(filler.store.message).toBe('数据库当前态恢复到文档失败');
|
});
|
|
it('synchronizes only changed source fields without discarding other drafts', async () => {
|
vi.mocked(fetchFillData)
|
.mockResolvedValueOnce({
|
fields: {
|
'eln.field.formula_result': { formulaStatus: 'SUCCESS', readOnly: true, type: 'text', value: '', valueSource: 'FORMULA' },
|
'eln.field.operator_note': { type: 'text', value: '' },
|
'eln.field.source': { readOnly: true, type: 'text', value: '', valueSource: 'CROSS_TEMPLATE' },
|
},
|
repeatGroups: [],
|
})
|
.mockResolvedValueOnce({
|
fields: {
|
'eln.field.formula_result': { formulaStatus: 'SUCCESS', readOnly: true, type: 'text', value: '42', valueSource: 'FORMULA' },
|
'eln.field.operator_note': { type: 'text', value: '' },
|
'eln.field.source': { readOnly: true, type: 'text', value: 'GYDW', valueSource: 'CROSS_TEMPLATE' },
|
},
|
repeatGroups: [],
|
});
|
vi.mocked(requestSourceSyncPreview).mockResolvedValue({
|
changedCount: 1,
|
differences: [{
|
bindingId: 'binding-source', changed: true, currentValue: 'GYDW', dataType: 'TEXT',
|
fieldCode: 'source', fieldName: '来源', previousValue: null,
|
sourceProperty: null, sourceRecordId: 'source-record-1',
|
sourceRevisionId: 'source-revision-2', sourceType: 'CROSS_TEMPLATE',
|
}],
|
previewToken: 'preview-token', recordId: 'record-1', recordVersion: 4, revisionId: 'revision-1',
|
});
|
vi.mocked(requestSourceSync).mockResolvedValue({
|
changedCount: 1, changedFieldCodes: ['source', 'formula_result'],
|
differences: [], operationCode: 'source-sync-1', recalculatedCount: 0,
|
recordId: 'record-1', recordVersion: 5, revisionId: 'revision-1',
|
});
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { filler, ready } = fixture({
|
applyFields,
|
getRuntimeOptions: () => ({
|
...context,
|
bizModule: 'eln.record', bizScene: 'eln.record.fill', scope: 'onlyoffice:eln-record-fill',
|
}),
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '来源', currentValue: '', internalId: '1', options: [], tag: 'eln.field.source', type: 'text' },
|
{ alias: '来源引用', currentValue: '', internalId: '1-ref', options: [], tag: 'eln.ref.source.instance_1', type: 'text' },
|
{ alias: '公式结果', currentValue: '', internalId: '3', options: [], tag: 'eln.field.formula_result', type: 'text' },
|
{ alias: '人工备注', currentValue: '', internalId: '2', options: [], tag: 'eln.field.operator_note', type: 'text' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
filler.store.setDraft('eln.field.operator_note', '人工草稿');
|
|
await filler.previewSourceSync();
|
expect(filler.store.message).toBe('来源快照差异已加载');
|
await filler.synchronizeSource();
|
|
expect(requestSourceSync).toHaveBeenCalledWith(expect.objectContaining({ ticket: 'opaque-ticket' }), {
|
expectedRecordVersion: 4,
|
operationCode: expect.stringMatching(/^source-sync-/u),
|
previewToken: 'preview-token',
|
});
|
expect(applyFields).toHaveBeenCalledTimes(1);
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.field.source', type: 'text', value: 'GYDW' },
|
{ tag: 'eln.ref.source.instance_1', type: 'text', value: 'GYDW' },
|
{ tag: 'eln.field.formula_result', type: 'text', value: '42' },
|
]);
|
expect(filler.store.formulaResultsStale).toBe(false);
|
expect(filler.store.message).toBe('来源快照已写入文档,等待保存确认');
|
expect(filler.store.fields.find((field) => field.tag === 'eln.field.source')).toMatchObject({
|
documentValue: 'GYDW', draftValue: 'GYDW',
|
});
|
expect(filler.store.fields.find((field) => field.tag === 'eln.field.operator_note')).toMatchObject({
|
documentValue: '', draftValue: '人工草稿',
|
});
|
expect(filler.store.fields.find((field) => field.tag === 'eln.field.source')).toMatchObject({
|
readOnly: true, valueSource: 'CROSS_TEMPLATE',
|
});
|
});
|
|
it('retries document refresh with the same operation after source values were synchronized', async () => {
|
const before = {
|
fields: {
|
'eln.field.source': { readOnly: true, type: 'text' as const, value: '', valueSource: 'CROSS_TEMPLATE' },
|
},
|
repeatGroups: [],
|
};
|
const after = {
|
fields: {
|
'eln.field.source': { readOnly: true, type: 'text' as const, value: 'GYDW', valueSource: 'CROSS_TEMPLATE' },
|
},
|
repeatGroups: [],
|
};
|
vi.mocked(fetchFillData)
|
.mockResolvedValueOnce(before)
|
.mockResolvedValueOnce(after)
|
.mockResolvedValueOnce(after);
|
vi.mocked(requestSourceSyncPreview).mockResolvedValue({
|
changedCount: 1,
|
differences: [{
|
bindingId: 'binding-source', changed: true, currentValue: 'GYDW', dataType: 'TEXT',
|
fieldCode: 'source', fieldName: '来源', previousValue: null,
|
sourceProperty: null, sourceRecordId: 'source-record-1',
|
sourceRevisionId: 'source-revision-2', sourceType: 'CROSS_TEMPLATE',
|
}],
|
previewToken: 'preview-token', recordId: 'record-1', recordVersion: 4, revisionId: 'revision-1',
|
});
|
vi.mocked(requestSourceSync).mockResolvedValue({
|
changedCount: 1, changedFieldCodes: ['source'], differences: [],
|
operationCode: 'source-sync-1', recalculatedCount: 0,
|
recordId: 'record-1', recordVersion: 5, revisionId: 'revision-1',
|
});
|
const applyFields = vi.fn()
|
.mockRejectedValueOnce(new Error('文档写入失败'))
|
.mockImplementationOnce(async (changes) => changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { filler, ready } = fixture({
|
applyFields,
|
getRuntimeOptions: () => ({
|
...context,
|
bizModule: 'eln.record', bizScene: 'eln.record.fill', scope: 'onlyoffice:eln-record-fill',
|
}),
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '来源', currentValue: '', internalId: '1', options: [], tag: 'eln.field.source', type: 'text' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
await filler.previewSourceSync();
|
|
await filler.synchronizeSource();
|
expect(filler.sourceSyncPreview.value).not.toBeNull();
|
await filler.synchronizeSource();
|
|
expect(requestSourceSync).toHaveBeenCalledTimes(2);
|
expect(vi.mocked(requestSourceSync).mock.calls[1]?.[1].operationCode)
|
.toBe(vi.mocked(requestSourceSync).mock.calls[0]?.[1].operationCode);
|
expect(applyFields).toHaveBeenCalledTimes(2);
|
expect(filler.sourceSyncPreview.value).toBeNull();
|
expect(filler.store.message).toBe('来源快照已写入文档,等待保存确认');
|
});
|
|
it.each([401, 403])('invalidates the fill context when source sync preview returns %s', async (status) => {
|
const error = Object.assign(new Error(status === 401 ? '填写凭证已过期,请重新打开文档' : '任务快照同步失败'), { status });
|
vi.mocked(requestSourceSyncPreview).mockRejectedValue(error);
|
const { filler, ready } = fixture({
|
getRuntimeOptions: () => ({
|
...context,
|
bizModule: 'eln.record', bizScene: 'eln.record.fill', scope: 'onlyoffice:eln-record-fill',
|
}),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
await filler.previewSourceSync();
|
await filler.previewSourceSync();
|
|
expect(filler.store.status).toBe('error');
|
expect(filler.store.message).toBe(error.message);
|
expect(filler.canFill()).toBe(false);
|
expect(filler.canSynchronizeSource.value).toBe(false);
|
expect(requestSourceSyncPreview).toHaveBeenCalledOnce();
|
});
|
|
it('enters an error state when SDK initialization times out', async () => {
|
vi.useFakeTimers();
|
const { filler } = fixture();
|
|
await vi.advanceTimersByTimeAsync(5_000);
|
|
expect(filler.store.status).toBe('error');
|
expect(filler.store.message).toBe('ONLYOFFICE SDK 初始化超时');
|
vi.useRealTimers();
|
});
|
|
it('reports an apply failure instead of remaining in working state', async () => {
|
const applyFields = vi.fn().mockRejectedValue(new Error('文档写入失败'));
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: false,
|
steps: [{ id: 'step-fill', name: '填写', order: 1 }],
|
},
|
controlSteps: { 'eln.field.name': 'step-fill' },
|
repeatGroupSteps: {},
|
},
|
});
|
const { filler, ready } = fixture({ applyFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
filler.store.setDraft('eln.field.name', '样品');
|
|
await filler.apply();
|
|
expect(filler.store.status).toBe('error');
|
expect(filler.store.message).toBe('文档写入失败');
|
expect(filler.store.fieldErrors).toEqual({ 'eln.field.name': '文档写入失败' });
|
expect(filler.stepProgress.value[0]?.status).toBe('problem');
|
});
|
|
it('does not apply changes when canFill is false', async () => {
|
const applyFields = vi.fn().mockResolvedValue([]);
|
const { filler, ready } = fixture({ applyFields, getRuntimeOptions: () => ({ ...context, canFill: false }) });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
filler.store.setDraft('eln.field.name', '样品');
|
|
await filler.apply();
|
|
expect(applyFields).not.toHaveBeenCalled();
|
expect(filler.store.message).toBe('当前记录仅可查看');
|
});
|
|
it('applies the selected checkbox and only changed peers from its runtime group', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.already_clear': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.already_clear': 'step-1',
|
'eln.field.failed': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { filler, ready } = fixture({
|
applyFields,
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '合格', currentValue: false, internalId: '1', options: [], tag: 'eln.field.passed', type: 'checkbox' },
|
{ alias: '不合格', currentValue: true, internalId: '2', options: [], tag: 'eln.field.failed', type: 'checkbox' },
|
{ alias: '已清空', currentValue: false, internalId: '3', options: [], tag: 'eln.field.already_clear', type: 'checkbox' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
filler.store.setDraft('eln.field.passed', true);
|
await filler.apply();
|
|
expect(applyFields).toHaveBeenCalledOnce();
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.field.passed', type: 'checkbox', value: true },
|
{ tag: 'eln.field.failed', type: 'checkbox', value: false },
|
]);
|
});
|
|
it('switches the visible step without discarding cross-step drafts', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: false,
|
steps: [
|
{ id: 'step-prepare', name: '准备', order: 1 },
|
{ id: 'step-test', name: '检验', order: 2 },
|
],
|
},
|
controlSteps: {
|
'eln.field.sample': 'step-prepare',
|
'eln.field.result': 'step-test',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const fields = [
|
{ alias: '样品', currentValue: '', internalId: '1', options: [], tag: 'eln.field.sample', type: 'text' as const },
|
{ alias: '结果', currentValue: '', internalId: '2', options: [], tag: 'eln.field.result', type: 'text' as const },
|
];
|
const applyFields = vi.fn().mockImplementation(async (changes) => changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { filler, ready } = fixture({ applyFields, inspectFields: vi.fn().mockResolvedValue(fields) });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(filler.isMultiStep.value).toBe(true);
|
expect(filler.currentStepId.value).toBe('step-prepare');
|
expect(filler.visibleFields.value.map((field) => field.tag)).toEqual(['eln.field.sample']);
|
filler.store.setDraft('eln.field.sample', 'S-001');
|
|
filler.selectStep('step-test');
|
expect(filler.visibleFields.value.map((field) => field.tag)).toEqual(['eln.field.result']);
|
filler.store.setDraft('eln.field.result', '合格');
|
await filler.apply();
|
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.field.sample', type: 'text', value: 'S-001' },
|
{ tag: 'eln.field.result', type: 'text', value: '合格' },
|
]);
|
});
|
|
it('assigns dotted primary field codes and their references to the same step', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: false,
|
steps: [
|
{ id: 'step-test', name: '检验', order: 1 },
|
{ id: 'step-review', name: '复核', order: 2 },
|
],
|
},
|
controlSteps: {
|
'eln.field.inspection.result': 'step-test',
|
'eln.field.review.conclusion': 'step-review',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const { filler, ready } = fixture({
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '检验结果', currentValue: '', internalId: '1', options: [], tag: 'eln.field.inspection.result', type: 'text' },
|
{ alias: '检验结果引用', currentValue: '', internalId: '2', options: [], tag: 'eln.ref.inspection.result.instance_1', type: 'text' },
|
{ alias: '复核结论', currentValue: '', internalId: '3', options: [], tag: 'eln.field.review.conclusion', type: 'text' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(filler.visibleFields.value.map((field) => field.tag)).toEqual([
|
'eln.field.inspection.result',
|
'eln.ref.inspection.result.instance_1',
|
]);
|
filler.selectStep('step-review');
|
expect(filler.visibleFields.value.map((field) => field.tag)).toEqual(['eln.field.review.conclusion']);
|
});
|
|
it('derives each step status from required drafts, repeat minimums, and field errors', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.sample': { required: true, type: 'text', value: '' },
|
},
|
repeatGroups: [{ activeRowCount: 0, groupCode: 'results', maxRows: 3, minRows: 1, rows: [] }],
|
stepStructure: {
|
configuration: {
|
singleStep: false,
|
steps: [
|
{ id: 'step-prepare', name: '准备', order: 1 },
|
{ id: 'step-test', name: '检验', order: 2 },
|
],
|
},
|
controlSteps: { 'eln.field.sample': 'step-prepare' },
|
repeatGroupSteps: { results: 'step-test' },
|
},
|
});
|
const { filler, ready } = fixture({
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '样品', currentValue: '', internalId: '1', options: [], tag: 'eln.field.sample', type: 'text' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(filler.stepProgress.value.map(({ status }) => status)).toEqual(['in-progress', 'in-progress']);
|
filler.store.setDraft('eln.field.sample', 'S-001');
|
expect(filler.stepProgress.value[0]?.status).toBe('complete');
|
filler.store.fieldErrors['eln.field.sample'] = '值不符合选项';
|
expect(filler.stepProgress.value[0]?.status).toBe('problem');
|
delete filler.store.fieldErrors['eln.field.sample'];
|
filler.store.formulaResultsStale = false;
|
filler.store.fields[0]!.formulaStatus = 'ERROR';
|
expect(filler.stepProgress.value[0]?.status).toBe('problem');
|
filler.store.fields[0]!.formulaStatus = 'MISSING_INPUT';
|
expect(filler.stepProgress.value[0]?.status).toBe('in-progress');
|
filler.store.fields[0]!.formulaStatus = 'SUCCESS';
|
expect(filler.stepProgress.value[0]?.status).toBe('complete');
|
});
|
|
it('marks loaded formula results stale after any editable draft changes', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.input': { required: true, type: 'text', value: '2' },
|
'eln.field.result': { formulaStatus: 'SUCCESS', required: true, type: 'text', value: '4' },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: false,
|
steps: [
|
{ id: 'step-input', name: '录入', order: 1 },
|
{ id: 'step-result', name: '计算', order: 2 },
|
],
|
},
|
controlSteps: {
|
'eln.field.input': 'step-input',
|
'eln.field.result': 'step-result',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const { filler, ready } = fixture({
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '输入', currentValue: '2', internalId: '1', options: [], tag: 'eln.field.input', type: 'text' },
|
{ alias: '结果', currentValue: '4', internalId: '2', options: [], tag: 'eln.field.result', type: 'text' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(filler.stepProgress.value.map(({ status }) => status)).toEqual(['complete', 'complete']);
|
filler.store.setDraft('eln.field.input', '3');
|
|
expect(filler.stepProgress.value.map(({ status }) => status)).toEqual(['complete', 'in-progress']);
|
});
|
|
it('absorbs an uncontested controlled-document edit into the sidebar draft', async () => {
|
const initial = { alias: '结果', currentValue: '原值', internalId: '1_2', options: [], tag: 'eln.field.result', type: 'text' as const };
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce([initial])
|
.mockResolvedValueOnce([{ ...initial, currentValue: '文档值' }]);
|
const { changed, filler, ready } = fixture({ inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
changed({ InternalId: '1_2', Tag: 'eln.field.result' });
|
await vi.waitFor(() => expect(filler.store.fields[0]?.draftValue).toBe('文档值'));
|
|
expect(filler.store.fields[0]?.documentValue).toBe('文档值');
|
expect(filler.store.conflicts).toEqual({});
|
});
|
|
it('cancels the selected peer after a grouped checkbox is checked in the document', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const initial = [
|
{ alias: '合格', currentValue: false, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' as const },
|
{ alias: '不合格', currentValue: true, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' as const },
|
];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce([{ ...initial[0]!, currentValue: true }, initial[1]!]);
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { changed, filler, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
changed({ InternalId: 'passed', Tag: 'eln.field.passed' });
|
|
await vi.waitFor(() => expect(applyFields).toHaveBeenCalledOnce());
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.field.failed', type: 'checkbox', value: false },
|
]);
|
expect(filler.store.fields.map((field) => field.draftValue)).toEqual([true, false]);
|
expect(filler.store.fields.map((field) => field.documentValue)).toEqual([true, false]);
|
expect(filler.store.documentSyncError).toBe('');
|
});
|
|
it('keeps existing selections when a direct document check stays within maxSelected', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': {
|
checkboxGroupCode: 'inspection_result', checkboxGroupMaxSelected: 2, checkboxGroupMinSelected: 0, type: 'checkbox', value: false,
|
},
|
'eln.field.passed': {
|
checkboxGroupCode: 'inspection_result', checkboxGroupMaxSelected: 2, checkboxGroupMinSelected: 0, type: 'checkbox', value: true,
|
},
|
},
|
repeatGroups: [],
|
});
|
const initial = [
|
{ alias: '合格', currentValue: true, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' as const },
|
{ alias: '不合格', currentValue: false, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' as const },
|
];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce([initial[0]!, { ...initial[1]!, currentValue: true }]);
|
const applyFields = vi.fn().mockResolvedValue([]);
|
const { changed, filler, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
changed({ InternalId: 'failed', Tag: 'eln.field.failed' });
|
await vi.waitFor(() => expect(filler.store.fields[1]?.documentValue).toBe(true));
|
|
expect(applyFields).not.toHaveBeenCalled();
|
expect(filler.store.fields.map((field) => field.draftValue)).toEqual([true, true]);
|
});
|
|
it('writes back a direct document check that exceeds maxSelected', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': {
|
checkboxGroupCode: 'inspection_result', checkboxGroupMaxSelected: 2, checkboxGroupMinSelected: 0, type: 'checkbox', value: true,
|
},
|
'eln.field.passed': {
|
checkboxGroupCode: 'inspection_result', checkboxGroupMaxSelected: 2, checkboxGroupMinSelected: 0, type: 'checkbox', value: true,
|
},
|
'eln.field.reviewed': {
|
checkboxGroupCode: 'inspection_result', checkboxGroupMaxSelected: 2, checkboxGroupMinSelected: 0, type: 'checkbox', value: false,
|
},
|
},
|
repeatGroups: [],
|
});
|
const initial = [
|
{ alias: '合格', currentValue: true, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' as const },
|
{ alias: '不合格', currentValue: true, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' as const },
|
{ alias: '复核', currentValue: false, internalId: 'reviewed', options: [], tag: 'eln.field.reviewed', type: 'checkbox' as const },
|
];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce([initial[0]!, initial[1]!, { ...initial[2]!, currentValue: true }]);
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { changed, filler, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
changed({ InternalId: 'reviewed', Tag: 'eln.field.reviewed' });
|
await vi.waitFor(() => expect(applyFields).toHaveBeenCalledOnce());
|
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.field.reviewed', type: 'checkbox', value: false },
|
]);
|
expect(filler.store.fields.map((field) => field.draftValue)).toEqual([true, true, false]);
|
expect(filler.store.checkboxGroupNotice).toBe('选择组“inspection_result”最多选择 2 项');
|
});
|
|
it('reports an ambiguous grouped checkbox state on initialization and blocks apply', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const applyFields = vi.fn().mockResolvedValue([]);
|
const { filler, ready } = fixture({
|
applyFields,
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '合格', currentValue: false, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' },
|
{ alias: '不合格', currentValue: false, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(filler.store.checkboxGroupError).toBe('选择组“inspection_result”最多选择 1 项');
|
await filler.apply();
|
expect(applyFields).not.toHaveBeenCalled();
|
});
|
|
it('rejects an ambiguous document checkbox group before restoring persisted values', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const applyFields = vi.fn().mockResolvedValue([]);
|
const { filler, ready } = fixture({
|
applyFields,
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '合格', currentValue: true, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' },
|
{ alias: '不合格', currentValue: true, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' },
|
]),
|
});
|
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(filler.store.checkboxGroupError).toBe('选择组“inspection_result”最多选择 1 项');
|
expect(filler.store.fields.map((field) => field.draftValue)).toEqual([true, true]);
|
expect(applyFields).not.toHaveBeenCalled();
|
});
|
|
it('blocks repeat-row operations while a checkbox group is ambiguous', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const applyFields = vi.fn().mockResolvedValue([]);
|
const { filler, ready } = fixture({
|
applyFields,
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '合格', currentValue: false, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' },
|
{ alias: '不合格', currentValue: false, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
await filler.appendRepeatRow('results');
|
|
expect(applyFields).not.toHaveBeenCalled();
|
expect(appendRepeatRow).not.toHaveBeenCalled();
|
expect(filler.store.repeatError).toBe('请先处理选择组错误');
|
});
|
|
it('re-reads the document and keeps a field error when automatic deselection fails', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const initial = [
|
{ alias: '合格', currentValue: false, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' as const },
|
{ alias: '不合格', currentValue: true, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' as const },
|
];
|
const invalidDocument = [{ ...initial[0]!, currentValue: true }, initial[1]!];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce(invalidDocument)
|
.mockResolvedValueOnce(invalidDocument);
|
const applyFields = vi.fn().mockResolvedValue([
|
{ error: '自动取消失败', ok: false, tag: 'eln.field.failed' },
|
]);
|
const { changed, filler, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
changed({ InternalId: 'passed', Tag: 'eln.field.passed' });
|
|
await vi.waitFor(() => expect(inspectFields).toHaveBeenCalledTimes(3));
|
expect(filler.store.fieldErrors).toEqual({ 'eln.field.failed': '自动取消失败' });
|
expect(filler.store.fields[1]).toMatchObject({
|
currentValue: true,
|
documentValue: true,
|
draftValue: false,
|
});
|
});
|
|
it('enforces independently selected checkbox groups from one debounced document read', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.no': { checkboxGroupCode: 'confirmation', type: 'checkbox', value: true },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
'eln.field.yes': { checkboxGroupCode: 'confirmation', type: 'checkbox', value: false },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.no': 'step-1',
|
'eln.field.passed': 'step-1',
|
'eln.field.yes': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const initial = [
|
{ alias: '合格', currentValue: false, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' as const },
|
{ alias: '不合格', currentValue: true, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' as const },
|
{ alias: '是', currentValue: false, internalId: 'yes', options: [], tag: 'eln.field.yes', type: 'checkbox' as const },
|
{ alias: '否', currentValue: true, internalId: 'no', options: [], tag: 'eln.field.no', type: 'checkbox' as const },
|
];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce([
|
{ ...initial[0]!, currentValue: true },
|
initial[1]!,
|
{ ...initial[2]!, currentValue: true },
|
initial[3]!,
|
]);
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { changed, filler, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
changed({ InternalId: 'passed', Tag: 'eln.field.passed' });
|
changed({ InternalId: 'yes', Tag: 'eln.field.yes' });
|
|
await vi.waitFor(() => expect(applyFields).toHaveBeenCalledOnce());
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.field.failed', type: 'checkbox', value: false },
|
{ tag: 'eln.field.no', type: 'checkbox', value: false },
|
]);
|
});
|
|
it('isolates direct grouped checkbox selection to one repeated runtime row', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.repeat.results.row-1.failed': { checkboxGroupCode: 'row_result', type: 'checkbox', value: true },
|
'eln.repeat.results.row-1.passed': { checkboxGroupCode: 'row_result', type: 'checkbox', value: false },
|
'eln.repeat.results.row-2.failed': { checkboxGroupCode: 'row_result', type: 'checkbox', value: true },
|
'eln.repeat.results.row-2.passed': { checkboxGroupCode: 'row_result', type: 'checkbox', value: false },
|
},
|
repeatGroups: [],
|
});
|
const initial = [
|
{ alias: '第1行合格', currentValue: false, internalId: 'row-1-passed', options: [], tag: 'eln.repeat.results.row-1.passed', type: 'checkbox' as const },
|
{ alias: '第1行不合格', currentValue: true, internalId: 'row-1-failed', options: [], tag: 'eln.repeat.results.row-1.failed', type: 'checkbox' as const },
|
{ alias: '第2行合格', currentValue: false, internalId: 'row-2-passed', options: [], tag: 'eln.repeat.results.row-2.passed', type: 'checkbox' as const },
|
{ alias: '第2行不合格', currentValue: true, internalId: 'row-2-failed', options: [], tag: 'eln.repeat.results.row-2.failed', type: 'checkbox' as const },
|
];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce([{ ...initial[0]!, currentValue: true }, ...initial.slice(1)]);
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { changed, filler, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
changed({ InternalId: 'row-1-passed', Tag: 'eln.repeat.results.row-1.passed' });
|
|
await vi.waitFor(() => expect(applyFields).toHaveBeenCalledOnce());
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.repeat.results.row-1.failed', type: 'checkbox', value: false },
|
]);
|
expect(filler.store.fields.map((field) => field.draftValue)).toEqual([true, false, false, true]);
|
});
|
|
it('delays plugin-generated checkbox events and does not recursively enforce the group', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const initial = [
|
{ alias: '合格', currentValue: false, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' as const },
|
{ alias: '不合格', currentValue: true, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' as const },
|
];
|
const selectedDocument = [{ ...initial[0]!, currentValue: true }, initial[1]!];
|
const enforcedDocument = [selectedDocument[0]!, { ...initial[1]!, currentValue: false }];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce(selectedDocument)
|
.mockResolvedValueOnce(enforcedDocument);
|
let emitPluginChange = () => undefined;
|
const applyFields = vi.fn().mockImplementation(async (changes) => {
|
emitPluginChange();
|
return changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag }));
|
});
|
const fixtureResult = fixture({ applyFields, inspectFields });
|
emitPluginChange = () => fixtureResult.changed({ InternalId: 'failed', Tag: 'eln.field.failed' });
|
fixtureResult.ready();
|
await vi.waitFor(() => expect(fixtureResult.filler.store.status).toBe('ready'));
|
|
fixtureResult.changed({ InternalId: 'passed', Tag: 'eln.field.passed' });
|
|
await vi.waitFor(() => expect(inspectFields).toHaveBeenCalledTimes(3));
|
expect(applyFields).toHaveBeenCalledOnce();
|
expect(fixtureResult.filler.store.fields.map((field) => field.documentValue)).toEqual([true, false]);
|
expect(fixtureResult.filler.store.fields.map((field) => field.draftValue)).toEqual([true, false]);
|
});
|
|
it('preserves unrelated field errors after automatic checkbox deselection succeeds', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.note': { type: 'text', value: '' },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.note': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const initial = [
|
{ alias: '合格', currentValue: false, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' as const },
|
{ alias: '不合格', currentValue: true, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' as const },
|
{ alias: '备注', currentValue: '', internalId: 'note', options: [], tag: 'eln.field.note', type: 'text' as const },
|
];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce([{ ...initial[0]!, currentValue: true }, ...initial.slice(1)]);
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { changed, filler, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
filler.store.fieldErrors['eln.field.note'] = '备注写入失败';
|
|
changed({ InternalId: 'passed', Tag: 'eln.field.passed' });
|
|
await vi.waitFor(() => expect(applyFields).toHaveBeenCalledOnce());
|
expect(filler.store.fieldErrors).toEqual({ 'eln.field.note': '备注写入失败' });
|
});
|
|
it('uses re-read document values when the sidebar already has a pending group selection', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.field.failed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: true },
|
'eln.field.passed': { checkboxGroupCode: 'inspection_result', type: 'checkbox', value: false },
|
},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: true,
|
steps: [{ id: 'step-1', name: '检验', order: 1 }],
|
},
|
controlSteps: {
|
'eln.field.failed': 'step-1',
|
'eln.field.passed': 'step-1',
|
},
|
repeatGroupSteps: {},
|
},
|
});
|
const initial = [
|
{ alias: '合格', currentValue: false, internalId: 'passed', options: [], tag: 'eln.field.passed', type: 'checkbox' as const },
|
{ alias: '不合格', currentValue: true, internalId: 'failed', options: [], tag: 'eln.field.failed', type: 'checkbox' as const },
|
];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce([{ ...initial[0]!, currentValue: true }, initial[1]!]);
|
const applyFields = vi.fn().mockImplementation(async (changes) =>
|
changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { changed, filler, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
filler.store.setDraft('eln.field.passed', true);
|
|
changed({ InternalId: 'passed', Tag: 'eln.field.passed' });
|
|
await vi.waitFor(() => expect(applyFields).toHaveBeenCalledOnce());
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: 'eln.field.failed', type: 'checkbox', value: false },
|
]);
|
});
|
|
it('blocks unified apply for a cross-step document conflict and follows document focus', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: false,
|
steps: [
|
{ id: 'step-first', name: '第一步', order: 1 },
|
{ id: 'step-second', name: '第二步', order: 2 },
|
],
|
},
|
controlSteps: { 'eln.field.first': 'step-first', 'eln.field.second': 'step-second' },
|
repeatGroupSteps: {},
|
},
|
});
|
const initial = [
|
{ alias: '第一项', currentValue: 'A', internalId: '1', options: [], tag: 'eln.field.first', type: 'text' as const },
|
{ alias: '第二项', currentValue: 'B', internalId: '2', options: [], tag: 'eln.field.second', type: 'text' as const },
|
];
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce(initial)
|
.mockResolvedValueOnce([initial[0], { ...initial[1], currentValue: '文档值' }]);
|
const applyFields = vi.fn().mockResolvedValue([]);
|
const { changed, filler, focused, ready } = fixture({ applyFields, inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
filler.store.setDraft('eln.field.second', '侧栏值');
|
|
changed({ InternalId: '2', Tag: 'eln.field.second' });
|
await vi.waitFor(() => expect(filler.conflictCount.value).toBe(1));
|
expect(filler.stepProgress.value.map(({ status }) => status)).toEqual(['in-progress', 'problem']);
|
await filler.apply();
|
expect(applyFields).not.toHaveBeenCalled();
|
|
focused({ InternalId: '2', Tag: 'eln.field.second' });
|
expect(filler.currentStepId.value).toBe('step-second');
|
expect(filler.store.focusedTag).toBe('eln.field.second');
|
});
|
|
it('locates a sidebar field in the document by its stable internal id', async () => {
|
const selectControl = vi.fn().mockResolvedValue(undefined);
|
const { filler, ready } = fixture({ selectControl });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
await filler.focusField('eln.field.name');
|
|
expect(selectControl).toHaveBeenCalledWith('1');
|
expect(filler.store.focusedTag).toBe('eln.field.name');
|
});
|
|
it('locates the first primary control in document order when selecting a directory step', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: false,
|
steps: [{ id: 'step-fill', name: '填写', order: 1 }],
|
},
|
controlSteps: { 'eln.field.name': 'step-fill' },
|
repeatGroupSteps: {},
|
},
|
});
|
const selectControl = vi.fn().mockResolvedValue(undefined);
|
const { filler, ready } = fixture({
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '名称引用', currentValue: '', internalId: 'reference-1', options: [], tag: 'eln.ref.name.instance_1', type: 'text' },
|
{ alias: '名称', currentValue: '', internalId: 'primary-1', options: [], tag: 'eln.field.name', type: 'text' },
|
]),
|
selectControl,
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
await filler.focusFirstFieldInStep('step-fill');
|
|
expect(selectControl).toHaveBeenCalledOnce();
|
expect(selectControl).toHaveBeenCalledWith('primary-1');
|
expect(filler.store.focusedTag).toBe('eln.field.name');
|
});
|
|
it('restores record current state and synchronizes matching references in one document apply', async () => {
|
const applyFields = vi.fn().mockResolvedValue([
|
{ tag: 'eln.field.name', ok: true },
|
{ tag: 'eln.ref.name.instance_1', ok: true },
|
]);
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: { 'eln.field.name': { type: 'text', value: '服务端当前态' } },
|
repeatGroups: [],
|
});
|
const { filler, ready } = fixture({
|
applyFields,
|
getRuntimeOptions: () => ({
|
...context,
|
bizModule: 'eln.record',
|
bizScene: 'eln.record.fill',
|
scope: 'onlyoffice:eln-record-fill',
|
}),
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '名称', currentValue: '过期值', internalId: '1', options: [], tag: 'eln.field.name', type: 'text' },
|
{ alias: '名称引用', currentValue: '过期引用', internalId: '2', options: [], tag: 'eln.ref.name.instance_1', type: 'text' },
|
]),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
expect(filler.store.fields.map((field) => field.draftValue)).toEqual(['服务端当前态', '服务端当前态']);
|
expect(applyFields).toHaveBeenNthCalledWith(1, [
|
{ tag: 'eln.field.name', type: 'text', value: '服务端当前态' },
|
{ tag: 'eln.ref.name.instance_1', type: 'text', value: '服务端当前态' },
|
]);
|
filler.store.setDraft('eln.field.name', '新值');
|
await filler.apply();
|
|
expect(applyFields).toHaveBeenNthCalledWith(2, [
|
{ tag: 'eln.field.name', type: 'text', value: '新值' },
|
{ tag: 'eln.ref.name.instance_1', type: 'text', value: '新值' },
|
]);
|
});
|
|
it('reuses the same operation code when document row cloning is retried', async () => {
|
vi.mocked(appendRepeatRow).mockResolvedValue({ groupCode: 'results', rowId: 'row-1', rowOrder: 1, status: 'PREPARED' });
|
const appendDocumentRow = vi.fn().mockRejectedValueOnce(new Error('文档克隆失败')).mockResolvedValueOnce([
|
'eln.repeat.results.row-1.value',
|
]);
|
const inspectFields = vi.fn().mockResolvedValue([
|
{ alias: '结果', currentValue: '', internalId: 'prototype', options: [], tag: 'eln.repeat.results.prototype.value', type: 'text' },
|
]);
|
const { filler, ready } = fixture({
|
appendRepeatRow: appendDocumentRow,
|
getRuntimeOptions: () => ({
|
...context,
|
bizModule: 'eln.record',
|
bizScene: 'eln.record.fill',
|
scope: 'onlyoffice:eln-record-fill',
|
}),
|
inspectFields,
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
await filler.appendRepeatRow('results');
|
expect(filler.store.repeatError).toBe('文档克隆失败');
|
await filler.appendRepeatRow('results');
|
|
expect(vi.mocked(appendRepeatRow)).toHaveBeenCalledTimes(2);
|
expect(vi.mocked(appendRepeatRow).mock.calls[0]?.[1].operationCode).toBe(
|
vi.mocked(appendRepeatRow).mock.calls[1]?.[1].operationCode,
|
);
|
expect(appendDocumentRow).toHaveBeenCalledTimes(2);
|
expect(filler.store.repeatError).toBe('');
|
expect(filler.store.message).toBe('已新增第 1 行');
|
});
|
|
it('reports an ambiguous checkbox group after a repeated row full refresh', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {
|
'eln.repeat.results.row-1.failed': { checkboxGroupCode: 'row_result', type: 'checkbox', value: false },
|
'eln.repeat.results.row-1.passed': { checkboxGroupCode: 'row_result', type: 'checkbox', value: true },
|
},
|
repeatGroups: [{
|
activeRowCount: 1,
|
groupCode: 'results',
|
maxRows: 3,
|
minRows: 0,
|
rows: [{ groupCode: 'results', rowId: 'row-1', rowOrder: 1, status: 'ACTIVE' }],
|
}],
|
});
|
vi.mocked(appendRepeatRow).mockResolvedValue({
|
groupCode: 'results', rowId: 'row-2', rowOrder: 2, status: 'PREPARED',
|
});
|
const row1 = [
|
{ alias: '第1行合格', currentValue: true, internalId: 'row-1-passed', options: [], tag: 'eln.repeat.results.row-1.passed', type: 'checkbox' as const },
|
{ alias: '第1行不合格', currentValue: false, internalId: 'row-1-failed', options: [], tag: 'eln.repeat.results.row-1.failed', type: 'checkbox' as const },
|
];
|
const row2 = [
|
{ alias: '第2行合格', currentValue: true, internalId: 'row-2-passed', options: [], tag: 'eln.repeat.results.row-2.passed', type: 'checkbox' as const },
|
{ alias: '第2行不合格', currentValue: true, internalId: 'row-2-failed', options: [], tag: 'eln.repeat.results.row-2.failed', type: 'checkbox' as const },
|
];
|
const inspectFields = vi.fn().mockResolvedValueOnce(row1).mockResolvedValueOnce([...row1, ...row2]);
|
const { filler, ready } = fixture({ inspectFields });
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
await filler.appendRepeatRow('results');
|
|
expect(filler.store.checkboxGroupError).toBe('选择组“row_result”最多选择 1 项');
|
});
|
|
it('marks the owning step as a problem when pending drafts fail before a repeat operation', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: {},
|
repeatGroups: [],
|
stepStructure: {
|
configuration: {
|
singleStep: false,
|
steps: [{ id: 'step-fill', name: '填写', order: 1 }],
|
},
|
controlSteps: { 'eln.field.name': 'step-fill' },
|
repeatGroupSteps: {},
|
},
|
});
|
const applyFields = vi.fn().mockRejectedValue(new Error('草稿写入失败'));
|
const { filler, ready } = fixture({
|
applyFields,
|
getRuntimeOptions: () => ({
|
...context,
|
bizModule: 'eln.record',
|
bizScene: 'eln.record.fill',
|
scope: 'onlyoffice:eln-record-fill',
|
}),
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
filler.store.setDraft('eln.field.name', '样品');
|
|
await filler.appendRepeatRow('results');
|
|
expect(vi.mocked(appendRepeatRow)).not.toHaveBeenCalled();
|
expect(filler.store.fieldErrors).toEqual({ 'eln.field.name': '草稿写入失败' });
|
expect(filler.stepProgress.value[0]?.status).toBe('problem');
|
});
|
|
it('restores the server delete intent with the same operation code when document deletion fails', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: { 'eln.repeat.results.row-1.value': { type: 'text', value: '合格' } },
|
repeatGroups: [{
|
activeRowCount: 1, groupCode: 'results', maxRows: 3, minRows: 0,
|
rows: [{ groupCode: 'results', rowId: 'row-1', rowOrder: 1, status: 'ACTIVE' }],
|
}],
|
});
|
vi.mocked(requestDeleteRepeatRow).mockImplementation(async (_context, form) => ({
|
...form, deleteOperationCode: form.operationCode, deleteReason: form.reason, rowOrder: 1, status: 'DELETE_PREPARED',
|
}));
|
vi.mocked(requestRestoreRepeatRow).mockImplementation(async (_context, form) => ({
|
...form, rowOrder: 1, status: 'ACTIVE',
|
}));
|
const removeRepeatRow = vi.fn().mockRejectedValue(new Error('文档删行失败'));
|
const { filler, ready } = fixture({
|
getRuntimeOptions: () => ({ ...context, bizModule: 'eln.record', bizScene: 'eln.record.fill', scope: 'onlyoffice:eln-record-fill' }),
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '结果', currentValue: '', internalId: 'prototype', options: [], tag: 'eln.repeat.results.prototype.value', type: 'text' },
|
{ alias: '结果', currentValue: '合格', internalId: 'row-1', options: [], tag: 'eln.repeat.results.row-1.value', type: 'text' },
|
]),
|
removeRepeatRow,
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
await filler.deleteRepeatRow('results', 'row-1', '录入错误');
|
|
const deleteCode = vi.mocked(requestDeleteRepeatRow).mock.calls[0]?.[1].operationCode;
|
expect(vi.mocked(requestRestoreRepeatRow).mock.calls[0]?.[1].operationCode).toBe(deleteCode);
|
expect(filler.store.repeatError).toBe('文档删行失败');
|
expect(filler.store.repeatGroups[0]?.rows[0]?.status).toBe('ACTIVE');
|
});
|
|
it('recreates and refills a missing pending-delete row before restoring server state', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: { 'eln.repeat.results.row-1.value': { type: 'text', value: '待恢复' } },
|
repeatGroups: [{
|
activeRowCount: 1, groupCode: 'results', maxRows: 3, minRows: 0,
|
rows: [{
|
deleteOperationCode: 'delete-1', deleteReason: '录入错误', groupCode: 'results',
|
rowId: 'row-1', rowOrder: 1, status: 'DELETE_PREPARED',
|
}],
|
}],
|
});
|
vi.mocked(requestRestoreRepeatRow).mockResolvedValue({
|
groupCode: 'results', rowId: 'row-1', rowOrder: 1, status: 'ACTIVE',
|
});
|
const prototype = { alias: '结果', currentValue: '', internalId: 'prototype', options: [], tag: 'eln.repeat.results.prototype.value', type: 'text' as const };
|
const runtime = { ...prototype, currentValue: '', internalId: 'row-1', tag: 'eln.repeat.results.row-1.value' };
|
const inspectFields = vi.fn().mockResolvedValueOnce([prototype]).mockResolvedValueOnce([prototype, runtime]);
|
const appendDocumentRow = vi.fn().mockResolvedValue(['eln.repeat.results.row-1.value']);
|
const applyFields = vi.fn().mockResolvedValue([{ ok: true, tag: runtime.tag }]);
|
const { filler, ready } = fixture({
|
appendRepeatRow: appendDocumentRow,
|
applyFields,
|
getRuntimeOptions: () => ({ ...context, bizModule: 'eln.record', bizScene: 'eln.record.fill', scope: 'onlyoffice:eln-record-fill' }),
|
inspectFields,
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
|
await filler.restoreRepeatRow('results', 'row-1');
|
|
expect(appendDocumentRow).toHaveBeenCalledWith('results', 'row-1');
|
expect(applyFields).toHaveBeenCalledWith([
|
{ tag: runtime.tag, type: 'text', value: '待恢复' },
|
]);
|
expect(vi.mocked(requestRestoreRepeatRow)).toHaveBeenCalledWith(expect.anything(), {
|
groupCode: 'results', operationCode: 'delete-1', rowId: 'row-1',
|
});
|
expect(filler.store.repeatGroups[0]?.rows[0]?.status).toBe('ACTIVE');
|
});
|
|
it('restores the latest in-memory row values after deleting and restoring in one session', async () => {
|
vi.mocked(fetchFillData).mockResolvedValue({
|
fields: { 'eln.repeat.results.row-1.value': { type: 'text', value: '服务端旧值' } },
|
repeatGroups: [{
|
activeRowCount: 1, groupCode: 'results', maxRows: 3, minRows: 0,
|
rows: [{ groupCode: 'results', rowId: 'row-1', rowOrder: 1, status: 'ACTIVE' }],
|
}],
|
});
|
vi.mocked(requestDeleteRepeatRow).mockImplementation(async (_context, form) => ({
|
...form, deleteOperationCode: form.operationCode, deleteReason: form.reason,
|
rowOrder: 1, status: 'DELETE_PREPARED',
|
}));
|
vi.mocked(requestRestoreRepeatRow).mockResolvedValue({
|
groupCode: 'results', rowId: 'row-1', rowOrder: 1, status: 'ACTIVE',
|
});
|
const prototype = { alias: '结果', currentValue: '', internalId: 'prototype', options: [], tag: 'eln.repeat.results.prototype.value', type: 'text' as const };
|
const runtime = { ...prototype, currentValue: '服务端旧值', internalId: 'row-1', tag: 'eln.repeat.results.row-1.value' };
|
const restoredRuntime = { ...runtime, currentValue: '' };
|
const inspectFields = vi.fn()
|
.mockResolvedValueOnce([prototype, runtime])
|
.mockResolvedValueOnce([prototype])
|
.mockResolvedValueOnce([prototype, restoredRuntime]);
|
const applyFields = vi.fn().mockImplementation(async (changes) => changes.map((change: { tag: string }) => ({ ok: true, tag: change.tag })));
|
const { filler, ready } = fixture({
|
applyFields,
|
getRuntimeOptions: () => ({ ...context, bizModule: 'eln.record', bizScene: 'eln.record.fill', scope: 'onlyoffice:eln-record-fill' }),
|
inspectFields,
|
});
|
ready();
|
await vi.waitFor(() => expect(filler.store.status).toBe('ready'));
|
filler.store.setDraft(runtime.tag, '未保存新值');
|
|
await filler.deleteRepeatRow('results', 'row-1', '重新录入');
|
await filler.restoreRepeatRow('results', 'row-1');
|
|
expect(applyFields).toHaveBeenNthCalledWith(1, [{ tag: runtime.tag, type: 'text', value: '未保存新值' }]);
|
expect(applyFields).toHaveBeenNthCalledWith(2, [{ tag: runtime.tag, type: 'text', value: '未保存新值' }]);
|
expect(filler.store.fields.find((field) => field.tag === runtime.tag)?.draftValue).toBe('未保存新值');
|
});
|
});
|