207 lines
12 KiB
JavaScript
207 lines
12 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { readFileSync } from 'node:fs';
|
|
import { loadPageActionRealm } from './page-action-test-realm.mjs';
|
|
|
|
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 = () => {}, omitDateMatcher = false } = {}) {
|
|
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;
|
|
loadPageActionRealm(window, sandbox, { 'inpage.js': inpage, 'lifecycle-adapters.js': adapter }, omitDateMatcher ? ['operation-plans.js'] : []);
|
|
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('fresh ERP MAIN world resolves the reported 2027 hotel update using the actual injection list', async () => {
|
|
const f = fixture();
|
|
f.fields.get('riqi1').value = '2027-9-12';
|
|
f.fields.get('riqis1').value = '2027-9-13';
|
|
f.input.data.existing_refs.identifier = 'LLW-270911A-A';
|
|
f.input.data.arrangement.selection = { start_date: '09-12', end_date: '09-13' };
|
|
f.input.data.arrangement.changes.room_count = 10;
|
|
const resolved = f.api.testResolveHotel(f.form, f.input);
|
|
assert.equal(resolved.ok, true, JSON.stringify(resolved));
|
|
assert.equal(resolved.operation.data.arrangement.start_date, '2027-09-12');
|
|
assert.equal(resolved.operation.data.arrangement.end_date, '2027-09-13');
|
|
const result = await f.api.testApply(f.form, resolved.operation);
|
|
assert.equal(result.blockers.length, 0, JSON.stringify(result));
|
|
assert.equal(f.fields.get('jianshu1').value, '10');
|
|
assert.equal(f.fields.get('riqi1').value, '2027-9-12');
|
|
assert.equal(f.fields.get('riqis1').value, '2027-9-13');
|
|
});
|
|
|
|
test('missing page date dependency is diagnosed before matching and never reported as zero ERP records', () => {
|
|
const f = fixture({ omitDateMatcher: true });
|
|
const before = [...f.fields].map(([key, control]) => [key, control.value]);
|
|
const resolved = f.api.testResolveHotel(f.form, f.input);
|
|
assert.equal(resolved.ok, false);
|
|
assert.deepEqual(Array.from(resolved.blockers), ['arrangement_date_matcher_unavailable']);
|
|
assert.equal(f.nativeSelections(), 0);
|
|
assert.deepEqual([...f.fields].map(([key, control]) => [key, control.value]), before);
|
|
});
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
test('month/day hotel selector freezes the unique actual ERP year and preserves native dates on replacement', async () => {
|
|
for (const year of [2026, 2027]) {
|
|
const f = fixture();
|
|
f.fields.get('riqi1').value = `${year}-9-4`;
|
|
f.fields.get('riqis1').value = `${year}-9-5`;
|
|
f.input.data.arrangement.selection = { start_date: '09-04', end_date: '09-05' };
|
|
const resolved = f.api.testResolveHotel(f.form, f.input);
|
|
assert.equal(resolved.ok, true, JSON.stringify(resolved));
|
|
assert.equal(resolved.operation.data.arrangement.start_date, `${year}-09-04`);
|
|
assert.equal(resolved.operation.data.arrangement.end_date, `${year}-09-05`);
|
|
assert.equal(resolved.operation.data.arrangement.selection, undefined);
|
|
const applied = await f.api.testApply(f.form, resolved.operation);
|
|
assert.equal(applied.blockers.length, 0, JSON.stringify(applied));
|
|
assert.equal(f.fields.get('riqi1').value, `${year}-9-4`);
|
|
assert.equal(f.fields.get('riqis1').value, `${year}-9-5`);
|
|
}
|
|
});
|
|
|
|
test('cross-year duplicate month/day rows remain ambiguous and report original dates before any mutation', () => {
|
|
const f = fixture();
|
|
for (const [index, year] of [[0,2026],[1,2027]]) {
|
|
f.fields.get(`riqi${index}`).value = `${year}-12-31`;
|
|
f.fields.get(`riqis${index}`).value = `${year+1}-1-2`;
|
|
}
|
|
f.input.data.arrangement.selection = { start_date: '12-31', end_date: '01-02' };
|
|
let resolved = f.api.testResolveHotel(f.form, f.input);
|
|
assert.equal(resolved.ok, false);
|
|
assert.match(resolved.blockers.join(), /target_candidate_count:2/);
|
|
assert.equal(resolved.evidence.candidates[0].date, '2026-12-31');
|
|
assert.equal(resolved.evidence.candidates[1].end_date, '2028-01-02');
|
|
assert.equal(f.nativeSelections(), 0);
|
|
f.input.data.arrangement.selection.start_date = '2027-12-31';
|
|
resolved = f.api.testResolveHotel(f.form, f.input);
|
|
assert.equal(resolved.ok, true);
|
|
assert.equal(resolved.operation.data.arrangement.target.slot_index, 1);
|
|
f.input.data.arrangement.selection.end_date = '2027-01-02';
|
|
assert.equal(f.api.testResolveHotel(f.form, f.input).ok, false, 'explicit wrong year must not fall back to month/day');
|
|
f.input.data.arrangement.selection = { start_date: '02-30', end_date: '03-01' };
|
|
f.fields.get('riqi1').value = '2026-02-30';
|
|
assert.equal(f.api.testResolveHotel(f.form, f.input).ok, false, 'invalid actual dates never match');
|
|
});
|