Files
KoalaSync/tests/e2e/popup-blacklist.spec.mjs
T
KoalaDev 66971150c7 test: cover the popup blacklist UI, tighten audit findings
The delta model was only covered at module level; the popup wiring around
it was not exercised at all. Seven specs now drive the real settings UI in
the packed extension, including the migration path: a pre-v3.1.0 snapshot
is converted on open, the legacy key is removed, and a default missing
from that snapshot is delivered again.

Also from the audit pass:
- await the blacklist read in init instead of firing a floating promise
- unify the debug report on the finder's own candidate list, which the
  separate traversal missed shadow-DOM videos from
- assert that a single candidate is always returned regardless of its
  ranking signals, so no scoring signal can regress a single-player site

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 07:39:41 +02:00

152 lines
7.2 KiB
JavaScript

import { test, expect, openPopup, readStorage, writeStorage } from './helpers/extension-fixture.mjs';
import { BLACKLIST_DOMAINS } from '../../shared/blacklist.js';
/**
* Drives the real settings UI in the real popup. The shared module is unit
* tested on its own; what this file covers is the wiring around it, which is
* where a delta model can quietly fall back to snapshot behaviour.
*/
const OVERRIDES_KEY = 'blacklistOverrides';
const LEGACY_KEY = 'customBlacklistDomains';
/** The editor body without its comment headers, in order. */
async function readEditorDomains(page) {
return page.evaluate(() => document.getElementById('blacklistDomains').value
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#')));
}
async function setEditorDomains(page, domains) {
await page.evaluate(value => {
document.getElementById('blacklistDomains').value = value;
}, domains.join('\n'));
}
test('shows the shipped defaults grouped under section headers', async ({ context, extensionId }) => {
const page = await openPopup(context, extensionId);
const body = await page.inputValue('#blacklistDomains');
const headers = body.split('\n').filter(line => line.startsWith('#'));
expect(headers, 'both section headers should be present').toHaveLength(2);
expect(headers[0].replace('#', '').trim(), 'the user section header should be translated').not.toBe('');
const domains = await readEditorDomains(page);
expect(domains).toEqual(BLACKLIST_DOMAINS);
const stored = await readStorage(page, [OVERRIDES_KEY]);
expect(stored[OVERRIDES_KEY], 'an untouched list stores nothing').toBeUndefined();
});
test('saves only the delta when a default is removed and a domain is added', async ({ context, extensionId }) => {
const page = await openPopup(context, extensionId);
const kept = BLACKLIST_DOMAINS.filter(domain => domain !== 'reddit.com');
await setEditorDomains(page, [...kept, 'videos.example']);
await page.click('#blacklistSave');
await expect(page.locator('#blacklistStatus')).toHaveAttribute('data-state', 'success');
const stored = await readStorage(page, [OVERRIDES_KEY]);
expect(stored[OVERRIDES_KEY]).toEqual({
removedDefaults: ['reddit.com'],
addedDomains: ['videos.example']
});
});
test('regroups a saved entry under the user section after reopening', async ({ context, extensionId }) => {
const first = await openPopup(context, extensionId);
await setEditorDomains(first, [...BLACKLIST_DOMAINS, 'videos.example']);
await first.click('#blacklistSave');
await expect(first.locator('#blacklistStatus')).toHaveAttribute('data-state', 'success');
await first.close();
const second = await openPopup(context, extensionId);
const body = await second.inputValue('#blacklistDomains');
const lines = body.split('\n').map(line => line.trim());
const userHeaderIndex = lines.findIndex(line => line.startsWith('#'));
const defaultHeaderIndex = lines.findIndex((line, i) => i > userHeaderIndex && line.startsWith('#'));
expect(lines.indexOf('videos.example'), 'the user entry sits in the user section')
.toBeLessThan(defaultHeaderIndex);
expect(lines.indexOf('google.com'), 'a shipped default sits in the defaults section')
.toBeGreaterThan(defaultHeaderIndex);
});
test('rejects an invalid entry without saving anything', async ({ context, extensionId }) => {
const page = await openPopup(context, extensionId);
await setEditorDomains(page, [...BLACKLIST_DOMAINS, 'not a domain']);
await page.click('#blacklistSave');
await expect(page.locator('#blacklistStatus')).toHaveAttribute('data-state', 'error');
const stored = await readStorage(page, [OVERRIDES_KEY]);
expect(stored[OVERRIDES_KEY], 'a rejected save must not write a partial delta').toBeUndefined();
});
test('restore defaults clears the stored delta', async ({ context, extensionId }) => {
const page = await openPopup(context, extensionId);
await setEditorDomains(page, ['videos.example']);
await page.click('#blacklistSave');
await expect(page.locator('#blacklistStatus')).toHaveAttribute('data-state', 'success');
expect((await readStorage(page, [OVERRIDES_KEY]))[OVERRIDES_KEY]).toBeDefined();
await page.click('#blacklistReset');
await expect(page.locator('#blacklistStatus')).toHaveAttribute('data-state', 'success');
const stored = await readStorage(page, [OVERRIDES_KEY, LEGACY_KEY]);
expect(stored[OVERRIDES_KEY]).toBeUndefined();
expect(stored[LEGACY_KEY]).toBeUndefined();
expect(await readEditorDomains(page)).toEqual(BLACKLIST_DOMAINS);
});
test('migrates a pre-v3.1.0 snapshot and delivers defaults it never had', async ({ context, extensionId }) => {
// A snapshot saved by an older version: the user removed one default and
// added one of their own. 'reddit.com' stands in for a default that shipped
// after they saved, so their frozen snapshot simply does not contain it.
const snapshot = BLACKLIST_DOMAINS
.filter(domain => domain !== 'imgur.com' && domain !== 'reddit.com')
.concat(['videos.example']);
const seed = await openPopup(context, extensionId);
await writeStorage(seed, { [LEGACY_KEY]: snapshot });
await seed.close();
const page = await openPopup(context, extensionId);
const stored = await readStorage(page, [OVERRIDES_KEY, LEGACY_KEY]);
expect(stored[LEGACY_KEY], 'the legacy key is cleaned up').toBeUndefined();
expect(stored[OVERRIDES_KEY].removedDefaults).toEqual(['reddit.com', 'imgur.com']);
expect(stored[OVERRIDES_KEY].addedDomains).toEqual(['videos.example']);
const domains = await readEditorDomains(page);
expect(domains, 'their own entry survives migration').toContain('videos.example');
expect(domains, 'their removals survive migration').not.toContain('imgur.com');
});
test('the tab list honours a removed default and an added domain', async ({ context, extensionId, baseURL }) => {
const page = await openPopup(context, extensionId);
// youtube.com is not a shipped default, so it is visible by default. Adding
// it must hide it; the fixture host must stay visible either way.
await setEditorDomains(page, [...BLACKLIST_DOMAINS, 'youtube.com']);
await page.click('#blacklistSave');
await expect(page.locator('#blacklistStatus')).toHaveAttribute('data-state', 'success');
const filtered = await page.evaluate(async ({ fixture }) => {
const { getEffectiveBlacklistDomains, isUrlBlacklisted } = await import('./shared/blacklist.js');
const overrides = (await chrome.storage.local.get(['blacklistOverrides'])).blacklistOverrides;
const domains = getEffectiveBlacklistDomains(overrides);
return {
youtube: isUrlBlacklisted('https://www.youtube.com/watch?v=x', domains),
fixtureHost: isUrlBlacklisted(fixture, domains),
drive: isUrlBlacklisted('https://drive.google.com/file/d/x/view', domains)
};
}, { fixture: `${baseURL}/pages/simple-player.html` });
expect(filtered.youtube, 'an added domain is filtered').toBe(true);
expect(filtered.fixtureHost, 'an unrelated host stays visible').toBe(false);
expect(filtered.drive, 'drive keeps its player-path exception').toBe(false);
});