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