diff --git a/flows/openai/background/steps/oauth-login.js b/flows/openai/background/steps/oauth-login.js index b627b9d..88bc6d0 100644 --- a/flows/openai/background/steps/oauth-login.js +++ b/flows/openai/background/steps/oauth-login.js @@ -277,6 +277,7 @@ try { const rawCurrentState = { ...(attempt === 1 ? state : await getState()), + visibleStep: completionStep, ...(resolvedIdentifierType === 'phone' ? { forceLoginIdentifierType: 'phone', forceEmailLogin: false, diff --git a/flows/openai/content/openai-auth.js b/flows/openai/content/openai-auth.js index 409ba62..c1d1ec9 100644 --- a/flows/openai/content/openai-auth.js +++ b/flows/openai/content/openai-auth.js @@ -231,6 +231,17 @@ const CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN = new RegExp([ String.raw`\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8`, ].join('|'), 'i'); const CHOOSE_ACCOUNT_ACTION_SELECTOR = 'button, a, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])'; +const CHOOSE_ACCOUNT_CARD_SELECTOR = [ + '[data-testid*="account" i]', + '[data-test-id*="account" i]', + '[class*="account" i]', + '[class*="user" i]', + '[class*="card" i]', + '[class*="option" i]', + '[class*="select" i]', + '[class*="list" i] > *', + 'li', +].join(', '); const PHONE_RESEND_SERVER_ERROR_PREFIX = 'PHONE_RESEND_SERVER_ERROR::'; const CONTACT_VERIFICATION_SERVER_ERROR_PATTERN = /this\s+page\s+isn['’]?t\s+working|currently\s+unable\s+to\s+handle\s+this\s+request|http\s+error\s+500|500\s+internal\s+server\s+error/i; @@ -3152,6 +3163,47 @@ function resolveChooseAccountClickTarget(element) { return null; } +function resolveChooseAccountCardTarget(element, normalizedEmail = '') { + let current = element; + let bestTarget = null; + + while (current && current !== document.body) { + if (isChooseAccountRemovalAction(current) || !isVisibleElement(current)) { + current = current.parentElement; + continue; + } + + const actionTarget = resolveChooseAccountClickTarget(current); + if (actionTarget) { + return actionTarget; + } + + const text = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(current)); + if (text.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(text)) { + bestTarget = current; + } + + const closestCard = current.closest?.(CHOOSE_ACCOUNT_CARD_SELECTOR) || null; + if ( + closestCard + && closestCard !== current + && closestCard !== document.body + && isVisibleElement(closestCard) + && !isChooseAccountRemovalAction(closestCard) + ) { + const cardText = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(closestCard)); + if (cardText.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(cardText)) { + const cardActionTarget = resolveChooseAccountClickTarget(closestCard); + return cardActionTarget || closestCard; + } + } + + current = current.parentElement; + } + + return bestTarget; +} + function findChooseAccountButtonForEmail(email) { const normalizedEmail = normalizeAuthAccountIdentifier(email); if (!normalizedEmail || !normalizedEmail.includes('@')) { @@ -3180,22 +3232,90 @@ function findChooseAccountButtonForEmail(email) { }); for (const node of emailNodes) { - let current = node; - while (current && current !== document.body) { - const target = resolveChooseAccountClickTarget(current); - if (target) { - const targetText = normalizeAuthAccountIdentifier(getChooseAccountCandidateText(target)); - if (targetText.includes(normalizedEmail) && !CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(targetText)) { - return target; - } - } - current = current.parentElement; + const target = resolveChooseAccountCardTarget(node, normalizedEmail); + if (target) { + return target; } } return null; } +function findChooseAccountOtherAccountButton() { + const candidates = Array.from(document.querySelectorAll(CHOOSE_ACCOUNT_ACTION_SELECTOR)) + .filter((element) => isVisibleElement(element) && isActionEnabled(element)); + + return candidates.find((candidate) => { + if (isChooseAccountRemovalAction(candidate)) { + return false; + } + const text = getChooseAccountCandidateText(candidate); + return CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN.test(text); + }) || null; +} + +function getChooseAccountListedEmails() { + const emailPattern = /[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9-]+(?:\.[a-z0-9-]+)+/ig; + const emails = new Set(); + const nodes = [ + ...Array.from(document.querySelectorAll(CHOOSE_ACCOUNT_ACTION_SELECTOR)), + ...Array.from(document.querySelectorAll('body *')).filter((element) => isVisibleElement(element)), + ]; + + for (const node of nodes) { + if (isChooseAccountRemovalAction(node)) { + continue; + } + const text = getChooseAccountCandidateText(node); + for (const match of text.matchAll(emailPattern)) { + emails.add(normalizeAuthAccountIdentifier(match[0])); + } + } + + return Array.from(emails).filter(Boolean); +} + +async function resolveChooseAccountAction(email, maxRounds = 8) { + let otherAccountButton = null; + let latestSnapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + + for (let round = 0; round < maxRounds; round += 1) { + throwIfStopped(); + latestSnapshot = normalizeStep6Snapshot(inspectLoginAuthState()); + if (latestSnapshot.state !== 'unknown' && latestSnapshot.state !== 'choose_account_page') { + return { + snapshot: latestSnapshot, + }; + } + + const target = findChooseAccountButtonForEmail(email); + if (target) { + return { + target, + snapshot: latestSnapshot, + }; + } + + otherAccountButton = findChooseAccountOtherAccountButton() || otherAccountButton; + const listedEmails = getChooseAccountListedEmails(); + if (otherAccountButton && round >= 2 && (listedEmails.length > 0 || round >= 4)) { + return { + otherAccountButton, + snapshot: latestSnapshot, + }; + } + + if (round < maxRounds - 1) { + await sleep(round === 0 ? 300 : 500); + } + } + + return { + otherAccountButton, + snapshot: latestSnapshot, + }; +} + function getOAuthConsentForm() { return document.querySelector(OAUTH_CONSENT_FORM_SELECTOR); } @@ -5888,8 +6008,65 @@ async function step6ChooseExistingAccount(payload, snapshot) { }); } - const target = findChooseAccountButtonForEmail(email); + const chooseAccountAction = await resolveChooseAccountAction(email); + if ( + chooseAccountAction.snapshot + && chooseAccountAction.snapshot.state !== 'unknown' + && chooseAccountAction.snapshot.state !== 'choose_account_page' + ) { + const resolvedSnapshot = chooseAccountAction.snapshot; + if (resolvedSnapshot.state === 'oauth_consent_page') { + return createStep6OAuthConsentSuccessResult(resolvedSnapshot, { + via: 'choose_account_oauth_consent_page', + }); + } + if (resolvedSnapshot.oauthAuthorizationRoute || isPostChooseAccountOAuthRoute(resolvedSnapshot)) { + return createStep6OAuthConsentSuccessResult(resolvedSnapshot, { + via: 'choose_account_oauth_authorization_route', + }); + } + if (resolvedSnapshot.state === 'email_page') { + return step6LoginFromEmailPage(payload, resolvedSnapshot); + } + if (resolvedSnapshot.state === 'entry_page') { + return step6OpenLoginEntry(payload, resolvedSnapshot); + } + if (resolvedSnapshot.state === 'password_page') { + return step6LoginFromPasswordPage(payload, resolvedSnapshot); + } + if (resolvedSnapshot.state === 'phone_entry_page') { + return step6LoginFromPhonePage(payload, resolvedSnapshot); + } + } + + const target = chooseAccountAction.target; if (!target) { + const otherAccountButton = chooseAccountAction.otherAccountButton; + if (otherAccountButton) { + const listedEmails = getChooseAccountListedEmails(); + const listedLabel = listedEmails.length ? `页面已有账号:${listedEmails.join(', ')}。` : '页面未读取到已有账号邮箱。'; + log(`OpenAI 选择账号页未列出目标邮箱 ${email},${listedLabel} 将点击“登录至另一个帐户”继续目标邮箱登录。`, 'warn', { step: visibleStep, stepKey: 'oauth-login' }); + await humanPause(350, 900); + await performOperationWithDelay({ stepKey: 'oauth-login', kind: 'click', label: 'choose-other-account' }, async () => { + simulateClick(otherAccountButton); + }); + const otherAccountSnapshot = normalizeStep6Snapshot(await waitForChooseAccountTransition(15000)); + if (otherAccountSnapshot.state === 'email_page') { + return step6LoginFromEmailPage(payload, otherAccountSnapshot); + } + if (otherAccountSnapshot.state === 'entry_page') { + return step6OpenLoginEntry(payload, otherAccountSnapshot); + } + if (otherAccountSnapshot.state === 'password_page') { + return step6LoginFromPasswordPage(payload, otherAccountSnapshot); + } + if (otherAccountSnapshot.state === 'phone_entry_page') { + return step6LoginFromPhonePage(payload, otherAccountSnapshot); + } + return createStep6RecoverableResult('choose_account_other_account_transition_stalled', otherAccountSnapshot, { + message: `Clicked another-account login because ${email} was not listed, but the page did not enter a supported login state.`, + }); + } return createStep6RecoverableResult('choose_account_target_not_found', currentSnapshot, { message: `OpenAI choose-account page does not contain target email ${email}.`, }); diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index d3b8032..a073a19 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -137,6 +137,45 @@ test('step 7 retries up to configured limit and then fails', async () => { assert.equal(events.completed, 0); }); +test('step 7 preserves visible step when refreshing OAuth after retry', async () => { + const source = fs.readFileSync('flows/openai/background/steps/oauth-login.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); + + const events = { + refreshSteps: [], + }; + + const executor = api.createStep7Executor({ + addLog: async () => {}, + completeNodeFromBackground: async () => {}, + getErrorMessage: (error) => error?.message || String(error || ''), + getLoginAuthStateLabel: (state) => state || 'unknown', + getState: async () => ({ email: 'user@example.com', password: 'secret' }), + isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable', + isStep6SuccessResult: (result) => result?.step6Outcome === 'success', + refreshOAuthUrlBeforeStep6: async (state) => { + events.refreshSteps.push(state.visibleStep); + return `https://oauth.example/${events.refreshSteps.length}`; + }, + reuseOrCreateTab: async () => {}, + sendToContentScriptResilient: async () => ({ + step6Outcome: 'recoverable', + state: 'email_page', + message: '当前仍停留在邮箱页。', + }), + STEP6_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await assert.rejects( + () => executor.executeStep7({ email: 'user@example.com', password: 'secret', visibleStep: 9 }), + /步骤 9:判断失败后已重试 2 次/ + ); + + assert.deepStrictEqual(events.refreshSteps, [9, 9, 9]); +}); + test('step 7 hands add-phone to the dedicated post-login phone node without internal retry', async () => { const source = fs.readFileSync('flows/openai/background/steps/oauth-login.js', 'utf8'); const globalScope = {}; diff --git a/tests/step6-oauth-consent-skip.test.js b/tests/step6-oauth-consent-skip.test.js index 5f756b3..6a1c790 100644 --- a/tests/step6-oauth-consent-skip.test.js +++ b/tests/step6-oauth-consent-skip.test.js @@ -241,13 +241,18 @@ ${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')} +${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')} ${extractFunction('getPageTextSnapshot')} ${extractFunction('normalizeAuthAccountIdentifier')} ${extractFunction('getChooseAccountCandidateText')} ${extractFunction('isChooseAccountPage')} ${extractFunction('isChooseAccountRemovalAction')} ${extractFunction('resolveChooseAccountClickTarget')} +${extractFunction('resolveChooseAccountCardTarget')} ${extractFunction('findChooseAccountButtonForEmail')} +${extractFunction('findChooseAccountOtherAccountButton')} +${extractFunction('getChooseAccountListedEmails')} +${extractFunction('resolveChooseAccountAction')} ${extractFunction('createStep6SuccessResult')} ${extractFunction('createStep6OAuthConsentSuccessResult')} ${extractFunction('createStep6AddEmailSuccessResult')} @@ -348,13 +353,18 @@ ${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')} +${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')} ${extractFunction('getPageTextSnapshot')} ${extractFunction('normalizeAuthAccountIdentifier')} ${extractFunction('getChooseAccountCandidateText')} ${extractFunction('isChooseAccountPage')} ${extractFunction('isChooseAccountRemovalAction')} ${extractFunction('resolveChooseAccountClickTarget')} +${extractFunction('resolveChooseAccountCardTarget')} ${extractFunction('findChooseAccountButtonForEmail')} +${extractFunction('findChooseAccountOtherAccountButton')} +${extractFunction('getChooseAccountListedEmails')} +${extractFunction('resolveChooseAccountAction')} ${extractFunction('createStep6SuccessResult')} ${extractFunction('createStep6OAuthConsentSuccessResult')} ${extractFunction('createStep6AddEmailSuccessResult')} @@ -386,6 +396,148 @@ return { assert.equal(result.via, 'choose_account_oauth_authorization_route'); }); +test('step 7 clicks matching choose-account card when email is inside a non-action div card', async () => { + const api = new Function(` +let pageState = 'choose_account_page'; +const clicked = []; +const location = { + href: 'https://auth.openai.com/choose-an-account', + pathname: '/choose-an-account', +}; + +function createElement(id, textContent = '', attrs = {}) { + return { + id, + textContent, + value: '', + parentElement: null, + disabled: false, + tagName: attrs.tagName || 'DIV', + tabIndex: attrs.tabIndex ?? -1, + className: attrs.className || '', + getAttribute(name) { + if (name === 'class') return this.className; + if (name === 'aria-disabled') return 'false'; + if (name === 'role') return attrs.role || ''; + return attrs[name] || ''; + }, + closest(selector) { + let current = this; + while (current) { + if ( + String(selector).includes('[class*="account" i]') + && String(current.className || '').toLowerCase().includes('account') + ) { + return current; + } + current = current.parentElement; + } + return null; + }, + }; +} + +const targetCard = createElement('target-card', 'Kenneth Wilson sedate-iodize-lisp@duck.com', { + className: 'account-card rounded-xl', +}); +const targetEmail = createElement('target-email', 'sedate-iodize-lisp@duck.com'); +targetEmail.parentElement = targetCard; +const removeButton = createElement('remove-button', '', { + tagName: 'BUTTON', + 'aria-label': 'Remove sedate-iodize-lisp@duck.com', +}); +removeButton.parentElement = targetCard; +const otherCard = createElement('other-card', 'Other User other@example.com', { + className: 'account-card rounded-xl', +}); + +const document = { + body: { + innerText: '欢迎回来 选择一个帐户 sedate-iodize-lisp@duck.com other@example.com', + textContent: '欢迎回来 选择一个帐户 sedate-iodize-lisp@duck.com other@example.com', + }, + querySelectorAll(selector) { + if (String(selector).includes('body *')) return [removeButton, targetEmail, targetCard, otherCard]; + return [removeButton]; + }, +}; + +function getOperationDelayRunner() { + return async (_metadata, operation) => operation(); +} +function isVisibleElement(element) { + return Boolean(element); +} +function isActionEnabled(element) { + return Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true'; +} +function simulateClick(element) { + clicked.push(element.id); + if (element === targetCard) { + pageState = 'oauth_consent_page'; + location.href = 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent'; + location.pathname = '/sign-in-with-chatgpt/codex/consent'; + } +} +function inspectLoginAuthState() { + return { state: pageState, url: location.href, chooseAccountPage: pageState === 'choose_account_page' }; +} +function throwIfStopped() {} +async function sleep() {} +async function humanPause() {} +function log() {} +async function finalizeStep6VerificationReady() { return { routed: 'verification' }; } +async function step6LoginFromPasswordPage() { return { routed: 'password' }; } +async function step6LoginFromEmailPage() { return { routed: 'email' }; } +async function step6LoginFromPhonePage() { return { routed: 'phone' }; } +async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'timeout' } }; } + +${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')} +${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')} +${extractFunction('getPageTextSnapshot')} +${extractFunction('normalizeAuthAccountIdentifier')} +${extractFunction('getChooseAccountCandidateText')} +${extractFunction('isChooseAccountPage')} +${extractFunction('isChooseAccountRemovalAction')} +${extractFunction('resolveChooseAccountClickTarget')} +${extractFunction('resolveChooseAccountCardTarget')} +${extractFunction('findChooseAccountButtonForEmail')} +${extractFunction('findChooseAccountOtherAccountButton')} +${extractFunction('getChooseAccountListedEmails')} +${extractFunction('resolveChooseAccountAction')} +${extractFunction('createStep6SuccessResult')} +${extractFunction('createStep6OAuthConsentSuccessResult')} +${extractFunction('createStep6AddEmailSuccessResult')} +${extractFunction('createStep6RecoverableResult')} +${extractFunction('normalizeStep6Snapshot')} +${extractFunction('isOpenAiOAuthAuthorizationRoute')} +${extractFunction('isPostChooseAccountOAuthRoute')} +${extractFunction('waitForChooseAccountTransition')} +${extractFunction('step6ChooseExistingAccount')} + +return { + clicked, + run() { + return step6ChooseExistingAccount( + { email: 'sedate-iodize-lisp@duck.com', loginIdentifierType: 'email', visibleStep: 9 }, + { state: 'choose_account_page', url: location.href } + ); + }, +}; +`)(); + + const result = await api.run(); + + assert.deepEqual(api.clicked, ['target-card']); + assert.equal(result.step6Outcome, 'success'); + assert.equal(result.state, 'oauth_consent_page'); + assert.equal(result.skipLoginVerificationStep, true); + assert.equal(result.directOAuthConsentPage, true); +}); + test('step 7 does not click choose-account page when target email is missing', async () => { const api = new Function(` const clicked = []; @@ -447,13 +599,18 @@ ${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')} ${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')} +${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')} ${extractFunction('getPageTextSnapshot')} ${extractFunction('normalizeAuthAccountIdentifier')} ${extractFunction('getChooseAccountCandidateText')} ${extractFunction('isChooseAccountPage')} ${extractFunction('isChooseAccountRemovalAction')} ${extractFunction('resolveChooseAccountClickTarget')} +${extractFunction('resolveChooseAccountCardTarget')} ${extractFunction('findChooseAccountButtonForEmail')} +${extractFunction('findChooseAccountOtherAccountButton')} +${extractFunction('getChooseAccountListedEmails')} +${extractFunction('resolveChooseAccountAction')} ${extractFunction('createStep6SuccessResult')} ${extractFunction('createStep6OAuthConsentSuccessResult')} ${extractFunction('createStep6AddEmailSuccessResult')} @@ -481,3 +638,167 @@ return { assert.equal(result.step6Outcome, 'recoverable'); assert.equal(result.reason, 'choose_account_target_not_found'); }); + +test('step 7 uses another-account login when choose-account lists a different email', async () => { + const api = new Function(` +let pageState = 'choose_account_page'; +const clicked = []; +const location = { + href: 'https://auth.openai.com/choose-an-account', + pathname: '/choose-an-account', +}; +const existingCard = { + id: 'existing-card', + tagName: 'BUTTON', + textContent: 'p 选择帐户 pang-pushing-dealt@duck.com pang-pushing-dealt@duck.com', + value: '', + parentElement: null, + disabled: false, + className: '_root_2sicu_62 _leftAlign_2sicu_99 _outline_2sicu_119', + getAttribute(name) { + if (name === 'aria-disabled') return 'false'; + if (name === 'class') return this.className; + return ''; + }, + closest() { + return null; + }, +}; +const removeButton = { + id: 'remove-button', + tagName: 'BUTTON', + textContent: '移除帐户:pang-pushing-dealt@duck.com', + value: '', + parentElement: null, + disabled: false, + getAttribute(name) { + if (name === 'aria-label') return '移除帐户:pang-pushing-dealt@duck.com'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + closest() { + return null; + }, +}; +const otherAccount = { + id: 'other-account', + tagName: 'A', + textContent: '登录至另一个帐户', + value: '', + parentElement: null, + disabled: false, + getAttribute(name) { + if (name === 'aria-disabled') return 'false'; + return ''; + }, + closest() { + return null; + }, +}; +const createAccount = { + id: 'create-account', + tagName: 'A', + textContent: '创建帐户', + value: '', + parentElement: null, + disabled: false, + getAttribute(name) { + if (name === 'aria-disabled') return 'false'; + return ''; + }, + closest() { + return null; + }, +}; +const document = { + body: { + innerText: '欢迎回来 选择一个帐户 pang-pushing-dealt@duck.com 登录至另一个帐户 创建帐户', + textContent: '欢迎回来 选择一个帐户 pang-pushing-dealt@duck.com 登录至另一个帐户 创建帐户', + }, + querySelectorAll(selector) { + if (String(selector).includes('body *')) return [existingCard, removeButton, otherAccount, createAccount]; + return [existingCard, removeButton, otherAccount, createAccount]; + }, +}; + +function getOperationDelayRunner() { + return async (_metadata, operation) => operation(); +} +function isVisibleElement(element) { + return Boolean(element); +} +function isActionEnabled(element) { + return Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true'; +} +function simulateClick(element) { + clicked.push(element.id); + if (element === otherAccount) { + pageState = 'email_page'; + location.href = 'https://auth.openai.com/log-in'; + location.pathname = '/log-in'; + } +} +function inspectLoginAuthState() { + return { state: pageState, url: location.href, chooseAccountPage: pageState === 'choose_account_page' }; +} +function throwIfStopped() {} +async function sleep() {} +async function humanPause() {} +function log() {} +async function finalizeStep6VerificationReady() { return { routed: 'verification' }; } +async function step6LoginFromPasswordPage() { return { routed: 'password' }; } +async function step6LoginFromEmailPage(payload, snapshot) { + return { + routed: 'email', + email: payload.email, + state: snapshot.state, + }; +} +async function step6LoginFromPhonePage() { return { routed: 'phone' }; } +async function step6OpenLoginEntry() { return { routed: 'entry' }; } +async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'timeout' } }; } + +${extractConst('CHOOSE_ACCOUNT_PAGE_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_REMOVE_ACTION_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_OTHER_ACCOUNT_PATTERN')} +${extractConst('CHOOSE_ACCOUNT_ACTION_SELECTOR')} +${extractConst('CHOOSE_ACCOUNT_CARD_SELECTOR')} +${extractFunction('getPageTextSnapshot')} +${extractFunction('normalizeAuthAccountIdentifier')} +${extractFunction('getChooseAccountCandidateText')} +${extractFunction('isChooseAccountPage')} +${extractFunction('isChooseAccountRemovalAction')} +${extractFunction('resolveChooseAccountClickTarget')} +${extractFunction('resolveChooseAccountCardTarget')} +${extractFunction('findChooseAccountButtonForEmail')} +${extractFunction('findChooseAccountOtherAccountButton')} +${extractFunction('getChooseAccountListedEmails')} +${extractFunction('resolveChooseAccountAction')} +${extractFunction('createStep6SuccessResult')} +${extractFunction('createStep6OAuthConsentSuccessResult')} +${extractFunction('createStep6AddEmailSuccessResult')} +${extractFunction('createStep6RecoverableResult')} +${extractFunction('normalizeStep6Snapshot')} +${extractFunction('isOpenAiOAuthAuthorizationRoute')} +${extractFunction('isPostChooseAccountOAuthRoute')} +${extractFunction('waitForChooseAccountTransition')} +${extractFunction('step6ChooseExistingAccount')} + +return { + clicked, + run() { + return step6ChooseExistingAccount( + { email: 'sedate-iodize-lisp@duck.com', loginIdentifierType: 'email', visibleStep: 9 }, + { state: 'choose_account_page', url: location.href } + ); + }, +}; +`)(); + + const result = await api.run(); + + assert.deepEqual(api.clicked, ['other-account']); + assert.equal(result.routed, 'email'); + assert.equal(result.email, 'sedate-iodize-lisp@duck.com'); + assert.equal(result.state, 'email_page'); +});