184 lines
10 KiB
JavaScript
184 lines
10 KiB
JavaScript
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);
|
||
});
|