diff --git a/background.js b/background.js index d665f84..77551f8 100644 --- a/background.js +++ b/background.js @@ -6667,7 +6667,7 @@ function isSignupPasswordPageUrl(rawUrl) { const parsed = parseUrlSafely(rawUrl); if (!parsed) return false; return isSignupPageHost(parsed.hostname) - && /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || ''); + && /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(parsed.pathname || ''); } function isSignupEmailVerificationPageUrl(rawUrl) { diff --git a/background/navigation-utils.js b/background/navigation-utils.js index 5292727..f1ceef3 100644 --- a/background/navigation-utils.js +++ b/background/navigation-utils.js @@ -72,7 +72,7 @@ const parsed = parseUrlSafely(rawUrl); if (!parsed) return false; return isSignupPageHost(parsed.hostname) - && /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || ''); + && /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(parsed.pathname || ''); } function isSignupEmailVerificationPageUrl(rawUrl) { diff --git a/content/signup-page.js b/content/signup-page.js index b1fe471..032e25d 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1719,11 +1719,88 @@ async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption return false; } + const getScrollableTargets = () => { + const seen = new Set(); + const targets = []; + const pushTarget = (element) => { + if (!element || seen.has(element)) { + return; + } + seen.add(element); + const scrollHeight = Number(element.scrollHeight) || 0; + const clientHeight = Number(element.clientHeight) || 0; + if (scrollHeight > clientHeight + 2) { + targets.push(element); + } + }; + + getVisibleSignupPhoneCountryListboxOptions().forEach((option) => { + let current = option.parentElement || null; + let depth = 0; + while (current && depth < 6) { + pushTarget(current); + if (current === document.body || current === document.documentElement) { + break; + } + current = current.parentElement || null; + depth += 1; + } + }); + + Array.from(document.querySelectorAll('[role="listbox"]')) + .filter((listbox) => isVisibleElement(listbox)) + .forEach(pushTarget); + + return targets; + }; + + const dispatchListboxScroll = (element) => { + if (!element || typeof element.dispatchEvent !== 'function') { + return; + } + try { + element.dispatchEvent(typeof Event === 'function' + ? new Event('scroll', { bubbles: true }) + : { type: 'scroll' }); + } catch { + try { + element.dispatchEvent({ type: 'scroll' }); + } catch { } + } + }; + + const resetListboxScroll = () => { + getScrollableTargets().forEach((target) => { + if ((Number(target.scrollTop) || 0) > 0) { + target.scrollTop = 0; + dispatchListboxScroll(target); + } + }); + }; + + const scrollListboxDown = () => { + let scrolled = false; + getScrollableTargets().forEach((target) => { + const before = Number(target.scrollTop) || 0; + const maxScrollTop = Math.max(0, (Number(target.scrollHeight) || 0) - (Number(target.clientHeight) || 0)); + if (maxScrollTop <= before + 1) { + return; + } + const step = Math.max(360, Math.floor((Number(target.clientHeight) || 0) * 0.85)); + target.scrollTop = Math.min(maxScrollTop, before + step); + dispatchListboxScroll(target); + scrolled = true; + }); + return scrolled; + }; + simulateClick(button); await sleep(200); + resetListboxScroll(); const start = Date.now(); - while (Date.now() - start < 3000) { + let reachedListEndAt = 0; + while (Date.now() - start < 8000) { throwIfStopped(); const option = findSignupPhoneCountryListboxOption(targetOption, options); if (option) { @@ -1733,7 +1810,17 @@ async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption return true; } } - await sleep(150); + + if (!scrollListboxDown()) { + reachedListEndAt += 1; + if (reachedListEndAt >= 6) { + break; + } + await sleep(150); + continue; + } + reachedListEndAt = 0; + await sleep(220); } return false; @@ -2198,7 +2285,11 @@ async function submitSignupPhoneNumberAndContinue(payload = {}) { }); if (countrySelection.hasCountryControl && !countrySelection.matched) { const currentCountryText = getSignupPhoneCountryButtonText(snapshot.phoneInput) || '未知'; - throw new Error(`步骤 2:手机号国家下拉框未能自动切换到 ${countryLabel || phoneNumber},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`); + const targetDialCode = resolveSignupPhoneTargetDialCode({ countryLabel, phoneNumber }, countrySelection.selectedOption); + const targetLabel = targetDialCode + ? `目标区号 +${targetDialCode}(号码 ${phoneNumber}${countryLabel ? `,国家 ${countryLabel}` : ''})` + : (countryLabel || phoneNumber); + throw new Error(`步骤 2:手机号国家下拉框未能自动切换到 ${targetLabel},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`); } const dialCode = resolveSignupPhoneDialCode(snapshot.phoneInput, { @@ -2973,7 +3064,7 @@ function getStep5ErrorText() { function isSignupPasswordPage() { - return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || ''); + return /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(location.pathname || ''); } function getSignupPasswordInput() { @@ -3564,7 +3655,8 @@ async function selectCountryForPhoneInput(phoneInput, phoneNumber = '', countryL if (selection.hasCountryControl && targetDialCode) { if (!selection.matched || (displayedDialCode && displayedDialCode !== targetDialCode)) { const currentCountryText = getSignupPhoneCountryButtonText(phoneInput) || displayedDialCode || '未知'; - throw new Error(`步骤 ${visibleStep}:手机号登录国家下拉框未能自动切换到 ${countryLabel || phoneNumber},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`); + const targetLabel = `目标区号 +${targetDialCode}(号码 ${phoneNumber}${countryLabel ? `,国家 ${countryLabel}` : ''})`; + throw new Error(`步骤 ${visibleStep}:手机号登录国家下拉框未能自动切换到 ${targetLabel},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`); } return targetDialCode; } diff --git a/tests/background-navigation-utils-module.test.js b/tests/background-navigation-utils-module.test.js index 44a9fd1..7ce8a09 100644 --- a/tests/background-navigation-utils-module.test.js +++ b/tests/background-navigation-utils-module.test.js @@ -16,6 +16,21 @@ test('navigation utils module exposes a factory', () => { assert.equal(typeof api?.createNavigationUtils, 'function'); }); +test('navigation utils recognize signup password pages for email and phone signup', () => { + const source = fs.readFileSync('background/navigation-utils.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope); + const utils = api.createNavigationUtils({ + DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts', + DEFAULT_SUB2API_URL: 'https://sub.example.com/admin/accounts', + normalizeLocalCpaStep9Mode: (value) => value, + }); + + assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/create-account/password'), true); + assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/log-in/password'), true); + assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/log-in'), false); +}); + test('navigation utils treat 126 mail hosts as part of the shared NetEase mail family', () => { const source = fs.readFileSync('background/navigation-utils.js', 'utf8'); const globalScope = {}; diff --git a/tests/background-signup-step2-branching.test.js b/tests/background-signup-step2-branching.test.js index 3a02ee2..b5d8de4 100644 --- a/tests/background-signup-step2-branching.test.js +++ b/tests/background-signup-step2-branching.test.js @@ -10,6 +10,10 @@ const signupFlowSource = fs.readFileSync('background/signup-flow-helpers.js', 'u const signupFlowGlobalScope = {}; const signupFlowApi = new Function('self', `${signupFlowSource}; return self.MultiPageSignupFlowHelpers;`)(signupFlowGlobalScope); +const navigationSource = fs.readFileSync('background/navigation-utils.js', 'utf8'); +const navigationGlobalScope = {}; +const navigationApi = new Function('self', `${navigationSource}; return self.MultiPageBackgroundNavigationUtils;`)(navigationGlobalScope); + test('step 2 completes with password step skipped when landing on email verification page', async () => { const completedPayloads = []; @@ -473,6 +477,69 @@ test('signup flow helper recognizes email verification page as post-email landin assert.equal(passwordReadyChecks, 0); }); +test('signup flow helper accepts phone signup landing on login password page', async () => { + let ensureCalls = 0; + let passwordReadyChecks = 0; + let predicateAcceptedLoginPassword = false; + const navigationUtils = navigationApi.createNavigationUtils({ + DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts', + DEFAULT_SUB2API_URL: 'https://sub.example.com/admin/accounts', + normalizeLocalCpaStep9Mode: (value) => value, + }); + + const helpers = signupFlowApi.createSignupFlowHelpers({ + buildGeneratedAliasEmail: () => '', + chrome: { + tabs: { + get: async () => ({ + id: 22, + url: 'https://auth.openai.com/log-in/password', + }), + }, + }, + ensureContentScriptReadyOnTab: async () => { + ensureCalls += 1; + }, + ensureHotmailAccountForFlow: async () => ({}), + ensureLuckmailPurchaseForFlow: async () => ({}), + isGeneratedAliasProvider: () => false, + isHotmailProvider: () => false, + isLuckmailProvider: () => false, + isSignupEmailVerificationPageUrl: navigationUtils.isSignupEmailVerificationPageUrl, + isSignupPasswordPageUrl: (url) => { + const accepted = navigationUtils.isSignupPasswordPageUrl(url); + if (accepted && /\/log-in\/password(?:[/?#]|$)/i.test(url || '')) { + predicateAcceptedLoginPassword = true; + } + return accepted; + }, + reuseOrCreateTab: async () => 22, + sendToContentScriptResilient: async (_source, message) => { + assert.equal(message.type, 'ENSURE_SIGNUP_PASSWORD_PAGE_READY'); + passwordReadyChecks += 1; + return {}; + }, + setEmailState: async () => {}, + SIGNUP_ENTRY_URL: 'https://chatgpt.com/', + SIGNUP_PAGE_INJECT_FILES: [], + waitForTabUrlMatch: async (_tabId, predicate) => { + const url = 'https://auth.openai.com/log-in/password'; + return predicate(url) ? { id: 22, url } : null; + }, + }); + + const result = await helpers.ensureSignupPostIdentityPageReadyInTab(22, 2); + + assert.deepStrictEqual(result, { + ready: true, + state: 'password_page', + url: 'https://auth.openai.com/log-in/password', + }); + assert.equal(predicateAcceptedLoginPassword, true); + assert.equal(ensureCalls, 1); + assert.equal(passwordReadyChecks, 1); +}); + test('signup flow helper reuses existing managed alias email when it is still compatible', async () => { let buildCalls = 0; let setEmailCalls = 0; diff --git a/tests/signup-entry-diagnostics.test.js b/tests/signup-entry-diagnostics.test.js index ab50b95..d32df30 100644 --- a/tests/signup-entry-diagnostics.test.js +++ b/tests/signup-entry-diagnostics.test.js @@ -51,6 +51,17 @@ function extractFunction(name) { return source.slice(start, end); } +test('signup password detector accepts create-account and phone login password paths', () => { + const run = (pathname) => new Function('location', ` +${extractFunction('isSignupPasswordPage')} +return isSignupPasswordPage(); +`)({ pathname }); + + assert.equal(run('/create-account/password'), true); + assert.equal(run('/log-in/password'), true); + assert.equal(run('/log-in'), false); +}); + test('signup entry diagnostics summarizes current page inputs and visible actions', () => { const api = new Function(` const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\\s*up|register|create\\s*account|create\\s+account/i;