Merge pull request #261 from QLHazyCoder/fix/start-crash-on-auto-run-and-autosave-jitter
修复启动清理与设置自动保存抖动
This commit is contained in:
@@ -9,14 +9,6 @@
|
||||
'auth0.openai.com',
|
||||
'accounts.openai.com',
|
||||
];
|
||||
const STEP1_COOKIE_CLEAR_ORIGINS = [
|
||||
'https://chatgpt.com',
|
||||
'https://chat.openai.com',
|
||||
'https://auth.openai.com',
|
||||
'https://auth0.openai.com',
|
||||
'https://accounts.openai.com',
|
||||
'https://openai.com',
|
||||
];
|
||||
|
||||
function normalizeCookieDomainForStep1(domain) {
|
||||
return String(domain || '').trim().replace(/^\.+/, '').toLowerCase();
|
||||
@@ -30,6 +22,16 @@
|
||||
));
|
||||
}
|
||||
|
||||
function buildStep1CookieKey(cookie, fallbackStoreId = '') {
|
||||
return [
|
||||
cookie?.storeId || fallbackStoreId || '',
|
||||
cookie?.domain || '',
|
||||
cookie?.path || '',
|
||||
cookie?.name || '',
|
||||
cookie?.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
}
|
||||
|
||||
function buildStep1CookieRemovalUrl(cookie) {
|
||||
const host = normalizeCookieDomainForStep1(cookie?.domain);
|
||||
const rawPath = String(cookie?.path || '/');
|
||||
@@ -51,22 +53,39 @@
|
||||
: [{ id: undefined }];
|
||||
const cookies = [];
|
||||
const seen = new Set();
|
||||
const queryDomains = Array.from(
|
||||
new Set(
|
||||
STEP1_COOKIE_CLEAR_DOMAINS
|
||||
.map((domain) => normalizeCookieDomainForStep1(domain))
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
|
||||
for (const store of stores) {
|
||||
const storeId = store?.id;
|
||||
const batch = await chromeApi.cookies.getAll(storeId ? { storeId } : {});
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearStep1Cookie(cookie)) continue;
|
||||
const key = [
|
||||
cookie.storeId || storeId || '',
|
||||
cookie.domain || '',
|
||||
cookie.path || '',
|
||||
cookie.name || '',
|
||||
cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : '',
|
||||
].join('|');
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
cookies.push(cookie);
|
||||
for (const domain of queryDomains) {
|
||||
let batch = [];
|
||||
try {
|
||||
batch = await chromeApi.cookies.getAll(
|
||||
storeId
|
||||
? { storeId, domain }
|
||||
: { domain }
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[MultiPage:step1] query cookies failed', {
|
||||
storeId: storeId || '',
|
||||
domain,
|
||||
message: getStep1ErrorMessage(error),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const cookie of batch || []) {
|
||||
if (!shouldClearStep1Cookie(cookie)) continue;
|
||||
const key = buildStep1CookieKey(cookie, storeId);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
cookies.push(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +131,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
await addLog('步骤 1:打开 ChatGPT 官网前清理 ChatGPT / OpenAI cookies...', 'info');
|
||||
const cookies = await collectStep1Cookies(chromeApi);
|
||||
let removedCount = 0;
|
||||
@@ -121,18 +141,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (chromeApi.browsingData?.removeCookies) {
|
||||
try {
|
||||
await chromeApi.browsingData.removeCookies({
|
||||
since: 0,
|
||||
origins: STEP1_COOKIE_CLEAR_ORIGINS,
|
||||
});
|
||||
} catch (error) {
|
||||
await addLog(`步骤 1:browsingData 补扫 cookies 失败:${getStep1ErrorMessage(error)}`, 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies。`, 'ok');
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
await addLog(`步骤 1:已清理 ${removedCount} 个 ChatGPT / OpenAI cookies(耗时 ${elapsedMs}ms)。`, 'ok');
|
||||
}
|
||||
|
||||
async function executeStep1() {
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
}
|
||||
|
||||
async function closeConflictingTabsForSource(source, currentUrl, options = {}) {
|
||||
const { excludeTabIds = [] } = options;
|
||||
const { excludeTabIds = [], preserveActiveTab = false } = options;
|
||||
const excluded = new Set(excludeTabIds.filter((id) => Number.isInteger(id)));
|
||||
const state = await getState();
|
||||
const lastUrl = getSourceMapValue(state.sourceLastUrls, source);
|
||||
@@ -299,8 +299,12 @@
|
||||
if (!referenceUrls.length) return;
|
||||
|
||||
const tabs = await queryTabsInAutomationWindow({});
|
||||
const activeTabId = preserveActiveTab
|
||||
? tabs.find((tab) => Boolean(tab?.active) && Number.isInteger(tab?.id))?.id || null
|
||||
: null;
|
||||
const matchedIds = tabs
|
||||
.filter((tab) => Number.isInteger(tab.id) && !excluded.has(tab.id))
|
||||
.filter((tab) => !(preserveActiveTab && activeTabId !== null && tab.id === activeTabId))
|
||||
.filter((tab) => referenceUrls.some((refUrl) => matchesSourceUrlFamily(source, tab.url, refUrl)))
|
||||
.map((tab) => tab.id);
|
||||
|
||||
@@ -728,8 +732,11 @@
|
||||
|
||||
async function reuseOrCreateTab(source, url, options = {}) {
|
||||
if (options.forceNew) {
|
||||
await closeConflictingTabsForSource(source, url);
|
||||
const tab = await createAutomationTab({ url, active: true }, options);
|
||||
await closeConflictingTabsForSource(source, url, {
|
||||
excludeTabIds: [tab.id],
|
||||
preserveActiveTab: false,
|
||||
});
|
||||
|
||||
if (options.inject) {
|
||||
await waitForTabUpdateComplete(tab.id);
|
||||
@@ -755,7 +762,10 @@
|
||||
const alive = await isTabAlive(source);
|
||||
if (alive) {
|
||||
const tabId = await getTabId(source);
|
||||
await closeConflictingTabsForSource(source, url, { excludeTabIds: [tabId] });
|
||||
await closeConflictingTabsForSource(source, url, {
|
||||
excludeTabIds: [tabId],
|
||||
preserveActiveTab: false,
|
||||
});
|
||||
const currentTab = await chrome.tabs.get(tabId);
|
||||
const sameUrl = currentTab.url === url;
|
||||
const shouldReloadOnReuse = sameUrl && options.reloadIfSameUrl;
|
||||
@@ -836,8 +846,11 @@
|
||||
return tabId;
|
||||
}
|
||||
|
||||
await closeConflictingTabsForSource(source, url);
|
||||
const tab = await createAutomationTab({ url, active: true }, options);
|
||||
await closeConflictingTabsForSource(source, url, {
|
||||
excludeTabIds: [tab.id],
|
||||
preserveActiveTab: false,
|
||||
});
|
||||
|
||||
if (options.inject) {
|
||||
await waitForTabUpdateComplete(tab.id);
|
||||
|
||||
+47
-4
@@ -9950,15 +9950,31 @@ function updateSaveButtonState() {
|
||||
btnSaveSettings.textContent = settingsSaveInFlight ? '保存中' : '保存';
|
||||
}
|
||||
|
||||
function isEditableElementInSettingsCard(element) {
|
||||
if (!element || !(element instanceof Element)) {
|
||||
return false;
|
||||
}
|
||||
const tagName = String(element.tagName || '').toLowerCase();
|
||||
const isEditableInput = (
|
||||
tagName === 'textarea'
|
||||
|| (tagName === 'input' && !['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'file', 'color'].includes(String(element.type || '').toLowerCase()))
|
||||
|| Boolean(element.isContentEditable)
|
||||
);
|
||||
if (!isEditableInput) {
|
||||
return false;
|
||||
}
|
||||
return !settingsCard || settingsCard.contains(element);
|
||||
}
|
||||
|
||||
function scheduleSettingsAutoSave() {
|
||||
clearTimeout(settingsAutoSaveTimer);
|
||||
settingsAutoSaveTimer = setTimeout(() => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
}, 500);
|
||||
saveSettings({ silent: true, source: 'autosave' }).catch(() => { });
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
async function saveSettings(options = {}) {
|
||||
const { silent = false, force = false } = options;
|
||||
const { silent = false, force = false, source = '' } = options;
|
||||
clearTimeout(settingsAutoSaveTimer);
|
||||
|
||||
if (!force && !settingsDirty && !settingsSaveInFlight && silent) {
|
||||
@@ -9970,6 +9986,14 @@ async function saveSettings(options = {}) {
|
||||
settingsSaveInFlight = true;
|
||||
updateSaveButtonState();
|
||||
|
||||
const shouldSkipStateApplyForFocusedEditor = (() => {
|
||||
if (!silent || source !== 'autosave') {
|
||||
return false;
|
||||
}
|
||||
const activeEl = typeof document !== 'undefined' ? document.activeElement : null;
|
||||
return isEditableElementInSettingsCard(activeEl);
|
||||
})();
|
||||
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SETTING',
|
||||
@@ -9982,7 +10006,12 @@ async function saveSettings(options = {}) {
|
||||
}
|
||||
|
||||
if (response?.state && saveRevision === settingsSaveRevision) {
|
||||
applySettingsState(response.state);
|
||||
if (shouldSkipStateApplyForFocusedEditor) {
|
||||
syncLatestState(response.state);
|
||||
markSettingsDirty(false);
|
||||
} else {
|
||||
applySettingsState(response.state);
|
||||
}
|
||||
} else {
|
||||
syncLatestState(payload);
|
||||
if (saveRevision === settingsSaveRevision) {
|
||||
@@ -16579,6 +16608,20 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
|
||||
case 'DATA_UPDATED': {
|
||||
syncLatestState(message.payload);
|
||||
const activeSettingsEditor = typeof document !== 'undefined' ? document.activeElement : null;
|
||||
const shouldDeferDataUpdatedUiApply = settingsSaveInFlight
|
||||
&& isEditableElementInSettingsCard(activeSettingsEditor);
|
||||
if (shouldDeferDataUpdatedUiApply) {
|
||||
// Avoid overwriting the focused editor while the current save request
|
||||
// is still in flight; otherwise typing can be interrupted by DATA_UPDATED.
|
||||
if (message.payload.operationDelayEnabled !== undefined && typeof applyOperationDelayState === 'function') {
|
||||
applyOperationDelayState(message.payload);
|
||||
}
|
||||
updateAccountRunHistorySettingsUI();
|
||||
renderContributionMode();
|
||||
void syncPlusManualConfirmationDialog();
|
||||
break;
|
||||
}
|
||||
if (message.payload.operationDelayEnabled !== undefined && typeof applyOperationDelayState === 'function') {
|
||||
applyOperationDelayState(message.payload);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadStep1Module() {
|
||||
const source = fs.readFileSync('background/steps/open-chatgpt.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.MultiPageBackgroundStep1;`)(globalScope);
|
||||
}
|
||||
|
||||
test('step 1 cookie cleanup queries target domains and skips browsingData sweep when direct removals succeed', async () => {
|
||||
const api = loadStep1Module();
|
||||
const events = {
|
||||
getAllCalls: [],
|
||||
removedCookies: [],
|
||||
browsingDataCalls: [],
|
||||
openedSteps: [],
|
||||
completedNodes: [],
|
||||
};
|
||||
|
||||
const chromeApi = {
|
||||
cookies: {
|
||||
getAllCookieStores: async () => [{ id: 'store-a' }],
|
||||
getAll: async (query) => {
|
||||
events.getAllCalls.push(query);
|
||||
if (query?.domain === 'chatgpt.com') {
|
||||
return [
|
||||
{ domain: '.chatgpt.com', path: '/', name: 'session', storeId: 'store-a' },
|
||||
];
|
||||
}
|
||||
if (query?.domain === 'openai.com') {
|
||||
return [
|
||||
{
|
||||
domain: '.openai.com',
|
||||
path: '/',
|
||||
name: 'shared',
|
||||
storeId: 'store-a',
|
||||
partitionKey: { topLevelSite: 'https://chatgpt.com' },
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
remove: async (details) => {
|
||||
events.removedCookies.push(details);
|
||||
return details;
|
||||
},
|
||||
},
|
||||
browsingData: {
|
||||
removeCookies: async (details) => {
|
||||
events.browsingDataCalls.push(details);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const executor = api.createStep1Executor({
|
||||
addLog: async () => {},
|
||||
chrome: chromeApi,
|
||||
openSignupEntryTab: async (step) => {
|
||||
events.openedSteps.push(step);
|
||||
},
|
||||
completeNodeFromBackground: async (nodeId) => {
|
||||
events.completedNodes.push(nodeId);
|
||||
},
|
||||
});
|
||||
|
||||
await executor.executeStep1();
|
||||
|
||||
assert.ok(events.getAllCalls.length > 0, 'should query cookies at least once');
|
||||
assert.ok(events.getAllCalls.every((entry) => typeof entry?.domain === 'string' && entry.domain.length > 0));
|
||||
assert.deepStrictEqual(events.removedCookies, [
|
||||
{
|
||||
url: 'https://chatgpt.com/',
|
||||
name: 'session',
|
||||
storeId: 'store-a',
|
||||
},
|
||||
{
|
||||
url: 'https://openai.com/',
|
||||
name: 'shared',
|
||||
storeId: 'store-a',
|
||||
partitionKey: { topLevelSite: 'https://chatgpt.com' },
|
||||
},
|
||||
]);
|
||||
assert.deepStrictEqual(events.browsingDataCalls, []);
|
||||
assert.deepStrictEqual(events.openedSteps, [1]);
|
||||
assert.deepStrictEqual(events.completedNodes, ['open-chatgpt']);
|
||||
});
|
||||
|
||||
test('step 1 cookie cleanup skips browsingData sweep when no direct cookie is removed', async () => {
|
||||
const api = loadStep1Module();
|
||||
const events = {
|
||||
removedCookies: 0,
|
||||
browsingDataCalls: [],
|
||||
};
|
||||
|
||||
const chromeApi = {
|
||||
cookies: {
|
||||
getAllCookieStores: async () => [{ id: 'store-a' }],
|
||||
getAll: async () => [],
|
||||
remove: async () => {
|
||||
events.removedCookies += 1;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
browsingData: {
|
||||
removeCookies: async (details) => {
|
||||
events.browsingDataCalls.push(details);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const executor = api.createStep1Executor({
|
||||
addLog: async () => {},
|
||||
chrome: chromeApi,
|
||||
openSignupEntryTab: async () => {},
|
||||
completeNodeFromBackground: async () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep1();
|
||||
|
||||
assert.equal(events.removedCookies, 0);
|
||||
assert.equal(events.browsingDataCalls.length, 0);
|
||||
});
|
||||
@@ -383,6 +383,61 @@ test('tab runtime opens new automation tabs in the locked window', async () => {
|
||||
assert.equal(created[0].windowId, 100);
|
||||
});
|
||||
|
||||
test('tab runtime force-new opens replacement before removing the active stale source tab', async () => {
|
||||
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundTabRuntime;`)(globalScope);
|
||||
const events = [];
|
||||
let tabs = [
|
||||
{ id: 1, active: true, windowId: 100, url: 'https://chatgpt.com/' },
|
||||
];
|
||||
const runtime = api.createTabRuntime({
|
||||
LOG_PREFIX: '[test]',
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async (payload) => {
|
||||
events.push({ type: 'create', payload });
|
||||
const tab = { id: 2, active: true, windowId: payload.windowId, url: payload.url };
|
||||
tabs = tabs.map((item) => ({ ...item, active: false })).concat(tab);
|
||||
return tab;
|
||||
},
|
||||
get: async (tabId) => tabs.find((tab) => tab.id === tabId),
|
||||
query: async () => tabs,
|
||||
remove: async (ids) => {
|
||||
events.push({ type: 'remove', ids });
|
||||
tabs = tabs.filter((tab) => !ids.includes(tab.id));
|
||||
},
|
||||
},
|
||||
},
|
||||
getSourceLabel: (sourceName) => sourceName || 'unknown',
|
||||
getState: async () => ({
|
||||
automationWindowId: 100,
|
||||
sourceLastUrls: { 'signup-page': 'https://chatgpt.com/' },
|
||||
tabRegistry: {},
|
||||
}),
|
||||
matchesSourceUrlFamily: (sourceName, candidateUrl) => (
|
||||
sourceName === 'signup-page'
|
||||
&& /chatgpt\.com|auth\.openai\.com/.test(String(candidateUrl || ''))
|
||||
),
|
||||
setState: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
const tabId = await runtime.reuseOrCreateTab('signup-page', 'https://auth.openai.com/authorize', {
|
||||
forceNew: true,
|
||||
});
|
||||
|
||||
assert.equal(tabId, 2);
|
||||
assert.deepEqual(events, [
|
||||
{ type: 'create', payload: { url: 'https://auth.openai.com/authorize', active: true, windowId: 100 } },
|
||||
{ type: 'remove', ids: [1] },
|
||||
]);
|
||||
assert.deepEqual(tabs, [
|
||||
{ id: 2, active: true, windowId: 100, url: 'https://auth.openai.com/authorize' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('tab runtime scopes tab queries to the locked automation window', async () => {
|
||||
const source = fs.readFileSync('background/tab-runtime.js', 'utf8');
|
||||
const globalScope = {};
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
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);
|
||||
assert.notEqual(start, -1, `missing ${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;
|
||||
if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) signatureEnded = true;
|
||||
}
|
||||
if (ch === '{' && signatureEnded) {
|
||||
braceStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
for (let i = braceStart; i < source.length; i += 1) {
|
||||
const ch = source[i];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return source.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`unterminated ${name}`);
|
||||
}
|
||||
|
||||
test('focused settings editor still autosaves without repainting the focused field', async () => {
|
||||
const bundle = [
|
||||
extractFunction('isEditableElementInSettingsCard'),
|
||||
extractFunction('scheduleSettingsAutoSave'),
|
||||
extractFunction('saveSettings'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
class MockElement {
|
||||
constructor(tagName, type = '') {
|
||||
this.tagName = tagName;
|
||||
this.type = type;
|
||||
this.isContentEditable = false;
|
||||
}
|
||||
}
|
||||
const Element = MockElement;
|
||||
const focusedInput = new MockElement('input', 'text');
|
||||
const settingsCard = { contains: (element) => element === focusedInput };
|
||||
const document = { activeElement: focusedInput };
|
||||
let settingsAutoSaveTimer = null;
|
||||
let scheduled = null;
|
||||
let clearedTimer = null;
|
||||
let settingsDirty = true;
|
||||
let settingsSaveInFlight = false;
|
||||
let settingsSaveRevision = 1;
|
||||
const messages = [];
|
||||
const syncedStates = [];
|
||||
const dirtyMarks = [];
|
||||
let applyCalls = 0;
|
||||
function setTimeout(fn, delay) {
|
||||
scheduled = { fn, delay };
|
||||
return 71;
|
||||
}
|
||||
function clearTimeout(value) {
|
||||
clearedTimer = value;
|
||||
}
|
||||
function collectSettingsPayload() {
|
||||
return { vpsUrl: 'typed-value' };
|
||||
}
|
||||
function syncLatestState(state) {
|
||||
syncedStates.push(state);
|
||||
}
|
||||
function markSettingsDirty(value = true) {
|
||||
settingsDirty = value;
|
||||
if (value) settingsSaveRevision += 1;
|
||||
dirtyMarks.push(value);
|
||||
}
|
||||
function applySettingsState() {
|
||||
applyCalls += 1;
|
||||
}
|
||||
function updateSaveButtonState() {}
|
||||
function updatePanelModeUI() {}
|
||||
function updateMailProviderUI() {}
|
||||
function updateButtonStates() {}
|
||||
const chrome = {
|
||||
runtime: {
|
||||
async sendMessage(message) {
|
||||
messages.push(message);
|
||||
return { state: { vpsUrl: message.payload.vpsUrl } };
|
||||
},
|
||||
},
|
||||
};
|
||||
${bundle}
|
||||
return {
|
||||
scheduleSettingsAutoSave,
|
||||
async fireScheduled() {
|
||||
await scheduled.fn();
|
||||
},
|
||||
getSnapshot() {
|
||||
return {
|
||||
applyCalls,
|
||||
clearedTimer,
|
||||
dirtyMarks,
|
||||
messages,
|
||||
scheduledDelay: scheduled?.delay,
|
||||
syncedStates,
|
||||
};
|
||||
},
|
||||
};
|
||||
`)();
|
||||
|
||||
api.scheduleSettingsAutoSave();
|
||||
let snapshot = api.getSnapshot();
|
||||
assert.equal(snapshot.scheduledDelay, 1200);
|
||||
assert.equal(snapshot.messages.length, 0);
|
||||
|
||||
await api.fireScheduled();
|
||||
snapshot = api.getSnapshot();
|
||||
|
||||
assert.equal(snapshot.clearedTimer, 71);
|
||||
assert.equal(snapshot.applyCalls, 0);
|
||||
assert.deepEqual(snapshot.messages, [
|
||||
{
|
||||
type: 'SAVE_SETTING',
|
||||
source: 'sidepanel',
|
||||
payload: { vpsUrl: 'typed-value' },
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(snapshot.syncedStates, [{ vpsUrl: 'typed-value' }]);
|
||||
assert.deepEqual(snapshot.dirtyMarks, [false]);
|
||||
});
|
||||
@@ -209,6 +209,25 @@ return {
|
||||
assert.deepStrictEqual(snapshot.removedBatches, [[11, 12]]);
|
||||
assert.strictEqual(snapshot.currentState.tabRegistry['signup-page'], null);
|
||||
|
||||
api.reset({
|
||||
tabs: [
|
||||
{ id: 21, active: true, url: 'https://chatgpt.com/' },
|
||||
{ id: 22, active: false, url: 'https://auth.openai.com/authorize?client_id=test' },
|
||||
],
|
||||
state: {
|
||||
sourceLastUrls: {
|
||||
'signup-page': 'https://auth.openai.com/authorize?client_id=test',
|
||||
},
|
||||
tabRegistry: {},
|
||||
},
|
||||
});
|
||||
|
||||
await api.closeConflictingTabsForSource('signup-page', 'https://chatgpt.com/');
|
||||
|
||||
snapshot = api.snapshot();
|
||||
assert.deepStrictEqual(snapshot.removedBatches, [[21, 22]]);
|
||||
assert.deepStrictEqual(snapshot.currentTabs, []);
|
||||
|
||||
console.log('signup page tab cleanup tests passed');
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
|
||||
Reference in New Issue
Block a user