mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-20 01:56:37 +03:00
AI-assisted implementation by Codex at maintainer pbakaus request. Merge current main without dropping upstream oracle updates. Resolve component waivers while the actual visual candidate is in hand; carry stamps through sampling and screenshot fallback. Forward project selectors through the live prelude and per-page scan config, and keep waived findings out of direct/snapshot/offscreen UI groups. Add Rust and Node regressions. Rust workspace, rebuilt bundle/engine, full Bun/Node/oracle/plugin suite and real Chrome direct/snapshot checks passed. Deterministic full live E2E sweep is still running. Generated provider harness output intentionally omitted.
383 lines
16 KiB
JavaScript
383 lines
16 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('selector waivers in scan consumers', () => {
|
|
const source = readFileSync(join(REPO_ROOT, 'browser-bundle/30-scan-common.js'), 'utf8');
|
|
const api = vm.runInThisContext(`(function () { ${source}; return {
|
|
options: __visualContrastOptions, reportable: __reportableGroups,
|
|
}; })()`, { filename: '30-scan-common.js' });
|
|
|
|
it('carries page-resolved selectors into the visual and lazy candidate pass', () => {
|
|
const ignoreSelectors = [{ rule: 'low-contrast', selector: '.Card' }];
|
|
assert.deepEqual(api.options({}, { ignoreSelectors }).ignoreSelectors, ignoreSelectors);
|
|
assert.equal(api.options({}, { ignoreSelectors: null }).ignoreSelectors, undefined);
|
|
});
|
|
|
|
it('keeps raw stamps but never renders waived-only groups', () => {
|
|
const groups = [
|
|
{ el: 1, findings: [{ type: 'low-contrast', ignoredBy: '.Card' }] },
|
|
{ el: 2, findings: [{ type: 'side-tab' }, { type: 'low-contrast', ignoredBy: '.Card' }] },
|
|
];
|
|
assert.deepEqual(api.reportable(groups), [{ el: 2, findings: [{ type: 'side-tab' }] }]);
|
|
assert.equal(groups[0].findings.length, 1);
|
|
assert.equal(groups[1].findings.length, 2);
|
|
});
|
|
});
|
|
|
|
describe('live-browser-ignores resolver', () => {
|
|
it('resolves component selectors for the served page without changing selector case', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
pageFiles: ['prototype/index.html', 'prototype/other.html'],
|
|
ignoreSelectors: [
|
|
{ rule: ' Low-Contrast ', selector: ' .Card ' },
|
|
{ rule: '*', selector: '#Hero', files: ['prototype/index.html'] },
|
|
{ rule: '*', selector: '.Other', files: ['prototype/other.html'] },
|
|
null, {}, { rule: '*', selector: 7 },
|
|
],
|
|
};
|
|
assert.deepEqual(resolve({ ignores, pathname: '/' }).ignoreSelectors, [
|
|
{ rule: 'low-contrast', selector: '.Card' },
|
|
{ rule: '*', selector: '#Hero' },
|
|
]);
|
|
assert.deepEqual(resolve({ ignores, pathname: '/other.html' }).ignoreSelectors, [
|
|
{ rule: 'low-contrast', selector: '.Card' },
|
|
{ rule: '*', selector: '.Other' },
|
|
]);
|
|
assert.deepEqual(resolve({ ignores: { ...ignores, ignoreFiles: ['**'] }, pathname: '/' }),
|
|
{ disabledRules: [], disabledValues: [], skipScan: true });
|
|
});
|
|
|
|
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']);
|
|
});
|
|
});
|