Skip to content

Commit ff5f23c

Browse files
committed
2 parents 8a33075 + 098e904 commit ff5f23c

5 files changed

Lines changed: 108 additions & 17 deletions

File tree

Package/dist/popup.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,6 @@ document.addEventListener("DOMContentLoaded", () => {
150150
initializePopup();
151151
});
152152

153-
// Track dwell time based on tab visibility
154-
// When user switches to another tab, end the current page tracking
155-
// When user comes back, restart tracking for the same page
156153
document.addEventListener("visibilitychange", () => {
157154
if (window.Analytics) {
158155
window.Analytics.handleVisibilityChange(document.visibilityState === "visible");
@@ -174,7 +171,6 @@ function popupLayout() {
174171
showPage("history");
175172
checkTextOverflow();
176173

177-
// Track initial page view when popup opens
178174
if (window.Analytics) {
179175
window.Analytics.trackPageView("history");
180176
}
@@ -203,11 +199,41 @@ function checkTextOverflow() {
203199
}
204200
}
205201

206-
async function getWarmState() {
202+
async function getWarmState(retries = 3, delay = 200) {
207203
return new Promise((resolve) => {
208-
chrome.runtime.sendMessage({ action: "getWarmState" }, (state) => {
209-
resolve(state ?? {});
210-
});
204+
if (!chrome.runtime?.id) {
205+
resolve({});
206+
return;
207+
}
208+
209+
const maxDelay = 3000;
210+
const backoffDelay = Math.min(delay * 2, maxDelay);
211+
212+
const timeout = setTimeout(() => {
213+
if (retries > 0) {
214+
resolve(getWarmState(retries - 1, backoffDelay));
215+
} else {
216+
resolve({});
217+
}
218+
}, delay);
219+
220+
try {
221+
chrome.runtime.sendMessage({ action: "getWarmState" }, (state) => {
222+
clearTimeout(timeout);
223+
if (chrome.runtime.lastError) {
224+
if (retries > 0) {
225+
resolve(getWarmState(retries - 1, backoffDelay));
226+
} else {
227+
resolve({});
228+
}
229+
} else {
230+
resolve(state ?? {});
231+
}
232+
});
233+
} catch (e) {
234+
clearTimeout(timeout);
235+
resolve({});
236+
}
211237
});
212238
}
213239

Package/dist/utils/analytics.module.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Analytics = {
1111
// Page view tracking state
1212
_currentPage: null,
1313
_pageStartTime: null,
14+
_lastPage: null,
1415

1516
// Get or create anonymous Client ID
1617
getOrCreateClientId() {
@@ -147,9 +148,6 @@ const Analytics = {
147148
}
148149
},
149150

150-
// Store last page for resuming
151-
_lastPage: null,
152-
153151
// Track keyboard shortcut usage (service worker only)
154152
trackShortcut(shortcutName) {
155153
this.trackEvent("shortcut_used", {

tests/analytics.module.test.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,13 @@ describe("analytics.module.js - Analytics ES Module", () => {
272272
});
273273

274274
test("should include page_name in params", async () => {
275-
Analytics.trackPageView("favorites");
275+
Analytics.trackPageView("favorite");
276276

277277
await flushPromises();
278278

279279
const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body);
280280

281-
expect(requestBody.events[0].params.page_name).toBe("favorites");
281+
expect(requestBody.events[0].params.page_name).toBe("favorite");
282282
});
283283

284284
test("should set _currentPage and _pageStartTime when tracking a page", () => {
@@ -301,10 +301,10 @@ describe("analytics.module.js - Analytics ES Module", () => {
301301
Analytics._pageStartTime = Date.now() - 5000; // 5 seconds ago
302302

303303
// Switch to another page
304-
Analytics.trackPageView("favorites");
304+
Analytics.trackPageView("favorite");
305305
await flushPromises();
306306

307-
// Should have sent page_dwell for "history" and page_view for "favorites"
307+
// Should have sent page_dwell for "history" and page_view for "favorite"
308308
expect(mockFetch).toHaveBeenCalledTimes(2);
309309

310310
const dwellCall = JSON.parse(mockFetch.mock.calls[0][1].body);
@@ -386,10 +386,10 @@ describe("analytics.module.js - Analytics ES Module", () => {
386386
});
387387

388388
test("should store lastPage when tab becomes hidden", () => {
389-
Analytics.trackPageView("favorites");
389+
Analytics.trackPageView("favorite");
390390
Analytics.handleVisibilityChange(false);
391391

392-
expect(Analytics._lastPage).toBe("favorites");
392+
expect(Analytics._lastPage).toBe("favorite");
393393
expect(Analytics._currentPage).toBeNull();
394394
});
395395

tests/popup.test.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,72 @@ describe("popup.js", () => {
332332

333333
expect(result).toEqual({});
334334
});
335+
336+
test("getWarmState returns empty object when extension context is invalid", async () => {
337+
const originalId = chrome.runtime.id;
338+
delete chrome.runtime.id;
339+
340+
const result = await popup.getWarmState();
341+
342+
expect(result).toEqual({});
343+
chrome.runtime.id = originalId;
344+
});
345+
346+
test("getWarmState retries on chrome.runtime.lastError", async () => {
347+
let callCount = 0;
348+
chrome.runtime.sendMessage.mockImplementation((message, callback) => {
349+
if (message.action === "getWarmState") {
350+
callCount++;
351+
if (callCount < 3) {
352+
// Simulate async behavior with setTimeout to allow retry to work
353+
setTimeout(() => {
354+
Object.defineProperty(chrome.runtime, "lastError", {
355+
value: { message: "Service worker not responding" },
356+
configurable: true,
357+
});
358+
callback(null);
359+
Object.defineProperty(chrome.runtime, "lastError", {
360+
value: null,
361+
configurable: true,
362+
});
363+
}, 0);
364+
} else {
365+
setTimeout(() => {
366+
callback({ searchHistoryList: ["success"] });
367+
}, 0);
368+
}
369+
}
370+
return true;
371+
});
372+
373+
const result = await popup.getWarmState();
374+
375+
expect(result).toEqual({ searchHistoryList: ["success"] });
376+
expect(callCount).toBe(3);
377+
});
378+
379+
test("getWarmState returns empty object after all retries exhausted", async () => {
380+
chrome.runtime.sendMessage.mockImplementation((message, callback) => {
381+
if (message.action === "getWarmState") {
382+
setTimeout(() => {
383+
Object.defineProperty(chrome.runtime, "lastError", {
384+
value: { message: "Service worker not responding" },
385+
configurable: true,
386+
});
387+
callback(null);
388+
Object.defineProperty(chrome.runtime, "lastError", {
389+
value: null,
390+
configurable: true,
391+
});
392+
}, 0);
393+
}
394+
return true;
395+
});
396+
397+
const result = await popup.getWarmState(0); // 0 retries = single attempt only
398+
399+
expect(result).toEqual({});
400+
});
335401
});
336402

337403
describe("fetchData", () => {

tests/setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ global.TextDecoder = TextDecoder;
3333
// Mock Chrome API
3434
global.chrome = {
3535
runtime: {
36+
id: "mock-extension-id",
3637
sendMessage: jest.fn(),
3738
onMessage: {
3839
addListener: jest.fn(),

0 commit comments

Comments
 (0)