fix: stabilize signup verification recovery

- merge the latest dev branch into PR #130 before applying local fixes
- detect visible split OTP inputs even when the page exposes numeric single-character fields
- stop assuming verification success after repeated auth retry-page recovery failures
This commit is contained in:
QLHazyCoder
2026-04-21 21:48:25 +08:00
30 changed files with 2043 additions and 427 deletions
@@ -143,3 +143,32 @@ return { getMailConfig };
navigateOnReuse: true,
});
});
test('getMailConfig keeps provider metadata for 2925 mailboxes', () => {
const bundle = extractFunction('getMailConfig');
const api = new Function(`
const ICLOUD_PROVIDER = 'icloud';
const GMAIL_PROVIDER = 'gmail';
const HOTMAIL_PROVIDER = 'hotmail-api';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
function normalizeIcloudHost(value = '') { return String(value || '').trim().toLowerCase(); }
function normalizeInbucketOrigin(value) { return String(value || '').trim(); }
function getConfiguredIcloudHostPreference() { return ''; }
function getIcloudLoginUrlForHost(host) { return host; }
function getIcloudMailUrlForHost(host) { return host; }
${bundle}
return { getMailConfig };
`)();
assert.deepEqual(api.getMailConfig({
mailProvider: '2925',
}), {
provider: '2925',
source: 'mail-2925',
url: 'https://2925.com/#/mailList',
label: '2925 邮箱',
inject: ['content/utils.js', 'content/mail-2925.js'],
injectSource: 'mail-2925',
});
});
@@ -0,0 +1,268 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const mail2925Utils = require('../mail2925-utils.js');
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
test('ensureMail2925MailboxSession reuses current mailbox page without sending login when page stays on mailList', async () => {
let currentState = {
autoRunning: false,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
let sendCalls = 0;
let readyCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/#/mailList' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {
readyCalls += 1;
},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return { loggedIn: true };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
actionLabel: '步骤 8:确认 2925 邮箱登录态',
});
assert.equal(sendCalls, 0);
assert.equal(readyCalls, 0);
assert.equal(result.result.usedExistingSession, true);
});
test('ensureMail2925MailboxSession does not require account-pool accounts when pool is off and mailbox page stays on mailList', async () => {
let currentState = {
autoRunning: false,
mail2925UseAccountPool: false,
mail2925Accounts: [],
currentMail2925AccountId: null,
};
let sendCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/#/mailList' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return { loggedIn: true };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const result = await manager.ensureMail2925MailboxSession({
accountId: null,
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(sendCalls, 0);
assert.equal(result.result.usedExistingSession, true);
assert.equal(result.account, null);
});
test('ensureMail2925MailboxSession stops immediately when login page is detected and account pool is off', async () => {
let currentState = {
autoRunning: true,
autoRunPhase: 'running',
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
const stopCalls = [];
let sendCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/login/' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: (state) => Boolean(state?.autoRunning) && state?.autoRunPhase === 'running',
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
requestStop: async (options = {}) => {
stopCalls.push(options);
},
reuseOrCreateTab: async () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return { loggedIn: true };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await assert.rejects(
() => manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
}),
/流程已被用户停止。/
);
assert.equal(sendCalls, 0);
assert.equal(stopCalls.length, 1);
});
test('ensureMail2925MailboxSession logs in when login page is detected and account pool is on', async () => {
let currentState = {
autoRunning: false,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
let sendCalls = 0;
let readyCalls = 0;
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: 'https://2925.com/login/' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {
readyCalls += 1;
},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async () => 9,
sendToMailContentScriptResilient: async () => {
sendCalls += 1;
return { loggedIn: true, currentView: 'mailbox' };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(sendCalls, 1);
assert.equal(readyCalls, 1);
assert.equal(result.result.loggedIn, true);
});
@@ -17,6 +17,7 @@ test('handleMail2925LimitReachedError disables current account and switches to t
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925UseAccountPool: true,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'current', email: 'current@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
{ id: 'next', email: 'next@2925.com', password: 'p2', enabled: true, lastUsedAt: 20 },
@@ -99,6 +100,7 @@ test('handleMail2925LimitReachedError requests stop when no next mail2925 accoun
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925UseAccountPool: true,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'only', email: 'only@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
@@ -223,5 +225,72 @@ test('ensureMail2925MailboxSession requests stop when auto run is active and log
);
assert.equal(events.stopCalls.length, 1);
assert.match(events.stopCalls[0].logMessage, /20 秒内未进入收件箱/);
assert.match(events.stopCalls[0].logMessage, /登录后仍未进入收件箱/);
});
test('handleMail2925LimitReachedError stops immediately when account pool is off even if another account exists', async () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925UseAccountPool: false,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'current', email: 'current@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
{ id: 'next', email: 'next@2925.com', password: 'p2', enabled: true, lastUsedAt: 20 },
]),
currentMail2925AccountId: 'current',
};
const events = {
stopCalls: [],
sessionChecks: 0,
};
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
requestStop: async (options = {}) => {
events.stopCalls.push(options);
},
reuseOrCreateTab: async () => 1,
sendToMailContentScriptResilient: async () => {
events.sessionChecks += 1;
return { loggedIn: true };
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
const error = await manager.handleMail2925LimitReachedError(
4,
new Error('MAIL2925_LIMIT_REACHED::子邮箱已达上限邮箱')
);
assert.equal(error.message, '流程已被用户停止。');
assert.equal(events.sessionChecks, 0);
assert.equal(events.stopCalls.length, 1);
assert.equal(currentState.currentMail2925AccountId, 'current');
});
+18 -4
View File
@@ -8,6 +8,9 @@ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep
test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling', async () => {
let capturedOptions = null;
let ensureCalls = 0;
const tabUpdates = [];
const tabReuses = [];
const realDateNow = Date.now;
Date.now = () => 700000;
@@ -15,12 +18,16 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
update: async (tabId, payload) => {
tabUpdates.push({ tabId, payload });
},
},
},
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
ensureMail2925MailboxSession: async () => {
ensureCalls += 1;
},
getMailConfig: () => ({
provider: '2925',
label: '2925 邮箱',
@@ -35,7 +42,9 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
resolveVerificationStep: async (_step, _state, _mail, options) => {
capturedOptions = options;
},
reuseOrCreateTab: async () => {},
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
sendToContentScriptResilient: async () => ({}),
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
@@ -46,12 +55,17 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
await executor.executeStep4({
email: 'user@example.com',
password: 'secret',
mail2925UseAccountPool: false,
mail2925UseAccountPool: true,
});
} finally {
Date.now = realDateNow;
}
assert.equal(ensureCalls, 1);
assert.deepStrictEqual(tabReuses, []);
assert.deepStrictEqual(tabUpdates, [
{ tabId: 1, payload: { active: true } },
]);
assert.equal(capturedOptions.filterAfterTimestamp, 100000);
assert.equal(capturedOptions.resendIntervalMs, 0);
});
+19 -2
View File
@@ -91,6 +91,9 @@ test('step 8 submits login verification directly without replaying step 7', asyn
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
let capturedOptions = null;
let ensureCalls = 0;
const tabUpdates = [];
const tabReuses = [];
const realDateNow = Date.now;
Date.now = () => 900000;
@@ -98,11 +101,16 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
update: async (tabId, payload) => {
tabUpdates.push({ tabId, payload });
},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {
ensureCalls += 1;
},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
@@ -123,7 +131,9 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
resolveVerificationStep: async (_step, _state, _mail, options) => {
capturedOptions = options;
},
reuseOrCreateTab: async () => {},
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
@@ -137,11 +147,18 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
mail2925UseAccountPool: true,
});
} finally {
Date.now = realDateNow;
}
assert.equal(ensureCalls, 0);
assert.deepStrictEqual(tabReuses, []);
assert.deepStrictEqual(tabUpdates, [
{ tabId: 1, payload: { active: true } },
{ tabId: 2, payload: { active: true } },
]);
assert.equal(capturedOptions.filterAfterTimestamp, 300000);
assert.equal(capturedOptions.resendIntervalMs, 0);
assert.equal(capturedOptions.targetEmail, '');
@@ -11,6 +11,8 @@ test('sidepanel html keeps a single contribution mode button in header', () => {
assert.equal(matches.length, 1);
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开上传页"/);
assert.match(html, />贡献\/使用<\/button>/);
assert.match(html, /<\/header>\s*<div id="contribution-update-layer"/);
assert.match(html, /id="contribution-update-layer"/);
assert.match(html, /id="contribution-update-hint"/);
assert.match(html, /id="contribution-update-hint-text"/);
assert.match(html, /id="btn-dismiss-contribution-update-hint"/);
+252 -1
View File
@@ -2,19 +2,74 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function createAccountPoolUiStub() {
return {
createAccountPoolFormController({
formShell,
toggleButton,
hiddenLabel = '添加账号',
visibleLabel = '取消添加',
onClear,
onFocus,
} = {}) {
let visible = false;
function sync() {
if (formShell) {
formShell.hidden = !visible;
}
if (toggleButton) {
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
toggleButton.setAttribute?.('aria-expanded', String(visible));
}
}
function setVisible(nextVisible, options = {}) {
visible = Boolean(nextVisible);
if (options.clearForm) {
onClear?.();
}
sync();
if (visible && options.focusField) {
onFocus?.();
}
}
sync();
return {
isVisible: () => visible,
setVisible,
sync,
};
},
};
}
test('sidepanel loads hotmail manager before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const helperIndex = html.indexOf('<script src="account-pool-ui.js"></script>');
const hotmailManagerIndex = html.indexOf('<script src="hotmail-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.notEqual(helperIndex, -1);
assert.notEqual(hotmailManagerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(helperIndex < hotmailManagerIndex);
assert.ok(hotmailManagerIndex < sidepanelIndex);
});
test('sidepanel html contains collapsible hotmail form controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="btn-toggle-hotmail-form"/);
assert.match(html, /id="hotmail-form-shell"/);
assert.match(html, /id="btn-import-hotmail-accounts"[^>]*>批量导入</);
});
test('hotmail manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
const windowObject = {};
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
@@ -77,3 +132,199 @@ test('hotmail manager exposes a factory and renders empty state', () => {
manager.renderHotmailAccounts();
assert.match(hotmailAccountsList.innerHTML, /还没有 Hotmail 账号/);
});
test('hotmail manager toggles form container from header button', () => {
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelHotmailManager;`)(
windowObject,
localStorageMock
);
const handlers = {};
const toggleFormButton = {
textContent: '',
disabled: false,
setAttribute(name, value) {
this[name] = value;
},
addEventListener(type, handler) {
if (type === 'click') handlers.toggleForm = handler;
},
};
const formShell = { hidden: true };
const manager = api.createHotmailManager({
state: {
getLatestState: () => ({ currentHotmailAccountId: null }),
syncLatestState() {},
},
dom: {
btnAddHotmailAccount: { textContent: '', disabled: false, addEventListener() {} },
btnClearUsedHotmailAccounts: { textContent: '', disabled: false, addEventListener() {} },
btnDeleteAllHotmailAccounts: { textContent: '', disabled: false, addEventListener() {} },
btnHotmailUsageGuide: { addEventListener() {} },
btnImportHotmailAccounts: { disabled: false, addEventListener() {} },
btnToggleHotmailForm: toggleFormButton,
btnToggleHotmailList: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
hotmailAccountsList: { innerHTML: '', addEventListener() {} },
hotmailFormShell: formShell,
hotmailListShell: { classList: { toggle() {} } },
inputEmail: { value: '' },
inputHotmailClientId: { value: '' },
inputHotmailEmail: { value: '', focus() { this.focused = true; } },
inputHotmailImport: { value: '' },
inputHotmailPassword: { value: '' },
inputHotmailRefreshToken: { value: '' },
selectMailProvider: { value: 'hotmail-api' },
},
helpers: {
getHotmailAccounts: () => [],
getCurrentHotmailEmail: () => '',
escapeHtml: (value) => String(value || ''),
showToast() {},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
},
runtime: {
sendMessage: async () => ({}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-hotmail-list-expanded',
},
hotmailUtils: {},
});
manager.bindHotmailEvents();
assert.equal(formShell.hidden, true);
assert.equal(toggleFormButton.textContent, '添加账号');
handlers.toggleForm();
assert.equal(formShell.hidden, false);
assert.equal(toggleFormButton.textContent, '取消添加');
handlers.toggleForm();
assert.equal(formShell.hidden, true);
assert.equal(toggleFormButton.textContent, '添加账号');
});
test('hotmail manager hides form after save succeeds', async () => {
const source = fs.readFileSync('sidepanel/hotmail-manager.js', 'utf8');
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelHotmailManager;`)(
windowObject,
localStorageMock
);
const handlers = {};
const formShell = { hidden: true };
const toggleFormButton = {
textContent: '',
disabled: false,
setAttribute() {},
addEventListener(type, handler) {
if (type === 'click') handlers.toggleForm = handler;
},
};
const addButton = {
textContent: '',
disabled: false,
addEventListener(type, handler) {
if (type === 'click') handlers.add = handler;
},
};
const inputHotmailEmail = { value: '', focus() {} };
const inputHotmailClientId = { value: '' };
const inputHotmailPassword = { value: '' };
const inputHotmailRefreshToken = { value: '' };
const toastMessages = [];
const manager = api.createHotmailManager({
state: {
getLatestState: () => ({ currentHotmailAccountId: null }),
syncLatestState() {},
},
dom: {
btnAddHotmailAccount: addButton,
btnClearUsedHotmailAccounts: { textContent: '', disabled: false, addEventListener() {} },
btnDeleteAllHotmailAccounts: { textContent: '', disabled: false, addEventListener() {} },
btnHotmailUsageGuide: { addEventListener() {} },
btnImportHotmailAccounts: { disabled: false, addEventListener() {} },
btnToggleHotmailForm: toggleFormButton,
btnToggleHotmailList: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
hotmailAccountsList: { innerHTML: '', addEventListener() {} },
hotmailFormShell: formShell,
hotmailListShell: { classList: { toggle() {} } },
inputEmail: { value: '' },
inputHotmailClientId,
inputHotmailEmail,
inputHotmailImport: { value: '' },
inputHotmailPassword,
inputHotmailRefreshToken,
selectMailProvider: { value: 'hotmail-api' },
},
helpers: {
getHotmailAccounts: () => [],
getCurrentHotmailEmail: () => '',
escapeHtml: (value) => String(value || ''),
showToast(message) {
toastMessages.push(message);
},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
},
runtime: {
sendMessage: async () => ({
account: {
id: 'acc-1',
email: 'demo@hotmail.com',
clientId: 'client-id',
refreshToken: 'refresh-token',
},
}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-hotmail-list-expanded',
},
hotmailUtils: {},
});
manager.bindHotmailEvents();
handlers.toggleForm();
inputHotmailEmail.value = 'demo@hotmail.com';
inputHotmailClientId.value = 'client-id';
inputHotmailPassword.value = 'secret';
inputHotmailRefreshToken.value = 'refresh-token';
await handlers.add();
assert.equal(formShell.hidden, true);
assert.equal(toggleFormButton.textContent, '添加账号');
assert.equal(inputHotmailEmail.value, '');
assert.equal(inputHotmailClientId.value, '');
assert.equal(inputHotmailPassword.value, '');
assert.equal(inputHotmailRefreshToken.value, '');
assert.match(toastMessages.at(-1) || '', /已保存 Hotmail 账号/);
});
+144
View File
@@ -0,0 +1,144 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => sidepanelSource.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 i = start; i < sidepanelSource.length; i += 1) {
const ch = sidepanelSource[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
break;
}
}
let depth = 0;
let end = braceStart;
for (; end < sidepanelSource.length; end += 1) {
const ch = sidepanelSource[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return sidepanelSource.slice(start, end);
}
test('syncSelectedMail2925PoolAccount writes selected pool email back to mail2925BaseEmail', async () => {
const bundle = [
extractFunction('getMail2925Accounts'),
extractFunction('getCurrentMail2925Account'),
extractFunction('getCurrentMail2925Email'),
extractFunction('isMail2925AccountPoolEnabled'),
extractFunction('syncMail2925PoolAccountOptions'),
extractFunction('getPreferredMail2925PoolAccountId'),
extractFunction('syncSelectedMail2925PoolAccount'),
].join('\n');
const api = new Function(`
let latestState = {
mail2925UseAccountPool: true,
mail2925BaseEmail: 'old@2925.com',
currentMail2925AccountId: '',
mail2925Accounts: [{ id: 'acc-1', email: 'new@2925.com' }],
};
const selectMail2925PoolAccount = { value: 'acc-1', innerHTML: '' };
const chrome = {
runtime: {
async sendMessage() {
return { account: { id: 'acc-1', email: 'new@2925.com' } };
},
},
};
const toastEvents = [];
function syncLatestState(patch) {
latestState = { ...latestState, ...patch };
}
function setManagedAliasBaseEmailInputForProvider() {}
function showToast(message) {
toastEvents.push(message);
}
function escapeHtml(value) {
return String(value || '');
}
${bundle}
return {
syncSelectedMail2925PoolAccount,
getLatestState() {
return latestState;
},
};
`)();
await api.syncSelectedMail2925PoolAccount({ silent: true });
assert.equal(api.getLatestState().currentMail2925AccountId, 'acc-1');
assert.equal(api.getLatestState().mail2925BaseEmail, 'new@2925.com');
});
test('syncMail2925BaseEmailFromCurrentAccount reuses current pool account email for manual base email field', async () => {
const bundle = [
extractFunction('getMail2925Accounts'),
extractFunction('getCurrentMail2925Account'),
extractFunction('getCurrentMail2925Email'),
extractFunction('isMail2925AccountPoolEnabled'),
extractFunction('syncMail2925BaseEmailFromCurrentAccount'),
].join('\n');
const api = new Function(`
let latestState = {
mail2925UseAccountPool: true,
mail2925BaseEmail: 'old@2925.com',
currentMail2925AccountId: 'acc-1',
mail2925Accounts: [{ id: 'acc-1', email: 'new@2925.com' }],
};
let saveCalls = 0;
function syncLatestState(patch) {
latestState = { ...latestState, ...patch };
}
async function saveSettings() {
saveCalls += 1;
}
${bundle}
return {
syncMail2925BaseEmailFromCurrentAccount,
getLatestState() {
return latestState;
},
getSaveCalls() {
return saveCalls;
},
};
`)();
const changed = api.syncMail2925BaseEmailFromCurrentAccount(undefined, { persist: true });
await Promise.resolve();
assert.equal(changed, true);
assert.equal(api.getLatestState().mail2925BaseEmail, 'new@2925.com');
assert.equal(api.getSaveCalls(), 1);
});
+52 -4
View File
@@ -2,14 +2,61 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('sidepanel html contains cancel edit button for mail2925 form', () => {
function createAccountPoolUiStub() {
return {
createAccountPoolFormController({
formShell,
toggleButton,
hiddenLabel = '添加账号',
visibleLabel = '取消添加',
onClear,
onFocus,
} = {}) {
let visible = false;
function sync() {
if (formShell) {
formShell.hidden = !visible;
}
if (toggleButton) {
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
toggleButton.setAttribute?.('aria-expanded', String(visible));
}
}
function setVisible(nextVisible, options = {}) {
visible = Boolean(nextVisible);
if (options.clearForm) {
onClear?.();
}
sync();
if (visible && options.focusField) {
onFocus?.();
}
}
sync();
return {
isVisible: () => visible,
setVisible,
sync,
};
},
};
}
test('sidepanel html contains collapsible mail2925 form controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="btn-cancel-mail2925-edit"/);
assert.match(html, /id="btn-toggle-mail2925-form"/);
assert.match(html, /id="mail2925-form-shell"/);
assert.doesNotMatch(html, /id="btn-cancel-mail2925-edit"/);
});
test('mail2925 manager renders edit action for existing accounts', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {};
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
@@ -43,14 +90,15 @@ test('mail2925 manager renders edit action for existing accounts', () => {
},
dom: {
btnAddMail2925Account: { textContent: '', disabled: false, addEventListener() {} },
btnCancelMail2925Edit: { style: { display: 'none' }, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925Form: { textContent: '', setAttribute() {}, addEventListener() {} },
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
inputMail2925Email: { value: '' },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList,
mail2925FormShell: { hidden: true },
mail2925ListShell: { classList: { toggle() {} } },
},
helpers: {
+254 -2
View File
@@ -2,13 +2,59 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function createAccountPoolUiStub() {
return {
createAccountPoolFormController({
formShell,
toggleButton,
hiddenLabel = '添加账号',
visibleLabel = '取消添加',
onClear,
onFocus,
} = {}) {
let visible = false;
function sync() {
if (formShell) {
formShell.hidden = !visible;
}
if (toggleButton) {
toggleButton.textContent = visible ? visibleLabel : hiddenLabel;
toggleButton.setAttribute?.('aria-expanded', String(visible));
}
}
function setVisible(nextVisible, options = {}) {
visible = Boolean(nextVisible);
if (options.clearForm) {
onClear?.();
}
sync();
if (visible && options.focusField) {
onFocus?.();
}
}
sync();
return {
isVisible: () => visible,
setVisible,
sync,
};
},
};
}
test('sidepanel loads mail2925 manager before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const helperIndex = html.indexOf('<script src="account-pool-ui.js"></script>');
const managerIndex = html.indexOf('<script src="mail-2925-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.notEqual(helperIndex, -1);
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(helperIndex < managerIndex);
assert.ok(managerIndex < sidepanelIndex);
});
@@ -17,12 +63,21 @@ test('sidepanel html contains mail2925 pool toggle and selector controls', () =>
assert.match(html, /id="input-mail2925-use-account-pool"/);
assert.match(html, /id="select-mail2925-pool-account"/);
assert.match(html, /id="btn-cancel-mail2925-edit"/);
assert.match(html, /id="btn-toggle-mail2925-form"/);
assert.match(html, /id="mail2925-form-shell"/);
assert.doesNotMatch(html, /id="btn-cancel-mail2925-edit"/);
});
test('sidepanel css keeps collapsed shared mailbox list near a single card height', () => {
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
assert.match(css, /\.hotmail-list-shell\.is-collapsed\s*\{\s*max-height:\s*176px;\s*\}/);
});
test('mail2925 manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {};
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
@@ -38,6 +93,12 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
assert.equal(typeof api?.createMail2925Manager, 'function');
const mail2925AccountsList = { innerHTML: '', addEventListener() {} };
const formToggleButton = {
textContent: '',
disabled: false,
setAttribute() {},
addEventListener() {},
};
const toggleButton = {
textContent: '',
disabled: false,
@@ -55,11 +116,13 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
btnAddMail2925Account: { disabled: false, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925Form: formToggleButton,
btnToggleMail2925List: toggleButton,
inputMail2925Email: { value: '' },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList,
mail2925FormShell: { hidden: true },
mail2925ListShell: { classList: noopClassList },
},
helpers: {
@@ -88,3 +151,192 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
manager.renderMail2925Accounts();
assert.match(mail2925AccountsList.innerHTML, /还没有 2925 账号/);
});
test('mail2925 manager toggles form container from header button', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
windowObject,
localStorageMock
);
const clickHandlers = {};
const formToggleButton = {
textContent: '',
disabled: false,
setAttribute(name, value) {
this[name] = value;
},
addEventListener(type, handler) {
clickHandlers[type] = handler;
},
};
const formShell = { hidden: true };
const manager = api.createMail2925Manager({
state: {
getLatestState: () => ({ currentMail2925AccountId: null, mail2925Accounts: [] }),
syncLatestState() {},
},
dom: {
btnAddMail2925Account: { textContent: '', disabled: false, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925Form: formToggleButton,
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
inputMail2925Email: { value: '', focus() { this.focused = true; } },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList: { innerHTML: '', addEventListener() {} },
mail2925FormShell: formShell,
mail2925ListShell: { classList: { toggle() {} } },
},
helpers: {
getMail2925Accounts: () => [],
escapeHtml: (value) => String(value || ''),
showToast() {},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
refreshManagedAliasBaseEmail() {},
},
runtime: {
sendMessage: async () => ({}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-mail2925-list-expanded',
},
mail2925Utils: {},
});
manager.bindMail2925Events();
assert.equal(formShell.hidden, true);
assert.equal(formToggleButton.textContent, '添加账号');
clickHandlers.click();
assert.equal(formShell.hidden, false);
assert.equal(formToggleButton.textContent, '取消添加');
clickHandlers.click();
assert.equal(formShell.hidden, true);
assert.equal(formToggleButton.textContent, '添加账号');
});
test('mail2925 manager hides form after save succeeds', async () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
windowObject,
localStorageMock
);
let latestState = { currentMail2925AccountId: null, mail2925Accounts: [] };
const handlers = {};
const formToggleButton = {
textContent: '',
disabled: false,
setAttribute() {},
addEventListener(type, handler) {
if (type === 'click') handlers.toggle = handler;
},
};
const addButton = {
textContent: '',
disabled: false,
addEventListener(type, handler) {
if (type === 'click') handlers.add = handler;
},
};
const formShell = { hidden: true };
const inputMail2925Email = { value: '', focus() {} };
const inputMail2925Password = { value: '' };
const toastMessages = [];
const manager = api.createMail2925Manager({
state: {
getLatestState: () => latestState,
syncLatestState(patch) {
latestState = { ...latestState, ...patch };
},
},
dom: {
btnAddMail2925Account: addButton,
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925Form: formToggleButton,
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
inputMail2925Email,
inputMail2925Import: { value: '' },
inputMail2925Password,
mail2925AccountsList: { innerHTML: '', addEventListener() {} },
mail2925FormShell: formShell,
mail2925ListShell: { classList: { toggle() {} } },
},
helpers: {
getMail2925Accounts: (state) => state.mail2925Accounts || [],
escapeHtml: (value) => String(value || ''),
showToast(message) {
toastMessages.push(message);
},
openConfirmModal: async () => true,
copyTextToClipboard: async () => {},
refreshManagedAliasBaseEmail() {},
},
runtime: {
sendMessage: async () => ({
account: {
id: 'acc-1',
email: 'demo@2925.com',
password: 'secret',
enabled: true,
lastLoginAt: 0,
lastUsedAt: 0,
lastLimitAt: 0,
disabledUntil: 0,
lastError: '',
},
}),
},
constants: {
copyIcon: '',
displayTimeZone: 'Asia/Shanghai',
expandedStorageKey: 'multipage-mail2925-list-expanded',
},
mail2925Utils: {
upsertMail2925AccountInList: (accounts, nextAccount) => accounts.concat(nextAccount),
},
});
manager.bindMail2925Events();
handlers.toggle();
inputMail2925Email.value = 'demo@2925.com';
inputMail2925Password.value = 'secret';
await handlers.add();
assert.equal(formShell.hidden, true);
assert.equal(formToggleButton.textContent, '添加账号');
assert.equal(addButton.textContent, '添加账号');
assert.equal(inputMail2925Email.value, '');
assert.equal(inputMail2925Password.value, '');
assert.match(toastMessages.at(-1) || '', /已保存 2925 账号/);
});
+17 -2
View File
@@ -58,6 +58,12 @@ const logs = [];
const clicks = [];
const filledValues = [];
let submitClicked = false;
const VERIFICATION_CODE_INPUT_SELECTOR = 'input[data-verification-code]';
const location = { href: 'https://auth.openai.com/email-verification' };
function KeyboardEvent(type, init = {}) {
this.type = type;
Object.assign(this, init);
}
const submitBtn = {
tagName: 'BUTTON',
@@ -75,13 +81,20 @@ const submitBtn = {
const inputs = Array.from({ length: 6 }, () => ({
value: '',
maxLength: 1,
getAttribute(name) {
if (name === 'maxlength') return '1';
if (name === 'aria-disabled') return 'false';
return '';
},
focus() {},
dispatchEvent() {},
closest() { return null; },
}));
const document = {
querySelector(selector) {
if (selector === 'button[type="submit"], input[type="submit"]') return submitBtn;
if (selector === VERIFICATION_CODE_INPUT_SELECTOR) return inputs[0];
return null;
},
querySelectorAll(selector) {
@@ -97,7 +110,6 @@ function log(message, level = 'info') { logs.push({ message, level }); }
async function waitForLoginVerificationPageReady() {}
function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
async function waitForElement() { throw new Error('not found'); }
function fillInput(el, value) {
el.value = value;
filledValues.push(value);
@@ -110,8 +122,11 @@ async function humanPause() {}
function simulateClick(el) { el.click(); clicks.push(el.textContent); }
async function waitForVerificationSubmitOutcome() { return { success: true }; }
${extractFunction('getVisibleSplitVerificationInputs')}
${extractFunction('getVerificationCodeTarget')}
${extractFunction('getVerificationSubmitButtonForTarget')}
${extractFunction('waitForVerificationSubmitButton')}
${extractFunction('waitForVerificationCodeTarget')}
${extractFunction('waitForSplitVerificationInputsFilled')}
${extractFunction('fillVerificationCode')}
+48
View File
@@ -101,3 +101,51 @@ return {
assert.deepStrictEqual(result, { success: true });
assert.equal(api.snapshot().recoverCalls, 1);
});
test('waitForVerificationSubmitOutcome does not assume success after repeated signup retry pages', async () => {
const api = new Function(`
let recoverCalls = 0;
const location = { href: 'https://auth.openai.com/email-verification' };
function throwIfStopped() {}
function log() {}
function getVerificationErrorText() { return ''; }
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVerificationPageStillVisible() { return false; }
function createSignupUserAlreadyExistsError() {
return new Error('SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。');
}
function getCurrentAuthRetryPageState(flow) {
if (flow === 'signup') {
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
};
}
return null;
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
}
async function sleep() {}
${extractFunction('waitForVerificationSubmitOutcome')}
return {
run() {
return waitForVerificationSubmitOutcome(4, 1000);
},
snapshot() {
return { recoverCalls };
},
};
`)();
await assert.rejects(
api.run(),
/连续进入认证重试页 2 次,页面仍未恢复/
);
assert.equal(api.snapshot().recoverCalls, 2);
});
+21
View File
@@ -146,6 +146,27 @@ return {
assert.strictEqual(snapshot.displayedEmail, 'display.user@example.com');
}
{
const api = createApi({
pathname: '/email-verification',
retryState: {
retryEnabled: true,
titleMatched: false,
detailMatched: false,
routeErrorMatched: true,
},
verificationTarget: { id: 'otp' },
verificationVisible: true,
});
const snapshot = api.inspectLoginAuthState();
assert.strictEqual(
snapshot.state,
'login_timeout_error_page',
'第七步在 /email-verification 的登录重试页应优先识别为登录超时报错页'
);
}
{
const api = createApi({
oauthConsentPage: true,
+108
View File
@@ -103,3 +103,111 @@ return {
assert.equal(result.state, 'login_timeout_error_page');
assert.equal(result.message, '当前页面处于登录超时报错页。');
});
test('step 7 finalize converts verification page that falls into retry page into recoverable result', async () => {
const api = new Function(`
const logs = [];
let recoverCalls = 0;
let currentState = 'verification_page';
const location = {
href: 'https://auth.openai.com/email-verification',
};
function inspectLoginAuthState() {
return {
state: currentState,
url: location.href,
};
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
return { recovered: true };
}
async function sleep() {
currentState = 'login_timeout_error_page';
}
function log(message, level = 'info') {
logs.push({ message, level });
}
function throwIfStopped() {}
function getLoginAuthStateLabel(snapshot) {
switch (snapshot?.state) {
case 'verification_page':
return '登录验证码页';
case 'login_timeout_error_page':
return '登录超时报错页';
default:
return '未知页面';
}
}
${extractFunction('createStep6SuccessResult')}
${extractFunction('createStep6RecoverableResult')}
${extractFunction('normalizeStep6Snapshot')}
${extractFunction('createStep6LoginTimeoutRecoverableResult')}
${extractFunction('finalizeStep6VerificationReady')}
return {
async run() {
return finalizeStep6VerificationReady({
logLabel: '步骤 7 收尾',
loginVerificationRequestedAt: 123,
via: 'password_submit',
});
},
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, '登录验证码页面准备就绪前进入登录超时报错页。');
});
test('waitForLoginVerificationPageReady reports login timeout page without step8 restart prefix', async () => {
const api = new Function(`
const location = {
href: 'https://auth.openai.com/email-verification',
};
function inspectLoginAuthState() {
return {
state: 'login_timeout_error_page',
url: location.href,
};
}
function throwIfStopped() {}
async function sleep() {}
function getLoginAuthStateLabel(snapshot) {
return snapshot?.state === 'login_timeout_error_page' ? '登录超时报错页' : '未知页面';
}
${extractFunction('waitForLoginVerificationPageReady')}
return {
run() {
return waitForLoginVerificationPageReady(10);
},
};
`)();
await assert.rejects(
() => api.run(),
/当前未进入登录验证码页面,请先重新完成步骤 7。当前状态:登录超时报错页。URL: https:\/\/auth\.openai\.com\/email-verification/
);
});