Skip to content

Commit fc14b0c

Browse files
committed
Harden action-click behavior and tighten extension smoke coverage
1 parent 60fc132 commit fc14b0c

File tree

7 files changed

+318
-37
lines changed

7 files changed

+318
-37
lines changed

background/action-click.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export async function executeActionClick({
2+
getFocusedWindowId,
3+
openPanelForWindow,
4+
openPanelForAllWindows,
5+
openDashboardTab
6+
}) {
7+
let focusedWindowId = null;
8+
try {
9+
focusedWindowId = await getFocusedWindowId();
10+
} catch {
11+
focusedWindowId = null;
12+
}
13+
14+
let openedFromFocusedWindow = false;
15+
if (typeof focusedWindowId === "number") {
16+
openedFromFocusedWindow = await openPanelForWindow(focusedWindowId);
17+
}
18+
19+
if (openedFromFocusedWindow) {
20+
// Best-effort global open to honor the global side panel requirement.
21+
await openPanelForAllWindows().catch(() => {});
22+
return {
23+
ok: true,
24+
mode: "panel_focused_window",
25+
focusedWindowId
26+
};
27+
}
28+
29+
const openedAnyWindow = await openPanelForAllWindows();
30+
if (openedAnyWindow) {
31+
return {
32+
ok: true,
33+
mode: "panel_all_windows",
34+
focusedWindowId
35+
};
36+
}
37+
38+
await openDashboardTab();
39+
return {
40+
ok: true,
41+
mode: "dashboard_fallback",
42+
focusedWindowId
43+
};
44+
}

background/service-worker.js

Lines changed: 81 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
updateSettings,
1515
upsertTabSnapshot
1616
} from "../shared/db.js";
17+
import { executeActionClick } from "./action-click.js";
18+
import { configureOpenOnActionClick } from "./side-panel-behavior.js";
1719
import { createSessionEngine } from "./session-engine.js";
1820
import { extractDomain, isExcludedDomain, isTrackableUrl, readableTitle } from "../shared/url.js";
1921

@@ -28,7 +30,10 @@ const state = {
2830
paused: false,
2931
excludedDomains: [],
3032
retentionDays: 30,
31-
theme: "dark"
33+
theme: "dark",
34+
openPanelOnActionClick: null,
35+
lastActionClickResult: null,
36+
lastOpenSidePanelResult: null
3237
};
3338

3439
const sessionEngine = createSessionEngine({
@@ -326,19 +331,22 @@ async function openPanelForAllWindows() {
326331
}
327332

328333
const windows = await chrome.windows.getAll({ populate: false });
334+
let openedCount = 0;
335+
329336
await Promise.all(
330337
windows
331338
.filter((windowEntry) => typeof windowEntry.id === "number")
332339
.map(async (windowEntry) => {
333340
try {
334341
await chrome.sidePanel.open({ windowId: windowEntry.id });
342+
openedCount += 1;
335343
} catch {
336344
// Ignore non-supported window types or gesture mismatches.
337345
}
338346
})
339347
);
340348

341-
return true;
349+
return openedCount > 0;
342350
}
343351

344352
async function openPanelForWindow(windowId) {
@@ -355,17 +363,14 @@ async function openPanelForWindow(windowId) {
355363
}
356364

357365
async function initializeSidePanelBehavior() {
358-
if (!chrome.sidePanel?.setPanelBehavior) {
366+
const result = await configureOpenOnActionClick(chrome.sidePanel?.setPanelBehavior?.bind(chrome.sidePanel));
367+
state.openPanelOnActionClick = result.ok;
368+
369+
if (result.ok || result.reason === "not_supported") {
359370
return;
360371
}
361372

362-
try {
363-
await chrome.sidePanel.setPanelBehavior({
364-
openPanelOnActionClick: false
365-
});
366-
} catch (error) {
367-
console.warn("Unable to configure side panel behavior", error);
368-
}
373+
console.warn("Unable to configure side panel behavior", result.error || result.reason);
369374
}
370375

371376
async function initializeExtension(reason) {
@@ -396,32 +401,30 @@ chrome.runtime.onStartup.addListener(() => {
396401
});
397402

398403
chrome.action.onClicked.addListener(() => {
399-
// Keep icon-click predictable: open current window panel first, then best-effort global open.
400-
chrome.windows
401-
.getLastFocused()
402-
.then(async (windowEntry) => {
403-
const focusedWindowId = typeof windowEntry?.id === "number" ? windowEntry.id : null;
404-
let opened = false;
405-
406-
if (focusedWindowId !== null) {
407-
opened = await openPanelForWindow(focusedWindowId);
408-
}
409-
410-
if (!opened) {
411-
opened = await openPanelForAllWindows();
412-
}
413-
414-
if (!opened) {
415-
await chrome.tabs.create({ url: getDashboardUrl() });
416-
return;
417-
}
418-
419-
openPanelForAllWindows().catch(() => {
420-
// Ignore follow-up global open failures.
421-
});
404+
executeActionClick({
405+
getFocusedWindowId: async () => {
406+
const windowEntry = await chrome.windows.getLastFocused();
407+
return typeof windowEntry?.id === "number" ? windowEntry.id : null;
408+
},
409+
openPanelForWindow,
410+
openPanelForAllWindows,
411+
openDashboardTab: async () => chrome.tabs.create({ url: getDashboardUrl() })
412+
})
413+
.then((result) => {
414+
state.lastActionClickResult = {
415+
...result,
416+
source: "action-click",
417+
at: Date.now()
418+
};
422419
})
423-
.catch(async () => {
424-
await chrome.tabs.create({ url: getDashboardUrl() });
420+
.catch((error) => {
421+
state.lastActionClickResult = {
422+
ok: false,
423+
source: "action-click",
424+
at: Date.now(),
425+
error: String(error)
426+
};
427+
console.error("Failed to handle action click", error);
425428
});
426429
});
427430

@@ -548,11 +551,48 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
548551

549552
if (message?.type === "open-side-panel") {
550553
openPanelForAllWindows()
551-
.then(() => sendResponse({ ok: true }))
554+
.then((opened) => {
555+
state.lastOpenSidePanelResult = {
556+
ok: opened,
557+
opened,
558+
at: Date.now()
559+
};
560+
sendResponse({ ok: opened, opened });
561+
})
552562
.catch((error) => sendResponse({ ok: false, error: String(error) }));
553563
return true;
554564
}
555565

566+
if (message?.type === "debug-trigger-action-click") {
567+
executeActionClick({
568+
getFocusedWindowId: async () => {
569+
const windowEntry = await chrome.windows.getLastFocused();
570+
return typeof windowEntry?.id === "number" ? windowEntry.id : null;
571+
},
572+
openPanelForWindow,
573+
openPanelForAllWindows,
574+
openDashboardTab: async () => chrome.tabs.create({ url: getDashboardUrl() })
575+
})
576+
.then((result) => {
577+
state.lastActionClickResult = {
578+
...result,
579+
source: "debug-trigger-action-click",
580+
at: Date.now()
581+
};
582+
sendResponse(result);
583+
})
584+
.catch((error) => {
585+
state.lastActionClickResult = {
586+
ok: false,
587+
source: "debug-trigger-action-click",
588+
at: Date.now(),
589+
error: String(error)
590+
};
591+
sendResponse({ ok: false, error: String(error) });
592+
});
593+
return true;
594+
}
595+
556596
if (message?.type === "get-runtime-status") {
557597
const runtimeState = sessionEngine.readState();
558598
sendResponse({
@@ -562,7 +602,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
562602
retentionDays: state.retentionDays,
563603
idleState: state.idleState,
564604
theme: state.theme,
565-
meaningfulThresholdSec: FOCUS_MEANINGFUL_THRESHOLD_SEC
605+
meaningfulThresholdSec: FOCUS_MEANINGFUL_THRESHOLD_SEC,
606+
sidePanelApiAvailable: Boolean(chrome.sidePanel?.open),
607+
openPanelOnActionClick: state.openPanelOnActionClick,
608+
lastActionClickResult: state.lastActionClickResult,
609+
lastOpenSidePanelResult: state.lastOpenSidePanelResult
566610
});
567611
return false;
568612
}

background/side-panel-behavior.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export async function configureOpenOnActionClick(setPanelBehavior) {
2+
if (typeof setPanelBehavior !== "function") {
3+
return {
4+
ok: false,
5+
reason: "not_supported"
6+
};
7+
}
8+
9+
try {
10+
await setPanelBehavior({
11+
openPanelOnActionClick: true
12+
});
13+
14+
return {
15+
ok: true,
16+
reason: "configured"
17+
};
18+
} catch (error) {
19+
return {
20+
ok: false,
21+
reason: "set_panel_behavior_failed",
22+
error: String(error)
23+
};
24+
}
25+
}

project-history.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ Chronological execution log:
160160
- All tabs proof: `count=6`, `never-focused=4`
161161
- Artifacts: `artifacts/validation/20260226-192909/`
162162

163+
30. Hardened action-click validation and fixed testing blind spot after user-reported regression:
164+
- extracted action-click flow to `background/action-click.js`
165+
- corrected all-windows panel open success criteria (`openedCount > 0`)
166+
- added side-panel behavior module + tests (`background/side-panel-behavior.js`, `tests/unit/side-panel-behavior.test.js`)
167+
- switched native side-panel behavior to `openPanelOnActionClick: true`
168+
- extended smoke assertions to require side-panel API availability and configured action-click behavior
169+
163170
## 4. What Were The Decisions That We Took?
164171

165172
### Product/Architecture Decisions
@@ -172,6 +179,7 @@ Chronological execution log:
172179
6. **UI model:** Global side panel + full dashboard + settings.
173180
7. **Default work view:** `Meaningful` (`focused time > 10s`) with toggles for `All tabs` and `Most recent`.
174181
8. **Theme policy:** Dark mode default with one shared setting across side panel/full dashboard.
182+
9. **Action-click reliability:** Enable native side-panel open-on-action-click and treat fallback-only behavior as a test smell.
175183

176184
### Engineering Decisions
177185

@@ -271,6 +279,7 @@ Not in MVP (intentionally out of scope):
271279
- `npm run test:all`: passing
272280
- `npm run test:smoke:extension`: passing
273281
- Long-duration headed validation: passing (`runId=20260226-192909`, `allTabsCount=6`, `neverFocused=4`, `retentionDays=30`, `theme=dark`)
282+
- Action-click config check: passing (`sidePanelApiAvailable=true`, `openPanelOnActionClick=true`)
274283

275284
### Branch/History Status
276285

@@ -291,6 +300,8 @@ Primary commits:
291300
- `manifest.json`
292301
- `background/service-worker.js`
293302
- `background/session-engine.js`
303+
- `background/action-click.js`
304+
- `background/side-panel-behavior.js`
294305
- `shared/db.js`
295306
- `shared/time.js`
296307
- `shared/url.js`
@@ -315,6 +326,8 @@ Primary commits:
315326

316327
- `tests/unit/session-engine.test.js`
317328
- `tests/unit/db.test.js`
329+
- `tests/unit/action-click.test.js`
330+
- `tests/unit/side-panel-behavior.test.js`
318331
- `tests/e2e/dashboard.spec.js`
319332
- `playwright.config.mjs`
320333

scripts/extension-smoke-test.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ async function run() {
3737
const status = await dashboardPage.evaluate(async () =>
3838
chrome.runtime.sendMessage({ type: "get-runtime-status" })
3939
);
40+
const actionClickSimulation = await dashboardPage.evaluate(async () =>
41+
chrome.runtime.sendMessage({ type: "debug-trigger-action-click" })
42+
);
4043

4144
const panelPage = await context.newPage();
4245
await panelPage.goto(panelUrl, { waitUntil: "domcontentloaded" });
@@ -52,9 +55,12 @@ async function run() {
5255
activityListPresent: activityListCount > 0,
5356
defaultViewActive: defaultViewActive > 0,
5457
panelViewToggleCount: panelViewCount,
58+
actionClickSimulation,
5559
runtimeStatusOk: status?.ok === true,
5660
retentionDays: status?.retentionDays,
5761
paused: status?.paused,
62+
sidePanelApiAvailable: status?.sidePanelApiAvailable,
63+
openPanelOnActionClick: status?.openPanelOnActionClick,
5864
settingsHeading
5965
};
6066

@@ -65,7 +71,11 @@ async function run() {
6571
!result.activityListPresent ||
6672
!result.defaultViewActive ||
6773
result.panelViewToggleCount < 3 ||
74+
!result.actionClickSimulation?.ok ||
75+
!["panel_focused_window", "panel_all_windows", "dashboard_fallback"].includes(result.actionClickSimulation?.mode) ||
6876
result.runtimeStatusOk !== true ||
77+
result.sidePanelApiAvailable !== true ||
78+
result.openPanelOnActionClick !== true ||
6979
result.settingsHeading !== "Settings"
7080
) {
7181
process.exitCode = 1;

0 commit comments

Comments
 (0)