| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- const fs = require('fs');
- const vm = require('vm');
- const babel = require('@babel/core');
- const source = fs.readFileSync(
- 'src/views/productionPlan/components/bomCompareDialog.logic.js',
- 'utf8'
- );
- const apiSource = fs.readFileSync(
- 'src/api/productionPlan/bomCompare.js',
- 'utf8'
- );
- function deferred() {
- let resolve;
- let reject;
- const promise = new Promise((res, rej) => {
- resolve = res;
- reject = rej;
- });
- return { promise, resolve, reject };
- }
- function loadMethods(api) {
- const compiled = babel.transformSync(source, {
- filename: 'bomCompareDialog.logic.js',
- babelrc: false,
- configFile: false,
- plugins: ['@babel/plugin-transform-modules-commonjs']
- }).code;
- const module = { exports: {} };
- const sandbox = {
- module,
- exports: module.exports,
- Blob,
- console,
- Promise,
- Map,
- Set,
- Date,
- String,
- Number,
- Array,
- Object,
- setTimeout,
- clearTimeout,
- window: { setTimeout, clearTimeout },
- require(id) {
- if (id === '@/api/productionPlan') {
- return { bomVersionList: api.bomVersionList };
- }
- if (id === '@/api/productionPlan/bomCompare') return api;
- throw new Error(`unexpected import: ${id}`);
- }
- };
- vm.runInNewContext(compiled, sandbox, {
- filename: 'bomCompareDialog.logic.js'
- });
- return module.exports.default;
- }
- function loadApi(request) {
- const compiled = babel.transformSync(apiSource, {
- filename: 'bomCompare.js',
- babelrc: false,
- configFile: false,
- plugins: ['@babel/plugin-transform-modules-commonjs']
- }).code;
- const module = { exports: {} };
- const sandbox = {
- module,
- exports: module.exports,
- Blob,
- Promise,
- Error,
- JSON,
- Number,
- String,
- decodeURIComponent,
- require(id) {
- if (id === '@/utils/request')
- return { __esModule: true, default: request };
- throw new Error(`unexpected import: ${id}`);
- }
- };
- vm.runInNewContext(compiled, sandbox, { filename: 'bomCompare.js' });
- return module.exports;
- }
- function createContext(methods) {
- const messages = { success: [], error: [], warning: [] };
- const ctx = {
- ...methods,
- visible: true,
- comparing: false,
- optionsLoading: false,
- detailLoading: false,
- exportingDifferences: false,
- exportingTree: false,
- compareLevel: 'multiple',
- compareMode: 'detail',
- showOnlyDifferences: false,
- showReport: true,
- keyword: '',
- activeDifferenceType: '',
- currentPage: 1,
- pageSize: 5,
- detailTotal: 0,
- taskId: '',
- taskStatus: '',
- taskMessage: '',
- requestSequence: 0,
- resultRequestSequence: 0,
- pollTimer: null,
- pollResolve: null,
- filterTimer: null,
- plan: { categoryId: 'category-1', deptId: 'dept-1' },
- source: { bomId: 'pbom-1', categoryId: 'category-1', version: 'V1.0' },
- target: { bomId: 'mbom-1', categoryId: 'category-1', version: 'V2.0' },
- sourceOptions: [],
- targetOptions: [],
- sourceTree: [],
- targetTree: [],
- differences: [],
- summary: { totalCount: 0 },
- pendingChanges: false,
- lastComparedAt: '',
- $message: {
- success(value) {
- messages.success.push(value);
- },
- error(value) {
- messages.error.push(value);
- },
- warning(value) {
- messages.warning.push(value);
- }
- },
- $nextTick(fn) {
- if (fn) fn();
- },
- handleTreeNodeClick() {},
- scrollNodeIntoView() {},
- downloadBlob(file) {
- this.downloaded = file;
- }
- };
- ctx.messages = messages;
- return ctx;
- }
- function assert(condition, message) {
- if (!condition) throw new Error(message);
- }
- function treeFixture() {
- return {
- baseBom: { bomId: 'pbom-1', code: 'P', name: 'PBOM', version: 'V1' },
- targetBom: { bomId: 'mbom-1', code: 'M', name: 'MBOM', version: 'V2' },
- baseTree: [
- {
- nodeKey: 'root',
- materialCode: 'ROOT',
- materialName: '根节点',
- differenceType: 'EQUAL',
- children: [
- {
- nodeKey: 'added',
- materialCode: 'A',
- materialName: '新增件占位',
- differenceType: 'ADDED',
- nodeExists: false
- }
- ]
- }
- ],
- targetTree: [
- {
- nodeKey: 'root',
- materialCode: 'ROOT',
- materialName: '根节点',
- differenceType: 'EQUAL',
- children: [
- {
- nodeKey: 'added',
- materialCode: 'A',
- materialName: '新增件',
- differenceType: 'ADDED',
- nodeExists: true
- }
- ]
- }
- ],
- summary: { addedCount: 1, totalCount: 1 }
- };
- }
- function pageFixture() {
- return {
- count: 1,
- current: 1,
- list: [
- {
- nodeKey: 'added',
- differenceType: 'ADDED',
- materialCode: 'A',
- materialName: '新增件'
- }
- ]
- };
- }
- async function run() {
- const results = [];
- const test = async (name, fn) => {
- await fn();
- results.push(name);
- };
- await test('任务成功后树、分页、汇总和占位节点闭环', async () => {
- const api = {
- startBomCompare: async () => ({ taskId: 'task-1', status: 'PENDING' }),
- getBomCompareTask: async () => ({ taskId: 'task-1', status: 'SUCCESS' }),
- getBomCompareTree: async () => treeFixture(),
- getBomComparePage: async () => pageFixture(),
- getBomCompareSummary: async () => ({ addedCount: 1, totalCount: 99 }),
- exportBomCompareDifferences: async () => ({}),
- exportBomCompareTree: async () => ({}),
- bomVersionList: async () => []
- };
- const ctx = createContext(loadMethods(api));
- await ctx.runCompare();
- assert(ctx.taskStatus === 'SUCCESS', 'success status not closed');
- assert(ctx.summary.totalCount === 1, 'summary total was not reconciled');
- assert(
- ctx.detailTotal === 1 && ctx.differences[0].type === 'added',
- 'page not normalized'
- );
- assert(
- ctx.sourceTree[0].children[0].nodeExists === false,
- 'placeholder node lost'
- );
- assert(
- ctx.sourceTree[0].children[0].nodeKey ===
- ctx.targetTree[0].children[0].nodeKey,
- 'tree keys not aligned'
- );
- });
- await test('任务失败进入可重试终态', async () => {
- const api = {
- startBomCompare: async () => ({ taskId: 'task-2', status: 'PENDING' }),
- getBomCompareTask: async () => ({
- status: 'failed',
- message: '后端比较失败'
- }),
- bomVersionList: async () => []
- };
- const ctx = createContext(loadMethods(api));
- await ctx.runCompare();
- assert(
- ctx.taskStatus === 'FAILED' && ctx.pendingChanges,
- 'failed task not closed'
- );
- assert(
- ctx.messages.error.includes('后端比较失败'),
- 'failed message missing'
- );
- });
- await test('轮询瞬时失败可恢复', async () => {
- let calls = 0;
- const api = {
- startBomCompare: async () => ({ taskId: 'task-3', status: 'PENDING' }),
- getBomCompareTask: async () => {
- calls += 1;
- if (calls === 1) throw new Error('temporary');
- return { status: 'SUCCESS' };
- },
- getBomCompareTree: async () => treeFixture(),
- getBomComparePage: async () => pageFixture(),
- getBomCompareSummary: async () => ({ addedCount: 1, totalCount: 1 }),
- bomVersionList: async () => []
- };
- const ctx = createContext(loadMethods(api));
- ctx.waitForNextPoll = async () => {};
- await ctx.runCompare();
- assert(
- calls === 2 && ctx.taskStatus === 'SUCCESS',
- 'poll retry did not recover'
- );
- });
- await test('结果加载失败不会误报任务失败', async () => {
- const api = {
- startBomCompare: async () => ({ taskId: 'task-4', status: 'PENDING' }),
- getBomCompareTask: async () => ({ status: 'SUCCESS' }),
- getBomCompareTree: async () => {
- throw new Error('结果树暂不可用');
- },
- getBomComparePage: async () => pageFixture(),
- getBomCompareSummary: async () => ({ addedCount: 1, totalCount: 1 }),
- bomVersionList: async () => []
- };
- const ctx = createContext(loadMethods(api));
- await ctx.runCompare();
- assert(ctx.taskStatus === 'LOAD_FAILED', 'result failure misclassified');
- assert(
- !ctx.pendingChanges && !ctx.comparing,
- 'result failure not terminal'
- );
- });
- await test('关闭弹窗后版本请求不会回写或发起比较', async () => {
- const source = deferred();
- const target = deferred();
- let versionCalls = 0;
- let starts = 0;
- const api = {
- bomVersionList: async () => {
- versionCalls += 1;
- return versionCalls === 1 ? source.promise : target.promise;
- },
- startBomCompare: async () => {
- starts += 1;
- return { taskId: 'never' };
- }
- };
- const ctx = createContext(loadMethods(api));
- const opening = ctx.open({ categoryId: 'category-1' });
- ctx.close();
- source.resolve([{ id: 'p', status: 1 }]);
- target.resolve([{ id: 'm', status: 1 }]);
- await opening;
- assert(!ctx.visible && starts === 0, 'closed dialog started a task');
- assert(
- ctx.sourceOptions.length === 0 && ctx.targetOptions.length === 0,
- 'closed dialog was mutated'
- );
- });
- await test('快速筛选仅接受最新响应', async () => {
- const firstTree = deferred();
- const firstPage = deferred();
- let treeCalls = 0;
- let pageCalls = 0;
- const api = {
- getBomCompareTree: async () => {
- treeCalls += 1;
- return treeCalls === 1
- ? firstTree.promise
- : { ...treeFixture(), baseTree: [] };
- },
- getBomComparePage: async () => {
- pageCalls += 1;
- return pageCalls === 1
- ? firstPage.promise
- : { count: 0, current: 1, list: [] };
- },
- getBomCompareSummary: async () => ({ addedCount: 1, totalCount: 1 }),
- bomVersionList: async () => []
- };
- const ctx = createContext(loadMethods(api));
- ctx.taskId = 'task-5';
- ctx.taskStatus = 'SUCCESS';
- const first = ctx.loadCompareResult();
- const second = ctx.loadCompareResult();
- await second;
- firstTree.resolve(treeFixture());
- firstPage.resolve(pageFixture());
- await first;
- assert(
- ctx.detailTotal === 0 && ctx.sourceTree.length === 0,
- 'stale filter response overwrote latest'
- );
- });
- await test('无差异的成功任务仍可导出结果树', async () => {
- let exported = false;
- const api = {
- exportBomCompareTree: async () => {
- exported = true;
- return { blob: new Blob(['tree']), fileName: 'tree.xlsx' };
- },
- bomVersionList: async () => []
- };
- const ctx = createContext(loadMethods(api));
- ctx.taskId = 'task-6';
- ctx.taskStatus = 'SUCCESS';
- ctx.summary = { totalCount: 0 };
- await ctx.exportTree();
- assert(
- exported && ctx.downloaded.fileName === 'tree.xlsx',
- 'zero-difference tree export blocked'
- );
- });
- await test('关闭执行中的轮询可立即收口', async () => {
- const api = {
- startBomCompare: async () => ({ taskId: 'task-7', status: 'PENDING' }),
- getBomCompareTask: async () => ({ status: 'RUNNING' }),
- bomVersionList: async () => []
- };
- const ctx = createContext(loadMethods(api));
- const running = ctx.runCompare();
- for (let index = 0; index < 20 && !ctx.pollResolve; index += 1) {
- await Promise.resolve();
- }
- assert(Boolean(ctx.pollResolve), 'poll wait was not entered');
- ctx.close();
- await Promise.race([
- running,
- new Promise((_, reject) =>
- setTimeout(
- () => reject(new Error('cancelled poll did not settle')),
- 100
- )
- )
- ]);
- assert(
- !ctx.visible && !ctx.comparing,
- 'closed polling task remained active'
- );
- });
- await test('导出接口识别伪 Blob 错误并解析文件名', async () => {
- let response = {
- data: new Blob([JSON.stringify({ message: '导出参数错误' })], {
- type: 'application/octet-stream'
- }),
- headers: {}
- };
- const api = loadApi({
- post: async () => response,
- get: async () => response
- });
- let message = '';
- try {
- await api.exportBomCompareTree({ taskId: 'task-8' });
- } catch (error) {
- message = error.message;
- }
- assert(
- message === '导出参数错误',
- 'octet-stream JSON error was downloaded'
- );
- response = {
- data: new Blob(['excel']),
- headers: {
- 'content-disposition':
- "attachment; filename*=UTF-8''BOM%E6%AF%94%E8%BE%83.xlsx"
- }
- };
- const file = await api.exportBomCompareTree({ taskId: 'task-8' });
- assert(file.fileName === 'BOM比较.xlsx', 'export filename was not decoded');
- const rejectedApi = loadApi({
- post: async () => {
- const error = new Error('http 500');
- error.response = {
- data: new Blob([JSON.stringify({ message: '导出服务异常' })]),
- headers: {}
- };
- throw error;
- }
- });
- message = '';
- try {
- await rejectedApi.exportBomCompareTree({ taskId: 'task-8' });
- } catch (error) {
- message = error.message;
- }
- assert(message === '导出服务异常', 'HTTP Blob error was not decoded');
- });
- console.log(`PASS ${results.length}`);
- results.forEach((name) => console.log(`- ${name}`));
- }
- run().catch((error) => {
- console.error(`FAIL: ${error.message}`);
- process.exitCode = 1;
- });
|