Skip to content

Commit ef664db

Browse files
refactor: migrate to React Query and enhance content script architecture (#102)
1 parent 2231597 commit ef664db

25 files changed

Lines changed: 2118 additions & 442 deletions

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
"@mui/material": "^5.15.1",
3535
"@mui/styles": "^5.15.2",
3636
"@mui/x-charts": "^6.18.4",
37+
"@tanstack/react-query": "^5.90.2",
38+
"@tanstack/react-query-devtools": "^5.90.2",
3739
"construct-style-sheets-polyfill": "3.1.0",
3840
"date-fns": "^3.0.6",
3941
"moment": "^2.30.1",

src/pages/background/index.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import 'webextension-polyfill';
33
import { currentTabAutoDetector } from '@root/src/pages/background/currentTabAutoDetector';
44
import { createNetworkTrafficHandler } from '@root/src/pages/background/lyticsNetworkHandler';
55
import { EmitLog } from '@root/src/shared/components/EmitLog';
6+
import { autoDetectedDomainsStore } from '@src/shared/storages/autoDetectedDomainsStorage';
67
import entityStore from '@src/shared/storages/entityStorage';
78
import { domainStore } from '@src/shared/storages/extensionDomainStorage';
89
import extensionStateStorage from '@src/shared/storages/extensionStateStorage';
10+
import domainStateStore from '@src/shared/storages/tabStateStorage';
911
import tagActivityStore from '@src/shared/storages/tagActivityStorage';
1012
import tagConfigStore from '@src/shared/storages/tagConfigStorage';
11-
import { autoDetectedDomainsStore } from '@src/shared/storages/autoDetectedDomainsStorage';
12-
import domainStateStore from '@src/shared/storages/tabStateStorage';
1313

14-
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(error => {
15-
EmitLog({ name: 'background', payload: { msg: 'Failed to set side panel behavior', error: error.message } });
16-
});
14+
import { messageBroker } from '../../shared/message-broker';
15+
import { IMessage } from '../../shared/message-broker/types';
16+
17+
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(error => console.error(error));
1718

1819
chrome.tabs.onUpdated.addListener(async (tabId, info, tab) => {
1920
if (!tab.url) return;
@@ -450,3 +451,10 @@ setInterval(async () => {
450451
});
451452
}
452453
}, 60000);
454+
messageBroker.handle('GET_CONFIG', async (message: IMessage) => {
455+
const tabMessage: IMessage = {
456+
key: 'GET_CONFIG',
457+
};
458+
459+
return await messageBroker.sendToTab(message.payload.currentTabId, tabMessage);
460+
});
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { EmitLog } from '@src/shared/components/EmitLog';
4+
5+
import {
6+
isJstagAvailable,
7+
notifyAutoDetectionFailed,
8+
notifyAutoDetectionSuccess,
9+
pollForJstag,
10+
startAutoDetection,
11+
} from './autoDetection';
12+
13+
vi.mock('@src/shared/components/EmitLog');
14+
15+
describe('autoDetection', () => {
16+
beforeEach(() => {
17+
vi.clearAllMocks();
18+
vi.useFakeTimers();
19+
delete (window as any).jstag;
20+
global.chrome = {
21+
runtime: {
22+
sendMessage: vi.fn().mockResolvedValue({}),
23+
},
24+
storage: {
25+
local: {
26+
get: vi.fn().mockResolvedValue({ extensionState: true }),
27+
set: vi.fn().mockResolvedValue(undefined),
28+
},
29+
},
30+
} as any;
31+
});
32+
33+
describe('isJstagAvailable', () => {
34+
it('should return true when jstag is defined', () => {
35+
(window as any).jstag = {};
36+
expect(isJstagAvailable()).toBe(true);
37+
});
38+
39+
it('should return false when jstag is undefined', () => {
40+
delete (window as any).jstag;
41+
expect(isJstagAvailable()).toBe(false);
42+
});
43+
44+
it('should return false when jstag is null', () => {
45+
(window as any).jstag = null;
46+
expect(isJstagAvailable()).toBe(false);
47+
});
48+
});
49+
50+
describe('notifyAutoDetectionSuccess', () => {
51+
it('should send message to background with correct payload', async () => {
52+
const domain = 'example.com';
53+
await notifyAutoDetectionSuccess(domain);
54+
55+
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
56+
action: 'recordDetection',
57+
domain: domain,
58+
confidence: 0.9,
59+
});
60+
expect(EmitLog).toHaveBeenCalledWith({
61+
name: 'content',
62+
payload: { msg: `Notified background of successful detection for: ${domain}` },
63+
});
64+
});
65+
66+
it('should handle sendMessage error gracefully', async () => {
67+
const domain = 'example.com';
68+
const error = new Error('Send failed');
69+
(chrome.runtime.sendMessage as any).mockRejectedValue(error);
70+
71+
await notifyAutoDetectionSuccess(domain);
72+
73+
expect(EmitLog).toHaveBeenCalledWith({
74+
name: 'content',
75+
payload: { msg: 'Error notifying background of successful detection', error: error.message },
76+
});
77+
});
78+
});
79+
80+
describe('notifyAutoDetectionFailed', () => {
81+
it('should send failure message to background', async () => {
82+
const domain = 'example.com';
83+
await notifyAutoDetectionFailed(domain);
84+
85+
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
86+
action: 'autoDetectionFailed',
87+
domain: domain,
88+
retryCount: 0,
89+
});
90+
expect(EmitLog).toHaveBeenCalledWith({
91+
name: 'content',
92+
payload: { msg: `Notified background of failed detection for: ${domain}` },
93+
});
94+
});
95+
96+
it('should handle sendMessage error gracefully', async () => {
97+
const domain = 'example.com';
98+
const error = new Error('Send failed');
99+
(chrome.runtime.sendMessage as any).mockRejectedValue(error);
100+
101+
await notifyAutoDetectionFailed(domain);
102+
103+
expect(EmitLog).toHaveBeenCalledWith({
104+
name: 'content',
105+
payload: { msg: 'Error notifying background of failed detection', error: error.message },
106+
});
107+
});
108+
});
109+
110+
describe('pollForJstag', () => {
111+
it('should detect jstag on first check', async () => {
112+
const domain = 'example.com';
113+
(window as any).jstag = {};
114+
115+
pollForJstag(domain);
116+
117+
await vi.runAllTimersAsync();
118+
119+
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
120+
action: 'recordDetection',
121+
domain: domain,
122+
confidence: 0.9,
123+
});
124+
});
125+
126+
it('should retry and eventually find jstag', async () => {
127+
const domain = 'example.com';
128+
129+
pollForJstag(domain);
130+
131+
await vi.advanceTimersByTimeAsync(750);
132+
expect(EmitLog).toHaveBeenCalledWith({
133+
name: 'content',
134+
payload: { msg: 'Auto-detection retry 1/5' },
135+
});
136+
137+
(window as any).jstag = {};
138+
await vi.advanceTimersByTimeAsync(750);
139+
140+
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
141+
action: 'recordDetection',
142+
domain: domain,
143+
confidence: 0.9,
144+
});
145+
});
146+
147+
it('should fail after max retries', async () => {
148+
const domain = 'example.com';
149+
150+
pollForJstag(domain);
151+
152+
for (let i = 0; i < 5; i++) {
153+
await vi.advanceTimersByTimeAsync(750);
154+
}
155+
156+
expect(EmitLog).toHaveBeenCalledWith({
157+
name: 'content',
158+
payload: { msg: 'Auto-detection failed - no jstag found after max retries' },
159+
});
160+
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
161+
action: 'autoDetectionFailed',
162+
domain: domain,
163+
retryCount: 0,
164+
});
165+
});
166+
});
167+
168+
describe('startAutoDetection', () => {
169+
it('should detect jstag immediately if available', async () => {
170+
const domain = 'example.com';
171+
(window as any).jstag = {};
172+
173+
await startAutoDetection(domain);
174+
175+
expect(EmitLog).toHaveBeenCalledWith({
176+
name: 'content',
177+
payload: { msg: 'Lytics jstag found immediately' },
178+
});
179+
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
180+
action: 'recordDetection',
181+
domain: domain,
182+
confidence: 0.9,
183+
});
184+
});
185+
186+
it('should start polling if jstag not immediately available', async () => {
187+
const domain = 'example.com';
188+
189+
await startAutoDetection(domain);
190+
191+
expect(EmitLog).toHaveBeenCalledWith({
192+
name: 'content',
193+
payload: { msg: `Starting auto-detection for domain: ${domain}` },
194+
});
195+
196+
(window as any).jstag = {};
197+
await vi.runAllTimersAsync();
198+
199+
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
200+
action: 'recordDetection',
201+
domain: domain,
202+
confidence: 0.9,
203+
});
204+
});
205+
});
206+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { EmitLog } from '@src/shared/components/EmitLog';
2+
import extensionStateStorage from '@src/shared/storages/extensionStateStorage';
3+
4+
export const isJstagAvailable = (): boolean => {
5+
return typeof (window as any).jstag !== 'undefined' && (window as any).jstag !== null;
6+
};
7+
8+
export const pollForJstag = (domain: string): void => {
9+
let retryCount = 0;
10+
const maxRetries = 5;
11+
const retryInterval = 750;
12+
13+
const checkForJstag = async () => {
14+
const isEnabled = await extensionStateStorage.get();
15+
if (!isEnabled) {
16+
EmitLog({ name: 'content', payload: { msg: 'Extension disabled - stopping auto-detection polling' } });
17+
return;
18+
}
19+
20+
retryCount++;
21+
22+
if (isJstagAvailable()) {
23+
EmitLog({ name: 'content', payload: { msg: 'Lytics jstag detected during auto-detection' } });
24+
await notifyAutoDetectionSuccess(domain);
25+
return;
26+
}
27+
28+
if (retryCount < maxRetries) {
29+
EmitLog({ name: 'content', payload: { msg: `Auto-detection retry ${retryCount}/${maxRetries}` } });
30+
setTimeout(checkForJstag, retryInterval);
31+
} else {
32+
EmitLog({ name: 'content', payload: { msg: 'Auto-detection failed - no jstag found after max retries' } });
33+
await notifyAutoDetectionFailed(domain);
34+
}
35+
};
36+
37+
checkForJstag();
38+
};
39+
40+
export const startAutoDetection = async (domain: string): Promise<void> => {
41+
const isEnabled = await extensionStateStorage.get();
42+
if (!isEnabled) {
43+
EmitLog({ name: 'content', payload: { msg: 'Extension disabled - skipping auto-detection', domain } });
44+
return;
45+
}
46+
47+
EmitLog({ name: 'content', payload: { msg: `Starting auto-detection for domain: ${domain}` } });
48+
49+
if (isJstagAvailable()) {
50+
EmitLog({ name: 'content', payload: { msg: 'Lytics jstag found immediately' } });
51+
await notifyAutoDetectionSuccess(domain);
52+
return;
53+
}
54+
55+
pollForJstag(domain);
56+
};
57+
58+
export const notifyAutoDetectionSuccess = async (domain: string): Promise<void> => {
59+
const isEnabled = await extensionStateStorage.get();
60+
if (!isEnabled) {
61+
EmitLog({ name: 'content', payload: { msg: 'Extension disabled - not notifying detection success', domain } });
62+
return;
63+
}
64+
65+
try {
66+
await chrome.runtime.sendMessage({
67+
action: 'recordDetection',
68+
domain: domain,
69+
confidence: 0.9,
70+
});
71+
EmitLog({ name: 'content', payload: { msg: `Notified background of successful detection for: ${domain}` } });
72+
} catch (error) {
73+
EmitLog({
74+
name: 'content',
75+
payload: { msg: 'Error notifying background of successful detection', error: error.message },
76+
});
77+
}
78+
};
79+
80+
export const notifyAutoDetectionFailed = async (domain: string): Promise<void> => {
81+
const isEnabled = await extensionStateStorage.get();
82+
if (!isEnabled) {
83+
EmitLog({ name: 'content', payload: { msg: 'Extension disabled - not notifying detection failure', domain } });
84+
return;
85+
}
86+
87+
try {
88+
await chrome.runtime.sendMessage({
89+
action: 'autoDetectionFailed',
90+
domain: domain,
91+
retryCount: 0,
92+
});
93+
EmitLog({ name: 'content', payload: { msg: `Notified background of failed detection for: ${domain}` } });
94+
} catch (error) {
95+
EmitLog({
96+
name: 'content',
97+
payload: { msg: 'Error notifying background of failed detection', error: error.message },
98+
});
99+
}
100+
};

0 commit comments

Comments
 (0)