feat:重试页面检测和重试点击的逻辑抽取为公共逻辑

This commit is contained in:
QLHazyCoder
2026-04-17 15:16:52 +08:00
parent 1a8cbc143b
commit b387ebfa91
15 changed files with 865 additions and 35 deletions
+96
View File
@@ -0,0 +1,96 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { createAuthPageRecovery } = require('../content/auth-page-recovery.js');
function createRetryButton() {
return {
disabled: false,
textContent: 'Try again',
getAttribute(name) {
if (name === 'data-dd-action-name') return 'Try again';
if (name === 'aria-disabled') return 'false';
return '';
},
};
}
function createRecoveryApi(state) {
const retryButton = createRetryButton();
global.location = {
pathname: '/log-in',
href: 'https://auth.openai.com/log-in',
};
global.document = {
title: 'Something went wrong',
querySelector(selector) {
if (selector === 'button[data-dd-action-name="Try again"]' && state.retryVisible) {
return retryButton;
}
return null;
},
querySelectorAll() {
return state.retryVisible ? [retryButton] : [];
},
};
return createAuthPageRecovery({
detailPattern: /timed out/i,
getActionText: (element) => element?.textContent || '',
getPageTextSnapshot: () => state.pageText,
humanPause: async () => {},
isActionEnabled: (element) => Boolean(element) && !element.disabled && element.getAttribute('aria-disabled') !== 'true',
isVisibleElement: () => true,
log: () => {},
simulateClick: () => {
state.clickCount += 1;
state.retryVisible = false;
state.pageText = 'Recovered login form';
},
sleep: async () => {},
throwIfStopped: () => {},
titlePattern: /something went wrong/i,
});
}
test('auth page recovery detects retry page state', () => {
const state = {
clickCount: 0,
pageText: 'Something went wrong. Operation timed out.',
retryVisible: true,
};
const api = createRecoveryApi(state);
const snapshot = api.getAuthTimeoutErrorPageState({
pathPatterns: [/\/log-in(?:[/?#]|$)/i],
});
assert.equal(Boolean(snapshot), true);
assert.equal(snapshot.retryEnabled, true);
assert.equal(snapshot.titleMatched, true);
assert.equal(snapshot.detailMatched, true);
});
test('auth page recovery clicks retry and waits until page recovers', async () => {
const state = {
clickCount: 0,
pageText: 'Something went wrong. Operation timed out.',
retryVisible: true,
};
const api = createRecoveryApi(state);
const result = await api.recoverAuthRetryPage({
logLabel: '步骤 8:检测到重试页,正在点击“重试”恢复',
pathPatterns: [/\/log-in(?:[/?#]|$)/i],
step: 8,
timeoutMs: 1000,
});
assert.deepStrictEqual(result, {
recovered: true,
clickCount: 1,
url: 'https://auth.openai.com/log-in',
});
assert.equal(state.clickCount, 1);
assert.equal(state.retryVisible, false);
});
@@ -11,6 +11,9 @@ function createRouter(overrides = {}) {
logs: [],
stepStatuses: [],
emailStates: [],
finalizePayloads: [],
notifyCompletions: [],
notifyErrors: [],
};
const router = api.createMessageRouter({
@@ -41,6 +44,9 @@ function createRouter(overrides = {}) {
executeStepViaCompletionSignal: async () => {},
exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '',
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
events.finalizePayloads.push(payload);
}),
finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
findHotmailAccount: async () => null,
flushCommand: async () => {},
@@ -63,8 +69,12 @@ function createRouter(overrides = {}) {
normalizeHotmailAccounts: (items) => items,
normalizeRunCount: (value) => value,
AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
notifyStepComplete: () => {},
notifyStepError: () => {},
notifyStepComplete: (step, payload) => {
events.notifyCompletions.push({ step, payload });
},
notifyStepError: (step, error) => {
events.notifyErrors.push({ step, error });
},
patchHotmailAccount: async () => {},
registerTab: async () => {},
requestStop: async () => {},
@@ -125,3 +135,63 @@ test('message router does not overwrite a completed step 3 when step 2 is replay
assert.deepStrictEqual(events.stepStatuses, []);
});
test('message router finalizes step 3 before marking it completed', async () => {
const { router, events } = createRouter();
const response = await router.handleMessage({
type: 'STEP_COMPLETE',
step: 3,
source: 'signup-page',
payload: {
email: 'user@example.com',
signupVerificationRequestedAt: 123,
},
}, {});
assert.deepStrictEqual(events.finalizePayloads, [
{
email: 'user@example.com',
signupVerificationRequestedAt: 123,
},
]);
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'completed' }]);
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.notifyCompletions, [
{
step: 3,
payload: {
email: 'user@example.com',
signupVerificationRequestedAt: 123,
},
},
]);
assert.deepStrictEqual(response, { ok: true });
});
test('message router marks step 3 failed when post-submit finalize fails', async () => {
const { router, events } = createRouter({
finalizeStep3Completion: async () => {
throw new Error('步骤 3 提交后仍停留在密码页。');
},
});
const response = await router.handleMessage({
type: 'STEP_COMPLETE',
step: 3,
source: 'signup-page',
payload: {
email: 'user@example.com',
},
}, {});
assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'failed' }]);
assert.deepStrictEqual(events.notifyErrors, [
{
step: 3,
error: '步骤 3 提交后仍停留在密码页。',
},
]);
assert.equal(events.logs.some(({ message }) => /步骤 3 失败步骤 3 提交后仍停留在密码页/.test(message)), true);
assert.deepStrictEqual(response, { ok: true, error: '步骤 3 提交后仍停留在密码页。' });
});
@@ -171,3 +171,45 @@ test('signup flow helper reuses existing managed alias email when it is still co
assert.equal(buildCalls, 0);
assert.equal(setEmailCalls, 0);
});
test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => {
let ensureCalls = 0;
const messages = [];
const helpers = signupFlowApi.createSignupFlowHelpers({
buildGeneratedAliasEmail: () => '',
chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async (...args) => {
ensureCalls += 1;
messages.push({ type: 'ensure', args });
},
ensureHotmailAccountForFlow: async () => ({}),
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => false,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
reuseOrCreateTab: async () => 31,
sendToContentScriptResilient: async (_source, message) => {
messages.push({ type: 'send', message });
return { ready: true, retried: 1 };
},
setEmailState: async () => {},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: ['content/utils.js', 'content/signup-page.js'],
waitForTabUrlMatch: async () => null,
});
const result = await helpers.finalizeSignupPasswordSubmitInTab(31, 'Secret123!', 3);
assert.deepStrictEqual(result, { ready: true, retried: 1 });
assert.equal(ensureCalls, 1);
assert.deepStrictEqual(messages.find((item) => item.type === 'send')?.message, {
type: 'PREPARE_SIGNUP_VERIFICATION',
step: 3,
source: 'background',
payload: { password: 'Secret123!' },
});
});
+105
View File
@@ -0,0 +1,105 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('content/signup-page.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let index = start; index < source.length; index += 1) {
const char = source[index];
if (char === '(') {
parenDepth += 1;
} else if (char === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (char === '{' && signatureEnded) {
braceStart = index;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const char = source[end];
if (char === '{') depth += 1;
if (char === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('step 6 timeout recoverable result clicks retry before asking background to rerun', async () => {
const api = new Function(`
const logs = [];
let recoverCalls = 0;
const location = {
href: 'https://auth.openai.com/log-in',
};
function inspectLoginAuthState() {
return {
state: 'login_timeout_error_page',
url: location.href,
};
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
return { recovered: true };
}
function log(message, level = 'info') {
logs.push({ message, level });
}
${extractFunction('createStep6RecoverableResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
return {
async run() {
return createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
{ state: 'login_timeout_error_page', url: location.href },
'当前页面处于登录超时报错页。'
);
},
snapshot() {
return { logs, recoverCalls };
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.equal(snapshot.recoverCalls, 1);
assert.equal(result.step6Outcome, 'recoverable');
assert.equal(result.reason, 'login_timeout_error_page');
assert.equal(result.state, 'login_timeout_error_page');
assert.equal(result.message, '当前页面处于登录超时报错页。');
});
+108
View File
@@ -0,0 +1,108 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let index = start; index < source.length; index += 1) {
const char = source[index];
if (char === '(') {
parenDepth += 1;
} else if (char === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (char === '{' && signatureEnded) {
braceStart = index;
break;
}
}
if (braceStart < 0) {
throw new Error(`missing body for function ${name}`);
}
let depth = 0;
let end = braceStart;
for (; end < source.length; end += 1) {
const char = source[end];
if (char === '{') depth += 1;
if (char === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('step 8 click effect returns restart_current_step when retry page is recovered', async () => {
const api = new Function(`
let recoverCalls = 0;
const chrome = {
tabs: {
async get() {
return {
id: 88,
url: 'https://auth.openai.com/authorize',
};
},
},
};
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
async function recoverAuthRetryPageOnTab() {
recoverCalls += 1;
return { recovered: true };
}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
retryPage: true,
addPhonePage: false,
consentPage: false,
verificationPage: false,
};
}
${extractFunction('waitForStep8ClickEffect')}
return {
async run() {
return waitForStep8ClickEffect(88, 'https://auth.openai.com/authorize', 1000);
},
snapshot() {
return { recoverCalls };
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
progressed: false,
reason: 'retry_page_recovered',
restartCurrentStep: true,
url: 'https://auth.openai.com/authorize',
});
assert.equal(snapshot.recoverCalls, 1);
});