Merge origin/dev into codex2api source

- resolve the sidepanel contribution-mode test conflict while keeping the new Codex2API assertions\n- update the mail2925 payload test harness for newly added Codex2API and random-subdomain inputs\n- renumber the README quick-start plan headings and strip trailing whitespace from the new tutorial doc
This commit is contained in:
Q3CC
2026-04-22 22:18:09 +08:00
31 changed files with 2587 additions and 176 deletions
+10
View File
@@ -81,6 +81,16 @@ test('isRecoverableStep9AuthFailure matches timeout and CPA auth failure statuse
true
);
assert.equal(
isRecoverableStep9AuthFailure('提交回调失败:请更新CLI Proxy API或检查连接'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证失败:Bad Request'),
true
);
assert.equal(
isRecoverableStep9AuthFailure('认证成功!'),
false
+95 -1
View File
@@ -71,6 +71,7 @@ test('executeStep reuses the active top-level auth chain instead of starting a d
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
@@ -106,6 +107,10 @@ function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
function isBrowserSwitchRequiredError() {
return false;
}
async function handleBrowserSwitchRequired() {}
function doesStepUseCompletionSignal() {
return false;
}
@@ -159,10 +164,99 @@ return {
assert.ok(events.logs.some(({ message }) => /复用当前授权链/.test(message)));
});
test('executeStep stops flow when browser-switch-required error is raised', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
const BROWSER_SWITCH_REQUIRED_ERROR_PREFIX = 'BROWSER_SWITCH_REQUIRED::';
const AUTH_CHAIN_STEP_IDS = new Set([7, 8, 9, 10]);
let activeTopLevelAuthChainExecution = null;
let stopRequested = false;
const events = {
logs: [],
statusCalls: [],
stopRequests: [],
appendRecords: [],
};
async function addLog(message, level = 'info') {
events.logs.push({ message, level });
}
async function setStepStatus(step, status) {
events.statusCalls.push({ step, status });
}
async function humanStepDelay() {}
async function getState() {
return {
flowStartTime: null,
stepStatuses: {},
};
}
function getErrorMessage(error) {
return error?.message || String(error || '');
}
async function appendManualAccountRunRecordIfNeeded(status, _state, reason) {
events.appendRecords.push({ status, reason });
}
function isTerminalSecurityBlockedError() {
return false;
}
async function handleCloudflareSecurityBlocked() {}
async function requestStop(options = {}) {
events.stopRequests.push(options);
}
function doesStepUseCompletionSignal() {
return false;
}
function isRetryableContentScriptTransportError() {
return false;
}
const stepRegistry = {
async executeStep() {
throw new Error('BROWSER_SWITCH_REQUIRED::请更换浏览器进行注册登录。');
},
};
${extractFunction('isStopError')}
${extractFunction('throwIfStopped')}
${extractFunction('isAuthChainStep')}
${extractFunction('acquireTopLevelAuthChainExecution')}
${extractFunction('isBrowserSwitchRequiredError')}
${extractFunction('getBrowserSwitchRequiredMessage')}
${extractFunction('handleBrowserSwitchRequired')}
${extractFunction('executeStep')}
return {
executeStep,
snapshot() {
return events;
},
};
`)();
await assert.rejects(
() => api.executeStep(10),
/流程已被用户停止。/
);
const events = api.snapshot();
assert.deepStrictEqual(events.stopRequests, [
{ logMessage: '请更换浏览器进行注册登录。' },
]);
assert.deepStrictEqual(events.statusCalls, [
{ step: 10, status: 'running' },
]);
assert.equal(
events.logs.some(({ message }) => /步骤 10 失败/.test(message)),
false,
'browser-switch-required error should stop the flow before generic failed logging'
);
});
test('oauth timeout budget ignores stale deadlines from an old oauth url', async () => {
const api = new Function(`
const LOG_PREFIX = '[test]';
const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
const OAUTH_FLOW_TIMEOUT_MS = 5 * 60 * 1000;
${extractFunction('normalizeOAuthFlowDeadlineAt')}
${extractFunction('normalizeOAuthFlowSourceUrl')}
${extractFunction('getOAuthFlowRemainingMs')}
@@ -0,0 +1,145 @@
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;
}
}
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('cloudflare temp email settings normalize and expose the random-subdomain toggle', () => {
const bundle = [
extractFunction('normalizeCloudflareTempEmailReceiveMailbox'),
extractFunction('getCloudflareTempEmailConfig'),
extractFunction('normalizePersistentSettingValue'),
extractFunction('buildPersistentSettingsPayload'),
].join('\n');
const api = new Function(`
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const PERSISTED_SETTING_DEFAULTS = {
panelMode: 'cpa',
autoStepDelaySeconds: null,
verificationResendCount: DEFAULT_VERIFICATION_RESEND_COUNT,
mailProvider: '163',
mail2925Mode: 'provide',
emailGenerator: 'duck',
autoDeleteUsedIcloudAlias: false,
accountRunHistoryTextEnabled: false,
cloudflareTempEmailUseRandomSubdomain: false,
cloudflareTempEmailDomain: '',
cloudflareTempEmailDomains: [],
};
const PERSISTED_SETTING_KEYS = Object.keys(PERSISTED_SETTING_DEFAULTS);
function normalizePanelMode(value) { return value === 'sub2api' ? 'sub2api' : 'cpa'; }
function normalizeLocalCpaStep9Mode(value) { return value === 'bypass' ? 'bypass' : 'submit'; }
function normalizeAutoRunFallbackThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoRunDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value, fallback = null) { return value == null || value === '' ? fallback : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number.isFinite(Number(value)) ? Number(value) : fallback; }
function normalizeMailProvider(value) { return String(value || '').trim().toLowerCase() || '163'; }
function normalizeMail2925Mode(value) { return String(value || '').trim().toLowerCase() === 'receive' ? 'receive' : 'provide'; }
function normalizeEmailGenerator(value) { return String(value || '').trim().toLowerCase() || 'duck'; }
function normalizeIcloudHost(value) { const normalized = String(value || '').trim().toLowerCase(); return normalized === 'icloud.com' || normalized === 'icloud.com.cn' ? normalized : ''; }
function normalizeAccountRunHistoryHelperBaseUrl(value) { return String(value || '').trim(); }
function normalizeHotmailServiceMode(value) { return String(value || '').trim().toLowerCase() === 'remote' ? 'remote' : 'local'; }
function normalizeHotmailRemoteBaseUrl(value) { return String(value || '').trim(); }
function normalizeHotmailLocalBaseUrl(value) { return String(value || '').trim(); }
function normalizeCloudflareDomain(value) { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareDomains(value) { return Array.isArray(value) ? value.map((item) => String(item || '').trim().toLowerCase()).filter(Boolean) : []; }
function normalizeCloudflareTempEmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailAddress(value = '') { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailDomain(value) { return String(value || '').trim().toLowerCase(); }
function normalizeCloudflareTempEmailDomains(value) {
const seen = new Set();
const domains = [];
for (const item of Array.isArray(value) ? value : []) {
const normalized = normalizeCloudflareTempEmailDomain(item);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
domains.push(normalized);
}
return domains;
}
function normalizeHotmailAccounts(value) { return Array.isArray(value) ? value : []; }
function normalizeMail2925Accounts(value) { return Array.isArray(value) ? value : []; }
function resolveLegacyAutoStepDelaySeconds() { return undefined; }
${bundle}
return {
buildPersistentSettingsPayload,
getCloudflareTempEmailConfig,
normalizePersistentSettingValue,
};
`)();
assert.equal(api.normalizePersistentSettingValue('cloudflareTempEmailUseRandomSubdomain', 1), true);
const payload = api.buildPersistentSettingsPayload({
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
cloudflareTempEmailDomains: ['mail.example.com', 'alt.example.com'],
});
assert.equal(payload.cloudflareTempEmailUseRandomSubdomain, true);
assert.equal(payload.cloudflareTempEmailDomain, 'mail.example.com');
assert.deepEqual(payload.cloudflareTempEmailDomains, ['mail.example.com', 'alt.example.com']);
const config = api.getCloudflareTempEmailConfig({
cloudflareTempEmailBaseUrl: 'https://temp.example.com',
cloudflareTempEmailAdminAuth: 'admin-secret',
cloudflareTempEmailCustomAuth: 'custom-secret',
cloudflareTempEmailReceiveMailbox: 'Forward@Example.com',
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
cloudflareTempEmailDomains: ['mail.example.com'],
});
assert.deepEqual(config, {
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: 'custom-secret',
receiveMailbox: 'forward@example.com',
useRandomSubdomain: true,
domain: 'mail.example.com',
domains: ['mail.example.com'],
});
});
+162 -7
View File
@@ -2,24 +2,25 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
function loadGeneratedEmailHelpersApi() {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
return new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
}
test('background imports generated email helper module', () => {
const source = fs.readFileSync('background.js', 'utf8');
assert.match(source, /importScripts\([\s\S]*'background\/generated-email-helpers\.js'/);
});
test('generated email helper module exposes a factory', () => {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
const api = loadGeneratedEmailHelpersApi();
assert.equal(typeof api?.createGeneratedEmailHelpers, 'function');
});
test('generated email helper falls back to normal generator when 2925 is in receive mode', async () => {
const source = fs.readFileSync('background/generated-email-helpers.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageGeneratedEmailHelpers;`)(globalScope);
const api = loadGeneratedEmailHelpersApi();
const events = [];
const helpers = api.createGeneratedEmailHelpers({
@@ -76,3 +77,157 @@ test('generated email helper falls back to normal generator when 2925 is in rece
['email', 'duck@example.com'],
]);
});
test('generated email helper uses the regular temp email domain when random subdomain mode is disabled', async () => {
const api = loadGeneratedEmailHelpersApi();
const requests = [];
const savedEmails = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async (url, options = {}) => {
requests.push({
url,
method: options.method,
body: options.body ? JSON.parse(options.body) : null,
});
return {
ok: true,
text: async () => JSON.stringify({ address: 'user@mail.example.com' }),
};
},
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
getCloudflareTempEmailConfig: () => ({
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: '',
useRandomSubdomain: false,
domain: 'mail.example.com',
}),
getState: async () => ({
mailProvider: '163',
emailGenerator: 'cloudflare-temp-email',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'cloudflare-temp-email',
}, {
generator: 'cloudflare-temp-email',
});
assert.equal(email, 'user@mail.example.com');
assert.deepEqual(savedEmails, ['user@mail.example.com']);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://temp.example.com/admin/new_address');
assert.equal(requests[0].method, 'POST');
assert.deepEqual(requests[0].body, {
enablePrefix: true,
enableRandomSubdomain: false,
name: requests[0].body.name,
domain: 'mail.example.com',
});
assert.match(requests[0].body.name, /^[a-z0-9]+$/);
});
test('generated email helper requests random subdomain creation while preserving the returned address', async () => {
const api = loadGeneratedEmailHelpersApi();
const requests = [];
const savedEmails = [];
const helpers = api.createGeneratedEmailHelpers({
addLog: async () => {},
buildGeneratedAliasEmail: () => {
throw new Error('should not build managed alias');
},
buildCloudflareTempEmailHeaders: () => ({ 'x-admin-auth': 'admin-secret' }),
CLOUDFLARE_TEMP_EMAIL_GENERATOR: 'cloudflare-temp-email',
DUCK_AUTOFILL_URL: 'https://duckduckgo.com/email',
fetch: async (url, options = {}) => {
requests.push({
url,
method: options.method,
body: options.body ? JSON.parse(options.body) : null,
});
return {
ok: true,
text: async () => JSON.stringify({ address: 'user@a1b2c3d4.example.com' }),
};
},
fetchIcloudHideMyEmail: async () => {
throw new Error('should not use icloud generator');
},
getCloudflareTempEmailAddressFromResponse: (payload) => payload.address,
getCloudflareTempEmailConfig: () => ({
baseUrl: 'https://temp.example.com',
adminAuth: 'admin-secret',
customAuth: '',
useRandomSubdomain: true,
domain: 'mail.example.com',
}),
getState: async () => ({
mailProvider: '163',
emailGenerator: 'cloudflare-temp-email',
}),
ensureMail2925AccountForFlow: async () => {
throw new Error('should not allocate mail2925 account');
},
joinCloudflareTempEmailUrl: (baseUrl, path) => `${baseUrl}${path}`,
normalizeCloudflareDomain: () => '',
normalizeCloudflareTempEmailAddress: (value) => String(value || '').trim().toLowerCase(),
normalizeEmailGenerator: (value) => String(value || '').trim().toLowerCase(),
isGeneratedAliasProvider: () => false,
reuseOrCreateTab: async () => {},
sendToContentScript: async () => {
throw new Error('should not use duck generator');
},
setEmailState: async (email) => {
savedEmails.push(email);
},
throwIfStopped: () => {},
});
const email = await helpers.fetchGeneratedEmail({
emailGenerator: 'cloudflare-temp-email',
}, {
generator: 'cloudflare-temp-email',
localPart: 'user',
});
assert.equal(email, 'user@a1b2c3d4.example.com');
assert.deepEqual(savedEmails, ['user@a1b2c3d4.example.com']);
assert.equal(requests.length, 1);
assert.equal(requests[0].url, 'https://temp.example.com/admin/new_address');
assert.equal(requests[0].method, 'POST');
assert.deepEqual(requests[0].body, {
enablePrefix: true,
enableRandomSubdomain: true,
name: 'user',
domain: 'mail.example.com',
});
});
@@ -266,3 +266,163 @@ test('ensureMail2925MailboxSession logs in when login page is detected and accou
assert.equal(readyCalls, 1);
assert.equal(result.result.loggedIn, true);
});
test('ensureMail2925MailboxSession relogs with selected account when mailbox page email mismatches and pool is on', async () => {
let currentState = {
autoRunning: false,
mail2925UseAccountPool: true,
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'target@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: 'acc-1',
};
const openedUrls = [];
const sendPayloads = [];
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {
tabs: {
get: async () => ({ id: 9, url: openedUrls.at(-1) || 'https://2925.com/#/mailList' }),
},
cookies: {
getAll: async () => [],
remove: async () => ({ ok: true }),
},
browsingData: {
removeCookies: async () => {},
},
},
ensureContentScriptReadyOnTab: async () => {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isAutoRunLockedState: () => false,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
reuseOrCreateTab: async (_source, url) => {
openedUrls.push(url);
return 9;
},
sendToMailContentScriptResilient: async (_mail, message) => {
sendPayloads.push(message.payload);
if (sendPayloads.length === 1) {
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'wrong@2925.com',
};
}
return {
loggedIn: true,
currentView: 'mailbox',
mailboxEmail: 'target@2925.com',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
waitForTabUrlMatch: async () => ({ url: 'https://2925.com/login/' }),
});
const result = await manager.ensureMail2925MailboxSession({
accountId: 'acc-1',
forceRelogin: false,
allowLoginWhenOnLoginPage: true,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
});
assert.equal(result.result.loggedIn, true);
assert.deepStrictEqual(openedUrls, [
'https://2925.com/#/mailList',
'https://2925.com/login/',
]);
assert.equal(sendPayloads.length, 2);
assert.equal(sendPayloads[0].allowLoginWhenOnLoginPage, true);
assert.equal(sendPayloads[1].forceLogin, true);
});
test('ensureMail2925MailboxSession stops when mailbox page email mismatches and pool is off', async () => {
let currentState = {
autoRunning: true,
autoRunPhase: 'running',
mail2925UseAccountPool: false,
mail2925Accounts: [],
currentMail2925AccountId: null,
};
const stopCalls = [];
let sendCalls = 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 () => {},
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,
currentView: 'mailbox',
mailboxEmail: 'wrong@2925.com',
};
},
setPersistentSettings: async (payload) => {
currentState = { ...currentState, ...payload };
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await assert.rejects(
() => manager.ensureMail2925MailboxSession({
accountId: null,
forceRelogin: false,
allowLoginWhenOnLoginPage: false,
expectedMailboxEmail: 'target@2925.com',
actionLabel: '步骤 4:确认 2925 邮箱登录态',
}),
/流程已被用户停止。/
);
assert.equal(sendCalls, 1);
assert.equal(stopCalls.length, 1);
assert.match(stopCalls[0].logMessage, /与目标账号 target@2925\.com 不一致/);
});
@@ -294,3 +294,46 @@ test('handleMail2925LimitReachedError stops immediately when account pool is off
assert.equal(events.stopCalls.length, 1);
assert.equal(currentState.currentMail2925AccountId, 'current');
});
test('setCurrentMail2925Account persists currentMail2925AccountId for browser restart restore', async () => {
const source = fs.readFileSync('background/mail-2925-session.js', 'utf8');
const globalScope = {};
const api = new Function('self', `${source}; return self.MultiPageBackgroundMail2925Session;`)(globalScope);
let currentState = {
mail2925Accounts: mail2925Utils.normalizeMail2925Accounts([
{ id: 'acc-1', email: 'acc1@2925.com', password: 'p1', enabled: true, lastUsedAt: 10 },
]),
currentMail2925AccountId: null,
};
const persistedUpdates = [];
const manager = api.createMail2925SessionManager({
addLog: async () => {},
broadcastDataUpdate: () => {},
chrome: {},
findMail2925Account: mail2925Utils.findMail2925Account,
getMail2925AccountStatus: mail2925Utils.getMail2925AccountStatus,
getState: async () => currentState,
isMail2925AccountAvailable: mail2925Utils.isMail2925AccountAvailable,
MAIL2925_LIMIT_COOLDOWN_MS: mail2925Utils.MAIL2925_LIMIT_COOLDOWN_MS,
normalizeMail2925Account: mail2925Utils.normalizeMail2925Account,
normalizeMail2925Accounts: mail2925Utils.normalizeMail2925Accounts,
pickMail2925AccountForRun: mail2925Utils.pickMail2925AccountForRun,
setPersistentSettings: async (payload) => {
persistedUpdates.push(payload);
},
setState: async (updates) => {
currentState = { ...currentState, ...updates };
},
throwIfStopped: () => {},
upsertMail2925AccountInList: mail2925Utils.upsertMail2925AccountInList,
});
await manager.setCurrentMail2925Account('acc-1');
assert.equal(currentState.currentMail2925AccountId, 'acc-1');
assert.deepStrictEqual(persistedUpdates, [
{ currentMail2925AccountId: 'acc-1' },
]);
});
+55
View File
@@ -12,6 +12,61 @@ test('ensureMail2925Session waits 1 second after filling credentials before clic
assert.match(source, /fillInput\(passwordInput,\s*password\);[\s\S]*?await sleep\(200\);[\s\S]*?await sleep\(1000\);[\s\S]*?simulateClick\(loginButton\);/);
});
test('detectMail2925ViewState treats top mailbox email as mailbox view', () => {
const bundle = [
extractFunction('normalizeNodeText'),
extractFunction('isVisibleNode'),
extractFunction('isMailItemNode'),
extractFunction('resolveActionTarget'),
extractFunction('findMailItems'),
extractFunction('extractEmails'),
extractFunction('getMail2925DisplayedMailboxEmail'),
extractFunction('detectMail2925ViewState'),
].join('\n');
const api = new Function(`
const MAIL_ITEM_SELECTORS = ['.mail-item'];
const MAIL_ITEM_SELECTOR_GROUP = '.mail-item';
const MAIL2925_REMEMBER_LOGIN_PATTERNS = [];
const MAIL2925_AGREEMENT_PATTERNS = [];
const document = {
querySelectorAll(selector) {
if (selector === '.mail-item') return [];
if (selector === 'body *') return [headerEmail];
if (selector.includes('[class*="user"]')) return [headerEmail];
return [];
},
body: {
innerText: 'QLHazycoder qlhazycoder@2925.com',
textContent: 'QLHazycoder qlhazycoder@2925.com',
},
};
const window = {
innerHeight: 900,
getComputedStyle() {
return { display: 'block', visibility: 'visible' };
},
};
const headerEmail = {
hidden: false,
textContent: 'qlhazycoder@2925.com',
innerText: 'qlhazycoder@2925.com',
getBoundingClientRect() { return { top: 40, left: 400, width: 120, height: 20 }; },
closest() { return null; },
};
function detectMail2925LimitMessage() { return ''; }
function findMail2925LoginPasswordInput() { return null; }
function findMail2925LoginEmailInput() { return null; }
function getPageTextSample() { return 'qlhazycoder@2925.com'; }
${bundle}
return { detectMail2925ViewState };
`)();
const state = api.detectMail2925ViewState();
assert.equal(state.view, 'mailbox');
assert.equal(state.mailboxEmail, 'qlhazycoder@2925.com');
});
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
const start = markers
@@ -0,0 +1,297 @@
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;
}
}
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);
}
function createRow(initialDisplay = 'none') {
return {
style: { display: initialDisplay },
};
}
test('sidepanel html places cloudflare temp email controls in a standalone section', () => {
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
assert.match(html, /id="cloudflare-temp-email-section"/);
assert.match(html, /id="btn-cloudflare-temp-email-usage-guide"/);
assert.match(html, /id="btn-cloudflare-temp-email-github"/);
assert.match(html, /id="row-temp-email-random-subdomain-toggle"/);
assert.match(html, /id="input-temp-email-use-random-subdomain"/);
assert.doesNotMatch(html, /id="row-temp-email-random-subdomain-domain"/);
});
test('sidepanel modal message preserves line breaks and supports inline links', () => {
const css = fs.readFileSync('sidepanel/sidepanel.css', 'utf8');
assert.match(css, /\.modal-message\s*\{[\s\S]*white-space:\s*pre-line;/);
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');
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(); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
};
`)();
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);
});
test('buildCloudflareTempEmailUsageGuideModalConfig returns a distinct alert for provider mode', () => {
const bundle = extractFunction('buildCloudflareTempEmailUsageGuideModalConfig');
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(); }
${bundle}
return {
buildCloudflareTempEmailUsageGuideModalConfig,
};
`)();
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);
});
test('openCloudflareTempEmailRepositoryPage opens the upstream repository', () => {
const bundle = extractFunction('openCloudflareTempEmailRepositoryPage');
const api = new Function(`
const calls = [];
const CLOUDFLARE_TEMP_EMAIL_REPOSITORY_URL = 'https://github.com/dreamhunter2333/cloudflare_temp_email';
function openExternalUrl(url) { calls.push(url); }
${bundle}
return {
calls,
openCloudflareTempEmailRepositoryPage,
};
`)();
api.openCloudflareTempEmailRepositoryPage();
assert.deepEqual(api.calls, ['https://github.com/dreamhunter2333/cloudflare_temp_email']);
});
test('applyCloudflareTempEmailSettingsState restores the random subdomain toggle and temp domain list', () => {
const bundle = extractFunction('applyCloudflareTempEmailSettingsState');
const api = new Function(`
const inputTempEmailBaseUrl = { value: '' };
const inputTempEmailAdminAuth = { value: '' };
const inputTempEmailCustomAuth = { value: '' };
const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const calls = {
domainOptions: [],
domainEditMode: [],
};
function renderCloudflareTempEmailDomainOptions(value) { calls.domainOptions.push(value); }
function setCloudflareTempEmailDomainEditMode(editing, options) { calls.domainEditMode.push({ editing, options }); }
${bundle}
return {
applyCloudflareTempEmailSettingsState,
calls,
inputTempEmailBaseUrl,
inputTempEmailAdminAuth,
inputTempEmailCustomAuth,
inputTempEmailReceiveMailbox,
inputTempEmailUseRandomSubdomain,
};
`)();
api.applyCloudflareTempEmailSettingsState({
cloudflareTempEmailBaseUrl: 'https://temp.example.com',
cloudflareTempEmailAdminAuth: 'admin-secret',
cloudflareTempEmailCustomAuth: 'custom-secret',
cloudflareTempEmailReceiveMailbox: 'relay@example.com',
cloudflareTempEmailUseRandomSubdomain: true,
cloudflareTempEmailDomain: 'mail.example.com',
});
assert.equal(api.inputTempEmailBaseUrl.value, 'https://temp.example.com');
assert.equal(api.inputTempEmailAdminAuth.value, 'admin-secret');
assert.equal(api.inputTempEmailCustomAuth.value, 'custom-secret');
assert.equal(api.inputTempEmailReceiveMailbox.value, 'relay@example.com');
assert.equal(api.inputTempEmailUseRandomSubdomain.checked, true);
assert.deepEqual(api.calls.domainOptions, ['mail.example.com']);
assert.deepEqual(api.calls.domainEditMode, [{ editing: false, options: { clearInput: true } }]);
});
test('updateMailProviderUI keeps the temp domain selector visible and updates the hint when random subdomain is enabled', () => {
const bundle = extractFunction('updateMailProviderUI');
const api = new Function(`
let latestState = {
cloudflareTempEmailDomains: ['mail.example.com'],
};
let cloudflareTempEmailDomainEditMode = false;
const ICLOUD_PROVIDER = 'icloud';
const GMAIL_PROVIDER = 'gmail';
const LUCKMAIL_PROVIDER = 'luckmail-api';
const rowMail2925Mode = ${JSON.stringify(createRow('none'))};
const rowMail2925PoolSettings = ${JSON.stringify(createRow('none'))};
const rowEmailPrefix = ${JSON.stringify(createRow('none'))};
const rowInbucketHost = ${JSON.stringify(createRow('none'))};
const rowInbucketMailbox = ${JSON.stringify(createRow('none'))};
const rowEmailGenerator = ${JSON.stringify(createRow(''))};
const rowCfDomain = ${JSON.stringify(createRow('none'))};
const rowTempEmailBaseUrl = ${JSON.stringify(createRow('none'))};
const rowTempEmailAdminAuth = ${JSON.stringify(createRow('none'))};
const rowTempEmailCustomAuth = ${JSON.stringify(createRow('none'))};
const rowTempEmailReceiveMailbox = ${JSON.stringify(createRow('none'))};
const rowTempEmailRandomSubdomainToggle = ${JSON.stringify(createRow('none'))};
const rowTempEmailDomain = ${JSON.stringify(createRow('none'))};
const cloudflareTempEmailSection = ${JSON.stringify(createRow('none'))};
const hotmailSection = ${JSON.stringify(createRow('none'))};
const mail2925Section = ${JSON.stringify(createRow('none'))};
const luckmailSection = ${JSON.stringify(createRow('none'))};
const icloudSection = ${JSON.stringify(createRow('none'))};
const labelEmailPrefix = { textContent: '' };
const inputEmailPrefix = { placeholder: '', style: { display: '' }, readOnly: false };
const labelMail2925UseAccountPool = ${JSON.stringify(createRow('none'))};
const selectMail2925PoolAccount = { style: { display: 'none' }, disabled: false };
const btnFetchEmail = { hidden: false, disabled: false, textContent: '' };
const btnMailLogin = { disabled: false, textContent: '', title: '' };
const inputEmail = { readOnly: false, placeholder: '', value: '' };
const autoHintText = { textContent: '' };
const rowHotmailServiceMode = ${JSON.stringify(createRow('none'))};
const rowHotmailRemoteBaseUrl = ${JSON.stringify(createRow('none'))};
const rowHotmailLocalBaseUrl = ${JSON.stringify(createRow('none'))};
const inputMail2925UseAccountPool = { checked: false };
const selectMailProvider = { value: '163' };
const selectEmailGenerator = { value: 'cloudflare-temp-email', disabled: false };
const inputTempEmailUseRandomSubdomain = { checked: false };
const calls = {
tempDomainEditMode: [],
};
function isLuckmailProvider() { return false; }
function isCustomMailProvider() { return false; }
function isIcloudMailProvider() { return false; }
function usesGeneratedAliasMailProvider() { return false; }
function getSelectedMail2925Mode() { return 'provide'; }
function getManagedAliasProviderUiCopy() { return null; }
function getCurrentRegistrationEmailUiCopy() {
return {
buttonLabel: '生成 Temp',
placeholder: '点击生成 Cloudflare Temp Email,或手动粘贴邮箱',
label: 'Cloudflare Temp Email',
};
}
function updateMailLoginButtonState() {}
function getSelectedHotmailServiceMode() { return 'local'; }
function getCloudflareDomainsFromState() { return { domains: [], activeDomain: '' }; }
function setCloudflareDomainEditMode() {}
function getCloudflareTempEmailDomainsFromState() { return { domains: ['mail.example.com'], activeDomain: 'mail.example.com' }; }
function setCloudflareTempEmailDomainEditMode(editing) { calls.tempDomainEditMode.push(editing); }
function queueIcloudAliasRefresh() {}
function hideIcloudLoginHelp() {}
function syncMail2925PoolAccountOptions() {}
function getMail2925Accounts() { return []; }
function renderHotmailAccounts() {}
function renderMail2925Accounts() {}
function renderLuckmailPurchases() {}
function getSelectedEmailGenerator() { return String(selectEmailGenerator.value || '').trim().toLowerCase(); }
function isAutoRunLockedPhase() { return false; }
${bundle}
return {
updateMailProviderUI,
cloudflareTempEmailSection,
rowTempEmailRandomSubdomainToggle,
rowTempEmailDomain,
inputTempEmailUseRandomSubdomain,
autoHintText,
calls,
};
`)();
api.updateMailProviderUI();
assert.equal(api.cloudflareTempEmailSection.style.display, '');
assert.equal(api.rowTempEmailRandomSubdomainToggle.style.display, '');
assert.equal(api.rowTempEmailDomain.style.display, '');
api.inputTempEmailUseRandomSubdomain.checked = true;
api.updateMailProviderUI();
assert.equal(api.cloudflareTempEmailSection.style.display, '');
assert.equal(api.rowTempEmailDomain.style.display, '');
assert.match(api.autoHintText.textContent, /RANDOM_SUBDOMAIN_DOMAINS/);
});
@@ -156,6 +156,7 @@ const inputTempEmailBaseUrl = { value: 'https://temp.example.com' };
const inputTempEmailAdminAuth = { value: 'admin-secret' };
const inputTempEmailCustomAuth = { value: 'custom-secret' };
const inputTempEmailReceiveMailbox = { value: 'relay@example.com' };
const inputTempEmailUseRandomSubdomain = { checked: true };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '5' };
const inputAutoDelayEnabled = { checked: true };
@@ -192,6 +193,7 @@ return {
assert.equal('customPassword' in contributionPayload, false);
assert.equal('accountRunHistoryTextEnabled' in contributionPayload, false);
assert.equal('accountRunHistoryHelperBaseUrl' in contributionPayload, false);
assert.equal(contributionPayload.cloudflareTempEmailUseRandomSubdomain, true);
api.setLatestState({ contributionMode: false });
const normalPayload = api.collectSettingsPayload();
@@ -200,6 +202,7 @@ return {
assert.equal(normalPayload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(normalPayload.codex2apiUrl, 'http://localhost:8080/admin/accounts');
assert.equal(normalPayload.codex2apiAdminKey, 'codex-admin-secret');
assert.equal(normalPayload.cloudflareTempEmailUseRandomSubdomain, true);
});
test('contribution mode manager enters mode, starts main auto flow, polls contribution status, and exits cleanly', async () => {
@@ -142,3 +142,89 @@ return {
assert.equal(api.getLatestState().mail2925BaseEmail, 'new@2925.com');
assert.equal(api.getSaveCalls(), 1);
});
test('collectSettingsPayload persists currentMail2925AccountId for 2925 account pool restore', () => {
const bundle = [
extractFunction('collectSettingsPayload'),
].join('\n');
const api = new Function(`
let latestState = {
contributionMode: false,
mail2925UseAccountPool: true,
currentMail2925AccountId: 'acc-2',
};
let cloudflareDomainEditMode = false;
let cloudflareTempEmailDomainEditMode = false;
const selectCfDomain = { value: 'example.com' };
const selectTempEmailDomain = { value: 'mail.example.com' };
const selectPanelMode = { value: 'cpa' };
const inputVpsUrl = { value: '' };
const inputVpsPassword = { value: '' };
const inputSub2ApiUrl = { value: '' };
const inputSub2ApiEmail = { value: '' };
const inputSub2ApiPassword = { value: '' };
const inputSub2ApiGroup = { value: '' };
const inputSub2ApiDefaultProxy = { value: '' };
const inputCodex2ApiUrl = { value: '' };
const inputCodex2ApiAdminKey = { value: '' };
const inputPassword = { value: '' };
const selectMailProvider = { value: '2925' };
const selectEmailGenerator = { value: 'duck' };
const checkboxAutoDeleteIcloud = { checked: false };
const selectIcloudHostPreference = { value: 'auto' };
const inputAccountRunHistoryTextEnabled = { checked: false };
const inputAccountRunHistoryHelperBaseUrl = { value: '' };
const inputMail2925UseAccountPool = { checked: true };
const inputInbucketHost = { value: '' };
const inputInbucketMailbox = { value: '' };
const inputHotmailRemoteBaseUrl = { value: '' };
const inputHotmailLocalBaseUrl = { value: '' };
const inputLuckmailApiKey = { value: '' };
const inputLuckmailBaseUrl = { value: '' };
const selectLuckmailEmailType = { value: 'ms_graph' };
const inputLuckmailDomain = { value: '' };
const inputTempEmailBaseUrl = { value: '' };
const inputTempEmailAdminAuth = { value: '' };
const inputTempEmailCustomAuth = { value: '' };
const inputTempEmailReceiveMailbox = { value: '' };
const inputTempEmailUseRandomSubdomain = { checked: false };
const inputAutoSkipFailures = { checked: false };
const inputAutoSkipFailuresThreadIntervalMinutes = { value: '0' };
const inputAutoDelayEnabled = { checked: false };
const inputAutoDelayMinutes = { value: '30' };
const inputAutoStepDelaySeconds = { value: '' };
const inputVerificationResendCount = { value: '4' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
function getCloudflareDomainsFromState() {
return { domains: [], activeDomain: '' };
}
function normalizeCloudflareDomainValue(value) { return String(value || '').trim(); }
function getCloudflareTempEmailDomainsFromState() {
return { domains: [], activeDomain: '' };
}
function normalizeCloudflareTempEmailDomainValue(value) { return String(value || '').trim(); }
function getSelectedLocalCpaStep9Mode() { return 'submit'; }
function getSelectedMail2925Mode() { return 'provide'; }
function getSelectedHotmailServiceMode() { return 'local'; }
function buildManagedAliasBaseEmailPayload() {
return { gmailBaseEmail: '', mail2925BaseEmail: 'demo@2925.com', emailPrefix: '' };
}
function normalizeLuckmailBaseUrl(value) { return String(value || '').trim(); }
function normalizeLuckmailEmailType(value) { return String(value || '').trim() || 'ms_graph'; }
function normalizeCloudflareTempEmailBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeCloudflareTempEmailReceiveMailboxValue(value) { return String(value || '').trim(); }
function normalizeAccountRunHistoryHelperBaseUrlValue(value) { return String(value || '').trim(); }
function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) || 0; }
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Number(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${bundle}
return { collectSettingsPayload };
`)();
const payload = api.collectSettingsPayload();
assert.equal(payload.currentMail2925AccountId, 'acc-2');
assert.equal(payload.mail2925UseAccountPool, true);
});
+314
View File
@@ -144,3 +144,317 @@ return {
]);
assert.match(result.bodyTextPreview, /Welcome to ChatGPT/);
});
test('signup entry diagnostics captures hidden signup button style and blocking ancestor details', () => {
const api = new Function(`
const SIGNUP_ENTRY_TRIGGER_PATTERN = /免费注册|立即注册|注册|sign\\s*up|register|create\\s*account|create\\s+account/i;
const location = { href: 'https://chatgpt.com/' };
const hiddenSection = {
tagName: 'DIV',
id: 'mobile-cta',
className: 'max-xs:hidden',
hidden: false,
parentElement: null,
hasAttribute() {
return false;
},
getAttribute(name) {
if (name === 'aria-hidden') return '';
return '';
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
_style: {
display: 'none',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const hiddenSignupButton = {
tagName: 'BUTTON',
textContent: 'Sign up for free',
disabled: false,
className: 'signup-button',
hidden: false,
parentElement: hiddenSection,
hasAttribute() {
return false;
},
getBoundingClientRect() {
return { width: 0, height: 0 };
},
getAttribute(name) {
if (name === 'type') return '';
if (name === 'aria-hidden') return '';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const document = {
title: 'ChatGPT',
readyState: 'complete',
querySelector() {
return null;
},
querySelectorAll(selector) {
if (selector === 'a, button, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [hiddenSignupButton];
}
return [];
},
};
const window = {
innerWidth: 390,
innerHeight: 844,
outerWidth: 390,
outerHeight: 844,
devicePixelRatio: 3,
getComputedStyle(el) {
return el?._style || {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
};
},
};
function isVisibleElement(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getSignupEmailInput() {
return null;
}
function getSignupPasswordInput() {
return null;
}
function getPageTextSnapshot() {
return 'ChatGPT 登录';
}
${extractFunction('getSignupEntryDiagnostics')}
return {
run() {
return getSignupEntryDiagnostics();
},
};
`)();
const result = api.run();
assert.deepStrictEqual(result.viewport, {
innerWidth: 390,
innerHeight: 844,
outerWidth: 390,
outerHeight: 844,
devicePixelRatio: 3,
});
assert.deepStrictEqual(result.signupLikeActionCounts, {
total: 1,
visible: 0,
hidden: 1,
});
assert.equal(result.signupLikeActions[0]?.text, 'Sign up for free');
assert.equal(result.signupLikeActions[0]?.className, 'signup-button');
assert.equal(result.signupLikeActions[0]?.display, 'block');
assert.equal(result.signupLikeActions[0]?.blockingAncestor?.className, 'max-xs:hidden');
assert.equal(result.signupLikeActions[0]?.blockingAncestor?.display, 'none');
});
test('signup password diagnostics summarizes password inputs, submit button, and alternate code entry', () => {
const api = new Function(`
const location = { href: 'https://auth.openai.com/create-account/password' };
const form = { action: 'https://auth.openai.com/u/signup/password' };
const passwordInput = {
tagName: 'INPUT',
type: 'password',
name: 'new-password',
id: 'password-field',
value: 'SecretLength14',
className: 'password-input',
disabled: false,
form,
getBoundingClientRect() {
return { width: 320, height: 44 };
},
getAttribute(name) {
if (name === 'type') return 'password';
if (name === 'name') return 'new-password';
if (name === 'autocomplete') return 'new-password';
if (name === 'placeholder') return 'Password';
if (name === 'aria-disabled') return 'false';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const submitButton = {
tagName: 'BUTTON',
textContent: 'Continue',
className: 'submit-btn',
disabled: false,
form,
getBoundingClientRect() {
return { width: 160, height: 40 };
},
getAttribute(name) {
if (name === 'type') return 'submit';
if (name === 'aria-disabled') return 'false';
if (name === 'data-dd-action-name') return 'Continue';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const oneTimeCodeButton = {
tagName: 'BUTTON',
textContent: 'Use a one-time code instead',
className: 'switch-btn',
disabled: false,
getBoundingClientRect() {
return { width: 220, height: 36 };
},
getAttribute(name) {
if (name === 'type') return 'button';
if (name === 'aria-disabled') return 'false';
return '';
},
_style: {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
},
};
const document = {
title: 'Create your account',
readyState: 'complete',
querySelectorAll(selector) {
if (selector === 'input[type="password"], input[name*="password" i], input[autocomplete="new-password"], input[autocomplete="current-password"]') {
return [passwordInput];
}
if (selector === 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]') {
return [submitButton, oneTimeCodeButton];
}
return [];
},
};
const window = {
getComputedStyle(el) {
return el?._style || {
display: 'block',
visibility: 'visible',
opacity: '1',
pointerEvents: 'auto',
};
},
};
function isVisibleElement(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function isActionEnabled(el) {
return Boolean(el) && !el.disabled && el.getAttribute('aria-disabled') !== 'true';
}
function getActionText(el) {
return [el?.textContent, el?.value, el?.getAttribute?.('aria-label'), el?.getAttribute?.('title')]
.filter(Boolean)
.join(' ')
.replace(/\\s+/g, ' ')
.trim();
}
function getPageTextSnapshot() {
return 'Create your account Use a one-time code instead';
}
function getSignupPasswordInput() {
return passwordInput;
}
function getSignupPasswordSubmitButton() {
return submitButton;
}
function getSignupPasswordDisplayedEmail() {
return 'user@example.com';
}
function findOneTimeCodeLoginTrigger() {
return oneTimeCodeButton;
}
function getSignupPasswordTimeoutErrorPageState() {
return {
retryEnabled: true,
userAlreadyExistsBlocked: false,
};
}
${extractFunction('getSignupPasswordDiagnostics')}
return {
run() {
return getSignupPasswordDiagnostics();
},
};
`)();
const result = api.run();
assert.equal(result.url, 'https://auth.openai.com/create-account/password');
assert.equal(result.displayedEmail, 'user@example.com');
assert.equal(result.hasVisiblePasswordInput, true);
assert.equal(result.passwordInputCount, 1);
assert.equal(result.visiblePasswordInputCount, 1);
assert.equal(result.passwordInputs[0]?.name, 'new-password');
assert.equal(result.passwordInputs[0]?.valueLength, 14);
assert.equal(result.submitButton?.text, 'Continue');
assert.equal(result.oneTimeCodeTrigger?.text, 'Use a one-time code instead');
assert.equal(result.retryPage, true);
assert.equal(result.retryEnabled, true);
assert.match(result.bodyTextPreview, /one-time code/);
});
+44 -19
View File
@@ -51,10 +51,8 @@ function extractFunction(name) {
return source.slice(start, end);
}
test('step 8 click effect returns restart_current_step when retry page is recovered', async () => {
test('step 8 click effect throws when retry page appears after clicking continue', async () => {
const api = new Function(`
let recoverCalls = 0;
const chrome = {
tabs: {
async get() {
@@ -69,10 +67,6 @@ const chrome = {
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
async function recoverAuthRetryPageOnTab() {
recoverCalls += 1;
return { recovered: true };
}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
@@ -89,20 +83,51 @@ return {
async run() {
return waitForStep8ClickEffect(88, 'https://auth.openai.com/authorize', 1000);
},
snapshot() {
return { recoverCalls };
};
`)();
await assert.rejects(
() => api.run(),
/点击“继续”后页面进入认证页重试页/
);
});
test('step 8 ready check throws when consent page is already a retry page before clicking', async () => {
const api = new Function(`
const chrome = {
tabs: {
async get() {
return {
id: 88,
url: 'https://auth.openai.com/authorize',
};
},
},
};
function throwIfStopped() {}
async function sleepWithStop() {}
async function ensureStep8SignupPageReady() {}
async function getStep8PageState() {
return {
url: 'https://auth.openai.com/authorize',
retryPage: true,
addPhonePage: false,
consentReady: false,
};
}
${extractFunction('waitForStep8Ready')}
return {
async run() {
return waitForStep8Ready(88, 1000);
},
};
`)();
const result = await api.run();
const snapshot = api.snapshot();
assert.deepStrictEqual(result, {
progressed: false,
reason: 'retry_page_recovered',
restartCurrentStep: true,
url: 'https://auth.openai.com/authorize',
});
assert.equal(snapshot.recoverCalls, 1);
await assert.rejects(
() => api.run(),
/当前认证页已进入重试页/
);
});
+114 -5
View File
@@ -58,23 +58,36 @@ const bundle = [
extractFunction('summarizeStatusBadgeEntries'),
extractFunction('normalizeStep9StatusText'),
extractFunction('isOAuthCallbackTimeoutFailure'),
extractFunction('isStep10CallbackSubmittedStatus'),
extractFunction('isStep10CallbackFailureText'),
extractFunction('isStep10MainWaitingStatus'),
extractFunction('isStep10MainFailureText'),
extractFunction('isStep9FailureText'),
extractFunction('isStep9SuccessStatus'),
extractFunction('isStep9SuccessLikeStatus'),
extractFunction('formatStep10StatusSummaryValue'),
extractFunction('isStep10BrowserSwitchRequiredConflict'),
extractFunction('getStep10BrowserSwitchRequiredMessage'),
extractFunction('buildStep9StatusDiagnostics'),
extractFunction('extractStep10FailureDetail'),
extractFunction('explainStep10Failure'),
].join('\n');
function createApi() {
return new Function(`
function isRecoverableStep9AuthFailure(text) {
return /(?:认证失败|回调 URL 提交失败):\\s*/i.test(String(text || '').trim())
|| /oauth flow is not pending/i.test(String(text || '').trim());
const normalized = String(text || '').trim();
return /(?:认证失败|回调\\s*url\\s*提交失败|回调url提交失败|提交回调失败)\\s*[:]?/i.test(normalized)
|| /oauth flow is not pending|请更新\\s*cli\\s*proxy\\s*api\\s*或检查连接|bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(normalized);
}
${bundle}
return {
buildStep9StatusDiagnostics,
explainStep10Failure,
isStep10BrowserSwitchRequiredConflict,
getStep10BrowserSwitchRequiredMessage,
};
`)();
}
@@ -86,6 +99,7 @@ test('step 9 does not treat red success badges as exact success', () => {
visible: true,
text: '认证成功!',
className: 'status-badge text-danger',
location: 'main',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
@@ -104,23 +118,118 @@ test('step 9 keeps failure state dominant when success badge and error banner co
visible: true,
text: '认证成功!',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[
{
visible: true,
text: '回调 URL 提交失败: oauth flow is not pending',
className: 'alert alert-danger',
className: 'status-badge error',
location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
[],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.hasFailureVisibleBadge, true);
assert.equal(diagnostics.failureText, '回调 URL 提交失败: oauth flow is not pending');
assert.equal(diagnostics.failureSource, 'callback');
});
test('step 10 treats plain 认证成功 as success when no failure is visible', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[],
'page'
);
assert.equal(diagnostics.hasExactSuccessVisibleBadge, true);
assert.equal(diagnostics.exactSuccessText, '认证成功');
});
test('step 10 recognizes callback accepted badge as in-progress signal', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '回调 URL 已提交,等待认证中...',
className: 'status-badge success',
location: 'callback',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
{
visible: true,
text: '等待认证中...',
className: 'status-badge',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
],
[],
'page'
);
assert.equal(diagnostics.hasCallbackSubmittedBadge, true);
assert.equal(diagnostics.callbackSubmittedText, '回调 URL 已提交,等待认证中...');
assert.equal(diagnostics.mainWaitingText, '等待认证中...');
assert.equal(diagnostics.hasExactSuccessVisibleBadge, false);
});
test('step 10 explains callback upgrade hint with user-friendly reason', () => {
const api = createApi();
const explanation = api.explainStep10Failure(
'回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
'callback'
);
assert.equal(explanation.code, 'callback_submit_api_unavailable');
assert.match(explanation.userMessage, /CLI Proxy API 版本过旧|管理接口未启动|连接异常/);
assert.match(explanation.userMessage, /回调提交阶段/);
});
test('step 10 requests browser switch when success badge and callback upgrade failure coexist', () => {
const api = createApi();
const diagnostics = api.buildStep9StatusDiagnostics(
[
{
visible: true,
text: '认证成功',
className: 'status-badge success',
location: 'main',
hasErrorVisualSignal: false,
errorVisualSummary: '',
},
{
visible: true,
text: '回调 URL 提交失败: 请更新CLI Proxy API或检查连接',
className: 'status-badge error',
location: 'callback',
hasErrorVisualSignal: true,
errorVisualSummary: 'color=rgb(220, 38, 38)',
},
],
[],
'page'
);
assert.equal(api.isStep10BrowserSwitchRequiredConflict(diagnostics), true);
assert.match(api.getStep10BrowserSwitchRequiredMessage(diagnostics), /更换浏览器后重新进行注册登录/);
});