Files
DashBoard/frontend/tests/query-tool-composables.test.js

240 lines
7.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { useEquipmentQuery } from '../src/query-tool/composables/useEquipmentQuery.js';
import { useLotDetail } from '../src/query-tool/composables/useLotDetail.js';
import { useLotLineage } from '../src/query-tool/composables/useLotLineage.js';
import { useLotResolve } from '../src/query-tool/composables/useLotResolve.js';
function setupWindowMesApi({ get, post } = {}) {
const originalWindow = globalThis.window;
const originalDocument = globalThis.document;
globalThis.window = {
MesApi: {
get: get || (async () => ({})),
post: post || (async () => ({})),
},
setTimeout: globalThis.setTimeout.bind(globalThis),
clearTimeout: globalThis.clearTimeout.bind(globalThis),
};
globalThis.document = {
querySelector: () => null,
};
return () => {
globalThis.window = originalWindow;
globalThis.document = originalDocument;
};
}
test('useLotResolve validates multi-query size and resolves deduplicated inputs', async () => {
const postCalls = [];
const restore = setupWindowMesApi({
post: async (url, payload) => {
postCalls.push({ url, payload });
return {
data: payload.values.map((value, index) => ({
container_id: `CID-${index + 1}`,
input_value: value,
})),
not_found: [],
expansion_info: {},
};
},
});
try {
const resolver = useLotResolve({
inputType: 'work_order',
allowedTypes: ['work_order', 'lot_id'],
});
resolver.setInputText(Array.from({ length: 51 }, (_, idx) => `WO-${idx + 1}`).join('\n'));
const overLimit = await resolver.resolveLots();
assert.equal(overLimit.ok, false);
assert.equal(overLimit.reason, 'validation');
assert.match(resolver.errorMessage.value, /50/);
assert.equal(postCalls.length, 0);
resolver.setInputText('WO-001\nWO-001, WO-002');
const resolved = await resolver.resolveLots();
assert.equal(resolved.ok, true);
assert.equal(postCalls.length, 1);
assert.deepEqual(postCalls[0].payload.values, ['WO-001', 'WO-002']);
assert.equal(resolver.resolvedLots.value.length, 2);
assert.match(resolver.successMessage.value, /解析完成/);
} finally {
restore();
}
});
test('useLotLineage deduplicates in-flight lineage requests and stores graph data', async () => {
const postCalls = [];
const restore = setupWindowMesApi({
post: async (url, payload) => {
postCalls.push({ url, payload });
await new Promise((resolve) => setTimeout(resolve, 15));
return {
roots: ['CID-ROOT'],
children_map: {
'CID-ROOT': ['CID-CHILD'],
'CID-CHILD': [],
},
names: {
'CID-ROOT': 'LOT-ROOT',
'CID-CHILD': 'LOT-CHILD',
},
nodes: {
'CID-ROOT': { container_name: 'LOT-ROOT', container_id: 'CID-ROOT' },
'CID-CHILD': { container_name: 'LOT-CHILD', container_id: 'CID-CHILD' },
},
edges: [
{
from_cid: 'CID-ROOT',
to_cid: 'CID-CHILD',
edge_type: 'split',
},
],
leaf_serials: {
'CID-CHILD': ['SN-001'],
},
};
},
});
try {
const lineage = useLotLineage();
await Promise.all([
lineage.fetchLineage(['CID-ROOT']),
lineage.fetchLineage(['CID-ROOT']),
]);
assert.equal(postCalls.length, 1);
assert.deepEqual(postCalls[0].payload.container_ids, ['CID-ROOT']);
assert.deepEqual(lineage.getChildren('CID-ROOT'), ['CID-CHILD']);
assert.deepEqual(lineage.getSerials('CID-CHILD'), ['SN-001']);
assert.equal(lineage.nameMap.get('CID-ROOT'), 'LOT-ROOT');
assert.equal(lineage.graphEdges.value.length, 1);
} finally {
restore();
}
});
test('useEquipmentQuery performs timeline multi-query and keeps validation errors user-friendly', async () => {
const postCalls = [];
const restore = setupWindowMesApi({
get: async (url) => {
if (url === '/api/query-tool/equipment-list') {
return {
data: [
{ RESOURCEID: 'EQ-1', RESOURCENAME: 'EQ Alpha' },
{ RESOURCEID: 'EQ-2', RESOURCENAME: 'EQ Beta' },
],
};
}
throw new Error(`unexpected GET url: ${url}`);
},
post: async (url, payload) => {
postCalls.push({ url, payload });
if (payload.query_type === 'status_hours') {
return { data: [{ STATUSNAME: 'RUN', HOURS: 12 }] };
}
if (payload.query_type === 'lots') {
return { data: [{ CONTAINERID: 'CID-1001' }] };
}
if (payload.query_type === 'jobs') {
return { data: [{ JOBID: 'JOB-001' }] };
}
return { data: [] };
},
});
try {
const equipment = useEquipmentQuery({
selectedEquipmentIds: ['EQ-1'],
startDate: '2026-02-01',
endDate: '2026-02-10',
activeSubTab: 'timeline',
});
const bootstrapped = await equipment.bootstrap();
assert.equal(bootstrapped, true);
assert.equal(equipment.equipmentOptions.value.length, 2);
const queried = await equipment.queryTimeline();
assert.equal(queried, true);
assert.deepEqual(
postCalls.map((call) => call.payload.query_type).sort(),
['jobs', 'lots', 'status_hours'],
);
assert.equal(equipment.queried.timeline, true);
assert.equal(equipment.statusRows.value.length, 1);
assert.equal(equipment.jobsRows.value.length, 1);
assert.equal(equipment.lotsRows.value.length, 1);
equipment.setSelectedEquipmentIds([]);
const invalid = await equipment.queryLots();
assert.equal(invalid, false);
assert.match(equipment.errors.filters, /至少一台設備/);
} finally {
restore();
}
});
test('useLotDetail batches selected container ids and preserves workcenter filters in follow-up query', async () => {
const getCalls = [];
const restore = setupWindowMesApi({
get: async (url) => {
getCalls.push(url);
const parsed = new URL(url, 'http://local.test');
if (parsed.pathname === '/api/query-tool/lot-history') {
return {
data: [
{
CONTAINERID: 'CID-001',
EQUIPMENTID: 'EQ-01',
TRACKINTIMESTAMP: '2026-02-01 08:00:00',
TRACKOUTTIMESTAMP: '2026-02-01 08:30:00',
},
],
};
}
if (parsed.pathname === '/api/query-tool/lot-associations') {
const assocType = parsed.searchParams.get('type');
return { data: [{ TYPE: assocType, CONTAINERID: 'CID-001' }] };
}
if (parsed.pathname === '/api/query-tool/workcenter-groups') {
return { data: [{ name: 'WB', sequence: 1 }] };
}
throw new Error(`unexpected GET url: ${url}`);
},
});
try {
const detail = useLotDetail({ activeSubTab: 'history' });
const ok = await detail.setSelectedContainerIds(['CID-001', 'CID-002']);
assert.equal(ok, true);
assert.equal(detail.historyRows.value.length, 1);
assert.equal(detail.associationRows.holds.length, 1);
assert.equal(detail.associationRows.materials.length, 1);
const historyCall = getCalls.find((url) => url.startsWith('/api/query-tool/lot-history?'));
assert.ok(historyCall, 'lot-history API should be called');
const historyParams = new URL(historyCall, 'http://local.test').searchParams;
assert.equal(historyParams.get('container_ids'), 'CID-001,CID-002');
await detail.setSelectedWorkcenterGroups(['WB']);
const latestHistoryCall = getCalls.filter((url) => url.startsWith('/api/query-tool/lot-history?')).at(-1);
const latestParams = new URL(latestHistoryCall, 'http://local.test').searchParams;
assert.equal(latestParams.get('workcenter_groups'), 'WB');
} finally {
restore();
}
});