diff --git a/background.js b/background.js index 20b2788..678fdd7 100644 --- a/background.js +++ b/background.js @@ -10097,6 +10097,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe setEmailState, SIGNUP_ENTRY_URL, SIGNUP_PAGE_INJECT_FILES, + waitForTabStableComplete, waitForTabUrlMatch, }); const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({ @@ -10232,6 +10233,7 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ shouldUseCustomRegistrationEmail, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, throwIfStopped, + waitForTabStableComplete, phoneVerificationHelpers, resolveSignupMethod, }); diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index f46ecae..c087af1 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -4079,15 +4079,21 @@ if (!normalizedActivation) { throw new Error('步骤 2:接码平台返回的手机号订单无效。'); } + const countryConfig = resolveCountryConfigFromActivation(normalizedActivation, state); + const signupActivation = normalizeActivation({ + ...normalizedActivation, + countryId: countryConfig?.id ?? normalizedActivation.countryId, + countryLabel: normalizedActivation.countryLabel || countryConfig?.label || '', + }) || normalizedActivation; await persistSignupPhoneRuntimeState({ - signupPhoneNumber: normalizedActivation.phoneNumber, - signupPhoneActivation: normalizedActivation, + signupPhoneNumber: signupActivation.phoneNumber, + signupPhoneActivation: signupActivation, signupPhoneVerificationRequestedAt: null, signupPhoneVerificationPurpose: 'signup', accountIdentifierType: 'phone', - accountIdentifier: normalizedActivation.phoneNumber, + accountIdentifier: signupActivation.phoneNumber, }); - return normalizedActivation; + return signupActivation; }); } diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index c8172a1..36671c2 100644 --- a/background/signup-flow-helpers.js +++ b/background/signup-flow-helpers.js @@ -24,6 +24,7 @@ setEmailState, SIGNUP_ENTRY_URL, SIGNUP_PAGE_INJECT_FILES, + waitForTabStableComplete = null, waitForTabUrlMatch, } = deps; @@ -139,6 +140,22 @@ throw new Error(`注册身份提交后未能识别当前页面,既不是密码页、验证码页,也不是资料页。URL: ${landingUrl || 'unknown'}`); } + if (landingState !== 'password_page' && typeof waitForTabStableComplete === 'function') { + const stableTab = await waitForTabStableComplete(tabId, { + timeoutMs: 45000, + retryDelayMs: 300, + stableMs: 800, + initialDelayMs: 300, + }); + if (stableTab?.url) { + const stableState = resolveSignupPostIdentityState(stableTab.url); + if (stableState) { + landingUrl = stableTab.url; + landingState = stableState; + } + } + } + await ensureContentScriptReadyOnTab('signup-page', tabId, { inject: SIGNUP_PAGE_INJECT_FILES, injectSource: 'signup-page', diff --git a/background/steps/fetch-signup-code.js b/background/steps/fetch-signup-code.js index 4c986c7..0d2051f 100644 --- a/background/steps/fetch-signup-code.js +++ b/background/steps/fetch-signup-code.js @@ -27,6 +27,7 @@ shouldUseCustomRegistrationEmail, STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, throwIfStopped, + waitForTabStableComplete = null, phoneVerificationHelpers = null, resolveSignupMethod = () => 'email', } = deps; @@ -119,6 +120,16 @@ await chrome.tabs.update(signupTabId, { active: true }); throwIfStopped(); + if (typeof waitForTabStableComplete === 'function') { + await addLog('步骤 4:等待注册验证码页面完成加载后再继续...', 'info'); + await waitForTabStableComplete(signupTabId, { + timeoutMs: 45000, + retryDelayMs: 300, + stableMs: 800, + initialDelayMs: 300, + }); + } + throwIfStopped(); await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...'); const prepareRequest = { diff --git a/content/signup-page.js b/content/signup-page.js index 235e8c2..862e01e 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -1147,8 +1147,8 @@ function normalizePhoneDigits(value) { } function extractDialCodeFromText(value) { - const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/); - return String(match?.[1] || match?.[2] || '').trim(); + const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*\(\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/); + return String(match?.[1] || match?.[2] || match?.[3] || '').trim(); } function dispatchSignupPhoneFieldEvents(element) { @@ -1162,12 +1162,48 @@ function normalizeSignupCountryLabel(value) { .normalize('NFKD') .replace(/[\u0300-\u036f]/g, '') .replace(/&/g, ' and ') - .replace(/[^\w\s]/g, ' ') + .replace(/[^\p{L}\p{N}\s]/gu, ' ') .replace(/\s+/g, ' ') .trim() .toLowerCase(); } +function getSignupCountryLabelAliases(value) { + const aliases = new Set(); + const addAlias = (alias) => { + const normalized = normalizeSignupCountryLabel(alias); + if (normalized) { + aliases.add(normalized); + } + }; + + const raw = String(value || '').trim(); + addAlias(raw); + + const normalized = normalizeSignupCountryLabel(raw); + const compact = normalized.replace(/\s+/g, ''); + if ( + /(?:^|\s)(?:gb|uk)(?:\s|$)/i.test(raw) + || /england|united\s*kingdom|great\s*britain|\bbritain\b/i.test(raw) + || /英国|英格兰|大不列颠/.test(raw) + || ['gb', 'uk', 'england', 'unitedkingdom', 'greatbritain', 'britain'].includes(compact) + ) { + [ + 'GB', + 'UK', + 'United Kingdom', + 'Great Britain', + 'Britain', + 'England', + '英国', + '英格兰', + '大不列颠', + ].forEach(addAlias); + } + + return Array.from(aliases); +} + function getSignupPhoneOptionLabel(option) { return String(option?.textContent || option?.label || '') .replace(/\s+/g, ' ') @@ -1242,12 +1278,72 @@ function getSignupPhoneForm(phoneInput = getSignupPhoneInput()) { return phoneInput?.closest?.('form') || null; } -function getSignupPhoneCountrySelect(phoneInput = getSignupPhoneInput()) { - const form = getSignupPhoneForm(phoneInput); - if (!form || typeof form.querySelector !== 'function') { - return null; +function getSignupPhoneControlRoots(phoneInput = getSignupPhoneInput()) { + const roots = []; + const addRoot = (root) => { + if (root && !roots.includes(root)) { + roots.push(root); + } + }; + + addRoot(phoneInput?.closest?.('form')); + addRoot(phoneInput?.closest?.('fieldset')); + addRoot(phoneInput?.closest?.('[data-rac]')); + addRoot(phoneInput?.closest?.('[role="group"]')); + addRoot(phoneInput?.parentElement); + addRoot(phoneInput?.parentElement?.parentElement); + addRoot(document); + + return roots; +} + +function querySignupPhoneCountryElements(root, selector) { + if (!root || !selector) { + return []; } - return form.querySelector('select'); + if (typeof root.querySelectorAll === 'function') { + const directMatches = Array.from(root.querySelectorAll(selector)); + if (directMatches.length > 0) { + return directMatches; + } + } + if (typeof root.querySelector === 'function') { + const selectors = String(selector || '') + .split(',') + .map((part) => part.trim()) + .filter(Boolean); + const matches = []; + for (const part of selectors) { + const element = root.querySelector(part); + if (element && !matches.includes(element)) { + matches.push(element); + } + } + return matches; + } + return []; +} + +function isSignupPhoneCountrySelect(select) { + if (!select) { + return false; + } + return Array.from(select.options || []).some((option) => ( + extractDialCodeFromText(getSignupPhoneOptionLabel(option)) + || /^[A-Z]{2}$/.test(normalizeSignupCountryOptionValue(option?.value)) + )); +} + +function getSignupPhoneCountrySelect(phoneInput = getSignupPhoneInput()) { + const selects = []; + for (const root of getSignupPhoneControlRoots(phoneInput)) { + for (const select of querySignupPhoneCountryElements(root, 'select')) { + if (!selects.includes(select)) { + selects.push(select); + } + } + } + return selects.find(isSignupPhoneCountrySelect) || selects[0] || null; } function getSignupPhoneSelectedCountryOption(phoneInput = getSignupPhoneInput()) { @@ -1259,9 +1355,7 @@ function getSignupPhoneSelectedCountryOption(phoneInput = getSignupPhoneInput()) } function getSignupPhoneCountryButtonText(phoneInput = getSignupPhoneInput()) { - const form = getSignupPhoneForm(phoneInput); - if (!form || typeof form.querySelector !== 'function') return ''; - const button = form.querySelector('button[aria-haspopup="listbox"]'); + const button = getSignupPhoneCountryButton(phoneInput); if (!button) return ''; const valueNode = button.querySelector('.react-aria-SelectValue'); return String(valueNode?.textContent || button.textContent || '') @@ -1269,6 +1363,24 @@ function getSignupPhoneCountryButtonText(phoneInput = getSignupPhoneInput()) { .trim(); } +function getSignupPhoneCountryButton(phoneInput = getSignupPhoneInput()) { + const candidates = []; + for (const root of getSignupPhoneControlRoots(phoneInput)) { + const buttons = querySignupPhoneCountryElements( + root, + 'button[aria-haspopup="listbox"], [role="button"][aria-haspopup="listbox"], [role="combobox"][aria-haspopup="listbox"], button[aria-expanded]' + ); + for (const button of buttons) { + if (!candidates.includes(button)) { + candidates.push(button); + } + } + } + return candidates.find((button) => isVisibleElement(button) && extractDialCodeFromText(getActionText(button))) + || candidates.find(isVisibleElement) + || null; +} + function getSignupPhoneDisplayedDialCode(phoneInput = getSignupPhoneInput()) { const buttonDialCode = extractDialCodeFromText(getSignupPhoneCountryButtonText(phoneInput)); if (buttonDialCode) { @@ -1295,29 +1407,134 @@ function getSignupPhoneHiddenNumberInput(phoneInput = getSignupPhoneInput()) { return form.querySelector('input[name="phoneNumber"]'); } +function resolveSignupPhoneDialCodeFromNumber(phoneNumber = '', texts = []) { + const digits = normalizePhoneDigits(phoneNumber); + if (!digits) { + return ''; + } + + const textDialCodes = texts + .map((text) => normalizePhoneDigits(extractDialCodeFromText(text))) + .filter((dialCode) => dialCode && digits.startsWith(dialCode) && digits.length > dialCode.length) + .sort((left, right) => right.length - left.length); + if (textDialCodes[0]) { + return textDialCodes[0]; + } + + const knownDialCodes = [ + '1246', '1264', '1268', '1284', '1340', '1345', '1441', '1473', '1649', '1664', '1670', '1671', '1684', + '1721', '1758', '1767', '1784', '1809', '1829', '1849', '1868', '1869', '1876', + '971', '962', '886', '880', '856', '855', '852', '853', '673', '672', '670', '599', '598', '597', '596', + '595', '594', '593', '592', '591', '590', '509', '508', '507', '506', '505', '504', '503', '502', '501', + '423', '421', '420', '389', '387', '386', '385', '383', '382', '381', '380', '379', '378', '377', '376', + '375', '374', '373', '372', '371', '370', '359', '358', '357', '356', '355', '354', '353', '352', '351', + '350', '299', '298', '297', '291', '290', '269', '268', '267', '266', '265', '264', '263', '262', '261', + '260', '258', '257', '256', '255', '254', '253', '252', '251', '250', '249', '248', '247', '246', '245', + '244', '243', '242', '241', '240', '239', '238', '237', '236', '235', '234', '233', '232', '231', '230', + '229', '228', '227', '226', '225', '224', '223', '222', '221', '220', '218', '216', '213', '212', '211', + '98', '95', '94', '93', '92', '91', '90', '89', '88', '86', '84', '82', '81', '66', '65', '64', '63', + '62', '61', '60', '58', '57', '56', '55', '54', '53', '52', '51', '49', '48', '47', '46', '45', '44', + '43', '41', '40', '39', '36', '34', '33', '32', '31', '30', '27', '20', '7', '1', + ]; + return knownDialCodes.find((code) => digits.startsWith(code) && digits.length > code.length) || ''; +} + +function resolveSignupPhoneTargetDialCode(options = {}, targetOption = null) { + const optionDialCode = extractDialCodeFromText(getSignupPhoneOptionLabel(targetOption)); + if (optionDialCode) { + return optionDialCode; + } + + const countryText = String(options.countryLabel || '').trim(); + if (/australia|澳大利亚/i.test(countryText)) return '61'; + if (/thailand|泰国/i.test(countryText)) return '66'; + if (/vietnam|越南/i.test(countryText)) return '84'; + if (/england|united\s*kingdom|great\s*britain|\bbritain\b|英国|英格兰|uk|gb/i.test(countryText)) return '44'; + + return resolveSignupPhoneDialCodeFromNumber(options.phoneNumber); +} + +function getSignupPhoneCountryTargetLabels(targetOption, options = {}) { + const labels = new Set(); + const addLabel = (value) => { + getSignupCountryLabelAliases(value).forEach((alias) => labels.add(alias)); + }; + + addLabel(options.countryLabel); + if (targetOption) { + getSignupPhoneCountryMatchLabels(targetOption).forEach(addLabel); + } + + return Array.from(labels); +} + +function doesSignupPhoneCountryTextMatchTarget(text, targetOption, options = {}) { + const normalizedText = normalizeSignupCountryLabel(text); + if (!normalizedText) { + return false; + } + + const labels = getSignupPhoneCountryTargetLabels(targetOption, options); + if (labels.some((label) => ( + label + && ( + normalizedText === label + || (label.length > 1 && normalizedText.includes(label)) + || (normalizedText.length > 2 && label.includes(normalizedText)) + ) + ))) { + return true; + } + + const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption); + return Boolean(targetDialCode && extractDialCodeFromText(text) === targetDialCode); +} + +function isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options = {}) { + const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption); + const displayedText = getSignupPhoneCountryButtonText(phoneInput); + const displayedDialCode = extractDialCodeFromText(displayedText); + + if (targetDialCode && displayedDialCode) { + return displayedDialCode === targetDialCode + && (!displayedText || doesSignupPhoneCountryTextMatchTarget(displayedText, targetOption, options)); + } + + if (displayedText && doesSignupPhoneCountryTextMatchTarget(displayedText, targetOption, options)) { + return true; + } + + const selectedOption = getSignupPhoneSelectedCountryOption(phoneInput); + if (selectedOption && targetOption && isSameSignupCountryOption(selectedOption, targetOption)) { + return !displayedDialCode || !targetDialCode || displayedDialCode === targetDialCode; + } + + return Boolean(selectedOption && !targetOption && targetDialCode && displayedDialCode === targetDialCode); +} + function findSignupPhoneCountryOptionByLabel(phoneInput, countryLabel) { const select = getSignupPhoneCountrySelect(phoneInput); if (!select) { return null; } - const normalizedTarget = normalizeSignupCountryLabel(countryLabel); - if (!normalizedTarget) { + const normalizedTargets = getSignupCountryLabelAliases(countryLabel); + if (normalizedTargets.length === 0) { return null; } const options = Array.from(select.options || []); return options.find((option) => ( - getSignupPhoneCountryMatchLabels(option).some((label) => normalizeSignupCountryLabel(label) === normalizedTarget) + getSignupPhoneCountryMatchLabels(option).some((label) => normalizedTargets.includes(normalizeSignupCountryLabel(label))) )) || options.find((option) => { const normalizedLabels = getSignupPhoneCountryMatchLabels(option) .map((label) => normalizeSignupCountryLabel(label)) .filter(Boolean); - return normalizedLabels.some((optionLabel) => ( - optionLabel.length > 2 - && normalizedTarget.length > 2 - && (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel)) - )); + return normalizedLabels.some((optionLabel) => normalizedTargets.some((normalizedTarget) => ( + optionLabel.length > 2 + && normalizedTarget.length > 2 + && (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel)) + ))); }) || null; } @@ -1347,7 +1564,7 @@ function findSignupPhoneCountryOptionByPhoneNumber(phoneInput, phoneNumber) { return bestMatch; } -async function trySelectSignupPhoneCountryOption(select, targetOption) { +async function trySelectSignupPhoneCountryOption(select, targetOption, phoneInput = getSignupPhoneInput(), options = {}) { if (!select || !targetOption) { return false; } @@ -1355,47 +1572,123 @@ async function trySelectSignupPhoneCountryOption(select, targetOption) { ? (select.options?.[select.selectedIndex] || null) : null; if (selectedOption && isSameSignupCountryOption(selectedOption, targetOption)) { - return true; + dispatchSignupPhoneFieldEvents(select); + await sleep(120); + return isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options); } select.value = String(targetOption.value || ''); dispatchSignupPhoneFieldEvents(select); await sleep(250); - const nextSelectedOption = select.selectedIndex >= 0 - ? (select.options?.[select.selectedIndex] || null) - : null; - return Boolean(nextSelectedOption && isSameSignupCountryOption(nextSelectedOption, targetOption)); + return isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options); +} + +function getVisibleSignupPhoneCountryListboxOptions() { + const seen = new Set(); + return Array.from(document.querySelectorAll('[role="listbox"] [role="option"], [role="option"]')) + .filter((option) => { + if (!option || seen.has(option)) { + return false; + } + seen.add(option); + return isVisibleElement(option); + }); +} + +function findSignupPhoneCountryListboxOption(targetOption, options = {}) { + const candidates = getVisibleSignupPhoneCountryListboxOptions(); + const byLabel = candidates.find((option) => doesSignupPhoneCountryTextMatchTarget(getActionText(option), targetOption, options)); + if (byLabel) { + return byLabel; + } + + const targetDialCode = resolveSignupPhoneTargetDialCode(options, targetOption); + if (!targetDialCode) { + const digits = normalizePhoneDigits(options.phoneNumber); + let bestMatch = null; + let bestDialCodeLength = 0; + for (const option of candidates) { + const dialCode = normalizePhoneDigits(extractDialCodeFromText(getActionText(option))); + if (!dialCode || !digits.startsWith(dialCode) || dialCode.length <= bestDialCodeLength) { + continue; + } + bestMatch = option; + bestDialCodeLength = dialCode.length; + } + return bestMatch; + } + return candidates.find((option) => extractDialCodeFromText(getActionText(option)) === targetDialCode) || null; +} + +async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption, options = {}) { + const button = getSignupPhoneCountryButton(phoneInput); + if (!button) { + return false; + } + + simulateClick(button); + await sleep(200); + + const start = Date.now(); + while (Date.now() - start < 3000) { + throwIfStopped(); + const option = findSignupPhoneCountryListboxOption(targetOption, options); + if (option) { + simulateClick(option); + await sleep(450); + if (isSignupPhoneCountrySelectionSynced(phoneInput, targetOption, options)) { + return true; + } + } + await sleep(150); + } + + return false; } async function ensureSignupPhoneCountrySelected(phoneInput, options = {}) { const select = getSignupPhoneCountrySelect(phoneInput); - if (!select) { + const hasCountryControl = Boolean(select || getSignupPhoneCountryButton(phoneInput)); + if (!hasCountryControl) { return { hasSelect: false, + hasCountryControl: false, matched: false, selectedOption: null, }; } const byLabel = findSignupPhoneCountryOptionByLabel(phoneInput, options.countryLabel); - if (await trySelectSignupPhoneCountryOption(select, byLabel)) { - return { - hasSelect: true, - matched: true, - selectedOption: getSignupPhoneSelectedCountryOption(phoneInput), - }; - } - const byPhoneNumber = findSignupPhoneCountryOptionByPhoneNumber(phoneInput, options.phoneNumber); - if (await trySelectSignupPhoneCountryOption(select, byPhoneNumber)) { - return { - hasSelect: true, - matched: true, - selectedOption: getSignupPhoneSelectedCountryOption(phoneInput), - }; + const targets = [byLabel, byPhoneNumber, null].filter((target, index, list) => ( + index === list.findIndex((item) => ( + (!item && !target) + || (item && target && isSameSignupCountryOption(item, target)) + )) + )); + + for (const targetOption of targets) { + if (await trySelectSignupPhoneCountryOption(select, targetOption, phoneInput, options)) { + return { + hasSelect: Boolean(select), + hasCountryControl: true, + matched: true, + selectedOption: getSignupPhoneSelectedCountryOption(phoneInput), + }; + } + + if (await trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption, options)) { + return { + hasSelect: Boolean(select), + hasCountryControl: true, + matched: true, + selectedOption: getSignupPhoneSelectedCountryOption(phoneInput), + }; + } } return { - hasSelect: true, + hasSelect: Boolean(select), + hasCountryControl: true, matched: false, selectedOption: getSignupPhoneSelectedCountryOption(phoneInput), }; @@ -1549,8 +1842,9 @@ async function submitSignupPhoneNumberAndContinue(payload = {}) { countryLabel, phoneNumber, }); - if (countrySelection.hasSelect && !countrySelection.matched) { - log(`步骤 2:手机号国家下拉框未能自动切换到 ${countryLabel || phoneNumber},将继续使用当前国家。`, 'warn'); + if (countrySelection.hasCountryControl && !countrySelection.matched) { + const currentCountryText = getSignupPhoneCountryButtonText(snapshot.phoneInput) || '未知'; + throw new Error(`步骤 2:手机号国家下拉框未能自动切换到 ${countryLabel || phoneNumber},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`); } const dialCode = resolveSignupPhoneDialCode(snapshot.phoneInput, { @@ -2046,6 +2340,44 @@ function isPhoneVerificationPageReady() { && /check\s+your\s+phone|phone\s+verification|verify\s+your\s+phone|sms|text\s+message|code\s+to\s+\+/.test(pageText); } +function getDocumentReadyStateSnapshot() { + const readyState = typeof document !== 'undefined' && document + ? String(document.readyState || '').trim().toLowerCase() + : ''; + return readyState || 'complete'; +} + +function isDocumentLoadComplete() { + return getDocumentReadyStateSnapshot() === 'complete'; +} + +async function waitForDocumentLoadComplete(timeout = 15000, label = '页面') { + const start = Date.now(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + if (isDocumentLoadComplete()) { + return true; + } + await sleep(150); + } + + throw new Error(`${label}长时间未完成加载,当前 readyState=${getDocumentReadyStateSnapshot()}。URL: ${location.href}`); +} + +function isSignupVerificationPageInteractiveReady(snapshot = null) { + if (!isDocumentLoadComplete()) { + return false; + } + + const resolvedSnapshot = snapshot || inspectSignupVerificationState(); + if (resolvedSnapshot?.state !== 'verification') { + return false; + } + + return Boolean(getVerificationCodeTarget()); +} + function isStep8Ready() { const continueBtn = getPrimaryContinueButton(); if (!continueBtn) return false; @@ -3191,6 +3523,10 @@ async function waitForSignupVerificationTransition(timeout = 5000) { throwIfStopped(); const snapshot = inspectSignupVerificationState(); + if (snapshot.state === 'verification' && !isSignupVerificationPageInteractiveReady(snapshot)) { + await sleep(200); + continue; + } if ( snapshot.state === 'step5' || snapshot.state === 'logged_in_home' @@ -3241,7 +3577,9 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { } if (snapshot.state === 'verification') { - log(`${prepareLogLabel}:验证码页面已就绪${recoveryRound ? `(期间自动恢复 ${recoveryRound} 次)` : ''}。`, 'ok'); + await waitForDocumentLoadComplete(15000, `${prepareLogLabel}:注册验证码页面`); + await waitForVerificationCodeTarget(15000); + log(`${prepareLogLabel}:验证码页面已完成加载并就绪${recoveryRound ? `(期间自动恢复 ${recoveryRound} 次)` : ''}。`, 'ok'); return { ready: true, retried: recoveryRound, prepareSource }; } @@ -3516,6 +3854,9 @@ async function fillVerificationCode(step, payload) { allowPhoneVerificationPage: payload?.purpose === 'login' || payload?.loginIdentifierType === 'phone', }); } + if (step === 4) { + await waitForDocumentLoadComplete(15000, `步骤 ${step}:注册验证码页面`); + } const combinedSignupProfilePage = step === 4 && await waitForCombinedSignupVerificationProfilePage(); diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index f3cf62f..8f23474 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -2271,6 +2271,11 @@ header { min-height: 33px; } +.signup-phone-runtime-inline, +.signup-phone-runtime-input { + width: 100%; +} + .hero-sms-price-preview-stack { width: 100%; display: flex; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 0792333..e1f387f 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -492,12 +492,6 @@ -
延迟
@@ -1424,32 +1418,38 @@
- + +
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index fb53c33..161aa15 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -6851,11 +6851,6 @@ function updatePhoneVerificationSettingsUI() { typeof rowNexSmsServiceCode !== 'undefined' ? rowNexSmsServiceCode : null, typeof rowHeroSmsMaxPrice !== 'undefined' ? rowHeroSmsMaxPrice : null, typeof rowFiveSimOperator !== 'undefined' ? rowFiveSimOperator : null, - typeof rowHeroSmsRuntimePair !== 'undefined' ? rowHeroSmsRuntimePair : null, - typeof rowHeroSmsCurrentNumber !== 'undefined' ? rowHeroSmsCurrentNumber : null, - typeof rowHeroSmsCurrentCountdown !== 'undefined' ? rowHeroSmsCurrentCountdown : null, - typeof rowHeroSmsCurrentCode !== 'undefined' ? rowHeroSmsCurrentCode : null, - typeof rowHeroSmsPreferredActivation !== 'undefined' ? rowHeroSmsPreferredActivation : null, typeof rowPhoneCodeSettingsGroup !== 'undefined' ? rowPhoneCodeSettingsGroup : null, typeof rowPhoneVerificationResendCount !== 'undefined' ? rowPhoneVerificationResendCount : null, typeof rowPhoneReplacementLimit !== 'undefined' ? rowPhoneReplacementLimit : null, @@ -6885,6 +6880,21 @@ function updatePhoneVerificationSettingsUI() { if (rowFiveSimOperator) { rowFiveSimOperator.style.display = showSettings && fiveSimProvider ? '' : 'none'; } + const runtimeVisible = enabled; + [ + typeof rowHeroSmsRuntimePair !== 'undefined' ? rowHeroSmsRuntimePair : null, + typeof rowHeroSmsCurrentNumber !== 'undefined' ? rowHeroSmsCurrentNumber : null, + typeof rowHeroSmsCurrentCountdown !== 'undefined' ? rowHeroSmsCurrentCountdown : null, + typeof rowHeroSmsCurrentCode !== 'undefined' ? rowHeroSmsCurrentCode : null, + typeof rowHeroSmsPreferredActivation !== 'undefined' ? rowHeroSmsPreferredActivation : null, + ].forEach((row) => { + if (row) { + row.style.display = runtimeVisible ? '' : 'none'; + } + }); + if (typeof syncSignupPhoneInputFromState === 'function') { + syncSignupPhoneInputFromState(latestState); + } if (!showSettings && typeof rowHeroSmsPriceTiers !== 'undefined' && rowHeroSmsPriceTiers) { rowHeroSmsPriceTiers.style.display = 'none'; } @@ -7008,6 +7018,9 @@ function syncSignupPhoneInputFromState(state = latestState) { inputSignupPhone.value = signupPhone; } if (typeof rowSignupPhone !== 'undefined' && rowSignupPhone) { + const phoneVerificationEnabled = typeof inputPhoneVerificationEnabled !== 'undefined' && inputPhoneVerificationEnabled + ? Boolean(inputPhoneVerificationEnabled.checked) + : Boolean(state?.phoneVerificationEnabled || latestState?.phoneVerificationEnabled); const rawSignupMethod = state?.signupMethod || ( typeof getSelectedSignupMethod === 'function' ? getSelectedSignupMethod() @@ -7016,7 +7029,7 @@ function syncSignupPhoneInputFromState(state = latestState) { const selectedMethod = typeof normalizeSignupMethod === 'function' ? normalizeSignupMethod(rawSignupMethod) : (String(rawSignupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'); - rowSignupPhone.style.display = (selectedMethod === 'phone' || Boolean(signupPhone)) ? '' : 'none'; + rowSignupPhone.style.display = phoneVerificationEnabled && (selectedMethod === 'phone' || Boolean(signupPhone)) ? '' : 'none'; } } @@ -7044,7 +7057,7 @@ async function openPlusManualConfirmationDialog(options = {}) { } const result = await sharedFormDialog.open({ title: String(options.title || '').trim() || 'GPC OTP 验证', - message: String(options.message || '').trim() || '请输入收到的 OTP 验证码。', + message: String(options.message || '').trim() || '请在WhatsApp里面获取验证码(耐心等待三十秒左右)', fields: [ { key: 'otp', diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js index 609973b..9586a9f 100644 --- a/tests/sidepanel-phone-verification-settings.test.js +++ b/tests/sidepanel-phone-verification-settings.test.js @@ -62,6 +62,18 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro assert.match(html, /id="row-signup-method"/); assert.match(html, /id="row-signup-phone"/); assert.match(html, /id="input-signup-phone"/); + assert.ok( + html.indexOf('id="row-signup-phone"') > html.indexOf('id="phone-verification-section"'), + 'signup phone runtime row should live inside the phone verification card' + ); + assert.ok( + html.indexOf('id="row-signup-phone"') > html.indexOf('id="row-hero-sms-runtime-pair"'), + 'signup phone runtime row should sit below the SMS order runtime row' + ); + assert.ok( + html.indexOf('id="row-signup-phone"') > html.indexOf('hero-sms-runtime-grid'), + 'signup phone runtime row should not be embedded in the SMS order runtime grid' + ); assert.match(html, /data-signup-method="email"/); assert.match(html, /data-signup-method="phone"/); assert.match(html, /id="row-phone-sms-provider"/); @@ -229,12 +241,13 @@ return { test('updatePhoneVerificationSettingsUI toggles SMS rows from the sms switch and provider selection', () => { const api = new Function(` -const phoneVerificationSectionExpanded = true; +let phoneVerificationSectionExpanded = false; let latestState = {}; const inputPhoneVerificationEnabled = { checked: false }; const rowPhoneVerificationEnabled = { style: { display: 'none' } }; const rowPhoneVerificationFold = { style: { display: 'none' } }; const rowSignupMethod = { style: { display: 'none' } }; +const rowSignupPhone = { style: { display: 'none' } }; const rowPhoneSmsProvider = { style: { display: 'none' } }; const rowPhoneSmsProviderOrder = { style: { display: 'none' } }; const rowPhoneSmsProviderOrderActions = { style: { display: 'none' } }; @@ -289,6 +302,7 @@ const rowNexSmsApiKey = { style: { display: 'none' } }; const rowNexSmsCountry = { style: { display: 'none' } }; const rowNexSmsCountryFallback = { style: { display: 'none' } }; const rowNexSmsServiceCode = { style: { display: 'none' } }; +const rowHeroSmsRuntimePair = { style: { display: 'none' } }; const rowHeroSmsCurrentNumber = { style: { display: 'none' } }; const rowHeroSmsCurrentCountdown = { style: { display: 'none' } }; const rowHeroSmsPriceTiers = { style: { display: 'none' } }; @@ -309,14 +323,19 @@ function updateHeroSmsPlatformDisplay() {} function updateSignupMethodUI() { rowSignupMethod.style.display = inputPhoneVerificationEnabled.checked ? '' : 'none'; } +function syncSignupPhoneInputFromState() { + rowSignupPhone.style.display = inputPhoneVerificationEnabled.checked && latestState.signupPhoneNumber ? '' : 'none'; +} ${extractFunction('updatePhoneVerificationSettingsUI')} return { + setExpanded(value) { phoneVerificationSectionExpanded = Boolean(value); }, setLatestState: (state) => { latestState = state || {}; }, rowPhoneVerificationEnabled, rowPhoneVerificationFold, rowSignupMethod, + rowSignupPhone, rowPhoneSmsProvider, rowPhoneSmsProviderOrder, rowPhoneSmsProviderOrderActions, @@ -338,6 +357,7 @@ return { rowNexSmsCountry, rowNexSmsCountryFallback, rowNexSmsServiceCode, + rowHeroSmsRuntimePair, rowHeroSmsCurrentNumber, rowHeroSmsCurrentCountdown, rowHeroSmsPriceTiers, @@ -358,12 +378,14 @@ return { assert.equal(api.rowPhoneVerificationEnabled.style.display, ''); assert.equal(api.rowPhoneVerificationFold.style.display, 'none'); assert.equal(api.rowSignupMethod.style.display, 'none'); + assert.equal(api.rowSignupPhone.style.display, 'none'); assert.equal(api.rowPhoneSmsProvider.style.display, 'none'); assert.equal(api.rowPhoneSmsProviderOrder.style.display, 'none'); assert.equal(api.rowPhoneSmsProviderOrderActions.style.display, 'none'); assert.equal(api.btnTogglePhoneVerificationSection.disabled, true); assert.equal(api.btnTogglePhoneVerificationSection.textContent, '展开设置'); assert.equal(api.rowHeroSmsPlatform.style.display, ''); + assert.equal(api.rowHeroSmsRuntimePair.style.display, 'none'); assert.equal(api.rowHeroSmsCountry.style.display, 'none'); assert.equal(api.rowHeroSmsCountryFallback.style.display, 'none'); assert.equal(api.rowHeroSmsAcquirePriority.style.display, 'none'); @@ -392,6 +414,19 @@ return { assert.equal(api.rowNexSmsServiceCode.style.display, 'none'); api.inputPhoneVerificationEnabled.checked = true; + api.setLatestState({ signupPhoneNumber: '66959916439' }); + api.updatePhoneVerificationSettingsUI(); + assert.equal(api.rowPhoneVerificationFold.style.display, 'none'); + assert.equal(api.rowSignupMethod.style.display, ''); + assert.equal(api.rowSignupPhone.style.display, ''); + assert.equal(api.rowPhoneSmsProvider.style.display, 'none'); + assert.equal(api.rowHeroSmsRuntimePair.style.display, ''); + assert.equal(api.rowHeroSmsCurrentNumber.style.display, ''); + assert.equal(api.rowHeroSmsCurrentCountdown.style.display, ''); + assert.equal(api.rowHeroSmsCurrentCode.style.display, ''); + assert.equal(api.rowHeroSmsPreferredActivation.style.display, ''); + + api.setExpanded(true); api.updatePhoneVerificationSettingsUI(); assert.equal(api.rowPhoneVerificationFold.style.display, ''); assert.equal(api.rowSignupMethod.style.display, ''); diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js index e9822c2..c81e807 100644 --- a/tests/sidepanel-plus-payment-method.test.js +++ b/tests/sidepanel-plus-payment-method.test.js @@ -331,7 +331,7 @@ let latestState = { plusManualConfirmationStep: 7, plusManualConfirmationMethod: 'gopay-otp', plusManualConfirmationTitle: 'GPC OTP 验证', - plusManualConfirmationMessage: '请输入 OTP。', + plusManualConfirmationMessage: '', }; let activePlusManualConfirmationRequestId = ''; let plusManualConfirmationDialogInFlight = false; @@ -364,6 +364,7 @@ return { events, syncPlusManualConfirmationDialog }; await api.syncPlusManualConfirmationDialog(); assert.equal(api.events[0].type, 'form'); + assert.equal(api.events[0].options.message, '请在WhatsApp里面获取验证码(耐心等待三十秒左右)'); assert.equal(api.events[0].options.confirmLabel, '提交 OTP'); const sendEvent = api.events.find((event) => event.type === 'send'); assert.deepEqual(sendEvent.message.payload, { diff --git a/tests/signup-step2-email-switch.test.js b/tests/signup-step2-email-switch.test.js index 744afd5..197aaf9 100644 --- a/tests/signup-step2-email-switch.test.js +++ b/tests/signup-step2-email-switch.test.js @@ -709,6 +709,9 @@ const countryButton = { get textContent() { return selectValueNode.textContent; }, + getBoundingClientRect() { + return { width: 240, height: 48 }; + }, }; const form = { @@ -862,20 +865,33 @@ ${extractFunction('normalizePhoneDigits')} ${extractFunction('extractDialCodeFromText')} ${extractFunction('dispatchSignupPhoneFieldEvents')} ${extractFunction('normalizeSignupCountryLabel')} +${extractFunction('getSignupCountryLabelAliases')} ${extractFunction('getSignupPhoneOptionLabel')} ${extractFunction('normalizeSignupCountryOptionValue')} ${extractFunction('getSignupRegionDisplayName')} ${extractFunction('getSignupPhoneCountryMatchLabels')} ${extractFunction('isSameSignupCountryOption')} ${extractFunction('getSignupPhoneForm')} +${extractFunction('getSignupPhoneControlRoots')} +${extractFunction('querySignupPhoneCountryElements')} +${extractFunction('isSignupPhoneCountrySelect')} ${extractFunction('getSignupPhoneCountrySelect')} ${extractFunction('getSignupPhoneSelectedCountryOption')} ${extractFunction('getSignupPhoneCountryButtonText')} +${extractFunction('getSignupPhoneCountryButton')} ${extractFunction('getSignupPhoneDisplayedDialCode')} ${extractFunction('getSignupPhoneHiddenNumberInput')} +${extractFunction('resolveSignupPhoneDialCodeFromNumber')} +${extractFunction('resolveSignupPhoneTargetDialCode')} +${extractFunction('getSignupPhoneCountryTargetLabels')} +${extractFunction('doesSignupPhoneCountryTextMatchTarget')} +${extractFunction('isSignupPhoneCountrySelectionSynced')} ${extractFunction('findSignupPhoneCountryOptionByLabel')} ${extractFunction('findSignupPhoneCountryOptionByPhoneNumber')} ${extractFunction('trySelectSignupPhoneCountryOption')} +${extractFunction('getVisibleSignupPhoneCountryListboxOptions')} +${extractFunction('findSignupPhoneCountryListboxOption')} +${extractFunction('trySelectSignupPhoneCountryListboxOption')} ${extractFunction('ensureSignupPhoneCountrySelected')} ${extractFunction('toNationalPhoneNumber')} ${extractFunction('toE164PhoneNumber')} @@ -918,6 +934,584 @@ return { ]); }); +test('submitSignupPhoneNumberAndContinue clicks the visible country listbox when the hidden select does not update the button', async () => { + const api = new Function(` +const logs = []; +const clicks = []; +const filled = []; +const selectEvents = []; +let now = 0; +let listboxOpen = false; +let visibleCountryValue = 'AU'; + +const continueButton = { + textContent: 'Continue', + value: '', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'submit'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + getBoundingClientRect() { + return { width: 200, height: 48 }; + }, +}; + +const countryOptions = [ + { value: 'AU', textContent: 'Australia (+61)', buttonText: '澳大利亚 (+61)' }, + { value: 'GB', textContent: 'United Kingdom (+44)', buttonText: '英国 (+44)' }, +]; + +const countrySelect = { + options: countryOptions, + selectedIndex: 0, + dispatchEvent(event) { + selectEvents.push(event?.type || ''); + return true; + }, +}; + +Object.defineProperty(countrySelect, 'value', { + get() { + return countryOptions[countrySelect.selectedIndex]?.value || ''; + }, + set(nextValue) { + const nextIndex = countryOptions.findIndex((option) => option.value === String(nextValue || '')); + if (nextIndex >= 0) { + countrySelect.selectedIndex = nextIndex; + } + }, +}); + +const hiddenPhoneInput = { + kind: 'hidden-phone', + value: '', + getAttribute() { + return ''; + }, +}; + +const phoneInput = { + kind: 'phone', + value: '', + getAttribute(name) { + if (name === 'type') return 'tel'; + return ''; + }, + closest(selector) { + if (selector === 'form') { + return form; + } + return form; + }, +}; + +const selectValueNode = { + get textContent() { + return countryOptions.find((option) => option.value === visibleCountryValue)?.buttonText || ''; + }, +}; + +const countryButton = { + querySelector(selector) { + return selector === '.react-aria-SelectValue' ? selectValueNode : null; + }, + get textContent() { + return selectValueNode.textContent; + }, +}; + +const gbOption = { + textContent: '英国 (+44)', + value: '', + getAttribute() { + return ''; + }, +}; + +const form = { + querySelector(selector) { + if (selector === 'select') return countrySelect; + if (selector === 'input[name="phoneNumber"]') return hiddenPhoneInput; + if (selector === 'button[aria-haspopup="listbox"]') return countryButton; + return null; + }, +}; + +const document = { + documentElement: { + lang: 'zh-CN', + getAttribute(name) { + return name === 'lang' ? 'zh-CN' : ''; + }, + }, + querySelector(selector) { + if (selector === SIGNUP_PHONE_INPUT_SELECTOR) { + return phoneInput; + } + if (selector === 'button[type="submit"], input[type="submit"]') { + return continueButton; + } + return null; + }, + querySelectorAll(selector) { + if (selector === 'button, a, [role="button"], [role="link"]') { + return []; + } + if (selector === 'a, button, [role="button"], [role="link"]') { + return []; + } + if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') { + return [continueButton]; + } + if (selector === 'input') { + return [phoneInput]; + } + if (selector.includes('[role="option"]')) { + return listboxOpen ? [gbOption] : []; + } + return []; + }, +}; + +const location = { + href: 'https://chatgpt.com/', +}; + +const window = { + setTimeout(fn) { + fn(); + }, +}; + +const Date = { + now() { + return now; + }, +}; + +class Event { + constructor(type) { + this.type = type; + } +} + +function isVisibleElement(el) { + return Boolean(el); +} + +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 getSignupPasswordInput() { + return null; +} + +function isSignupPasswordPage() { + return false; +} + +function getSignupPasswordSubmitButton() { + return null; +} + +function findSignupEntryTrigger() { + return null; +} + +function getSignupPasswordDisplayedEmail() { + return ''; +} + +function getPageTextSnapshot() { + return countryButton.textContent; +} + +function throwIfStopped() {} +function isStopError() { return false; } + +function log(message, level = 'info') { + logs.push({ message, level }); +} + +async function humanPause() {} + +function simulateClick(target) { + clicks.push(getActionText(target)); + if (target === countryButton) { + listboxOpen = true; + } + if (target === gbOption) { + visibleCountryValue = 'GB'; + countrySelect.value = 'GB'; + listboxOpen = false; + } +} + +function fillInput(target, value) { + target.value = value; + filled.push({ target: target.kind, value }); +} + +async function sleep(ms) { + now += ms; +} + +${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')} +${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')} +${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')} +${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')} +${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')} +${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')} +${extractConst('SIGNUP_WORK_EMAIL_PATTERN')} +${extractConst('SIGNUP_PHONE_ACTION_PATTERN')} +${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')} +${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')} + +${extractFunction('getSignupEmailInput')} +${extractFunction('getSignupPhoneInput')} +${extractFunction('findSignupUseEmailTrigger')} +${extractFunction('findSignupUsePhoneTrigger')} +${extractFunction('findSignupMoreOptionsTrigger')} +${extractFunction('getSignupEmailContinueButton')} +${extractFunction('inspectSignupEntryState')} +${extractFunction('getSignupEntryStateSummary')} +function getSignupEntryDiagnostics() { return {}; } +${extractFunction('normalizePhoneDigits')} +${extractFunction('extractDialCodeFromText')} +${extractFunction('dispatchSignupPhoneFieldEvents')} +${extractFunction('normalizeSignupCountryLabel')} +${extractFunction('getSignupCountryLabelAliases')} +${extractFunction('getSignupPhoneOptionLabel')} +${extractFunction('normalizeSignupCountryOptionValue')} +${extractFunction('getSignupRegionDisplayName')} +${extractFunction('getSignupPhoneCountryMatchLabels')} +${extractFunction('isSameSignupCountryOption')} +${extractFunction('getSignupPhoneForm')} +${extractFunction('getSignupPhoneControlRoots')} +${extractFunction('querySignupPhoneCountryElements')} +${extractFunction('isSignupPhoneCountrySelect')} +${extractFunction('getSignupPhoneCountrySelect')} +${extractFunction('getSignupPhoneSelectedCountryOption')} +${extractFunction('getSignupPhoneCountryButtonText')} +${extractFunction('getSignupPhoneCountryButton')} +${extractFunction('getSignupPhoneDisplayedDialCode')} +${extractFunction('getSignupPhoneHiddenNumberInput')} +${extractFunction('resolveSignupPhoneDialCodeFromNumber')} +${extractFunction('resolveSignupPhoneTargetDialCode')} +${extractFunction('getSignupPhoneCountryTargetLabels')} +${extractFunction('doesSignupPhoneCountryTextMatchTarget')} +${extractFunction('isSignupPhoneCountrySelectionSynced')} +${extractFunction('findSignupPhoneCountryOptionByLabel')} +${extractFunction('findSignupPhoneCountryOptionByPhoneNumber')} +${extractFunction('trySelectSignupPhoneCountryOption')} +${extractFunction('getVisibleSignupPhoneCountryListboxOptions')} +${extractFunction('findSignupPhoneCountryListboxOption')} +${extractFunction('trySelectSignupPhoneCountryListboxOption')} +${extractFunction('ensureSignupPhoneCountrySelected')} +${extractFunction('toNationalPhoneNumber')} +${extractFunction('toE164PhoneNumber')} +${extractFunction('resolveSignupPhoneDialCode')} +${extractFunction('waitForSignupPhoneEntryState')} +${extractFunction('submitSignupPhoneNumberAndContinue')} + +return { + async run() { + return submitSignupPhoneNumberAndContinue({ + phoneNumber: '+447859232013', + countryLabel: 'United Kingdom', + }); + }, + getClicks() { + return clicks.slice(); + }, + getFilled() { + return filled.slice(); + }, + getSelectValue() { + return countrySelect.value; + }, + getVisibleCountryText() { + return countryButton.textContent; + }, + getSelectEvents() { + return selectEvents.slice(); + }, +}; +`)(); + + const result = await api.run(); + + assert.equal(result.submitted, true); + assert.equal(result.phoneInputValue, '7859232013'); + assert.equal(api.getSelectValue(), 'GB'); + assert.equal(api.getVisibleCountryText(), '英国 (+44)'); + assert.deepEqual(api.getSelectEvents(), ['input', 'change']); + assert.deepEqual(api.getClicks(), ['澳大利亚 (+61)', '英国 (+44)', 'Continue']); + assert.deepEqual(api.getFilled(), [ + { target: 'phone', value: '7859232013' }, + { target: 'hidden-phone', value: '+447859232013' }, + ]); +}); + +test('submitSignupPhoneNumberAndContinue can select country by phone dial code without country label or hidden select', async () => { + const api = new Function(` +const clicks = []; +const filled = []; +let now = 0; +let listboxOpen = false; +let visibleCountryText = '印度尼西亚 +(62)'; + +const continueButton = { + textContent: 'Continue', + value: '', + disabled: false, + getAttribute(name) { + if (name === 'type') return 'submit'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + getBoundingClientRect() { + return { width: 200, height: 48 }; + }, +}; + +const phoneInput = { + kind: 'phone', + value: '', + parentElement: null, + getAttribute(name) { + if (name === 'type') return 'tel'; + return ''; + }, + closest() { + return null; + }, +}; + +const countryButton = { + textContent: '', + querySelector(selector) { + return selector === '.react-aria-SelectValue' ? { textContent: visibleCountryText } : null; + }, + getAttribute(name) { + if (name === 'aria-haspopup') return 'listbox'; + return ''; + }, + getBoundingClientRect() { + return { width: 240, height: 48 }; + }, +}; + +const gbOption = { + textContent: '英国 +(44)', + getAttribute() { + return ''; + }, + getBoundingClientRect() { + return { width: 200, height: 36 }; + }, +}; + +const idOption = { + textContent: '印度尼西亚 +(62)', + getAttribute() { + return ''; + }, + getBoundingClientRect() { + return { width: 200, height: 36 }; + }, +}; + +const document = { + documentElement: { + lang: 'zh-CN', + getAttribute(name) { + return name === 'lang' ? 'zh-CN' : ''; + }, + }, + querySelector(selector) { + if (selector === SIGNUP_PHONE_INPUT_SELECTOR) return phoneInput; + if (selector === 'button[type="submit"], input[type="submit"]') return continueButton; + return null; + }, + querySelectorAll(selector) { + if (selector === 'button, a, [role="button"], [role="link"]') return []; + if (selector === 'a, button, [role="button"], [role="link"]') return []; + if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') return [continueButton]; + if (selector === 'input') return [phoneInput]; + if (selector === 'select') return []; + if (selector.includes('aria-haspopup="listbox"') || selector.includes('aria-expanded')) return [countryButton]; + if (selector.includes('[role="option"]')) return listboxOpen ? [idOption, gbOption] : []; + return []; + }, +}; + +const location = { + href: 'https://chatgpt.com/', +}; + +const window = { + setTimeout(fn) { + fn(); + }, +}; + +const Date = { + now() { + return now; + }, +}; + +class Event { + constructor(type) { + this.type = type; + } +} + +function isVisibleElement(el) { + return Boolean(el) && (!el.getBoundingClientRect || el.getBoundingClientRect().width > 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 getSignupPasswordInput() { return null; } +function isSignupPasswordPage() { return false; } +function getSignupPasswordSubmitButton() { return null; } +function findSignupEntryTrigger() { return null; } +function getSignupPasswordDisplayedEmail() { return ''; } +function getPageTextSnapshot() { return visibleCountryText; } +function throwIfStopped() {} +function isStopError() { return false; } +function log() {} +async function humanPause() {} + +function simulateClick(target) { + clicks.push(getActionText(target) || visibleCountryText); + if (target === countryButton) { + listboxOpen = true; + } + if (target === gbOption) { + visibleCountryText = '英国 +(44)'; + listboxOpen = false; + } +} + +function fillInput(target, value) { + target.value = value; + filled.push({ target: target.kind, value }); +} + +async function sleep(ms) { + now += ms; +} + +${extractConst('SIGNUP_ENTRY_TRIGGER_PATTERN')} +${extractConst('SIGNUP_EMAIL_INPUT_SELECTOR')} +${extractConst('SIGNUP_PHONE_INPUT_SELECTOR')} +${extractConst('SIGNUP_SWITCH_TO_EMAIL_PATTERN')} +${extractConst('SIGNUP_SWITCH_ACTION_PATTERN')} +${extractConst('SIGNUP_EMAIL_ACTION_PATTERN')} +${extractConst('SIGNUP_WORK_EMAIL_PATTERN')} +${extractConst('SIGNUP_PHONE_ACTION_PATTERN')} +${extractConst('SIGNUP_SWITCH_TO_PHONE_PATTERN')} +${extractConst('SIGNUP_MORE_OPTIONS_PATTERN')} + +${extractFunction('getSignupEmailInput')} +${extractFunction('getSignupPhoneInput')} +${extractFunction('findSignupUseEmailTrigger')} +${extractFunction('findSignupUsePhoneTrigger')} +${extractFunction('findSignupMoreOptionsTrigger')} +${extractFunction('getSignupEmailContinueButton')} +${extractFunction('inspectSignupEntryState')} +${extractFunction('getSignupEntryStateSummary')} +function getSignupEntryDiagnostics() { return {}; } +${extractFunction('normalizePhoneDigits')} +${extractFunction('extractDialCodeFromText')} +${extractFunction('dispatchSignupPhoneFieldEvents')} +${extractFunction('normalizeSignupCountryLabel')} +${extractFunction('getSignupCountryLabelAliases')} +${extractFunction('getSignupPhoneOptionLabel')} +${extractFunction('normalizeSignupCountryOptionValue')} +${extractFunction('getSignupRegionDisplayName')} +${extractFunction('getSignupPhoneCountryMatchLabels')} +${extractFunction('isSameSignupCountryOption')} +${extractFunction('getSignupPhoneForm')} +${extractFunction('getSignupPhoneControlRoots')} +${extractFunction('querySignupPhoneCountryElements')} +${extractFunction('isSignupPhoneCountrySelect')} +${extractFunction('getSignupPhoneCountrySelect')} +${extractFunction('getSignupPhoneSelectedCountryOption')} +${extractFunction('getSignupPhoneCountryButtonText')} +${extractFunction('getSignupPhoneCountryButton')} +${extractFunction('getSignupPhoneDisplayedDialCode')} +${extractFunction('getSignupPhoneHiddenNumberInput')} +${extractFunction('resolveSignupPhoneDialCodeFromNumber')} +${extractFunction('resolveSignupPhoneTargetDialCode')} +${extractFunction('getSignupPhoneCountryTargetLabels')} +${extractFunction('doesSignupPhoneCountryTextMatchTarget')} +${extractFunction('isSignupPhoneCountrySelectionSynced')} +${extractFunction('findSignupPhoneCountryOptionByLabel')} +${extractFunction('findSignupPhoneCountryOptionByPhoneNumber')} +${extractFunction('trySelectSignupPhoneCountryOption')} +${extractFunction('getVisibleSignupPhoneCountryListboxOptions')} +${extractFunction('findSignupPhoneCountryListboxOption')} +${extractFunction('trySelectSignupPhoneCountryListboxOption')} +${extractFunction('ensureSignupPhoneCountrySelected')} +${extractFunction('toNationalPhoneNumber')} +${extractFunction('toE164PhoneNumber')} +${extractFunction('resolveSignupPhoneDialCode')} +${extractFunction('waitForSignupPhoneEntryState')} +${extractFunction('submitSignupPhoneNumberAndContinue')} + +return { + async run() { + return submitSignupPhoneNumberAndContinue({ + phoneNumber: '447423278610', + countryLabel: '', + }); + }, + getClicks() { + return clicks.slice(); + }, + getFilled() { + return filled.slice(); + }, + getVisibleCountryText() { + return visibleCountryText; + }, +}; +`)(); + + const result = await api.run(); + + assert.equal(result.submitted, true); + assert.equal(result.phoneInputValue, '7423278610'); + assert.equal(api.getVisibleCountryText(), '英国 +(44)'); + assert.deepEqual(api.getClicks(), ['印度尼西亚 +(62)', '英国 +(44)', 'Continue']); + assert.deepEqual(api.getFilled(), [{ target: 'phone', value: '7423278610' }]); +}); + test('submitSignupPhoneNumberAndContinue switches from email mode to phone mode and submits local number', async () => { const api = new Function(` const logs = []; @@ -1110,20 +1704,33 @@ ${extractFunction('normalizePhoneDigits')} ${extractFunction('extractDialCodeFromText')} ${extractFunction('dispatchSignupPhoneFieldEvents')} ${extractFunction('normalizeSignupCountryLabel')} +${extractFunction('getSignupCountryLabelAliases')} ${extractFunction('getSignupPhoneOptionLabel')} ${extractFunction('normalizeSignupCountryOptionValue')} ${extractFunction('getSignupRegionDisplayName')} ${extractFunction('getSignupPhoneCountryMatchLabels')} ${extractFunction('isSameSignupCountryOption')} ${extractFunction('getSignupPhoneForm')} +${extractFunction('getSignupPhoneControlRoots')} +${extractFunction('querySignupPhoneCountryElements')} +${extractFunction('isSignupPhoneCountrySelect')} ${extractFunction('getSignupPhoneCountrySelect')} ${extractFunction('getSignupPhoneSelectedCountryOption')} ${extractFunction('getSignupPhoneCountryButtonText')} +${extractFunction('getSignupPhoneCountryButton')} ${extractFunction('getSignupPhoneDisplayedDialCode')} ${extractFunction('getSignupPhoneHiddenNumberInput')} +${extractFunction('resolveSignupPhoneDialCodeFromNumber')} +${extractFunction('resolveSignupPhoneTargetDialCode')} +${extractFunction('getSignupPhoneCountryTargetLabels')} +${extractFunction('doesSignupPhoneCountryTextMatchTarget')} +${extractFunction('isSignupPhoneCountrySelectionSynced')} ${extractFunction('findSignupPhoneCountryOptionByLabel')} ${extractFunction('findSignupPhoneCountryOptionByPhoneNumber')} ${extractFunction('trySelectSignupPhoneCountryOption')} +${extractFunction('getVisibleSignupPhoneCountryListboxOptions')} +${extractFunction('findSignupPhoneCountryListboxOption')} +${extractFunction('trySelectSignupPhoneCountryListboxOption')} ${extractFunction('ensureSignupPhoneCountrySelected')} ${extractFunction('toNationalPhoneNumber')} ${extractFunction('toE164PhoneNumber')} diff --git a/tests/step4-split-code-submit.test.js b/tests/step4-split-code-submit.test.js index 73bc6d8..52e022f 100644 --- a/tests/step4-split-code-submit.test.js +++ b/tests/step4-split-code-submit.test.js @@ -115,6 +115,7 @@ function fillInput(el, value) { filledValues.push(value); } async function sleep() {} +async function waitForDocumentLoadComplete() {} function isStep5Ready() { return false; } function isStep8Ready() { return false; } function isAddPhonePageReady() { return false; } @@ -177,6 +178,7 @@ function is405MethodNotAllowedPage() { return false; } async function handle405ResendError() {} function fillInput() {} async function sleep() {} +async function waitForDocumentLoadComplete() {} function isStep5Ready() { return true; } function isStep8Ready() { return false; } function isAddPhonePageReady() { return false; } @@ -336,6 +338,7 @@ function fillInput(el, value) { filledValues.push({ target: el === nameInput ? 'name' : (el === ageInput ? 'age' : 'code'), value }); } async function sleep() {} +async function waitForDocumentLoadComplete() {} function isStep5Ready() { return true; } function isStep8Ready() { return false; } function isAddPhonePageReady() { return false; } @@ -531,6 +534,7 @@ function fillInput(el, value) { el.value = value; } async function sleep() {} +async function waitForDocumentLoadComplete() {} function isStep5Ready() { return false; } function isStep8Ready() { return false; } function isAddPhonePageReady() { return false; } @@ -614,3 +618,102 @@ return { assert.equal(snapshot.submitClicked, true); assert.equal(snapshot.nameQueryCount >= 3, true); }); + +test('prepareSignupVerificationFlow waits for complete verification page before reporting ready', async () => { + const api = new Function(` +const logs = []; +let now = 0; +let sleepCalls = 0; +let targetChecks = 0; +const location = { + href: 'https://auth.openai.com/email-verification', + pathname: '/email-verification', +}; +const document = { + readyState: 'loading', + title: '', + body: { + textContent: 'Enter the verification code we just sent', + innerText: 'Enter the verification code we just sent', + }, + querySelector(selector) { + if (selector === 'form[action*="email-verification" i]') { + return {}; + } + return null; + }, + querySelectorAll(selector) { + if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') { + return []; + } + return []; + }, +}; + +Date.now = () => now; +function throwIfStopped() {} +function log(message, level = 'info') { logs.push({ message, level }); } +async function sleep(ms = 0) { + sleepCalls += 1; + now += ms || 200; + if (sleepCalls >= 3) { + document.readyState = 'complete'; + } +} +function isVisibleElement() { return true; } +function isActionEnabled() { return true; } +function getActionText(el) { return el?.textContent || ''; } +function getCurrentAuthRetryPageState() { return null; } +function isPhoneVerificationPageReady() { return false; } +function findResendVerificationCodeTrigger() { return null; } +function isEmailVerificationPage() { return true; } +function getPageTextSnapshot() { return document.body.textContent; } +function getVerificationCodeTarget() { + targetChecks += 1; + return document.readyState === 'complete' + ? { type: 'single', element: { value: '' } } + : null; +} +function is405MethodNotAllowedPage() { return false; } +async function recoverCurrentAuthRetryPage() {} +function createSignupUserAlreadyExistsError() { return new Error('user already exists'); } +function getSignupPasswordInput() { return null; } +function getSignupPasswordSubmitButton() { return null; } +function isSignupEmailAlreadyExistsPage() { return false; } +function isSignupPasswordErrorPage() { return false; } +function getSignupPasswordTimeoutErrorPageState() { return null; } +function isStep5Ready() { return false; } + +const VERIFICATION_PAGE_PATTERN = /verification code/i; + +${extractFunction('getDocumentReadyStateSnapshot')} +${extractFunction('isDocumentLoadComplete')} +${extractFunction('waitForDocumentLoadComplete')} +${extractFunction('isSignupVerificationPageInteractiveReady')} +${extractFunction('isVerificationPageStillVisible')} +${extractFunction('isSignupProfilePageUrl')} +${extractFunction('isLikelyLoggedInChatgptHomeUrl')} +${extractFunction('getStep4PostVerificationState')} +${extractFunction('inspectSignupVerificationState')} +${extractFunction('waitForVerificationCodeTarget')} +${extractFunction('waitForSignupVerificationTransition')} +${extractFunction('prepareSignupVerificationFlow')} + +return { + run() { + return prepareSignupVerificationFlow({ prepareLogLabel: '步骤 4 执行' }, 10000); + }, + snapshot() { + return { logs, sleepCalls, targetChecks, readyState: document.readyState }; + }, +}; +`)(); + + const result = await api.run(); + const snapshot = api.snapshot(); + + assert.equal(result.ready, true); + assert.equal(snapshot.readyState, 'complete'); + assert.equal(snapshot.sleepCalls >= 3, true); + assert.equal(snapshot.targetChecks >= 1, true); +}); diff --git a/项目完整链路说明.md b/项目完整链路说明.md index 563227c..604b24b 100644 --- a/项目完整链路说明.md +++ b/项目完整链路说明.md @@ -326,7 +326,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功 2. 打开或复用注册页 3. 邮箱注册时,如果注册弹窗默认停留在手机号输入模式,会先尝试点击 `继续使用电子邮件地址登录 / Continue using email address` 一类按钮切回邮箱输入模式 4. 邮箱注册分支:提交邮箱;邮箱输入框识别同时兼容本地化占位与 `aria-label` -5. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,再从接码平台申请号码,填写手机号并提交 +5. 手机号注册分支:先点击官网“免费注册”并切换到手机号注册入口,必要时展开更多选项;确认手机号输入页就绪后,再从接码平台申请号码;内容脚本会按号码区号和平台国家名切换手机号国家下拉框,必须确认可视下拉按钮的区号已同步后才填写并提交,若仍显示旧国家则停止提交 6. 以当前统一账号标识先写入一条“停止(流程尚未完成)”的记录占位;邮箱账号占位邮箱,phone-only 账号占位手机号 7. 等待身份提交后的真实落地页 8. 如果进入密码页,则继续执行 Step 3 diff --git a/项目文件结构说明.md b/项目文件结构说明.md index 36ef5f2..f1c1885 100644 --- a/项目文件结构说明.md +++ b/项目文件结构说明.md @@ -92,7 +92,7 @@ - `content/phone-auth.js`:认证页手机号验证脚本,负责识别 `add-phone / phone-verification` 页面、选择国家、填写手机号、提交短信验证码、触发重发短信,以及把“回到 add-phone / 进入 OAuth 同意页”的结果反馈给后台。 - `content/plus-checkout.js`:ChatGPT Plus checkout 页面脚本,负责读取 `/api/auth/session` 创建 Plus checkout、选择 PayPal、填写账单姓名、触发 Google 地址推荐、校验结构化地址字段并点击订阅。 - `content/qq-mail.js`:QQ 邮箱轮询脚本,负责网页邮箱验证码读取。 -- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号填写与提交;Step 7 登录链路会额外识别手机号输入页、手机号登录入口和 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。 +- `content/signup-page.js`:注册、登录、授权主内容脚本,负责 OpenAI / ChatGPT 页面上的步骤执行;其中 Step 2 / 3 的延迟点击与延迟提交在真正触发前会先检查 Stop 状态,避免停止后页面继续自动点击;当前 Step 2 同时支持邮箱注册与手机号注册入口切换、手机号国家下拉框同步校验、手机号填写与提交;Step 7 登录链路会额外识别手机号输入页、手机号登录入口和 `phone-verification` 页面,避免把手机验证码页误判成邮箱验证码页或普通未知页。 - `content/sub2api-panel.js`:SUB2API 后台内容脚本,负责获取 OAuth 地址和提交 localhost 回调;平台验证请求会读取 `visibleStep`,普通模式承接步骤 10,Plus 模式承接步骤 13。 - `content/utils.js`:内容脚本公共工具层,负责结构化日志、READY / COMPLETE / ERROR 上报、元素等待、输入与点击;内容脚本日志通过 `{ step, stepKey }` 上报,正文不再承担步骤号解析职责。 - `content/vps-panel.js`:CPA 面板内容脚本,负责获取 OAuth 地址、提交回调 URL,并基于精确成功徽标与错误态做平台验证判定;当前支持普通步骤 10 与 Plus 可见步骤 13。 @@ -238,7 +238,7 @@ - `tests/sidepanel-mail2925-mode.test.js`:测试侧边栏保留 `2925 provide / receive` 模式行与独立的号池配置行,并验证 sidepanel 只在 provide 模式下把 2925 视为别名邮箱 provider。 - `tests/sidepanel-paypal-manager.test.js`:测试侧边栏公共表单弹窗脚本与 PayPal manager 的加载顺序,以及 PayPal 账号保存后会立即选中当前账号。 - `tests/signup-entry-diagnostics.test.js`:测试注册入口诊断快照输出。 -- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式,以及本地化邮箱输入框识别。 +- `tests/signup-step2-email-switch.test.js`:测试 Step 2 在手机号输入模式下切回邮箱输入模式、本地化邮箱输入框识别,以及手机号注册时国家下拉框可视区号同步。 - `tests/signup-page-tab-cleanup.test.js`:测试注册页来源标签的冲突清理逻辑。 - `tests/step-definitions-module.test.js`:测试共享步骤定义模块及侧边栏脚本加载顺序。 - `tests/step3-direct-complete.test.js`:测试步骤 3 在提交前先完成上报,并保留延迟提交回调的可执行性验证。