Merge remote-tracking branch 'origin/dev' into pr-112

This commit is contained in:
Ryan Liu
2026-04-25 11:50:26 +08:00
29 changed files with 1468 additions and 179 deletions
+121
View File
@@ -330,3 +330,124 @@ return {
assert.deepStrictEqual(result.events.steps, [1, 2, 3, 4]);
assert.equal(result.events.logs.some(({ message }) => /沿用当前邮箱回到步骤 1 重新开始/.test(message)), false);
});
test('auto-run skips steps 4/5 when step 2 has already marked registration chain as skipped', 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: 'already@login.example',
password: 'Secret123!',
mailProvider: 'icloud',
stepStatuses: {
1: 'pending',
2: 'pending',
3: 'pending',
4: 'pending',
5: 'pending',
6: 'pending',
7: 'pending',
8: 'pending',
9: 'pending',
10: 'pending',
},
};
const events = {
steps: [],
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 (error?.message || String(error || '')) === '流程已被用户停止。';
}
function isStepDoneStatus(status) {
return status === 'completed' || status === 'manual_completed' || status === 'skipped';
}
async function executeStepAndWait(step) {
events.steps.push(step);
if (step === 2) {
currentState = {
...currentState,
stepStatuses: {
...currentState.stepStatuses,
3: 'skipped',
4: 'skipped',
5: 'skipped',
},
};
}
}
async function getTabId() {
return 1;
}
async function invalidateDownstreamAfterStepRestart() {}
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() {
await runAutoSequenceFromStep(1, {
targetRun: 1,
totalRuns: 1,
attemptRuns: 1,
continued: false,
});
return { events, currentState };
},
};
`)();
const { events } = await api.run();
assert.deepStrictEqual(events.steps, [1, 2, 6, 7, 8, 9, 10]);
assert.equal(events.logs.some(({ message }) => /步骤 4 当前状态为 skipped/.test(message)), true);
assert.equal(events.logs.some(({ message }) => /步骤 5 当前状态为 skipped/.test(message)), true);
});
@@ -361,3 +361,55 @@ test('generated email helper requests random subdomain creation while preserving
domain: 'mail.example.com',
});
});
test('generated email helper honors iCloud always-new fetch mode', async () => {
const api = loadGeneratedEmailHelpersApi();
const icloudOptions = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({}),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async () => ({ ok: true, text: async () => '{}' }),
fetchIcloudHideMyEmail: async (options) => {
icloudOptions.push(options);
return 'fresh@icloud.example.com';
},
getCloudflareTempEmailAddressFromResponse: () => '',
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
getState: async () => ({
emailGenerator: 'icloud',
icloudFetchMode: 'always_new',
mailProvider: 'gmail',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: () => '',
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: () => '',
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async () => {},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'icloud',
icloudFetchMode: 'always_new',
mailProvider: 'gmail',
}, {
generator: 'icloud',
});
assert.equal(email, 'fresh@icloud.example.com');
assert.deepEqual(icloudOptions, [{ generateNew: true }]);
});
+146
View File
@@ -291,6 +291,152 @@ return {
assert.equal(snapshot.buildCalls.length, 1);
});
test('pollLuckmailVerificationCode snapshots existing mails before accepting new LuckMail code', async () => {
const bundle = extractFunction('pollLuckmailVerificationCode');
const factory = new Function(`
let currentState = {
currentLuckmailPurchase: {
id: 7,
email_address: 'luck@example.com',
token: 'tok-luck',
},
currentLuckmailMailCursor: null,
};
const logs = [];
const cursorWrites = [];
let tokenCodeCalls = 0;
function getCurrentLuckmailPurchase(state) {
return state.currentLuckmailPurchase;
}
function createLuckmailClient() {
return {
user: {
async getTokenMails() {
if (tokenCodeCalls === 0) {
return {
mails: [
{ message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
],
};
}
return {
mails: [
{ message_id: 'new-mail', received_at: '2026-04-14 13:32:05', verification_code: '222222' },
{ message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
],
};
},
async getTokenCode() {
tokenCodeCalls += 1;
return tokenCodeCalls === 1
? {
has_new_mail: true,
verification_code: '111111',
mail: { message_id: 'old-mail', received_at: '2026-04-14 13:31:15', verification_code: '111111' },
}
: {
has_new_mail: true,
verification_code: '222222',
mail: { message_id: 'new-mail', received_at: '2026-04-14 13:32:05', verification_code: '222222' },
};
},
async getTokenMailDetail(_token, messageId) {
return { message_id: messageId, received_at: '2026-04-14 13:32:05', verification_code: '222222' };
},
},
};
}
async function getState() {
return currentState;
}
async function setLuckmailMailCursorState(cursor) {
currentState = { ...currentState, currentLuckmailMailCursor: cursor };
cursorWrites.push(cursor);
}
function normalizeLuckmailMailCursor(cursor) {
return {
messageId: cursor?.messageId || cursor?.message_id || '',
receivedAt: cursor?.receivedAt || cursor?.received_at || '',
};
}
function normalizeLuckmailTimestamp(value) {
return Date.parse(String(value || '').replace(' ', 'T') + 'Z') || 0;
}
function buildLuckmailMailCursor(mail) {
return { messageId: mail.message_id || '', receivedAt: mail.received_at || '' };
}
function buildLuckmailBaselineCursor(mails) {
const latest = mails[0] || null;
return latest ? buildLuckmailMailCursor(latest) : null;
}
function isLuckmailMailNewerThanCursor(mail, cursor) {
if (!cursor?.messageId && !cursor?.receivedAt) return true;
if (mail.message_id === cursor.messageId) return false;
return normalizeLuckmailTimestamp(mail.received_at) > normalizeLuckmailTimestamp(cursor.receivedAt);
}
function pickLuckmailVerificationMail(mails, filters) {
const excludeCodes = new Set(filters.excludeCodes || []);
for (const mail of mails || []) {
if (!mail.verification_code || excludeCodes.has(mail.verification_code)) continue;
return { mail, code: mail.verification_code };
}
return null;
}
function normalizeLuckmailTokenCode(result) {
return result;
}
async function resolveLuckmailVerificationMail(client, token, filters, tokenCodeResult) {
if (tokenCodeResult?.mail) {
const inline = pickLuckmailVerificationMail([tokenCodeResult.mail], filters);
if (inline) return inline;
}
const mailList = await client.user.getTokenMails(token);
return pickLuckmailVerificationMail(mailList.mails, filters);
}
async function addLog(message, level) {
logs.push({ message, level });
}
function throwIfStopped() {}
function isStopError() {
return false;
}
async function sleepWithStop() {}
${bundle}
return {
pollLuckmailVerificationCode,
snapshot() {
return { currentState, cursorWrites, logs, tokenCodeCalls };
},
};
`);
const api = factory();
const result = await api.pollLuckmailVerificationCode(4, await api.snapshot().currentState, {
maxAttempts: 2,
intervalMs: 1000,
senderFilters: ['openai'],
subjectFilters: ['code'],
excludeCodes: [],
});
assert.equal(result.code, '222222');
const snapshot = api.snapshot();
assert.deepStrictEqual(snapshot.cursorWrites[0], {
messageId: 'old-mail',
receivedAt: '2026-04-14 13:31:15',
});
assert.deepStrictEqual(snapshot.cursorWrites.at(-1), {
messageId: 'new-mail',
receivedAt: '2026-04-14 13:32:05',
});
assert.equal(snapshot.logs.some((entry) => /已保存当前邮箱旧邮件快照/.test(entry.message)), true);
assert.equal(snapshot.tokenCodeCalls, 2);
});
test('listLuckmailPurchasesByProject only keeps openai purchases', async () => {
const bundle = extractFunction('listLuckmailPurchasesByProject');
@@ -151,6 +151,39 @@ test('message router does not overwrite a completed step 3 when step 2 is replay
assert.deepStrictEqual(events.stepStatuses, []);
});
test('message router skips steps 3/4/5 when step 2 detects already logged-in session', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 3: 'pending', 4: 'completed', 5: 'pending' } },
});
await router.handleStepData(2, {
email: 'user@example.com',
skipRegistrationFlow: true,
skippedPasswordStep: true,
});
assert.deepStrictEqual(events.emailStates, ['user@example.com']);
assert.deepStrictEqual(events.stepStatuses, [
{ step: 3, status: 'skipped' },
{ step: 5, status: 'skipped' },
]);
assert.equal(events.logs[0]?.message, '步骤 2:检测到当前已登录会话,已自动跳过步骤 3/4/5,流程将直接进入步骤 6。');
});
test('message router skips step 5 when step 4 reports already logged-in transition', async () => {
const { router, events } = createRouter({
state: { stepStatuses: { 5: 'pending' } },
});
await router.handleStepData(4, {
emailTimestamp: 123,
skipProfileStep: true,
});
assert.deepStrictEqual(events.stepStatuses, [{ step: 5, status: 'skipped' }]);
assert.equal(events.logs[0]?.message, '步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。');
});
test('message router finalizes step 3 before marking it completed', async () => {
const { router, events } = createRouter();
@@ -84,6 +84,55 @@ test('step 2 keeps password flow when landing on password page', async () => {
]);
});
test('step 2 falls back to already-logged-in branch when auth entry recovery fails on chatgpt home', async () => {
const completedPayloads = [];
const logs = [];
const executor = step2Api.createStep2Executor({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {
tabs: {
update: async () => {},
get: async () => ({ url: 'https://chatgpt.com/' }),
},
},
completeStepFromBackground: async (step, payload) => {
completedPayloads.push({ step, payload });
},
ensureContentScriptReadyOnTab: async () => {},
ensureSignupAuthEntryPageReady: async () => {
throw new Error('当前页面没有可用的注册入口,也不在邮箱/密码页。URL: https://chatgpt.com/');
},
ensureSignupEntryPageReady: async () => ({ tabId: 13 }),
ensureSignupPostEmailPageReadyInTab: async () => ({
state: 'password_page',
url: 'https://auth.openai.com/create-account/password',
}),
getTabId: async () => 13,
isTabAlive: async () => true,
resolveSignupEmailForFlow: async () => 'user@example.com',
sendToContentScriptResilient: async () => ({ submitted: true }),
SIGNUP_PAGE_INJECT_FILES: [],
});
await executor.executeStep2({ email: 'user@example.com' });
assert.equal(completedPayloads.length, 1);
assert.deepStrictEqual(completedPayloads[0], {
step: 2,
payload: {
email: 'user@example.com',
nextSignupState: 'already_logged_in_home',
nextSignupUrl: 'https://chatgpt.com/',
skippedPasswordStep: true,
skipRegistrationFlow: true,
},
});
assert.ok(logs.some((item) => /已跳过注册链路/.test(item.message)));
});
test('signup flow helper recognizes email verification page as post-email landing page', async () => {
let ensureCalls = 0;
let passwordReadyChecks = 0;
@@ -119,3 +119,49 @@ test('step 4 does not request a fresh code first for Cloudflare temp mail', asyn
assert.equal(capturedOptions.requestFreshCodeFirst, false);
assert.equal(capturedOptions.resendIntervalMs, 25000);
});
test('step 4 checks iCloud session before polling iCloud mailbox', async () => {
let icloudChecks = 0;
let resolved = false;
const executor = api.createStep4Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
completeStepFromBackground: async () => {},
confirmCustomVerificationStepBypass: async () => {},
ensureIcloudMailSession: async () => {
icloudChecks += 1;
},
ensureMail2925MailboxSession: async () => {},
getMailConfig: () => ({
source: 'icloud-mail',
url: 'https://www.icloud.com/mail/',
label: 'iCloud 邮箱',
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
resolveVerificationStep: async () => {
resolved = true;
},
reuseOrCreateTab: async () => {},
sendToContentScriptResilient: async () => ({}),
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
await executor.executeStep4({
email: 'user@example.com',
password: 'secret',
});
assert.equal(icloudChecks, 1);
assert.equal(resolved, true);
});
+54
View File
@@ -296,3 +296,57 @@ test('step 8 does not rerun step 7 when verification submit lands on add-phone',
assert.equal(calls.rerunStep7, 0);
assert.ok(!calls.logs.some(({ message }) => /准备从步骤 7 重新开始/.test(message)));
});
test('step 8 checks iCloud session before polling iCloud mailbox', async () => {
let icloudChecks = 0;
let resolved = false;
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureIcloudMailSession: async () => {
icloudChecks += 1;
},
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: '' }),
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async () => 8000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
getMailConfig: () => ({
source: 'icloud-mail',
url: 'https://www.icloud.com/mail/',
label: 'iCloud 邮箱',
navigateOnReuse: true,
}),
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async () => {
resolved = true;
},
reuseOrCreateTab: async () => {},
setState: async () => {},
setStepStatus: async () => {},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeStep8({
email: 'user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
});
assert.equal(icloudChecks, 1);
assert.equal(resolved, true);
});
@@ -70,68 +70,40 @@ test('sidepanel modal message preserves line breaks and supports inline links',
assert.match(css, /\.modal-message a,\s*[\s\S]*\.modal-alert a/);
});
test('buildCloudflareTempEmailUsageGuideModalConfig returns a modal payload with inline links for generator mode', () => {
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
test('openCloudflareTempEmailUsageGuidePage opens the contribution portal home page', () => {
const bundle = extractFunction('openCloudflareTempEmailUsageGuidePage');
const api = new Function(`
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'cloudflare-temp-email' };
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
const openedUrls = [];
function getContributionPortalUrl() { return 'https://apikey.qzz.io'; }
function openExternalUrl(url) { openedUrls.push(url); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
openedUrls,
openCloudflareTempEmailUsageGuidePage,
};
`)();
const modalConfig = api.buildCloudflareTempEmailUsageGuideModalConfig();
assert.equal(typeof modalConfig.title, 'string');
assert.equal(typeof modalConfig.messageHtml, 'string');
assert.equal(typeof modalConfig.alert?.text, 'string');
assert.equal(modalConfig.title.length > 0, true);
assert.equal(modalConfig.messageHtml.length > 0, true);
assert.equal(modalConfig.alert.text.length > 0, true);
assert.equal(modalConfig.messageHtml.includes('<a '), true);
assert.equal(modalConfig.messageHtml.includes('Issue #942'), true);
assert.equal(modalConfig.messageHtml.includes('LINUX DO 教程'), true);
api.openCloudflareTempEmailUsageGuidePage();
assert.deepEqual(api.openedUrls, ['https://apikey.qzz.io']);
});
test('buildCloudflareTempEmailUsageGuideModalConfig returns a distinct alert for provider mode', () => {
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
test('openCloudflareTempEmailUsageGuidePage skips opening when the contribution portal URL is empty', () => {
const bundle = extractFunction('openCloudflareTempEmailUsageGuidePage');
const api = new Function(`
const selectMailProvider = { value: 'cloudflare-temp-email' };
const selectEmailGenerator = { value: 'duck' };
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
const openedUrls = [];
function getContributionPortalUrl() { return ''; }
function openExternalUrl(url) { openedUrls.push(url); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
openedUrls,
openCloudflareTempEmailUsageGuidePage,
};
`)();
const providerConfig = api.buildCloudflareTempEmailUsageGuideModalConfig();
const generatorApi = new Function(`
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'cloudflare-temp-email' };
const CLOUDFLARE_TEMP_EMAIL_BUILD_TUTORIAL_URL = 'https://linux.do/t/topic/316819';
const CLOUDFLARE_TEMP_EMAIL_RANDOM_SUBDOMAIN_ISSUE_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email/issues/942';
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
};
`)();
const generatorConfig = generatorApi.buildCloudflareTempEmailUsageGuideModalConfig();
assert.equal(typeof providerConfig.alert?.text, 'string');
assert.equal(typeof providerConfig.messageHtml, 'string');
assert.equal(providerConfig.alert.text.length > 0, true);
assert.equal(providerConfig.messageHtml.length > 0, true);
assert.notEqual(providerConfig.alert.text, generatorConfig.alert.text);
api.openCloudflareTempEmailUsageGuidePage();
assert.deepEqual(api.openedUrls, []);
});
test('openCloudflareTempEmailRepositoryPage opens the upstream repository', () => {
+1 -1
View File
@@ -9,7 +9,7 @@ test('sidepanel html keeps a single contribution mode button in header', () => {
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
assert.equal(matches.length, 1);
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开上传页"/);
assert.match(html, /id="btn-contribution-mode"[^>]*title="进入贡献模式并打开官网页"/);
assert.match(html, />贡献\/使用<\/button>/);
assert.match(html, /<\/header>\s*<div id="contribution-update-layer"/);
assert.match(html, /id="contribution-update-layer"/);
+3 -2
View File
@@ -402,7 +402,8 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
},
},
constants: {
contributionUploadUrl: 'https://apikey.qzz.io',
contributionPortalUrl: 'https://apikey.qzz.io',
contributionUploadUrl: 'https://apikey.qzz.io/upload',
pollIntervalMs: 2500,
},
});
@@ -452,7 +453,7 @@ test('contribution mode manager enters mode, starts main auto flow, polls contri
assert.equal(dom.contributionModeSummary.textContent, '\u5df2\u63d0\u4ea4\u56de\u8c03\uff0c\u7b49\u5f85 CPA \u786e\u8ba4');
dom.btnOpenContributionUpload.listeners.click();
assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io', 'https://apikey.qzz.io']);
assert.deepStrictEqual(openedUrls, ['https://apikey.qzz.io', 'https://apikey.qzz.io/upload']);
await dom.btnExitContributionMode.listeners.click();
manager.render();
+17
View File
@@ -12,6 +12,23 @@ test('sidepanel loads icloud manager before sidepanel bootstrap', () => {
assert.ok(icloudManagerIndex < sidepanelIndex);
});
test('sidepanel source binds the icloud fetch mode control before using it', () => {
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
assert.match(source, /const selectIcloudFetchMode = document\.getElementById\('select-icloud-fetch-mode'\);/);
assert.match(source, /selectIcloudFetchMode\?\.addEventListener\('change'/);
});
test('update card highlights exporting config before upgrade', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
assert.match(html, /<p class="update-card-reminder">一定请先导出配置,再执行更新<\/p>/);
assert.match(css, /\.update-card-reminder\s*\{/);
assert.match(css, /font-weight:\s*700;/);
assert.match(css, /color:\s*var\(--orange\);/);
});
test('icloud manager exposes a factory and renders empty state', () => {
const source = fs.readFileSync('sidepanel/icloud-manager.js', 'utf8');
const windowObject = {};
+3
View File
@@ -115,6 +115,9 @@ function fillInput(el, value) {
filledValues.push(value);
}
async function sleep() {}
function isStep5Ready() { return false; }
function isStep8Ready() { return false; }
function isAddPhonePageReady() { return false; }
function isVisibleElement() { return true; }
function isActionEnabled(el) { return Boolean(el) && !el.disabled; }
function getActionText(el) { return el.textContent || ''; }
+162
View File
@@ -758,3 +758,165 @@ test('verification flow uses configured login resend count for step 8', async ()
assert.deepStrictEqual(resendSteps, [8, 8]);
assert.equal(pollCalls, 3);
});
test('verification flow waits during resend cooldown instead of tight-looping', async () => {
const sleepCalls = [];
let pollCalls = 0;
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 (_mail, message) => {
if (message.type !== 'POLL_EMAIL') {
return {};
}
pollCalls += 1;
return pollCalls === 1
? {}
: { code: '654321', emailTimestamp: 123 };
},
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async (ms) => {
sleepCalls.push(ms);
},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.pollFreshVerificationCodeWithResendInterval(
4,
{
email: 'user@example.com',
lastSignupCode: null,
},
{ provider: 'qq', label: 'QQ 邮箱' },
{
maxResendRequests: 0,
resendIntervalMs: 25000,
lastResendAt: Date.now(),
}
);
assert.equal(result.code, '654321');
assert.equal(pollCalls, 2);
assert.ok(sleepCalls.length >= 1);
assert.ok(sleepCalls[0] >= 1000);
});
test('verification flow uses resilient signup-page transport when submitting verification code', async () => {
const resilientCalls = [];
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 () => {
throw new Error('should not use non-resilient channel');
},
sendToContentScriptResilient: async (_source, message, options) => {
resilientCalls.push({ message, options });
return { success: true };
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.submitVerificationCode(4, '654321');
assert.deepStrictEqual(result, { success: true });
assert.equal(resilientCalls.length, 1);
assert.equal(resilientCalls[0].message.type, 'FILL_CODE');
assert.equal(resilientCalls[0].message.payload.code, '654321');
assert.ok(resilientCalls[0].options.timeoutMs >= 30000);
});
test('verification flow treats retryable submit transport failure as success when step 4 already redirected to logged-in home', async () => {
const logs = [];
const helpers = api.createVerificationFlowHelpers({
addLog: async (message, level = 'info') => {
logs.push({ message, level });
},
chrome: {
tabs: {
update: async () => {},
get: async () => ({ url: 'https://chatgpt.com/' }),
},
},
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',
isRetryableContentScriptTransportError: (error) => /message channel is closed/i.test(String(error?.message || error || '')),
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 () => {
throw new Error('should not use non-resilient channel');
},
sendToContentScriptResilient: async () => {
throw new Error('The page keeping the extension port is moved into back/forward cache, so the message channel is closed.');
},
sendToMailContentScriptResilient: async () => ({}),
setState: async () => {},
setStepStatus: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
VERIFICATION_POLL_MAX_ROUNDS: 5,
});
const result = await helpers.submitVerificationCode(4, '654321');
assert.equal(result.success, true);
assert.equal(result.skipProfileStep, true);
assert.equal(result.assumed, true);
assert.equal(result.transportRecovered, true);
assert.equal(logs.some(({ message }) => /验证码提交后页面已切换到ChatGPT 已登录首页/.test(message)), true);
});