Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
QLHazycoder
2026-04-26 08:38:16 +00:00
13 changed files with 837 additions and 171 deletions
+5
View File
@@ -9,6 +9,7 @@ test('address sources normalize supported countries and return local seeds', ()
assert.equal(api.normalizeCountryCode('Deutschland'), 'DE');
assert.equal(api.normalizeCountryCode('澳大利亚'), 'AU');
assert.equal(api.normalizeCountryCode('日本'), 'JP');
assert.equal(api.normalizeCountryCode('unknown'), '');
const deSeed = api.getAddressSeedForCountry('DE');
@@ -20,4 +21,8 @@ test('address sources normalize supported countries and return local seeds', ()
const fallbackSeed = api.getAddressSeedForCountry('unknown', { fallbackCountry: 'AU' });
assert.equal(fallbackSeed.countryCode, 'AU');
assert.equal(fallbackSeed.fallback.region, 'New South Wales');
const jpSeed = api.getAddressSeedForCountry('日本');
assert.equal(jpSeed.countryCode, 'JP');
assert.equal(jpSeed.fallback.region, 'Tokyo');
});
+13
View File
@@ -92,3 +92,16 @@ return { shouldReportReadyForFrame };
assert.equal(api.shouldReportReadyForFrame('plus-checkout', false), true);
assert.equal(api.shouldReportReadyForFrame('paypal-flow', true), true);
});
test('getRuntimeScriptSource follows injected source overrides after utils is already loaded', () => {
const bundle = [extractFunction('getRuntimeScriptSource')].join('\n');
const api = new Function('window', 'SCRIPT_SOURCE', `
${bundle}
return { getRuntimeScriptSource };
`);
const windowRef = {};
assert.equal(api(windowRef, 'chatgpt').getRuntimeScriptSource(), 'chatgpt');
windowRef.__MULTIPAGE_SOURCE = 'plus-checkout';
assert.equal(api(windowRef, 'chatgpt').getRuntimeScriptSource(), 'plus-checkout');
});
+120
View File
@@ -1,9 +1,43 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const source = fs.readFileSync('content/plus-checkout.js', 'utf8');
test('plus checkout content script can be injected repeatedly on the same page', () => {
const attrs = new Map();
const context = {
console: { log() {}, warn() {}, error() {}, info() {} },
location: { href: 'https://chatgpt.com/' },
window: {},
document: {
documentElement: {
getAttribute(name) {
return attrs.get(name) || null;
},
setAttribute(name, value) {
attrs.set(name, String(value));
},
},
},
chrome: {
runtime: {
onMessage: {
addListener() {},
},
},
},
};
context.window = context;
vm.createContext(context);
vm.runInContext(source, context);
vm.runInContext(source, context);
assert.equal(context.__MULTIPAGE_PLUS_CHECKOUT_READY__, true);
});
function extractFunction(name) {
const plainStart = source.indexOf(`function ${name}(`);
const asyncStart = source.indexOf(`async function ${name}(`);
@@ -253,3 +287,89 @@ return { selectRegionDropdown };
assert.deepEqual(clicks, [stateDropdown, options[1]]);
});
test('country and region helpers recognize the dropdown-style localized address form', () => {
const bundle = [
extractFunction('isVisibleElement'),
extractFunction('normalizeText'),
extractFunction('getActionText'),
extractFunction('getFieldText'),
extractFunction('getVisibleControls'),
extractFunction('isEnabledControl'),
extractFunction('isDocumentLevelContainer'),
extractFunction('getCountryCandidates'),
extractFunction('matchesCountryOption'),
extractFunction('findCountryDropdown'),
extractFunction('getRegionCandidates'),
extractFunction('matchesRegionOption'),
extractFunction('findRegionDropdown'),
].join('\n');
const countryDropdown = createElement({
tagName: 'DIV',
text: '国家或地区 日本',
attrs: {
role: 'combobox',
'aria-haspopup': 'listbox',
},
});
const regionDropdown = createElement({
tagName: 'DIV',
text: '辖区 选择',
attrs: {
role: 'combobox',
'aria-haspopup': 'listbox',
},
});
const elements = [countryDropdown, regionDropdown];
const documentMock = {
documentElement: {},
body: {},
querySelectorAll: (selector) => {
if (String(selector || '').includes('label[for=')) return [];
if (String(selector || '').includes('combobox') || String(selector || '').includes('button') || String(selector || '').includes('select')) {
return elements;
}
return [];
},
};
const windowMock = {
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
};
const cssMock = {
escape: (value) => String(value),
};
const api = new Function('window', 'document', 'CSS', `
${bundle}
return { findCountryDropdown, findRegionDropdown, matchesCountryOption, matchesRegionOption };
`)(windowMock, documentMock, cssMock);
assert.equal(api.findCountryDropdown(), countryDropdown);
assert.equal(api.findRegionDropdown(), regionDropdown);
assert.equal(api.matchesCountryOption('日本', 'JP'), true);
assert.equal(api.matchesCountryOption('德国', 'DE'), true);
assert.equal(api.matchesRegionOption('東京都', 'Tokyo'), true);
});
test('fillIfEmpty can overwrite invalid structured address values in the dropdown branch', () => {
const bundle = [
extractFunction('fillIfEmpty'),
].join('\n');
const input = { value: '77022' };
const writes = [];
const api = new Function('input', 'writes', `
function fillInput(el, value) {
writes.push(value);
el.value = value;
}
${bundle}
return { fillIfEmpty };
`)(input, writes);
assert.equal(api.fillIfEmpty(input, '100-0005'), false);
assert.equal(input.value, '77022');
assert.equal(api.fillIfEmpty(input, '100-0005', { overwrite: true }), true);
assert.equal(input.value, '100-0005');
assert.deepEqual(writes, ['100-0005']);
});
@@ -344,7 +344,7 @@ test('Plus checkout billing uses the detected checkout country before choosing a
await executor.executePlusCheckoutBilling({ plusCheckoutCountry: 'DE' });
const combinedFillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
assert.equal(requestedCountries[0], 'Australia');
assert.equal(requestedCountries[0], 'AU');
assert.equal(combinedFillMessage.message.payload.addressSeed.countryCode, 'AU');
assert.equal(combinedFillMessage.message.payload.addressSeed.fallback.region, 'New South Wales');
assert.deepEqual(JSON.parse(fetchRequests[0].init.body), {
@@ -354,6 +354,56 @@ test('Plus checkout billing uses the detected checkout country before choosing a
});
});
test('Plus checkout billing uses meiguodizhi country paths for localized countries without local seeds', async () => {
const fetchRequests = [];
const { events, executor } = createExecutorHarness({
frames: [
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_ie/cs_test' },
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
],
stateByFrame: {
0: { hasPayPal: false, paypalCandidates: [], hasSubscribeButton: true },
7: { hasPayPal: true, paypalCandidates: [{ tag: 'button', text: 'PayPal' }] },
8: {
hasPayPal: false,
paypalCandidates: [],
billingFieldsVisible: true,
countryText: '日本',
},
},
fetchImpl: async (url, init) => {
fetchRequests.push({ url, init });
return {
ok: true,
status: 200,
json: async () => ({
status: 'ok',
address: {
Address: 'トウキョウト, ミナトク, シバダイモン, 10-4',
Trans_Address: '10-4, Shiba Daimon 2-chome, Minato-ku, Tokyo',
City: 'Tokyo',
State: 'Tokyo',
Zip_Code: '105-0012',
},
}),
};
},
});
await executor.executePlusCheckoutBilling({ plusCheckoutCountry: 'DE' });
const combinedFillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
assert.equal(combinedFillMessage.message.payload.addressSeed.countryCode, 'JP');
assert.equal(combinedFillMessage.message.payload.addressSeed.source, 'meiguodizhi');
assert.equal(combinedFillMessage.message.payload.addressSeed.fallback.address1, '10-4, Shiba Daimon 2-chome, Minato-ku, Tokyo');
assert.deepEqual(JSON.parse(fetchRequests[0].init.body), {
city: 'Tokyo',
path: '/jp-address',
method: 'refresh',
});
});
test('Plus checkout billing reports when the payment iframe exists but cannot receive the content script', async () => {
const { executor } = createExecutorHarness({
frames: [
+80 -1
View File
@@ -25,7 +25,10 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page'
ensureContentScriptReadyOnTabUntilStopped: async () => {
events.push({ type: 'ready' });
},
reuseOrCreateTab: async () => 42,
reuseOrCreateTab: async (source, url, options) => {
events.push({ type: 'reuse-tab', source, url, options });
return 42;
},
sendTabMessageUntilStopped: async () => ({
checkoutUrl: 'https://checkout.stripe.com/c/pay/session',
country: 'US',
@@ -44,6 +47,11 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page'
await executor.executePlusCheckoutCreate();
const reuseEvent = events.find((event) => event.type === 'reuse-tab');
assert.equal(reuseEvent.source, 'plus-checkout');
assert.equal(reuseEvent.options.reloadIfSameUrl, false);
assert.equal(Object.hasOwn(reuseEvent.options, 'inject'), false);
const sleepEvents = events.filter((event) => event.type === 'sleep');
assert.deepStrictEqual(sleepEvents.map((event) => event.ms), [1000, 1000]);
@@ -54,3 +62,74 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page'
assert.equal(events.some((event) => event.type === 'sleep' && event.ms === 20000), false);
assert.equal(events.some((event) => event.type === 'log' && /固定等待 20 秒后继续下一步/.test(event.message)), false);
});
test('Plus checkout create reuses the ChatGPT tab from signup step 5', async () => {
const events = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async (message, level = 'info') => {
events.push({ type: 'log', message, level });
},
chrome: {
tabs: {
get: async (tabId) => {
events.push({ type: 'tab-get', tabId });
return { id: tabId, url: 'https://chatgpt.com/' };
},
update: async (tabId, payload) => {
events.push({ type: 'tab-update', tabId, payload });
},
},
},
completeStepFromBackground: async (step, payload) => {
events.push({ type: 'complete', step, payload });
},
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => {
events.push({ type: 'ready', source, tabId, options });
},
getTabId: async (source) => {
events.push({ type: 'get-tab-id', source });
return source === 'signup-page' ? 55 : null;
},
isTabAlive: async (source) => {
events.push({ type: 'alive', source });
return source === 'signup-page';
},
registerTab: async (source, tabId) => {
events.push({ type: 'register', source, tabId });
},
reuseOrCreateTab: async () => {
events.push({ type: 'reuse-tab' });
return 42;
},
sendTabMessageUntilStopped: async () => ({
checkoutUrl: 'https://checkout.stripe.com/c/pay/session',
country: 'US',
currency: 'USD',
}),
setState: async (payload) => {
events.push({ type: 'set-state', payload });
},
sleepWithStop: async (ms) => {
events.push({ type: 'sleep', ms });
},
waitForTabCompleteUntilStopped: async (tabId) => {
events.push({ type: 'tab-complete', tabId });
},
});
await executor.executePlusCheckoutCreate();
assert.equal(events.some((event) => event.type === 'reuse-tab'), false);
assert.deepEqual(
events.find((event) => event.type === 'register'),
{ type: 'register', source: 'plus-checkout', tabId: 55 }
);
assert.deepEqual(
events.find((event) => event.type === 'ready').options.inject,
['content/plus-checkout.js']
);
assert.equal(
events.some((event) => event.type === 'log' && /直接接管当前标签页/.test(event.message)),
true
);
});