feat: 增强手机号登录功能,添加手机号输入框确认逻辑及相关测试用例
This commit is contained in:
+68
-2
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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。
|
||||
|
||||
Reference in New Issue
Block a user