feat: 增强2925邮箱登录流程,添加登录失败处理和超时机制,更新相关文档和测试用例
This commit is contained in:
@@ -145,7 +145,7 @@
|
|||||||
2. 在 `2925 账号池` 中添加 `邮箱 / 密码`
|
2. 在 `2925 账号池` 中添加 `邮箱 / 密码`
|
||||||
3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线
|
3. 在“2925 基邮箱”右侧打开 `号池` 开关后,基邮箱输入框会切成下拉框,只能从 2925 账号池中选择邮箱;关闭开关则回退到原来的手填基邮箱路线
|
||||||
4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态
|
4. 可先点 `使用此账号` 让当前别名基邮箱切到这条 2925 账号,再点 `登录` 手动验证网页邮箱登录态
|
||||||
5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会直接使用当前账号自动登录
|
5. 只有在 `号池` 开关开启时,自动流程执行到 Step 4 / Step 8 前才会自动检查 2925 登录态;如果未登录,会直接使用当前账号自动登录;点击登录后若 `20 秒`内仍未进入收件箱,则会判定当前登录失败
|
||||||
6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程
|
6. 当 Step 4 / Step 8 轮询邮箱时遇到“子邮箱已达上限邮箱”,扩展会记录当前时间;如果还有下一个可用账号,就禁用当前账号 24 小时并自动切换登录;如果没有下一个可用账号,或当前未启用号池模式,则会直接复用现有“手动暂停 / 停止”逻辑终止自动流程
|
||||||
7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入
|
7. 如果你同时开启了 `Auto` 的自动重试,当前尝试结束后会按现有逻辑自动进入下一次尝试,不需要再手动介入
|
||||||
8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com`
|
8. `Mail = 2925` 仍然走 Gmail / 2925 共用的别名邮箱链路;实际注册邮箱会基于当前 2925 账号邮箱生成,例如 `name@2925.com -> name123456@2925.com`
|
||||||
|
|||||||
@@ -5387,6 +5387,7 @@ const mail2925SessionManager = self.MultiPageBackgroundMail2925Session?.createMa
|
|||||||
findMail2925Account,
|
findMail2925Account,
|
||||||
getMail2925AccountStatus,
|
getMail2925AccountStatus,
|
||||||
getState,
|
getState,
|
||||||
|
isAutoRunLockedState,
|
||||||
isMail2925AccountAvailable: self.Mail2925Utils?.isMail2925AccountAvailable,
|
isMail2925AccountAvailable: self.Mail2925Utils?.isMail2925AccountAvailable,
|
||||||
MAIL2925_LIMIT_COOLDOWN_MS,
|
MAIL2925_LIMIT_COOLDOWN_MS,
|
||||||
normalizeMail2925Account,
|
normalizeMail2925Account,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
normalizeMail2925Accounts,
|
normalizeMail2925Accounts,
|
||||||
pickMail2925AccountForRun,
|
pickMail2925AccountForRun,
|
||||||
getState,
|
getState,
|
||||||
|
isAutoRunLockedState,
|
||||||
requestStop,
|
requestStop,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToMailContentScriptResilient,
|
sendToMailContentScriptResilient,
|
||||||
@@ -60,6 +61,25 @@
|
|||||||
return new Error(`${MAIL2925_THREAD_TERMINATED_ERROR_PREFIX}${String(message || '').trim()}`);
|
return new Error(`${MAIL2925_THREAD_TERMINATED_ERROR_PREFIX}${String(message || '').trim()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function stopAutoRunForMail2925LoginFailure(errorMessage = '') {
|
||||||
|
if (typeof requestStop !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await getState();
|
||||||
|
const autoRunning = typeof isAutoRunLockedState === 'function'
|
||||||
|
? isAutoRunLockedState(state)
|
||||||
|
: Boolean(state?.autoRunning);
|
||||||
|
if (!autoRunning) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await requestStop({
|
||||||
|
logMessage: errorMessage || '2925 登录失败,已按手动停止逻辑暂停自动流程。',
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function isMail2925LimitReachedError(error) {
|
function isMail2925LimitReachedError(error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|
return message.startsWith(MAIL2925_LIMIT_ERROR_PREFIX)
|
||||||
@@ -352,7 +372,9 @@
|
|||||||
injectSource: MAIL2925_INJECT_SOURCE,
|
injectSource: MAIL2925_INJECT_SOURCE,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await sendToMailContentScriptResilient(
|
let result;
|
||||||
|
try {
|
||||||
|
result = await sendToMailContentScriptResilient(
|
||||||
getMail2925MailConfig(),
|
getMail2925MailConfig(),
|
||||||
{
|
{
|
||||||
type: 'ENSURE_MAIL2925_SESSION',
|
type: 'ENSURE_MAIL2925_SESSION',
|
||||||
@@ -365,13 +387,38 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timeoutMs: forceRelogin ? 90000 : 45000,
|
timeoutMs: forceRelogin ? 30000 : 25000,
|
||||||
responseTimeoutMs: forceRelogin ? 90000 : 45000,
|
responseTimeoutMs: forceRelogin ? 30000 : 25000,
|
||||||
maxRecoveryAttempts: 2,
|
maxRecoveryAttempts: 2,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
const stopped = await stopAutoRunForMail2925LoginFailure(
|
||||||
|
`2925:${actionLabel}失败(${getErrorMessage(err) || '20 秒内未进入收件箱'}),已按手动停止逻辑暂停自动流程。`
|
||||||
|
);
|
||||||
|
if (stopped) {
|
||||||
|
throw new Error('流程已被用户停止。');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result?.loggedIn) {
|
||||||
|
const stopped = await stopAutoRunForMail2925LoginFailure(
|
||||||
|
`2925:${actionLabel}失败(20 秒内未进入收件箱),已按手动停止逻辑暂停自动流程。`
|
||||||
|
);
|
||||||
|
if (stopped) {
|
||||||
|
throw new Error('流程已被用户停止。');
|
||||||
|
}
|
||||||
|
throw new Error(`2925:${actionLabel}失败,当前页面仍未进入收件箱。`);
|
||||||
|
}
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
|
const stopped = await stopAutoRunForMail2925LoginFailure(
|
||||||
|
`2925:${actionLabel}失败(${result.error}),已按手动停止逻辑暂停自动流程。`
|
||||||
|
);
|
||||||
|
if (stopped) {
|
||||||
|
throw new Error('流程已被用户停止。');
|
||||||
|
}
|
||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
}
|
}
|
||||||
if (result?.limitReached) {
|
if (result?.limitReached) {
|
||||||
|
|||||||
+83
-21
@@ -193,6 +193,12 @@ const MAIL2925_AGREEMENT_PATTERNS = [
|
|||||||
/服务协议/,
|
/服务协议/,
|
||||||
/隐私政策/,
|
/隐私政策/,
|
||||||
];
|
];
|
||||||
|
const MAIL2925_REMEMBER_LOGIN_PATTERNS = [
|
||||||
|
/30天内免登录/,
|
||||||
|
/免登录/,
|
||||||
|
/记住登录/,
|
||||||
|
/保持登录/,
|
||||||
|
];
|
||||||
|
|
||||||
function normalizeNodeText(value) {
|
function normalizeNodeText(value) {
|
||||||
return String(value || '').replace(/\s+/g, ' ').trim();
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
||||||
@@ -405,46 +411,102 @@ function findAgreementContainer() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findAgreementCheckbox() {
|
function isAgreementText(text = '') {
|
||||||
const agreementContainer = findAgreementContainer();
|
const normalizedText = normalizeNodeText(text);
|
||||||
if (agreementContainer) {
|
if (!normalizedText || MAIL2925_REMEMBER_LOGIN_PATTERNS.some((pattern) => pattern.test(normalizedText))) {
|
||||||
const checkbox = agreementContainer.querySelector('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
return false;
|
||||||
if (checkbox) {
|
}
|
||||||
return resolveActionTarget(checkbox);
|
|
||||||
}
|
return /我已阅读并同意/.test(normalizedText)
|
||||||
const nearbyCheckbox = agreementContainer.parentElement?.querySelector?.('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
|| (/服务协议/.test(normalizedText) && /隐私政策/.test(normalizedText));
|
||||||
if (nearbyCheckbox) {
|
}
|
||||||
return resolveActionTarget(nearbyCheckbox);
|
|
||||||
|
function getCheckboxContextText(target) {
|
||||||
|
if (!target) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const textParts = [];
|
||||||
|
const candidates = [
|
||||||
|
target,
|
||||||
|
target.closest?.('label'),
|
||||||
|
target.parentElement,
|
||||||
|
target.parentElement?.parentElement,
|
||||||
|
target.nextElementSibling,
|
||||||
|
target.previousElementSibling,
|
||||||
|
target.closest?.('label, div, span, p, li, form'),
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const text = normalizeNodeText(candidate.innerText || candidate.textContent || '');
|
||||||
|
if (text) {
|
||||||
|
textParts.push(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return normalizeNodeText(textParts.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAgreementCheckbox() {
|
||||||
const genericCheckboxes = document.querySelectorAll('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
const genericCheckboxes = document.querySelectorAll('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
||||||
for (const checkbox of genericCheckboxes) {
|
for (const checkbox of genericCheckboxes) {
|
||||||
const target = resolveActionTarget(checkbox);
|
const target = resolveActionTarget(checkbox);
|
||||||
if (!isVisibleNode(target)) {
|
if (!isVisibleNode(target)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const wrapper = target.closest('label, div, span') || target.parentElement || target;
|
|
||||||
const text = normalizeNodeText(wrapper?.innerText || wrapper?.textContent || '');
|
const contextText = getCheckboxContextText(target);
|
||||||
if (/我已阅读并同意/.test(text) || /服务协议/.test(text) || /隐私政策/.test(text)) {
|
if (MAIL2925_REMEMBER_LOGIN_PATTERNS.some((pattern) => pattern.test(contextText))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isAgreementText(contextText)) {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agreementContainer = findAgreementContainer();
|
||||||
|
if (agreementContainer) {
|
||||||
|
const checkbox = agreementContainer.querySelector('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
||||||
|
if (checkbox) {
|
||||||
|
const target = resolveActionTarget(checkbox);
|
||||||
|
const contextText = getCheckboxContextText(target);
|
||||||
|
if (!MAIL2925_REMEMBER_LOGIN_PATTERNS.some((pattern) => pattern.test(contextText))) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nearbyCheckbox = agreementContainer.parentElement?.querySelector?.('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox');
|
||||||
|
if (nearbyCheckbox) {
|
||||||
|
const target = resolveActionTarget(nearbyCheckbox);
|
||||||
|
const contextText = getCheckboxContextText(target);
|
||||||
|
if (!MAIL2925_REMEMBER_LOGIN_PATTERNS.some((pattern) => pattern.test(contextText))) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureAgreementChecked() {
|
async function ensureAgreementChecked() {
|
||||||
const agreementCheckbox = findAgreementCheckbox();
|
const checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"], [role="checkbox"], .ivu-checkbox, .el-checkbox'))
|
||||||
if (!agreementCheckbox) {
|
.map((checkbox) => resolveActionTarget(checkbox))
|
||||||
|
.filter((target, index, list) => target && isVisibleNode(target) && list.indexOf(target) === index);
|
||||||
|
|
||||||
|
if (!checkboxes.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isCheckboxChecked(agreementCheckbox)) {
|
|
||||||
return true;
|
let changed = false;
|
||||||
|
for (const checkbox of checkboxes) {
|
||||||
|
if (isCheckboxChecked(checkbox)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
simulateClick(checkbox);
|
||||||
|
changed = true;
|
||||||
|
await sleep(120);
|
||||||
}
|
}
|
||||||
simulateClick(agreementCheckbox);
|
|
||||||
await sleep(150);
|
return changed || checkboxes.every((checkbox) => isCheckboxChecked(checkbox));
|
||||||
return isCheckboxChecked(agreementCheckbox);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectMail2925ViewState() {
|
function detectMail2925ViewState() {
|
||||||
@@ -832,7 +894,7 @@ async function ensureMail2925Session(payload = {}) {
|
|||||||
await sleep(200);
|
await sleep(200);
|
||||||
simulateClick(loginButton);
|
simulateClick(loginButton);
|
||||||
|
|
||||||
const finalState = await waitForMail2925View('mailbox', 60000);
|
const finalState = await waitForMail2925View('mailbox', 20000);
|
||||||
if (finalState.view !== 'mailbox') {
|
if (finalState.view !== 'mailbox') {
|
||||||
throw new Error('2925:提交账号密码后未进入收件箱。');
|
throw new Error('2925:提交账号密码后未进入收件箱。');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,3 +159,69 @@ test('handleMail2925LimitReachedError requests stop when no next mail2925 accoun
|
|||||||
assert.equal(events.stopCalls.length, 1);
|
assert.equal(events.stopCalls.length, 1);
|
||||||
assert.match(events.stopCalls[0].logMessage, /没有可切换的下一个账号/);
|
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 秒内未进入收件箱/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ const fs = require('node:fs');
|
|||||||
|
|
||||||
const source = fs.readFileSync('content/mail-2925.js', 'utf8');
|
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) {
|
function extractFunction(name) {
|
||||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
const start = markers
|
const start = markers
|
||||||
@@ -488,3 +492,187 @@ return {
|
|||||||
assert.equal(result, true);
|
assert.equal(result, true);
|
||||||
assert.deepEqual(api.getCalls(), ['inbox', 'select-all', 'delete']);
|
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);
|
||||||
|
});
|
||||||
|
|||||||
+3
-2
@@ -526,9 +526,10 @@
|
|||||||
|
|
||||||
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
|
1. 用户在 sidepanel 的 2925 账号池中保存 `email / password`
|
||||||
2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
|
2. 只有当 sidepanel 中的 `mail2925UseAccountPool` 开关开启时,别名基邮箱才会优先取当前账号池选中的 2925 账号邮箱;关闭时会回退到原来的手填 `mail2925BaseEmail`
|
||||||
3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱
|
3. 手动点击 `登录` 或自动流程进入 Step 4 / Step 8 前,只有在号池模式开启时,后台才会确保当前 2925 账号已登录网页邮箱;登录动作提交后,内容脚本会在 20 秒内轮询“是否已进入收件箱”
|
||||||
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录
|
4. 一旦轮询期间出现“子邮箱已达上限邮箱”,后台会先记录当前时间;若当前处于号池模式且还有下一个可用账号,则把当前账号禁用 24 小时并自动切到下一个账号重新登录
|
||||||
5. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
|
5. 如果登录提交后 20 秒内仍未进入收件箱,且当前正处于自动运行中,则后台会直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态;这类情况常见于图片验证、行为验证或其他阻断登录的中间页
|
||||||
|
6. 如果没有下一个可用账号,或当前未启用号池模式,则不会继续消耗自动重试次数,而是直接复用现有 `requestStop()` 停止链路,把整个自动流程停成和用户手动点击“停止”一致的状态
|
||||||
|
|
||||||
### 7.5 iCloud
|
### 7.5 iCloud
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user