376 lines
11 KiB
JavaScript
376 lines
11 KiB
JavaScript
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}(`);
|
|
const start = asyncStart >= 0
|
|
? asyncStart
|
|
: plainStart;
|
|
if (start < 0) {
|
|
throw new Error(`missing function ${name}`);
|
|
}
|
|
|
|
let parenDepth = 0;
|
|
let signatureEnded = false;
|
|
let braceStart = -1;
|
|
for (let index = start; index < source.length; index += 1) {
|
|
const ch = source[index];
|
|
if (ch === '(') {
|
|
parenDepth += 1;
|
|
} else if (ch === ')') {
|
|
parenDepth -= 1;
|
|
if (parenDepth === 0) {
|
|
signatureEnded = true;
|
|
}
|
|
} else if (ch === '{' && signatureEnded) {
|
|
braceStart = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (braceStart < 0) {
|
|
throw new Error(`missing body for function ${name}`);
|
|
}
|
|
|
|
let depth = 0;
|
|
let end = braceStart;
|
|
for (; end < source.length; end += 1) {
|
|
const ch = source[end];
|
|
if (ch === '{') depth += 1;
|
|
if (ch === '}') {
|
|
depth -= 1;
|
|
if (depth === 0) {
|
|
end += 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
function createInput({ id = '', name = '', placeholder = '', containerText = '' }) {
|
|
const attrs = {
|
|
id,
|
|
name,
|
|
placeholder,
|
|
type: 'text',
|
|
};
|
|
const container = {
|
|
textContent: containerText,
|
|
};
|
|
return {
|
|
id,
|
|
name,
|
|
type: 'text',
|
|
value: '',
|
|
textContent: '',
|
|
getAttribute: (key) => attrs[key] || '',
|
|
closest: (selector) => {
|
|
if (selector === 'label') return null;
|
|
if (String(selector || '').includes('[data-testid]')) return container;
|
|
return null;
|
|
},
|
|
getBoundingClientRect: () => ({ width: 240, height: 40 }),
|
|
};
|
|
}
|
|
|
|
function createElement({ tagName = 'BUTTON', text = '', attrs = {}, className = '' }) {
|
|
return {
|
|
tagName,
|
|
textContent: text,
|
|
value: '',
|
|
className,
|
|
dataset: {},
|
|
id: attrs.id || '',
|
|
checked: false,
|
|
getAttribute: (key) => attrs[key] || '',
|
|
closest: () => null,
|
|
getBoundingClientRect: () => ({ width: 180, height: 64 }),
|
|
};
|
|
}
|
|
|
|
test('findAddressSearchInput skips the name field when its container says billing address', async () => {
|
|
const bundle = [
|
|
'function throwIfStopped() {}',
|
|
'function sleep() { return Promise.resolve(); }',
|
|
extractFunction('waitUntil'),
|
|
extractFunction('isVisibleElement'),
|
|
extractFunction('normalizeText'),
|
|
extractFunction('getActionText'),
|
|
extractFunction('getFieldText'),
|
|
extractFunction('getVisibleControls'),
|
|
extractFunction('getVisibleTextInputs'),
|
|
extractFunction('findInputByFieldText'),
|
|
extractFunction('getDirectFieldHintText'),
|
|
extractFunction('isNonAddressSearchInput'),
|
|
extractFunction('isLikelyAddressSearchInput'),
|
|
extractFunction('findAddressSearchInput'),
|
|
].join('\n');
|
|
|
|
const nameInput = createInput({
|
|
name: 'name',
|
|
placeholder: 'Name',
|
|
containerText: 'Billing address',
|
|
});
|
|
const addressInput = createInput({
|
|
name: 'addressLine1',
|
|
placeholder: 'Address',
|
|
containerText: 'Billing address',
|
|
});
|
|
const inputs = [nameInput, addressInput];
|
|
const documentMock = {
|
|
readyState: 'complete',
|
|
querySelectorAll: (selector) => {
|
|
if (selector === 'input, textarea') return inputs;
|
|
return [];
|
|
},
|
|
};
|
|
const windowMock = {
|
|
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
|
};
|
|
const cssMock = {
|
|
escape: (value) => String(value),
|
|
};
|
|
|
|
const api = new Function('window', 'document', 'CSS', `
|
|
${bundle}
|
|
return { findAddressSearchInput, isNonAddressSearchInput };
|
|
`)(windowMock, documentMock, cssMock);
|
|
|
|
assert.equal(api.isNonAddressSearchInput(nameInput), true);
|
|
assert.equal(await api.findAddressSearchInput(), addressInput);
|
|
});
|
|
|
|
test('isPayPalPaymentMethodActive requires a selected PayPal control', () => {
|
|
const bundle = [
|
|
extractFunction('isVisibleElement'),
|
|
extractFunction('normalizeText'),
|
|
extractFunction('getActionText'),
|
|
extractFunction('getSearchText'),
|
|
extractFunction('getFieldText'),
|
|
extractFunction('getCombinedSearchText'),
|
|
extractFunction('getVisibleControls'),
|
|
extractFunction('getVisibleTextInputs'),
|
|
extractFunction('isDocumentLevelContainer'),
|
|
extractFunction('getPayPalSearchCandidates'),
|
|
extractFunction('hasCreditCardFields'),
|
|
extractFunction('hasSelectedPayPalControl'),
|
|
extractFunction('isPayPalPaymentMethodActive'),
|
|
].join('\n');
|
|
|
|
const paypalButton = createElement({
|
|
text: 'PayPal',
|
|
attrs: {
|
|
id: 'paypal-tab',
|
|
role: 'tab',
|
|
'aria-selected': '',
|
|
},
|
|
});
|
|
const elements = [paypalButton];
|
|
const documentMock = {
|
|
documentElement: {},
|
|
body: {},
|
|
querySelectorAll: (selector) => {
|
|
if (selector === 'input, textarea') return [];
|
|
if (String(selector || '').includes('label[for=')) return [];
|
|
return elements;
|
|
},
|
|
};
|
|
const windowMock = {
|
|
innerWidth: 1200,
|
|
innerHeight: 900,
|
|
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
|
};
|
|
const cssMock = {
|
|
escape: (value) => String(value),
|
|
};
|
|
|
|
const api = new Function('window', 'document', 'CSS', `
|
|
${bundle}
|
|
return { isPayPalPaymentMethodActive };
|
|
`)(windowMock, documentMock, cssMock);
|
|
|
|
assert.equal(api.isPayPalPaymentMethodActive(), false);
|
|
paypalButton.getAttribute = (key) => (key === 'aria-selected' ? 'true' : (paypalButton.id && key === 'id' ? paypalButton.id : ''));
|
|
assert.equal(api.isPayPalPaymentMethodActive(), true);
|
|
});
|
|
|
|
test('selectRegionDropdown opens the state dropdown and clicks the matching option', async () => {
|
|
const bundle = [
|
|
'function throwIfStopped() {}',
|
|
'function sleep() { return Promise.resolve(); }',
|
|
extractFunction('isVisibleElement'),
|
|
extractFunction('normalizeText'),
|
|
extractFunction('getActionText'),
|
|
extractFunction('getRegionCandidates'),
|
|
extractFunction('matchesRegionOption'),
|
|
extractFunction('getRegionDropdownValue'),
|
|
extractFunction('getVisibleRegionOptions'),
|
|
extractFunction('selectRegionDropdown'),
|
|
].join('\n');
|
|
|
|
const state = { opened: false };
|
|
const clicks = [];
|
|
const stateDropdown = createElement({
|
|
tagName: 'DIV',
|
|
text: 'State New South Wales',
|
|
attrs: {
|
|
role: 'combobox',
|
|
'aria-haspopup': 'listbox',
|
|
},
|
|
});
|
|
const options = [
|
|
createElement({ tagName: 'DIV', text: 'New South Wales', attrs: { role: 'option' } }),
|
|
createElement({ tagName: 'DIV', text: 'Western Australia', attrs: { role: 'option' } }),
|
|
];
|
|
const documentMock = {
|
|
querySelectorAll: (selector) => {
|
|
if (!state.opened) return [];
|
|
if (selector === '[role="listbox"] [role="option"]' || selector === '[role="option"]') return options;
|
|
if (selector === 'li') return [];
|
|
return [];
|
|
},
|
|
};
|
|
const windowMock = {
|
|
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
|
};
|
|
|
|
const api = new Function('window', 'document', 'Event', 'clicks', 'stateDropdown', 'state', `
|
|
function simulateClick(el) {
|
|
clicks.push(el);
|
|
if (el === stateDropdown) state.opened = true;
|
|
}
|
|
${bundle}
|
|
return { selectRegionDropdown };
|
|
`)(windowMock, documentMock, Event, clicks, stateDropdown, state);
|
|
|
|
await api.selectRegionDropdown(stateDropdown, 'Western Australia');
|
|
|
|
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']);
|
|
});
|