fix(plus): 增强 checkout 免费试用校验
(cherry picked from commit c52f5058a4b5b68902bccb4d1f099d00c471b7bc)
This commit is contained in:
@@ -180,6 +180,75 @@ return { findAddressSearchInput, isNonAddressSearchInput };
|
||||
assert.equal(await api.findAddressSearchInput(), addressInput);
|
||||
});
|
||||
|
||||
test('getCheckoutAmountSummary reads non-zero today due amount', () => {
|
||||
const label = createElement({ tagName: 'DIV', text: '今日应付金额' });
|
||||
const amount = createElement({ tagName: 'DIV', text: '€19.33' });
|
||||
const row = createElement({ tagName: 'DIV', text: '今日应付金额 €19.33' });
|
||||
label.parentElement = row;
|
||||
amount.parentElement = row;
|
||||
row.children = [label, amount];
|
||||
row.parentElement = null;
|
||||
|
||||
const bundle = [
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('parseLocalizedAmount'),
|
||||
extractFunction('getTextAfterTodayDueLabel'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('getCheckoutAmountSummary'),
|
||||
'return { getCheckoutAmountSummary };',
|
||||
].join('\n');
|
||||
|
||||
const api = new Function('window', 'document', bundle)(
|
||||
{
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
||||
},
|
||||
{
|
||||
querySelectorAll: () => [label, amount, row],
|
||||
}
|
||||
);
|
||||
|
||||
const summary = api.getCheckoutAmountSummary();
|
||||
assert.equal(summary.hasTodayDue, true);
|
||||
assert.equal(summary.isZero, false);
|
||||
assert.equal(summary.amount, 19.33);
|
||||
assert.equal(summary.rawAmount, '€19.33');
|
||||
});
|
||||
|
||||
test('getCheckoutAmountSummary accepts zero today due amount', () => {
|
||||
const label = createElement({ tagName: 'DIV', text: '今日应付金额' });
|
||||
const amount = createElement({ tagName: 'DIV', text: '€0.00' });
|
||||
const row = createElement({ tagName: 'DIV', text: '今日应付金额 €0.00' });
|
||||
label.parentElement = row;
|
||||
amount.parentElement = row;
|
||||
row.children = [label, amount];
|
||||
row.parentElement = null;
|
||||
|
||||
const bundle = [
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('parseLocalizedAmount'),
|
||||
extractFunction('getTextAfterTodayDueLabel'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('getCheckoutAmountSummary'),
|
||||
'return { getCheckoutAmountSummary };',
|
||||
].join('\n');
|
||||
|
||||
const api = new Function('window', 'document', bundle)(
|
||||
{
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
||||
},
|
||||
{
|
||||
querySelectorAll: () => [label, amount, row],
|
||||
}
|
||||
);
|
||||
|
||||
const summary = api.getCheckoutAmountSummary();
|
||||
assert.equal(summary.hasTodayDue, true);
|
||||
assert.equal(summary.isZero, true);
|
||||
assert.equal(summary.amount, 0);
|
||||
});
|
||||
|
||||
test('isPayPalPaymentMethodActive requires a selected PayPal control', () => {
|
||||
const bundle = [
|
||||
extractFunction('isVisibleElement'),
|
||||
@@ -234,6 +303,164 @@ return { isPayPalPaymentMethodActive };
|
||||
assert.equal(api.isPayPalPaymentMethodActive(), true);
|
||||
});
|
||||
|
||||
test('getStructuredAddressFields recognizes Stripe localized address field names', () => {
|
||||
const bundle = [
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getFieldText'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('getVisibleTextInputs'),
|
||||
extractFunction('findInputByFieldText'),
|
||||
extractFunction('getStructuredAddressFields'),
|
||||
].join('\n');
|
||||
|
||||
const address1 = createInput({ name: 'addressLine1', placeholder: '住所' });
|
||||
const city = createInput({ name: 'locality', placeholder: '市区町村' });
|
||||
const region = createInput({ name: 'administrativeArea', placeholder: '辖区' });
|
||||
const postalCode = createInput({ name: 'postalCode', placeholder: '郵便番号' });
|
||||
const inputs = [address1, city, region, postalCode];
|
||||
const documentMock = {
|
||||
querySelectorAll: (selector) => {
|
||||
if (selector === 'input, textarea') return inputs;
|
||||
if (String(selector || '').includes('label[for=')) return [];
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const windowMock = {
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
||||
};
|
||||
const cssMock = {
|
||||
escape: (value) => String(value),
|
||||
};
|
||||
|
||||
const api = new Function('window', 'document', 'CSS', `
|
||||
${bundle}
|
||||
return { getStructuredAddressFields };
|
||||
`)(windowMock, documentMock, cssMock);
|
||||
|
||||
assert.deepEqual(api.getStructuredAddressFields(), {
|
||||
address1,
|
||||
address2: null,
|
||||
city,
|
||||
region,
|
||||
postalCode,
|
||||
});
|
||||
});
|
||||
|
||||
test('findSubscribeButton prefers the submit subscription button', () => {
|
||||
const bundle = [
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getSearchText'),
|
||||
extractFunction('getFieldText'),
|
||||
extractFunction('getCombinedSearchText'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('findClickableByText'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('findSubscribeButton'),
|
||||
].join('\n');
|
||||
|
||||
const submitButton = createElement({
|
||||
tagName: 'BUTTON',
|
||||
text: '订阅',
|
||||
attrs: {
|
||||
'aria-label': '订阅',
|
||||
type: 'submit',
|
||||
},
|
||||
});
|
||||
submitButton.type = 'submit';
|
||||
|
||||
const genericButton = createElement({
|
||||
tagName: 'BUTTON',
|
||||
text: '继续',
|
||||
attrs: {
|
||||
type: 'button',
|
||||
},
|
||||
});
|
||||
genericButton.type = 'button';
|
||||
|
||||
const documentMock = {
|
||||
body: {},
|
||||
documentElement: {},
|
||||
querySelectorAll: (selector) => {
|
||||
if (selector === 'button[type="submit"], input[type="submit"]') return [submitButton];
|
||||
if (selector === 'button, a, [role="button"], input[type="button"], input[type="submit"], [tabindex]') {
|
||||
return [genericButton, submitButton];
|
||||
}
|
||||
if (String(selector || '').includes('label[for=')) return [];
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const windowMock = {
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
||||
};
|
||||
const cssMock = {
|
||||
escape: (value) => String(value),
|
||||
};
|
||||
|
||||
const api = new Function('window', 'document', 'CSS', `
|
||||
${bundle}
|
||||
return { findSubscribeButton };
|
||||
`)(windowMock, documentMock, cssMock);
|
||||
|
||||
assert.equal(api.findSubscribeButton(), submitButton);
|
||||
});
|
||||
|
||||
test('humanLikeClick submits a detached submit button through its form attribute', async () => {
|
||||
const bundle = [
|
||||
'function throwIfStopped() {}',
|
||||
'function sleep() { return Promise.resolve(); }',
|
||||
'function summarizeElementForDebug() { return {}; }',
|
||||
'function log() {}',
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getAssociatedForm'),
|
||||
extractFunction('humanLikeClick'),
|
||||
].join('\n');
|
||||
|
||||
let submittedWith = null;
|
||||
const form = {
|
||||
requestSubmit(button) {
|
||||
submittedWith = button;
|
||||
},
|
||||
};
|
||||
const button = createElement({
|
||||
tagName: 'BUTTON',
|
||||
text: '订阅',
|
||||
attrs: {
|
||||
form: '_r_l_',
|
||||
type: 'submit',
|
||||
'aria-label': '订阅',
|
||||
},
|
||||
});
|
||||
button.type = 'submit';
|
||||
button.scrollIntoView = () => {};
|
||||
button.focus = () => {};
|
||||
button.dispatchEvent = () => true;
|
||||
button.click = () => {};
|
||||
|
||||
const documentMock = {
|
||||
getElementById: (id) => (id === '_r_l_' ? form : null),
|
||||
};
|
||||
const windowMock = {};
|
||||
class FakeMouseEvent {
|
||||
constructor(type, init = {}) {
|
||||
this.type = type;
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
const api = new Function('window', 'document', 'MouseEvent', 'PointerEvent', 'console', `
|
||||
${bundle}
|
||||
return { humanLikeClick };
|
||||
`)(windowMock, documentMock, FakeMouseEvent, undefined, { log() {} });
|
||||
|
||||
await api.humanLikeClick(button);
|
||||
assert.equal(submittedWith, button);
|
||||
});
|
||||
|
||||
test('selectRegionDropdown opens the state dropdown and clicks the matching option', async () => {
|
||||
const bundle = [
|
||||
'function throwIfStopped() {}',
|
||||
|
||||
@@ -53,6 +53,7 @@ function createExecutorHarness({
|
||||
readyByFrame = {},
|
||||
fetchImpl = null,
|
||||
getAddressSeedForCountry = () => createAddressSeed(),
|
||||
markCurrentRegistrationAccountUsed = async () => {},
|
||||
}) {
|
||||
const api = loadPlusCheckoutBillingModule();
|
||||
const events = {
|
||||
@@ -100,6 +101,9 @@ function createExecutorHarness({
|
||||
if (message.type === 'PLUS_CHECKOUT_GET_STATE') {
|
||||
return stateByFrame[frameId] || { hasPayPal: false, paypalCandidates: [] };
|
||||
}
|
||||
if (message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE') {
|
||||
checkoutTab.url = 'https://www.paypal.com/checkoutnow';
|
||||
}
|
||||
return createSuccessfulBillingResult();
|
||||
},
|
||||
},
|
||||
@@ -121,6 +125,7 @@ function createExecutorHarness({
|
||||
getAddressSeedForCountry,
|
||||
getTabId: async () => null,
|
||||
isTabAlive: async () => false,
|
||||
markCurrentRegistrationAccountUsed,
|
||||
setState: async (updates) => events.states.push(updates),
|
||||
sleepWithStop: async () => {},
|
||||
waitForTabCompleteUntilStopped: async () => checkoutTab,
|
||||
@@ -134,6 +139,42 @@ function createExecutorHarness({
|
||||
return { checkoutTab, events, executor };
|
||||
}
|
||||
|
||||
test('Plus checkout billing stops before PayPal when today due amount is non-zero', async () => {
|
||||
const markCalls = [];
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_ie/cs_test' }],
|
||||
stateByFrame: {
|
||||
0: {
|
||||
hasPayPal: true,
|
||||
paypalCandidates: [{ tag: 'button', text: 'PayPal' }],
|
||||
billingFieldsVisible: true,
|
||||
hasSubscribeButton: true,
|
||||
checkoutAmountSummary: {
|
||||
hasTodayDue: true,
|
||||
amount: 19.33,
|
||||
isZero: false,
|
||||
rawAmount: '€19.33',
|
||||
},
|
||||
},
|
||||
},
|
||||
markCurrentRegistrationAccountUsed: async (state, options) => {
|
||||
markCalls.push({ state, options });
|
||||
return { updated: true };
|
||||
},
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => executor.executePlusCheckoutBilling({ email: 'paid@example.com' }),
|
||||
/PLUS_CHECKOUT_NON_FREE_TRIAL::/
|
||||
);
|
||||
|
||||
assert.equal(events.messages.some((entry) => entry.message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE'), false);
|
||||
assert.equal(events.completed.length, 0);
|
||||
assert.equal(markCalls.length, 1);
|
||||
assert.equal(markCalls[0].state.email, 'paid@example.com');
|
||||
assert.equal(events.logs.some((entry) => /今日应付金额不是 0/.test(entry.message)), true);
|
||||
});
|
||||
|
||||
test('Plus checkout billing uses the current checkout tab when step 6 did not register one', async () => {
|
||||
const { checkoutTab, events, executor } = createExecutorHarness({
|
||||
frames: [{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_ie/cs_test' }],
|
||||
|
||||
Reference in New Issue
Block a user