feat: 更新密码页面识别逻辑,支持手机号注册和登录路径,并添加相关测试用例
This commit is contained in:
+1
-1
@@ -6667,7 +6667,7 @@ function isSignupPasswordPageUrl(rawUrl) {
|
|||||||
const parsed = parseUrlSafely(rawUrl);
|
const parsed = parseUrlSafely(rawUrl);
|
||||||
if (!parsed) return false;
|
if (!parsed) return false;
|
||||||
return isSignupPageHost(parsed.hostname)
|
return isSignupPageHost(parsed.hostname)
|
||||||
&& /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
|
&& /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSignupEmailVerificationPageUrl(rawUrl) {
|
function isSignupEmailVerificationPageUrl(rawUrl) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
const parsed = parseUrlSafely(rawUrl);
|
const parsed = parseUrlSafely(rawUrl);
|
||||||
if (!parsed) return false;
|
if (!parsed) return false;
|
||||||
return isSignupPageHost(parsed.hostname)
|
return isSignupPageHost(parsed.hostname)
|
||||||
&& /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
|
&& /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSignupEmailVerificationPageUrl(rawUrl) {
|
function isSignupEmailVerificationPageUrl(rawUrl) {
|
||||||
|
|||||||
+96
-4
@@ -1719,11 +1719,88 @@ async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getScrollableTargets = () => {
|
||||||
|
const seen = new Set();
|
||||||
|
const targets = [];
|
||||||
|
const pushTarget = (element) => {
|
||||||
|
if (!element || seen.has(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(element);
|
||||||
|
const scrollHeight = Number(element.scrollHeight) || 0;
|
||||||
|
const clientHeight = Number(element.clientHeight) || 0;
|
||||||
|
if (scrollHeight > clientHeight + 2) {
|
||||||
|
targets.push(element);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getVisibleSignupPhoneCountryListboxOptions().forEach((option) => {
|
||||||
|
let current = option.parentElement || null;
|
||||||
|
let depth = 0;
|
||||||
|
while (current && depth < 6) {
|
||||||
|
pushTarget(current);
|
||||||
|
if (current === document.body || current === document.documentElement) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = current.parentElement || null;
|
||||||
|
depth += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Array.from(document.querySelectorAll('[role="listbox"]'))
|
||||||
|
.filter((listbox) => isVisibleElement(listbox))
|
||||||
|
.forEach(pushTarget);
|
||||||
|
|
||||||
|
return targets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatchListboxScroll = (element) => {
|
||||||
|
if (!element || typeof element.dispatchEvent !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
element.dispatchEvent(typeof Event === 'function'
|
||||||
|
? new Event('scroll', { bubbles: true })
|
||||||
|
: { type: 'scroll' });
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
element.dispatchEvent({ type: 'scroll' });
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetListboxScroll = () => {
|
||||||
|
getScrollableTargets().forEach((target) => {
|
||||||
|
if ((Number(target.scrollTop) || 0) > 0) {
|
||||||
|
target.scrollTop = 0;
|
||||||
|
dispatchListboxScroll(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollListboxDown = () => {
|
||||||
|
let scrolled = false;
|
||||||
|
getScrollableTargets().forEach((target) => {
|
||||||
|
const before = Number(target.scrollTop) || 0;
|
||||||
|
const maxScrollTop = Math.max(0, (Number(target.scrollHeight) || 0) - (Number(target.clientHeight) || 0));
|
||||||
|
if (maxScrollTop <= before + 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const step = Math.max(360, Math.floor((Number(target.clientHeight) || 0) * 0.85));
|
||||||
|
target.scrollTop = Math.min(maxScrollTop, before + step);
|
||||||
|
dispatchListboxScroll(target);
|
||||||
|
scrolled = true;
|
||||||
|
});
|
||||||
|
return scrolled;
|
||||||
|
};
|
||||||
|
|
||||||
simulateClick(button);
|
simulateClick(button);
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
|
resetListboxScroll();
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < 3000) {
|
let reachedListEndAt = 0;
|
||||||
|
while (Date.now() - start < 8000) {
|
||||||
throwIfStopped();
|
throwIfStopped();
|
||||||
const option = findSignupPhoneCountryListboxOption(targetOption, options);
|
const option = findSignupPhoneCountryListboxOption(targetOption, options);
|
||||||
if (option) {
|
if (option) {
|
||||||
@@ -1733,7 +1810,17 @@ async function trySelectSignupPhoneCountryListboxOption(phoneInput, targetOption
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!scrollListboxDown()) {
|
||||||
|
reachedListEndAt += 1;
|
||||||
|
if (reachedListEndAt >= 6) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
reachedListEndAt = 0;
|
||||||
|
await sleep(220);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -2198,7 +2285,11 @@ async function submitSignupPhoneNumberAndContinue(payload = {}) {
|
|||||||
});
|
});
|
||||||
if (countrySelection.hasCountryControl && !countrySelection.matched) {
|
if (countrySelection.hasCountryControl && !countrySelection.matched) {
|
||||||
const currentCountryText = getSignupPhoneCountryButtonText(snapshot.phoneInput) || '未知';
|
const currentCountryText = getSignupPhoneCountryButtonText(snapshot.phoneInput) || '未知';
|
||||||
throw new Error(`步骤 2:手机号国家下拉框未能自动切换到 ${countryLabel || phoneNumber},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`);
|
const targetDialCode = resolveSignupPhoneTargetDialCode({ countryLabel, phoneNumber }, countrySelection.selectedOption);
|
||||||
|
const targetLabel = targetDialCode
|
||||||
|
? `目标区号 +${targetDialCode}(号码 ${phoneNumber}${countryLabel ? `,国家 ${countryLabel}` : ''})`
|
||||||
|
: (countryLabel || phoneNumber);
|
||||||
|
throw new Error(`步骤 2:手机号国家下拉框未能自动切换到 ${targetLabel},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialCode = resolveSignupPhoneDialCode(snapshot.phoneInput, {
|
const dialCode = resolveSignupPhoneDialCode(snapshot.phoneInput, {
|
||||||
@@ -2973,7 +3064,7 @@ function getStep5ErrorText() {
|
|||||||
|
|
||||||
|
|
||||||
function isSignupPasswordPage() {
|
function isSignupPasswordPage() {
|
||||||
return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
|
return /\/(?:create-account|log-in)\/password(?:[/?#]|$)/i.test(location.pathname || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSignupPasswordInput() {
|
function getSignupPasswordInput() {
|
||||||
@@ -3564,7 +3655,8 @@ async function selectCountryForPhoneInput(phoneInput, phoneNumber = '', countryL
|
|||||||
if (selection.hasCountryControl && targetDialCode) {
|
if (selection.hasCountryControl && targetDialCode) {
|
||||||
if (!selection.matched || (displayedDialCode && displayedDialCode !== targetDialCode)) {
|
if (!selection.matched || (displayedDialCode && displayedDialCode !== targetDialCode)) {
|
||||||
const currentCountryText = getSignupPhoneCountryButtonText(phoneInput) || displayedDialCode || '未知';
|
const currentCountryText = getSignupPhoneCountryButtonText(phoneInput) || displayedDialCode || '未知';
|
||||||
throw new Error(`步骤 ${visibleStep}:手机号登录国家下拉框未能自动切换到 ${countryLabel || phoneNumber},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`);
|
const targetLabel = `目标区号 +${targetDialCode}(号码 ${phoneNumber}${countryLabel ? `,国家 ${countryLabel}` : ''})`;
|
||||||
|
throw new Error(`步骤 ${visibleStep}:手机号登录国家下拉框未能自动切换到 ${targetLabel},当前显示为 ${currentCountryText},已停止提交以避免区号不匹配。`);
|
||||||
}
|
}
|
||||||
return targetDialCode;
|
return targetDialCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,21 @@ test('navigation utils module exposes a factory', () => {
|
|||||||
assert.equal(typeof api?.createNavigationUtils, 'function');
|
assert.equal(typeof api?.createNavigationUtils, 'function');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('navigation utils recognize signup password pages for email and phone signup', () => {
|
||||||
|
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||||
|
const globalScope = {};
|
||||||
|
const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
|
||||||
|
const utils = api.createNavigationUtils({
|
||||||
|
DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts',
|
||||||
|
DEFAULT_SUB2API_URL: 'https://sub.example.com/admin/accounts',
|
||||||
|
normalizeLocalCpaStep9Mode: (value) => value,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/create-account/password'), true);
|
||||||
|
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/log-in/password'), true);
|
||||||
|
assert.equal(utils.isSignupPasswordPageUrl('https://auth.openai.com/log-in'), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('navigation utils treat 126 mail hosts as part of the shared NetEase mail family', () => {
|
test('navigation utils treat 126 mail hosts as part of the shared NetEase mail family', () => {
|
||||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||||
const globalScope = {};
|
const globalScope = {};
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const signupFlowSource = fs.readFileSync('background/signup-flow-helpers.js', 'u
|
|||||||
const signupFlowGlobalScope = {};
|
const signupFlowGlobalScope = {};
|
||||||
const signupFlowApi = new Function('self', `${signupFlowSource}; return self.MultiPageSignupFlowHelpers;`)(signupFlowGlobalScope);
|
const signupFlowApi = new Function('self', `${signupFlowSource}; return self.MultiPageSignupFlowHelpers;`)(signupFlowGlobalScope);
|
||||||
|
|
||||||
|
const navigationSource = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||||
|
const navigationGlobalScope = {};
|
||||||
|
const navigationApi = new Function('self', `${navigationSource}; return self.MultiPageBackgroundNavigationUtils;`)(navigationGlobalScope);
|
||||||
|
|
||||||
test('step 2 completes with password step skipped when landing on email verification page', async () => {
|
test('step 2 completes with password step skipped when landing on email verification page', async () => {
|
||||||
const completedPayloads = [];
|
const completedPayloads = [];
|
||||||
|
|
||||||
@@ -473,6 +477,69 @@ test('signup flow helper recognizes email verification page as post-email landin
|
|||||||
assert.equal(passwordReadyChecks, 0);
|
assert.equal(passwordReadyChecks, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('signup flow helper accepts phone signup landing on login password page', async () => {
|
||||||
|
let ensureCalls = 0;
|
||||||
|
let passwordReadyChecks = 0;
|
||||||
|
let predicateAcceptedLoginPassword = false;
|
||||||
|
const navigationUtils = navigationApi.createNavigationUtils({
|
||||||
|
DEFAULT_CODEX2API_URL: 'http://localhost:8080/admin/accounts',
|
||||||
|
DEFAULT_SUB2API_URL: 'https://sub.example.com/admin/accounts',
|
||||||
|
normalizeLocalCpaStep9Mode: (value) => value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const helpers = signupFlowApi.createSignupFlowHelpers({
|
||||||
|
buildGeneratedAliasEmail: () => '',
|
||||||
|
chrome: {
|
||||||
|
tabs: {
|
||||||
|
get: async () => ({
|
||||||
|
id: 22,
|
||||||
|
url: 'https://auth.openai.com/log-in/password',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensureContentScriptReadyOnTab: async () => {
|
||||||
|
ensureCalls += 1;
|
||||||
|
},
|
||||||
|
ensureHotmailAccountForFlow: async () => ({}),
|
||||||
|
ensureLuckmailPurchaseForFlow: async () => ({}),
|
||||||
|
isGeneratedAliasProvider: () => false,
|
||||||
|
isHotmailProvider: () => false,
|
||||||
|
isLuckmailProvider: () => false,
|
||||||
|
isSignupEmailVerificationPageUrl: navigationUtils.isSignupEmailVerificationPageUrl,
|
||||||
|
isSignupPasswordPageUrl: (url) => {
|
||||||
|
const accepted = navigationUtils.isSignupPasswordPageUrl(url);
|
||||||
|
if (accepted && /\/log-in\/password(?:[/?#]|$)/i.test(url || '')) {
|
||||||
|
predicateAcceptedLoginPassword = true;
|
||||||
|
}
|
||||||
|
return accepted;
|
||||||
|
},
|
||||||
|
reuseOrCreateTab: async () => 22,
|
||||||
|
sendToContentScriptResilient: async (_source, message) => {
|
||||||
|
assert.equal(message.type, 'ENSURE_SIGNUP_PASSWORD_PAGE_READY');
|
||||||
|
passwordReadyChecks += 1;
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
setEmailState: async () => {},
|
||||||
|
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
|
||||||
|
SIGNUP_PAGE_INJECT_FILES: [],
|
||||||
|
waitForTabUrlMatch: async (_tabId, predicate) => {
|
||||||
|
const url = 'https://auth.openai.com/log-in/password';
|
||||||
|
return predicate(url) ? { id: 22, url } : null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await helpers.ensureSignupPostIdentityPageReadyInTab(22, 2);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, {
|
||||||
|
ready: true,
|
||||||
|
state: 'password_page',
|
||||||
|
url: 'https://auth.openai.com/log-in/password',
|
||||||
|
});
|
||||||
|
assert.equal(predicateAcceptedLoginPassword, true);
|
||||||
|
assert.equal(ensureCalls, 1);
|
||||||
|
assert.equal(passwordReadyChecks, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('signup flow helper reuses existing managed alias email when it is still compatible', async () => {
|
test('signup flow helper reuses existing managed alias email when it is still compatible', async () => {
|
||||||
let buildCalls = 0;
|
let buildCalls = 0;
|
||||||
let setEmailCalls = 0;
|
let setEmailCalls = 0;
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ function extractFunction(name) {
|
|||||||
return source.slice(start, end);
|
return source.slice(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('signup password detector accepts create-account and phone login password paths', () => {
|
||||||
|
const run = (pathname) => new Function('location', `
|
||||||
|
${extractFunction('isSignupPasswordPage')}
|
||||||
|
return isSignupPasswordPage();
|
||||||
|
`)({ pathname });
|
||||||
|
|
||||||
|
assert.equal(run('/create-account/password'), true);
|
||||||
|
assert.equal(run('/log-in/password'), true);
|
||||||
|
assert.equal(run('/log-in'), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('signup entry diagnostics summarizes current page inputs and visible actions', () => {
|
test('signup entry diagnostics summarizes current page inputs and visible actions', () => {
|
||||||
const api = new Function(`
|
const api = new Function(`
|
||||||
const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\\s*up|register|create\\s*account|create\\s+account/i;
|
const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\\s*up|register|create\\s*account|create\\s+account/i;
|
||||||
|
|||||||
Reference in New Issue
Block a user