From e732dd2a202eb0aa787b16b9e85d895729f8fbe4 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Wed, 22 Apr 2026 13:21:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E9=A1=B5=E5=92=8C=E5=8F=AF=E8=A7=81=E6=80=A7?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE=E6=8D=95=E8=8E=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/signup-page.js | 230 +++++++++++++++++- docs/使用教程.md | 155 ++++++++++++ manifest.json | 2 +- sidepanel/sidepanel.html | 2 +- tests/signup-entry-diagnostics.test.js | 314 +++++++++++++++++++++++++ 5 files changed, 691 insertions(+), 12 deletions(-) create mode 100644 docs/使用教程.md diff --git a/content/signup-page.js b/content/signup-page.js index 6f68fba..53881b7 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -385,13 +385,82 @@ function getSignupEntryStateSummary(snapshot = inspectSignupEntryState()) { } function getSignupEntryDiagnostics() { + const view = typeof window !== 'undefined' ? window : globalThis; + const safeGetComputedStyle = (el) => { + if (!el || typeof view?.getComputedStyle !== 'function') { + return null; + } + try { + return view.getComputedStyle(el); + } catch { + return null; + } + }; + const buildRectSummary = (el) => { + const rect = typeof el?.getBoundingClientRect === 'function' + ? el.getBoundingClientRect() + : null; + return rect + ? { + width: Math.round(rect.width || 0), + height: Math.round(rect.height || 0), + } + : null; + }; + const buildVisibilityMeta = (el) => { + const style = safeGetComputedStyle(el); + return { + className: String(el?.className || '').slice(0, 200), + hidden: Boolean(el?.hidden), + ariaHidden: el?.getAttribute?.('aria-hidden') || '', + inert: typeof el?.hasAttribute === 'function' ? el.hasAttribute('inert') : false, + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + }; + }; + const findBlockingAncestor = (el) => { + let current = el?.parentElement || null; + while (current) { + const style = safeGetComputedStyle(current); + const rect = buildRectSummary(current); + const hidden = Boolean(current.hidden); + const ariaHidden = current.getAttribute?.('aria-hidden') || ''; + const inert = typeof current.hasAttribute === 'function' ? current.hasAttribute('inert') : false; + const blockedByStyle = Boolean( + style + && ( + style.display === 'none' + || style.visibility === 'hidden' + || style.opacity === '0' + || style.pointerEvents === 'none' + ) + ); + const blockedByRect = Boolean(rect && (rect.width === 0 || rect.height === 0)); + if (hidden || ariaHidden === 'true' || inert || blockedByStyle || blockedByRect) { + return { + tag: (current.tagName || '').toLowerCase(), + id: current.id || '', + className: String(current.className || '').slice(0, 200), + hidden, + ariaHidden, + inert, + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + rect, + }; + } + current = current.parentElement; + } + return null; + }; const actionCandidates = document.querySelectorAll( 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]' ); const allActions = Array.from(actionCandidates).map((el) => { - const rect = typeof el?.getBoundingClientRect === 'function' - ? el.getBoundingClientRect() - : null; const text = getActionText(el); return { tag: (el.tagName || '').toLowerCase(), @@ -399,12 +468,7 @@ function getSignupEntryDiagnostics() { text: text.slice(0, 80), visible: isVisibleElement(el), enabled: isActionEnabled(el), - rect: rect - ? { - width: Math.round(rect.width || 0), - height: Math.round(rect.height || 0), - } - : null, + rect: buildRectSummary(el), }; }); const visibleActions = Array.from(actionCandidates) @@ -417,7 +481,20 @@ function getSignupEntryDiagnostics() { enabled: isActionEnabled(el), })) .filter((item) => item.text); - const signupLikeActions = allActions + const signupLikeActions = Array.from(actionCandidates) + .map((el) => { + const text = getActionText(el); + return { + tag: (el.tagName || '').toLowerCase(), + type: el.getAttribute?.('type') || '', + text: text.slice(0, 80), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + rect: buildRectSummary(el), + ...buildVisibilityMeta(el), + blockingAncestor: findBlockingAncestor(el), + }; + }) .filter((item) => item.text && SIGNUP_ENTRY_TRIGGER_PATTERN.test(item.text)) .slice(0, 12); @@ -425,15 +502,133 @@ function getSignupEntryDiagnostics() { url: location.href, title: document.title || '', readyState: document.readyState || '', + viewport: { + innerWidth: Math.round(Number(view?.innerWidth) || 0), + innerHeight: Math.round(Number(view?.innerHeight) || 0), + outerWidth: Math.round(Number(view?.outerWidth) || 0), + outerHeight: Math.round(Number(view?.outerHeight) || 0), + devicePixelRatio: Number(view?.devicePixelRatio) || 0, + }, hasEmailInput: Boolean(getSignupEmailInput()), hasPasswordInput: Boolean(getSignupPasswordInput()), bodyContainsSignupText: SIGNUP_ENTRY_TRIGGER_PATTERN.test(getPageTextSnapshot()), + signupLikeActionCounts: { + total: signupLikeActions.length, + visible: signupLikeActions.filter((item) => item.visible).length, + hidden: signupLikeActions.filter((item) => !item.visible).length, + }, signupLikeActions, visibleActions, bodyTextPreview: getPageTextSnapshot().slice(0, 240), }; } +function getSignupPasswordDiagnostics() { + const view = typeof window !== 'undefined' ? window : globalThis; + const safeGetComputedStyle = (el) => { + if (!el || typeof view?.getComputedStyle !== 'function') { + return null; + } + try { + return view.getComputedStyle(el); + } catch { + return null; + } + }; + const buildRectSummary = (el) => { + const rect = typeof el?.getBoundingClientRect === 'function' + ? el.getBoundingClientRect() + : null; + return rect + ? { + width: Math.round(rect.width || 0), + height: Math.round(rect.height || 0), + } + : null; + }; + const buildInputSummary = (el) => { + const style = safeGetComputedStyle(el); + return { + tag: (el?.tagName || '').toLowerCase(), + type: el?.getAttribute?.('type') || el?.type || '', + name: el?.getAttribute?.('name') || el?.name || '', + id: el?.id || '', + autocomplete: el?.getAttribute?.('autocomplete') || '', + placeholder: String(el?.getAttribute?.('placeholder') || '').slice(0, 80), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + valueLength: String(el?.value || '').length, + rect: buildRectSummary(el), + className: String(el?.className || '').slice(0, 200), + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + formAction: el?.form?.action || '', + }; + }; + const buildActionSummary = (el) => { + const style = safeGetComputedStyle(el); + return { + tag: (el?.tagName || '').toLowerCase(), + type: el?.getAttribute?.('type') || el?.type || '', + role: el?.getAttribute?.('role') || '', + text: getActionText(el).slice(0, 120), + visible: isVisibleElement(el), + enabled: isActionEnabled(el), + rect: buildRectSummary(el), + className: String(el?.className || '').slice(0, 200), + display: style?.display || '', + visibility: style?.visibility || '', + opacity: style?.opacity || '', + pointerEvents: style?.pointerEvents || '', + dataDdActionName: el?.getAttribute?.('data-dd-action-name') || '', + formAction: el?.form?.action || '', + }; + }; + const passwordInputs = Array.from(document.querySelectorAll( + 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]' + )) + .map(buildInputSummary) + .slice(0, 8); + const actionCandidates = Array.from(document.querySelectorAll( + 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + )) + .map(buildActionSummary) + .filter((item) => item.text) + .slice(0, 16); + const visibleActions = actionCandidates.filter((item) => item.visible).slice(0, 12); + const submitButton = getSignupPasswordSubmitButton({ allowDisabled: true }); + const oneTimeCodeTrigger = findOneTimeCodeLoginTrigger(); + const retryState = getSignupPasswordTimeoutErrorPageState(); + + return { + url: location.href, + title: document.title || '', + readyState: document.readyState || '', + displayedEmail: getSignupPasswordDisplayedEmail(), + hasVisiblePasswordInput: Boolean(getSignupPasswordInput()), + passwordInputCount: passwordInputs.length, + visiblePasswordInputCount: passwordInputs.filter((item) => item.visible).length, + passwordInputs, + submitButton: submitButton ? buildActionSummary(submitButton) : null, + oneTimeCodeTrigger: oneTimeCodeTrigger ? buildActionSummary(oneTimeCodeTrigger) : null, + retryPage: Boolean(retryState), + retryEnabled: Boolean(retryState?.retryEnabled), + userAlreadyExistsBlocked: Boolean(retryState?.userAlreadyExistsBlocked), + visibleActions, + bodyTextPreview: getPageTextSnapshot().slice(0, 240), + }; +} + +function logSignupPasswordDiagnostics(context, level = 'warn') { + try { + log(`${context}:密码页诊断快照:${JSON.stringify(getSignupPasswordDiagnostics())}`, level); + } catch (error) { + console.warn('[MultiPage:signup-page] failed to build signup password diagnostics:', error?.message || error); + } +} + async function waitForSignupEntryState(options = {}) { const { timeout = 15000, @@ -620,6 +815,10 @@ async function step3_fillEmailPassword(payload) { snapshot = inspectSignupEntryState(); } + if (snapshot.state !== 'password_page' || !snapshot.passwordInput) { + logSignupPasswordDiagnostics('步骤 3:未能识别可填写的密码输入框'); + } + if (snapshot.state !== 'password_page' || !snapshot.passwordInput) { throw new Error('在密码页未找到密码输入框。URL: ' + location.href); } @@ -635,6 +834,12 @@ async function step3_fillEmailPassword(payload) { || getSignupPasswordSubmitButton({ allowDisabled: true }) || await waitForElementByText('button', /continue|sign\s*up|submit|注册|创建|create/i, 5000).catch(() => null); + if (!submitBtn) { + logSignupPasswordDiagnostics('步骤 3:未找到可提交的密码页按钮'); + } else if (typeof findOneTimeCodeLoginTrigger === 'function' && findOneTimeCodeLoginTrigger()) { + logSignupPasswordDiagnostics('步骤 3:当前密码页同时存在一次性验证码入口', 'info'); + } + // Report complete BEFORE submit, because submit causes page navigation // which kills the content script connection const signupVerificationRequestedAt = submitBtn ? Date.now() : null; @@ -1640,6 +1845,7 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { const start = Date.now(); let recoveryRound = 0; const maxRecoveryRounds = 3; + let passwordPageDiagnosticsLogged = false; while (Date.now() - start < timeout && recoveryRound < maxRecoveryRounds) { throwIfStopped(); @@ -1678,6 +1884,10 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) { } if (snapshot.state === 'password') { + if (!passwordPageDiagnosticsLogged) { + passwordPageDiagnosticsLogged = true; + logSignupPasswordDiagnostics(`${prepareLogLabel}:页面仍停留在密码页`); + } if (!password) { throw new Error('当前回到了密码页,但没有可用密码,无法自动重新提交。'); } diff --git a/docs/使用教程.md b/docs/使用教程.md new file mode 100644 index 0000000..90eda7b --- /dev/null +++ b/docs/使用教程.md @@ -0,0 +1,155 @@ +# Codex 注册扩展更新与 Clash Verge 非港轮询配置教程 + +本教程用于说明如何更新 `Codex 注册扩展`,以及如何在 `Clash Verge` 中配置 `🔁 非港轮询`。 + +## 适用场景 + +- 已经安装本扩展,想更新到最新版本 +- 想用更方便的方式长期同步扩展更新 +- 需要在 `Clash Verge` 中启用 `🔁 非港轮询` + +## 准备内容 + +- 已安装好的扩展文件夹 +- 可以打开浏览器的 `扩展程序管理` 页面 +- 如需使用 `git pull`,本地已安装 `git` +- 如需使用 `GitHub Desktop`,本地已安装 `GitHub Desktop` +- 已安装 `Clash Verge`,并已导入可用订阅 + +## 操作步骤 + +### 第一部分:更新扩展 + +1. 使用 `GitHub Desktop` 更新 + 先安装 `GitHub Desktop`。 + 把当前扩展仓库交给 `GitHub Desktop` 管理后,之后就可以随时更新。 + `GitHub Desktop` 的具体使用方法本教程不展开,需要时可直接询问豆包。 + +2. 使用 `git pull` 更新(最方便,最推荐) + 先在本地安装 `git`。 + 打开终端后执行 `cd 扩展文件夹路径` 进入当前扩展目录。 + 接着执行 `git pull` 拉取最新更新。 + 这是最方便、最推荐的更新方式。 + +3. 手动下载最新版本覆盖更新 + 点击左上角的版本号或 `更新` 按钮。 + 下载最新版本到本地后,直接覆盖现有扩展文件夹即可。 + +4. 更新完成后重新加载扩展 + 不论使用哪种更新方式,更新完成后都必须打开浏览器的 `扩展程序管理` 页面。 + 找到该扩展后,手动点击一次 `重新加载`。 + 这一步一定要做,否则浏览器可能仍在使用旧版本。 + +### 第二部分:配置 `Clash Verge` 的 `🔁 非港轮询` + +#### 第一步:添加扩展脚本 + +1. 打开 `Clash Verge`,进入左侧的 `订阅`(`Profiles`)界面。 +2. 在右上角或对应位置找到并双击打开 `全局扩展脚本`(`Merge/Script`)。 +3. 将里面的内容全部清空,替换为下方格式化好的代码。 +4. 点击保存,使用右上角保存按钮或按 `Ctrl+S`。 + +💻 格式化后的代码(如果遇到格式错误,则让豆包修复一下) + +```javascript +function uniqPrepend(arr, items) { + if (!Array.isArray(arr)) arr = []; + for (var i = items.length - 1; i >= 0; i--) { + var item = items[i]; + var exists = false; + for (var j = 0; j < arr.length; j++) { + if (arr[j] === item) { + exists = true; + break; + } + } + if (!exists) arr.unshift(item); + } + return arr; +} + +function upsertGroup(groups, group) { + for (var i = 0; i < groups.length; i++) { + if (groups[i] && groups[i].name === group.name) { + groups[i] = group; + return groups; + } + } + groups.unshift(group); + return groups; +} + +function main(config, profileName) { + if (!config) return config; + + if (!Array.isArray(config["proxy-groups"])) { + config["proxy-groups"] = []; + } + + var groups = config["proxy-groups"]; + var LB_NAME = "🔁 非港轮询"; + + var excludeRegex = + "(?i)(" + + "香港|hong[ -]?kong|\\bhk\\b|\\bhkg\\b|🇭🇰" + + "|剩余流量|套餐到期|下次重置剩余|重置剩余|到期时间|流量重置" + + "|traffic|expire|expiration|subscription|subscribe|reset|plan" + + ")"; + + groups = upsertGroup(groups, { + name: LB_NAME, + type: "load-balance", + strategy: "round-robin", + "include-all-proxies": true, + "exclude-filter": excludeRegex, + url: "https://www.gstatic.com/generate_204", + interval: 300, + lazy: true, + "expected-status": 204 + }); + + var injected = false; + var entryNameRegex = /节点选择|代理|Proxy|PROXY|默认|GLOBAL|全局|选择/i; + + for (var i = 0; i < groups.length; i++) { + var g = groups[i]; + if (!g || g.type !== "select") continue; + + if (entryNameRegex.test(g.name || "")) { + if (!Array.isArray(g.proxies)) g.proxies = []; + g.proxies = uniqPrepend(g.proxies, [LB_NAME]); + injected = true; + } + } + + if (!injected) { + for (var k = 0; k < groups.length; k++) { + var g2 = groups[k]; + if (g2 && g2.type === "select") { + if (!Array.isArray(g2.proxies)) g2.proxies = []; + g2.proxies = uniqPrepend(g2.proxies, [LB_NAME]); + break; + } + } + } + + config["proxy-groups"] = groups; + return config; +} +``` + +#### 第二步:应用设置 + +1. 切换到左侧的 `代理`(`Proxies`)界面,也就是首页。 +2. 在顶部的分类中,通常会看到 `节点选择`、`Proxy` 或 `当前节点` 之类的分组。 +3. 在对应下拉框中选择 `🔁 非港轮询`。 +4. 确认右上角的 `代理模式`(`Mode`)已经设置为 `规则模式`(`Rule`)。 +5. 确认左侧 `设置` 中的 `系统代理`(`System Proxy`)已经开启。 + + +## 注意事项 + +- 不论使用哪种方式更新扩展,更新完成后都必须在浏览器的 `扩展程序管理` 页面重新加载一次该扩展。 +- 如果你长期更新扩展,优先使用 `git pull`,这是最方便、最推荐的方式。 +- 使用手动覆盖更新时,请直接覆盖当前扩展文件夹,避免同时保留多份不同版本的目录。 +- 在 `Clash Verge` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。 diff --git a/manifest.json b/manifest.json index c58c1fa..ced3774 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "manifest_version": 3, - "name": "多页面自动化", + "name": "codex-oauth-automation-extension", "version": "5.8", "version_name": "Pro5.8", "description": "用于自动执行多步骤 OAuth 注册流程", diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 6b75d35..6b99430 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -4,7 +4,7 @@ - 多页面自动化面板 + codex-oauth-automation-extension { +const api = new Function(` +const SIGNUP_ENTRY_TRIGGER_PATTERN = /鍏嶈垂娉ㄥ唽|绔嬪嵆娉ㄥ唽|娉ㄥ唽|sign\\s*up|register|create\\s*account|create\\s+account/i; +const location = { href: 'https://chatgpt.com/' }; +const hiddenSection = { + tagName: 'DIV', + id: 'mobile-cta', + className: 'max-xs:hidden', + hidden: false, + parentElement: null, + hasAttribute() { + return false; + }, + getAttribute(name) { + if (name === 'aria-hidden') return ''; + return ''; + }, + getBoundingClientRect() { + return { width: 0, height: 0 }; + }, + _style: { + display: 'none', + visibility: 'visible', + opacity: '1', + pointerEvents: 'auto', + }, +}; +const hiddenSignupButton = { + tagName: 'BUTTON', + textContent: 'Sign up for free', + disabled: false, + className: 'signup-button', + hidden: false, + parentElement: hiddenSection, + hasAttribute() { + return false; + }, + getBoundingClientRect() { + return { width: 0, height: 0 }; + }, + getAttribute(name) { + if (name === 'type') return ''; + if (name === 'aria-hidden') return ''; + return ''; + }, + _style: { + display: 'block', + visibility: 'visible', + opacity: '1', + pointerEvents: 'auto', + }, +}; +const document = { + title: 'ChatGPT', + readyState: 'complete', + querySelector() { + return null; + }, + querySelectorAll(selector) { + if (selector === 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]') { + return [hiddenSignupButton]; + } + return []; + }, +}; +const window = { + innerWidth: 390, + innerHeight: 844, + outerWidth: 390, + outerHeight: 844, + devicePixelRatio: 3, + getComputedStyle(el) { + return el?._style || { + display: 'block', + visibility: 'visible', + opacity: '1', + pointerEvents: 'auto', + }; + }, +}; + +function isVisibleElement(el) { + const style = window.getComputedStyle(el); + const rect = el.getBoundingClientRect(); + return style.display !== 'none' + && style.visibility !== 'hidden' + && rect.width > 0 + && rect.height > 0; +} + +function getActionText(el) { + return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')] + .filter(Boolean) + .join(' ') + .replace(/\\s+/g, ' ') + .trim(); +} + +function isActionEnabled(el) { + return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true'; +} + +function getSignupEmailInput() { + return null; +} + +function getSignupPasswordInput() { + return null; +} + +function getPageTextSnapshot() { + return 'ChatGPT 登录'; +} + +${extractFunction('getSignupEntryDiagnostics')} + +return { + run() { + return getSignupEntryDiagnostics(); + }, +}; +`)(); + + const result = api.run(); + + assert.deepStrictEqual(result.viewport, { + innerWidth: 390, + innerHeight: 844, + outerWidth: 390, + outerHeight: 844, + devicePixelRatio: 3, + }); + assert.deepStrictEqual(result.signupLikeActionCounts, { + total: 1, + visible: 0, + hidden: 1, + }); + assert.equal(result.signupLikeActions[0]?.text, 'Sign up for free'); + assert.equal(result.signupLikeActions[0]?.className, 'signup-button'); + assert.equal(result.signupLikeActions[0]?.display, 'block'); + assert.equal(result.signupLikeActions[0]?.blockingAncestor?.className, 'max-xs:hidden'); + assert.equal(result.signupLikeActions[0]?.blockingAncestor?.display, 'none'); +}); + +test('signup password diagnostics summarizes password inputs, submit button, and alternate code entry', () => { +const api = new Function(` +const location = { href: 'https://auth.openai.com/create-account/password' }; +const form = { action: 'https://auth.openai.com/u/signup/password' }; +const passwordInput = { + tagName: 'INPUT', + type: 'password', + name: 'new-password', + id: 'password-field', + value: 'SecretLength14', + className: 'password-input', + disabled: false, + form, + getBoundingClientRect() { + return { width: 320, height: 44 }; + }, + getAttribute(name) { + if (name === 'type') return 'password'; + if (name === 'name') return 'new-password'; + if (name === 'autocomplete') return 'new-password'; + if (name === 'placeholder') return 'Password'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + _style: { + display: 'block', + visibility: 'visible', + opacity: '1', + pointerEvents: 'auto', + }, +}; +const submitButton = { + tagName: 'BUTTON', + textContent: 'Continue', + className: 'submit-btn', + disabled: false, + form, + getBoundingClientRect() { + return { width: 160, height: 40 }; + }, + getAttribute(name) { + if (name === 'type') return 'submit'; + if (name === 'aria-disabled') return 'false'; + if (name === 'data-dd-action-name') return 'Continue'; + return ''; + }, + _style: { + display: 'block', + visibility: 'visible', + opacity: '1', + pointerEvents: 'auto', + }, +}; +const oneTimeCodeButton = { + tagName: 'BUTTON', + textContent: 'Use a one-time code instead', + className: 'switch-btn', + disabled: false, + getBoundingClientRect() { + return { width: 220, height: 36 }; + }, + getAttribute(name) { + if (name === 'type') return 'button'; + if (name === 'aria-disabled') return 'false'; + return ''; + }, + _style: { + display: 'block', + visibility: 'visible', + opacity: '1', + pointerEvents: 'auto', + }, +}; +const document = { + title: 'Create your account', + readyState: 'complete', + querySelectorAll(selector) { + if (selector === 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]') { + return [passwordInput]; + } + if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') { + return [submitButton, oneTimeCodeButton]; + } + return []; + }, +}; +const window = { + getComputedStyle(el) { + return el?._style || { + display: 'block', + visibility: 'visible', + opacity: '1', + pointerEvents: 'auto', + }; + }, +}; + +function isVisibleElement(el) { + const style = window.getComputedStyle(el); + const rect = el.getBoundingClientRect(); + return style.display !== 'none' + && style.visibility !== 'hidden' + && 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 'Create your account Use a one-time code instead'; +} + +function getSignupPasswordInput() { + return passwordInput; +} + +function getSignupPasswordSubmitButton() { + return submitButton; +} + +function getSignupPasswordDisplayedEmail() { + return 'user@example.com'; +} + +function findOneTimeCodeLoginTrigger() { + return oneTimeCodeButton; +} + +function getSignupPasswordTimeoutErrorPageState() { + return { + retryEnabled: true, + userAlreadyExistsBlocked: false, + }; +} + +${extractFunction('getSignupPasswordDiagnostics')} + +return { + run() { + return getSignupPasswordDiagnostics(); + }, +}; +`)(); + + const result = api.run(); + + assert.equal(result.url, 'https://auth.openai.com/create-account/password'); + assert.equal(result.displayedEmail, 'user@example.com'); + assert.equal(result.hasVisiblePasswordInput, true); + assert.equal(result.passwordInputCount, 1); + assert.equal(result.visiblePasswordInputCount, 1); + assert.equal(result.passwordInputs[0]?.name, 'new-password'); + assert.equal(result.passwordInputs[0]?.valueLength, 14); + assert.equal(result.submitButton?.text, 'Continue'); + assert.equal(result.oneTimeCodeTrigger?.text, 'Use a one-time code instead'); + assert.equal(result.retryPage, true); + assert.equal(result.retryEnabled, true); + assert.match(result.bodyTextPreview, /one-time code/); +});