From 42c1ad75c8ebbdf2a55a52c4301c95143a775064 Mon Sep 17 00:00:00 2001 From: robot Date: Wed, 30 Sep 2026 03:09:25 +0000 Subject: [PATCH] feat(viewer): persist hide/show instructions state in chrome.storage.local Read the instructions panel's visibility on mount from chrome.storage.local['goon_instructions_visible'] (absent => shown, the first-time default) and persist the new value on every user toggle, so a 'hide instructions' choice survives popup reloads and service-worker termination and is respected across the app via the shared storage key. Adds unit tests: stored-hidden honored on mount, persistence on both toggle directions, and first-time (absent key) defaulting to shown. --- __tests__/instructions_ui.test.mjs | 85 ++++++++++++++++++++++++++---- src/viewer/index.js | 22 +++++++- 2 files changed, 95 insertions(+), 12 deletions(-) diff --git a/__tests__/instructions_ui.test.mjs b/__tests__/instructions_ui.test.mjs index 1cf4032..33ea83a 100644 --- a/__tests__/instructions_ui.test.mjs +++ b/__tests__/instructions_ui.test.mjs @@ -130,16 +130,26 @@ function installPopupDom() { }; } -async function mountPopup() { +async function mountPopupWithStorage(storageGet = async () => ({}), captureWrite = null) { const { dom, cleanup } = installPopupDom(); + // Override the chrome.storage.local stub so we can seed a value and capture writes. + const writes = []; + globalThis.chrome.storage.local.get = async (...args) => { + const res = await storageGet(...args); + return res; + }; + globalThis.chrome.storage.local.set = async (items) => { + if (captureWrite) captureWrite(items); else writes.push(items); + }; const moduleUrl = new URL('../src/viewer/index.js?test=' + Date.now(), import.meta.url); await import(moduleUrl.href); - return { dom, cleanup }; + const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); + return { dom, cleanup, writes, flush }; } describe('Viewer popup instructions', () => { it('renders the drafted instructions copy on mount with accessible labeling', async () => { - const { dom, cleanup } = await mountPopup(); + const { dom, cleanup } = await mountPopupWithStorage(); try { const doc = dom.window.document; const region = doc.querySelector('.instructions-panel'); @@ -180,7 +190,7 @@ describe('Viewer popup instructions', () => { }); it('toggles the instructions panel open and closed', async () => { - const { dom, cleanup } = await mountPopup(); + const { dom, cleanup } = await mountPopupWithStorage(); try { const doc = dom.window.document; const toggle = doc.getElementById('instructions-toggle'); @@ -194,11 +204,66 @@ describe('Viewer popup instructions', () => { if (toggle.textContent !== 'Show instructions') throw new Error('Collapsed label should switch to Show instructions'); toggle.click(); - if (toggle.getAttribute('aria-expanded') !== 'true') throw new Error('Toggle should expand the instructions'); - if (body.hidden) throw new Error('Instructions body should be visible after expanding'); - if (toggle.textContent !== 'Hide instructions') throw new Error('Expanded label should switch back to Hide instructions'); - } finally { + if (toggle.getAttribute('aria-expanded') !== 'true') throw new Error('Toggle should expand the instructions'); + if (body.hidden) throw new Error('Instructions body should be visible after expanding'); + if (toggle.textContent !== 'Hide instructions') throw new Error('Expanded label should switch back to Hide instructions'); + } finally { cleanup(); - } - }); + } + }); + + it('honors a stored hidden state on mount', async () => { + const { dom, cleanup } = await mountPopupWithStorage( + async () => ({ goon_instructions_visible: false }) + ); + try { + const doc = dom.window.document; + const toggle = doc.getElementById('instructions-toggle'); + const body = doc.getElementById('instructions-body'); + // Wait for the async storage.get() to resolve and apply. + await new Promise((r) => setTimeout(r, 0)); + if (toggle.getAttribute('aria-expanded') !== 'false') throw new Error('Stored hidden state should start collapsed'); + if (!body.hidden) throw new Error('Stored hidden state should start hidden'); + if (toggle.textContent !== 'Show instructions') throw new Error('Stored hidden state should show the "Show instructions" label'); + } finally { + cleanup(); + } + }); + + it('persists the new visibility on every user toggle', async () => { + let written = null; + const { dom, cleanup, flush } = await mountPopupWithStorage( + async () => ({}), + (items) => { written = items; } + ); + try { + const doc = dom.window.document; + const toggle = doc.getElementById('instructions-toggle'); + // Collapse -> must persist false. + toggle.click(); + await flush(); + if (written === null) throw new Error('Collapsing the panel must write to chrome.storage.local'); + if (written.goon_instructions_visible !== false) throw new Error('Collapsing must persist goon_instructions_visible=false'); + // Expand -> must persist true. + toggle.click(); + await flush(); + if (written.goon_instructions_visible !== true) throw new Error('Expanding must persist goon_instructions_visible=true'); + } finally { + cleanup(); + } + }); + + it('defaults a first-time user (no stored key) to shown', async () => { + const { dom, cleanup } = await mountPopupWithStorage(async () => ({})); + try { + const doc = dom.window.document; + const toggle = doc.getElementById('instructions-toggle'); + const body = doc.getElementById('instructions-body'); + await new Promise((r) => setTimeout(r, 0)); + if (toggle.getAttribute('aria-expanded') !== 'true') throw new Error('First-time user should start expanded'); + if (body.hidden) throw new Error('First-time instructions body should be visible on mount'); + } finally { + cleanup(); + } + }); }); diff --git a/src/viewer/index.js b/src/viewer/index.js index 018c7a8..9ec34ab 100644 --- a/src/viewer/index.js +++ b/src/viewer/index.js @@ -6,6 +6,11 @@ const STORAGE_KEY = 'goon_videos'; const TAGGED_KEY = 'goon_tagged'; +// Durable "hide instructions" toggle. Absent key === SHOWN (true), matching the +// first-time "expanded on mount" behavior. chrome.storage.local is extension-wide, +// so the viewer window and every popup instance read/write the same value — the +// preference is respected application-wide and survives reloads / SW termination. +const INSTRUCTIONS_VISIBLE_KEY = 'goon_instructions_visible'; // Set to true to enable verbose console output in the popup. const DEBUG = true; // TEMP: enabled for debugging — revert to false when done @@ -48,9 +53,22 @@ function setInstructionsExpanded(expanded) { } if (instructionsToggle && instructionsBody) { - setInstructionsExpanded(true); + // On mount, read the stored visibility. An absent key means SHOWN (true), so a + // first-time user still sees the steps. The viewer window reads the same + // extension-wide key, so the choice is respected across the whole app. + chrome.storage.local.get(INSTRUCTIONS_VISIBLE_KEY).then(stored => { + const visible = stored[INSTRUCTIONS_VISIBLE_KEY]; + setInstructionsExpanded(visible === undefined ? true : visible); + }).catch(() => { + // Any read failure falls back to shown. + setInstructionsExpanded(true); + }); + // On every user toggle, apply in-DOM and persist. Both directions write, so + // the state survives reloads and SW termination until the user flips it again. instructionsToggle.addEventListener('click', () => { - setInstructionsExpanded(instructionsBody.hidden); + const next = instructionsBody.hidden; + setInstructionsExpanded(next); + chrome.storage.local.set({ [INSTRUCTIONS_VISIBLE_KEY]: next }); }); }