Files
LWLT-AIBOT/tools/single-order-product-preflight.test.mjs
2026-09-19 11:06:02 +08:00

184 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict';
import test from 'node:test';
import { loadPageActionRealm } from './page-action-test-realm.mjs';
// Exercise the public preflight with synthetic ERP lookup responses, native
// product linkage and submit interception. No real browser or ERP is used.
function fixture({ products = ['单订房 (测试市场)'], query = '单订房(测试市场)', customer = '测试客户', productStatus = 200, incompleteLinkage = false } = {}) {
const requests = [];
const linkedNames = [];
let submittedBody = '';
const fields = new Map();
const window = { location: new URL('https://erp.invalid/System/Business/orders_add.asp'),
setTimeout: (...args) => setTimeout(...args).unref(), clearTimeout };
const document = { defaultView: window, querySelector: (selector) => selector === '#ListForm' ? form : null,
querySelectorAll: () => [] };
const form = { elements: [], ownerDocument: document,
querySelectorAll: () => [updateButton], querySelector: () => null };
const values = {
fabudanwei: '测试单位', zutuanshe: '', zutuansheid: '', lianxiren: '', zutuanshegzr: '', bizhong: 'CNY',
chufa_ri: '', chanpinming: '', TianShu: '', zhuanxianming: '', tuanxuhao1: '', tuanxuhao2: '',
gendanren: '测试员工', xiaoshouren: '测试员工', darenshu: '', xiaorenshu: '', ertrenshu: '', yingrenshu: '', quanrenshu: '',
frenshu0: '', frenshu1: '', frenshu2: '', frenshu3: '', frenshu4: '', frenshu5: '', frenshu6: '',
danzhuangtai: '', yaobeian: '', ys_danjia0: '', ys_danwei0: '', ys_danweiid0: '', ys_bizhong0: '', ys_shuliang0: '', ys_jine0: ''
};
for (let i = 0; i < 800; i++) values[`unused${i}`] = '';
for (const [name, value] of Object.entries(values)) {
const field = { name, value, tagName: 'INPUT', type: 'text', dispatchEvent() {} };
fields.set(name, field); form.elements.push(field); form.elements[name] = field;
}
const serialize = () => new URLSearchParams(form.elements.map((field) => [field.name, field.value])).toString();
const jq = () => ({ serialize });
jq.ajax = (config) => {
requests.push({ native: true, url: config.url, data: config.data });
assert.ok(config.url.includes('Act=GetProduct'), 'a real submit must never escape the preflight guard');
config.success?.('synthetic product response', 'success');
config.complete?.();
};
window.jQuery = jq;
window.Find_product = () => {
const selectedName = fields.get('chanpinming').value;
linkedNames.push(selectedName);
jq.ajax({ url: `../dat/AjaxPublicFun.asp?Act=GetProduct&cpm=${encodeURIComponent(selectedName)}`,
success() {
const effects = { TianShu: incompleteLinkage ? '' : '1D', zhuanxianming: '测试线路', tuanxuhao1: 'TEST', tuanxuhao2: 'A', ys_danjia0: '100' };
for (const [name, value] of Object.entries(effects)) fields.get(name).value = value;
} });
};
window.SubmitInfoForm = () => {
submittedBody = `Act=DoInfoJH&${serialize()}`;
jq.ajax({ url: '/System/DAT/orders.asp', type: 'POST', data: submittedBody });
};
const updateButton = { innerText: '更新行程', getAttribute: () => '', click: () => window.Find_product() };
const globals = { window, document, location: window.location, Event, URL, URLSearchParams, TextEncoder,
crypto: globalThis.crypto, Blob, performance, setTimeout: window.setTimeout, clearTimeout,
async fetch(url) {
requests.push({ native: false, url });
const action = new URL(url).searchParams.get('Act');
const responses = {
ProGetProductname: products.map((name, i) => `${i + 1}◆${name}`).join('◇'),
GetInformation: '1◆测试线路◆unused◆TEST',
ProTravel: 'CNY◆123◆测试客户◆测试联系人◆测试负责人◆unused',
ProDanwei_Yuangong: '测试员工◆1'
};
assert.ok(Object.hasOwn(responses, action), `unexpected lookup ${action}`);
const status = action === 'ProGetProductname' ? productStatus : 200;
return { ok: status === 200, status, text: async () => responses[action] };
} };
loadPageActionRealm(window, globals);
const operation = { action: 'team_order_create', order_nature: 'formal', submit_mode: 'dry_run', data: {
customer: { name: customer, keyword: customer }, product: { name: query, keyword: query },
departure_dates: ['2026-09-19'], passenger_counts: { adult: 1 }, room_counts: { DBL: 1 }
} };
return { operation, fields, requests, linkedNames, submitted: () => submittedBody,
snapshot: serialize, preflight: () => window.LTJTOrderAssistant.preflightRawInstruction(operation, { interceptSubmit: true }) };
}
function productCheck(result) {
return result.lookup_checks.find((check) => check.name === 'product_initial_selection');
}
async function assertUntouched(f) {
const before = f.snapshot();
const result = await f.preflight();
assert.equal(result.status, 'raw_instruction_preflight_blocked');
assert.equal(result.no_erp_write, true);
assert.equal(result.write_attempted, false);
assert.equal(result.set_summary.set_field_count, 0);
assert.equal(result.product_side_effect.status, 'skipped');
assert.equal(f.snapshot(), before);
assert.equal(f.linkedNames.length, 0);
assert.equal(f.submitted(), '');
assert.equal(f.requests.length, 4);
assert.ok(f.requests.every((request) => !request.native));
assert.equal(result.submit_intercept, null);
assert.equal(result.skipped_stages.length, 4);
assert.doesNotMatch(result.blockers.join(), /GetProduct|after_effect|after_restore|Required fields still blank/);
return result;
}
test('single-order preflight normalizes both sides and uses the exact ERP spelling through native linkage and intercepted submission', async () => {
for (const [query, erpName] of [
['单订房(测试市场)', '单订房 (测试市场)'],
['单订房(测试市场)', '单订房 (测试市场)'],
['单订房\u3000(测试市场)', '单订房\u00a0(测试市场)'],
['AB12(测试市场)', 'AB12 (测试市场)']
]) {
const f = fixture({ products: [erpName], query });
const result = await f.preflight();
assert.equal(result.status, 'raw_instruction_preflight_passed', JSON.stringify(result.blockers));
assert.equal(productCheck(result).match_rule, 'exact_normalized');
assert.equal(productCheck(result).exact_match_count, 1);
assert.deepEqual(f.linkedNames, [erpName]);
assert.equal(f.fields.get('chanpinming').value, erpName);
assert.equal(new URLSearchParams(f.submitted()).get('chanpinming'), erpName);
assert.equal(new URLSearchParams(f.submitted()).get('frenshu3'), '1');
assert.equal(result.submit_intercept.status, 'submit_intercept_captured');
assert.equal(result.submit_safety.live_submit_attempted, false);
assert.equal(f.requests.filter((request) => request.native).length, 1);
}
});
test('normalized exact match takes precedence over longer containing product names', async () => {
const f = fixture({ products: ['单订房 (测试市场)', '单订房(测试市场)升级版'] });
const result = await f.preflight();
assert.equal(result.status, 'raw_instruction_preflight_passed', JSON.stringify(result.blockers));
assert.equal(productCheck(result).exact_match_count, 1);
assert.equal(productCheck(result).contains_match_count, 2);
assert.deepEqual(f.linkedNames, ['单订房 (测试市场)']);
});
test('a unique shortened keyword still selects the ERP product', async () => {
const f = fixture({ query: '单订房', products: ['单订房 (测试市场)', '单订车(测试市场)'] });
const result = await f.preflight();
assert.equal(result.status, 'raw_instruction_preflight_passed', JSON.stringify(result.blockers));
assert.equal(productCheck(result).match_rule, 'unique_contains_normalized');
assert.deepEqual(f.linkedNames, ['单订房 (测试市场)']);
});
test('normalization collisions block even when one raw spelling matches exactly', async () => {
const f = fixture({ products: ['单订房(测试市场)', '单订房 (测试市场)'] });
const result = await assertUntouched(f);
assert.equal(productCheck(result).exact_match_count, 2);
assert.equal(result.blockers.length, 1);
assert.match(result.blockers[0], /匹配到 2 个产品/);
});
test('multiple shortened-keyword matches block instead of selecting the first', async () => {
const f = fixture({ query: '单订房', products: ['单订房(市场甲)', '单订房(市场乙)'] });
const result = await assertUntouched(f);
assert.equal(productCheck(result).exact_match_count, 0);
assert.equal(productCheck(result).contains_match_count, 2);
assert.equal(result.blockers.length, 1);
assert.match(result.blockers[0], /匹配到 2 个产品/);
});
test('unmatched product stops with one useful error and preserves punctuation and qualifiers', async () => {
for (const products of [[], ['单订房'], ['单订房(市场乙)'], ['单订房-测试市场'], ['单订房测试市场']]) {
const result = await assertUntouched(fixture({ products }));
assert.equal(productCheck(result).exact_match_count, 0);
assert.equal(productCheck(result).contains_match_count, 0);
assert.equal(result.blockers.length, 1);
assert.match(result.blockers[0], /未找到匹配的产品/);
}
});
test('failed lookup transport or customer selection never advances into product linkage', async () => {
const transport = await assertUntouched(fixture({ productStatus: 503 }));
assert.match(transport.blockers.join(), /HTTP 503/);
assert.equal(productCheck(transport).exact_match_count, 1);
const customer = await assertUntouched(fixture({ customer: '不存在的客户' }));
assert.match(customer.blockers.join(), /customer_initial_selection/);
});
test('successful name resolution still requires complete native linkage before submission', async () => {
const f = fixture({ incompleteLinkage: true });
const result = await f.preflight();
assert.equal(result.status, 'raw_instruction_preflight_blocked');
assert.equal(productCheck(result).exact_match_count, 1);
assert.match(result.blockers.join(), /product_template_trip_nonempty/);
assert.equal(f.linkedNames.length, 1);
assert.equal(f.submitted(), '');
assert.equal(result.submit_safety.live_submit_attempted, false);
});