Files
goon_plugin/__tests__/instructions_ui.test.mjs
T
robot 42c1ad75c8
CI / test (pull_request) Successful in 6m29s
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.
2026-09-30 03:09:25 +00:00

270 lines
10 KiB
JavaScript

import { describe, it } from './runner.mjs';
import { JSDOM } from 'jsdom';
const POPUP_HTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VidPlex</title>
</head>
<body>
<div id="app">
<h1>VidPlex</h1>
<section class="instructions-panel" role="region" aria-labelledby="instructions-title">
<div class="instructions-header">
<div>
<p class="instructions-kicker">Quick start</p>
<h2 id="instructions-title">How to use VidPlex</h2>
</div>
<button
id="instructions-toggle"
type="button"
class="instructions-toggle"
aria-expanded="true"
aria-controls="instructions-body"
>
Hide instructions
</button>
</div>
<div id="instructions-body" class="instructions-body">
<ol class="instructions-list">
<li>
<strong>Scan videos</strong>
<span>Click <strong>Find Videos</strong> to search the current browser window for videos.</span>
</li>
<li>
<strong>Review the list</strong>
<span>Check the popup list; each found video URL is clickable.</span>
</li>
<li>
<strong>Open the viewer</strong>
<span>Click <strong>Open Viewer</strong> to open the 2x2 grid player in a new window.</span>
</li>
<li>
<strong>Reset and rescan</strong>
<span>Click <strong>Clear</strong> to erase results and scan again.</span>
</li>
</ol>
<p class="instructions-note">Warning: Browser pages, extension pages, and restricted frames can hide videos, so results may be incomplete.</p>
<p class="instructions-learn-more">Learn more tooltip: Read the VidPlex README and audit notes.</p>
</div>
</section>
<button id="find-btn">Find Videos (all tabs)</button>
<div id="video-list" class="video-list" style="display:none;"></div>
<div id="status" class="status" style="min-height:16px;"></div>
<div id="btn-row" class="btn-row" style="display:none;">
<button id="clear-btn" class="btn-secondary">Clear</button>
<button id="open-btn">Open Viewer</button>
</div>
</div>
</body>
</html>`;
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 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);
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 mountPopupWithStorage();
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 mountPopupWithStorage();
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();
}
});
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();
}
});
});