feat: 增强手机号登录功能,添加手机号输入框确认逻辑及相关测试用例
This commit is contained in:
+68
-2
@@ -1822,6 +1822,68 @@ function toE164PhoneNumber(value, dialCode) {
|
|||||||
return `+${normalizedDialCode}${digits}`;
|
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 = {}) {
|
function resolveSignupPhoneDialCode(phoneInput, options = {}) {
|
||||||
const { phoneNumber = '', countryLabel = '' } = options;
|
const { phoneNumber = '', countryLabel = '' } = options;
|
||||||
const displayedDialCode = getSignupPhoneDisplayedDialCode(phoneInput);
|
const displayedDialCode = getSignupPhoneDisplayedDialCode(phoneInput);
|
||||||
@@ -4588,8 +4650,12 @@ async function step6LoginFromPhonePage(payload, snapshot) {
|
|||||||
|
|
||||||
log(`步骤 ${visibleStep}:正在填写手机号 ${phoneNumber}...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
|
log(`步骤 ${visibleStep}:正在填写手机号 ${phoneNumber}...`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
|
||||||
await humanPause(500, 1400);
|
await humanPause(500, 1400);
|
||||||
fillInput(phoneInput, inputValue);
|
const fillResult = await fillLoginPhoneInputAndConfirm(phoneInput, {
|
||||||
log(`步骤 ${visibleStep}:手机号已填写${dialCode ? `(区号 +${dialCode})` : ''}。`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
|
phoneNumber,
|
||||||
|
dialCode,
|
||||||
|
visibleStep,
|
||||||
|
});
|
||||||
|
log(`步骤 ${visibleStep}:手机号已填写${dialCode ? `(区号 +${dialCode},本地号 ${fillResult.inputValue})` : ''}。`, 'info', { step: visibleStep, stepKey: 'oauth-login' });
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const phoneSubmittedAt = Date.now();
|
const phoneSubmittedAt = Date.now();
|
||||||
|
|||||||
@@ -311,3 +311,69 @@ return {
|
|||||||
assert.equal(api.getVisibleCountryText(), '\u82f1\u56fd +(44)');
|
assert.equal(api.getVisibleCountryText(), '\u82f1\u56fd +(44)');
|
||||||
assert.deepEqual(api.getClicks(), ['\u6fb3\u5927\u5229\u4e9a (+61)', '\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 链接
|
2. 打开最新 OAuth 链接
|
||||||
3. 根据统一账号标识选择登录身份:
|
3. 根据统一账号标识选择登录身份:
|
||||||
- 邮箱账号:沿用现有邮箱登录链路
|
- 邮箱账号:沿用现有邮箱登录链路
|
||||||
- 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交
|
- 手机号账号:优先探测登录页是否存在手机号登录入口,必要时展开更多选项或从邮箱页切到手机号页;进入手机号输入页后会复用 Step 2 的可视国家下拉框同步逻辑,按本轮手机号最长区号切到目标国家,填写后还会读回输入框确认已包含本地号码,避免英国 `+44` 号码仍按默认澳大利亚 `+61` 提交或只提交区号
|
||||||
4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
|
4. 登录;如果进入密码页且当前有密码,则填写并提交密码;如果当前没有密码但检测到一次性验证码入口,则直接切换到一次性验证码登录
|
||||||
5. 确保真正进入验证码页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点
|
5. 确保真正进入验证码页;手机号登录允许以 `phone-verification` 作为 Step 7 的成功落点
|
||||||
6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询
|
6. 如果页面没有可用的手机号登录入口,则 Step 7 直接抛出明确业务错误,不再误进入 Step 8 邮箱轮询
|
||||||
|
|||||||
+1
-1
@@ -248,7 +248,7 @@
|
|||||||
- `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。
|
- `tests/step5-direct-complete.test.js`:测试步骤 5 在资料页点击提交后立即完成当前步骤。
|
||||||
- `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路,并覆盖 `phone-verification` 页面不会被误判成邮箱验证码页。
|
- `tests/step6-login-state.test.js`:测试 OAuth 登录状态识别逻辑,当前对应步骤 7 链路,并覆盖 `phone-verification` 页面不会被误判成邮箱验证码页。
|
||||||
- `tests/step6-timeout-recovery.test.js`:测试 OAuth 登录超时报错页的可恢复结果构造,当前对应步骤 7 链路。
|
- `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-callback-handling.test.js`:测试 localhost 回调地址捕获逻辑,当前对应步骤 9。
|
||||||
- `tests/step8-debugger-stop.test.js`:测试调试器点击在 Stop 场景下的中止行为,当前对应步骤 9。
|
- `tests/step8-debugger-stop.test.js`:测试调试器点击在 Stop 场景下的中止行为,当前对应步骤 9。
|
||||||
- `tests/step8-retry-page-recovery.test.js`:测试 OAuth 同意页点击后的重试页恢复分支,以及手机号验证完成后会继续回到 OAuth 同意页等待,当前对应步骤 9。
|
- `tests/step8-retry-page-recovery.test.js`:测试 OAuth 同意页点击后的重试页恢复分支,以及手机号验证完成后会继续回到 OAuth 同意页等待,当前对应步骤 9。
|
||||||
|
|||||||
Reference in New Issue
Block a user