Files
LWLT-AIBOT/tools/hotel-update-selection.test.mjs
2026-09-11 21:22:14 +08:00

138 lines
7.5 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';
import { readFileSync } from 'node:fs';
const inpage = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8')
.replace('inspectArrangementLookupReadiness(form,', 'testResolveHotel: resolveArrangementExecutionOperation,\n inspectArrangementLookupReadiness(form,');
const adapter = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/lifecycle-adapters.js', import.meta.url), 'utf8')
.replace('assistant.preflightLifecycleOperation = preflightLifecycleOperation;', `
assistant.testSlot = arrangementSlotEvidence;
assistant.testApply = applyArrangement;
assistant.testOperationBlockers = operationBlockers;
assistant.testExpected = arrangementExpectedFields;
assistant.testResource = arrangementResourceProof;
assistant.preflightLifecycleOperation = preflightLifecycleOperation;`);
function fixture({ duplicate = false, slotIndex = 1, onSelection = () => {} } = {}) {
let now = 10000;
let nativeSelections = 0;
const form = { elements: [], id: 'InfoForm1' };
const fields = new Map();
const document = { querySelector: () => form, querySelectorAll: () => [] };
const window = { document, Event: class {}, location: { href: 'https://erp.invalid/System/Business/teams_jiudian.asp?tdID=123', pathname: '/System/Business/teams_jiudian.asp' } };
document.defaultView = window;
form.ownerDocument = document;
const add = (name, value) => {
const control = {
name, id: name, value: String(value), type: 'text', tagName: 'INPUT', disabled: false,
ownerDocument: document, dispatchEvent() {},
getAttribute: (key) => key.toLowerCase() === 'data' ? '270◆测试新酒店◆TWN◆CNY◆100' : key.toLowerCase() === 'setval' ? name : null
};
fields.set(name, control); form.elements.push(control);
};
for (const index of [...new Set([0, slotIndex, slotIndex + 1])]) {
const dated = index === slotIndex || duplicate;
const values = {
id: 800 + index, danwei: `原酒店${index}`, danweiid: 100 + index, fangxing: 'DBL',
riqi: dated ? '2026-9-15' : '2026-9-20', riqis: dated ? '2026-9-16' : '2026-9-21',
jianshu: 8, zt: '未确认', beizhu: `保留备注${index}`, yf: 0, sh: 0, shenhe: 0,
diqu: '旧地区', dianhua: '旧电话', bizhong: 'CNY', danjia: 0, fangshi: '公司现付'
};
for (const [name, value] of Object.entries(values)) add(`${name}${index}`, value);
}
add('tdID', 123); add('ddID', 0);
form.elements.namedItem = (name) => fields.get(name);
form.querySelector = (selector) => fields.get(/name="([^"]+)"/.exec(selector)?.[1]);
window.SelectBox = {
SetVal: '',
SetValToObj(raw) {
nativeSelections++;
const index = /danwei(\d+)/.exec(this.SetVal)[1];
const [id, name, roomType] = raw.split('◆');
for (const [key, value] of Object.entries({ danwei: name, danweiid: id, fangxing: roomType, danjia: '100' })) fields.get(`${key}${index}`).value = value;
onSelection(fields, index);
}
};
const sandbox = {
window, document, Event: window.Event, URL, URLSearchParams, TextEncoder,
Date: class extends Date { static now() { return now; } },
setTimeout(callback, ms) { now += ms; callback(); }
};
window.setTimeout = sandbox.setTimeout;
vm.runInNewContext(inpage, sandbox);
vm.runInNewContext(adapter, sandbox);
const api = window.LTJTOrderAssistant;
const input = {
action: 'arrangement_hotel', data: {
existing_refs: { identifier: 'LW-TEST-HOTEL', kind: 'shared_plan' },
arrangement: {
mode: 'update', selection: { start_date: '2026-09-15', end_date: '2026-09-16' },
changes: { resource: { name: '测试新酒店TWN', keyword: '测试新酒店TWN' }, room_count: 9 }
}
}
};
return { api, form, fields, input, nativeSelections: () => nativeSelections };
}
test('hotel replacement resolves a dated existing row and uses native linked selection on that row only', async () => {
for (const slotIndex of [0, 1, 10]) {
const f = fixture({ slotIndex });
const resolved = f.api.testResolveHotel(f.form, f.input);
assert.equal(resolved.ok, true, JSON.stringify(resolved));
const arrangement = resolved.operation.data.arrangement;
assert.equal(arrangement.target.slot_index, slotIndex);
assert.equal(arrangement.resource.name, `原酒店${slotIndex}`);
assert.equal(arrangement.changes.resource.id, '270');
assert.equal(arrangement.changes.room_type, 'TWN');
assert.equal(arrangement.selection, undefined);
assert.equal(f.api.testSlot(f.form, resolved.operation).ready, true);
assert.equal(f.api.testResource(f.form, resolved.operation).matched, true);
assert.equal(f.api.testOperationBlockers(resolved.operation).length, 0);
const before = new Map([...f.fields].map(([key, control]) => [key, control.value]));
const result = await f.api.testApply(f.form, resolved.operation);
assert.equal(result.blockers.length, 0, JSON.stringify(result));
assert.equal(f.nativeSelections(), 1);
assert.equal(f.fields.get(`danwei${slotIndex}`).value, '测试新酒店');
assert.equal(f.fields.get(`fangxing${slotIndex}`).value, 'TWN');
assert.equal(f.fields.get(`jianshu${slotIndex}`).value, '9');
for (const name of ['id', 'beizhu', 'riqi', 'riqis', 'zt', 'yf', 'sh']) assert.equal(f.fields.get(`${name}${slotIndex}`).value, before.get(`${name}${slotIndex}`));
for (const [name, value] of before) if (!new RegExp(`^[a-zA-Z_]+${slotIndex}$`).test(name)) assert.equal(f.fields.get(name).value, value, name);
const expected = f.api.testExpected(f.form, resolved.operation);
assert.equal(expected.blockers.length, 0);
assert.ok(expected.fields.some((field) => field.name === `id${slotIndex}` && field.expected === String(800 + slotIndex)));
assert.ok(expected.fields.some((field) => field.name === `danweiid${slotIndex}` && field.expected === '270'));
assert.ok(expected.fields.some((field) => field.name === `fangxing${slotIndex}` && field.expected === 'TWN'));
}
});
test('hotel replacement stops on ambiguous or missing dated rows before selecting a hotel', () => {
const duplicate = fixture({ duplicate: true });
const result = duplicate.api.testResolveHotel(duplicate.form, duplicate.input);
assert.equal(result.ok, false);
assert.match(result.blockers.join(), /target_candidate_count:3/);
const missing = fixture(); missing.input.data.arrangement.selection.start_date = '2026-09-14';
assert.equal(missing.api.testResolveHotel(missing.form, missing.input).ok, false);
assert.equal(duplicate.nativeSelections(), 0);
});
test('hotel replacement blocks stale row identity, payment, audit and confirmed status', async () => {
for (const field of ['id1', 'yf1', 'sh1', 'zt1']) {
const f = fixture();
const resolved = f.api.testResolveHotel(f.form, f.input).operation;
f.fields.get(field).value = field === 'zt1' ? '已确认' : '999';
const result = await f.api.testApply(f.form, resolved);
assert.ok(result.blockers.length, field);
assert.equal(f.nativeSelections(), 0);
}
});
test('hotel replacement rejects native callbacks that change another row or change the record id', async () => {
for (const field of ['danwei0', 'id1', 'riqi1']) {
const f = fixture({ onSelection(fields) { fields.get(field).value = 'unexpected'; } });
const resolved = f.api.testResolveHotel(f.form, f.input).operation;
const result = await f.api.testApply(f.form, resolved);
assert.ok(result.blockers.length, field);
}
});