feat: 增强手机号登录功能,添加手机号输入框确认逻辑及相关测试用例

This commit is contained in:
QLHazyCoder
2026-05-04 22:49:28 +08:00
parent efa77757aa
commit 6b3917d4b7
4 changed files with 136 additions and 4 deletions
+68 -2
View File
@@ -1822,6 +1822,68 @@ function toE164PhoneNumber(value, dialCode) {
return `+${normalizedDialCode}${digits}`;
}
function getPhoneInputRenderedValue(phoneInput) {
return String(phoneInput?.value ?? phoneInput?.getAttribute?.('value') ?? '').trim();
}
function isPhoneInputValueComplete(phoneInput, phoneNumber, dialCode, expectedLocalNumber = '') {
const renderedDigits = normalizePhoneDigits(getPhoneInputRenderedValue(phoneInput));
const targetDigits = normalizePhoneDigits(phoneNumber);
const localDigits = normalizePhoneDigits(expectedLocalNumber || toNationalPhoneNumber(phoneNumber, dialCode));
const normalizedDialCode = normalizePhoneDigits(dialCode);
if (!renderedDigits) {
return false;
}
if (normalizedDialCode && renderedDigits === normalizedDialCode) {
return false;
}
if (localDigits && renderedDigits === localDigits) {
return true;
}
if (localDigits && renderedDigits.endsWith(localDigits) && renderedDigits.length > localDigits.length) {
return true;
}
return Boolean(targetDigits && renderedDigits === targetDigits);
}
function getLoginPhoneFillCandidates(phoneNumber, dialCode) {
const candidates = [
toNationalPhoneNumber(phoneNumber, dialCode),
toE164PhoneNumber(phoneNumber, dialCode),
normalizePhoneDigits(phoneNumber),
];
return candidates.filter((value, index, list) => value && list.indexOf(value) === index);
}
async function fillLoginPhoneInputAndConfirm(phoneInput, options = {}) {
const {
phoneNumber = '',
dialCode = '',
visibleStep = 7,
} = options;
const localNumber = toNationalPhoneNumber(phoneNumber, dialCode);
if (!localNumber) {
throw new Error(`步骤 ${visibleStep}:手机号为空,无法填写。`);
}
let lastRenderedValue = '';
for (const candidate of getLoginPhoneFillCandidates(phoneNumber, dialCode)) {
fillInput(phoneInput, candidate);
await sleep(350);
lastRenderedValue = getPhoneInputRenderedValue(phoneInput);
if (isPhoneInputValueComplete(phoneInput, phoneNumber, dialCode, localNumber)) {
return {
inputValue: localNumber,
attemptedValue: candidate,
renderedValue: lastRenderedValue,
};
}
}
const displayedValue = lastRenderedValue || '空';
throw new Error(`步骤 ${visibleStep}:手机号填写后页面显示为 ${displayedValue},未包含本地号码 ${localNumber},已停止提交以避免空号提交。`);
}
function resolveSignupPhoneDialCode(phoneInput, options = {}) {
const { phoneNumber = '', countryLabel = '' } = options;
const displayedDialCode = getSignupPhoneDisplayedDialCode(phoneInput);
@@ -4588,8 +4650,12 @@ async function step6LoginFromPhonePage(payload, snapshot) {
log(`步骤 ${visibleStep}:正在填写手机号 ${phoneNumber}...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await humanPause(500, 1400);
fillInput(phoneInput, inputValue);
log(`步骤 ${visibleStep}:手机号已填写${dialCode ? `(区号 +${dialCode}` : ''}`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
const fillResult = await fillLoginPhoneInputAndConfirm(phoneInput, {
phoneNumber,
dialCode,
visibleStep,
});
log(`步骤 ${visibleStep}:手机号已填写${dialCode ? `(区号 +${dialCode},本地号 ${fillResult.inputValue}` : ''}`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
await sleep(500);
const phoneSubmittedAt = Date.now();
+66
View File
@@ -311,3 +311,69 @@ return {
assert.equal(api.getVisibleCountryText(), '\u82f1\u56fd +(44)');
assert.deepEqual(api.getClicks(), ['\u6fb3\u5927\u5229\u4e9a (+61)', '\u82f1\u56fd +(44)']);
});
function createPhoneFillApi(fillBehavior) {
return new Function('fillBehavior', `
const fills = [];
const phoneInput = {
value: '+44',
getAttribute(name) {
return name === 'value' ? this.value : '';
},
};
function fillInput(input, value) {
fills.push(value);
fillBehavior(input, value);
}
async function sleep() {}
${extractFunction('normalizePhoneDigits')}
${extractFunction('toNationalPhoneNumber')}
${extractFunction('toE164PhoneNumber')}
${extractFunction('getPhoneInputRenderedValue')}
${extractFunction('isPhoneInputValueComplete')}
${extractFunction('getLoginPhoneFillCandidates')}
${extractFunction('fillLoginPhoneInputAndConfirm')}
return {
run() {
return fillLoginPhoneInputAndConfirm(phoneInput, {
phoneNumber: '447780579093',
dialCode: '44',
visibleStep: 7,
});
},
getFills() {
return fills.slice();
},
getValue() {
return phoneInput.value;
},
};
`)(fillBehavior);
}
test('step 7 retries phone fill with e164 when the auth input keeps only the dial code', async () => {
const api = createPhoneFillApi((input, value) => {
input.value = value === '7780579093' ? '+44' : value;
});
const result = await api.run();
assert.equal(result.inputValue, '7780579093');
assert.equal(result.attemptedValue, '+447780579093');
assert.equal(api.getValue(), '+447780579093');
assert.deepEqual(api.getFills(), ['7780579093', '+447780579093']);
});
test('step 7 stops before submit when phone fill never includes the local number', async () => {
const api = createPhoneFillApi((input) => {
input.value = '+44';
});
await assert.rejects(api.run, /7780579093/);
assert.equal(api.getValue(), '+44');
assert.deepEqual(api.getFills(), ['7780579093', '+447780579093', '447780579093']);
});
+1 -1
View File
@@ -436,7 +436,7 @@ IP 代理模块在同步、切换、Change、出口探测和自动运行成功
2. 打开最新 OAuth 链接
3. 根据统一账号标识选择登录身份:
- 邮箱账号:沿用现有邮箱登录链路
- 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交
- 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,填写后还会读回输入框确认已包含本地号码,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交或只提交区号
4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
5. 确保真正进入验证码页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点
6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询
+1 -1
View File
@@ -248,7 +248,7 @@
- `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。
- `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路,并覆盖 `phone-verification` 页面不会被误判成邮箱验证码页。
- `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。
- `tests/step7-phone-login-entry.test.js`:测试 OAuth 手机号登录入口识别、真实 add-phone 与手机号登录页区分、慢切换等待窗口,以及手机号登录页可视国家下拉框会按本轮号码区号自动切换。
- `tests/step7-phone-login-entry.test.js`:测试 OAuth 手机号登录入口识别、真实 add-phone 与手机号登录页区分、慢切换等待窗口手机号登录页可视国家下拉框会按本轮号码区号自动切换,以及输入框只剩区号时不会继续提交
- `tests/step8-callback-handling.test.js`:测试 localhost 回调地址捕获逻辑,当前对应步骤 9。
- `tests/step8-debugger-stop.test.js`:测试调试器点击在 Stop 场景下的中止行为,当前对应步骤 9。
- `tests/step8-retry-page-recovery.test.js`:测试 OAuth 同意页点击后的重试页恢复分支,以及手机号验证完成后会继续回到 OAuth 同意页等待,当前对应步骤 9。