feat: relax query limits for query-tool and mid-section-defect
This commit is contained in:
239
frontend/tests/query-tool-composables.test.js
Normal file
239
frontend/tests/query-tool-composables.test.js
Normal file
@@ -0,0 +1,239 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user