/* @vitest-environment happy-dom */
|
|
import type { SharedFillPreview } from '#/api/x/eln/records';
|
|
import { flushPromises, mount } from '@vue/test-utils';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import SharedFillView from '../index.vue';
|
import { buildSharedFillCommand, ElnSharedFillParamError, resolveTestWorkItemIds } from '../sharedFillView';
|
|
const apiMocks = vi.hoisted(() => ({
|
createRecordsBatch: vi.fn(),
|
executeSharedFill: vi.fn(),
|
previewSharedFill: vi.fn(),
|
}));
|
const onlineMocks = vi.hoisted(() => ({ getViewParam: vi.fn() }));
|
|
vi.mock('#/api/x/eln/records', () => ({
|
createRecordsBatch: apiMocks.createRecordsBatch,
|
executeSharedFill: apiMocks.executeSharedFill,
|
previewSharedFill: apiMocks.previewSharedFill,
|
}));
|
vi.mock('#/utils/jnpf', () => ({ onlineUtils: onlineMocks }));
|
|
const PREVIEW: SharedFillPreview = {
|
fields: [
|
{
|
conflictingValues: false,
|
dataType: 'DECIMAL',
|
defaultExcludedTestWorkItemIds: [],
|
fieldCode: 'temperature',
|
fieldName: '环境温度',
|
fieldVersionId: 'version-temperature',
|
targetTestWorkItemIds: ['item-1', 'item-2'],
|
unit: 'C',
|
},
|
{
|
conflictingValues: true,
|
dataType: 'DECIMAL',
|
defaultExcludedTestWorkItemIds: ['item-2'],
|
fieldCode: 'pressure',
|
fieldName: '环境压力',
|
fieldVersionId: 'version-pressure',
|
targetTestWorkItemIds: ['item-1', 'item-2'],
|
unit: 'kPa',
|
},
|
],
|
previewToken: 'preview-1',
|
testDefinitionCode: 'IGNITION_RESIDUE',
|
testDefinitionId: 'definition-1',
|
testDefinitionName: '炽灼残渣',
|
targets: [
|
{
|
currentValues: { 'version-pressure': '', 'version-temperature': '' },
|
eligible: true,
|
recordId: 'record-1',
|
recordVersion: 3,
|
revisionId: 'revision-1',
|
testWorkItemId: 'item-1',
|
testWorkItemName: '炽灼残渣',
|
},
|
{
|
currentValues: { 'version-pressure': '100', 'version-temperature': '' },
|
eligible: true,
|
recordId: 'record-2',
|
recordVersion: 5,
|
revisionId: 'revision-2',
|
testWorkItemId: 'item-2',
|
testWorkItemName: '炽灼残渣',
|
},
|
],
|
};
|
|
describe('eLN shared fill view', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
onlineMocks.getViewParam.mockReturnValue(['item-1', 'item-2']);
|
apiMocks.previewSharedFill.mockResolvedValue({ data: PREVIEW });
|
apiMocks.createRecordsBatch.mockResolvedValue({ data: { items: [] } });
|
apiMocks.executeSharedFill.mockResolvedValue({
|
data: { fieldVersionIds: ['version-temperature'], operationCode: '', targets: [] },
|
});
|
});
|
|
it('requires at least two unique test work items', () => {
|
expect(resolveTestWorkItemIds([' item-1 ', 'item-2', 'item-1'])).toEqual(['item-1', 'item-2']);
|
expect(() => resolveTestWorkItemIds(['item-1'])).toThrow(ElnSharedFillParamError);
|
});
|
|
it('builds one multi-field request and requires a reason for a reselected target', () => {
|
expect(() =>
|
buildSharedFillCommand(PREVIEW, {
|
fieldValues: { 'version-pressure': '101.325', 'version-temperature': '20.5' },
|
operationCode: 'shared-fill-1',
|
overwriteReasons: {},
|
selectedFieldIds: ['version-temperature', 'version-pressure'],
|
selectedTargetIds: ['item-1', 'item-2'],
|
}),
|
).toThrow('请填写 item-2 的覆盖原因');
|
|
expect(
|
buildSharedFillCommand(PREVIEW, {
|
fieldValues: { 'version-pressure': '101.325', 'version-temperature': '20.5' },
|
operationCode: 'shared-fill-1',
|
overwriteReasons: { 'item-2': '复测后统一修正' },
|
selectedFieldIds: ['version-temperature', 'version-pressure'],
|
selectedTargetIds: ['item-1', 'item-2'],
|
}),
|
).toEqual({
|
operationCode: 'shared-fill-1',
|
previewToken: 'preview-1',
|
testWorkItemIds: ['item-1', 'item-2'],
|
targets: [
|
{ expectedRecordVersion: 3, overwrite: false, recordId: 'record-1', testWorkItemId: 'item-1' },
|
{
|
expectedRecordVersion: 5,
|
overwrite: true,
|
overwriteReason: '复测后统一修正',
|
recordId: 'record-2',
|
testWorkItemId: 'item-2',
|
},
|
],
|
values: [
|
{ fieldVersionId: 'version-temperature', value: '20.5' },
|
{ fieldVersionId: 'version-pressure', value: '101.325' },
|
],
|
});
|
});
|
|
it('does not require an overwrite reason when the requested decimal equals the target value', () => {
|
const command = buildSharedFillCommand(PREVIEW, {
|
fieldValues: { 'version-pressure': '100.0' },
|
operationCode: 'shared-fill-same-value',
|
overwriteReasons: {},
|
selectedFieldIds: ['version-pressure'],
|
selectedTargetIds: ['item-2'],
|
});
|
|
expect(command.targets).toEqual([{ expectedRecordVersion: 5, overwrite: false, recordId: 'record-2', testWorkItemId: 'item-2' }]);
|
});
|
|
it('previews the selected test work items on mount', async () => {
|
const wrapper = mount(SharedFillView);
|
await flushPromises();
|
|
expect(apiMocks.previewSharedFill).toHaveBeenCalledWith({ testWorkItemIds: ['item-1', 'item-2'] });
|
expect(wrapper.get('[data-test="shared-fill-fields"]').text()).toContain('环境温度');
|
expect(wrapper.text()).toContain('检验项目 炽灼残渣');
|
expect(wrapper.get('[data-test="shared-fill-targets"]').text()).toContain('炽灼残渣');
|
expect(wrapper.text()).not.toContain('definition-1');
|
expect(wrapper.get('[data-test="shared-fill-targets"]').text()).not.toContain('item-2');
|
});
|
|
it('uses the shared alert spacing when preview fails', async () => {
|
apiMocks.previewSharedFill.mockRejectedValueOnce(new Error('批量填写只允许选择同一检验项目'));
|
const wrapper = mount(SharedFillView);
|
await flushPromises();
|
|
expect(wrapper.get('[data-test="shared-fill-error"]').classes()).toContain('shared-fill__alert');
|
});
|
|
it('creates only missing records and refreshes the preview', async () => {
|
const missingPreview: SharedFillPreview = {
|
...PREVIEW,
|
fields: [],
|
targets: [
|
{
|
currentValues: {},
|
eligible: false,
|
errorCode: 'RECORD_NOT_FOUND',
|
errorMessage: '检验项电子记录不存在',
|
testWorkItemId: 'item-1',
|
testWorkItemName: '炽灼残渣',
|
},
|
PREVIEW.targets[1]!,
|
],
|
};
|
apiMocks.previewSharedFill.mockResolvedValueOnce({ data: missingPreview }).mockResolvedValueOnce({ data: PREVIEW });
|
apiMocks.createRecordsBatch.mockResolvedValue({
|
data: {
|
items: [
|
{
|
created: { document: { fileId: 'file-1', name: 'record.docx' }, existing: false, recordId: 'record-1', revisionId: 'revision-1' },
|
status: 'CREATED',
|
testWorkItemId: 'item-1',
|
},
|
],
|
},
|
});
|
const randomUUID = vi.spyOn(globalThis.crypto, 'randomUUID').mockReturnValue('00000000-0000-4000-8000-000000000003');
|
const wrapper = mount(SharedFillView);
|
await flushPromises();
|
|
expect(wrapper.get('[data-test="shared-fill-missing-records"]').text()).toContain('1 条检验项尚未创建电子记录');
|
await wrapper.get('[data-test="shared-fill-create-missing"]').trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.createRecordsBatch).toHaveBeenCalledWith({
|
operationCode: 'record-batch-00000000-0000-4000-8000-000000000003',
|
testWorkItemIds: ['item-1'],
|
});
|
expect(apiMocks.previewSharedFill).toHaveBeenCalledTimes(2);
|
expect(wrapper.find('[data-test="shared-fill-missing-records"]').exists()).toBe(false);
|
randomUUID.mockRestore();
|
});
|
|
it('keeps per-record creation failures after refreshing successful records', async () => {
|
const missingTarget = (testWorkItemId: string) => ({
|
currentValues: {},
|
eligible: false,
|
errorCode: 'RECORD_NOT_FOUND',
|
errorMessage: '检验项电子记录不存在',
|
testWorkItemId,
|
testWorkItemName: '炽灼残渣',
|
});
|
const missingPreview: SharedFillPreview = {
|
...PREVIEW,
|
fields: [],
|
targets: [missingTarget('item-1'), missingTarget('item-2')],
|
};
|
const refreshedPreview: SharedFillPreview = {
|
...PREVIEW,
|
targets: [PREVIEW.targets[0]!, missingTarget('item-2')],
|
};
|
apiMocks.previewSharedFill.mockResolvedValueOnce({ data: missingPreview }).mockResolvedValueOnce({ data: refreshedPreview });
|
apiMocks.createRecordsBatch.mockResolvedValue({
|
data: {
|
items: [
|
{
|
created: { document: { fileId: 'file-1', name: 'record.docx' }, existing: false, recordId: 'record-1', revisionId: 'revision-1' },
|
status: 'CREATED',
|
testWorkItemId: 'item-1',
|
},
|
{ errorMessage: '未配置可用模板', status: 'FAILED', testWorkItemId: 'item-2' },
|
],
|
},
|
});
|
const randomUUID = vi.spyOn(globalThis.crypto, 'randomUUID').mockReturnValue('00000000-0000-4000-8000-000000000004');
|
const wrapper = mount(SharedFillView);
|
await flushPromises();
|
|
await wrapper.get('[data-test="shared-fill-create-missing"]').trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.previewSharedFill).toHaveBeenCalledTimes(2);
|
expect(wrapper.get('[data-test="shared-fill-error"]').text()).toContain('炽灼残渣(item-2):未配置可用模板');
|
expect(wrapper.get('[data-test="shared-fill-missing-records"]').text()).toContain('1 条检验项尚未创建电子记录');
|
randomUUID.mockRestore();
|
});
|
|
it('reuses an operation code for a retry and rotates it after a new preview', async () => {
|
const randomUUID = vi
|
.spyOn(globalThis.crypto, 'randomUUID')
|
.mockReturnValueOnce('00000000-0000-4000-8000-000000000001')
|
.mockReturnValueOnce('00000000-0000-4000-8000-000000000002');
|
const wrapper = mount(SharedFillView);
|
await flushPromises();
|
|
const fieldCheckbox = wrapper.findAll('input[type="checkbox"]')[0]!;
|
await fieldCheckbox.setValue(true);
|
await wrapper.get('input[placeholder="填写统一值"]').setValue('20.5');
|
const confirm = wrapper.findAll('button').find((button) => button.text().includes('确认填写'))!;
|
|
await confirm.trigger('click');
|
await flushPromises();
|
await confirm.trigger('click');
|
await flushPromises();
|
|
const firstCode = apiMocks.executeSharedFill.mock.calls[0]![0].operationCode;
|
expect(apiMocks.executeSharedFill.mock.calls[1]![0].operationCode).toBe(firstCode);
|
|
const reload = wrapper.findAll('button').find((button) => button.text().includes('重新预览'))!;
|
await reload.trigger('click');
|
await flushPromises();
|
await wrapper.findAll('input[type="checkbox"]')[0]!.setValue(true);
|
await wrapper.get('input[placeholder="填写统一值"]').setValue('20.5');
|
const nextConfirm = wrapper.findAll('button').find((button) => button.text().includes('确认填写'))!;
|
await nextConfirm.trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.executeSharedFill.mock.calls[2]![0].operationCode).not.toBe(firstCode);
|
randomUUID.mockRestore();
|
});
|
});
|