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('当前回到了密码页,但没有可用密码,无法自动重新提交。');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user