feat: 更新 Plus Checkout 逻辑,优化子框架的就绪状态处理,增加地址输入和 PayPal 付款方法的检测功能
This commit is contained in:
@@ -125,9 +125,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function inspectCheckoutFrame(tabId, frame) {
|
async function inspectCheckoutFrame(tabId, frame) {
|
||||||
if (frame.ready === false) {
|
|
||||||
return { frame, error: 'content-script-not-ready' };
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const result = await sendFrameMessage(tabId, frame.frameId, {
|
const result = await sendFrameMessage(tabId, frame.frameId, {
|
||||||
type: 'PLUS_CHECKOUT_GET_STATE',
|
type: 'PLUS_CHECKOUT_GET_STATE',
|
||||||
@@ -137,9 +134,11 @@
|
|||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
return { frame, error: result.error };
|
return { frame, error: result.error };
|
||||||
}
|
}
|
||||||
return { frame, result: result || {} };
|
return { frame: { ...frame, ready: true }, result: result || {} };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { frame, error: error?.message || String(error || '') };
|
const readyError = frame.ready === false ? 'content-script-not-ready' : '';
|
||||||
|
const message = error?.message || String(error || '');
|
||||||
|
return { frame, error: readyError ? `${readyError}: ${message}` : message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,29 @@ function findInputByFieldText(patterns, options = {}) {
|
|||||||
}) || null;
|
}) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDirectFieldHintText(el) {
|
||||||
|
const id = el?.id || '';
|
||||||
|
const labels = [];
|
||||||
|
if (id) {
|
||||||
|
labels.push(...Array.from(document.querySelectorAll(`label[for="${CSS.escape(id)}"]`)).map((label) => label.textContent));
|
||||||
|
}
|
||||||
|
const wrappingLabel = el?.closest?.('label');
|
||||||
|
if (wrappingLabel) {
|
||||||
|
labels.push(wrappingLabel.textContent);
|
||||||
|
}
|
||||||
|
return normalizeText([
|
||||||
|
getActionText(el),
|
||||||
|
...labels,
|
||||||
|
].filter(Boolean).join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNonAddressSearchInput(input) {
|
||||||
|
const directText = getDirectFieldHintText(input);
|
||||||
|
const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
|
||||||
|
return /name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-/i.test(directText)
|
||||||
|
|| ['email', 'tel', 'password'].includes(type);
|
||||||
|
}
|
||||||
|
|
||||||
function isDocumentLevelContainer(el) {
|
function isDocumentLevelContainer(el) {
|
||||||
return !el
|
return !el
|
||||||
|| el === document.documentElement
|
|| el === document.documentElement
|
||||||
@@ -432,9 +455,9 @@ async function selectPayPalPaymentMethod() {
|
|||||||
});
|
});
|
||||||
console.info('[MultiPage:plus-checkout] PayPal target selected', summarizeElementForDebug(target));
|
console.info('[MultiPage:plus-checkout] PayPal target selected', summarizeElementForDebug(target));
|
||||||
simulateClick(target);
|
simulateClick(target);
|
||||||
await sleep(1000);
|
log('Plus Checkout:已点击 PayPal 付款方式,正在确认选中状态。');
|
||||||
|
|
||||||
if (!isPayPalPaymentMethodActive()) {
|
if (!await waitForPayPalPaymentMethodActive()) {
|
||||||
const diagnostics = writePayPalDiagnostics('点击 PayPal 后页面仍未进入 PayPal 账单表单', 'error');
|
const diagnostics = writePayPalDiagnostics('点击 PayPal 后页面仍未进入 PayPal 账单表单', 'error');
|
||||||
throw new Error(`Plus Checkout:已尝试点击 PayPal,但页面未切换到 PayPal 表单。请提供控制台 PayPal diagnostics 结构。候选数量:${diagnostics.paypalCandidates.length},银行卡字段仍可见:${diagnostics.cardFieldsVisible ? '是' : '否'}。`);
|
throw new Error(`Plus Checkout:已尝试点击 PayPal,但页面未切换到 PayPal 表单。请提供控制台 PayPal diagnostics 结构。候选数量:${diagnostics.paypalCandidates.length},银行卡字段仍可见:${diagnostics.cardFieldsVisible ? '是' : '否'}。`);
|
||||||
}
|
}
|
||||||
@@ -484,6 +507,9 @@ function readCountryText() {
|
|||||||
|
|
||||||
function isLikelyAddressSearchInput(input) {
|
function isLikelyAddressSearchInput(input) {
|
||||||
const text = getFieldText(input);
|
const text = getFieldText(input);
|
||||||
|
if (isNonAddressSearchInput(input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (/name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(text)) {
|
if (/name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -533,10 +559,19 @@ function hasSelectedPayPalControl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isPayPalPaymentMethodActive() {
|
function isPayPalPaymentMethodActive() {
|
||||||
if (hasSelectedPayPalControl()) {
|
return hasSelectedPayPalControl();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForPayPalPaymentMethodActive(timeoutMs = 5000) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
throwIfStopped();
|
||||||
|
if (isPayPalPaymentMethodActive()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return getPayPalSearchCandidates().length > 0 && !hasCreditCardFields();
|
await sleep(250);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findAddressSearchInput() {
|
async function findAddressSearchInput() {
|
||||||
@@ -547,7 +582,7 @@ async function findAddressSearchInput() {
|
|||||||
], {
|
], {
|
||||||
exclude: (input) => /city|state|province|postal|zip|country|城市|省|州|邮编|国家|地区/i.test(getFieldText(input)),
|
exclude: (input) => /city|state|province|postal|zip|country|城市|省|州|邮编|国家|地区/i.test(getFieldText(input)),
|
||||||
});
|
});
|
||||||
if (direct) return direct;
|
if (direct && !isNonAddressSearchInput(direct)) return direct;
|
||||||
const candidates = getVisibleTextInputs().filter(isLikelyAddressSearchInput);
|
const candidates = getVisibleTextInputs().filter(isLikelyAddressSearchInput);
|
||||||
return candidates[0] || null;
|
return candidates[0] || null;
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
+15
-4
@@ -421,9 +421,20 @@ async function humanPause(min = 250, max = 850) {
|
|||||||
await sleep(duration);
|
await sleep(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-report ready on load
|
function shouldReportReadyForFrame(source, isChildFrame) {
|
||||||
// Skip ready signal from child iframes of mail pages to avoid overwriting the top frame's registration
|
if (!isChildFrame) return true;
|
||||||
const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163' || SCRIPT_SOURCE === 'gmail-mail' || SCRIPT_SOURCE === 'mail-2925' || SCRIPT_SOURCE === 'inbucket-mail') && window !== window.top;
|
return ![
|
||||||
if (!_isMailChildFrame) {
|
'qq-mail',
|
||||||
|
'mail-163',
|
||||||
|
'gmail-mail',
|
||||||
|
'mail-2925',
|
||||||
|
'inbucket-mail',
|
||||||
|
'plus-checkout',
|
||||||
|
].includes(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-report ready on load. Child frames are probed explicitly by frameId, so
|
||||||
|
// they should not overwrite the tab-level registration or spam the side panel.
|
||||||
|
if (shouldReportReadyForFrame(SCRIPT_SOURCE, window !== window.top)) {
|
||||||
reportReady();
|
reportReady();
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -1,6 +1,6 @@
|
|||||||
# Codex 注册扩展相关项目、更新、邮箱切换、PayPal 与 Clash Verge 配置教程
|
# Codex 注册扩展相关项目、更新、邮箱切换、PayPal 与 Clash Verge 配置教程
|
||||||
|
|
||||||
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email` 与 `QQ 邮箱` 的使用方法、`PayPal` 注册绑卡流程,以及 `Clash Verge` 的 `🔁 非港轮询` 配置方法。
|
本教程用于说明相关项目地址、扩展更新方式、`Cloudflare Temp Email` 与 `QQ 邮箱` 的使用方法、`PayPal` 注册绑卡流程,以及 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询` 配置方法。
|
||||||
|
|
||||||
## 适用场景
|
## 适用场景
|
||||||
|
|
||||||
@@ -21,15 +21,15 @@
|
|||||||
- 一个可正常接收短信的手机号
|
- 一个可正常接收短信的手机号
|
||||||
- 一张可在线支付的借记卡或信用卡
|
- 一张可在线支付的借记卡或信用卡
|
||||||
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
|
- 如需部署 `cpa`,部署环境必须可以访问 `OpenAI`
|
||||||
- 已安装 `Clash Verge`,并已导入可用订阅
|
- 已安装 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev),并已导入可用订阅
|
||||||
|
|
||||||
## 操作步骤
|
## 操作步骤
|
||||||
|
|
||||||
### 第一部分:相关项目地址与部署说明
|
### 第一部分:相关项目地址与部署说明
|
||||||
|
|
||||||
1. 查看项目地址
|
1. 查看项目地址
|
||||||
`cpa` 项目地址:`https://github.com/router-for-me/CLIProxyAPI`
|
`cpa` 项目地址:[https://github.com/router-for-me/CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)
|
||||||
`sub2api` 项目地址:`https://github.com/Wei-Shaw/sub2api`
|
`sub2api` 项目地址:[https://github.com/Wei-Shaw/sub2api](https://github.com/Wei-Shaw/sub2api)
|
||||||
|
|
||||||
2. 拉取项目到本地
|
2. 拉取项目到本地
|
||||||
先将你需要的项目拉取到本地目录。
|
先将你需要的项目拉取到本地目录。
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
如果两边都选择了它,就需要把两套配置都填完整。
|
如果两边都选择了它,就需要把两套配置都填完整。
|
||||||
|
|
||||||
2. 填写 `Temp API`
|
2. 填写 `Temp API`
|
||||||
这里填写 `Cloudflare Temp Email` 后端地址,例如 `https://your-worker-domain`。
|
这里填写 `Cloudflare Temp Email` 后端地址,例如 [https://your-worker-domain](https://your-worker-domain)。
|
||||||
不论你是拿它来生成邮箱,还是接收转发邮件,这一项都需要先配好。
|
不论你是拿它来生成邮箱,还是接收转发邮件,这一项都需要先配好。
|
||||||
|
|
||||||
3. 作为 `邮箱生成` 使用时填写 `Admin Auth`
|
3. 作为 `邮箱生成` 使用时填写 `Admin Auth`
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
先登录你当前正在使用的 `QQ 邮箱` 账号。
|
先登录你当前正在使用的 `QQ 邮箱` 账号。
|
||||||
|
|
||||||
2. 进入 `账号与安全` 页面
|
2. 进入 `账号与安全` 页面
|
||||||
打开 `账号与安全` 页面:`https://wx.mail.qq.com/account/index?sid=zdd4Voy7S04uZjBnAKhFZQAA#/`
|
打开 `账号与安全` 页面:[https://wx.mail.qq.com/account/index?sid=zdd4Voy7S04uZjBnAKhFZQAA#/](https://wx.mail.qq.com/account/index?sid=zdd4Voy7S04uZjBnAKhFZQAA#/)
|
||||||
|
|
||||||
3. 进入 `账号管理`
|
3. 进入 `账号管理`
|
||||||
在 `账号与安全` 页面中找到 `账号管理`。
|
在 `账号与安全` 页面中找到 `账号管理`。
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
### 第五部分:`PayPal` 注册与绑卡使用教程
|
### 第五部分:`PayPal` 注册与绑卡使用教程
|
||||||
|
|
||||||
1. 打开注册页面
|
1. 打开注册页面
|
||||||
打开 `https://www.paypal.com/signin`。
|
打开 [https://www.paypal.com/signin](https://www.paypal.com/signin)。
|
||||||
然后点击 `注册`。
|
然后点击 `注册`。
|
||||||
|
|
||||||
2. 选择账户类型
|
2. 选择账户类型
|
||||||
@@ -171,11 +171,11 @@
|
|||||||
常见情况是上传身份证件。
|
常见情况是上传身份证件。
|
||||||
`PayPal` 官方帮助中心说明,通常会在 `2 个工作日` 内审核,但某些情况可能更久。
|
`PayPal` 官方帮助中心说明,通常会在 `2 个工作日` 内审核,但某些情况可能更久。
|
||||||
|
|
||||||
### 第六部分:配置 `Clash Verge` 的 `🔁 非港轮询`
|
### 第六部分:配置 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev) 的 `🔁 非港轮询`
|
||||||
|
|
||||||
#### 第一步:添加扩展脚本
|
#### 第一步:添加扩展脚本
|
||||||
|
|
||||||
1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。
|
1. 打开 [Clash Verge](https://github.com/clash-verge-rev/clash-verge-rev),进入左侧的 `订阅`(`Profiles`)界面。
|
||||||
2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`。
|
2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`。
|
||||||

|

|
||||||
3. 将里面的内容全部清空,替换为下方脚本代码。
|
3. 将里面的内容全部清空,替换为下方脚本代码。
|
||||||
|
|||||||
@@ -80,3 +80,15 @@ return { detectScriptSource };
|
|||||||
'mail-163'
|
'mail-163'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shouldReportReadyForFrame suppresses noisy plus checkout child frame ready logs', () => {
|
||||||
|
const bundle = [extractFunction('shouldReportReadyForFrame')].join('\n');
|
||||||
|
const api = new Function(`
|
||||||
|
${bundle}
|
||||||
|
return { shouldReportReadyForFrame };
|
||||||
|
`)();
|
||||||
|
|
||||||
|
assert.equal(api.shouldReportReadyForFrame('plus-checkout', true), false);
|
||||||
|
assert.equal(api.shouldReportReadyForFrame('plus-checkout', false), true);
|
||||||
|
assert.equal(api.shouldReportReadyForFrame('paypal-flow', true), true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const source = fs.readFileSync('content/plus-checkout.js', 'utf8');
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -67,12 +67,16 @@ function createExecutorHarness({ frames, stateByFrame, readyByFrame = {} }) {
|
|||||||
sendMessage: async (tabId, message, options = {}) => {
|
sendMessage: async (tabId, message, options = {}) => {
|
||||||
const frameId = Number.isInteger(options.frameId) ? options.frameId : 0;
|
const frameId = Number.isInteger(options.frameId) ? options.frameId : 0;
|
||||||
events.messages.push({ tabId, message, frameId });
|
events.messages.push({ tabId, message, frameId });
|
||||||
|
const hasConfiguredState = Object.prototype.hasOwnProperty.call(stateByFrame, frameId);
|
||||||
if (message.type === 'PING') {
|
if (message.type === 'PING') {
|
||||||
if (readyByFrame[frameId] === false) {
|
if (readyByFrame[frameId] === false) {
|
||||||
throw new Error('No receiving end');
|
throw new Error('No receiving end');
|
||||||
}
|
}
|
||||||
return { ok: true, source: 'plus-checkout' };
|
return { ok: true, source: 'plus-checkout' };
|
||||||
}
|
}
|
||||||
|
if (readyByFrame[frameId] === false && !hasConfiguredState) {
|
||||||
|
throw new Error('No receiving end');
|
||||||
|
}
|
||||||
if (message.type === 'PLUS_CHECKOUT_GET_STATE') {
|
if (message.type === 'PLUS_CHECKOUT_GET_STATE') {
|
||||||
return stateByFrame[frameId] || { hasPayPal: false, paypalCandidates: [] };
|
return stateByFrame[frameId] || { hasPayPal: false, paypalCandidates: [] };
|
||||||
}
|
}
|
||||||
@@ -159,6 +163,34 @@ test('Plus checkout billing sends the billing command to the iframe that contain
|
|||||||
assert.equal(events.completed[0].step, 7);
|
assert.equal(events.completed[0].step, 7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Plus checkout billing still inspects a frame when ping readiness is stale', async () => {
|
||||||
|
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: true,
|
||||||
|
paypalCandidates: [{ tag: 'button', text: 'PayPal' }],
|
||||||
|
hasSubscribeButton: true,
|
||||||
|
},
|
||||||
|
7: { hasPayPal: false, paypalCandidates: [] },
|
||||||
|
8: { hasPayPal: false, paypalCandidates: [], billingFieldsVisible: true },
|
||||||
|
},
|
||||||
|
readyByFrame: {
|
||||||
|
0: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executePlusCheckoutBilling({});
|
||||||
|
|
||||||
|
const selectMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL');
|
||||||
|
assert.equal(selectMessage.frameId, 0);
|
||||||
|
assert.equal(events.completed[0].step, 7);
|
||||||
|
});
|
||||||
|
|
||||||
test('Plus checkout billing uses the autocomplete iframe for address suggestions when Stripe splits it out', async () => {
|
test('Plus checkout billing uses the autocomplete iframe for address suggestions when Stripe splits it out', async () => {
|
||||||
const { events, executor } = createExecutorHarness({
|
const { events, executor } = createExecutorHarness({
|
||||||
frames: [
|
frames: [
|
||||||
|
|||||||
Reference in New Issue
Block a user