Merge branch 'dev'

This commit is contained in:
QLHazyCoder
2026-04-21 02:20:39 +08:00
31 changed files with 3504 additions and 110 deletions
@@ -0,0 +1,183 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const source = fs.readFileSync('background.js', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
.map((marker) => source.indexOf(marker))
.find((index) => index >= 0);
if (start < 0) {
throw new Error(`missing function ${name}`);
}
let parenDepth = 0;
let signatureEnded = false;
let braceStart = -1;
for (let 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);
}
const bundle = [
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isMail2925ThreadTerminatedError'),
extractFunction('isSignupUserAlreadyExistsFailure'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
].join('\n');
test('auto-run stops step4 restart path when mail2925 ends the current thread', async () => {
const api = new Function(`
const AUTO_STEP_DELAYS = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0 };
const LAST_STEP_ID = 10;
const FINAL_OAUTH_CHAIN_START_STEP = 7;
const chrome = {
tabs: {
update: async () => {},
},
runtime: {
sendMessage: async () => {},
},
};
let currentState = {
email: 'demo123456@2925.com',
password: 'Secret123!',
mailProvider: '2925',
stepStatuses: {
1: 'pending',
2: 'pending',
3: 'pending',
4: 'pending',
5: 'pending',
6: 'pending',
7: 'pending',
8: 'pending',
9: 'pending',
10: 'pending',
},
};
const events = {
steps: [],
invalidations: [],
logs: [],
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function ensureAutoEmailReady() {
return currentState.email;
}
async function broadcastAutoRunStatus() {}
async function getState() {
return currentState;
}
async function setState(updates) {
currentState = {
...currentState,
...updates,
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
};
}
function isStopError(error) {
return false;
}
function isStepDoneStatus(status) {
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
}
async function executeStepAndWait(step) {
events.steps.push(step);
if (step === 4) {
throw new Error('MAIL2925_THREAD_TERMINATED::步骤 42925 已切换账号并要求结束当前尝试。');
}
}
async function getTabId() {
return 1;
}
async function invalidateDownstreamAfterStepRestart(step, options = {}) {
events.invalidations.push({ step, options });
}
function getLoginAuthStateLabel(state) {
return state || 'unknown';
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function getLoginAuthStateFromContent() {
return { state: 'password_page', url: 'https://auth.openai.com/log-in' };
}
${bundle}
return {
async run() {
try {
await runAutoSequenceFromStep(1, {
targetRun: 1,
totalRuns: 1,
attemptRuns: 1,
continued: false,
});
return { events, currentState, error: null };
} catch (error) {
return { events, currentState, error: error.message };
}
},
};
`)();
const result = await api.run();
assert.match(result.error, /^MAIL2925_THREAD_TERMINATED::/);
assert.deepStrictEqual(result.events.invalidations, []);
assert.deepStrictEqual(result.events.steps, [1, 2, 3, 4]);
assert.equal(result.events.logs.some(({ message }) => /回到步骤 1/.test(message)), false);
});
+1
View File
@@ -55,6 +55,7 @@ function extractFunction(name) {
const bundle = [
extractFunction('isAddPhoneAuthUrl'),
extractFunction('isAddPhoneAuthState'),
extractFunction('isMail2925ThreadTerminatedError'),
extractFunction('isSignupUserAlreadyExistsFailure'),
extractFunction('getPostStep6AutoRestartDecision'),
extractFunction('runAutoSequenceFromStep'),
@@ -0,0 +1,66 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const mail2925Utils = require('../mail2925-utils.js');
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 = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
autoRunning: false,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
const events = {
sleeps: [],
};
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,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
requestStop: async () => {},
reuseOrCreateTab: async () => 1,
sendToMailContentScriptResilient: async () => ({ loggedIn: true }),
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async (ms) => {
events.sleeps.push(ms);
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: true,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.deepStrictEqual(events.sleeps, [3000, 3000]);
});
@@ -0,0 +1,227 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const mail2925Utils = require('../mail2925-utils.js');
test('background mail2925 session module exposes a factory', () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
assert.equal(typeof api?.createMail2925SessionManager, 'function');
});
test('handleMail2925LimitReachedError disables current account and switches to the next one', 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: '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 = {
logs: [],
dataUpdates: [],
tabOpens: 0,
sessionChecks: 0,
};
const manager = api.createMail2925SessionManager({
addLog: async (message, level = 'info') => {
events.logs.push({ message, level });
},
broadcastDataUpdate: (payload) => {
events.dataUpdates.push(payload);
},
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,
reuseOrCreateTab: async () => {
events.tabOpens += 1;
return 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.match(error.message, /^MAIL2925_THREAD_TERMINATED::/);
assert.equal(currentState.currentMail2925AccountId, 'next');
assert.ok(events.tabOpens >= 1);
assert.ok(events.sessionChecks >= 1);
const currentAccount = currentState.mail2925Accounts.find((account) => account.id === 'current');
assert.equal(currentAccount.lastError, '子邮箱已达上限邮箱');
assert.ok(currentAccount.disabledUntil > Date.now());
});
test('handleMail2925LimitReachedError requests stop when no next mail2925 account is available', 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: 'only', email: 'only@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'only',
};
const events = {
stopCalls: [],
};
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 () => ({ 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(currentState.currentMail2925AccountId, null);
assert.equal(events.stopCalls.length, 1);
assert.match(events.stopCalls[0].logMessage, /没有可切换的下一个账号/);
});
test('ensureMail2925MailboxSession requests stop when auto run is active and login does not reach mailbox', 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 = {
autoRunning: true,
autoRunPhase: 'running',
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
const events = {
stopCalls: [],
};
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,
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 = {}) => {
events.stopCalls.push(options);
},
reuseOrCreateTab: async () => 1,
sendToMailContentScriptResilient: async () => ({ loggedIn: false }),
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: true,
actionLabel: '步骤 4:确认 2925 邮箱登录态',
}),
/流程已被用户停止。/
);
assert.equal(events.stopCalls.length, 1);
assert.match(events.stopCalls[0].logMessage, /20 秒内未进入收件箱/);
});
@@ -0,0 +1,103 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const signupFlowSource = fs.readFileSync('background/signup-flow-helpers.js', 'utf8');
const signupFlowGlobalScope = {};
const signupFlowApi = new Function('self', `${signupFlowSource}; return self.MultiPageSignupFlowHelpers;`)(signupFlowGlobalScope);
test('signup flow helper allocates mail2925 account before generating alias email', async () => {
const calls = {
ensureMail2925: [],
buildAlias: 0,
setEmail: [],
};
const helpers = signupFlowApi.createSignupFlowHelpers({
buildGeneratedAliasEmail: (state) => {
calls.buildAlias += 1;
assert.equal(state.currentMail2925AccountId, 'acc-2');
return 'demo123456@2925.com';
},
chrome: { tabs: { get: async () => ({ id: 1, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async () => {},
ensureHotmailAccountForFlow: async () => ({}),
ensureMail2925AccountForFlow: async (options) => {
calls.ensureMail2925.push(options);
return { id: 'acc-2', email: 'demo@2925.com' };
},
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => true,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
reuseOrCreateTab: async () => 1,
sendToContentScriptResilient: async () => ({}),
setEmailState: async (email) => {
calls.setEmail.push(email);
},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabUrlMatch: async () => null,
});
const email = await helpers.resolveSignupEmailForFlow({
mailProvider: '2925',
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-2',
email: '',
});
assert.equal(email, 'demo123456@2925.com');
assert.deepStrictEqual(calls.ensureMail2925, [
{
allowAllocate: true,
preferredAccountId: 'acc-2',
markUsed: true,
},
]);
assert.equal(calls.buildAlias, 1);
assert.deepStrictEqual(calls.setEmail, ['demo123456@2925.com']);
});
test('signup flow helper skips mail2925 account allocation when account pool switch is off', async () => {
const calls = {
ensureMail2925: 0,
};
const helpers = signupFlowApi.createSignupFlowHelpers({
buildGeneratedAliasEmail: () => 'manual123456@2925.com',
chrome: { tabs: { get: async () => ({ id: 1, url: 'https://auth.openai.com/create-account/password' }) } },
ensureContentScriptReadyOnTab: async () => {},
ensureHotmailAccountForFlow: async () => ({}),
ensureMail2925AccountForFlow: async () => {
calls.ensureMail2925 += 1;
return { id: 'acc-2', email: 'demo@2925.com' };
},
ensureLuckmailPurchaseForFlow: async () => ({}),
isGeneratedAliasProvider: () => true,
isReusableGeneratedAliasEmail: () => false,
isHotmailProvider: () => false,
isLuckmailProvider: () => false,
isSignupEmailVerificationPageUrl: () => false,
isSignupPasswordPageUrl: () => true,
reuseOrCreateTab: async () => 1,
sendToContentScriptResilient: async () => ({}),
setEmailState: async () => {},
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
SIGNUP_PAGE_INJECT_FILES: [],
waitForTabUrlMatch: async () => null,
});
const email = await helpers.resolveSignupEmailForFlow({
mailProvider: '2925',
mail2925UseAccountPool: false,
currentMail2925AccountId: 'acc-2',
email: '',
});
assert.equal(email, 'manual123456@2925.com');
assert.equal(calls.ensureMail2925, 0);
});
+188
View File
@@ -4,6 +4,10 @@ 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\)/);
});
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
@@ -488,3 +492,187 @@ return {
assert.equal(result, true);
assert.deepEqual(api.getCalls(), ['inbox', 'select-all', 'delete']);
});
test('findAgreementCheckbox skips 30-day login checkbox and picks agreement checkbox', async () => {
const bundle = [
extractFunction('normalizeNodeText'),
extractFunction('isVisibleNode'),
extractFunction('resolveActionTarget'),
extractFunction('findAgreementContainer'),
extractFunction('isAgreementText'),
extractFunction('getCheckboxContextText'),
extractFunction('findAgreementCheckbox'),
].join('\n');
const api = new Function(`
const MAIL2925_REMEMBER_LOGIN_PATTERNS = [
/30天内免登录/,
/免登录/,
/记住登录/,
/保持登录/,
];
const MAIL2925_AGREEMENT_PATTERNS = [
/我已阅读并同意/,
/服务协议/,
/隐私政策/,
];
const rememberCheckbox = {
kind: 'remember-checkbox',
disabled: false,
readOnly: false,
hidden: false,
classList: { contains() { return false; } },
getBoundingClientRect() { return { width: 14, height: 14 }; },
closest(selector) {
if (selector === 'button, [role="button"], a, label, .el-checkbox, .el-checkbox__input') return this;
if (selector === 'label') return rememberLabel;
if (selector === 'label, div, span, p, li, form') return rememberLabel;
return null;
},
};
const agreementCheckbox = {
kind: 'agreement-checkbox',
disabled: false,
readOnly: false,
hidden: false,
classList: { contains() { return false; } },
getBoundingClientRect() { return { width: 14, height: 14 }; },
closest(selector) {
if (selector === 'button, [role="button"], a, label, .el-checkbox, .el-checkbox__input') return this;
if (selector === 'label') return agreementLabel;
if (selector === 'label, div, span, p, li, form') return agreementLabel;
return null;
},
};
const rememberLabel = {
innerText: '30天内免登录',
textContent: '30天内免登录',
hidden: false,
getBoundingClientRect() { return { width: 100, height: 20 }; },
parentElement: null,
};
const agreementLabel = {
innerText: '我已阅读并同意 《服务协议》 和 《隐私政策》',
textContent: '我已阅读并同意 《服务协议》 和 《隐私政策》',
hidden: false,
getBoundingClientRect() { return { width: 220, height: 24 }; },
parentElement: null,
querySelector(selector) {
return selector.includes('checkbox') ? agreementCheckbox : null;
},
};
rememberCheckbox.parentElement = rememberLabel;
agreementCheckbox.parentElement = agreementLabel;
const document = {
querySelectorAll(selector) {
if (selector === 'label, div, span, p, form') {
return [rememberLabel, agreementLabel];
}
if (selector === 'input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox') {
return [rememberCheckbox, agreementCheckbox];
}
return [];
},
};
const window = {
getComputedStyle() {
return { display: 'block', visibility: 'visible' };
},
};
${bundle}
return {
findAgreementCheckbox,
rememberCheckbox,
agreementCheckbox,
};
`)();
assert.equal(api.findAgreementCheckbox(), api.agreementCheckbox);
});
test('ensureAgreementChecked clicks all visible login checkboxes', async () => {
const bundle = [
extractFunction('isVisibleNode'),
extractFunction('resolveActionTarget'),
extractFunction('isCheckboxChecked'),
extractFunction('ensureAgreementChecked'),
].join('\n');
const api = new Function(`
const rememberCheckbox = {
disabled: false,
readOnly: false,
hidden: false,
checked: false,
classList: { contains() { return false; } },
getBoundingClientRect() { return { width: 14, height: 14 }; },
click() { this.checked = true; },
closest(selector) {
if (selector === 'button, [role="button"], a, label, .el-checkbox, .el-checkbox__input') return this;
return null;
},
querySelector() { return null; },
getAttribute(name) {
if (name === 'aria-checked') return this.checked ? 'true' : 'false';
return '';
},
};
const agreementCheckbox = {
disabled: false,
readOnly: false,
hidden: false,
checked: false,
classList: { contains() { return false; } },
getBoundingClientRect() { return { width: 14, height: 14 }; },
click() { this.checked = true; },
closest(selector) {
if (selector === 'button, [role="button"], a, label, .el-checkbox, .el-checkbox__input') return this;
return null;
},
querySelector() { return null; },
getAttribute(name) {
if (name === 'aria-checked') return this.checked ? 'true' : 'false';
return '';
},
};
const document = {
querySelectorAll(selector) {
if (selector === 'input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox') {
return [rememberCheckbox, agreementCheckbox];
}
return [];
},
};
const window = {
getComputedStyle() {
return { display: 'block', visibility: 'visible' };
},
};
async function sleep() {}
function simulateClick(node) {
node.click();
}
${bundle}
return {
rememberCheckbox,
agreementCheckbox,
ensureAgreementChecked,
};
`)();
const result = await api.ensureAgreementChecked();
assert.equal(result, true);
assert.equal(api.rememberCheckbox.checked, true);
assert.equal(api.agreementCheckbox.checked, true);
});
+62
View File
@@ -0,0 +1,62 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const utils = require('../mail2925-utils.js');
test('normalizeMail2925Account normalizes key fields', () => {
const account = utils.normalizeMail2925Account({
id: 'a-1',
email: ' Demo@2925.com ',
password: 'secret',
enabled: 0,
lastUsedAt: '123',
disabledUntil: '456',
});
assert.deepStrictEqual(account, {
id: 'a-1',
email: 'demo@2925.com',
password: 'secret',
enabled: false,
lastUsedAt: 123,
lastLoginAt: 0,
lastLimitAt: 0,
disabledUntil: 456,
lastError: '',
});
});
test('pickMail2925AccountForRun skips cooldown and picks the least recently used account', () => {
const now = 1000;
const picked = utils.pickMail2925AccountForRun([
{ id: 'cooldown', email: 'cool@2925.com', password: 'x', disabledUntil: now + 10, lastUsedAt: 1 },
{ id: 'b', email: 'b@2925.com', password: 'x', lastUsedAt: 50 },
{ id: 'a', email: 'a@2925.com', password: 'x', lastUsedAt: 10 },
], { now });
assert.equal(picked.id, 'a');
});
test('getMail2925AccountStatus reports cooldown before error', () => {
const status = utils.getMail2925AccountStatus({
email: 'demo@2925.com',
password: 'secret',
enabled: true,
disabledUntil: Date.now() + 60_000,
lastError: '子邮箱已达上限邮箱',
});
assert.equal(status, 'cooldown');
});
test('parseMail2925ImportText parses 邮箱----密码 rows', () => {
const parsed = utils.parseMail2925ImportText(`
邮箱----密码
demo1@2925.com----pass1
demo2@2925.com----pass2
`);
assert.deepStrictEqual(parsed, [
{ email: 'demo1@2925.com', password: 'pass1' },
{ email: 'demo2@2925.com', password: 'pass2' },
]);
});
@@ -8,6 +8,7 @@ test('sidepanel html keeps a single contribution mode button in header', () => {
assert.equal(matches.length, 1);
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式"/);
assert.match(html, />贡献\/使用<\/button>/);
});
test('sidepanel source no longer keeps the legacy upload-page handler on the header contribution button', () => {
+81
View File
@@ -0,0 +1,81 @@
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', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(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 localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
windowObject,
localStorageMock
);
const mail2925AccountsList = { innerHTML: '', addEventListener() {} };
const manager = api.createMail2925Manager({
state: {
getLatestState: () => ({
currentMail2925AccountId: 'acc-1',
mail2925Accounts: [{
id: 'acc-1',
email: 'demo@2925.com',
password: 'secret',
enabled: true,
lastLoginAt: 0,
lastUsedAt: 0,
lastLimitAt: 0,
disabledUntil: 0,
lastError: '',
}],
}),
syncLatestState() {},
},
dom: {
btnAddMail2925Account: { textContent: '', disabled: false, addEventListener() {} },
btnCancelMail2925Edit: { style: { display: 'none' }, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925List: { textContent: '', disabled: false, setAttribute() {}, addEventListener() {} },
inputMail2925Email: { value: '' },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList,
mail2925ListShell: { classList: { toggle() {} } },
},
helpers: {
getMail2925Accounts: (state) => state.mail2925Accounts || [],
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: {
getMail2925AccountStatus: () => 'ready',
getMail2925ListToggleLabel: () => '展开列表(1',
upsertMail2925AccountInList: (_accounts, nextAccount) => [nextAccount],
},
});
manager.renderMail2925Accounts();
assert.match(mail2925AccountsList.innerHTML, /data-account-action="edit"/);
});
+90
View File
@@ -0,0 +1,90 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
test('sidepanel loads mail2925 manager before sidepanel bootstrap', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const managerIndex = html.indexOf('<script src="mail-2925-manager.js"></script>');
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.notEqual(managerIndex, -1);
assert.notEqual(sidepanelIndex, -1);
assert.ok(managerIndex < sidepanelIndex);
});
test('sidepanel html contains mail2925 pool toggle and selector controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
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"/);
});
test('mail2925 manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {};
const localStorageMock = {
getItem() {
return null;
},
setItem() {},
};
const api = new Function('window', 'localStorage', `${source}; return window.SidepanelMail2925Manager;`)(
windowObject,
localStorageMock
);
assert.equal(typeof api?.createMail2925Manager, 'function');
const mail2925AccountsList = { innerHTML: '', addEventListener() {} };
const toggleButton = {
textContent: '',
disabled: false,
setAttribute() {},
addEventListener() {},
};
const noopClassList = { toggle() {} };
const manager = api.createMail2925Manager({
state: {
getLatestState: () => ({ currentMail2925AccountId: null, mail2925Accounts: [] }),
syncLatestState() {},
},
dom: {
btnAddMail2925Account: { disabled: false, addEventListener() {} },
btnDeleteAllMail2925Accounts: { textContent: '', disabled: false, addEventListener() {} },
btnImportMail2925Accounts: { disabled: false, addEventListener() {} },
btnToggleMail2925List: toggleButton,
inputMail2925Email: { value: '' },
inputMail2925Import: { value: '' },
inputMail2925Password: { value: '' },
mail2925AccountsList,
mail2925ListShell: { classList: noopClassList },
},
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: {},
});
assert.equal(typeof manager.renderMail2925Accounts, 'function');
assert.equal(typeof manager.bindMail2925Events, 'function');
assert.equal(typeof manager.initMail2925ListExpandedState, 'function');
manager.renderMail2925Accounts();
assert.match(mail2925AccountsList.innerHTML, /还没有 2925 账号/);
});