Skip to content

Commit 2427fb6

Browse files
Kirill LebedenkoKirill Lebedenko
authored andcommitted
feat show badge count based on headers applied to current page When URL filters are configured the extension icon badge now shows 0 empty if the current page doesn't match any filter and the actual header count only when headers are actively being injected - Add countActiveHeadersForUrl utility with doesUrlMatchFilter - Update setBrowserHeaders to pass current tab URL to badge logic - Add tabsonUpdated listener to refresh badge on navigation - Add 'tabs' permission to all manifests
1 parent a72afe3 commit 2427fb6

9 files changed

Lines changed: 325 additions & 66 deletions

manifest.chromium.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"permissions": [
2424
"storage",
2525
"declarativeNetRequest",
26-
"declarativeNetRequestFeedback"
26+
"declarativeNetRequestFeedback",
27+
"tabs"
2728
],
2829
"background": {
2930
"service_worker": "background.bundle.js"

manifest.dev.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"permissions": [
2222
"storage",
2323
"declarativeNetRequest",
24-
"declarativeNetRequestFeedback"
24+
"declarativeNetRequestFeedback",
25+
"tabs"
2526
],
2627
"background": {
2728
"service_worker": "background.bundle.js"

manifest.firefox.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"storage",
2121
"declarativeNetRequest",
2222
"declarativeNetRequestFeedback",
23-
"activeTab"
23+
"activeTab",
24+
"tabs"
2425
],
2526
"host_permissions": [
2627
"<all_urls>"

src/background.ts

Lines changed: 42 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import browser from 'webextension-polyfill';
22

3-
import type { Profile, RequestHeader } from '#entities/request-profile/types';
4-
53
import { BrowserStorageKey, ServiceWorkerEvent } from './shared/constants';
64
import { browserAction } from './shared/utils/browserAPI';
75
import { logger, LogLevel } from './shared/utils/logger';
86
import { setBrowserHeaders } from './shared/utils/setBrowserHeaders';
9-
import { setIconBadge } from './shared/utils/setIconBadge';
107
import { enableExtensionReload } from './utils/extension-reload';
118

129
logger.configure({
@@ -21,6 +18,15 @@ logger.info('🎯 Background script loaded successfully!');
2118
logger.debug('🎯 Background script loaded successfully! (debug)');
2219
logger.info('🔍 About to check storage contents...');
2320

21+
async function getCurrentTabUrl(): Promise<string | undefined> {
22+
try {
23+
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
24+
return tabs[0]?.url;
25+
} catch {
26+
return undefined;
27+
}
28+
}
29+
2430
// Check storage immediately on background script load
2531
(async () => {
2632
try {
@@ -34,35 +40,13 @@ logger.info('🔍 About to check storage contents...');
3440
logger.info(' - Profiles:', result[BrowserStorageKey.Profiles] ? 'Present' : 'Missing');
3541
logger.info(' - Selected Profile:', result[BrowserStorageKey.SelectedProfile] || 'None');
3642
logger.info(' - Is Paused:', result[BrowserStorageKey.IsPaused] || false);
37-
38-
// Log profile count if present
39-
let activeHeadersCount = 0;
40-
if (result[BrowserStorageKey.Profiles]) {
41-
try {
42-
const profiles = JSON.parse(result[BrowserStorageKey.Profiles] as string);
43-
logger.info(` - Profiles count: ${profiles.length}`);
44-
if (profiles.length > 0) {
45-
logger.info(' - Profile names:', profiles.map((p: Profile) => p.name || p.id).join(', '));
46-
47-
// Count active headers for the badge
48-
const selectedProfile = profiles.find((p: Profile) => p.id === result[BrowserStorageKey.SelectedProfile]);
49-
if (selectedProfile) {
50-
activeHeadersCount = selectedProfile.requestHeaders?.filter((h: RequestHeader) => !h.disabled).length || 0;
51-
logger.info(` - Active headers count: ${activeHeadersCount}`);
52-
}
53-
}
54-
} catch (error) {
55-
logger.warn(' - Failed to parse profiles:', error);
56-
}
57-
}
43+
logger.groupEnd();
5844

5945
logger.debug('Background script load storage data:', JSON.stringify(result, null, 2));
60-
logger.groupEnd();
6146

62-
// Set the badge based on storage data
63-
const isPaused = (result[BrowserStorageKey.IsPaused] as boolean) || false;
64-
await setIconBadge({ isPaused, activeRulesCount: activeHeadersCount });
65-
logger.info(`🏷️ Badge set: paused=${isPaused}, activeRules=${activeHeadersCount}`);
47+
const currentTabUrl = await getCurrentTabUrl();
48+
await setBrowserHeaders(result, currentTabUrl);
49+
logger.info(`🏷️ Initial badge set for URL: ${currentTabUrl}`);
6650
} catch (error) {
6751
logger.error('Failed to check storage on background script load:', error);
6852
}
@@ -89,7 +73,7 @@ async function notify(message: ServiceWorkerEvent) {
8973
]);
9074

9175
logger.info('📦 Storage data for reload:', result);
92-
await setBrowserHeaders(result);
76+
await setBrowserHeaders(result, await getCurrentTabUrl());
9377
}
9478
return undefined;
9579
}
@@ -110,25 +94,12 @@ browser.runtime.onStartup.addListener(async function () {
11094
logger.info(' - Is Paused:', result[BrowserStorageKey.IsPaused] || false);
11195
logger.debug('Startup storage data:', JSON.stringify(result, null, 2));
11296

113-
// Log profile count if present
114-
if (result[BrowserStorageKey.Profiles]) {
115-
try {
116-
const profiles = JSON.parse(result[BrowserStorageKey.Profiles] as string);
117-
logger.info(` - Profiles count: ${profiles.length}`);
118-
if (profiles.length > 0) {
119-
logger.info(' - Profile names:', profiles.map((p: Profile) => p.name || p.id).join(', '));
120-
}
121-
} catch (error) {
122-
logger.warn(' - Failed to parse profiles:', error);
123-
}
124-
}
125-
12697
logger.debug('Startup storage data:', result);
12798

12899
if (Object.keys(result).length) {
129100
logger.info('🚀 Storage data found, setting browser headers on startup');
130101
try {
131-
await setBrowserHeaders(result);
102+
await setBrowserHeaders(result, await getCurrentTabUrl());
132103
} catch (error) {
133104
logger.error('Failed to set browser headers on startup:', error);
134105
}
@@ -156,7 +127,7 @@ browser.storage.onChanged.addListener(async (changes, areaName) => {
156127
]);
157128
logger.debug('Storage changes data:', result);
158129
try {
159-
await setBrowserHeaders(result);
130+
await setBrowserHeaders(result, await getCurrentTabUrl());
160131
} catch (error) {
161132
logger.error('Failed to set browser headers on storage change:', error);
162133
}
@@ -181,25 +152,12 @@ browser.runtime.onInstalled.addListener(async details => {
181152
logger.debug('Install/update storage data:', JSON.stringify(result, null, 2));
182153
logger.groupEnd();
183154

184-
// Log profile count if present
185-
if (result[BrowserStorageKey.Profiles]) {
186-
try {
187-
const profiles = JSON.parse(result[BrowserStorageKey.Profiles] as string);
188-
logger.info(` - Profiles count: ${profiles.length}`);
189-
if (profiles.length > 0) {
190-
logger.info(' - Profile names:', profiles.map((p: Profile) => p.name || p.id).join(', '));
191-
}
192-
} catch (error) {
193-
logger.warn(' - Failed to parse profiles:', error);
194-
}
195-
}
196-
197155
logger.debug('Install/update storage data:', result);
198156

199157
if (Object.keys(result).length) {
200158
logger.info('🔧 Storage data found, initializing browser headers on install/update');
201159
try {
202-
await setBrowserHeaders(result);
160+
await setBrowserHeaders(result, await getCurrentTabUrl());
203161
} catch (error) {
204162
logger.error('Failed to set browser headers on install/update:', error);
205163
}
@@ -222,7 +180,8 @@ browser.tabs.onActivated.addListener(async activeInfo => {
222180
if (Object.keys(result).length) {
223181
logger.info('📱 Tab activated, updating headers');
224182
try {
225-
await setBrowserHeaders(result);
183+
const tab = await browser.tabs.get(activeInfo.tabId);
184+
await setBrowserHeaders(result, tab.url);
226185
} catch (error) {
227186
logger.error('Failed to set browser headers on tab activation:', error);
228187
}
@@ -231,6 +190,29 @@ browser.tabs.onActivated.addListener(async activeInfo => {
231190
}
232191
});
233192

193+
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
194+
if (changeInfo.status !== 'complete') return;
195+
196+
const activeTabs = await browser.tabs.query({ active: true, currentWindow: true });
197+
if (activeTabs[0]?.id !== tabId) return;
198+
199+
logger.debug('Active tab URL updated:', tab.url);
200+
201+
const result = await browser.storage.local.get([
202+
BrowserStorageKey.Profiles,
203+
BrowserStorageKey.SelectedProfile,
204+
BrowserStorageKey.IsPaused,
205+
]);
206+
207+
if (Object.keys(result).length) {
208+
try {
209+
await setBrowserHeaders(result, tab.url);
210+
} catch (error) {
211+
logger.error('Failed to set browser headers on tab URL update:', error);
212+
}
213+
}
214+
});
215+
234216
browserAction.setBadgeBackgroundColor({ color: BADGE_COLOR });
235217

236218
browser.runtime.onMessage.addListener((message: unknown) => {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { countActiveHeadersForUrl, doesUrlMatchFilter } from '../countActiveHeadersForUrl';
4+
5+
const makeHeaders = (count: number) =>
6+
Array.from({ length: count }, (_, i) => ({
7+
id: i + 1,
8+
name: `X-Header-${i + 1}`,
9+
value: `value-${i + 1}`,
10+
disabled: false,
11+
}));
12+
13+
describe('doesUrlMatchFilter', () => {
14+
describe('regex path (*:// patterns)', () => {
15+
it('matches *://example.com/* against https://example.com/path', () => {
16+
expect(doesUrlMatchFilter('https://example.com/path', '*://example.com/*')).toBe(true);
17+
});
18+
19+
it('does not match *://example.com/* against https://other.com/path', () => {
20+
expect(doesUrlMatchFilter('https://other.com/path', '*://example.com/*')).toBe(false);
21+
});
22+
23+
it('matches *://api-test*/* against https://api-test.example.org/v1/resource', () => {
24+
expect(doesUrlMatchFilter('https://api-test.example.org/v1/resource', '*://api-test*/*')).toBe(true);
25+
});
26+
27+
it('does not match *://api*/* when host does not start with api', () => {
28+
expect(doesUrlMatchFilter('https://service.example.com/api/test', '*://api*/*')).toBe(false);
29+
});
30+
31+
it('matches *://api*/* when host starts with api', () => {
32+
expect(doesUrlMatchFilter('https://api-test.example.org/api/test', '*://api*/*')).toBe(true);
33+
});
34+
});
35+
36+
describe('urlFilter path (non-*:// patterns)', () => {
37+
it('matches a simple domain substring', () => {
38+
expect(doesUrlMatchFilter('https://example.com/path', 'example.com')).toBe(true);
39+
});
40+
41+
it('does not match a simple domain against an unrelated URL', () => {
42+
expect(doesUrlMatchFilter('https://other.com/path', 'example.com')).toBe(false);
43+
});
44+
45+
it('matches https:// prefix filter with wildcard', () => {
46+
expect(doesUrlMatchFilter('https://example.com/api', 'https://example.com/*')).toBe(true);
47+
});
48+
49+
it('does not match https:// filter against http://', () => {
50+
expect(doesUrlMatchFilter('http://example.com/api', 'https://example.com/*')).toBe(false);
51+
});
52+
53+
it('matches a wildcard pattern for a path prefix', () => {
54+
expect(doesUrlMatchFilter('https://example.com/api/v2', 'example.com/api/*')).toBe(true);
55+
});
56+
57+
it('is case-insensitive for urlFilter path', () => {
58+
expect(doesUrlMatchFilter('https://EXAMPLE.COM/path', 'example.com')).toBe(true);
59+
});
60+
61+
it('matches a keyword filter as substring', () => {
62+
expect(doesUrlMatchFilter('https://api-test.internal/v1', 'api-test')).toBe(true);
63+
});
64+
65+
it('does not match keyword filter when not present in URL', () => {
66+
expect(doesUrlMatchFilter('https://service.internal/v1', 'api-test')).toBe(false);
67+
});
68+
});
69+
70+
describe('edge cases', () => {
71+
it('returns false for empty url', () => {
72+
expect(doesUrlMatchFilter('', 'example.com')).toBe(false);
73+
});
74+
75+
it('returns false for empty filter', () => {
76+
expect(doesUrlMatchFilter('https://example.com', '')).toBe(false);
77+
});
78+
});
79+
});
80+
81+
describe('countActiveHeadersForUrl', () => {
82+
const twoHeaders = makeHeaders(2);
83+
84+
describe('no URL filters configured', () => {
85+
it('returns activeHeaders.length when activeUrlFilters is empty', () => {
86+
expect(countActiveHeadersForUrl(twoHeaders, [], 'https://example.com')).toBe(2);
87+
});
88+
89+
it('returns activeHeaders.length even when currentUrl is undefined', () => {
90+
expect(countActiveHeadersForUrl(twoHeaders, [], undefined)).toBe(2);
91+
});
92+
});
93+
94+
describe('URL filters configured, URL matches', () => {
95+
it('returns activeHeaders.length when URL matches a filter', () => {
96+
expect(countActiveHeadersForUrl(twoHeaders, ['*://example.com/*'], 'https://example.com/page')).toBe(2);
97+
});
98+
99+
it('returns activeHeaders.length when any one of multiple filters matches', () => {
100+
const filters = ['*://other.com/*', '*://example.com/*'];
101+
expect(countActiveHeadersForUrl(twoHeaders, filters, 'https://example.com/page')).toBe(2);
102+
});
103+
104+
it('returns activeHeaders.length for simple domain filter match', () => {
105+
expect(countActiveHeadersForUrl(twoHeaders, ['example.com'], 'https://example.com/page')).toBe(2);
106+
});
107+
});
108+
109+
describe('URL filters configured, URL does not match', () => {
110+
it('returns 0 when URL does not match the filter', () => {
111+
expect(countActiveHeadersForUrl(twoHeaders, ['*://example.com/*'], 'https://other.com/page')).toBe(0);
112+
});
113+
114+
it('returns 0 when none of multiple filters match', () => {
115+
const filters = ['*://other.com/*', '*://third.com/*'];
116+
expect(countActiveHeadersForUrl(twoHeaders, filters, 'https://example.com/page')).toBe(0);
117+
});
118+
});
119+
120+
describe('unknown currentUrl fallback', () => {
121+
it('falls back to activeHeaders.length when currentUrl is undefined (safe default)', () => {
122+
expect(countActiveHeadersForUrl(twoHeaders, ['*://example.com/*'], undefined)).toBe(2);
123+
});
124+
125+
it('falls back to activeHeaders.length when currentUrl is empty string', () => {
126+
expect(countActiveHeadersForUrl(twoHeaders, ['*://example.com/*'], '')).toBe(2);
127+
});
128+
});
129+
130+
describe('zero active headers', () => {
131+
it('returns 0 when there are no active headers (no filters)', () => {
132+
expect(countActiveHeadersForUrl([], [], 'https://example.com')).toBe(0);
133+
});
134+
135+
it('returns 0 when there are no active headers (matching filter)', () => {
136+
expect(countActiveHeadersForUrl([], ['*://example.com/*'], 'https://example.com/page')).toBe(0);
137+
});
138+
139+
it('returns 0 when there are no active headers (non-matching filter)', () => {
140+
expect(countActiveHeadersForUrl([], ['*://example.com/*'], 'https://other.com/page')).toBe(0);
141+
});
142+
});
143+
});

0 commit comments

Comments
 (0)