mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 10:06:54 +03:00
The live overlay's detect scan ran unfiltered: requestDetectScan() posted
only { scanId }, so detector.ignoreRules and detector.ignoreValues in
.impeccable/config.json reached impeccable detect and the edit hook but
never the surface a designer actually watches.
The server now serializes the project's detector waivers into the /live.js
prelude (window.__IMPECCABLE_PROJECT_IGNORES__), read per request through
hook-lib's readConfig so config.local.json wins and edits land on the next
tab reload. A new script part, live-browser-ignores.js, resolves that
config against the page URL when a scan starts: ignoreRules suppress
outright, wildcard ignoreValues suppress their rule in the files their
globs name, and the remaining entries ride along as disabledValues for the
detector to match on each finding's own value. The detector bundle applies
those where the findings are assembled, since the overlay draws its own
markers from the collected findings.
Scope resolution mirrors cli/lib/impeccable-config.mjs deliberately: the
same glob dialect (globToRegex, including {a,b} alternation), the same
path-suffix matching as findingMatchesScopedIgnoreFile, and the same
refusal to apply an unscoped wildcard entry. The served-root prefixes that
bridge project-relative globs and site-relative URLs come from the inject
config's own files globs, never from the ignore globs; deriving them from
the ignore globs lets one entry scoped to prototype/library/** lend its
prefix to every page and suppress site-wide, which looks like success
because the numbers go down.
Known gaps, recorded in the detector comment: the motion value extractor
is not mirrored, so a value-scoped bounce-easing waiver only matches when
the finding carries ignoreValue directly, and design-system-color matches
on the normalized string without the CLI's color-equality fallback.
Tests: unit tests for the resolver part (stale globals, string ignoreRules,
malformed entries, directory URLs, percent-escapes, glob metacharacters,
the roots trap), an extension-mode puppeteer test that disabledValues
suppress exactly the waived findings, and the live-browser regression pin
now asserts the new scan config shape instead of { scanId }.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
198 lines
7.6 KiB
JavaScript
198 lines
7.6 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import vm from 'node:vm';
|
|
|
|
const REPO_ROOT = process.cwd();
|
|
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: [] };
|
|
|
|
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('survives malformed roots and percent-escapes without throwing', () => {
|
|
const out = resolve({
|
|
ignores: {
|
|
roots: 7,
|
|
ignoreRules: ['dark-glow'],
|
|
ignoreValues: [{ rule: 'gradient-text', value: '*', files: ['broken%.html'] }],
|
|
},
|
|
pathname: '/broken%.html',
|
|
});
|
|
assert.deepEqual(out.disabledRules.sort(), ['dark-glow', 'gradient-text']);
|
|
});
|
|
});
|