diff --git a/background.js b/background.js index 3b301de..a16dbb6 100644 --- a/background.js +++ b/background.js @@ -10768,7 +10768,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe }, isSignupProfilePageUrl: (rawUrl) => { const parsed = parseUrlSafely(rawUrl); - return Boolean(parsed && isSignupPageHost(parsed.hostname) && /\/create-account\/profile(?:[/?#]|$)/i.test(parsed.pathname || '')); + return Boolean(parsed && isSignupPageHost(parsed.hostname) && /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(parsed.pathname || '')); }, isRetryableContentScriptTransportError, isHotmailProvider, @@ -12081,6 +12081,29 @@ function throwIfStep8SettledOrStopped(isSettled = false) { } } +function isStep9AuthCallbackWaitPageUrl(rawUrl) { + if (!rawUrl) return false; + try { + const parsed = new URL(rawUrl); + const hostname = String(parsed.hostname || '').toLowerCase(); + if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(hostname)) { + return false; + } + const pathname = String(parsed.pathname || ''); + return /\/api\/oauth\/oauth2\/auth(?:[/?#]|$)/i.test(pathname) + || /\/oauth\/oauth2\/auth(?:[/?#]|$)/i.test(pathname); + } catch { + return false; + } +} + +async function shouldDeferStep9CallbackTimeout(details = {}) { + const tabId = details?.tabId; + if (!Number.isInteger(tabId)) return false; + const tab = await chrome.tabs.get(tabId).catch(() => null); + return isStep9AuthCallbackWaitPageUrl(tab?.url || ''); +} + async function ensureStep8SignupPageReady(tabId, options = {}) { const visibleStep = Math.floor(Number(options.visibleStep || options.logStep || options.step) || 0); await ensureContentScriptReadyOnTab('signup-page', tabId, { @@ -12535,6 +12558,7 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({ setStep8TabUpdatedListener, setWebNavCommittedListener, setWebNavListener, + shouldDeferStep9CallbackTimeout, sleepWithStop, STEP8_CLICK_RETRY_DELAY_MS, STEP8_MAX_ROUNDS, diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index b85736e..e6dbbc8 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -85,7 +85,7 @@ function fallbackSignupProfilePageUrl(rawUrl) { const parsed = parseUrlSafely(rawUrl); if (!parsed) return false; - return /\/create-account\/profile(?:[/?#]|$)/i.test(parsed.pathname || ''); + return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(parsed.pathname || ''); } function resolveSignupPostIdentityState(rawUrl) { diff --git a/background/steps/confirm-oauth.js b/background/steps/confirm-oauth.js index a2cff0c..52fe3ce 100644 --- a/background/steps/confirm-oauth.js +++ b/background/steps/confirm-oauth.js @@ -33,6 +33,7 @@ setWebNavCommittedListener, setStep8PendingReject, setStep8TabUpdatedListener, + shouldDeferStep9CallbackTimeout, } = deps; const LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS = 240000; @@ -96,6 +97,7 @@ let signupTabId = null; const callbackWaitStartedAt = Date.now(); let timeoutCheckTimer = null; + let timeoutDeferredLogged = false; const cleanupListener = () => { if (timeoutCheckTimer) { @@ -128,11 +130,46 @@ }); }; + const isCallbackTimeoutDeferred = async (elapsedMs) => { + if (typeof shouldDeferStep9CallbackTimeout !== 'function') { + return false; + } + try { + const deferred = await shouldDeferStep9CallbackTimeout({ + tabId: signupTabId, + visibleStep, + elapsedMs, + oauthUrl: activeState?.oauthUrl || '', + }); + if (deferred && !timeoutDeferredLogged) { + timeoutDeferredLogged = true; + await addStepLog( + visibleStep, + '检测到认证页仍在安全验证/授权跳转中,暂停本地回调超时判定,继续等待 localhost 回调...', + 'info' + ); + } + return Boolean(deferred); + } catch (error) { + await addStepLog( + visibleStep, + `复核认证页跳转状态失败(${error?.message || error}),继续按原超时规则等待回调。`, + 'warn' + ); + return false; + } + }; + const checkCallbackTimeout = async () => { if (resolved) { return; } const elapsedMs = Date.now() - callbackWaitStartedAt; + if (await isCallbackTimeoutDeferred(elapsedMs)) { + timeoutCheckTimer = setTimeout(checkCallbackTimeout, CALLBACK_TIMEOUT_CHECK_INTERVAL_MS); + return; + } + if (elapsedMs >= LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS) { rejectStep9(new Error(`${Math.round(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`)); return; diff --git a/background/verification-flow.js b/background/verification-flow.js index d3fd5e5..9a236e6 100644 --- a/background/verification-flow.js +++ b/background/verification-flow.js @@ -135,7 +135,7 @@ if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) { return false; } - return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || '')); + return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(String(parsed.pathname || '')); } catch { return false; } @@ -225,11 +225,21 @@ const authState = String(result?.state || '').trim(); const authUrl = String(result?.url || '').trim(); + const verificationErrorText = String(result?.verificationErrorText || '').trim(); lastSnapshot = { state: authState || 'unknown', url: authUrl, }; + if (authState === 'verification_page' && verificationErrorText) { + return { + success: false, + reason: 'invalid_code', + invalidCode: true, + errorText: verificationErrorText, + url: authUrl, + }; + } if (authState === 'oauth_consent_page') { return { success: true, @@ -1068,7 +1078,8 @@ }, }; let result; - if (typeof sendToContentScriptResilient === 'function') { + const shouldAvoidReplaySubmit = step === 8; + if (typeof sendToContentScriptResilient === 'function' && !shouldAvoidReplaySubmit) { try { result = await sendToContentScriptResilient('signup-page', message, { timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000), @@ -1131,6 +1142,56 @@ } throw err; } + } else if (shouldAvoidReplaySubmit) { + try { + result = await sendToContentScript('signup-page', message, { + responseTimeoutMs: baseResponseTimeoutMs, + }); + } catch (err) { + if (isRetryableVerificationTransportError(err)) { + await addLog('认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...', 'warn', { + step: completionStep, + stepKey: 'fetch-login-code', + }); + const fallback = await detectStep8PostSubmitFallback({ + step, + timeoutMs: 9000, + pollIntervalMs: 300, + }); + if (fallback.invalidCode) { + return { + invalidCode: true, + errorText: fallback.errorText || '验证码被拒绝。', + url: fallback.url || '', + }; + } + if (fallback.success) { + if (fallback.addPhonePage) { + await addLog('验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn', { + step: completionStep, + stepKey: 'fetch-login-code', + }); + } else { + await addLog('验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn', { + step: completionStep, + stepKey: 'fetch-login-code', + }); + } + return { + success: true, + assumed: true, + transportRecovered: true, + addPhonePage: Boolean(fallback.addPhonePage), + url: fallback.url || '', + }; + } + if (fallback.restartStep7) { + const urlPart = fallback.url ? ` URL: ${fallback.url}` : ''; + throw new Error(`STEP8_RESTART_STEP7::步骤 ${completionStep}:验证码提交后认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim()); + } + } + throw err; + } } else { result = await sendToContentScript('signup-page', message, { responseTimeoutMs: baseResponseTimeoutMs, @@ -1275,20 +1336,8 @@ continue; } - const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0 - ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt)) - : 0; - if (remainingBeforeResendMs > 0) { - await addLog( - `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`, - 'warn' - ); - await sleepWithStop(Math.min(remainingBeforeResendMs, 2000)); - continue; - } - if (remainingAutomaticResendCount <= 0) { - await addLog(`步骤 ${step}:已达到自动重新发送验证码次数上限,将直接使用当前时间窗口继续重试。`, 'warn'); + await addLog(`步骤 ${step}:已达到自动重新发送验证码次数上限,将排除已拒绝验证码并继续轮询新邮件。`, 'warn'); continue; } diff --git a/content/signup-page.js b/content/signup-page.js index 03bf259..625955c 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -2627,7 +2627,7 @@ function isSignupProfilePageUrl(rawUrl = location.href) { if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) { return false; } - return /\/create-account\/profile(?:[/?#]|$)/i.test(String(parsed.pathname || '')); + return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile)(?:[/?#]|$)/i.test(String(parsed.pathname || '')); } catch { return false; } @@ -3991,6 +3991,7 @@ function serializeLoginAuthState(snapshot) { url: snapshot?.url || location.href, path: snapshot?.path || location.pathname || '', displayedEmail: snapshot?.displayedEmail || '', + verificationErrorText: getVerificationErrorText(), retryEnabled: Boolean(snapshot?.retryEnabled), titleMatched: Boolean(snapshot?.titleMatched), detailMatched: Boolean(snapshot?.detailMatched), @@ -6028,14 +6029,23 @@ function getSerializableRect(el) { // Step 5: Fill Name & Birthday / Age // ============================================================ -function getStep5DirectCompletionPayload({ isAgeMode = false } = {}) { +function getStep5DirectCompletionPayload({ isAgeMode = false, navigationStarted = false, outcome = null } = {}) { const payload = { - skippedPostSubmitCheck: true, - directProceedToStep6: true, + profileSubmitted: true, + postSubmitChecked: true, }; if (isAgeMode) { payload.ageMode = true; } + if (navigationStarted) { + payload.navigationStarted = true; + } + if (outcome?.state) { + payload.outcome = outcome.state; + } + if (outcome?.url) { + payload.url = outcome.url; + } return payload; } @@ -6084,6 +6094,252 @@ async function waitForCombinedSignupVerificationProfilePage(timeout = 2500) { return isCombinedSignupVerificationProfilePage(); } +function getStep5ProfilePathPatterns() { + return [ + /\/create-account\/profile(?:[/?#]|$)/i, + /\/u\/signup\/profile(?:[/?#]|$)/i, + /\/signup\/profile(?:[/?#]|$)/i, + ]; +} + +function getStep5AuthRetryPathPatterns() { + const signupPatterns = typeof getSignupAuthRetryPathPatterns === 'function' + ? getSignupAuthRetryPathPatterns() + : []; + return [ + ...signupPatterns, + ...getStep5ProfilePathPatterns(), + ]; +} + +function isStep5ProfilePageUrl(rawUrl = location.href) { + return isSignupProfilePageUrl(rawUrl); +} + +function getStep5AuthRetryPageState() { + if (typeof getAuthTimeoutErrorPageState === 'function') { + return getAuthTimeoutErrorPageState({ + pathPatterns: getStep5AuthRetryPathPatterns(), + }); + } + + if (typeof getCurrentAuthRetryPageState === 'function') { + return getCurrentAuthRetryPageState('signup'); + } + + return null; +} + +function getStep5SubmitButton() { + const direct = document.querySelector('button[type="submit"], input[type="submit"]'); + if (direct && isVisibleElement(direct)) { + return direct; + } + + const candidates = document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]'); + return Array.from(candidates).find((el) => { + if (!isVisibleElement(el)) return false; + const text = typeof getActionText === 'function' + ? getActionText(el) + : [ + el?.textContent, + el?.value, + el?.getAttribute?.('aria-label'), + el?.getAttribute?.('title'), + ] + .filter(Boolean) + .join(' ') + .replace(/\s+/g, ' ') + .trim(); + return /完成|创建|create|continue|finish|done|agree/i.test(text); + }) || null; +} + +async function waitForStep5SubmitButton(timeout = 5000) { + const start = Date.now(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + const button = getStep5SubmitButton(); + if (button) { + return button; + } + await sleep(150); + } + + return null; +} + +function isStep5SubmitButtonClickable(button) { + return Boolean(button) + && isVisibleElement(button) + && !button.disabled + && button.getAttribute?.('aria-disabled') !== 'true'; +} + +function isStep5ProfileStillVisible() { + if (isStep5ProfilePageUrl()) { + return true; + } + + return typeof isStep5Ready === 'function' ? isStep5Ready() : false; +} + +function getStep5PostSubmitSuccessState() { + if (getStep5AuthRetryPageState()) { + return null; + } + + if (isLikelyLoggedInChatgptHomeUrl()) { + return { + state: 'logged_in_home', + url: location.href, + }; + } + + if (typeof isOAuthConsentPage === 'function' && isOAuthConsentPage()) { + return { + state: 'oauth_consent', + url: location.href, + }; + } + + if (typeof isAddPhonePageReady === 'function' && isAddPhonePageReady()) { + return { + state: 'add_phone', + url: location.href, + }; + } + + if (!isStep5ProfileStillVisible()) { + return { + state: 'left_profile', + url: location.href, + }; + } + + return null; +} + +function installStep5NavigationCompletionReporter(completeOnce) { + if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') { + return () => {}; + } + + const onNavigationStarted = () => { + completeOnce({ + navigationStarted: true, + outcome: { + state: 'navigation_started', + url: location.href, + }, + }); + }; + + window.addEventListener('pagehide', onNavigationStarted, { once: true }); + window.addEventListener('beforeunload', onNavigationStarted, { once: true }); + + return () => { + window.removeEventListener('pagehide', onNavigationStarted); + window.removeEventListener('beforeunload', onNavigationStarted); + }; +} + +async function waitForStep5SubmitOutcome(options = {}) { + const { + timeoutMs = 45000, + maxAuthRetryRecoveries = 2, + maxSubmitClicks = 3, + retryClickIntervalMs = 3500, + } = options; + const start = Date.now(); + let authRetryRecoveryCount = 0; + let submitClickCount = 1; + let lastSubmitClickAt = Date.now(); + let lastStep5Error = ''; + + while (Date.now() - start < timeoutMs) { + throwIfStopped(); + + const retryState = getStep5AuthRetryPageState(); + if (retryState?.userAlreadyExistsBlocked) { + throw createSignupUserAlreadyExistsError(); + } + if (retryState?.maxCheckAttemptsBlocked) { + throw createAuthMaxCheckAttemptsError(); + } + if (retryState) { + if (authRetryRecoveryCount >= maxAuthRetryRecoveries) { + throw new Error(`步骤 5:资料提交后连续进入认证重试页 ${maxAuthRetryRecoveries} 次,页面仍未恢复。URL: ${location.href}`); + } + authRetryRecoveryCount += 1; + log(`步骤 5:资料提交后进入认证重试页,正在自动恢复(${authRetryRecoveryCount}/${maxAuthRetryRecoveries})...`, 'warn'); + await recoverCurrentAuthRetryPage({ + flow: 'signup', + logLabel: '步骤 5:资料提交后检测到认证重试页,正在点击“重试”恢复', + maxClickAttempts: 2, + pathPatterns: getStep5AuthRetryPathPatterns(), + step: 5, + timeoutMs: 12000, + }); + lastSubmitClickAt = Date.now(); + continue; + } + + const successState = getStep5PostSubmitSuccessState(); + if (successState) { + return successState; + } + + const step5Error = typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : ''; + if (step5Error) { + lastStep5Error = step5Error; + } + + if ( + isStep5ProfileStillVisible() + && submitClickCount < maxSubmitClicks + && Date.now() - lastSubmitClickAt >= retryClickIntervalMs + ) { + const submitButton = getStep5SubmitButton(); + if (isStep5SubmitButtonClickable(submitButton)) { + submitClickCount += 1; + log(`步骤 5:资料提交后仍停留在资料页,正在重新点击“完成帐户创建”(第 ${submitClickCount}/${maxSubmitClicks} 次)...`, 'warn'); + await humanPause(350, 900); + simulateClick(submitButton); + lastSubmitClickAt = Date.now(); + await sleep(1000); + continue; + } + } + + await sleep(250); + } + + const finalRetryState = getStep5AuthRetryPageState(); + if (finalRetryState?.userAlreadyExistsBlocked) { + throw createSignupUserAlreadyExistsError(); + } + if (finalRetryState?.maxCheckAttemptsBlocked) { + throw createAuthMaxCheckAttemptsError(); + } + if (finalRetryState) { + throw new Error(`步骤 5:资料提交后仍停留在认证重试页,自动恢复未完成。URL: ${location.href}`); + } + + const finalSuccessState = getStep5PostSubmitSuccessState(); + if (finalSuccessState) { + return finalSuccessState; + } + + const finalStep5Error = (typeof getStep5ErrorText === 'function' ? getStep5ErrorText() : '') || lastStep5Error; + if (finalStep5Error) { + throw new Error(`步骤 5:资料提交后页面返回错误:${finalStep5Error}。URL: ${location.href}`); + } + + throw new Error(`步骤 5:资料提交后未检测到页面跳转或恢复成功(已点击提交 ${submitClickCount}/${maxSubmitClicks} 次)。URL: ${location.href}`); +} + async function step5_fillNameBirthday(payload) { const { firstName, lastName, age, year, month, day, prefillOnly = false } = payload; if (!firstName || !lastName) throw new Error('未提供姓名数据。'); @@ -6291,7 +6547,7 @@ async function step5_fillNameBirthday(payload) { // Click "完成帐户创建" button await sleep(500); - const completeBtn = document.querySelector('button[type="submit"]') + const completeBtn = await waitForStep5SubmitButton(5000) || await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null); if (!completeBtn) { throw new Error('未找到“完成帐户创建”按钮。URL: ' + location.href); @@ -6299,20 +6555,40 @@ async function step5_fillNameBirthday(payload) { const isAgeMode = !birthdayMode && Boolean(ageInput); if (isAgeMode) { - log('步骤 5:当前为年龄输入模式,点击“完成帐户创建”后将直接完成当前步骤。', 'warn'); + log('步骤 5:当前为年龄输入模式,点击“完成帐户创建”后将等待页面结果。', 'info'); } - await humanPause(500, 1300); - simulateClick(completeBtn); + let reportedCompletionPayload = null; + function completeStep5Once(extra = {}) { + if (reportedCompletionPayload) { + return reportedCompletionPayload; + } - const completionPayload = getStep5DirectCompletionPayload({ isAgeMode }); - reportComplete(5, completionPayload); - - if (isAgeMode) { - log('步骤 5:年龄模式已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。', 'warn'); + const completionPayload = getStep5DirectCompletionPayload({ + isAgeMode, + navigationStarted: Boolean(extra.navigationStarted), + outcome: extra.outcome || null, + }); + reportedCompletionPayload = completionPayload; + reportComplete(5, completionPayload); return completionPayload; } - log('步骤 5:已点击“完成帐户创建”,当前步骤直接完成,不再等待页面结果。'); - return completionPayload; + const cleanupNavigationReporter = installStep5NavigationCompletionReporter(completeStep5Once); + + await humanPause(500, 1300); + simulateClick(completeBtn); + log('步骤 5:已点击“完成帐户创建”,正在等待页面跳转、重试页或提交结果。'); + + try { + const outcome = await waitForStep5SubmitOutcome(); + cleanupNavigationReporter(); + + const completionPayload = completeStep5Once({ outcome }); + log(`步骤 5:资料提交结果已确认(${outcome.state || 'success'}),准备继续后续步骤。`, 'ok'); + return completionPayload; + } catch (error) { + cleanupNavigationReporter(); + throw error; + } } diff --git a/tests/step5-age-consent.test.js b/tests/step5-age-consent.test.js index 2199a00..2cffdfe 100644 --- a/tests/step5-age-consent.test.js +++ b/tests/step5-age-consent.test.js @@ -54,9 +54,21 @@ function extractFunction(name) { function getStep5Bundle() { return [ extractFunction('getStep5DirectCompletionPayload'), + extractFunction('isSignupProfilePageUrl'), extractFunction('isStep5AllConsentText'), extractFunction('findStep5AllConsentCheckbox'), extractFunction('isStep5CheckboxChecked'), + extractFunction('getStep5ProfilePathPatterns'), + extractFunction('getStep5AuthRetryPathPatterns'), + extractFunction('isStep5ProfilePageUrl'), + extractFunction('getStep5AuthRetryPageState'), + extractFunction('getStep5SubmitButton'), + extractFunction('waitForStep5SubmitButton'), + extractFunction('isStep5SubmitButtonClickable'), + extractFunction('isStep5ProfileStillVisible'), + extractFunction('getStep5PostSubmitSuccessState'), + extractFunction('installStep5NavigationCompletionReporter'), + extractFunction('waitForStep5SubmitOutcome'), extractFunction('step5_fillNameBirthday'), ].join('\n'); } @@ -73,6 +85,9 @@ const completeButton = { tagName: 'BUTTON', textContent: '\\u5b8c\\u6210\\u8d26\\u6237\\u521b\\u5efa', hidden: false, + getAttribute() { + return ''; + }, }; const allConsentLabel = { hidden: false, @@ -110,6 +125,7 @@ const document = { return null; case 'input[name="age"]': return ageInput; + case 'button[type="submit"], input[type="submit"]': case 'button[type="submit"]': return completeButton; default: @@ -136,6 +152,8 @@ function log(message, level = 'info') { logs.push({ message, level }); } +function throwIfStopped() {} + async function waitForElement() { return nameInput; } @@ -155,6 +173,14 @@ function isVisibleElement(el) { return Boolean(el) && !el.hidden; } +function isActionEnabled(el) { + return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true'; +} + +function getActionText(el) { + return el.textContent || ''; +} + async function setReactAriaBirthdaySelect() { throw new Error('setReactAriaBirthdaySelect should not run in age-mode test'); } @@ -168,6 +194,9 @@ function simulateClick(el) { if (el === allConsentLabel || el === allConsentCheckbox) { allConsentCheckbox.checked = true; } + if (el === completeButton) { + location.href = 'https://chatgpt.com/'; + } } function reportComplete(step, payload) { @@ -178,6 +207,17 @@ function normalizeInlineText(text) { return String(text || '').replace(/\\s+/g, ' ').trim(); } +function getSignupAuthRetryPathPatterns() { return []; } +function getAuthTimeoutErrorPageState() { return null; } +async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); } +function createSignupUserAlreadyExistsError() { return new Error('user already exists'); } +function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); } +function getStep5ErrorText() { return ''; } +function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); } +function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); } +function isOAuthConsentPage() { return false; } +function isAddPhonePageReady() { return false; } + ${getStep5Bundle()} return { @@ -205,9 +245,11 @@ return { const snapshot = api.snapshot(); assert.deepStrictEqual(result, { - skippedPostSubmitCheck: true, - directProceedToStep6: true, + profileSubmitted: true, + postSubmitChecked: true, ageMode: true, + outcome: 'logged_in_home', + url: 'https://chatgpt.com/', }); assert.equal(snapshot.nameValue, 'Mia Harris'); assert.equal(snapshot.ageValue, '19'); @@ -220,9 +262,11 @@ return { { step: 5, payload: { - skippedPostSubmitCheck: true, - directProceedToStep6: true, + profileSubmitted: true, + postSubmitChecked: true, ageMode: true, + outcome: 'logged_in_home', + url: 'https://chatgpt.com/', }, }, ]); diff --git a/tests/step5-direct-complete.test.js b/tests/step5-direct-complete.test.js index 1c11b6e..f485b9d 100644 --- a/tests/step5-direct-complete.test.js +++ b/tests/step5-direct-complete.test.js @@ -51,21 +51,43 @@ function extractFunction(name) { return source.slice(start, end); } +function getStep5OutcomeBundle() { + return [ + extractFunction('getStep5ProfilePathPatterns'), + extractFunction('getStep5AuthRetryPathPatterns'), + extractFunction('isStep5ProfilePageUrl'), + extractFunction('getStep5AuthRetryPageState'), + extractFunction('getStep5SubmitButton'), + extractFunction('waitForStep5SubmitButton'), + extractFunction('isStep5SubmitButtonClickable'), + extractFunction('isStep5ProfileStillVisible'), + extractFunction('getStep5PostSubmitSuccessState'), + extractFunction('installStep5NavigationCompletionReporter'), + extractFunction('waitForStep5SubmitOutcome'), + ].join('\n'); +} + function getStep5Bundle() { return [ extractFunction('getStep5DirectCompletionPayload'), + extractFunction('isSignupProfilePageUrl'), extractFunction('isStep5AllConsentText'), extractFunction('findStep5AllConsentCheckbox'), extractFunction('isStep5CheckboxChecked'), + getStep5OutcomeBundle(), extractFunction('step5_fillNameBirthday'), ].join('\n'); } -test('step 5 clicks submit and completes immediately on birthday page', async () => { +test('step 5 waits for post-submit outcome before completing on birthday page', async () => { const step5Source = extractFunction('step5_fillNameBirthday'); assert.ok( - !step5Source.includes('waitForStep5SubmitOutcome('), - 'Step 5 提交后不应再等待页面结果' + step5Source.includes('waitForStep5SubmitOutcome('), + 'Step 5 提交后必须等待页面结果' + ); + assert.ok( + !step5Source.includes('不再等待页面结果'), + 'Step 5 不应再保留直接完成的旧日志' ); const api = new Function(` @@ -84,6 +106,7 @@ const completeButton = { tagName: 'BUTTON', textContent: '完成帐户创建', hidden: false, + getAttribute() { return ''; }, }; const birthdaySelects = { @@ -92,6 +115,10 @@ const birthdaySelects = { '天': { label: '天', button: { hidden: false }, nativeSelect: {} }, }; +const location = { + href: 'https://auth.openai.com/u/signup/profile', +}; + const document = { querySelector(selector) { switch (selector) { @@ -102,6 +129,7 @@ const document = { return null; case 'input[name="birthday"]': return hiddenBirthday; + case 'button[type="submit"], input[type="submit"]': case 'button[type="submit"]': return completeButton; default: @@ -112,15 +140,14 @@ const document = { if (selector === 'input[name="allCheckboxes"][type="checkbox"]') { return []; } + if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') { + return [completeButton]; + } return []; }, execCommand() {}, }; -const location = { - href: 'https://auth.openai.com/u/signup/profile', -}; - function Event(type, init = {}) { this.type = type; this.bubbles = Boolean(init.bubbles); @@ -130,10 +157,8 @@ function log(message, level = 'info') { logs.push({ message, level }); } -async function waitForElement() { - return nameInput; -} - +function throwIfStopped() {} +async function waitForElement() { return nameInput; } async function humanPause() {} async function sleep() {} @@ -149,6 +174,14 @@ function isVisibleElement(el) { return Boolean(el) && !el.hidden; } +function isActionEnabled(el) { + return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true'; +} + +function getActionText(el) { + return el.textContent || ''; +} + async function setReactAriaBirthdaySelect(select, value) { selectedBirthday[select.label] = String(value).padStart(select.label === '年' ? 4 : 2, '0'); if (selectedBirthday['年'] && selectedBirthday['月'] && selectedBirthday['天']) { @@ -162,17 +195,30 @@ async function waitForElementByText() { function simulateClick(el) { clicks.push(el.textContent || el.tagName || 'element'); + if (el === completeButton) { + location.href = 'https://chatgpt.com/'; + } } function reportComplete(step, payload) { completions.push({ step, payload }); } - function normalizeInlineText(text) { - return String(text || '').replace(/\\s+/g, ' ').trim(); - } +function normalizeInlineText(text) { + return String(text || '').replace(/\\s+/g, ' ').trim(); +} +function getSignupAuthRetryPathPatterns() { return []; } +function getAuthTimeoutErrorPageState() { return null; } +async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); } +function createSignupUserAlreadyExistsError() { return new Error('user already exists'); } +function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); } +function getStep5ErrorText() { return ''; } +function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); } +function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); } +function isOAuthConsentPage() { return false; } +function isAddPhonePageReady() { return false; } - ${getStep5Bundle()} +${getStep5Bundle()} return { async run(payload) { @@ -199,20 +245,20 @@ return { }); const snapshot = api.snapshot(); - assert.deepStrictEqual( - result, - { - skippedPostSubmitCheck: true, - directProceedToStep6: true, - }, - '生日模式点击提交后应直接返回完成载荷' - ); + assert.deepStrictEqual(result, { + profileSubmitted: true, + postSubmitChecked: true, + outcome: 'logged_in_home', + url: 'https://chatgpt.com/', + }); assert.deepStrictEqual(snapshot.completions, [ { step: 5, payload: { - skippedPostSubmitCheck: true, - directProceedToStep6: true, + profileSubmitted: true, + postSubmitChecked: true, + outcome: 'logged_in_home', + url: 'https://chatgpt.com/', }, }, ]); @@ -220,7 +266,185 @@ return { assert.equal(snapshot.nameValue, 'Test User'); assert.equal(snapshot.birthdayValue, '2003-06-19'); assert.ok( - snapshot.logs.some(({ message }) => /不再等待页面结果/.test(message)), - '日志应明确说明 Step 5 已直接完成' + snapshot.logs.some(({ message }) => /资料提交结果已确认/.test(message)), + '日志应明确说明 Step 5 已完成提交后检测' ); }); + +test('step 5 retries submit while profile page remains visible', async () => { + const api = new Function(` +const realDateNow = Date.now; +let now = 0; +const logs = []; +const completions = []; +const clicks = []; +const nameInput = { value: '', hidden: false }; +const ageInput = { value: '', hidden: false }; +const completeButton = { + tagName: 'BUTTON', + textContent: '完成帐户创建', + hidden: false, + getAttribute() { return ''; }, +}; +const location = { + href: 'https://auth.openai.com/u/signup/profile', +}; +const document = { + querySelector(selector) { + switch (selector) { + case '[role="spinbutton"][data-type="year"]': + case '[role="spinbutton"][data-type="month"]': + case '[role="spinbutton"][data-type="day"]': + case 'input[name="birthday"]': + return null; + case 'input[name="age"]': + return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href) ? ageInput : null; + case 'button[type="submit"], input[type="submit"]': + case 'button[type="submit"]': + return completeButton; + default: + return null; + } + }, + querySelectorAll(selector) { + if (selector === 'input[name="allCheckboxes"][type="checkbox"]') return []; + if (selector === 'input[type="checkbox"]') return []; + if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [completeButton]; + return []; + }, + execCommand() {}, +}; + +Date.now = () => now; +function log(message, level = 'info') { logs.push({ message, level }); } +function throwIfStopped() {} +async function waitForElement() { return nameInput; } +async function humanPause() {} +async function sleep(ms = 0) { now += ms || 250; } +function fillInput(input, value) { input.value = value; } +function findBirthdayReactAriaSelect() { return null; } +function isVisibleElement(el) { return Boolean(el) && !el.hidden; } +function isActionEnabled(el) { return Boolean(el) && !el.disabled && el.getAttribute?.('aria-disabled') !== 'true'; } +function getActionText(el) { return el.textContent || ''; } +async function setReactAriaBirthdaySelect() { throw new Error('setReactAriaBirthdaySelect should not run'); } +async function waitForElementByText() { return completeButton; } +function simulateClick(el) { + clicks.push(el.textContent || el.tagName || 'element'); + if (el === completeButton && clicks.filter((text) => text === '完成帐户创建').length >= 3) { + location.href = 'https://chatgpt.com/'; + } +} +function reportComplete(step, payload) { completions.push({ step, payload }); } +function normalizeInlineText(text) { return String(text || '').replace(/\\s+/g, ' ').trim(); } +function getSignupAuthRetryPathPatterns() { return []; } +function getAuthTimeoutErrorPageState() { return null; } +async function recoverCurrentAuthRetryPage() { throw new Error('should not recover retry page'); } +function createSignupUserAlreadyExistsError() { return new Error('user already exists'); } +function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); } +function getStep5ErrorText() { return ''; } +function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); } +function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); } +function isOAuthConsentPage() { return false; } +function isAddPhonePageReady() { return false; } + +${getStep5Bundle()} + +return { + run() { + return step5_fillNameBirthday({ + firstName: 'Mia', + lastName: 'Harris', + age: 19, + }); + }, + snapshot() { + return { logs, completions, clicks, nameValue: nameInput.value, ageValue: ageInput.value }; + }, + restore() { + Date.now = realDateNow; + }, +}; +`)(); + + let result; + try { + result = await api.run(); + } finally { + api.restore(); + } + const snapshot = api.snapshot(); + + assert.deepStrictEqual(result, { + profileSubmitted: true, + postSubmitChecked: true, + ageMode: true, + outcome: 'logged_in_home', + url: 'https://chatgpt.com/', + }); + assert.equal(snapshot.nameValue, 'Mia Harris'); + assert.equal(snapshot.ageValue, '19'); + assert.equal(snapshot.clicks.filter((text) => text === '完成帐户创建').length, 3); + assert.equal(snapshot.logs.some(({ message }) => /仍停留在资料页,正在重新点击/.test(message)), true); +}); + +test('step 5 recovers auth retry page after profile submit', async () => { + const api = new Function(` +let retryVisible = true; +let recoverCalls = 0; +const location = { href: 'https://auth.openai.com/create-account/profile' }; +const document = { + querySelector() { return null; }, + querySelectorAll() { return []; }, +}; + +function throwIfStopped() {} +function log() {} +async function sleep() {} +async function humanPause() {} +function simulateClick() {} +function isVisibleElement() { return true; } +function isActionEnabled() { return true; } +function getActionText(el) { return el?.textContent || ''; } +function getSignupAuthRetryPathPatterns() { return []; } +function getAuthTimeoutErrorPageState() { + if (!retryVisible) return null; + return { + retryEnabled: true, + userAlreadyExistsBlocked: false, + maxCheckAttemptsBlocked: false, + }; +} +async function recoverCurrentAuthRetryPage() { + recoverCalls += 1; + retryVisible = false; + location.href = 'https://chatgpt.com/'; +} +function createSignupUserAlreadyExistsError() { return new Error('user already exists'); } +function createAuthMaxCheckAttemptsError() { return new Error('max_check_attempts'); } +function getStep5ErrorText() { return ''; } +function isStep5Ready() { return /^https:\\/\\/auth\\.openai\\.com\\//.test(location.href); } +function isLikelyLoggedInChatgptHomeUrl() { return /^https:\\/\\/chatgpt\\.com\\//.test(location.href); } +function isOAuthConsentPage() { return false; } +function isAddPhonePageReady() { return false; } + +${extractFunction('isSignupProfilePageUrl')} +${getStep5OutcomeBundle()} + +return { + run() { + return waitForStep5SubmitOutcome({ timeoutMs: 1000 }); + }, + snapshot() { + return { recoverCalls }; + }, +}; +`)(); + + const result = await api.run(); + + assert.deepStrictEqual(result, { + state: 'logged_in_home', + url: 'https://chatgpt.com/', + }); + assert.equal(api.snapshot().recoverCalls, 1); +}); diff --git a/tests/step9-oauth-timeout-toggle.test.js b/tests/step9-oauth-timeout-toggle.test.js index 00803a1..62a4a70 100644 --- a/tests/step9-oauth-timeout-toggle.test.js +++ b/tests/step9-oauth-timeout-toggle.test.js @@ -182,3 +182,175 @@ return { }, }); }); + +test('step9 defers callback timeout while auth security verification is still redirecting', async () => { + const api = new Function('step9ModuleSource', ` +const self = {}; +let webNavListener = null; +let webNavCommittedListener = null; +let step8TabUpdatedListener = null; +let cleanupCalls = 0; +let deferCalls = 0; +let completePayload = null; +const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz'; + +const chrome = { + webNavigation: { + onBeforeNavigate: { + addListener(listener) { + webNavListener = listener; + setTimeout(() => { + if (typeof webNavListener === 'function') { + webNavListener({ tabId: 123, url: callbackUrl }); + } + }, 25); + }, + removeListener() {}, + }, + onCommitted: { + addListener(listener) { + webNavCommittedListener = listener; + }, + removeListener() {}, + }, + }, + tabs: { + onUpdated: { + addListener(listener) { + step8TabUpdatedListener = listener; + }, + removeListener() {}, + }, + async update() {}, + }, +}; + +async function addLog() {} +function cleanupStep8NavigationListeners() { + cleanupCalls += 1; + webNavListener = null; + webNavCommittedListener = null; + step8TabUpdatedListener = null; +} +function throwIfStep8SettledOrStopped(resolved) { + if (resolved) throw new Error('already resolved'); +} +async function getTabId() { return 123; } +async function isTabAlive() { return true; } +async function ensureStep8SignupPageReady() {} +async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) { + if (options.actionLabel === 'OAuth localhost 回调') { + return 1; + } + return defaultTimeoutMs; +} +async function shouldDeferStep9CallbackTimeout(details = {}) { + deferCalls += 1; + return Number(details.tabId) === 123; +} +async function waitForStep8Ready() { + return { + consentReady: true, + url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent', + }; +} +async function triggerStep8ContentStrategy() {} +async function waitForStep8ClickEffect() { + return { + progressed: true, + reason: 'url_changed', + url: 'https://auth.openai.com/api/oauth/oauth2/auth?client_id=app_test', + }; +} +function getStep8CallbackUrlFromNavigation(details, signupTabId) { + return Number(details?.tabId) === Number(signupTabId) ? details.url : ''; +} +function getStep8CallbackUrlFromTabUpdate() { return ''; } +function getStep8EffectLabel() { return 'URL 已变化'; } +async function prepareStep8DebuggerClick() { return { rect: { centerX: 10, centerY: 10 } }; } +async function clickWithDebugger() {} +async function reloadStep8ConsentPage() {} +async function reuseOrCreateTab() { return 123; } +async function sleepWithStop() {} +function setWebNavListener(listener) { webNavListener = listener; } +function getWebNavListener() { return webNavListener; } +function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; } +function getWebNavCommittedListener() { return webNavCommittedListener; } +function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; } +function getStep8TabUpdatedListener() { return step8TabUpdatedListener; } +function setStep8PendingReject() {} +async function completeStepFromBackground(step, payload) { + completePayload = { step, payload }; +} + +const STEP8_CLICK_RETRY_DELAY_MS = 1; +const STEP8_READY_WAIT_TIMEOUT_MS = 5; +const STEP8_MAX_ROUNDS = 1; +const STEP8_STRATEGIES = [ + { mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' }, +]; + +${step9ModuleSource} + +const executor = self.MultiPageBackgroundStep9.createStep9Executor({ + addLog, + chrome, + cleanupStep8NavigationListeners, + clickWithDebugger, + completeStepFromBackground, + ensureStep8SignupPageReady, + getOAuthFlowStepTimeoutMs, + getStep8CallbackUrlFromNavigation, + getStep8CallbackUrlFromTabUpdate, + getStep8EffectLabel, + getTabId, + getWebNavCommittedListener, + getWebNavListener, + getStep8TabUpdatedListener, + isTabAlive, + prepareStep8DebuggerClick, + reloadStep8ConsentPage, + reuseOrCreateTab, + setStep8PendingReject, + setStep8TabUpdatedListener, + setWebNavCommittedListener, + setWebNavListener, + shouldDeferStep9CallbackTimeout, + sleepWithStop, + STEP8_CLICK_RETRY_DELAY_MS, + STEP8_MAX_ROUNDS, + STEP8_READY_WAIT_TIMEOUT_MS, + STEP8_STRATEGIES, + throwIfStep8SettledOrStopped, + triggerStep8ContentStrategy, + waitForStep8ClickEffect, + waitForStep8Ready, +}); + +return { + executeStep9: executor.executeStep9, + snapshot() { + return { + cleanupCalls, + deferCalls, + completePayload, + }; + }, +}; +`)(step9ModuleSource); + + await api.executeStep9({ + oauthUrl: 'https://auth.openai.com/original-oauth', + visibleStep: 12, + }); + + const snapshot = api.snapshot(); + assert.equal(snapshot.deferCalls >= 1, true); + assert.equal(snapshot.cleanupCalls >= 1, true); + assert.deepEqual(snapshot.completePayload, { + step: 12, + payload: { + localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz', + }, + }); +}); diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js index 85a5ea8..3b024f8 100644 --- a/tests/verification-flow-polling.test.js +++ b/tests/verification-flow-polling.test.js @@ -448,7 +448,12 @@ test('verification flow keeps step 8 successful when code submit transport fails pollCloudflareTempEmailVerificationCode: async () => ({}), pollHotmailVerificationCode: async () => ({}), pollLuckmailVerificationCode: async () => ({}), - sendToContentScript: async () => ({}), + sendToContentScript: async (_source, message) => { + if (message.type === 'FILL_CODE') { + throw new Error('message channel is closed before a response was received'); + } + return {}; + }, sendToContentScriptResilient: async (_source, message) => { if (message.type === 'FILL_CODE') { throw new Error('message channel is closed before a response was received'); @@ -1282,6 +1287,151 @@ test('verification flow uses resilient signup-page transport when submitting ver assert.ok(resilientCalls[0].options.timeoutMs >= 30000); }); +test('verification flow does not replay step 8 code submit after transient auth-page transport failure', async () => { + const directMessages = []; + const resilientMessages = []; + + const helpers = api.createVerificationFlowHelpers({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeStepFromBackground: async () => {}, + confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }), + getHotmailVerificationPollConfig: () => ({}), + getHotmailVerificationRequestTimestamp: () => 0, + getState: async () => ({}), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isRetryableContentScriptTransportError: (error) => /did not respond/i.test(String(error?.message || error || '')), + isStopError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + MAIL_2925_VERIFICATION_INTERVAL_MS: 15000, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15, + pollCloudflareTempEmailVerificationCode: async () => ({}), + pollHotmailVerificationCode: async () => ({}), + pollLuckmailVerificationCode: async () => ({}), + sendToContentScript: async (_source, message) => { + directMessages.push(message.type); + if (message.type === 'FILL_CODE') { + throw new Error('步骤 8:页面通信异常 did not respond in 30s'); + } + return {}; + }, + sendToContentScriptResilient: async (_source, message) => { + resilientMessages.push(message.type); + if (message.type === 'GET_LOGIN_AUTH_STATE') { + return { + state: 'verification_page', + verificationErrorText: '代码不正确', + url: 'https://auth.openai.com/email-verification', + }; + } + throw new Error('FILL_CODE should not be replayed through resilient transport'); + }, + sendToMailContentScriptResilient: async () => ({}), + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + const result = await helpers.submitVerificationCode(8, '510725', { completionStep: 11 }); + + assert.deepStrictEqual(result, { + invalidCode: true, + errorText: '代码不正确', + url: 'https://auth.openai.com/email-verification', + }); + assert.deepStrictEqual(directMessages, ['FILL_CODE']); + assert.deepStrictEqual(resilientMessages, ['GET_LOGIN_AUTH_STATE']); +}); + +test('verification flow requests a new code immediately after Cloudflare Temp Email code is rejected', async () => { + const events = []; + const pollPayloads = []; + const completed = []; + + const helpers = api.createVerificationFlowHelpers({ + addLog: async () => {}, + chrome: { + tabs: { + update: async () => {}, + }, + }, + CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email', + completeStepFromBackground: async (_step, payload) => { + completed.push(payload); + }, + confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }), + getHotmailVerificationPollConfig: () => ({}), + getHotmailVerificationRequestTimestamp: () => 0, + getState: async () => ({}), + getTabId: async () => 1, + HOTMAIL_PROVIDER: 'hotmail-api', + isStopError: () => false, + LUCKMAIL_PROVIDER: 'luckmail-api', + MAIL_2925_VERIFICATION_INTERVAL_MS: 15000, + MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15, + pollCloudflareTempEmailVerificationCode: async (_step, _state, payload) => { + pollPayloads.push(payload); + const code = pollPayloads.length === 1 ? '111111' : '222222'; + events.push(['poll', code]); + return { code, emailTimestamp: pollPayloads.length }; + }, + pollHotmailVerificationCode: async () => ({}), + pollLuckmailVerificationCode: async () => ({}), + sendToContentScript: async (_source, message) => { + if (message.type === 'RESEND_VERIFICATION_CODE') { + events.push(['resend', message.step]); + return {}; + } + if (message.type === 'FILL_CODE') { + events.push(['submit', message.payload.code]); + return message.payload.code === '111111' + ? { invalidCode: true, errorText: '代码不正确' } + : { success: true }; + } + return {}; + }, + sendToMailContentScriptResilient: async () => ({}), + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, + }); + + await helpers.resolveVerificationStep( + 8, + { + email: 'user@example.com', + loginVerificationRequestedAt: Date.now(), + verificationResendCount: 1, + }, + { provider: 'cloudflare-temp-email', label: 'Cloudflare Temp Email' }, + { + completionStep: 11, + lastResendAt: Date.now(), + resendIntervalMs: 25000, + } + ); + + assert.deepStrictEqual(events, [ + ['poll', '111111'], + ['submit', '111111'], + ['resend', 8], + ['poll', '222222'], + ['submit', '222222'], + ]); + assert.deepStrictEqual(pollPayloads[1].excludeCodes, ['111111']); + assert.equal(completed[0].code, '222222'); +}); + test('verification flow forwards optional signup profile payload when submitting signup verification code', async () => { const resilientCalls = [];