导入问题修复
This commit is contained in:
1 parent
0282c30a2a
commit
ede1d47c47
26 files changed
+934
-45
No files matched your search
@@ -1374,7 +1374,7 @@ test('passenger explicit server success is terminal without a post-save row requ
|
||||
assert.doesNotMatch(passengerBranch, /passengerRequery|verifyLifecycleOperation/);
|
||||
assert.doesNotMatch(background, /attemptAutomaticPassengerReconciliation/);
|
||||
assert.match(background, /名单已取得 ERP 明确成功响应,按业务规则确认录入成功/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.183'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.184'/);
|
||||
});
|
||||
|
||||
test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => {
|
||||
@@ -1584,7 +1584,7 @@ test('lifecycle execution keeps the MV3 worker alive until the durable task sett
|
||||
assert.match(background, /arrangement_resource_candidate_data_missing/);
|
||||
});
|
||||
|
||||
test('extension 0.5.183 keeps reload dormant unless its durable execution state is fully idle', async () => {
|
||||
test('extension 0.5.184 keeps reload dormant unless its durable execution state is fully idle', async () => {
|
||||
const [background, bridge] = await Promise.all([
|
||||
readFile(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8'),
|
||||
readFile(new URL('../chrome-extension/ltjt-order-assistant/business-bridge.js', import.meta.url), 'utf8')
|
||||
@@ -2035,7 +2035,7 @@ test('delete guard distinguishes independent, shared child, and shared parent ro
|
||||
for (const operation of operations) assert.equal(plans.validateOperation(operation).ok, true, plans.validateOperation(operation).blockers.join('; '));
|
||||
|
||||
const mapping = JSON.parse(await readFile(new URL('../mappings/lifecycle.mapping.json', import.meta.url), 'utf8'));
|
||||
assert.equal(mapping.current_extension_version, '0.5.183');
|
||||
assert.equal(mapping.current_extension_version, '0.5.184');
|
||||
assert.equal(
|
||||
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
|
||||
'ertrenshu'
|
||||
@@ -2419,10 +2419,10 @@ test('schema and browser adapters contain the v2 safety fields and no confirm ov
|
||||
assert.match(inpage, /`ys_danweiid\$\{index\}`, resolvedCustomerId/);
|
||||
assert.doesNotMatch(inpage, /product_customer_source_region|sourceRegionCheck|source_reference/);
|
||||
assert.doesNotMatch(teamBatchInpage, /product_customer_source_region|sourceRegionCheck/);
|
||||
assert.equal(extensionManifest.version, '0.5.183');
|
||||
assert.match(inpage, /version: '0\.5\.183'/);
|
||||
assert.match(teamBatchInpage, /version: '0\.5\.183'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.183'/);
|
||||
assert.equal(extensionManifest.version, '0.5.184');
|
||||
assert.match(inpage, /version: '0\.5\.184'/);
|
||||
assert.match(teamBatchInpage, /version: '0\.5\.184'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.184'/);
|
||||
assert.match(inpage, /function strictIsoDate\(value\)/);
|
||||
assert.match(inpage, /const startDate = strictIsoDate\(current\('riqi'\)\)/);
|
||||
assert.match(inpage, /const endDate = strictIsoDate\(current\('riqis'\)\)/);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const source = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
|
||||
const testSource = source.replace(' resolveNativeListSearchValues,', ' testSearch: searchMainPage,\n resolveNativeListSearchValues,');
|
||||
|
||||
function fixture({ framed = true, tick = () => {} } = {}) {
|
||||
let now = 0;
|
||||
const calls = [];
|
||||
const state = { current: null, framePresent: framed, inaccessible: false };
|
||||
function list({ path = '/System/Business/orders.asp', complete = true, loader = true, button = true, disabled = false } = {}) {
|
||||
const listeners = new Map();
|
||||
const fields = new Map();
|
||||
const search = { isConnected: true, disabled };
|
||||
for (const name of ['S_chufariqi', 'S_chufarizhi', 'S_tuanxuhao', 'S_kehuming', 'S_chanpinming', 'S_youkexinxi', 'S_lingdui', 'S_zhuangtai']) fields.set(name, { value: '' });
|
||||
const doc = {
|
||||
location: { pathname: path, href: `https://erp.invalid${path}` }, readyState: complete ? 'complete' : 'loading',
|
||||
querySelector(selector) {
|
||||
if (selector.startsWith('#SearchButton')) return button ? search : null;
|
||||
return fields.get(selector.match(/^#([^,]+)/)?.[1]) || null;
|
||||
},
|
||||
querySelectorAll: () => [],
|
||||
fields, search
|
||||
};
|
||||
const events = { on: (name, callback) => listeners.set(name.split('.')[0], callback), off: () => listeners.clear() };
|
||||
const jq = () => events;
|
||||
jq.ajax = () => {};
|
||||
jq.active = 0;
|
||||
doc.defaultView = { document: doc, jQuery: jq };
|
||||
if (loader) doc.defaultView.AjaxLoadData = () => {
|
||||
calls.push({ document: doc, identifier: fields.get('S_tuanxuhao').value });
|
||||
const xhr = { status: 200 };
|
||||
const data = new URLSearchParams({ Act: 'PrintGridList', ...Object.fromEntries([...fields].map(([name, field]) => [name, field.value])) });
|
||||
listeners.get('ajaxSend')?.({}, xhr, { url: '/System/Dat/order.asp', data: data.toString() });
|
||||
listeners.get('ajaxComplete')?.({}, xhr);
|
||||
};
|
||||
return doc;
|
||||
}
|
||||
const initial = list();
|
||||
state.current = initial;
|
||||
const frame = { get contentDocument() { if (state.inaccessible) throw new Error('fixture access denied'); return state.current; } };
|
||||
const root = framed ? {
|
||||
location: { pathname: '/System/Mainlt.asp' },
|
||||
getElementById: () => state.framePresent ? frame : null,
|
||||
querySelector: () => null
|
||||
} : initial;
|
||||
if (!framed) { root.getElementById = () => null; }
|
||||
const window = { document: root, location: root.location };
|
||||
vm.runInNewContext(testSource, {
|
||||
window, document: root, location: root.location, URL, URLSearchParams,
|
||||
Date: class extends Date { static now() { return now; } },
|
||||
performance: { now: () => now },
|
||||
setTimeout(callback, delay) { now += delay; tick({ now, state, initial }); callback(); }
|
||||
});
|
||||
return { api: window.LTJTOrderAssistant, list, initial, root, state, calls, elapsed: () => now,
|
||||
run: () => window.LTJTOrderAssistant.testSearch(initial, { S_tuanxuhao: 'TEST-LIST-001', S_zhuangtai: '' }) };
|
||||
}
|
||||
|
||||
test('ready native list searches once and retains exact request evidence', async () => {
|
||||
for (const framed of [true, false]) {
|
||||
const f = fixture({ framed });
|
||||
const result = await f.run();
|
||||
assert.equal(result, f.initial);
|
||||
assert.equal(f.calls.length, 1);
|
||||
assert.equal(f.calls[0].identifier, 'TEST-LIST-001');
|
||||
assert.equal(result.__ltjtNativeListSearchEvidence.completed, true);
|
||||
}
|
||||
});
|
||||
|
||||
test('a loading same-path Document replacement is reacquired before searching', async () => {
|
||||
let replacement;
|
||||
const f = fixture({ tick({ now, state }) { if (now >= 500) state.current = replacement; } });
|
||||
f.initial.readyState = 'loading';
|
||||
replacement = f.list();
|
||||
const result = await f.run();
|
||||
assert.equal(result, replacement);
|
||||
assert.equal(f.calls.length, 1);
|
||||
assert.equal(f.calls[0].document, replacement);
|
||||
assert.equal(f.initial.fields.get('S_tuanxuhao').value, '', 'detached document must not be filled');
|
||||
assert.equal(result.__ltjtNativeListSearchEvidence.readiness.document_replacements, 1);
|
||||
});
|
||||
|
||||
test('replaced same-path document can finish loading later without using its predecessor', async () => {
|
||||
let replacement;
|
||||
const f = fixture({ tick({ now, state }) {
|
||||
if (now >= 100) state.current = replacement;
|
||||
if (now >= 1200) replacement.readyState = 'complete';
|
||||
} });
|
||||
f.initial.readyState = 'loading';
|
||||
replacement = f.list({ complete: false });
|
||||
assert.equal(await f.run(), replacement);
|
||||
assert.equal(f.elapsed(), 1200);
|
||||
});
|
||||
|
||||
for (const [name, change, missing] of [
|
||||
['unfinished page', f => { f.initial.readyState = 'loading'; }, 'document_complete'],
|
||||
['missing loader', f => { delete f.initial.defaultView.AjaxLoadData; }, 'loader_ready'],
|
||||
['disabled button', f => { f.initial.search.disabled = true; }, 'search_button_ready'],
|
||||
['detached button', f => { f.initial.search.isConnected = false; }, 'search_button_ready'],
|
||||
['wrong page', f => { f.state.current = f.list({ path: '/System/login.asp' }); }, 'path_matched'],
|
||||
['missing document', f => { f.state.current = null; }, 'document_present'],
|
||||
['removed frame', f => { f.state.framePresent = false; }, 'document_present'],
|
||||
['inaccessible frame', f => { f.state.inaccessible = true; }, 'document_accessible']
|
||||
]) test(`native readiness stops without searching: ${name}`, async () => {
|
||||
const f = fixture(); change(f);
|
||||
await assert.rejects(f.run(), error => {
|
||||
assert.match(error.message, /native_list_search_not_ready/);
|
||||
assert.ok(error.message.includes(missing));
|
||||
assert.equal(error.nativeListReadiness[missing], false);
|
||||
assert.ok(!JSON.stringify(error.nativeListReadiness).includes('TEST-LIST'));
|
||||
return true;
|
||||
});
|
||||
assert.equal(f.calls.length, 0);
|
||||
assert.equal(f.elapsed(), 15000);
|
||||
});
|
||||
|
||||
test('document replacement during prior Ajax wait fails closed without searching either page', async () => {
|
||||
const f = fixture({ tick({ state, initial }) { state.current = f.list(); initial.defaultView.jQuery.active = 0; } });
|
||||
f.initial.defaultView.jQuery.active = 1;
|
||||
await assert.rejects(f.run(), /native_list_search_document_changed/);
|
||||
assert.equal(f.calls.length, 0);
|
||||
});
|
||||
|
||||
test('missing button is diagnosed separately from a ready document and loader', async () => {
|
||||
const f = fixture(); f.state.current = f.list({ button: false });
|
||||
await assert.rejects(f.run(), error => error.nativeListReadiness.search_button_ready === false
|
||||
&& error.nativeListReadiness.document_complete === true && error.nativeListReadiness.loader_ready === true);
|
||||
assert.equal(f.calls.length, 0);
|
||||
});
|
||||
|
||||
test('a page change during the native search is rejected instead of using stale result rows', async () => {
|
||||
const f = fixture(); const load = f.initial.defaultView.AjaxLoadData;
|
||||
f.initial.defaultView.AjaxLoadData = () => { load(); f.state.current = f.list(); };
|
||||
await assert.rejects(f.run(), /native_list_search_document_changed/);
|
||||
assert.equal(f.calls.length, 1, 'only the original read-only search is issued; no retry');
|
||||
assert.equal(f.initial.__ltjtNativeListSearchEvidence, undefined);
|
||||
});
|
||||
|
||||
test('the real resolution failure carries explicit no-write proof and value-free readiness details', async () => {
|
||||
const f = fixture(); delete f.initial.defaultView.AjaxLoadData;
|
||||
const report = await f.api.resolveLifecycleOperation({ action: 'arrangement_hotel', data: { existing_refs: { identifier: 'TEST-LIST-001' } } });
|
||||
assert.equal(report.status, 'erp_resolution_blocked');
|
||||
assert.equal(report.no_erp_write, true);
|
||||
assert.equal(report.write_attempted, false);
|
||||
assert.equal(report.native_request, null);
|
||||
assert.equal(report.preflight.stage, 'erp_readonly_resolution');
|
||||
assert.equal(report.preflight.native_list_readiness.loader_ready, false);
|
||||
assert.equal(f.calls.length, 0);
|
||||
});
|
||||
|
||||
test('the worker describes readiness failures accurately while preserving not-found errors', () => {
|
||||
const worker = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8');
|
||||
const fn = worker.slice(worker.indexOf('function erpResolutionFailure('), worker.indexOf('function lifecycleRouteFailure('));
|
||||
const context = vm.createContext({}); vm.runInContext(fn, context);
|
||||
for (const cause of ['native_list_search_not_ready', 'native_list_search_not_ready:loader_ready', 'native_list_search_document_changed']) {
|
||||
const failure = context.erpResolutionFailure({}, { blockers: [`erp_resolution_navigation_failed:${cause}`] });
|
||||
assert.equal(failure.errorCode, 'erp_list_search_not_ready');
|
||||
assert.match(failure.message, /未写入 ERP/);
|
||||
assert.doesNotMatch(failure.message, /确认 ERP 已登录/);
|
||||
}
|
||||
assert.equal(context.erpResolutionFailure({}, { blockers: ['erp_resolution_candidate_count:0'] }).errorCode, 'erp_target_not_found');
|
||||
});
|
||||
Reference in new issue
Block a user