修复一个插件bug
This commit is contained in:
1 parent
275564d1bf
commit
7ea7c211cc
18 files changed
+412
-20
No files matched your search
@@ -0,0 +1,176 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const adapterSource = readFileSync(new URL('../chrome-extension/ltjt-order-assistant/lifecycle-adapters.js', import.meta.url), 'utf8');
|
||||
// Exercise the actual polling function without adding a production message/API.
|
||||
const testSource = adapterSource.replace(
|
||||
'assistant.preflightLifecycleOperation = preflightLifecycleOperation;',
|
||||
'assistant.testReadiness = (operation, form, timeout) => waitForFormReadiness(operation, contractFor(operation), form, timeout);\n'
|
||||
+ 'assistant.testSlot = arrangementSlotEvidence;\n'
|
||||
+ 'if (window.testProofs) { arrangementResourceProof = window.testProofs.resource; ownershipListProof = window.testProofs.ownership; }\n'
|
||||
+ 'assistant.preflightLifecycleOperation = preflightLifecycleOperation;'
|
||||
);
|
||||
const hotelFields = ['riqi0', 'riqis0', 'danwei0', 'danweiid0', 'fangxing0', 'jianshu0', 'zt0', 'beizhu0', 'id0', 'yf0', 'sh0', 'MaxI', 'tdID', 'ddID', 'chufari', 'SubmitButton'];
|
||||
|
||||
function fixture({ count = 154, maxI = '6', mode = 'create', tick = () => {}, proofs } = {}) {
|
||||
let now = 10000;
|
||||
const initialTime = now;
|
||||
const values = { MaxI: maxI, tdID: '123', ddID: '0', chufari: '2026-09-15' };
|
||||
const field = (name) => ({
|
||||
name, id: name, tagName: 'INPUT', type: name === 'SubmitButton' ? 'submit' : 'text',
|
||||
disabled: false, value: values[name] ?? ''
|
||||
});
|
||||
const form = { id: 'InfoForm1', isConnected: true, elements: hotelFields.map(field) };
|
||||
for (let i = hotelFields.length - 1; i < count; i += 1) form.elements.push(field(`extra${i}`));
|
||||
const state = { loading: false };
|
||||
const loadingElement = {
|
||||
className: 'datagrid-mask',
|
||||
getBoundingClientRect: () => ({ width: 50, height: 50, left: 0, top: 0, right: 50, bottom: 50 }),
|
||||
getClientRects: () => [{}]
|
||||
};
|
||||
const document = {
|
||||
querySelectorAll: (selector) => selector.startsWith('.datagrid-mask') && state.loading ? [loadingElement] : [],
|
||||
querySelector: () => form,
|
||||
readyState: 'complete'
|
||||
};
|
||||
const window = {
|
||||
document, innerWidth: 1024, innerHeight: 768,
|
||||
location: { pathname: '/System/Business/teams_jiudian.asp', href: 'https://erp.invalid/System/Business/teams_jiudian.asp?tdID=123' },
|
||||
testProofs: proofs,
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible', opacity: '1' })
|
||||
};
|
||||
document.defaultView = window;
|
||||
window.parent = window;
|
||||
form.ownerDocument = document;
|
||||
const sandbox = {
|
||||
window, document, URL, URLSearchParams, TextEncoder, location: window.location,
|
||||
Date: class extends Date { static now() { return now; } },
|
||||
setTimeout(callback, ms) { now += ms; tick({ elapsed: now - initialTime, form, state }); callback(); }
|
||||
};
|
||||
window.setTimeout = sandbox.setTimeout;
|
||||
vm.runInNewContext(testSource, sandbox);
|
||||
const operation = { action: 'arrangement_hotel', data: { arrangement: { mode } } };
|
||||
return {
|
||||
form, state, operation, api: window.LTJTOrderAssistant,
|
||||
elapsed: () => now - initialTime,
|
||||
run: (timeout = 2500) => window.LTJTOrderAssistant.testReadiness(operation, form, timeout)
|
||||
};
|
||||
}
|
||||
|
||||
test('hotel create accepts the reported complete 154-field form after one stable second', async () => {
|
||||
const page = fixture();
|
||||
const result = await page.run();
|
||||
assert.equal(result.successful_control_count, 154);
|
||||
assert.equal(result.ready, true, JSON.stringify(result.blockers));
|
||||
assert.ok(page.elapsed() >= 1000 && page.elapsed() < 2000);
|
||||
});
|
||||
|
||||
test('hotel create fails closed without a form', async () => {
|
||||
const page = fixture();
|
||||
const missing = await page.api.testReadiness(page.operation, null, 2500);
|
||||
assert.equal(missing.ready, false);
|
||||
assert.ok(missing.blockers.includes('lifecycle_complete_form_missing'));
|
||||
});
|
||||
|
||||
test('hotel create uses the requested 150-field boundary', async () => {
|
||||
for (const count of [149, 150, 154, 195, 442]) {
|
||||
const result = await fixture({ count }).run();
|
||||
assert.equal(result.successful_control_count, count);
|
||||
assert.equal(result.minimum_successful_controls, 150);
|
||||
assert.equal(result.ready, count >= 150, `${count}: ${JSON.stringify(result.blockers)}`);
|
||||
if (count < 150) assert.ok(result.blockers.includes(`lifecycle_form_incomplete:${count}:150`));
|
||||
}
|
||||
});
|
||||
|
||||
test('hotel create rejects every missing required field even with more than 195 fields', async () => {
|
||||
for (const name of hotelFields) {
|
||||
const page = fixture({ count: 442 });
|
||||
page.form.elements = page.form.elements.filter((control) => control.name !== name);
|
||||
const result = await page.run();
|
||||
assert.equal(result.ready, false, name);
|
||||
assert.ok(result.missing_required_fields.includes(name), name);
|
||||
}
|
||||
});
|
||||
|
||||
test('hotel create waits for visible loading to disappear and then stabilize', async () => {
|
||||
const page = fixture({ tick: ({ elapsed, state }) => { if (elapsed >= 750) state.loading = false; } });
|
||||
page.state.loading = true;
|
||||
assert.equal((await page.run()).ready, true);
|
||||
assert.equal(page.elapsed(), 1750);
|
||||
const stuck = fixture();
|
||||
stuck.state.loading = true;
|
||||
const result = await stuck.run();
|
||||
assert.equal(result.ready, false);
|
||||
assert.ok(result.blockers.includes('lifecycle_loading_overlay_visible'));
|
||||
});
|
||||
|
||||
test('hotel create retains the existing one-second count stability gate', async () => {
|
||||
const grow = (form) => form.elements.push({ name: `extra${form.elements.length}`, tagName: 'INPUT', type: 'text', value: '' });
|
||||
const settling = fixture({ tick: ({ elapsed, form }) => { if (elapsed === 750) grow(form); } });
|
||||
assert.equal((await settling.run()).ready, true);
|
||||
assert.equal(settling.elapsed(), 1750);
|
||||
const changing = fixture({ tick: ({ form }) => grow(form) });
|
||||
const result = await changing.run();
|
||||
assert.equal(result.ready, false);
|
||||
assert.ok(result.blockers.includes('lifecycle_form_not_stable'));
|
||||
});
|
||||
|
||||
test('hotel update/clear and unrelated lifecycle actions retain their current count gates', async () => {
|
||||
for (const mode of ['update', 'clear']) {
|
||||
const result = await fixture({ count: 149, mode }).run();
|
||||
assert.equal(result.ready, false);
|
||||
assert.ok(result.blockers.includes('lifecycle_form_incomplete:149:150'));
|
||||
assert.equal((await fixture({ count: 154, mode }).run()).ready, true);
|
||||
}
|
||||
const page = fixture();
|
||||
page.operation.action = 'arrangement_vehicle';
|
||||
page.form.elements = page.form.elements.slice(0, 70);
|
||||
const result = await page.run();
|
||||
assert.equal(result.ready, false);
|
||||
assert.equal(result.minimum_successful_controls, 90);
|
||||
});
|
||||
|
||||
test('154-field readiness still leaves occupied slots and payment/audit state blocked', async () => {
|
||||
for (const [name, value, blocker] of [
|
||||
['id0', '1234', 'arrangement_slot_occupied:id0'],
|
||||
['danweiid0', '5678', 'arrangement_slot_occupied:danweiid0'],
|
||||
['yf0', '1', 'arrangement_slot_financial_or_audit_state:yf0'],
|
||||
['sh0', '1', 'arrangement_slot_financial_or_audit_state:sh0']
|
||||
]) {
|
||||
const page = fixture();
|
||||
page.form.elements.find((item) => item.name === name).value = value;
|
||||
assert.equal((await page.run()).ready, true);
|
||||
const slot = page.api.testSlot(page.form, page.operation);
|
||||
assert.equal(slot.ready, false);
|
||||
assert.ok(slot.blockers.includes(blocker), JSON.stringify(slot.blockers));
|
||||
}
|
||||
});
|
||||
|
||||
test('full hotel preflight preserves identity, resource and ownership gates after the count check', async () => {
|
||||
for (const failure of ['', 'identity', 'resource', 'ownership', 'slot']) {
|
||||
const calls = [];
|
||||
const page = fixture({ proofs: {
|
||||
resource: () => { calls.push('resource'); return { matched: failure !== 'resource', blockers: ['resource_mismatch'] }; },
|
||||
ownership: async () => { calls.push('ownership'); return { matched: failure !== 'ownership', blockers: ['account_mismatch'] }; }
|
||||
} });
|
||||
page.operation.data.existing_refs = {
|
||||
kind: 'independent_order', identifier: 'TEST-HOTEL-READINESS', tid: failure === 'identity' ? '999' : '123', ddid: '456',
|
||||
resolved: true, resolution_source: 'erp_unique_match'
|
||||
};
|
||||
Object.assign(page.operation.data.arrangement, {
|
||||
start_date: '2026-09-16', end_date: '2026-09-18', room_count: 9,
|
||||
resource: { id: '789', name: '测试酒店' }, status: '未安排', side_effect_policy: 'no_external'
|
||||
});
|
||||
if (failure === 'slot') page.form.elements.find((item) => item.name === 'yf0').value = '1';
|
||||
const result = await page.api.preflightLifecycleOperation(page.operation);
|
||||
assert.equal(result.preflight.form_readiness.ready, true);
|
||||
assert.equal(result.status, failure ? 'lifecycle_preflight_blocked' : 'lifecycle_preflight_ready', JSON.stringify(result.blockers));
|
||||
const expected = { identity: 'form_tid_mismatch:', resource: 'resource_mismatch', ownership: 'ownership_proof:account_mismatch', slot: 'arrangement_slot_financial_or_audit_state:yf0' };
|
||||
if (failure) assert.ok(result.blockers.some((blocker) => blocker.startsWith(expected[failure])), JSON.stringify(result.blockers));
|
||||
assert.deepEqual(calls, failure === 'identity' ? [] : ['resource', ...(['', 'ownership'].includes(failure) ? ['ownership'] : [])]);
|
||||
assert.equal(result.no_erp_write, true);
|
||||
assert.equal(result.write_attempted, false);
|
||||
}
|
||||
});
|
||||
@@ -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\.170'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.172'/);
|
||||
});
|
||||
|
||||
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.170 keeps reload dormant unless its durable execution state is fully idle', async () => {
|
||||
test('extension 0.5.172 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')
|
||||
@@ -2023,7 +2023,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.170');
|
||||
assert.equal(mapping.current_extension_version, '0.5.172');
|
||||
assert.equal(
|
||||
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
|
||||
'ertrenshu'
|
||||
@@ -2407,10 +2407,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.170');
|
||||
assert.match(inpage, /version: '0\.5\.170'/);
|
||||
assert.match(teamBatchInpage, /version: '0\.5\.170'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.170'/);
|
||||
assert.equal(extensionManifest.version, '0.5.172');
|
||||
assert.match(inpage, /version: '0\.5\.172'/);
|
||||
assert.match(teamBatchInpage, /version: '0\.5\.172'/);
|
||||
assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.172'/);
|
||||
assert.match(inpage, /function strictIsoDate\(value\)/);
|
||||
assert.match(inpage, /const startDate = strictIsoDate\(controlCanonicalValue\(form, 'riqi0'\)\)/);
|
||||
assert.match(inpage, /const endDate = strictIsoDate\(controlCanonicalValue\(form, 'riqis0'\)\)/);
|
||||
|
||||
Reference in new issue
Block a user