240 lines
7.7 KiB
JavaScript
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();
|
|
}
|
|
});
|