fix(mail2925): 修复验证码轮询与登录态复用逻辑并清理中文文案,重构 sidepanel:统一 Hotmail 和 2925 号池表单交互

- 抽出共享 account-pool-ui helper
- 统一添加账号、取消添加、批量导入交互
- 调整号池列表收起高度与按钮布局
- 补充测试并同步更新项目文档
This commit is contained in:
QLHazyCoder
2026-04-21 18:02:59 +08:00
parent 9228c7080e
commit 7c20405685
22 changed files with 1014 additions and 341 deletions
@@ -143,3 +143,32 @@ return { getMailConfig };
navigateOnReuse: true,
});
});
test('getMailConfig keeps provider metadata for 2925 mailboxes', () => {
const bundle = extractFunction('getMailConfig');
const api = new Function(`
const ICLOUD_PROVIDER = 'icloud';
const GMAIL_PROVIDER = 'gmail';
const HOTMAIL_PROVIDER = 'hotmail-api';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
function normalizeIcloudHost(value = '') { return String(value || '').trim().toLowerCase(); }
function normalizeInbucketOrigin(value) { return String(value || '').trim(); }
function getConfiguredIcloudHostPreference() { return ''; }
function getIcloudLoginUrlForHost(host) { return host; }
function getIcloudMailUrlForHost(host) { return host; }
${bundle}
return { getMailConfig };
`)();
assert.deepEqual(api.getMailConfig({
mailProvider: '2925',
}), {
provider: '2925',
source: 'mail-2925',
url: 'https://2925.com/#/mailList',
label: '2925 邮箱',
inject: ['content/utils.js', 'content/mail-2925.js'],
injectSource: 'mail-2925',
});
});
@@ -0,0 +1,207 @@
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 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);
});
@@ -78,5 +78,5 @@ test('ensureMail2925MailboxSession waits 3 seconds before and after opening logi
assert.deepStrictEqual(events.openedUrls, ['https://2925.com/login/']);
assert.deepStrictEqual(events.sleeps, [3000, 3000]);
assert.equal(events.readyCalls, 1);
assert.equal(events.readyCalls, 2);
});
@@ -223,5 +223,5 @@ 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, /登录后仍未进入收件箱/);
});
+18 -4
View File
@@ -8,6 +8,9 @@ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep
test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling', async () => {
let capturedOptions = null;
let ensureCalls = 0;
const tabUpdates = [];
const tabReuses = [];
const realDateNow = Date.now;
Date.now = () => 700000;
@@ -15,12 +18,16 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
update: async (tabId, payload) => {
tabUpdates.push({ tabId, payload });
},
},
},
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {},
ensureMail2925MailboxSession: async () => {
ensureCalls += 1;
},
getMailConfig: () => ({
provider: '2925',
label: '2925 邮箱',
@@ -35,7 +42,9 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
resolveVerificationStep: async (_step, _state, _mail, options) => {
capturedOptions = options;
},
reuseOrCreateTab: async () => {},
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
sendToContentScriptResilient: async () => ({}),
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
@@ -46,12 +55,17 @@ test('step 4 passes a fixed 10-minute lookback window to 2925 mailbox polling',
await executor.executeStep4({
email: 'user@example.com',
password: 'secret',
mail2925UseAccountPool: false,
mail2925UseAccountPool: true,
});
} finally {
Date.now = realDateNow;
}
assert.equal(ensureCalls, 1);
assert.deepStrictEqual(tabReuses, []);
assert.deepStrictEqual(tabUpdates, [
{ tabId: 1, payload: { active: true } },
]);
assert.equal(capturedOptions.filterAfterTimestamp, 100000);
assert.equal(capturedOptions.resendIntervalMs, 0);
});
+19 -2
View File
@@ -91,6 +91,9 @@ test('step 8 submits login verification directly without replaying step 7', asyn
test('step 8 uses a fixed 10-minute lookback window and disables resend interval for 2925 mailbox polling', async () => {
let capturedOptions = null;
let ensureCalls = 0;
const tabUpdates = [];
const tabReuses = [];
const realDateNow = Date.now;
Date.now = () => 900000;
@@ -98,11 +101,16 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
update: async (tabId, payload) => {
tabUpdates.push({ tabId, payload });
},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureMail2925MailboxSession: async () => {
ensureCalls += 1;
},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
@@ -123,7 +131,9 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
resolveVerificationStep: async (_step, _state, _mail, options) => {
capturedOptions = options;
},
reuseOrCreateTab: async () => {},
reuseOrCreateTab: async (source, url) => {
tabReuses.push({ source, url });
},
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
@@ -137,11 +147,18 @@ test('step 8 uses a fixed 10-minute lookback window and disables resend interval
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
mail2925UseAccountPool: true,
});
} finally {
Date.now = realDateNow;
}
assert.equal(ensureCalls, 0);
assert.deepStrictEqual(tabReuses, []);
assert.deepStrictEqual(tabUpdates, [
{ tabId: 1, payload: { active: true } },
{ tabId: 2, payload: { active: true } },
]);
assert.equal(capturedOptions.filterAfterTimestamp, 300000);
assert.equal(capturedOptions.resendIntervalMs, 0);
assert.equal(capturedOptions.targetEmail, '');
+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 账号/);
});
+46 -1
View File
@@ -2,6 +2,49 @@ 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 html contains collapsible mail2925 form controls', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="btn-toggle-mail2925-form"/);
@@ -11,7 +54,9 @@ test('sidepanel html contains collapsible mail2925 form controls', () => {
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;
+60 -3
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);
});
@@ -22,9 +68,16 @@ test('sidepanel html contains mail2925 pool toggle and selector controls', () =>
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;
@@ -101,7 +154,9 @@ test('mail2925 manager exposes a factory and renders empty state', () => {
test('mail2925 manager toggles form container from header button', () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {};
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;
@@ -179,7 +234,9 @@ test('mail2925 manager toggles form container from header button', () => {
test('mail2925 manager hides form after save succeeds', async () => {
const source = fs.readFileSync('sidepanel/mail-2925-manager.js', 'utf8');
const windowObject = {};
const windowObject = {
SidepanelAccountPoolUi: createAccountPoolUiStub(),
};
const localStorageMock = {
getItem() {
return null;