feat: 增强注册诊断功能,添加密码页和可见性元数据捕获
This commit is contained in:
+220
-10
@@ -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('当前回到了密码页,但没有可用密码,无法自动重新提交。');
|
||||
}
|
||||
|
||||
+155
@@ -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` 中粘贴脚本时,请先清空旧内容,再完整粘贴新代码并保存。
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "多页面自动化",
|
||||
"name": "codex-oauth-automation-extension",
|
||||
"version": "5.8",
|
||||
"version_name": "Pro5.8",
|
||||
"description": "用于自动执行多步骤 OAuth 注册流程",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>多页面自动化面板</title>
|
||||
<title>codex-oauth-automation-extension</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
|
||||
@@ -144,3 +144,317 @@ return {
|
||||
]);
|
||||
assert.match(result.bodyTextPreview, /Welcome to ChatGPT/);
|
||||
});
|
||||
|
||||
test('signup entry diagnostics captures hidden signup button style and blocking ancestor details', () => {
|
||||
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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user