feat: add step definitions module and integrate with sidepanel
- Introduced a new module for step definitions in `data/step-definitions.js` to manage shared step metadata. - Updated `sidepanel.html` to dynamically render steps based on the new step definitions module. - Refactored `sidepanel.js` to utilize the step definitions for rendering and managing step statuses. - Enhanced tests to validate the step definitions module and its integration with the sidepanel. - Cleaned up existing tests to ensure they align with the new structure and functionality.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const helperSource = fs.readFileSync('background.js', 'utf8');
|
||||
const autoRunModuleSource = fs.readFileSync('background/auto-run-controller.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
function extractFunction(source, name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map(marker => source.indexOf(marker))
|
||||
@@ -50,30 +51,25 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const bundle = [
|
||||
extractFunction('clearStopRequest'),
|
||||
extractFunction('throwIfStopped'),
|
||||
extractFunction('isStopError'),
|
||||
extractFunction('isStepDoneStatus'),
|
||||
extractFunction('isRestartCurrentAttemptError'),
|
||||
extractFunction('getFirstUnfinishedStep'),
|
||||
extractFunction('hasSavedProgress'),
|
||||
extractFunction('getRunningSteps'),
|
||||
extractFunction('getAutoRunStatusPayload'),
|
||||
extractFunction('createAutoRunRoundSummary'),
|
||||
extractFunction('normalizeAutoRunRoundSummary'),
|
||||
extractFunction('buildAutoRunRoundSummaries'),
|
||||
extractFunction('serializeAutoRunRoundSummaries'),
|
||||
extractFunction('getAutoRunRoundRetryCount'),
|
||||
extractFunction('formatAutoRunFailureReasons'),
|
||||
extractFunction('logAutoRunFinalSummary'),
|
||||
extractFunction('waitBetweenAutoRunRounds'),
|
||||
extractFunction('autoRunLoop'),
|
||||
const helperBundle = [
|
||||
extractFunction(helperSource, 'clearStopRequest'),
|
||||
extractFunction(helperSource, 'throwIfStopped'),
|
||||
extractFunction(helperSource, 'isStopError'),
|
||||
extractFunction(helperSource, 'isStepDoneStatus'),
|
||||
extractFunction(helperSource, 'isRestartCurrentAttemptError'),
|
||||
extractFunction(helperSource, 'getFirstUnfinishedStep'),
|
||||
extractFunction(helperSource, 'hasSavedProgress'),
|
||||
extractFunction(helperSource, 'getRunningSteps'),
|
||||
extractFunction(helperSource, 'getAutoRunStatusPayload'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const api = new Function('autoRunModuleSource', `
|
||||
const self = {};
|
||||
const STOP_ERROR_MESSAGE = 'Flow stopped.';
|
||||
const AUTO_RUN_MAX_RETRIES_PER_ROUND = 3;
|
||||
const AUTO_RUN_RETRY_DELAY_MS = 3000;
|
||||
const AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS = 'between_rounds';
|
||||
const AUTO_RUN_TIMER_KIND_BEFORE_RETRY = 'before_retry';
|
||||
const DEFAULT_STATE = {
|
||||
stepStatuses: {
|
||||
1: 'pending',
|
||||
@@ -89,10 +85,6 @@ const DEFAULT_STATE = {
|
||||
};
|
||||
|
||||
let stopRequested = false;
|
||||
let autoRunActive = false;
|
||||
let autoRunCurrentRun = 0;
|
||||
let autoRunTotalRuns = 1;
|
||||
let autoRunAttemptRun = 0;
|
||||
let runCalls = 0;
|
||||
|
||||
const logs = [];
|
||||
@@ -189,21 +181,10 @@ function cancelPendingCommands() {}
|
||||
function normalizeAutoRunFallbackThreadIntervalMinutes(value) {
|
||||
return Math.max(0, Math.floor(Number(value) || 0));
|
||||
}
|
||||
function buildAutoRunRoundSummaries(totalRuns, rawSummaries = []) {
|
||||
return Array.from({ length: totalRuns }, (_, index) => ({
|
||||
round: index + 1,
|
||||
status: rawSummaries[index]?.status || 'pending',
|
||||
attempts: rawSummaries[index]?.attempts || 0,
|
||||
failureReasons: [...(rawSummaries[index]?.failureReasons || [])],
|
||||
finalFailureReason: rawSummaries[index]?.finalFailureReason || '',
|
||||
}));
|
||||
}
|
||||
function serializeAutoRunRoundSummaries(totalRuns, roundSummaries = []) {
|
||||
return buildAutoRunRoundSummaries(totalRuns, roundSummaries);
|
||||
}
|
||||
async function logAutoRunFinalSummary() {}
|
||||
async function waitBetweenAutoRunRounds() {}
|
||||
|
||||
async function persistAutoRunTimerPlan() {}
|
||||
async function launchAutoRunTimerPlan() { return false; }
|
||||
function getPendingAutoRunTimerPlan() { return null; }
|
||||
function getErrorMessage(error) { return error?.message || String(error || ''); }
|
||||
const chrome = {
|
||||
runtime: {
|
||||
sendMessage() {
|
||||
@@ -245,24 +226,73 @@ async function runAutoSequenceFromStep() {
|
||||
};
|
||||
}
|
||||
|
||||
${bundle}
|
||||
${helperBundle}
|
||||
${autoRunModuleSource}
|
||||
|
||||
const runtime = {
|
||||
state: {
|
||||
autoRunActive: false,
|
||||
autoRunCurrentRun: 0,
|
||||
autoRunTotalRuns: 1,
|
||||
autoRunAttemptRun: 0,
|
||||
},
|
||||
get() {
|
||||
return { ...this.state };
|
||||
},
|
||||
set(updates = {}) {
|
||||
this.state = { ...this.state, ...updates };
|
||||
},
|
||||
};
|
||||
|
||||
const controller = self.MultiPageBackgroundAutoRunController.createAutoRunController({
|
||||
addLog,
|
||||
AUTO_RUN_MAX_RETRIES_PER_ROUND,
|
||||
AUTO_RUN_RETRY_DELAY_MS,
|
||||
AUTO_RUN_TIMER_KIND_BEFORE_RETRY,
|
||||
AUTO_RUN_TIMER_KIND_BETWEEN_ROUNDS,
|
||||
broadcastAutoRunStatus,
|
||||
broadcastStopToContentScripts,
|
||||
cancelPendingCommands,
|
||||
clearStopRequest,
|
||||
getAutoRunStatusPayload,
|
||||
getErrorMessage,
|
||||
getFirstUnfinishedStep,
|
||||
getPendingAutoRunTimerPlan,
|
||||
getRunningSteps,
|
||||
getState,
|
||||
getStopRequested: () => stopRequested,
|
||||
hasSavedProgress,
|
||||
isRestartCurrentAttemptError,
|
||||
isStopError,
|
||||
launchAutoRunTimerPlan,
|
||||
normalizeAutoRunFallbackThreadIntervalMinutes,
|
||||
persistAutoRunTimerPlan,
|
||||
resetState,
|
||||
runAutoSequenceFromStep,
|
||||
runtime,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForRunningStepsToFinish,
|
||||
throwIfStopped,
|
||||
chrome,
|
||||
});
|
||||
|
||||
return {
|
||||
autoRunLoop,
|
||||
autoRunLoop: controller.autoRunLoop,
|
||||
snapshot() {
|
||||
return {
|
||||
runCalls,
|
||||
autoRunActive,
|
||||
autoRunCurrentRun,
|
||||
autoRunTotalRuns,
|
||||
autoRunAttemptRun,
|
||||
autoRunActive: runtime.state.autoRunActive,
|
||||
autoRunCurrentRun: runtime.state.autoRunCurrentRun,
|
||||
autoRunTotalRuns: runtime.state.autoRunTotalRuns,
|
||||
autoRunAttemptRun: runtime.state.autoRunAttemptRun,
|
||||
currentState,
|
||||
logs,
|
||||
broadcasts,
|
||||
};
|
||||
},
|
||||
};
|
||||
`)();
|
||||
`)(autoRunModuleSource);
|
||||
|
||||
(async () => {
|
||||
await api.autoRunLoop(2, { autoRunSkipFailures: false, mode: 'restart' });
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports auto-run controller module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/auto-run-controller\.js/);
|
||||
});
|
||||
|
||||
test('auto-run controller module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/auto-run-controller.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundAutoRunController;`)(globalScope);
|
||||
|
||||
assert.equal(typeof api?.createAutoRunController, 'function');
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports logging/status module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/logging-status\.js/);
|
||||
});
|
||||
|
||||
test('logging/status module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/logging-status.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundLoggingStatus;`)(globalScope);
|
||||
|
||||
assert.equal(typeof api?.createLoggingStatus, 'function');
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports message router module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/message-router\.js/);
|
||||
});
|
||||
|
||||
test('message router module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
|
||||
assert.equal(typeof api?.createMessageRouter, 'function');
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports navigation utils module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/navigation-utils\.js/);
|
||||
});
|
||||
|
||||
test('navigation utils module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/navigation-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundNavigationUtils;`)(globalScope);
|
||||
|
||||
assert.equal(typeof api?.createNavigationUtils, 'function');
|
||||
});
|
||||
@@ -2,10 +2,10 @@ const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports step registry module and uses sparse order values', () => {
|
||||
test('background imports step registry and shared step definitions', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/steps\/registry\.js/);
|
||||
assert.match(source, /order:\s*10/);
|
||||
assert.match(source, /order:\s*90/);
|
||||
assert.match(source, /data\/step-definitions\.js/);
|
||||
assert.match(source, /MultiPageStepDefinitions\?\.getSteps/);
|
||||
assert.match(source, /stepRegistry\.executeStep\(step,\s*state\)/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('background imports tab runtime module', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /background\/tab-runtime\.js/);
|
||||
});
|
||||
|
||||
test('tab runtime module exposes a factory', () => {
|
||||
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
|
||||
|
||||
assert.equal(typeof api?.createTabRuntime, 'function');
|
||||
});
|
||||
@@ -1,37 +1,31 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const helperSource = fs.readFileSync('background.js', 'utf8');
|
||||
const tabRuntimeSource = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
function extractFunction(source, 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}`);
|
||||
}
|
||||
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 === ')') {
|
||||
if (ch === '(') parenDepth += 1;
|
||||
else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
if (parenDepth === 0) signatureEnded = true;
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
if (braceStart < 0) throw new Error(`missing body for function ${name}`);
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
@@ -50,16 +44,15 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const bundle = [
|
||||
extractFunction('getTabRegistry'),
|
||||
extractFunction('parseUrlSafely'),
|
||||
extractFunction('isSignupPageHost'),
|
||||
extractFunction('isSignupEntryHost'),
|
||||
extractFunction('matchesSourceUrlFamily'),
|
||||
extractFunction('closeConflictingTabsForSource'),
|
||||
const helperBundle = [
|
||||
extractFunction(helperSource, 'parseUrlSafely'),
|
||||
extractFunction(helperSource, 'isSignupPageHost'),
|
||||
extractFunction(helperSource, 'isSignupEntryHost'),
|
||||
extractFunction(helperSource, 'matchesSourceUrlFamily'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const api = new Function('tabRuntimeSource', `
|
||||
const self = {};
|
||||
let currentState = {
|
||||
sourceLastUrls: {},
|
||||
tabRegistry: {},
|
||||
@@ -96,11 +89,38 @@ function getSourceLabel(source) {
|
||||
return source;
|
||||
}
|
||||
|
||||
${bundle}
|
||||
function isLocalhostOAuthCallbackUrl() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isRetryableContentScriptTransportError() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function throwIfStopped() {}
|
||||
const LOG_PREFIX = '[test:bg]';
|
||||
const STOP_ERROR_MESSAGE = 'Flow stopped.';
|
||||
|
||||
${helperBundle}
|
||||
${tabRuntimeSource}
|
||||
|
||||
const runtime = self.MultiPageBackgroundTabRuntime.createTabRuntime({
|
||||
addLog,
|
||||
chrome,
|
||||
getSourceLabel,
|
||||
getState,
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isRetryableContentScriptTransportError,
|
||||
LOG_PREFIX,
|
||||
matchesSourceUrlFamily,
|
||||
setState,
|
||||
STOP_ERROR_MESSAGE,
|
||||
throwIfStopped,
|
||||
});
|
||||
|
||||
return {
|
||||
matchesSourceUrlFamily,
|
||||
closeConflictingTabsForSource,
|
||||
closeConflictingTabsForSource: runtime.closeConflictingTabsForSource,
|
||||
reset({ tabs, state }) {
|
||||
currentTabs = tabs;
|
||||
removedBatches.length = 0;
|
||||
@@ -120,7 +140,7 @@ return {
|
||||
};
|
||||
},
|
||||
};
|
||||
`)();
|
||||
`)(tabRuntimeSource);
|
||||
|
||||
(async () => {
|
||||
assert.strictEqual(
|
||||
@@ -156,19 +176,11 @@ return {
|
||||
});
|
||||
|
||||
let snapshot = api.snapshot();
|
||||
assert.deepStrictEqual(
|
||||
snapshot.removedBatches,
|
||||
[[1, 2]],
|
||||
'opening auth page should clean up stale ChatGPT entry tabs'
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
snapshot.currentTabs,
|
||||
[
|
||||
{ id: 3, url: 'https://auth.openai.com/authorize?client_id=test' },
|
||||
{ id: 4, url: 'https://example.com/' },
|
||||
],
|
||||
'non-signup tabs and excluded current tab should remain'
|
||||
);
|
||||
assert.deepStrictEqual(snapshot.removedBatches, [[1, 2]]);
|
||||
assert.deepStrictEqual(snapshot.currentTabs, [
|
||||
{ id: 3, url: 'https://auth.openai.com/authorize?client_id=test' },
|
||||
{ id: 4, url: 'https://example.com/' },
|
||||
]);
|
||||
|
||||
api.reset({
|
||||
tabs: [
|
||||
@@ -188,16 +200,8 @@ return {
|
||||
await api.closeConflictingTabsForSource('signup-page', 'https://chatgpt.com/');
|
||||
|
||||
snapshot = api.snapshot();
|
||||
assert.deepStrictEqual(
|
||||
snapshot.removedBatches,
|
||||
[[11, 12]],
|
||||
'opening ChatGPT entry should remove older signup-family tabs'
|
||||
);
|
||||
assert.strictEqual(
|
||||
snapshot.currentState.tabRegistry['signup-page'],
|
||||
null,
|
||||
'registry should be cleared when the tracked signup tab is removed'
|
||||
);
|
||||
assert.deepStrictEqual(snapshot.removedBatches, [[11, 12]]);
|
||||
assert.strictEqual(snapshot.currentState.tabRegistry['signup-page'], null);
|
||||
|
||||
console.log('signup page tab cleanup tests passed');
|
||||
})().catch((error) => {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
test('step definitions module exposes ordered shared step metadata', () => {
|
||||
const source = fs.readFileSync('data/step-definitions.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope);
|
||||
const steps = api.getSteps();
|
||||
|
||||
assert.equal(Array.isArray(steps), true);
|
||||
assert.equal(steps.length >= 9, true);
|
||||
assert.deepStrictEqual(
|
||||
steps.map((step) => step.order),
|
||||
steps.map((step) => step.order).slice().sort((left, right) => left - right)
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
steps.map((step) => step.key),
|
||||
[
|
||||
'open-chatgpt',
|
||||
'submit-signup-email',
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'oauth-login',
|
||||
'fetch-login-code',
|
||||
'confirm-oauth',
|
||||
'platform-verify',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
const definitionsIndex = html.indexOf('<script src="../data/step-definitions.js"></script>');
|
||||
const sidepanelIndex = html.indexOf('<script src="sidepanel.js"></script>');
|
||||
|
||||
assert.notEqual(definitionsIndex, -1);
|
||||
assert.notEqual(sidepanelIndex, -1);
|
||||
assert.ok(definitionsIndex < sidepanelIndex);
|
||||
});
|
||||
@@ -1,37 +1,31 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
const helperSource = fs.readFileSync('background.js', 'utf8');
|
||||
const tabRuntimeSource = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
function extractFunction(source, 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}`);
|
||||
}
|
||||
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++) {
|
||||
const ch = source[i];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
if (ch === '(') parenDepth += 1;
|
||||
else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
if (parenDepth === 0) signatureEnded = true;
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for function ${name}`);
|
||||
}
|
||||
if (braceStart < 0) throw new Error(`missing body for function ${name}`);
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
@@ -50,25 +44,21 @@ function extractFunction(name) {
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
const bundle = [
|
||||
extractFunction('getTabRegistry'),
|
||||
extractFunction('normalizeEmailGenerator'),
|
||||
extractFunction('normalizeMail2925Mode'),
|
||||
extractFunction('getMail2925Mode'),
|
||||
extractFunction('parseUrlSafely'),
|
||||
extractFunction('isHotmailProvider'),
|
||||
extractFunction('isCustomMailProvider'),
|
||||
extractFunction('isGeneratedAliasProvider'),
|
||||
extractFunction('shouldUseCustomRegistrationEmail'),
|
||||
extractFunction('isLocalhostOAuthCallbackUrl'),
|
||||
extractFunction('isLocalhostOAuthCallbackTabMatch'),
|
||||
extractFunction('closeLocalhostCallbackTabs'),
|
||||
extractFunction('buildLocalhostCleanupPrefix'),
|
||||
extractFunction('closeTabsByUrlPrefix'),
|
||||
extractFunction('handleStepData'),
|
||||
const helperBundle = [
|
||||
extractFunction(helperSource, 'normalizeEmailGenerator'),
|
||||
extractFunction(helperSource, 'normalizeMail2925Mode'),
|
||||
extractFunction(helperSource, 'getMail2925Mode'),
|
||||
extractFunction(helperSource, 'parseUrlSafely'),
|
||||
extractFunction(helperSource, 'isHotmailProvider'),
|
||||
extractFunction(helperSource, 'isCustomMailProvider'),
|
||||
extractFunction(helperSource, 'isGeneratedAliasProvider'),
|
||||
extractFunction(helperSource, 'shouldUseCustomRegistrationEmail'),
|
||||
extractFunction(helperSource, 'isLocalhostOAuthCallbackUrl'),
|
||||
extractFunction(helperSource, 'handleStepData'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
const api = new Function('tabRuntimeSource', `
|
||||
const self = {};
|
||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
|
||||
const CLOUDFLARE_TEMP_EMAIL_GENERATOR = 'cloudflare-temp-email';
|
||||
@@ -114,34 +104,50 @@ async function setEmailStateSilently(email) {
|
||||
currentState = { ...currentState, email };
|
||||
}
|
||||
|
||||
function isHotmailProvider() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isLuckmailProvider() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function patchHotmailAccount() {}
|
||||
|
||||
async function clearLuckmailRuntimeState() {}
|
||||
|
||||
function shouldUseCustomRegistrationEmail() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function broadcastDataUpdate() {}
|
||||
|
||||
async function addLog(message) {
|
||||
logMessages.push(message);
|
||||
}
|
||||
|
||||
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
|
||||
function shouldUseCustomRegistrationEmail() {
|
||||
function matchesSourceUrlFamily() {
|
||||
return false;
|
||||
}
|
||||
function getSourceLabel(source) {
|
||||
return source;
|
||||
}
|
||||
function isRetryableContentScriptTransportError() {
|
||||
return false;
|
||||
}
|
||||
function throwIfStopped() {}
|
||||
const LOG_PREFIX = '[test:bg]';
|
||||
const STOP_ERROR_MESSAGE = 'Flow stopped.';
|
||||
|
||||
${bundle}
|
||||
${helperBundle}
|
||||
${tabRuntimeSource}
|
||||
|
||||
const tabRuntime = self.MultiPageBackgroundTabRuntime.createTabRuntime({
|
||||
addLog,
|
||||
chrome,
|
||||
getSourceLabel,
|
||||
getState,
|
||||
isLocalhostOAuthCallbackUrl,
|
||||
isRetryableContentScriptTransportError,
|
||||
LOG_PREFIX,
|
||||
matchesSourceUrlFamily,
|
||||
setState,
|
||||
STOP_ERROR_MESSAGE,
|
||||
throwIfStopped,
|
||||
});
|
||||
const closeLocalhostCallbackTabs = tabRuntime.closeLocalhostCallbackTabs;
|
||||
const isLocalhostOAuthCallbackTabMatch = tabRuntime.isLocalhostOAuthCallbackTabMatch;
|
||||
const buildLocalhostCleanupPrefix = tabRuntime.buildLocalhostCleanupPrefix;
|
||||
const closeTabsByUrlPrefix = tabRuntime.closeTabsByUrlPrefix;
|
||||
|
||||
return {
|
||||
handleStepData,
|
||||
@@ -163,27 +169,15 @@ return {
|
||||
};
|
||||
},
|
||||
};
|
||||
`)();
|
||||
`)(tabRuntimeSource);
|
||||
|
||||
(async () => {
|
||||
const codexCallbackUrl = 'http://127.0.0.1:8317/codex/callback?code=abc&state=xyz';
|
||||
const authCallbackUrl = 'http://localhost:1455/auth/callback?code=def&state=uvw';
|
||||
|
||||
assert.strictEqual(
|
||||
api.isLocalhostOAuthCallbackTabMatch(codexCallbackUrl, codexCallbackUrl),
|
||||
true,
|
||||
'真实 callback 页应命中清理规则'
|
||||
);
|
||||
assert.strictEqual(
|
||||
api.isLocalhostOAuthCallbackTabMatch(codexCallbackUrl, authCallbackUrl),
|
||||
false,
|
||||
'/codex/callback 不应误伤 /auth/callback'
|
||||
);
|
||||
assert.strictEqual(
|
||||
api.isLocalhostOAuthCallbackTabMatch(authCallbackUrl, codexCallbackUrl),
|
||||
false,
|
||||
'/auth/callback 不应误伤 /codex/callback'
|
||||
);
|
||||
assert.strictEqual(api.isLocalhostOAuthCallbackTabMatch(codexCallbackUrl, codexCallbackUrl), true);
|
||||
assert.strictEqual(api.isLocalhostOAuthCallbackTabMatch(codexCallbackUrl, authCallbackUrl), false);
|
||||
assert.strictEqual(api.isLocalhostOAuthCallbackTabMatch(authCallbackUrl, codexCallbackUrl), false);
|
||||
|
||||
api.reset({
|
||||
tabs: [
|
||||
@@ -200,21 +194,9 @@ return {
|
||||
|
||||
await api.handleStepData(9, { localhostUrl: codexCallbackUrl });
|
||||
let snapshot = api.snapshot();
|
||||
assert.deepStrictEqual(
|
||||
snapshot.removedBatches,
|
||||
[[1], [2]],
|
||||
'handleStepData(9) 应先关闭当前 callback 页,再按同源首段路径清理残留页'
|
||||
);
|
||||
assert.strictEqual(
|
||||
snapshot.currentState.tabRegistry['signup-page'],
|
||||
null,
|
||||
'关闭 callback 页后应同步清理 signup-page 的 tabRegistry'
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
snapshot.currentState.tabRegistry['vps-panel'],
|
||||
{ tabId: 99, ready: true },
|
||||
'不相关的 tabRegistry 项不应被误清理'
|
||||
);
|
||||
assert.deepStrictEqual(snapshot.removedBatches, [[1], [2]]);
|
||||
assert.strictEqual(snapshot.currentState.tabRegistry['signup-page'], null);
|
||||
assert.deepStrictEqual(snapshot.currentState.tabRegistry['vps-panel'], { tabId: 99, ready: true });
|
||||
|
||||
api.reset({
|
||||
tabs: [
|
||||
@@ -227,9 +209,9 @@ return {
|
||||
|
||||
const closedCount = await api.closeLocalhostCallbackTabs(authCallbackUrl);
|
||||
snapshot = api.snapshot();
|
||||
assert.strictEqual(closedCount, 1, 'auth callback 也应只关闭当前命中的 callback 页');
|
||||
assert.deepStrictEqual(snapshot.removedBatches, [[4]], '不应按 /auth 前缀批量清理页面');
|
||||
assert.strictEqual(snapshot.logMessages.length, 1, '发生清理时应记录一条日志');
|
||||
assert.strictEqual(closedCount, 1);
|
||||
assert.deepStrictEqual(snapshot.removedBatches, [[4]]);
|
||||
assert.strictEqual(snapshot.logMessages.length, 1);
|
||||
|
||||
console.log('step9 localhost cleanup scope tests passed');
|
||||
})().catch((error) => {
|
||||
|
||||
Reference in New Issue
Block a user