fix: harden signup verification recovery for Cloudflare and split OTP flows
- 合并 PR #130 的核心改动:修正 Cloudflare Temp Email 在 Step 4 的验证码轮询入口,并补上提交后认证重试页恢复逻辑 - 本地补充修复:正确识别分格 OTP 输入框,不再把单字符 numeric 输入误判为单一验证码框;连续重试页恢复失败时也不再误判成功 - 影响范围:background Step 4 验证码入口、signup-page 内容脚本、相关回归测试
This commit is contained in:
@@ -122,11 +122,16 @@
|
||||
await focusOrOpenMailTab(mail);
|
||||
}
|
||||
|
||||
const shouldRequestFreshCodeFirst = ![
|
||||
HOTMAIL_PROVIDER,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
].includes(mail.provider);
|
||||
|
||||
await resolveVerificationStep(4, state, mail, {
|
||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
|
||||
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
|
||||
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
? 0
|
||||
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
|
||||
+177
-33
@@ -114,16 +114,24 @@ function isVisibleElement(el) {
|
||||
&& rect.height > 0;
|
||||
}
|
||||
|
||||
function getVisibleSplitVerificationInputs() {
|
||||
return Array.from(document.querySelectorAll('input[maxlength="1"]'))
|
||||
.filter(isVisibleElement);
|
||||
}
|
||||
|
||||
function getVerificationCodeTarget() {
|
||||
const splitInputs = getVisibleSplitVerificationInputs();
|
||||
const codeInput = document.querySelector(VERIFICATION_CODE_INPUT_SELECTOR);
|
||||
if (codeInput && isVisibleElement(codeInput)) {
|
||||
const maxLength = Number(codeInput.getAttribute?.('maxlength') || codeInput.maxLength || 0);
|
||||
if (maxLength === 1 && splitInputs.length >= 6) {
|
||||
return { type: 'split', elements: splitInputs };
|
||||
}
|
||||
return { type: 'single', element: codeInput };
|
||||
}
|
||||
|
||||
const singleInputs = Array.from(document.querySelectorAll('input[maxlength="1"]'))
|
||||
.filter(isVisibleElement);
|
||||
if (singleInputs.length >= 6) {
|
||||
return { type: 'split', elements: singleInputs };
|
||||
if (splitInputs.length >= 6) {
|
||||
return { type: 'split', elements: splitInputs };
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1702,15 +1710,30 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
|
||||
async function waitForVerificationSubmitOutcome(step, timeout) {
|
||||
const resolvedTimeout = timeout ?? (step === 8 ? 30000 : 12000);
|
||||
const start = Date.now();
|
||||
let recoveryCount = 0;
|
||||
const maxRecoveryCount = 2;
|
||||
|
||||
while (Date.now() - start < resolvedTimeout) {
|
||||
throwIfStopped();
|
||||
|
||||
if (step === 4) {
|
||||
const signupRetryState = getCurrentAuthRetryPageState('signup');
|
||||
if (signupRetryState?.userAlreadyExistsBlocked) {
|
||||
throw createSignupUserAlreadyExistsError();
|
||||
const retryFlow = step === 4 ? 'signup' : 'login';
|
||||
const retryState = getCurrentAuthRetryPageState(retryFlow);
|
||||
if (retryState?.userAlreadyExistsBlocked) {
|
||||
throw createSignupUserAlreadyExistsError();
|
||||
}
|
||||
if (retryState) {
|
||||
if (recoveryCount >= maxRecoveryCount) {
|
||||
throw new Error(`步骤 ${step}:验证码提交后连续进入认证重试页 ${maxRecoveryCount} 次,页面仍未恢复。URL: ${location.href}`);
|
||||
}
|
||||
recoveryCount += 1;
|
||||
log(`步骤 ${step}:验证码提交后进入认证重试页,正在自动恢复(${recoveryCount}/${maxRecoveryCount})...`, 'warn');
|
||||
await recoverCurrentAuthRetryPage({
|
||||
flow: retryFlow,
|
||||
logLabel: `步骤 ${step}:验证码提交后检测到认证重试页,正在点击“重试”恢复`,
|
||||
step,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const errorText = getVerificationErrorText();
|
||||
@@ -1750,6 +1773,96 @@ async function waitForVerificationSubmitOutcome(step, timeout) {
|
||||
return { success: true, assumed: true };
|
||||
}
|
||||
|
||||
function getVerificationSubmitButtonForTarget(codeInput, options = {}) {
|
||||
const { allowDisabled = false } = options;
|
||||
const form = codeInput?.form || codeInput?.closest?.('form') || null;
|
||||
const isUsableAction = (element) => {
|
||||
if (!element || !isVisibleElement(element)) return false;
|
||||
return allowDisabled || isActionEnabled(element);
|
||||
};
|
||||
|
||||
const findSubmitInRoot = (root) => {
|
||||
if (!root?.querySelectorAll) return null;
|
||||
|
||||
const directCandidates = root.querySelectorAll('button[type="submit"], input[type="submit"]');
|
||||
for (const element of directCandidates) {
|
||||
if (isUsableAction(element)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
const textCandidates = root.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]');
|
||||
return Array.from(textCandidates).find((element) => {
|
||||
if (!isUsableAction(element)) return false;
|
||||
const text = getActionText(element);
|
||||
return /verify|confirm|submit|continue|确认|验证|继续/i.test(text);
|
||||
}) || null;
|
||||
};
|
||||
|
||||
return findSubmitInRoot(form) || findSubmitInRoot(document);
|
||||
}
|
||||
|
||||
async function waitForVerificationSubmitButton(codeInput, timeout = 5000) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
throw new Error('当前页面处于 405 错误恢复流程中,暂时无法定位验证码提交按钮。');
|
||||
}
|
||||
|
||||
const button = getVerificationSubmitButtonForTarget(codeInput, { allowDisabled: false });
|
||||
if (button) {
|
||||
return button;
|
||||
}
|
||||
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForVerificationCodeTarget(timeout = 10000) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
throw new Error('当前页面处于 405 错误恢复流程中,暂时无法定位验证码输入框。');
|
||||
}
|
||||
|
||||
const target = getVerificationCodeTarget();
|
||||
if (target) {
|
||||
return target;
|
||||
}
|
||||
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
throw new Error('未找到验证码输入框。URL: ' + location.href);
|
||||
}
|
||||
|
||||
async function waitForSplitVerificationInputsFilled(inputs, code, timeout = 2500) {
|
||||
const expected = String(code || '').slice(0, 6);
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
const current = Array.from(inputs || [])
|
||||
.slice(0, expected.length)
|
||||
.map((input) => String(input?.value || '').trim())
|
||||
.join('');
|
||||
|
||||
if (current === expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function fillVerificationCode(step, payload) {
|
||||
const { code } = payload;
|
||||
if (!code) throw new Error('未提供验证码。');
|
||||
@@ -1764,6 +1877,7 @@ async function fillVerificationCode(step, payload) {
|
||||
// Retry with 405 error recovery if needed
|
||||
const maxRetries = 3;
|
||||
let codeInput = null;
|
||||
let splitInputs = null;
|
||||
|
||||
for (let retry = 0; retry <= maxRetries; retry++) {
|
||||
throwIfStopped();
|
||||
@@ -1776,28 +1890,14 @@ async function fillVerificationCode(step, payload) {
|
||||
}
|
||||
|
||||
try {
|
||||
codeInput = await waitForElement(VERIFICATION_CODE_INPUT_SELECTOR, 10000);
|
||||
const verificationTarget = await waitForVerificationCodeTarget(10000);
|
||||
if (verificationTarget.type === 'split') {
|
||||
splitInputs = verificationTarget.elements;
|
||||
} else {
|
||||
codeInput = verificationTarget.element;
|
||||
}
|
||||
break; // Found it
|
||||
} catch {
|
||||
// Check for multiple single-digit inputs (common pattern)
|
||||
const singleInputs = document.querySelectorAll('input[maxlength="1"]');
|
||||
if (singleInputs.length >= 6) {
|
||||
log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
|
||||
for (let i = 0; i < 6 && i < singleInputs.length; i++) {
|
||||
fillInput(singleInputs[i], code[i]);
|
||||
await sleep(100);
|
||||
}
|
||||
const outcome = await waitForVerificationSubmitOutcome(step);
|
||||
if (outcome.invalidCode) {
|
||||
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
|
||||
} else if (outcome.addPhonePage) {
|
||||
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
|
||||
} else {
|
||||
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
|
||||
// No input found — check if it's a 405 error and can be recovered
|
||||
if (is405MethodNotAllowedPage() && retry < maxRetries) {
|
||||
log(`步骤 ${step}:未找到验证码输入框且页面出现 405 错误,正在恢复...`, 'warn');
|
||||
@@ -1809,6 +1909,51 @@ async function fillVerificationCode(step, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
if (splitInputs?.length >= 6) {
|
||||
log(`步骤 ${step}:发现分开的单字符验证码输入框,正在逐个填写...`);
|
||||
for (let i = 0; i < 6 && i < splitInputs.length; i++) {
|
||||
const targetInput = splitInputs[i];
|
||||
try {
|
||||
targetInput.focus?.();
|
||||
} catch {}
|
||||
fillInput(splitInputs[i], code[i]);
|
||||
try {
|
||||
targetInput.dispatchEvent(new KeyboardEvent('keyup', { key: code[i], bubbles: true }));
|
||||
} catch {}
|
||||
await sleep(100);
|
||||
}
|
||||
const filled = await waitForSplitVerificationInputsFilled(splitInputs, code, 2500);
|
||||
if (!filled) {
|
||||
const current = Array.from(splitInputs)
|
||||
.slice(0, 6)
|
||||
.map((input) => String(input?.value || '').trim() || '_')
|
||||
.join('');
|
||||
log(`步骤 ${step}:分格验证码输入框未稳定呈现目标值,当前页面值为 ${current},准备继续观察提交流程。`, 'warn');
|
||||
} else {
|
||||
log(`步骤 ${step}:分格验证码输入框已稳定显示 ${code}。`, 'info');
|
||||
}
|
||||
|
||||
await sleep(800);
|
||||
const splitSubmitBtn = await waitForVerificationSubmitButton(splitInputs[0], 2000).catch(() => null);
|
||||
if (splitSubmitBtn) {
|
||||
await humanPause(450, 1200);
|
||||
simulateClick(splitSubmitBtn);
|
||||
log(`步骤 ${step}:分格验证码已提交`);
|
||||
} else {
|
||||
log(`步骤 ${step}:分格验证码页面未找到可点击提交按钮,继续等待页面自动推进。`, 'info');
|
||||
}
|
||||
|
||||
const outcome = await waitForVerificationSubmitOutcome(step);
|
||||
if (outcome.invalidCode) {
|
||||
log(`步骤 ${step}:验证码被拒绝:${outcome.errorText}`, 'warn');
|
||||
} else if (outcome.addPhonePage) {
|
||||
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
|
||||
} else {
|
||||
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}。`, 'ok');
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
|
||||
if (!codeInput) {
|
||||
throw new Error('未找到验证码输入框。URL: ' + location.href);
|
||||
}
|
||||
@@ -1816,17 +1961,16 @@ async function fillVerificationCode(step, payload) {
|
||||
fillInput(codeInput, code);
|
||||
log(`步骤 ${step}:验证码已填写`);
|
||||
|
||||
// Report complete BEFORE submit (page may navigate away)
|
||||
|
||||
// Submit
|
||||
await sleep(500);
|
||||
const submitBtn = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /verify|confirm|submit|continue|确认|验证/i, 5000).catch(() => null);
|
||||
await sleep(800);
|
||||
const submitBtn = await waitForVerificationSubmitButton(codeInput, 5000).catch(() => null);
|
||||
|
||||
if (submitBtn) {
|
||||
await humanPause(450, 1200);
|
||||
simulateClick(submitBtn);
|
||||
log(`步骤 ${step}:验证码已提交`);
|
||||
} else {
|
||||
log(`步骤 ${step}:未找到可提交的验证码按钮,先等待页面自动推进或反馈结果。`, 'warn');
|
||||
}
|
||||
|
||||
const outcome = await waitForVerificationSubmitOutcome(step);
|
||||
|
||||
@@ -69,3 +69,53 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
|
||||
assert.equal(capturedOptions.filterAfterTimestamp, 100000);
|
||||
assert.equal(capturedOptions.resendIntervalMs, 0);
|
||||
});
|
||||
|
||||
test('step 4 does not request a fresh code first for Cloudflare temp mail', async () => {
|
||||
let capturedOptions = null;
|
||||
const realDateNow = Date.now;
|
||||
Date.now = () => 700000;
|
||||
|
||||
const executor = api.createStep4Executor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureMail2925MailboxSession: async () => {},
|
||||
getMailConfig: () => ({
|
||||
provider: 'cloudflare-temp-email',
|
||||
label: 'Cloudflare Temp Email',
|
||||
source: 'cloudflare-temp-email',
|
||||
url: 'https://temp.peekcart.com',
|
||||
}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isTabAlive: async () => true,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
resolveVerificationStep: async (_step, _state, _mail, options) => {
|
||||
capturedOptions = options;
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async () => ({}),
|
||||
shouldUseCustomRegistrationEmail: () => false,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
try {
|
||||
await executor.executeStep4({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
});
|
||||
} finally {
|
||||
Date.now = realDateNow;
|
||||
}
|
||||
|
||||
assert.equal(capturedOptions.filterAfterTimestamp, 700000);
|
||||
assert.equal(capturedOptions.requestFreshCodeFirst, false);
|
||||
assert.equal(capturedOptions.resendIntervalMs, 25000);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/signup-page.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
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);
|
||||
}
|
||||
|
||||
test('fillVerificationCode submits after split inputs are stably filled', async () => {
|
||||
const api = new Function(`
|
||||
const logs = [];
|
||||
const clicks = [];
|
||||
const filledValues = [];
|
||||
let submitClicked = false;
|
||||
const VERIFICATION_CODE_INPUT_SELECTOR = 'input[data-verification-code]';
|
||||
const location = { href: 'https://auth.openai.com/email-verification' };
|
||||
function KeyboardEvent(type, init = {}) {
|
||||
this.type = type;
|
||||
Object.assign(this, init);
|
||||
}
|
||||
|
||||
const submitBtn = {
|
||||
tagName: 'BUTTON',
|
||||
textContent: 'Continue',
|
||||
disabled: false,
|
||||
getAttribute(name) {
|
||||
if (name === 'type') return 'submit';
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
click() {
|
||||
submitClicked = true;
|
||||
},
|
||||
};
|
||||
|
||||
const inputs = Array.from({ length: 6 }, () => ({
|
||||
value: '',
|
||||
maxLength: 1,
|
||||
getAttribute(name) {
|
||||
if (name === 'maxlength') return '1';
|
||||
if (name === 'aria-disabled') return 'false';
|
||||
return '';
|
||||
},
|
||||
focus() {},
|
||||
dispatchEvent() {},
|
||||
closest() { return null; },
|
||||
}));
|
||||
|
||||
const document = {
|
||||
querySelector(selector) {
|
||||
if (selector === VERIFICATION_CODE_INPUT_SELECTOR) return inputs[0];
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'input[maxlength="1"]') return inputs;
|
||||
if (selector === 'button[type="submit"], input[type="submit"]') return [submitBtn];
|
||||
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [submitBtn];
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
function throwIfStopped() {}
|
||||
function log(message, level = 'info') { logs.push({ message, level }); }
|
||||
async function waitForLoginVerificationPageReady() {}
|
||||
function is405MethodNotAllowedPage() { return false; }
|
||||
async function handle405ResendError() {}
|
||||
function fillInput(el, value) {
|
||||
el.value = value;
|
||||
filledValues.push(value);
|
||||
}
|
||||
async function sleep() {}
|
||||
function isVisibleElement() { return true; }
|
||||
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
|
||||
function getActionText(el) { return el.textContent || ''; }
|
||||
async function humanPause() {}
|
||||
function simulateClick(el) { el.click(); clicks.push(el.textContent); }
|
||||
async function waitForVerificationSubmitOutcome() { return { success: true }; }
|
||||
|
||||
${extractFunction('getVisibleSplitVerificationInputs')}
|
||||
${extractFunction('getVerificationCodeTarget')}
|
||||
${extractFunction('getVerificationSubmitButtonForTarget')}
|
||||
${extractFunction('waitForVerificationSubmitButton')}
|
||||
${extractFunction('waitForVerificationCodeTarget')}
|
||||
${extractFunction('waitForSplitVerificationInputsFilled')}
|
||||
${extractFunction('fillVerificationCode')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return fillVerificationCode(4, { code: '123456' });
|
||||
},
|
||||
snapshot() {
|
||||
return {
|
||||
logs,
|
||||
clicks,
|
||||
filledValues,
|
||||
submitClicked,
|
||||
currentValue: inputs.map((input) => input.value).join(''),
|
||||
};
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
const snapshot = api.snapshot();
|
||||
|
||||
assert.deepStrictEqual(result, { success: true });
|
||||
assert.equal(snapshot.currentValue, '123456');
|
||||
assert.equal(snapshot.submitClicked, true);
|
||||
assert.deepStrictEqual(snapshot.clicks, ['Continue']);
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/signup-page.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
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);
|
||||
}
|
||||
|
||||
test('waitForVerificationSubmitOutcome recovers signup retry page after submit', async () => {
|
||||
const api = new Function(`
|
||||
let retryVisible = true;
|
||||
let step5Ready = false;
|
||||
let recoverCalls = 0;
|
||||
const location = { href: 'https://auth.openai.com/email-verification' };
|
||||
|
||||
function throwIfStopped() {}
|
||||
function log() {}
|
||||
function getVerificationErrorText() { return ''; }
|
||||
function isStep5Ready() { return step5Ready; }
|
||||
function isStep8Ready() { return false; }
|
||||
function isAddPhonePageReady() { return false; }
|
||||
function isVerificationPageStillVisible() { return false; }
|
||||
function createSignupUserAlreadyExistsError() {
|
||||
return new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。');
|
||||
}
|
||||
function getCurrentAuthRetryPageState(flow) {
|
||||
if (flow === 'signup' && retryVisible) {
|
||||
return {
|
||||
retryEnabled: true,
|
||||
userAlreadyExistsBlocked: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function recoverCurrentAuthRetryPage() {
|
||||
recoverCalls += 1;
|
||||
retryVisible = false;
|
||||
step5Ready = true;
|
||||
}
|
||||
async function sleep() {}
|
||||
|
||||
${extractFunction('waitForVerificationSubmitOutcome')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return waitForVerificationSubmitOutcome(4, 1000);
|
||||
},
|
||||
snapshot() {
|
||||
return { recoverCalls };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
const result = await api.run();
|
||||
|
||||
assert.deepStrictEqual(result, { success: true });
|
||||
assert.equal(api.snapshot().recoverCalls, 1);
|
||||
});
|
||||
|
||||
test('waitForVerificationSubmitOutcome does not assume success after repeated signup retry pages', async () => {
|
||||
const api = new Function(`
|
||||
let recoverCalls = 0;
|
||||
const location = { href: 'https://auth.openai.com/email-verification' };
|
||||
|
||||
function throwIfStopped() {}
|
||||
function log() {}
|
||||
function getVerificationErrorText() { return ''; }
|
||||
function isStep5Ready() { return false; }
|
||||
function isStep8Ready() { return false; }
|
||||
function isAddPhonePageReady() { return false; }
|
||||
function isVerificationPageStillVisible() { return false; }
|
||||
function createSignupUserAlreadyExistsError() {
|
||||
return new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。');
|
||||
}
|
||||
function getCurrentAuthRetryPageState(flow) {
|
||||
if (flow === 'signup') {
|
||||
return {
|
||||
retryEnabled: true,
|
||||
userAlreadyExistsBlocked: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async function recoverCurrentAuthRetryPage() {
|
||||
recoverCalls += 1;
|
||||
}
|
||||
async function sleep() {}
|
||||
|
||||
${extractFunction('waitForVerificationSubmitOutcome')}
|
||||
|
||||
return {
|
||||
run() {
|
||||
return waitForVerificationSubmitOutcome(4, 1000);
|
||||
},
|
||||
snapshot() {
|
||||
return { recoverCalls };
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
await assert.rejects(
|
||||
api.run(),
|
||||
/连续进入认证重试页 2 次,页面仍未恢复/
|
||||
);
|
||||
assert.equal(api.snapshot().recoverCalls, 2);
|
||||
});
|
||||
Reference in New Issue
Block a user