merge: sync origin/dev into pr-112
This commit is contained in:
@@ -331,3 +331,168 @@ test('auto-run controller skips user_already_exists failures to the next round i
|
||||
assert.equal(runtime.state.autoRunActive, false);
|
||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||
});
|
||||
|
||||
test('auto-run controller keeps retrying the same custom mail provider pool email until success', async () => {
|
||||
const events = {
|
||||
logs: [],
|
||||
broadcasts: [],
|
||||
accountRecords: [],
|
||||
runCalls: 0,
|
||||
};
|
||||
|
||||
let currentState = {
|
||||
stepStatuses: {},
|
||||
vpsUrl: 'https://example.com/vps',
|
||||
vpsPassword: 'secret',
|
||||
customPassword: '',
|
||||
autoRunSkipFailures: true,
|
||||
autoRunFallbackThreadIntervalMinutes: 0,
|
||||
autoRunDelayEnabled: false,
|
||||
autoRunDelayMinutes: 30,
|
||||
autoStepDelaySeconds: null,
|
||||
mailProvider: 'custom',
|
||||
customMailProviderPool: ['first@example.com'],
|
||||
emailGenerator: 'duck',
|
||||
gmailBaseEmail: '',
|
||||
mail2925BaseEmail: '',
|
||||
emailPrefix: 'demo',
|
||||
inbucketHost: '',
|
||||
inbucketMailbox: '',
|
||||
cloudflareDomain: '',
|
||||
cloudflareDomains: [],
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
autoRunRoundSummaries: [],
|
||||
};
|
||||
|
||||
const runtime = {
|
||||
state: {
|
||||
autoRunActive: false,
|
||||
autoRunCurrentRun: 0,
|
||||
autoRunTotalRuns: 1,
|
||||
autoRunAttemptRun: 0,
|
||||
autoRunSessionId: 0,
|
||||
},
|
||||
get() {
|
||||
return { ...this.state };
|
||||
},
|
||||
set(updates = {}) {
|
||||
this.state = { ...this.state, ...updates };
|
||||
},
|
||||
};
|
||||
|
||||
let sessionSeed = 0;
|
||||
|
||||
const controller = api.createAutoRunController({
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.logs.push({ message, level });
|
||||
},
|
||||
appendAccountRunRecord: async (status, _state, reason) => {
|
||||
events.accountRecords.push({ status, reason });
|
||||
return { status, reason };
|
||||
},
|
||||
AUTO_RUN_MAX_RETRIES_PER_ROUND: 3,
|
||||
AUTO_RUN_RETRY_DELAY_MS: 3000,
|
||||
AUTO_RUN_TIMER_KIND_BEFORE_RETRY: 'before_retry',
|
||||
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS: 'between_rounds',
|
||||
broadcastAutoRunStatus: async (phase, payload = {}) => {
|
||||
events.broadcasts.push({ phase, ...payload });
|
||||
currentState = {
|
||||
...currentState,
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? runtime.state.autoRunCurrentRun,
|
||||
autoRunTotalRuns: payload.totalRuns ?? runtime.state.autoRunTotalRuns,
|
||||
autoRunAttemptRun: payload.attemptRun ?? runtime.state.autoRunAttemptRun,
|
||||
autoRunSessionId: payload.sessionId ?? runtime.state.autoRunSessionId,
|
||||
};
|
||||
},
|
||||
broadcastStopToContentScripts: async () => {},
|
||||
cancelPendingCommands: () => {},
|
||||
clearStopRequest: () => {},
|
||||
createAutoRunSessionId: () => {
|
||||
sessionSeed += 1;
|
||||
return sessionSeed;
|
||||
},
|
||||
getAutoRunStatusPayload: (phase, payload = {}) => ({
|
||||
autoRunning: ['scheduled', 'running', 'waiting_step', 'waiting_email', 'retrying', 'waiting_interval'].includes(phase),
|
||||
autoRunPhase: phase,
|
||||
autoRunCurrentRun: payload.currentRun ?? 0,
|
||||
autoRunTotalRuns: payload.totalRuns ?? 1,
|
||||
autoRunAttemptRun: payload.attemptRun ?? 0,
|
||||
autoRunSessionId: payload.sessionId ?? 0,
|
||||
}),
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getFirstUnfinishedStep: () => 1,
|
||||
getPendingAutoRunTimerPlan: () => null,
|
||||
getRunningSteps: () => [],
|
||||
getState: async () => ({
|
||||
...currentState,
|
||||
stepStatuses: { ...(currentState.stepStatuses || {}) },
|
||||
tabRegistry: { ...(currentState.tabRegistry || {}) },
|
||||
sourceLastUrls: { ...(currentState.sourceLastUrls || {}) },
|
||||
customMailProviderPool: [...(currentState.customMailProviderPool || [])],
|
||||
}),
|
||||
getStopRequested: () => false,
|
||||
hasSavedProgress: () => false,
|
||||
isAddPhoneAuthFailure: (error) => /add-phone|手机号页面|手机号页|手机号码|手机号/i.test(error?.message || String(error || '')),
|
||||
isRestartCurrentAttemptError: () => false,
|
||||
isSignupUserAlreadyExistsFailure: (error) => /SIGNUP_USER_ALREADY_EXISTS::|user_already_exists/i.test(error?.message || String(error || '')),
|
||||
isStopError: (error) => (error?.message || String(error || '')) === '流程已被用户停止。',
|
||||
launchAutoRunTimerPlan: async () => false,
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes: (value) => Math.max(0, Math.floor(Number(value) || 0)),
|
||||
persistAutoRunTimerPlan: async () => ({}),
|
||||
resetState: async () => {
|
||||
currentState = {
|
||||
...currentState,
|
||||
stepStatuses: {},
|
||||
tabRegistry: {},
|
||||
sourceLastUrls: {},
|
||||
};
|
||||
},
|
||||
runAutoSequenceFromStep: async () => {
|
||||
events.runCalls += 1;
|
||||
if (events.runCalls <= 2) {
|
||||
throw new Error('步骤 3:页面异常,当前尝试失败。');
|
||||
}
|
||||
},
|
||||
runtime,
|
||||
setState: async (updates = {}) => {
|
||||
currentState = {
|
||||
...currentState,
|
||||
...updates,
|
||||
stepStatuses: updates.stepStatuses ? { ...updates.stepStatuses } : currentState.stepStatuses,
|
||||
tabRegistry: updates.tabRegistry ? { ...updates.tabRegistry } : currentState.tabRegistry,
|
||||
sourceLastUrls: updates.sourceLastUrls ? { ...updates.sourceLastUrls } : currentState.sourceLastUrls,
|
||||
};
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfAutoRunSessionStopped: (sessionId) => {
|
||||
if (sessionId && sessionId !== runtime.state.autoRunSessionId) {
|
||||
throw new Error('流程已被用户停止。');
|
||||
}
|
||||
},
|
||||
waitForRunningStepsToFinish: async () => currentState,
|
||||
chrome: {
|
||||
runtime: {
|
||||
sendMessage() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await controller.autoRunLoop(1, {
|
||||
autoRunSkipFailures: true,
|
||||
mode: 'restart',
|
||||
});
|
||||
|
||||
assert.equal(events.runCalls, 3, 'custom mail provider pool should keep retrying the same round until success');
|
||||
assert.equal(events.broadcasts.filter(({ phase }) => phase === 'retrying').length, 2);
|
||||
assert.ok(events.broadcasts.filter(({ phase }) => phase === 'retrying').every(({ currentRun }) => currentRun === 1));
|
||||
assert.ok(events.logs.some(({ message }) => /继续使用当前邮箱/.test(message)));
|
||||
assert.equal(events.logs.some(({ message }) => /达到 3 次重试上限,继续下一轮/.test(message)), false);
|
||||
assert.equal(events.accountRecords.length, 0);
|
||||
assert.equal(runtime.state.autoRunActive, false);
|
||||
assert.equal(runtime.state.autoRunSessionId, 0);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
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('normalizeEmailGenerator'),
|
||||
extractFunction('normalizeCustomEmailPool'),
|
||||
extractFunction('getCustomEmailPool'),
|
||||
extractFunction('getCustomEmailPoolEmailForRun'),
|
||||
extractFunction('getCustomMailProviderPool'),
|
||||
extractFunction('getCustomMailProviderPoolEmailForRun'),
|
||||
extractFunction('getEmailGeneratorLabel'),
|
||||
].join('\n');
|
||||
|
||||
function createApi() {
|
||||
return new Function(`
|
||||
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
|
||||
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
normalizeEmailGenerator,
|
||||
normalizeCustomEmailPool,
|
||||
getCustomEmailPool,
|
||||
getCustomEmailPoolEmailForRun,
|
||||
getCustomMailProviderPool,
|
||||
getCustomMailProviderPoolEmailForRun,
|
||||
getEmailGeneratorLabel,
|
||||
};
|
||||
`)();
|
||||
}
|
||||
|
||||
test('background recognizes custom email pool generator and label', () => {
|
||||
const api = createApi();
|
||||
|
||||
assert.equal(api.normalizeEmailGenerator('custom-pool'), 'custom-pool');
|
||||
assert.equal(api.getEmailGeneratorLabel('custom-pool'), '自定义邮箱池');
|
||||
});
|
||||
|
||||
test('background normalizes custom email pool input and keeps order', () => {
|
||||
const api = createApi();
|
||||
|
||||
assert.deepEqual(
|
||||
api.normalizeCustomEmailPool(' Foo@Example.com \ninvalid\nbar@example.com;baz@example.com '),
|
||||
['foo@example.com', 'bar@example.com', 'baz@example.com']
|
||||
);
|
||||
});
|
||||
|
||||
test('background selects the matching email for the current auto-run round', () => {
|
||||
const api = createApi();
|
||||
const state = {
|
||||
customEmailPool: ['first@example.com', 'second@example.com', 'third@example.com'],
|
||||
};
|
||||
|
||||
assert.equal(api.getCustomEmailPoolEmailForRun(state, 1), 'first@example.com');
|
||||
assert.equal(api.getCustomEmailPoolEmailForRun(state, 2), 'second@example.com');
|
||||
assert.equal(api.getCustomEmailPoolEmailForRun(state, 4), '');
|
||||
});
|
||||
|
||||
test('background selects the matching custom provider pool email for the current auto-run round', () => {
|
||||
const api = createApi();
|
||||
const state = {
|
||||
customMailProviderPool: ['first@example.com', 'second@example.com', 'third@example.com'],
|
||||
};
|
||||
|
||||
assert.deepEqual(api.getCustomMailProviderPool(state), [
|
||||
'first@example.com',
|
||||
'second@example.com',
|
||||
'third@example.com',
|
||||
]);
|
||||
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 1), 'first@example.com');
|
||||
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 3), 'third@example.com');
|
||||
assert.equal(api.getCustomMailProviderPoolEmailForRun(state, 4), '');
|
||||
});
|
||||
@@ -78,6 +78,136 @@ test('generated email helper falls back to normal generator when 2925 is in rece
|
||||
]);
|
||||
});
|
||||
|
||||
test('generated email helper can read the requested address from custom email pool', async () => {
|
||||
const api = loadGeneratedEmailHelpersApi();
|
||||
const events = [];
|
||||
|
||||
const helpers = api.createGeneratedEmailHelpers({
|
||||
addLog: async () => {},
|
||||
buildGeneratedAliasEmail: () => {
|
||||
throw new Error('should not build alias');
|
||||
},
|
||||
buildCloudflareTempEmailHeaders: () => ({}),
|
||||
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
|
||||
CUSTOM_EMAIL_POOL_GENERATOR: 'custom-pool',
|
||||
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
|
||||
fetch: async () => ({ ok: true, text: async () => '{}' }),
|
||||
fetchIcloudHideMyEmail: async () => {
|
||||
throw new Error('should not use icloud generator');
|
||||
},
|
||||
getCloudflareTempEmailAddressFromResponse: () => '',
|
||||
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
|
||||
getCustomEmailPoolEmail: (state, targetRun) => state.customEmailPool?.[targetRun - 1] || '',
|
||||
getState: async () => ({
|
||||
customEmailPool: ['first@example.com', 'second@example.com'],
|
||||
emailGenerator: 'custom-pool',
|
||||
mailProvider: 'gmail',
|
||||
}),
|
||||
ensureMail2925AccountForFlow: async () => {
|
||||
throw new Error('should not allocate 2925 account');
|
||||
},
|
||||
joinCloudflareTempEmailUrl: () => '',
|
||||
normalizeCloudflareDomain: () => '',
|
||||
normalizeCloudflareTempEmailAddress: () => '',
|
||||
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
|
||||
isGeneratedAliasProvider: () => false,
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScript: async () => {
|
||||
throw new Error('should not open duck tab');
|
||||
},
|
||||
setEmailState: async (email) => {
|
||||
events.push(['email', email]);
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const email = await helpers.fetchGeneratedEmail({
|
||||
customEmailPool: ['first@example.com', 'second@example.com'],
|
||||
emailGenerator: 'custom-pool',
|
||||
mailProvider: 'gmail',
|
||||
}, {
|
||||
generator: 'custom-pool',
|
||||
poolIndex: 1,
|
||||
});
|
||||
|
||||
assert.equal(email, 'second@example.com');
|
||||
assert.deepStrictEqual(events, [
|
||||
['email', 'second@example.com'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('generated email helper respects runtime generator overrides when deciding alias flow', async () => {
|
||||
const api = loadGeneratedEmailHelpersApi();
|
||||
const aliasStates = [];
|
||||
const savedEmails = [];
|
||||
|
||||
const helpers = api.createGeneratedEmailHelpers({
|
||||
addLog: async () => {},
|
||||
buildGeneratedAliasEmail: (state) => {
|
||||
aliasStates.push({ ...state });
|
||||
return 'base+tag@gmail.com';
|
||||
},
|
||||
buildCloudflareTempEmailHeaders: () => ({}),
|
||||
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
|
||||
CUSTOM_EMAIL_POOL_GENERATOR: 'custom-pool',
|
||||
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
|
||||
fetch: async () => ({ ok: true, text: async () => '{}' }),
|
||||
fetchIcloudHideMyEmail: async () => {
|
||||
throw new Error('should not use icloud generator');
|
||||
},
|
||||
getCloudflareTempEmailAddressFromResponse: () => '',
|
||||
getCloudflareTempEmailConfig: () => ({ baseUrl: '', adminAuth: '', domain: '' }),
|
||||
getCustomEmailPoolEmail: () => '',
|
||||
getState: async () => ({
|
||||
mailProvider: '163',
|
||||
emailGenerator: 'duck',
|
||||
gmailBaseEmail: '',
|
||||
}),
|
||||
ensureMail2925AccountForFlow: async () => {
|
||||
throw new Error('should not allocate mail2925 account');
|
||||
},
|
||||
joinCloudflareTempEmailUrl: () => '',
|
||||
normalizeCloudflareDomain: () => '',
|
||||
normalizeCloudflareTempEmailAddress: () => '',
|
||||
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
|
||||
isGeneratedAliasProvider: (stateOrProvider, mail2925Mode) => {
|
||||
const provider = typeof stateOrProvider === 'string'
|
||||
? stateOrProvider
|
||||
: stateOrProvider?.mailProvider;
|
||||
const generator = typeof stateOrProvider === 'string'
|
||||
? ''
|
||||
: stateOrProvider?.emailGenerator;
|
||||
return String(provider || '').trim().toLowerCase() === 'gmail'
|
||||
&& String(generator || '').trim().toLowerCase() !== 'custom-pool'
|
||||
&& String(mail2925Mode || '').trim().toLowerCase() !== 'receive';
|
||||
},
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScript: async () => {
|
||||
throw new Error('should not use duck generator');
|
||||
},
|
||||
setEmailState: async (email) => {
|
||||
savedEmails.push(email);
|
||||
},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const email = await helpers.fetchGeneratedEmail({
|
||||
mailProvider: '163',
|
||||
emailGenerator: 'duck',
|
||||
}, {
|
||||
generator: 'gmail-alias',
|
||||
mailProvider: 'gmail',
|
||||
gmailBaseEmail: 'base@gmail.com',
|
||||
});
|
||||
|
||||
assert.equal(email, 'base+tag@gmail.com');
|
||||
assert.deepEqual(savedEmails, ['base+tag@gmail.com']);
|
||||
assert.equal(aliasStates.length, 1);
|
||||
assert.equal(aliasStates[0].mailProvider, 'gmail');
|
||||
assert.equal(aliasStates[0].emailGenerator, 'gmail-alias');
|
||||
assert.equal(aliasStates[0].gmailBaseEmail, 'base@gmail.com');
|
||||
});
|
||||
|
||||
test('generated email helper uses the regular temp email domain when random subdomain mode is disabled', async () => {
|
||||
const api = loadGeneratedEmailHelpersApi();
|
||||
const requests = [];
|
||||
|
||||
@@ -175,8 +175,12 @@ test('signup flow helper reuses existing managed alias email when it is still co
|
||||
test('signup flow helper finalizes step 3 submit by reusing signup verification preparation', async () => {
|
||||
let ensureCalls = 0;
|
||||
const messages = [];
|
||||
const logs = [];
|
||||
|
||||
const helpers = signupFlowApi.createSignupFlowHelpers({
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
buildGeneratedAliasEmail: () => '',
|
||||
chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } },
|
||||
ensureContentScriptReadyOnTab: async (...args) => {
|
||||
@@ -188,6 +192,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
|
||||
isGeneratedAliasProvider: () => false,
|
||||
isReusableGeneratedAliasEmail: () => false,
|
||||
isHotmailProvider: () => false,
|
||||
isRetryableContentScriptTransportError: () => false,
|
||||
isLuckmailProvider: () => false,
|
||||
isSignupEmailVerificationPageUrl: () => false,
|
||||
isSignupPasswordPageUrl: () => true,
|
||||
@@ -206,6 +211,7 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
|
||||
|
||||
assert.deepStrictEqual(result, { ready: true, retried: 1 });
|
||||
assert.equal(ensureCalls, 1);
|
||||
assert.deepStrictEqual(logs, []);
|
||||
assert.deepStrictEqual(messages.find((item) => item.type === 'send')?.message, {
|
||||
type: 'PREPARE_SIGNUP_VERIFICATION',
|
||||
step: 3,
|
||||
@@ -217,3 +223,45 @@ test('signup flow helper finalizes step 3 submit by reusing signup verification
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('signup flow helper rewrites retryable step 3 finalize transport timeout into a Chinese error', async () => {
|
||||
const logs = [];
|
||||
|
||||
const helpers = signupFlowApi.createSignupFlowHelpers({
|
||||
addLog: async (message, level = 'info') => {
|
||||
logs.push({ message, level });
|
||||
},
|
||||
buildGeneratedAliasEmail: () => '',
|
||||
chrome: { tabs: { get: async () => ({ id: 31, url: 'https://auth.openai.com/create-account/password' }) } },
|
||||
ensureContentScriptReadyOnTab: async () => {},
|
||||
ensureHotmailAccountForFlow: async () => ({}),
|
||||
ensureLuckmailPurchaseForFlow: async () => ({}),
|
||||
isGeneratedAliasProvider: () => false,
|
||||
isReusableGeneratedAliasEmail: () => false,
|
||||
isHotmailProvider: () => false,
|
||||
isRetryableContentScriptTransportError: (error) => /did not respond in 45s/i.test(error?.message || String(error || '')),
|
||||
isLuckmailProvider: () => false,
|
||||
isSignupEmailVerificationPageUrl: () => false,
|
||||
isSignupPasswordPageUrl: () => true,
|
||||
reuseOrCreateTab: async () => 31,
|
||||
sendToContentScriptResilient: async () => {
|
||||
throw new Error('Content script on signup-page did not respond in 45s. Try refreshing the tab and retry.');
|
||||
},
|
||||
setEmailState: async () => {},
|
||||
SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
|
||||
SIGNUP_PAGE_INJECT_FILES: ['content/utils.js', 'content/signup-page.js'],
|
||||
waitForTabUrlMatch: async () => null,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => helpers.finalizeSignupPasswordSubmitInTab(31, 'Secret123!', 3),
|
||||
/步骤 3:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。/
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(logs, [
|
||||
{
|
||||
message: '步骤 3:认证页在提交后切换过程中页面通信超时,未能重新就绪,暂时无法确认是否进入下一页面。请重试当前轮。',
|
||||
level: 'warn',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,41 @@ test('tab runtime module exposes a factory', () => {
|
||||
assert.equal(typeof api?.createTabRuntime, 'function');
|
||||
});
|
||||
|
||||
test('tab runtime caps per-attempt response timeout to the remaining resilient timeout budget', () => {
|
||||
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
|
||||
|
||||
const runtime = api.createTabRuntime({
|
||||
LOG_PREFIX: '[test]',
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
get: async () => ({ id: 1, url: 'https://example.com', status: 'complete' }),
|
||||
query: async () => [],
|
||||
},
|
||||
},
|
||||
getSourceLabel: (source) => source || 'unknown',
|
||||
getState: async () => ({ tabRegistry: {}, sourceLastUrls: {} }),
|
||||
matchesSourceUrlFamily: () => false,
|
||||
normalizeLocalCpaStep9Mode: () => 'submit',
|
||||
parseUrlSafely: () => null,
|
||||
registerTab: async () => {},
|
||||
setState: async () => {},
|
||||
shouldBypassStep9ForLocalCpa: () => false,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
runtime.resolveResponseTimeoutMs({ type: 'PREPARE_SIGNUP_VERIFICATION' }, undefined, 30000),
|
||||
30000
|
||||
);
|
||||
assert.equal(
|
||||
runtime.resolveResponseTimeoutMs({ type: 'PREPARE_SIGNUP_VERIFICATION' }, 12000, 5000),
|
||||
5000
|
||||
);
|
||||
});
|
||||
|
||||
test('tab runtime waitForTabComplete waits until tab status becomes complete', async () => {
|
||||
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -48,7 +48,7 @@ function extractFunction(name) {
|
||||
return sidepanelSource.slice(start, end);
|
||||
}
|
||||
|
||||
function createApi({ refreshImpl } = {}) {
|
||||
function createApi({ refreshImpl, confirmImpl, dismissImpl } = {}) {
|
||||
const bundle = extractFunction('startAutoRunFromCurrentSettings');
|
||||
|
||||
return new Function(`
|
||||
@@ -90,6 +90,14 @@ async function refreshContributionContentHint() {
|
||||
events.push({ type: 'refresh' });
|
||||
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
|
||||
}
|
||||
async function maybeConfirmContributionUpdateBeforeAutoRun(snapshot) {
|
||||
events.push({ type: 'confirm-check', snapshot });
|
||||
${confirmImpl ? 'return (' + confirmImpl + ')(snapshot);' : 'return true;'}
|
||||
}
|
||||
function dismissContributionUpdateHint() {
|
||||
events.push({ type: 'dismiss-hint' });
|
||||
${dismissImpl ? '(' + dismissImpl + ')();' : ''}
|
||||
}
|
||||
${bundle}
|
||||
return {
|
||||
startAutoRunFromCurrentSettings,
|
||||
@@ -108,9 +116,9 @@ test('startAutoRunFromCurrentSettings refreshes contribution content hint before
|
||||
assert.equal(result, true);
|
||||
assert.deepEqual(
|
||||
api.getEvents().map((entry) => entry.type),
|
||||
['refresh', 'send']
|
||||
['refresh', 'confirm-check', 'send']
|
||||
);
|
||||
assert.equal(api.getEvents()[1].message.type, 'AUTO_RUN');
|
||||
assert.equal(api.getEvents()[2].message.type, 'AUTO_RUN');
|
||||
});
|
||||
|
||||
test('startAutoRunFromCurrentSettings continues auto run when contribution content refresh fails', async () => {
|
||||
@@ -124,8 +132,26 @@ test('startAutoRunFromCurrentSettings continues auto run when contribution conte
|
||||
assert.equal(result, true);
|
||||
assert.deepEqual(
|
||||
events.map((entry) => entry.type),
|
||||
['refresh', 'warn', 'send']
|
||||
['refresh', 'warn', 'confirm-check', 'send']
|
||||
);
|
||||
assert.match(String(events[1].args[0]), /Failed to refresh contribution content hint before auto run/);
|
||||
assert.equal(events[2].message.type, 'AUTO_RUN');
|
||||
assert.equal(events[3].message.type, 'AUTO_RUN');
|
||||
});
|
||||
|
||||
test('startAutoRunFromCurrentSettings aborts when contribution update confirmation is declined', async () => {
|
||||
const api = createApi({
|
||||
refreshImpl: `async () => ({
|
||||
promptVersion: 'questionnaire:2026-04-23T00:00:00Z',
|
||||
items: [{ slug: 'questionnaire', isVisible: true }],
|
||||
})`,
|
||||
confirmImpl: 'async () => false',
|
||||
});
|
||||
|
||||
const result = await api.startAutoRunFromCurrentSettings();
|
||||
|
||||
assert.equal(result, false);
|
||||
assert.deepEqual(
|
||||
api.getEvents().map((entry) => entry.type),
|
||||
['refresh', 'confirm-check']
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => sidepanelSource.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < sidepanelSource.length; i += 1) {
|
||||
const ch = sidepanelSource[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < sidepanelSource.length; end += 1) {
|
||||
const ch = sidepanelSource[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sidepanelSource.slice(start, end);
|
||||
}
|
||||
|
||||
const helperBundle = [
|
||||
extractFunction('getContributionUpdatePromptLines'),
|
||||
extractFunction('getContributionUpdateHintMessage'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
${helperBundle}
|
||||
return {
|
||||
getContributionUpdatePromptLines,
|
||||
getContributionUpdateHintMessage,
|
||||
};
|
||||
`)();
|
||||
|
||||
test('getContributionUpdateHintMessage numbers contribution updates when both content and questionnaire are visible', () => {
|
||||
const message = api.getContributionUpdateHintMessage({
|
||||
promptVersion: 'announcement:2026-04-23T00:00:00Z|questionnaire:2026-04-23T00:00:01Z',
|
||||
items: [
|
||||
{ slug: 'announcement', isVisible: true },
|
||||
{ slug: 'questionnaire', isVisible: true },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
message,
|
||||
'1. 公告 / 使用教程有更新了,可点上方“贡献/使用”查看。\n2. 有新的征求意见,请佬友共同参与选择。'
|
||||
);
|
||||
});
|
||||
|
||||
test('getContributionUpdateHintMessage returns questionnaire prompt alone when only questionnaire is updated', () => {
|
||||
const message = api.getContributionUpdateHintMessage({
|
||||
promptVersion: 'questionnaire:2026-04-23T00:00:01Z',
|
||||
items: [
|
||||
{ slug: 'questionnaire', isVisible: true },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(message, '有新的征求意见,请佬友共同参与选择。');
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('sidepanel html exposes custom email pool generator option and input row', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
|
||||
assert.match(html, /option value="custom-pool">自定义邮箱池<\/option>/);
|
||||
assert.match(html, /id="row-custom-email-pool"/);
|
||||
assert.match(html, /id="input-custom-email-pool"/);
|
||||
assert.match(html, /id="row-custom-mail-provider-pool"/);
|
||||
assert.match(html, /id="input-custom-mail-provider-pool"/);
|
||||
});
|
||||
|
||||
test('sidepanel locks run count to custom email pool size', () => {
|
||||
const bundle = [
|
||||
extractFunction('isCustomMailProvider'),
|
||||
extractFunction('normalizeCustomEmailPoolEntries'),
|
||||
extractFunction('getSelectedEmailGenerator'),
|
||||
extractFunction('usesGeneratedAliasMailProvider'),
|
||||
extractFunction('usesCustomEmailPoolGenerator'),
|
||||
extractFunction('getCustomMailProviderPoolSize'),
|
||||
extractFunction('usesCustomMailProviderPool'),
|
||||
extractFunction('getLockedRunCountFromEmailPool'),
|
||||
extractFunction('getCustomEmailPoolSize'),
|
||||
extractFunction('getRunCountValue'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const GMAIL_PROVIDER = 'gmail';
|
||||
const GMAIL_ALIAS_GENERATOR = 'gmail-alias';
|
||||
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
|
||||
const selectMailProvider = { value: 'gmail' };
|
||||
const selectEmailGenerator = { value: 'custom-pool' };
|
||||
const inputCustomEmailPool = { value: 'first@example.com\\nsecond@example.com' };
|
||||
const inputRunCount = { value: '99' };
|
||||
|
||||
function isLuckmailProvider() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isManagedAliasProvider() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getSelectedMail2925Mode() {
|
||||
return 'provide';
|
||||
}
|
||||
|
||||
function isManagedAliasProvider(provider) {
|
||||
return String(provider || '').trim().toLowerCase() === GMAIL_PROVIDER;
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
getSelectedEmailGenerator,
|
||||
usesGeneratedAliasMailProvider,
|
||||
usesCustomEmailPoolGenerator,
|
||||
getCustomEmailPoolSize,
|
||||
getRunCountValue,
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.getSelectedEmailGenerator(), 'custom-pool');
|
||||
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'provide', 'gmail-alias'), true);
|
||||
assert.equal(api.usesGeneratedAliasMailProvider('gmail', 'provide', 'custom-pool'), false);
|
||||
assert.equal(api.usesCustomEmailPoolGenerator(), true);
|
||||
assert.equal(api.getCustomEmailPoolSize(), 2);
|
||||
assert.equal(api.getRunCountValue(), 2);
|
||||
});
|
||||
|
||||
test('sidepanel locks run count to custom mail provider pool size', () => {
|
||||
const bundle = [
|
||||
extractFunction('isCustomMailProvider'),
|
||||
extractFunction('normalizeCustomEmailPoolEntries'),
|
||||
extractFunction('getSelectedEmailGenerator'),
|
||||
extractFunction('usesGeneratedAliasMailProvider'),
|
||||
extractFunction('usesCustomEmailPoolGenerator'),
|
||||
extractFunction('getCustomMailProviderPoolSize'),
|
||||
extractFunction('usesCustomMailProviderPool'),
|
||||
extractFunction('getLockedRunCountFromEmailPool'),
|
||||
extractFunction('getCustomEmailPoolSize'),
|
||||
extractFunction('getRunCountValue'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const GMAIL_PROVIDER = 'gmail';
|
||||
const GMAIL_ALIAS_GENERATOR = 'gmail-alias';
|
||||
const CUSTOM_EMAIL_POOL_GENERATOR = 'custom-pool';
|
||||
const selectMailProvider = { value: 'custom' };
|
||||
const selectEmailGenerator = { value: 'duck' };
|
||||
const inputCustomMailProviderPool = { value: 'first@example.com\\nsecond@example.com\\nthird@example.com' };
|
||||
const inputCustomEmailPool = { value: '' };
|
||||
const inputRunCount = { value: '99' };
|
||||
|
||||
function isLuckmailProvider() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isManagedAliasProvider() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getSelectedMail2925Mode() {
|
||||
return 'provide';
|
||||
}
|
||||
|
||||
function isManagedAliasProvider(provider) {
|
||||
return String(provider || '').trim().toLowerCase() === GMAIL_PROVIDER;
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
usesCustomMailProviderPool,
|
||||
getCustomMailProviderPoolSize,
|
||||
getLockedRunCountFromEmailPool,
|
||||
getRunCountValue,
|
||||
};
|
||||
`)();
|
||||
|
||||
assert.equal(api.usesCustomMailProviderPool(), true);
|
||||
assert.equal(api.getCustomMailProviderPoolSize(), 3);
|
||||
assert.equal(api.getLockedRunCountFromEmailPool(), 3);
|
||||
assert.equal(api.getRunCountValue(), 3);
|
||||
});
|
||||
|
||||
test('sidepanel custom verification dialog exposes add-phone action for step 8', async () => {
|
||||
const bundle = [
|
||||
extractFunction('getCustomVerificationPromptCopy'),
|
||||
extractFunction('openCustomVerificationConfirmDialog'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let openActionModalPayload = null;
|
||||
|
||||
async function openActionModal(options) {
|
||||
openActionModalPayload = options;
|
||||
return options.buildResult('add_phone');
|
||||
}
|
||||
|
||||
async function openConfirmModal() {
|
||||
throw new Error('step 8 should use action modal');
|
||||
}
|
||||
|
||||
${bundle}
|
||||
|
||||
return {
|
||||
getCustomVerificationPromptCopy,
|
||||
openCustomVerificationConfirmDialog,
|
||||
getOpenActionModalPayload: () => openActionModalPayload,
|
||||
};
|
||||
`)();
|
||||
|
||||
const prompt = api.getCustomVerificationPromptCopy(8);
|
||||
assert.equal(prompt.phoneActionLabel, '出现手机号验证');
|
||||
|
||||
const result = await api.openCustomVerificationConfirmDialog(8);
|
||||
assert.deepEqual(result, {
|
||||
confirmed: false,
|
||||
addPhoneDetected: true,
|
||||
});
|
||||
|
||||
const modalPayload = api.getOpenActionModalPayload();
|
||||
assert.equal(modalPayload.actions.length, 3);
|
||||
assert.equal(modalPayload.actions[1].id, 'add_phone');
|
||||
assert.equal(modalPayload.actions[1].label, '出现手机号验证');
|
||||
});
|
||||
@@ -345,6 +345,51 @@ test('verification flow treats add-phone after login code submit as fatal instea
|
||||
]);
|
||||
});
|
||||
|
||||
test('verification flow treats manual step 8 add-phone confirmation as the same fatal add-phone error', async () => {
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {
|
||||
throw new Error('should not complete step 8');
|
||||
},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({
|
||||
confirmed: false,
|
||||
addPhoneDetected: true,
|
||||
}),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async () => ({}),
|
||||
sendToMailContentScriptResilient: async () => ({}),
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {
|
||||
throw new Error('should not mark step skipped when add-phone is chosen');
|
||||
},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => helpers.confirmCustomVerificationStepBypass(8),
|
||||
/验证码提交后页面进入手机号页面/
|
||||
);
|
||||
});
|
||||
|
||||
test('verification flow caps mail polling timeout to the remaining oauth budget', async () => {
|
||||
const mailPollCalls = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user