From 97abaf42d983142e74446f18024ccf585461dded Mon Sep 17 00:00:00 2001 From: robot Date: Mon, 31 Aug 2026 03:26:50 +0000 Subject: [PATCH] feat: add user instructions to viewer popup --- __tests__/instructions_ui.test.mjs | 204 +++++++++++++++++++++++++++++ __tests__/run.mjs | 1 + docs/instructions-audit.md | 25 ++++ docs/instructions-copy.md | 17 +++ docs/instructions-demo.svg | 50 +++++++ docs/instructions-design.md | 26 ++++ src/viewer/index.css | 84 ++++++++++++ src/viewer/index.html | 39 ++++++ src/viewer/index.js | 16 +++ 9 files changed, 462 insertions(+) create mode 100644 __tests__/instructions_ui.test.mjs create mode 100644 docs/instructions-audit.md create mode 100644 docs/instructions-copy.md create mode 100644 docs/instructions-demo.svg create mode 100644 docs/instructions-design.md diff --git a/__tests__/instructions_ui.test.mjs b/__tests__/instructions_ui.test.mjs new file mode 100644 index 0000000..1cf4032 --- /dev/null +++ b/__tests__/instructions_ui.test.mjs @@ -0,0 +1,204 @@ +import { describe, it } from './runner.mjs'; +import { JSDOM } from 'jsdom'; + +const POPUP_HTML = ` + + + + + + VidPlex + + +
+

VidPlex

+
+
+
+

Quick start

+

How to use VidPlex

+
+ +
+
+
    +
  1. + Scan videos + Click Find Videos to search the current browser window for videos. +
  2. +
  3. + Review the list + Check the popup list; each found video URL is clickable. +
  4. +
  5. + Open the viewer + Click Open Viewer to open the 2x2 grid player in a new window. +
  6. +
  7. + Reset and rescan + Click Clear to erase results and scan again. +
  8. +
+

Warning: Browser pages, extension pages, and restricted frames can hide videos, so results may be incomplete.

+

Learn more tooltip: Read the VidPlex README and audit notes.

+
+
+ + +
+ +
+ +`; + +function installPopupDom() { + const dom = new JSDOM(POPUP_HTML, { url: 'chrome-extension://test/src/viewer/index.html' }); + const previous = { + window: globalThis.window, + document: globalThis.document, + navigator: globalThis.navigator, + chrome: globalThis.chrome, + HTMLElement: globalThis.HTMLElement, + Node: globalThis.Node, + }; + + globalThis.window = dom.window; + globalThis.document = dom.window.document; + Object.defineProperty(globalThis, 'navigator', { + value: dom.window.navigator, + configurable: true, + writable: true, + }); + Object.defineProperty(globalThis, 'HTMLElement', { + value: dom.window.HTMLElement, + configurable: true, + writable: true, + }); + Object.defineProperty(globalThis, 'Node', { + value: dom.window.Node, + configurable: true, + writable: true, + }); + globalThis.chrome = { + storage: { + local: { + get: async () => ({}), + set: async () => undefined, + remove: async () => undefined, + }, + }, + tabs: { query: async () => [] }, + scripting: { executeScript: async () => [] }, + runtime: { getURL: (path) => `chrome-extension://test/${path}` }, + windows: { create: async () => ({ id: 1 }) }, + }; + + return { + dom, + cleanup() { + dom.window.close(); + globalThis.window = previous.window; + globalThis.document = previous.document; + Object.defineProperty(globalThis, 'navigator', { + value: previous.navigator, + configurable: true, + writable: true, + }); + globalThis.chrome = previous.chrome; + Object.defineProperty(globalThis, 'HTMLElement', { + value: previous.HTMLElement, + configurable: true, + writable: true, + }); + Object.defineProperty(globalThis, 'Node', { + value: previous.Node, + configurable: true, + writable: true, + }); + }, + }; +} + +async function mountPopup() { + const { dom, cleanup } = installPopupDom(); + const moduleUrl = new URL('../src/viewer/index.js?test=' + Date.now(), import.meta.url); + await import(moduleUrl.href); + return { dom, cleanup }; +} + +describe('Viewer popup instructions', () => { + it('renders the drafted instructions copy on mount with accessible labeling', async () => { + const { dom, cleanup } = await mountPopup(); + try { + const doc = dom.window.document; + const region = doc.querySelector('.instructions-panel'); + const title = doc.getElementById('instructions-title'); + const toggle = doc.getElementById('instructions-toggle'); + const body = doc.getElementById('instructions-body'); + + if (!region) throw new Error('Instructions region missing'); + if (region.getAttribute('role') !== 'region') throw new Error('Instructions region missing role="region"'); + if (region.getAttribute('aria-labelledby') !== 'instructions-title') throw new Error('Instructions region missing aria-labelledby'); + if (!title || title.textContent !== 'How to use VidPlex') throw new Error('Instructions title not rendered'); + if (!toggle) throw new Error('Instructions toggle missing'); + if (toggle.getAttribute('aria-controls') !== 'instructions-body') throw new Error('Toggle missing aria-controls'); + if (toggle.getAttribute('aria-expanded') !== 'true') throw new Error('Toggle should start expanded'); + if (!body || body.hidden) throw new Error('Instructions body should be visible on mount'); + + const expectedTexts = [ + 'Scan videos', + 'Click Find Videos to search the current browser window for videos.', + 'Review the list', + 'Check the popup list; each found video URL is clickable.', + 'Open the viewer', + 'Click Open Viewer to open the 2x2 grid player in a new window.', + 'Reset and rescan', + 'Click Clear to erase results and scan again.', + 'Warning: Browser pages, extension pages, and restricted frames can hide videos, so results may be incomplete.', + 'Learn more tooltip: Read the VidPlex README and audit notes.', + ]; + + for (const text of expectedTexts) { + if (!doc.body.textContent.includes(text)) { + throw new Error(`Missing instructions copy: ${text}`); + } + } + } finally { + cleanup(); + } + }); + + it('toggles the instructions panel open and closed', async () => { + const { dom, cleanup } = await mountPopup(); + try { + const doc = dom.window.document; + const toggle = doc.getElementById('instructions-toggle'); + const body = doc.getElementById('instructions-body'); + + if (!toggle || !body) throw new Error('Instructions controls missing'); + + toggle.click(); + if (toggle.getAttribute('aria-expanded') !== 'false') throw new Error('Toggle should collapse the instructions'); + if (!body.hidden) throw new Error('Instructions body should be hidden after collapse'); + 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 { + cleanup(); + } + }); +}); diff --git a/__tests__/run.mjs b/__tests__/run.mjs index 5d1270b..73e2026 100644 --- a/__tests__/run.mjs +++ b/__tests__/run.mjs @@ -3,6 +3,7 @@ */ import './schema.test.mjs'; import './viewer_units.test.mjs'; +import './instructions_ui.test.mjs'; import './data_flow.test.mjs'; import './viewer_integration.test.mjs'; import './config_validation.test.mjs'; diff --git a/docs/instructions-audit.md b/docs/instructions-audit.md new file mode 100644 index 0000000..101bc9e --- /dev/null +++ b/docs/instructions-audit.md @@ -0,0 +1,25 @@ +# Instructions UI audit + +## Scope +This audit covers the VidPlex popup instructions banner added in `src/viewer/index.html`, `src/viewer/index.css`, and `src/viewer/index.js`. + +## What the UI now explains +- How to scan the current page for videos +- How to review clickable results +- How to open the 2x2 viewer +- How to clear and rescan +- A caveat that browser pages, extension pages, and restricted frames may hide videos + +## Accessibility checks +- The instructions section uses `role="region"` and an accessible label +- The collapse toggle is wired with `aria-expanded` and `aria-controls` +- The body is hidden with the `hidden` attribute when collapsed + +## UX notes +- The panel is visible on mount so first-time users see the steps immediately +- The toggle label changes between “Hide instructions” and “Show instructions” +- The copy intentionally stays short so the popup remains readable in a narrow extension window + +## Validation +- Covered by `__tests__/instructions_ui.test.mjs` +- Verified alongside the existing build/test flow in the parent implementation task diff --git a/docs/instructions-copy.md b/docs/instructions-copy.md new file mode 100644 index 0000000..03fc59e --- /dev/null +++ b/docs/instructions-copy.md @@ -0,0 +1,17 @@ +# Instructions UI copy + +## Banner copy +Quick start +How to use VidPlex + +## Steps +1. Scan videos — Click Find Videos to search the current browser window for videos. +2. Review the list — Check the popup list; each found video URL is clickable. +3. Open the viewer — Click Open Viewer to open the 2x2 grid player in a new window. +4. Reset and rescan — Click Clear to erase results and scan again. + +## Warning copy +Warning: Browser pages, extension pages, and restricted frames can hide videos, so results may be incomplete. + +## Learn-more copy +Learn more tooltip: Read the VidPlex README and audit notes. diff --git a/docs/instructions-demo.svg b/docs/instructions-demo.svg new file mode 100644 index 0000000..997fa78 --- /dev/null +++ b/docs/instructions-demo.svg @@ -0,0 +1,50 @@ + + + + + + + VidPlex + + + QUICK START + How to use VidPlex + + + Hide instructions + + + Scan videos + Click Find Videos to search the current browser window for videos. + + Review the list + Check the popup list; each found video URL is clickable. + + Open the viewer + Click Open Viewer to open the 2x2 grid player in a new window. + + Reset and rescan + Click Clear to erase results and scan again. + + Warning: Browser pages, extension pages, and restricted frames can hide videos, so results may be incomplete. + Learn more tooltip: Read the VidPlex README and audit notes. + + + + Find Videos (all tabs) + diff --git a/docs/instructions-design.md b/docs/instructions-design.md new file mode 100644 index 0000000..3e88e3a --- /dev/null +++ b/docs/instructions-design.md @@ -0,0 +1,26 @@ +# Instructions UI design + +## Placement +The instructions banner sits above the primary action button so new users can read the steps before interacting with the popup. + +## Structure +- Short kicker: “Quick start” +- Heading: “How to use VidPlex” +- Expand/collapse toggle for users who want a smaller popup +- Ordered list of the core tasks +- One warning line for edge cases +- One short learn-more line that points to the README and audit notes + +## Copy strategy +The microcopy avoids jargon and keeps each step tied to a visible control: +- Find Videos +- Open Viewer +- Clear + +## Accessibility strategy +- A labelled region exposes the whole panel to assistive tech +- The toggle reflects the current visibility state +- The collapsed state removes the body from the layout entirely + +## Notes +This design intentionally favors clarity over density because extension popups are small and users typically need the instructions only once. diff --git a/src/viewer/index.css b/src/viewer/index.css index 2266b6e..4f63b3d 100644 --- a/src/viewer/index.css +++ b/src/viewer/index.css @@ -32,6 +32,90 @@ h1 { font-weight: 600; } +.instructions-panel { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: #f8fafc; +} + +.instructions-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.instructions-kicker { + margin-bottom: 2px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #64748b; +} + +.instructions-panel h2 { + font-size: 14px; + line-height: 1.3; + font-weight: 600; +} + +.instructions-toggle { + flex: 0 0 auto; + padding: 6px 10px; + font-size: 12px; + background: transparent; + color: var(--accent); + border: 1px solid var(--border); +} + +.instructions-toggle:hover { + background: #eef2ff; +} + +.instructions-body { + display: flex; + flex-direction: column; + gap: 8px; +} + +.instructions-body[hidden] { + display: none; +} + +.instructions-list { + display: flex; + flex-direction: column; + gap: 8px; + padding-left: 18px; +} + +.instructions-list li { + display: flex; + flex-direction: column; + gap: 2px; +} + +.instructions-list strong { + font-size: 12px; +} + +.instructions-list span, +.instructions-note, +.instructions-learn-more { + font-size: 12px; + line-height: 1.45; + color: #475569; +} + +.instructions-note { + color: #7c3aed; +} + button { padding: 8px 16px; background: var(--accent); diff --git a/src/viewer/index.html b/src/viewer/index.html index 13d579e..9cfe23c 100644 --- a/src/viewer/index.html +++ b/src/viewer/index.html @@ -9,6 +9,45 @@

VidPlex

+
+
+
+

Quick start

+

How to use VidPlex

+
+ +
+
+
    +
  1. + Scan videos + Click Find Videos to search the current browser window for videos. +
  2. +
  3. + Review the list + Check the popup list; each found video URL is clickable. +
  4. +
  5. + Open the viewer + Click Open Viewer to open the 2x2 grid player in a new window. +
  6. +
  7. + Reset and rescan + Click Clear to erase results and scan again. +
  8. +
+

Warning: Browser pages, extension pages, and restricted frames can hide videos, so results may be incomplete.

+

Learn more tooltip: Read the VidPlex README and audit notes.

+
+
diff --git a/src/viewer/index.js b/src/viewer/index.js index b604719..018c7a8 100644 --- a/src/viewer/index.js +++ b/src/viewer/index.js @@ -34,10 +34,26 @@ const openBtn = document.getElementById('open-btn'); const statusEl = document.getElementById('status'); const listEl = document.getElementById('video-list'); const btnRow = document.getElementById('btn-row'); +const instructionsToggle = document.getElementById('instructions-toggle'); +const instructionsBody = document.getElementById('instructions-body'); listEl.style.wordBreak = 'break-word'; listEl.style.whiteSpace = 'pre-wrap'; +function setInstructionsExpanded(expanded) { + if (!instructionsToggle || !instructionsBody) return; + instructionsBody.hidden = !expanded; + instructionsToggle.setAttribute('aria-expanded', String(expanded)); + instructionsToggle.textContent = expanded ? 'Hide instructions' : 'Show instructions'; +} + +if (instructionsToggle && instructionsBody) { + setInstructionsExpanded(true); + instructionsToggle.addEventListener('click', () => { + setInstructionsExpanded(instructionsBody.hidden); + }); +} + /* ======================================================================== */ /* UI helpers */ /* ======================================================================== */