Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const DEFAULT_TARGETS = ['instagram.com', 'reddit.com', 'youtube.com'];
const processingTabs = new Set(); // Tracks tabs currently being processed (short-lived lock)
const pendingPromptTabs = new Set(); // Tracks tabs redirected to prompt.html, waiting for session start
const tabsCurrentlyAtPrompt = new Set(); // Tracks tabs that have committed to prompt.html

// --- Time Range Helpers ---

Expand Down Expand Up @@ -119,17 +120,23 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
// when status events fire after a redirect has already been issued.
const currentUrl = changeInfo.url || tab.url;

// If the tab is heading to or already at the prompt, leave pendingPromptTabs intact and stop.
if (currentUrl.startsWith(chrome.runtime.getURL('prompt.html'))) return;
// If the tab has committed to the prompt page, mark it and clear the debounce lock
// (the redirect is complete; the lock is no longer needed).
if (currentUrl.startsWith(chrome.runtime.getURL('prompt.html'))) {
tabsCurrentlyAtPrompt.add(tabId);
processingTabs.delete(tabId);
return;
}

if (pendingPromptTabs.has(tabId)) {
if (changeInfo.url) {
// A fresh URL navigation away from the prompt — clear pending state and process normally.
pendingPromptTabs.delete(tabId);
} else {
// Stale status (loading/complete) event for a tab still waiting at the prompt — skip.
return;
}
// If the tab was genuinely at prompt.html and is now navigating away (e.g. after starting a session):
if (tabsCurrentlyAtPrompt.has(tabId)) {
tabsCurrentlyAtPrompt.delete(tabId);
pendingPromptTabs.delete(tabId);
// Fall through — process this navigation normally
} else if (pendingPromptTabs.has(tabId)) {
// Tab was redirected to prompt but hasn't committed there yet — this is a redirect-chain
// event (e.g. youtube.com → www.youtube.com) arriving before the tab reaches prompt.html.
return;
}

if (processingTabs.has(tabId)) return;
Expand Down Expand Up @@ -455,6 +462,7 @@ chrome.alarms.onAlarm.addListener(async (alarm) => {
// Handle Messages from Prompt or Content Script
chrome.tabs.onRemoved.addListener((tabId) => {
pendingPromptTabs.delete(tabId);
tabsCurrentlyAtPrompt.delete(tabId);
processingTabs.delete(tabId);
});

Expand Down
42 changes: 35 additions & 7 deletions tests/flows.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Cross-site and shared flows: time ranges, pendingPromptTabs, alarms, startSession message.
const {
loadBackground, fireUpdated, fireRemoved, fireAlarm, setStorage,
loadBackground, fireUpdated, fireRemoved, fireCommitted, fireAlarm, setStorage,
expectPromptRedirect, expectNoRedirect, flushPromises, NOW,
} = require('./helpers');

Expand Down Expand Up @@ -125,12 +125,15 @@ test('pendingPromptTabs: stale status:complete after redirect does not double-pr
});

test('pendingPromptTabs: fresh URL navigation away from prompt clears pending state', async () => {
// Redirect to prompt
// Step 1: navigate to YT — no session → redirect to prompt
await nav(TAB, YT_HOME);
expect(__mockFns__['tabs.update'].mock.calls.length).toBe(1);

// User starts a session — simulate what prompt.js does: sends startSession message,
// then calls window.location.replace(intendedUrl) which fires a changeInfo.url event
// Step 2: tab commits to prompt.html — this is required before any away-navigation is processed
const promptUrl = `chrome-extension://fakeid/prompt.html?url=${encodeURIComponent(YT_HOME)}`;
await fireUpdated(TAB, { url: promptUrl }, { url: promptUrl, status: 'loading' });

// Step 3: user starts a session via the prompt UI
setStorage({
activeSessions: {
'youtube.com': {
Expand All @@ -142,13 +145,38 @@ test('pendingPromptTabs: fresh URL navigation away from prompt clears pending st
},
});

// The replace fires a new URL navigation event — should clear pendingPromptTabs
// and then isTargetSite check should find an active sessionno redirect
// Step 4: prompt.js calls window.location.replace(intendedUrl) → tab navigates back to YT
// Should clear pending state, find an active session, and allow access (no new redirect)
await fireUpdated(TAB, { url: YT_HOME }, { url: YT_HOME });
// Active session exists → should be allowed (no new redirect)
expect(__mockFns__['tabs.update'].mock.calls.length).toBe(1);
});

test('pendingPromptTabs: www-redirect chain does not double-prompt', async () => {
// Step 1: navigate to bare domain (no www) — triggers redirect to prompt
await nav(TAB, 'https://youtube.com/');
expect(__mockFns__['tabs.update'].mock.calls.length).toBe(1);

// Step 2: browser fires www-redirect BEFORE tab commits to prompt.html
// This must be suppressed — tab hasn't reached the prompt page yet
await nav(TAB, 'https://www.youtube.com/');
expect(__mockFns__['tabs.update'].mock.calls.length).toBe(1); // still only 1
});

test('pendingPromptTabs: back button without session re-shows prompt', async () => {
// Step 1: navigate to YT — no session → redirect to prompt
await nav(TAB, YT_HOME);
expect(__mockFns__['tabs.update'].mock.calls.length).toBe(1);

// Step 2: tab commits to prompt.html
const promptUrl = `chrome-extension://fakeid/prompt.html?url=${encodeURIComponent(YT_HOME)}`;
await fireUpdated(TAB, { url: promptUrl }, { url: promptUrl, status: 'loading' });

// Step 3: user presses back button without starting a session — tab returns to YT
await nav(TAB, YT_HOME);
// Should show prompt again — no valid session was started
expect(__mockFns__['tabs.update'].mock.calls.length).toBe(2);
});

test('pendingPromptTabs: tab removed cleans up state', async () => {
await nav(TAB, YT_HOME);
expect(__mockFns__['tabs.update'].mock.calls.length).toBe(1);
Expand Down
7 changes: 7 additions & 0 deletions tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ function fireMessage(message, sender = {}) {
});
}

// Fire a webNavigation.onCommitted event.
async function fireCommitted({ tabId, url, frameId = 0 }) {
const handlers = global.__listeners__.onCommitted;
await Promise.all(handlers.map(fn => fn({ tabId, url, frameId })));
}

// Fire an alarm event.
async function fireAlarm(alarm) {
const handlers = global.__listeners__.onAlarm;
Expand Down Expand Up @@ -85,6 +91,7 @@ module.exports = {
loadBackground,
fireUpdated,
fireRemoved,
fireCommitted,
fireMessage,
fireAlarm,
setStorage,
Expand Down
5 changes: 4 additions & 1 deletion tests/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Test files access storage via global.__store__ and listeners via global.__listeners__.

const store = {};
const listeners = { onUpdated: [], onAlarm: [], onMessage: [], onRemoved: [] };
const listeners = { onUpdated: [], onAlarm: [], onMessage: [], onRemoved: [], onCommitted: [] };
const mockFns = {};

function makeMockFn(key) {
Expand Down Expand Up @@ -74,4 +74,7 @@ global.chrome = {
onAlarm: { addListener: (fn) => listeners.onAlarm.push(fn) },
create: (...args) => mockFns['alarms.create'](...args),
},
webNavigation: {
onCommitted: { addListener: (fn) => listeners.onCommitted.push(fn) },
},
};