mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Read waiver config from every live root (appRoot, contextRoot, repoRoot), so monorepo projects whose config lives at the repo root reach the overlay; serialize served roots and page identities repo-relative there. Resolve each page URL to its actual serving file via the inject config's resolved page list before applying file-scoped waivers; ambiguous URLs keep the conservative common-ancestor fallback (PR #645 review discussion r3840011436). Honour detector.ignoreFiles: a wholly waived page now scans to zero findings in the overlay, matching the CLI and the edit hook. Guard the resolver call so a throwing resolver degrades to an unfiltered scan instead of breaking the detect toggle. Match design-system-color waivers by color value across hex and rgb() spellings, and stop extracting font values for bounce-easing findings, mirroring extractFindingIgnoreValue. Regenerate the browser bundle. AI-assisted change: reviewed, planned, and implemented with Claude Code under maintainer direction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
337 lines
14 KiB
JavaScript
337 lines
14 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import vm from 'node:vm';
|
|
|
|
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const SCRIPT = join(REPO_ROOT, 'skill/scripts/live-browser-ignores.js');
|
|
|
|
// Evaluated in the test realm (not a vm context) so the arrays the resolver
|
|
// returns share this realm's prototypes and deepEqual compares them plainly.
|
|
function loadIgnoresApi() {
|
|
const source = readFileSync(SCRIPT, 'utf-8');
|
|
const factory = vm.runInThisContext(
|
|
`(function (window) {\n${source}\nreturn window.__IMPECCABLE_LIVE_IGNORES__;\n})`,
|
|
{ filename: SCRIPT },
|
|
);
|
|
return factory({});
|
|
}
|
|
|
|
const resolve = loadIgnoresApi().resolveDetectIgnores;
|
|
|
|
const EMPTY = { disabledRules: [], disabledValues: [], skipScan: false };
|
|
|
|
describe('live-browser-ignores resolver', () => {
|
|
it('registers a versioned API on the root', () => {
|
|
const api = loadIgnoresApi();
|
|
assert.equal(api.version, 1);
|
|
assert.equal(typeof api.resolveDetectIgnores, 'function');
|
|
});
|
|
|
|
it('degrades to an empty filter when the config global is missing or malformed', () => {
|
|
assert.deepEqual(resolve(), EMPTY);
|
|
assert.deepEqual(resolve({ ignores: undefined, pathname: '/index.html' }), EMPTY);
|
|
assert.deepEqual(resolve({ ignores: null, pathname: '/index.html' }), EMPTY);
|
|
assert.deepEqual(resolve({ ignores: 'nonsense', pathname: '/index.html' }), EMPTY);
|
|
assert.deepEqual(resolve({ ignores: {}, pathname: '/index.html' }), EMPTY);
|
|
});
|
|
|
|
it('does not spread a string ignoreRules into characters', () => {
|
|
// `"ignoreRules": "foo"` in a hand-edited config must disable nothing,
|
|
// not look like it disabled three one-letter rules.
|
|
const out = resolve({ ignores: { ignoreRules: 'foo' }, pathname: '/index.html' });
|
|
assert.deepEqual(out, EMPTY);
|
|
});
|
|
|
|
it('forwards ignoreRules normalized and deduplicated', () => {
|
|
const out = resolve({
|
|
ignores: { ignoreRules: ['Dark-Glow', 'dark-glow', ' gradient-text ', '', 42, null] },
|
|
pathname: '/index.html',
|
|
});
|
|
assert.deepEqual(out.disabledRules, ['dark-glow', 'gradient-text']);
|
|
});
|
|
|
|
it('forwards unscoped value entries and drops malformed ones', () => {
|
|
const out = resolve({
|
|
ignores: {
|
|
ignoreValues: [
|
|
{ rule: 'overused-font', value: 'Geist+Mono' },
|
|
null,
|
|
'not-an-entry',
|
|
{ rule: '', value: 'x' },
|
|
{ rule: 'overused-font', value: '' },
|
|
],
|
|
},
|
|
pathname: '/index.html',
|
|
});
|
|
assert.deepEqual(out.disabledValues, [{ rule: 'overused-font', value: 'geist mono' }]);
|
|
});
|
|
|
|
it('applies wildcard entries only on pages their globs name', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['prototype/attack-the-soc.html'] },
|
|
],
|
|
};
|
|
const onPage = resolve({ ignores, pathname: '/attack-the-soc.html' });
|
|
assert.deepEqual(onPage.disabledRules, ['dark-glow']);
|
|
const elsewhere = resolve({ ignores, pathname: '/index.html' });
|
|
assert.deepEqual(elsewhere.disabledRules, []);
|
|
});
|
|
|
|
it('never applies an unscoped wildcard entry, matching the CLI', () => {
|
|
// isIgnoredFindingValue in cli/lib/impeccable-config.mjs returns false
|
|
// for a wildcard entry with no files; project-wide suppression is
|
|
// ignoreRules' job.
|
|
const out = resolve({
|
|
ignores: { ignoreValues: [{ rule: 'dark-glow', value: '*' }] },
|
|
pathname: '/index.html',
|
|
});
|
|
assert.deepEqual(out, EMPTY);
|
|
});
|
|
|
|
it('drops scoped value entries on pages outside their globs', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'overused-font', value: 'geist mono', files: ['prototype/mgmt-demo.html'] },
|
|
],
|
|
};
|
|
const onPage = resolve({ ignores, pathname: '/mgmt-demo.html' });
|
|
assert.deepEqual(onPage.disabledValues, [{ rule: 'overused-font', value: 'geist mono' }]);
|
|
const elsewhere = resolve({ ignores, pathname: '/index.html' });
|
|
assert.deepEqual(elsewhere.disabledValues, []);
|
|
});
|
|
|
|
it('does not lend one entry\'s glob prefix to other pages', () => {
|
|
// The trap from issue #639: prefixes come only from `roots`, never from
|
|
// the ignore globs themselves. An entry scoped to prototype/library/**
|
|
// must not suppress on prototype/index.html.
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'em-dash-overuse', value: '*', files: ['prototype/library/**'] },
|
|
],
|
|
};
|
|
const inside = resolve({ ignores, pathname: '/library/buttons.html' });
|
|
assert.deepEqual(inside.disabledRules, ['em-dash-overuse']);
|
|
const outside = resolve({ ignores, pathname: '/index.html' });
|
|
assert.deepEqual(outside.disabledRules, []);
|
|
});
|
|
|
|
it('resolves root and directory URLs to their index file', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['prototype/index.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['prototype/news/index.html'] },
|
|
],
|
|
};
|
|
assert.deepEqual(resolve({ ignores, pathname: '/' }).disabledRules, ['dark-glow']);
|
|
assert.deepEqual(resolve({ ignores, pathname: '/news/' }).disabledRules, ['gradient-text']);
|
|
});
|
|
|
|
it('matches path suffixes like the CLI scoped-file matcher', () => {
|
|
// findingMatchesScopedIgnoreFile tries every path suffix of the finding's
|
|
// file, so `library/**` written without the prototype/ prefix still
|
|
// scopes to the library pages.
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'em-dash-overuse', value: '*', files: ['library/**'] },
|
|
{ rule: 'dark-glow', value: '*', files: ['buttons.html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/library/buttons.html' });
|
|
assert.deepEqual(out.disabledRules.sort(), ['dark-glow', 'em-dash-overuse']);
|
|
});
|
|
|
|
it('supports the CLI glob dialect, including alternation', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['prototype/{index,about}.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['prototype/page-?.html'] },
|
|
],
|
|
};
|
|
assert.deepEqual(resolve({ ignores, pathname: '/about.html' }).disabledRules, ['dark-glow']);
|
|
assert.deepEqual(resolve({ ignores, pathname: '/page-3.html' }).disabledRules, ['gradient-text']);
|
|
assert.deepEqual(resolve({ ignores, pathname: '/page-33.html' }).disabledRules, []);
|
|
});
|
|
|
|
it('treats glob metacharacters in filenames literally', () => {
|
|
const ignores = {
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['pricing (v2).html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/pricing (v2).html' });
|
|
assert.deepEqual(out.disabledRules, ['dark-glow']);
|
|
const near = resolve({ ignores, pathname: '/pricing xv2y.html' });
|
|
assert.deepEqual(near.disabledRules, []);
|
|
});
|
|
|
|
it('accepts a single `file` string alongside `files`', () => {
|
|
const out = resolve({
|
|
ignores: {
|
|
ignoreValues: [{ rule: 'dark-glow', value: '*', file: 'index.html' }],
|
|
},
|
|
pathname: '/index.html',
|
|
});
|
|
assert.deepEqual(out.disabledRules, ['dark-glow']);
|
|
});
|
|
|
|
it('asserts no prefix when the configured roots share no common ancestor', () => {
|
|
// With src/**/*.html and public/**/*.html both configured, no single
|
|
// document root maps /foo.html to a unique project file, so no prefix
|
|
// is asserted. A waiver naming src/foo.html must not hide a finding on
|
|
// a page served from public/foo.html; ambiguity resolves to showing
|
|
// the finding. Bare-path spellings still apply whichever root serves it.
|
|
const ignores = {
|
|
roots: ['src/', 'public/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['src/foo.html'] },
|
|
{ rule: 'clipped-overflow-container', value: '*', files: ['src/foo.html', 'public/foo.html'] },
|
|
{ rule: 'em-dash-overuse', value: '*', files: ['foo.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['**/foo.html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/foo.html' });
|
|
assert.deepEqual(out.disabledRules.sort(), ['em-dash-overuse', 'gradient-text']);
|
|
});
|
|
|
|
it('reduces nested roots to their common ancestor so normal waivers keep applying', () => {
|
|
// Globs at two depths in one tree (prototype/*.html plus
|
|
// prototype/library/**/*.html) derive the prefixes prototype/ and
|
|
// prototype/library/. Those are not alternative identities: one server
|
|
// serves both, so the document root sits at their common ancestor and
|
|
// a project-relative waiver like prototype/index.html must apply.
|
|
const ignores = {
|
|
roots: ['prototype/', 'prototype/library/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['prototype/index.html'] },
|
|
{ rule: 'em-dash-overuse', value: '*', files: ['prototype/library/**'] },
|
|
],
|
|
};
|
|
assert.deepEqual(
|
|
resolve({ ignores, pathname: '/index.html' }).disabledRules,
|
|
['dark-glow'],
|
|
);
|
|
assert.deepEqual(
|
|
resolve({ ignores, pathname: '/library/buttons.html' }).disabledRules,
|
|
['em-dash-overuse'],
|
|
);
|
|
});
|
|
|
|
it('resolves a URL to its one served file when pageFiles knows it', () => {
|
|
// PR #645 review discussion r3840011436: with src/ and public/ both
|
|
// served, /foo.html used to borrow identities from every root. The
|
|
// served page list disambiguates: this URL serves public/foo.html, so
|
|
// src-scoped waivers must not apply.
|
|
const ignores = {
|
|
roots: ['src/', 'public/'],
|
|
pageFiles: ['src/other.html', 'public/foo.html'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['src/foo.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['public/foo.html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/foo.html' });
|
|
assert.deepEqual(out.disabledRules, ['gradient-text']);
|
|
});
|
|
|
|
it('keeps ambiguity conservative when served files share the URL suffix', () => {
|
|
const ignores = {
|
|
roots: ['src/', 'public/'],
|
|
pageFiles: ['src/foo.html', 'public/foo.html'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['src/foo.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['public/foo.html'] },
|
|
{ rule: 'em-dash-overuse', value: '*', files: ['foo.html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/foo.html' });
|
|
// Neither root-scoped waiver can claim the page; the bare spelling
|
|
// still applies whichever root serves it.
|
|
assert.deepEqual(out.disabledRules, ['em-dash-overuse']);
|
|
});
|
|
|
|
it('falls back to the common ancestor when index files collide across depths', () => {
|
|
// /index.html suffix-matches both served index files; the ambiguity
|
|
// resolves through the common ancestor, which still yields the correct
|
|
// shallow identity and never the deep one.
|
|
const ignores = {
|
|
roots: ['prototype/', 'prototype/library/'],
|
|
pageFiles: ['prototype/index.html', 'prototype/library/index.html'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['prototype/index.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['prototype/library/index.html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/index.html' });
|
|
assert.deepEqual(out.disabledRules, ['dark-glow']);
|
|
});
|
|
|
|
it('skips the scan on pages named by ignoreFiles', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreFiles: ['prototype/library/**'],
|
|
ignoreRules: ['dark-glow'],
|
|
};
|
|
const waived = resolve({ ignores, pathname: '/library/buttons.html' });
|
|
assert.deepEqual(waived, { disabledRules: [], disabledValues: [], skipScan: true });
|
|
const scanned = resolve({ ignores, pathname: '/index.html' });
|
|
assert.equal(scanned.skipScan, false);
|
|
assert.deepEqual(scanned.disabledRules, ['dark-glow']);
|
|
});
|
|
|
|
it('matches ignoreFiles by basename like the CLI glob matcher', () => {
|
|
const out = resolve({
|
|
ignores: { ignoreFiles: ['buttons.html'], roots: ['prototype/'] },
|
|
pathname: '/library/buttons.html',
|
|
});
|
|
assert.equal(out.skipScan, true);
|
|
});
|
|
|
|
it('treats a malformed ignoreFiles value as no waiver at all', () => {
|
|
const out = resolve({
|
|
ignores: { ignoreFiles: 'prototype/**', roots: [] },
|
|
pathname: '/index.html',
|
|
});
|
|
assert.equal(out.skipScan, false);
|
|
});
|
|
|
|
it('drops entries scoped to source paths that no route URL can match', () => {
|
|
// Framework apps inject into source files while scans see route URLs; a
|
|
// source-scoped entry must fail conservative (finding shown), never
|
|
// suppress by accident. Pinned so a refactor cannot flip the direction.
|
|
const ignores = {
|
|
roots: ['src/'],
|
|
pageFiles: ['src/routes/about/+page.svelte'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['src/routes/about/+page.svelte'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/about' });
|
|
assert.deepEqual(out.disabledRules, []);
|
|
assert.equal(out.skipScan, false);
|
|
});
|
|
|
|
it('survives malformed roots and percent-escapes without throwing', () => {
|
|
const out = resolve({
|
|
ignores: {
|
|
roots: 7,
|
|
pageFiles: 'not-a-list',
|
|
ignoreFiles: [null, 42],
|
|
ignoreRules: ['dark-glow'],
|
|
ignoreValues: [{ rule: 'gradient-text', value: '*', files: ['broken%.html'] }],
|
|
},
|
|
pathname: '/broken%.html',
|
|
});
|
|
assert.deepEqual(out.disabledRules.sort(), ['dark-glow', 'gradient-text']);
|
|
});
|
|
});
|