This commit is contained in:
QLHazyCoder
2026-04-22 21:52:12 +08:00
51 changed files with 4918 additions and 583 deletions
+10
View File
@@ -81,6 +81,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
true
);
assert.equal(
isRecoverableStep9AuthFailure('提交回调失败:请更新CLI Proxy API或检查连接'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证失败:Bad Request'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证成功!'),
false
+95 -1
View File
@@ -71,6 +71,7 @@ test('executeStep reuses the active top-level auth chain instead of starting a d
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
@@ -106,6 +107,10 @@ function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
function isBrowserSwitchRequiredError() {
return false;
}
async function handleBrowserSwitchRequired() {}
function doesStepUseCompletionSignal() {
return false;
}
@@ -159,10 +164,99 @@ return {
assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message)));
});
test('executeStep stops flow when browser-switch-required error is raised', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
const events = {
logs: [],
statusCalls: [],
stopRequests: [],
appendRecords: [],
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function setStepStatus(step, status) {
events.statusCalls.push({ step, status });
}
async function humanStepDelay() {}
async function getState() {
return {
flowStartTime: null,
stepStatuses: {},
};
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function appendManualAccountRunRecordIfNeeded(status, _state, reason) {
events.appendRecords.push({ status, reason });
}
function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
async function requestStop(options = {}) {
events.stopRequests.push(options);
}
function doesStepUseCompletionSignal() {
return false;
}
function isRetryableContentScriptTransportError() {
return false;
}
const stepRegistry = {
async executeStep() {
throw new Error('BROWSER_SWITCH_REQUIRED::请更换浏览器进行注册登录。');
},
};
${extractFunction('isStopError')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('isBrowserSwitchRequiredError')}
${extractFunction('getBrowserSwitchRequiredMessage')}
${extractFunction('handleBrowserSwitchRequired')}
${extractFunction('executeStep')}
return {
executeStep,
snapshot() {
return events;
},
};
`)();
await assert.rejects(
() => api.executeStep(10),
/流程已被用户停止。/
);
const events = api.snapshot();
assert.deepStrictEqual(events.stopRequests, [
{ logMessage: '请更换浏览器进行注册登录。' },
]);
assert.deepStrictEqual(events.statusCalls, [
{ step: 10, status: 'running' },
]);
assert.equal(
events.logs.some(({ message }) => /步骤 10 失败/.test(message)),
false,
'browser-switch-required error should stop the flow before generic failed logging'
);
});
test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
@@ -15,3 +15,64 @@ test('generated email helper module exposes a factory', () => {
assert.equal(typeof api?.createGeneratedEmailHelpers, 'function');
});
test('generated email helper falls back to normal generator when 2925 is in receive mode', async () => {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
const events = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build alias in receive mode');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getState: async () => ({
mailProvider: '2925',
mail2925Mode: 'receive',
emailGenerator: 'duck',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate 2925 account in receive mode');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: (_provider, mail2925Mode) => mail2925Mode === 'provide',
reuseOrCreateTab: async () => {},
sendToContentScript: async (_source, message) => {
events.push(message.type);
return { email: 'duck@example.com', generated: true };
},
setEmailState: async (email) => {
events.push(['email', email]);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
mailProvider: '2925',
mail2925Mode: 'receive',
emailGenerator: 'duck',
}, {
mailProvider: '2925',
mail2925Mode: 'receive',
generator: 'duck',
});
assert.equal(email, 'duck@example.com');
assert.deepStrictEqual(events, [
'FETCH_DUCK_EMAIL',
['email', 'duck@example.com'],
]);
});
@@ -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,428 @@
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);
});
test('ensureMail2925MailboxSession relogs with selected account when mailbox page email mismatches and pool is on', async () => {
let currentState = {
autoRunning: false,
mail2925UseAccountPool: true,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'target@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
const openedUrls = [];
const sendPayloads = [];
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: openedUrls.at(-1) || '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 (_source, url) => {
openedUrls.push(url);
return 9;
},
sendToMailContentScriptResilient: async (_mail, message) => {
sendPayloads.push(message.payload);
if (sendPayloads.length === 1) {
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'wrong@2925.com',
};
}
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'target@2925.com',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
waitForTabUrlMatch: async () => ({ url: 'https://2925.com/login/' }),
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(result.result.loggedIn, true);
assert.deepStrictEqual(openedUrls, [
'https://2925.com/#/mailList',
'https://2925.com/login/',
]);
assert.equal(sendPayloads.length, 2);
assert.equal(sendPayloads[0].allowLoginWhenOnLoginPage, true);
assert.equal(sendPayloads[1].forceLogin, true);
});
test('ensureMail2925MailboxSession stops when mailbox page email mismatches and pool is off', async () => {
let currentState = {
autoRunning: true,
autoRunPhase: 'running',
mail2925UseAccountPool: false,
mail2925Accounts: [],
currentMail2925AccountId: null,
};
const stopCalls = [];
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: (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,
currentView: 'mailbox',
mailboxEmail: 'wrong@2925.com',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await assert.rejects(
() => manager.ensureMail2925MailboxSession({
accountId: null,
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
}),
/流程已被用户停止。/
);
assert.equal(sendCalls, 1);
assert.equal(stopCalls.length, 1);
assert.match(stopCalls[0].logMessage, /与目标账号 target@2925\.com 不一致/);
});
@@ -8,6 +8,13 @@ test('background mail2925 session uses /login/ as relogin entry url', () => {
assert.match(source, /const MAIL2925_LOGIN_URL = 'https:\/\/2925\.com\/login\/';/);
});
test('background mail2925 session keeps login message timeout above the 40-second mailbox wait', () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
assert.match(source, /timeoutMs:\s*50000,/);
assert.match(source, /responseTimeoutMs:\s*50000,/);
assert.match(source, /40 秒内未进入收件箱/);
});
test('ensureMail2925MailboxSession waits 3 seconds before and after opening login page on force relogin', async () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
@@ -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,115 @@ 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');
});
test('setCurrentMail2925Account persists currentMail2925AccountId for browser restart restore', 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 = {
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: null,
};
const persistedUpdates = [];
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {},
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,
setPersistentSettings: async (payload) => {
persistedUpdates.push(payload);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await manager.setCurrentMail2925Account('acc-1');
assert.equal(currentState.currentMail2925AccountId, 'acc-1');
assert.deepStrictEqual(persistedUpdates, [
{ currentMail2925AccountId: 'acc-1' },
]);
});
+71 -7
View File
@@ -7,6 +7,70 @@ const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep4;`)(globalScope);
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;
const executor = api.createStep4Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async (tabId, payload) => {
tabUpdates.push({ tabId, payload });
},
},
},
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {
ensureCalls += 1;
},
getMailConfig: () => ({
provider: '2925',
label: '2925 邮箱',
source: 'mail-2925',
url: 'https://2925.com',
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
resolveVerificationStep: async (_step, _state, _mail, options) => {
capturedOptions = options;
},
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
sendToContentScriptResilient: async () => ({}),
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
try {
await executor.executeStep4({
email: 'user@example.com',
password: 'secret',
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);
});
test('step 4 does not request a fresh code first for Cloudflare temp mail', async () => {
let capturedOptions = null;
const realDateNow = Date.now;
Date.now = () => 700000;
@@ -22,10 +86,10 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => ({
provider: '2925',
label: '2925 邮箱',
source: 'mail-2925',
url: 'https://2925.com',
provider: 'cloudflare-temp-email',
label: 'Cloudflare Temp Email',
source: 'cloudflare-temp-email',
url: 'https://temp.peekcart.com',
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
@@ -46,12 +110,12 @@ 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,
});
} finally {
Date.now = realDateNow;
}
assert.equal(capturedOptions.filterAfterTimestamp, 100000);
assert.equal(capturedOptions.resendIntervalMs, 0);
assert.equal(capturedOptions.filterAfterTimestamp, 700000);
assert.equal(capturedOptions.requestFreshCodeFirst, false);
assert.equal(capturedOptions.resendIntervalMs, 25000);
});
+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, '');
+153 -3
View File
@@ -4,8 +4,67 @@ const fs = require('node:fs');
const source = fs.readFileSync('content/mail-2925.js', 'utf8');
test('ensureMail2925Session waits at most 20 seconds for mailbox after clicking login', () => {
assert.match(source, /waitForMail2925View\('mailbox',\s*20000\)/);
test('ensureMail2925Session waits at most 40 seconds for mailbox after clicking login', () => {
assert.match(source, /waitForMail2925View\('mailbox',\s*40000\)/);
});
test('ensureMail2925Session waits 1 second after filling credentials before clicking login', () => {
assert.match(source, /fillInput\(passwordInput,\s*password\);[\s\S]*?await sleep\(200\);[\s\S]*?await sleep\(1000\);[\s\S]*?simulateClick\(loginButton\);/);
});
test('detectMail2925ViewState treats top mailbox email as mailbox view', () => {
const bundle = [
extractFunction('normalizeNodeText'),
extractFunction('isVisibleNode'),
extractFunction('isMailItemNode'),
extractFunction('resolveActionTarget'),
extractFunction('findMailItems'),
extractFunction('extractEmails'),
extractFunction('getMail2925DisplayedMailboxEmail'),
extractFunction('detectMail2925ViewState'),
].join('\n');
const api = new Function(`
const MAIL_ITEM_SELECTORS = ['.mail-item'];
const MAIL_ITEM_SELECTOR_GROUP = '.mail-item';
const MAIL2925_REMEMBER_LOGIN_PATTERNS = [];
const MAIL2925_AGREEMENT_PATTERNS = [];
const document = {
querySelectorAll(selector) {
if (selector === '.mail-item') return [];
if (selector === 'body *') return [headerEmail];
if (selector.includes('[class*="user"]')) return [headerEmail];
return [];
},
body: {
innerText: 'QLHazycoder qlhazycoder@2925.com',
textContent: 'QLHazycoder qlhazycoder@2925.com',
},
};
const window = {
innerHeight: 900,
getComputedStyle() {
return { display: 'block', visibility: 'visible' };
},
};
const headerEmail = {
hidden: false,
textContent: 'qlhazycoder@2925.com',
innerText: 'qlhazycoder@2925.com',
getBoundingClientRect() { return { top: 40, left: 400, width: 120, height: 20 }; },
closest() { return null; },
};
function detectMail2925LimitMessage() { return ''; }
function findMail2925LoginPasswordInput() { return null; }
function findMail2925LoginEmailInput() { return null; }
function getPageTextSample() { return 'qlhazycoder@2925.com'; }
${bundle}
return { detectMail2925ViewState };
`)();
const state = api.detectMail2925ViewState();
assert.equal(state.view, 'mailbox');
assert.equal(state.mailboxEmail, 'qlhazycoder@2925.com');
});
function extractFunction(name) {
@@ -164,7 +223,7 @@ return {
assert.deepEqual(api.getReadAndDeleteCalls(), ['baseline', 'new']);
});
test('handlePollEmail ignores targetEmail and still tests any matching ChatGPT mail', async () => {
test('handlePollEmail keeps ignoring targetEmail when receive-mode matching is disabled', async () => {
const bundle = [
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
@@ -242,12 +301,103 @@ return {
maxAttempts: 4,
intervalMs: 1,
targetEmail: 'expected@example.com',
mail2925MatchTargetEmail: false,
});
assert.equal(result.code, '112233');
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-1']);
});
test('handlePollEmail skips explicit mismatched target emails when receive-mode matching is enabled', async () => {
const bundle = [
extractFunction('extractEmails'),
extractFunction('emailMatchesTarget'),
extractFunction('getTargetEmailMatchState'),
extractFunction('normalizeMinuteTimestamp'),
extractFunction('handlePollEmail'),
].join('\n');
const api = new Function(`
let state = 'ready';
const seenCodes = new Set();
const readAndDeleteCalls = [];
const mismatchMail = {
id: 'mail-1',
text: 'ChatGPT verification code 112233 for another.user@example.com',
};
const targetMail = {
id: 'mail-2',
text: 'ChatGPT verification code 445566 for expected@example.com',
};
function findMailItems() {
return state === 'ready' ? [mismatchMail, targetMail] : [];
}
function getMailItemId(item) {
return item.id;
}
function getCurrentMailIds(items = []) {
return new Set(items.map((item) => item.id));
}
function parseMailItemTimestamp() {
return Date.now();
}
function matchesMailFilters(text) {
return /chatgpt|openai|verification/i.test(String(text || ''));
}
function getMailItemText(item) {
return item.text;
}
function extractVerificationCode(text) {
const match = String(text || '').match(/(\\d{6})/);
return match ? match[1] : null;
}
async function sleep() {}
async function sleepRandom() {}
async function returnToInbox() {
return true;
}
async function refreshInbox() {}
async function openMailAndDeleteAfterRead(item) {
readAndDeleteCalls.push(item.id);
return item.text;
}
async function ensureSeenCodesSession() {}
function persistSeenCodes() {}
function log() {}
${bundle}
return {
handlePollEmail,
getReadAndDeleteCalls() {
return readAndDeleteCalls.slice();
},
};
`)();
const result = await api.handlePollEmail(8, {
senderFilters: ['chatgpt'],
subjectFilters: ['verification'],
maxAttempts: 1,
intervalMs: 1,
targetEmail: 'expected@example.com',
mail2925MatchTargetEmail: true,
});
assert.equal(result.code, '445566');
assert.deepEqual(api.getReadAndDeleteCalls(), ['mail-2']);
});
test('handlePollEmail only accepts 2925 mails inside the fixed lookback window', async () => {
const bundle = [
extractFunction('normalizeMinuteTimestamp'),
+9
View File
@@ -26,3 +26,12 @@ test('managed alias utils validate provider email with or without configured bas
assert.equal(api.isManagedAliasEmail('manual@gmail.com', 'gmail', ''), true);
assert.equal(api.isManagedAliasEmail('manual@qq.com', 'gmail', ''), false);
});
test('managed alias utils keep 2925 alias generation behind provide mode only', () => {
assert.equal(api.normalizeMail2925Mode('provide'), 'provide');
assert.equal(api.normalizeMail2925Mode('receive'), 'receive');
assert.equal(api.normalizeMail2925Mode('other'), 'provide');
assert.equal(api.usesManagedAliasGeneration('gmail'), true);
assert.equal(api.usesManagedAliasGeneration('2925', { mail2925Mode: 'provide' }), true);
assert.equal(api.usesManagedAliasGeneration('2925', { mail2925Mode: 'receive' }), false);
});
+93
View File
@@ -0,0 +1,93 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function extractFunction(source, 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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('sidepanel run count input no longer hardcodes max=50', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const inputTag = html.match(/<input type="number" id="input-run-count"[^>]+>/);
assert.ok(inputTag, 'run count input should exist');
assert.doesNotMatch(inputTag[0], /\smax="50"/);
});
test('sidepanel getRunCountValue no longer clamps run count to 50', () => {
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
const bundle = extractFunction(source, 'getRunCountValue');
const api = new Function(`
const inputRunCount = { value: '88' };
${bundle}
return {
getRunCountValue,
setValue(value) {
inputRunCount.value = value;
},
};
`)();
assert.equal(api.getRunCountValue(), 88);
api.setValue('0');
assert.equal(api.getRunCountValue(), 1);
});
test('background normalizeRunCount no longer clamps values to 50', () => {
const source = fs.readFileSync('background.js', 'utf8');
const bundle = extractFunction(source, 'normalizeRunCount');
const api = new Function(`
${bundle}
return { normalizeRunCount };
`)();
assert.equal(api.normalizeRunCount(88), 88);
assert.equal(api.normalizeRunCount('120'), 120);
assert.equal(api.normalizeRunCount(0), 1);
assert.equal(api.normalizeRunCount('bad'), 1);
});
@@ -0,0 +1,96 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('sidepanel/sidepanel.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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('auto-run fallback risk warning starts at 3 runs', () => {
const bundle = extractFunction('shouldWarnAutoRunFallbackRisk');
const api = new Function(`
const AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS = 3;
${bundle}
return { shouldWarnAutoRunFallbackRisk };
`)();
assert.equal(api.shouldWarnAutoRunFallbackRisk(2, false), false);
assert.equal(api.shouldWarnAutoRunFallbackRisk(3, false), true);
assert.equal(api.shouldWarnAutoRunFallbackRisk(10, true), true);
});
test('auto-run fallback risk modal uses the single-node warning copy', async () => {
const bundle = extractFunction('openAutoRunFallbackRiskConfirmModal');
const api = new Function(`
let capturedOptions = null;
async function openConfirmModalWithOption(options) {
capturedOptions = options;
return { confirmed: true, optionChecked: false };
}
${bundle}
return {
openAutoRunFallbackRiskConfirmModal,
getCapturedOptions() {
return capturedOptions;
},
};
`)();
const result = await api.openAutoRunFallbackRiskConfirmModal(3);
const options = api.getCapturedOptions();
assert.deepStrictEqual(result, { confirmed: true, dismissPrompt: false });
assert.equal(options.title, '自动运行风险提醒');
assert.equal(
options.message,
'当前轮数已经不适合单节点情况,请确保已经配置并打开节点轮询功能(若没有配置,请点击贡献/使用按钮,根据网页中使用教程进行配置),避免连续使用一个节点注册,导致出现手机号验证。'
);
assert.equal(options.confirmLabel, '继续');
});
+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 账号/);
});
+227
View File
@@ -0,0 +1,227 @@
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);
});
test('collectSettingsPayload persists currentMail2925AccountId for 2925 account pool restore', () => {
const bundle = [
extractFunction('collectSettingsPayload'),
].join('\n');
const api = new Function(`
let latestState = {
contributionMode: false,
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-2',
};
let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false;
const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
const inputSub2ApiEmail = { value: '' };
const inputSub2ApiPassword = { value: '' };
const inputSub2ApiGroup = { value: '' };
const inputSub2ApiDefaultProxy = { value: '' };
const inputPassword = { value: '' };
const selectMailProvider = { value: '2925' };
const selectEmailGenerator = { value: 'duck' };
const checkboxAutoDeleteIcloud = { checked: false };
const selectIcloudHostPreference = { value: 'auto' };
const inputAccountRunHistoryTextEnabled = { checked: false };
const inputAccountRunHistoryHelperBaseUrl = { value: '' };
const inputMail2925UseAccountPool = { checked: true };
const inputInbucketHost = { value: '' };
const inputInbucketMailbox = { value: '' };
const inputHotmailRemoteBaseUrl = { value: '' };
const inputHotmailLocalBaseUrl = { value: '' };
const inputLuckmailApiKey = { value: '' };
const inputLuckmailBaseUrl = { value: '' };
const selectLuckmailEmailType = { value: 'ms_graph' };
const inputLuckmailDomain = { value: '' };
const inputTempEmailBaseUrl = { value: '' };
const inputTempEmailAdminAuth = { value: '' };
const inputTempEmailCustomAuth = { value: '' };
const inputTempEmailReceiveMailbox = { value: '' };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
function getCloudflareDomainsFromState() {
return { domains: [], activeDomain: '' };
}
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
function getCloudflareTempEmailDomainsFromState() {
return { domains: [], activeDomain: '' };
}
function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
function getSelectedLocalCpaStep9Mode() { return 'submit'; }
function getSelectedMail2925Mode() { return 'provide'; }
function getSelectedHotmailServiceMode() { return 'local'; }
function buildManagedAliasBaseEmailPayload() {
return { gmailBaseEmail: '', mail2925BaseEmail: 'demo@2925.com', emailPrefix: '' };
}
function normalizeLuckmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeLuckmailEmailType(value) { return String(value || '').trim() || 'ms_graph'; }
function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${bundle}
return { collectSettingsPayload };
`)();
const payload = api.collectSettingsPayload();
assert.equal(payload.currentMail2925AccountId, 'acc-2');
assert.equal(payload.mail2925UseAccountPool, true);
});
+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 账号/);
});
+109
View File
@@ -0,0 +1,109 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('sidepanel/sidepanel.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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('sidepanel html keeps 2925 mode row and standalone pool settings row', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="row-mail-2925-mode"/);
assert.match(html, /data-mail2925-mode="provide"/);
assert.match(html, /data-mail2925-mode="receive"/);
assert.match(html, /id="row-mail2925-pool-settings"/);
});
test('sidepanel only treats 2925 as generated alias provider in provide mode', () => {
const bundle = [
extractFunction('isManagedAliasProvider'),
extractFunction('usesGeneratedAliasMailProvider'),
].join('\n');
const api = new Function(`
const GMAIL_PROVIDER = 'gmail';
const MAIL_2925_MODE_PROVIDE = 'provide';
const MAIL_2925_MODE_RECEIVE = 'receive';
const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE;
const selectMailProvider = { value: '2925' };
function normalizeMail2925Mode(value = '') {
return String(value || '').trim().toLowerCase() === MAIL_2925_MODE_RECEIVE
? MAIL_2925_MODE_RECEIVE
: DEFAULT_MAIL_2925_MODE;
}
function getSelectedMail2925Mode() {
return MAIL_2925_MODE_PROVIDE;
}
function getManagedAliasUtils() {
return {
usesManagedAliasGeneration(provider, options = {}) {
return String(provider || '').trim().toLowerCase() === 'gmail'
|| (String(provider || '').trim().toLowerCase() === '2925'
&& normalizeMail2925Mode(options.mail2925Mode) === MAIL_2925_MODE_PROVIDE);
},
};
}
${bundle}
return {
isManagedAliasProvider,
usesGeneratedAliasMailProvider,
};
`)();
assert.equal(api.isManagedAliasProvider('2925', 'provide'), true);
assert.equal(api.isManagedAliasProvider('2925', 'receive'), false);
assert.equal(api.usesGeneratedAliasMailProvider('2925', 'provide'), true);
assert.equal(api.usesGeneratedAliasMailProvider('2925', 'receive'), false);
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'receive'), true);
});
+314
View File
@@ -144,3 +144,317 @@ return {
]);
assert.match(result.bodyTextPreview, /Welcome to ChatGPT/);
});
test('signup entry diagnostics captures hidden signup button style and blocking ancestor details', () => {
const api = new Function(`
const SIGNUP_ENTRY_TRIGGER_PATTERN = /鍏嶈垂娉ㄥ唽|绔嬪嵆娉ㄥ唽|娉ㄥ唽|sign\\s*up|register|create\\s*account|create\\s+account/i;
const location = { href: 'https://chatgpt.com/' };
const hiddenSection = {
tagName: 'DIV',
id: 'mobile-cta',
className: 'max-xs:hidden',
hidden: false,
parentElement: null,
hasAttribute() {
return false;
},
getAttribute(name) {
if (name === 'aria-hidden') return '';
return '';
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
_style: {
display: 'none',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const hiddenSignupButton = {
tagName: 'BUTTON',
textContent: 'Sign up for free',
disabled: false,
className: 'signup-button',
hidden: false,
parentElement: hiddenSection,
hasAttribute() {
return false;
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
getAttribute(name) {
if (name === 'type') return '';
if (name === 'aria-hidden') return '';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const document = {
title: 'ChatGPT',
readyState: 'complete',
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [hiddenSignupButton];
}
return [];
},
};
const window = {
innerWidth: 390,
innerHeight: 844,
outerWidth: 390,
outerHeight: 844,
devicePixelRatio: 3,
getComputedStyle(el) {
return el?._style || {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
};
},
};
function isVisibleElement(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getSignupEmailInput() {
return null;
}
function getSignupPasswordInput() {
return null;
}
function getPageTextSnapshot() {
return 'ChatGPT 登录';
}
${extractFunction('getSignupEntryDiagnostics')}
return {
run() {
return getSignupEntryDiagnostics();
},
};
`)();
const result = api.run();
assert.deepStrictEqual(result.viewport, {
innerWidth: 390,
innerHeight: 844,
outerWidth: 390,
outerHeight: 844,
devicePixelRatio: 3,
});
assert.deepStrictEqual(result.signupLikeActionCounts, {
total: 1,
visible: 0,
hidden: 1,
});
assert.equal(result.signupLikeActions[0]?.text, 'Sign up for free');
assert.equal(result.signupLikeActions[0]?.className, 'signup-button');
assert.equal(result.signupLikeActions[0]?.display, 'block');
assert.equal(result.signupLikeActions[0]?.blockingAncestor?.className, 'max-xs:hidden');
assert.equal(result.signupLikeActions[0]?.blockingAncestor?.display, 'none');
});
test('signup password diagnostics summarizes password inputs, submit button, and alternate code entry', () => {
const api = new Function(`
const location = { href: 'https://auth.openai.com/create-account/password' };
const form = { action: 'https://auth.openai.com/u/signup/password' };
const passwordInput = {
tagName: 'INPUT',
type: 'password',
name: 'new-password',
id: 'password-field',
value: 'SecretLength14',
className: 'password-input',
disabled: false,
form,
getBoundingClientRect() {
return { width: 320, height: 44 };
},
getAttribute(name) {
if (name === 'type') return 'password';
if (name === 'name') return 'new-password';
if (name === 'autocomplete') return 'new-password';
if (name === 'placeholder') return 'Password';
if (name === 'aria-disabled') return 'false';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const submitButton = {
tagName: 'BUTTON',
textContent: 'Continue',
className: 'submit-btn',
disabled: false,
form,
getBoundingClientRect() {
return { width: 160, height: 40 };
},
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
if (name === 'data-dd-action-name') return 'Continue';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const oneTimeCodeButton = {
tagName: 'BUTTON',
textContent: 'Use a one-time code instead',
className: 'switch-btn',
disabled: false,
getBoundingClientRect() {
return { width: 220, height: 36 };
},
getAttribute(name) {
if (name === 'type') return 'button';
if (name === 'aria-disabled') return 'false';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const document = {
title: 'Create your account',
readyState: 'complete',
querySelectorAll(selector) {
if (selector === 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]') {
return [passwordInput];
}
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [submitButton, oneTimeCodeButton];
}
return [];
},
};
const window = {
getComputedStyle(el) {
return el?._style || {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
};
},
};
function isVisibleElement(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getPageTextSnapshot() {
return 'Create your account Use a one-time code instead';
}
function getSignupPasswordInput() {
return passwordInput;
}
function getSignupPasswordSubmitButton() {
return submitButton;
}
function getSignupPasswordDisplayedEmail() {
return 'user@example.com';
}
function findOneTimeCodeLoginTrigger() {
return oneTimeCodeButton;
}
function getSignupPasswordTimeoutErrorPageState() {
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
};
}
${extractFunction('getSignupPasswordDiagnostics')}
return {
run() {
return getSignupPasswordDiagnostics();
},
};
`)();
const result = api.run();
assert.equal(result.url, 'https://auth.openai.com/create-account/password');
assert.equal(result.displayedEmail, 'user@example.com');
assert.equal(result.hasVisiblePasswordInput, true);
assert.equal(result.passwordInputCount, 1);
assert.equal(result.visiblePasswordInputCount, 1);
assert.equal(result.passwordInputs[0]?.name, 'new-password');
assert.equal(result.passwordInputs[0]?.valueLength, 14);
assert.equal(result.submitButton?.text, 'Continue');
assert.equal(result.oneTimeCodeTrigger?.text, 'Use a one-time code instead');
assert.equal(result.retryPage, true);
assert.equal(result.retryEnabled, true);
assert.match(result.bodyTextPreview, /one-time code/);
});
+156
View File
@@ -0,0 +1,156 @@
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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('fillVerificationCode submits after split inputs are stably filled', async () => {
const api = new Function(`
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',
textContent: 'Continue',
disabled: false,
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
return '';
},
click() {
submitClicked = true;
},
};
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 === VERIFICATION_CODE_INPUT_SELECTOR) return inputs[0];
return null;
},
querySelectorAll(selector) {
if (selector === 'input[maxlength="1"]') return inputs;
if (selector === 'button[type="submit"], input[type="submit"]') return [submitBtn];
if (selector === 'button, [role="button"], input[type="button"], input[type="submit"]') return [submitBtn];
return [];
},
};
function throwIfStopped() {}
function log(message, level = 'info') { logs.push({ message, level }); }
async function waitForLoginVerificationPageReady() {}
function is405MethodNotAllowedPage() { return false; }
async function handle405ResendError() {}
function fillInput(el, value) {
el.value = value;
filledValues.push(value);
}
async function sleep() {}
function isVisibleElement() { return true; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
function getActionText(el) { return el.textContent || ''; }
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')}
return {
run() {
return fillVerificationCode(4, { code: '123456' });
},
snapshot() {
return {
logs,
clicks,
filledValues,
submitClicked,
currentValue: inputs.map((input) => input.value).join(''),
};
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.deepStrictEqual(result, { success: true });
assert.equal(snapshot.currentValue, '123456');
assert.equal(snapshot.submitClicked, true);
assert.deepStrictEqual(snapshot.clicks, ['Continue']);
});
+151
View File
@@ -0,0 +1,151 @@
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 i = start; i < source.length; i += 1) {
const ch = source[i];
if (ch === '(') {
parenDepth += 1;
} else if (ch === ')') {
parenDepth -= 1;
if (parenDepth === 0) {
signatureEnded = true;
}
} else if (ch === '{' && signatureEnded) {
braceStart = i;
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 ch = source[end];
if (ch === '{') depth += 1;
if (ch === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return source.slice(start, end);
}
test('waitForVerificationSubmitOutcome recovers signup retry page after submit', async () => {
const api = new Function(`
let retryVisible = true;
let step5Ready = false;
let recoverCalls = 0;
const location = { href: 'https://auth.openai.com/email-verification' };
function throwIfStopped() {}
function log() {}
function getVerificationErrorText() { return ''; }
function isStep5Ready() { return step5Ready; }
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' && retryVisible) {
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
};
}
return null;
}
async function recoverCurrentAuthRetryPage() {
recoverCalls += 1;
retryVisible = false;
step5Ready = true;
}
async function sleep() {}
${extractFunction('waitForVerificationSubmitOutcome')}
return {
run() {
return waitForVerificationSubmitOutcome(4, 1000);
},
snapshot() {
return { recoverCalls };
},
};
`)();
const result = await api.run();
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/
);
});
+44 -19
View File
@@ -51,10 +51,8 @@ function extractFunction(name) {
return source.slice(start, end);
}
test('step 8 click effect returns restart_current_step when retry page is recovered', async () => {
test('step 8 click effect throws when retry page appears after clicking continue', async () => {
const api = new Function(`
let recoverCalls = 0;
const chrome = {
tabs: {
async get() {
@@ -69,10 +67,6 @@ const chrome = {
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',
@@ -89,20 +83,51 @@ return {
async run() {
return waitForStep8ClickEffect(88, 'https://auth.openai.com/authorize', 1000);
},
snapshot() {
return { recoverCalls };
};
`)();
await assert.rejects(
() => api.run(),
/点击“继续”后页面进入认证页重试页/
);
});
test('step 8 ready check throws when consent page is already a retry page before clicking', async () => {
const api = new Function(`
const chrome = {
tabs: {
async get() {
return {
id: 88,
url: 'https://auth.openai.com/authorize',
};
},
},
};
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
retryPage: true,
addPhonePage: false,
consentReady: false,
};
}
${extractFunction('waitForStep8Ready')}
return {
async run() {
return waitForStep8Ready(88, 1000);
},
};
`)();
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);
await assert.rejects(
() => api.run(),
/当前认证页已进入重试页/
);
});
+114 -5
View File
@@ -58,23 +58,36 @@ const bundle = [
extractFunction('summarizeStatusBadgeEntries'),
extractFunction('normalizeStep9StatusText'),
extractFunction('isOAuthCallbackTimeoutFailure'),
extractFunction('isStep10CallbackSubmittedStatus'),
extractFunction('isStep10CallbackFailureText'),
extractFunction('isStep10MainWaitingStatus'),
extractFunction('isStep10MainFailureText'),
extractFunction('isStep9FailureText'),
extractFunction('isStep9SuccessStatus'),
extractFunction('isStep9SuccessLikeStatus'),
extractFunction('formatStep10StatusSummaryValue'),
extractFunction('isStep10BrowserSwitchRequiredConflict'),
extractFunction('getStep10BrowserSwitchRequiredMessage'),
extractFunction('buildStep9StatusDiagnostics'),
extractFunction('extractStep10FailureDetail'),
extractFunction('explainStep10Failure'),
].join('\n');
function createApi() {
return new Function(`
function isRecoverableStep9AuthFailure(text) {
return /(?:认证失败|回调 URL 提交失败):\\s*/i.test(String(text || '').trim())
|| /oauth flow is not pending/i.test(String(text || '').trim());
const normalized = String(text || '').trim();
return /(?:认证失败|回调\\s*url\\s*提交失败|回调url提交失败|提交回调失败)\\s*[:]?/i.test(normalized)
|| /oauth flow is not pending|请更新\\s*cli\\s*proxy\\s*api\\s*或检查连接|bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(normalized);
}
${bundle}
return {
buildStep9StatusDiagnostics,
explainStep10Failure,
isStep10BrowserSwitchRequiredConflict,
getStep10BrowserSwitchRequiredMessage,
};
`)();
}
@@ -86,6 +99,7 @@ test('step 9 does not treat red success badges as exact success', () => {
visible: true,
text: '认证成功!',
className: 'status-badge text-danger',
location: 'main',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
@@ -104,23 +118,118 @@ test('step 9 keeps failure state dominant when success badge and error banner co
visible: true,
text: '认证成功!',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[
{
visible: true,
text: '回调 URL 提交失败: oauth flow is not pending',
className: 'alert alert-danger',
className: 'status-badge error',
location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
[],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.hasFailureVisibleBadge, true);
assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
assert.equal(diagnostics.failureSource, 'callback');
});
test('step 10 treats plain 认证成功 as success when no failure is visible', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.exactSuccessText, '认证成功');
});
test('step 10 recognizes callback accepted badge as in-progress signal', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '回调 URL 已提交,等待认证中...',
className: 'status-badge success',
location: 'callback',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
{
visible: true,
text: '等待认证中...',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[],
'page'
);
assert.equal(diagnostics.hasCallbackSubmittedBadge, true);
assert.equal(diagnostics.callbackSubmittedText, '回调 URL 已提交,等待认证中...');
assert.equal(diagnostics.mainWaitingText, '等待认证中...');
assert.equal(diagnostics.hasExactSuccessVisibleBadge, false);
});
test('step 10 explains callback upgrade hint with user-friendly reason', () => {
const api = createApi();
const explanation = api.explainStep10Failure(
'回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
'callback'
);
assert.equal(explanation.code, 'callback_submit_api_unavailable');
assert.match(explanation.userMessage, /CLI Proxy API 版本过旧|管理接口未启动|连接异常/);
assert.match(explanation.userMessage, /回调提交阶段/);
});
test('step 10 requests browser switch when success badge and callback upgrade failure coexist', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功',
className: 'status-badge success',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
{
visible: true,
text: '回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
className: 'status-badge error',
location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
[],
'page'
);
assert.equal(api.isStep10BrowserSwitchRequiredConflict(diagnostics), true);
assert.match(api.getStep10BrowserSwitchRequiredMessage(diagnostics), /更换浏览器后重新进行注册登录/);
});
+43
View File
@@ -43,6 +43,49 @@ test('verification flow keeps 2925 polling cadence in the default payload', () =
assert.equal(step8Payload.intervalMs, 15000);
});
test('verification flow only enables 2925 target email matching in receive mode', () => {
const helpers = api.createVerificationFlowHelpers({
addLog: async () => {},
chrome: { tabs: { update: async () => {} } },
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
getHotmailVerificationPollConfig: () => ({}),
getHotmailVerificationRequestTimestamp: () => 0,
getState: async () => ({}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isStopError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
pollCloudflareTempEmailVerificationCode: async () => ({}),
pollHotmailVerificationCode: async () => ({}),
pollLuckmailVerificationCode: async () => ({}),
sendToContentScript: async () => ({}),
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const providePayload = helpers.getVerificationPollPayload(4, {
email: 'user@example.com',
mailProvider: '2925',
mail2925Mode: 'provide',
});
const receivePayload = helpers.getVerificationPollPayload(4, {
email: 'user@example.com',
mailProvider: '2925',
mail2925Mode: 'receive',
});
assert.equal(providePayload.mail2925MatchTargetEmail, false);
assert.equal(receivePayload.mail2925MatchTargetEmail, true);
});
test('verification flow runs beforeSubmit hook before filling the code', async () => {
const events = [];