diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index d3c890e..ef15a39 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -35,6 +35,23 @@ return null; } + if (typeof chrome?.tabs?.get === 'function' && typeof chrome?.windows?.update === 'function') { + try { + const tab = await chrome.tabs.get(tabId); + const windowId = Number(tab?.windowId); + if (Number.isInteger(windowId) && windowId >= 0) { + await chrome.windows.update(windowId, { state: 'normal', focused: true }).catch(() => {}); + await chrome.windows.update(windowId, { + focused: true, + width: 1200, + height: 900, + }).catch(() => {}); + } + } catch { + // Best-effort only. Step 2 still has content-side entry retries. + } + } + if (typeof addLog === 'function') { await addLog( `步骤 ${step}:注册页已打开,正在等待页面加载完成并额外稳定 3 秒...`, diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index 44b0a1c..49e75f0 100644 --- a/background/steps/submit-signup-email.js +++ b/background/steps/submit-signup-email.js @@ -185,6 +185,33 @@ }); } + async function normalizeSignupTabWindowForStep2(tabId) { + if ( + !Number.isInteger(tabId) + || typeof chrome?.tabs?.get !== 'function' + || typeof chrome?.windows?.update !== 'function' + ) { + return; + } + + try { + const tab = await chrome.tabs.get(tabId); + const windowId = Number(tab?.windowId); + if (!Number.isInteger(windowId) || windowId < 0) { + return; + } + + await chrome.windows.update(windowId, { state: 'normal', focused: true }).catch(() => {}); + await chrome.windows.update(windowId, { + focused: true, + width: 1200, + height: 900, + }).catch(() => {}); + } catch { + // Best-effort only: content-side retries still handle layout recovery. + } + } + async function ensureSignupPhoneEntryReady(tabId) { if (!Number.isInteger(tabId)) { throw new Error('步骤 2:未找到可用的注册页标签,无法切换到手机号注册入口。'); @@ -230,6 +257,7 @@ signupTabId = (await ensureSignupEntryPageReady(2)).tabId; } else { await chrome.tabs.update(signupTabId, { active: true }); + await normalizeSignupTabWindowForStep2(signupTabId); await waitForStep2SignupTabToSettle( signupTabId, '步骤 2:已切换到注册页标签,正在等待页面加载完成并额外稳定 3 秒...' diff --git a/content/signup-page.js b/content/signup-page.js index 5da8a6c..d6b282f 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -602,12 +602,38 @@ function getSignupEmailContinueButton({ allowDisabled = false } = {}) { }) || null; } -function findSignupEntryTrigger() { +function findSignupEntryTrigger(options = {}) { + const { allowHiddenFallback = true } = options || {}; const candidates = document.querySelectorAll('a, button, [role="button"], [role="link"]'); - return Array.from(candidates).find((el) => { - if (!isVisibleElement(el) || !isActionEnabled(el)) return false; - return SIGNUP_ENTRY_TRIGGER_PATTERN.test(getActionText(el)); - }) || null; + let hiddenSignupTrigger = null; + + for (const el of Array.from(candidates)) { + if (!isActionEnabled(el)) continue; + if (!SIGNUP_ENTRY_TRIGGER_PATTERN.test(getActionText(el))) continue; + if (isVisibleElement(el)) { + return el; + } + if (!hiddenSignupTrigger) { + hiddenSignupTrigger = el; + } + } + + if (!allowHiddenFallback || !hiddenSignupTrigger) { + return null; + } + + const view = typeof window !== 'undefined' ? window : globalThis; + const collapsedViewport = Boolean( + Math.round(Number(view?.innerWidth) || 0) < 240 + || Math.round(Number(view?.innerHeight) || 0) < 160 + || Math.round(Number(view?.outerWidth) || 0) < 320 + || Math.round(Number(view?.outerHeight) || 0) < 180 + ); + const pageText = typeof getPageTextSnapshot === 'function' + ? getPageTextSnapshot() + : ''; + const looksLikeLoggedOutHome = /登录|登入|log\s*in|sign\s*in/i.test(pageText); + return collapsedViewport || looksLikeLoggedOutHome ? hiddenSignupTrigger : null; } function getSignupPasswordDisplayedEmail() { @@ -719,6 +745,7 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) { summary.signupTrigger = { tag: (snapshot.signupTrigger.tagName || '').toLowerCase(), text: getActionText(snapshot.signupTrigger).slice(0, 80), + visible: isVisibleElement(snapshot.signupTrigger), }; } @@ -1085,9 +1112,13 @@ async function waitForSignupEntryState(options = {}) { : `步骤 ${step}:已找到官网注册入口,等待 3 秒后点击...`); await sleep(3000); throwIfStopped(); + const clickTarget = findSignupEntryTrigger({ allowHiddenFallback: false }) || snapshot.signupTrigger; + if (!isVisibleElement(clickTarget)) { + log(`步骤 ${step}:注册入口仍处于不可见状态,继续按入口重试节奏尝试恢复点击...`, 'warn'); + } await humanPause(350, 900); await performOperationWithDelay({ stepKey: 'signup-entry', kind: 'click', label: 'open-signup-entry' }, async () => { - simulateClick(snapshot.signupTrigger); + simulateClick(clickTarget); }); } } @@ -2380,9 +2411,13 @@ async function waitForSignupPhoneEntryState(options = {}) { : `步骤 ${step}:已找到官网注册入口,等待 3 秒后点击...`); await sleep(3000); throwIfStopped(); + const clickTarget = findSignupEntryTrigger({ allowHiddenFallback: false }) || snapshot.signupTrigger; + if (!isVisibleElement(clickTarget)) { + log(`步骤 ${step}:注册入口仍处于不可见状态,继续按入口重试节奏尝试恢复点击...`, 'warn'); + } await humanPause(350, 900); await performOperationWithDelay({ stepKey: 'signup-phone-entry', kind: 'click', label: 'open-signup-entry' }, async () => { - simulateClick(snapshot.signupTrigger); + simulateClick(clickTarget); }); } await sleep(250); diff --git a/tests/signup-step2-email-switch.test.js b/tests/signup-step2-email-switch.test.js index c5bc89b..4f36183 100644 --- a/tests/signup-step2-email-switch.test.js +++ b/tests/signup-step2-email-switch.test.js @@ -561,6 +561,139 @@ return { assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true); }); +test('waitForSignupEntryState treats hidden signup action on collapsed logged-out home as retryable entry', async () => { + const api = new Function(` +const logs = []; +const clicks = []; +let now = 0; + +const signupButton = { + textContent: '免费注册', + value: '', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'button'; + return ''; + }, + getBoundingClientRect() { + return { width: 0, height: 0 }; + }, +}; + +const document = { + querySelector() { + return null; + }, + querySelectorAll(selector) { + if (selector === 'a, button, [role="button"], [role="link"]') { + return [signupButton]; + } + return []; + }, +}; + +const window = { + innerWidth: 0, + innerHeight: 0, + outerWidth: 159, + outerHeight: 27, +}; + +const location = { + href: 'https://chatgpt.com/', +}; + +const Date = { + now() { + return now; + }, +}; + +${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')} +${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')} +${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')} + +function isVisibleElement(el) { + const rect = el?.getBoundingClientRect?.() || { width: 0, height: 0 }; + return rect.width > 0 && rect.height > 0; +} + +function isActionEnabled(el) { + return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; +} + +function getActionText(el) { + return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')] + .filter(Boolean) + .join(' ') + .replace(/\\s+/g, ' ') + .trim(); +} + +function getPageTextSnapshot() { + return '跳至内容 ChatGPT 登录 我们先从哪里开始呢?'; +} + +function getSignupPasswordInput() { + return null; +} + +function isSignupPasswordPage() { + return false; +} + +function getSignupPasswordSubmitButton() { + return null; +} + +function getSignupPasswordDisplayedEmail() { + return ''; +} + +function throwIfStopped() {} + +function log(message, level = 'info') { + logs.push({ message, level }); +} + +async function humanPause() {} + +function simulateClick(target) { + clicks.push(getActionText(target)); +} + +async function sleep(ms) { + now += ms; +} + +${extractFunction('getSignupEmailInput')} +${extractFunction('getSignupPhoneInput')} +${extractFunction('getSignupEmailContinueButton')} +${extractFunction('findSignupEntryTrigger')} +${extractFunction('inspectSignupEntryState')} +${extractFunction('waitForSignupEntryState')} + +return { + async run() { + return waitForSignupEntryState({ timeout: 30000, autoOpenEntry: true, step: 2 }); + }, + getClicks() { + return clicks.slice(); + }, + getLogs() { + return logs.slice(); + }, +}; +`)(); + + const snapshot = await api.run(); + + assert.equal(snapshot.state, 'entry_home'); + assert.equal(api.getClicks().length, 6); + assert.equal(api.getLogs().some(({ message }) => message.includes('注册入口仍处于不可见状态')), true); + assert.equal(api.getLogs().some(({ message, level }) => level === 'warn' && /已完成 5 次重试/.test(message)), true); +}); + test('ensureSignupPhoneEntryReady opens free signup before switching to the phone entry', async () => { const api = new Function(` const logs = [];