feat: 增强步骤 5 的错误处理,添加生日选择功能并优化提交结果检测
This commit is contained in:
+158
-8
@@ -356,6 +356,7 @@ const INVALID_VERIFICATION_CODE_PATTERN = /代码不正确|验证码不正确|
|
||||
const VERIFICATION_PAGE_PATTERN = /检查您的收件箱|输入我们刚刚向|重新发送电子邮件|重新发送验证码|验证码|代码不正确|email\s+verification/i;
|
||||
const OAUTH_CONSENT_PAGE_PATTERN = /使用\s*ChatGPT\s*登录到\s*Codex|login\s+to\s+codex|log\s+in\s+to\s+codex|authorize|授权/i;
|
||||
const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手机号|phone\s+number|telephone/i;
|
||||
const STEP5_SUBMIT_ERROR_PATTERN = /无法根据该信息创建帐户|请重试|unable\s+to\s+create\s+(?:your\s+)?account|couldn'?t\s+create\s+(?:your\s+)?account|something\s+went\s+wrong|invalid\s+(?:birthday|birth|date)|生日|出生日期/i;
|
||||
const SIGNUP_PASSWORD_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+wrong|oops/i;
|
||||
const SIGNUP_PASSWORD_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
|
||||
|
||||
@@ -448,6 +449,118 @@ function isStep8Ready() {
|
||||
return OAUTH_CONSENT_PAGE_PATTERN.test(getPageTextSnapshot());
|
||||
}
|
||||
|
||||
function normalizeInlineText(text) {
|
||||
return (text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function findBirthdayReactAriaSelect(labelText) {
|
||||
const normalizedLabel = normalizeInlineText(labelText);
|
||||
const roots = document.querySelectorAll('.react-aria-Select');
|
||||
|
||||
for (const root of roots) {
|
||||
const labelEl = Array.from(root.querySelectorAll('span')).find((el) => normalizeInlineText(el.textContent) === normalizedLabel);
|
||||
if (!labelEl) continue;
|
||||
|
||||
const item = root.closest('[class*="selectItem"], ._selectItem_ppsls_113') || root.parentElement;
|
||||
const nativeSelect = item?.querySelector('[data-testid="hidden-select-container"] select') || null;
|
||||
const button = root.querySelector('button[aria-haspopup="listbox"]') || null;
|
||||
const valueEl = root.querySelector('.react-aria-SelectValue') || null;
|
||||
|
||||
return { root, item, labelEl, nativeSelect, button, valueEl };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function setReactAriaBirthdaySelect(control, value) {
|
||||
if (!control?.nativeSelect) {
|
||||
throw new Error('未找到可写入的生日下拉框。');
|
||||
}
|
||||
|
||||
const desiredValue = String(value);
|
||||
const option = Array.from(control.nativeSelect.options).find((item) => item.value === desiredValue);
|
||||
if (!option) {
|
||||
throw new Error(`生日下拉框中不存在值 ${desiredValue}。`);
|
||||
}
|
||||
|
||||
control.nativeSelect.value = desiredValue;
|
||||
option.selected = true;
|
||||
control.nativeSelect.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
control.nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
await sleep(120);
|
||||
}
|
||||
|
||||
function getStep5ErrorText() {
|
||||
const messages = [];
|
||||
const selectors = [
|
||||
'.react-aria-FieldError',
|
||||
'[slot="errorMessage"]',
|
||||
'[id$="-error"]',
|
||||
'[id$="-errors"]',
|
||||
'[role="alert"]',
|
||||
'[aria-live="assertive"]',
|
||||
'[aria-live="polite"]',
|
||||
'[class*="error"]',
|
||||
];
|
||||
|
||||
for (const selector of selectors) {
|
||||
document.querySelectorAll(selector).forEach((el) => {
|
||||
if (!isVisibleElement(el)) return;
|
||||
const text = normalizeInlineText(el.textContent);
|
||||
if (text) {
|
||||
messages.push(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const invalidField = Array.from(document.querySelectorAll('[aria-invalid="true"], [data-invalid="true"]'))
|
||||
.find((el) => isVisibleElement(el));
|
||||
if (invalidField) {
|
||||
const wrapper = invalidField.closest('form, fieldset, [data-rac], div');
|
||||
if (wrapper) {
|
||||
const text = normalizeInlineText(wrapper.textContent);
|
||||
if (text) {
|
||||
messages.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages.find((text) => STEP5_SUBMIT_ERROR_PATTERN.test(text)) || '';
|
||||
}
|
||||
|
||||
async function waitForStep5SubmitOutcome(timeout = 15000) {
|
||||
const start = Date.now();
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
|
||||
const errorText = getStep5ErrorText();
|
||||
if (errorText) {
|
||||
return { invalidProfile: true, errorText };
|
||||
}
|
||||
|
||||
if (isAddPhonePageReady()) {
|
||||
return { success: true, addPhonePage: true };
|
||||
}
|
||||
|
||||
if (isStep8Ready()) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
await sleep(150);
|
||||
}
|
||||
|
||||
const errorText = getStep5ErrorText();
|
||||
if (errorText) {
|
||||
return { invalidProfile: true, errorText };
|
||||
}
|
||||
|
||||
return {
|
||||
invalidProfile: true,
|
||||
errorText: '提交后未进入下一阶段,请检查生日是否真正被页面接受。',
|
||||
};
|
||||
}
|
||||
|
||||
function isSignupPasswordPage() {
|
||||
return /\/create-account\/password(?:[/?#]|$)/i.test(location.pathname || '');
|
||||
}
|
||||
@@ -864,6 +977,36 @@ async function step5_fillNameBirthday(payload) {
|
||||
const yearSpinner = document.querySelector('[role="spinbutton"][data-type="year"]');
|
||||
const monthSpinner = document.querySelector('[role="spinbutton"][data-type="month"]');
|
||||
const daySpinner = document.querySelector('[role="spinbutton"][data-type="day"]');
|
||||
const yearReactSelect = findBirthdayReactAriaSelect('年');
|
||||
const monthReactSelect = findBirthdayReactAriaSelect('月');
|
||||
const dayReactSelect = findBirthdayReactAriaSelect('天');
|
||||
|
||||
if (yearReactSelect?.nativeSelect && monthReactSelect?.nativeSelect && dayReactSelect?.nativeSelect) {
|
||||
const desiredDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const hiddenBirthday = document.querySelector('input[name="birthday"]');
|
||||
|
||||
log('步骤 5:检测到 React Aria 下拉生日字段,正在填写生日...');
|
||||
await humanPause(450, 1100);
|
||||
await setReactAriaBirthdaySelect(yearReactSelect, year);
|
||||
await humanPause(250, 650);
|
||||
await setReactAriaBirthdaySelect(monthReactSelect, month);
|
||||
await humanPause(250, 650);
|
||||
await setReactAriaBirthdaySelect(dayReactSelect, day);
|
||||
|
||||
if (hiddenBirthday) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 2000) {
|
||||
if ((hiddenBirthday.value || '') === desiredDate) break;
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
if ((hiddenBirthday.value || '') !== desiredDate) {
|
||||
throw new Error(`生日值未成功写入页面。期望 ${desiredDate},实际 ${(hiddenBirthday.value || '空')}。`);
|
||||
}
|
||||
}
|
||||
|
||||
log(`步骤 5:React Aria 生日已填写:${desiredDate}`);
|
||||
}
|
||||
|
||||
if (yearSpinner && monthSpinner && daySpinner) {
|
||||
log('步骤 5:检测到生日字段,正在填写生日...');
|
||||
@@ -901,6 +1044,7 @@ async function step5_fillNameBirthday(payload) {
|
||||
if (hiddenBirthday) {
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
hiddenBirthday.value = dateStr;
|
||||
hiddenBirthday.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
hiddenBirthday.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
log(`步骤 5:已设置隐藏生日输入框:${dateStr}`);
|
||||
}
|
||||
@@ -919,13 +1063,19 @@ async function step5_fillNameBirthday(payload) {
|
||||
await sleep(500);
|
||||
const completeBtn = document.querySelector('button[type="submit"]')
|
||||
|| await waitForElementByText('button', /完成|create|continue|finish|done|agree/i, 5000).catch(() => null);
|
||||
|
||||
// Report complete BEFORE submit (page navigates to add-phone after this)
|
||||
reportComplete(5);
|
||||
|
||||
if (completeBtn) {
|
||||
await humanPause(500, 1300);
|
||||
simulateClick(completeBtn);
|
||||
log('步骤 5:已点击“完成帐户创建”');
|
||||
if (!completeBtn) {
|
||||
throw new Error('未找到“完成帐户创建”按钮。URL: ' + location.href);
|
||||
}
|
||||
|
||||
await humanPause(500, 1300);
|
||||
simulateClick(completeBtn);
|
||||
log('步骤 5:已点击“完成帐户创建”,正在等待页面结果...');
|
||||
|
||||
const outcome = await waitForStep5SubmitOutcome();
|
||||
if (outcome.invalidProfile) {
|
||||
throw new Error(`步骤 5:${outcome.errorText}`);
|
||||
}
|
||||
|
||||
log(`步骤 5:资料已通过。`, 'ok');
|
||||
reportComplete(5, { addPhonePage: Boolean(outcome.addPhonePage) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user