mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 08:36:25 +03:00
An author who wants a rule off for one component has had two choices: put `data-impeccable-ignore` on every instance, or silence the rule (or the file) for the whole project. On impeccable-site #34 that meant eleven attributes on eleven copies of the same 10px label for `undersized-ui-text`, 25 opt-out attributes in all. The count is the problem: the markup carries noise, and the reviewer never sees how much was waived. `detector.ignoreSelectors` is the declared twin of that attribute. One entry, `{ rule, selector }`, waives the rule for every element the selector matches and for that element's subtree, the same waiver the attribute grants the element carrying it: impeccable ignores add-selector undersized-ui-text ".ks-tag" \ --reason "10px mono index labels, confirmed" The waiver is never silent. The engines stamp a waived finding with `ignoredBy: "<selector>"` instead of dropping it; the config layer drops and counts, and every scan prints one line per entry on stderr, in `--json` runs too, so stdout stays the findings array: 3 undersized-ui-text hits ignored by detector.ignoreSelectors on .ks-tag. Where it applies: the browser engine (`BrowserConfig.ignoreSelectors`, also readable from `window.__IMPECCABLE_CONFIG__`), the static HTML engine (`DetectHtmlOptions.ignore_selectors`, and the `ignoreSelectors` option of the wasm `detect_html_source_json` export), the detect CLI, and the design hook. The text engine has no DOM and ignores the key. Entries can be scoped with `files` globs like `ignoreValues`; `--no-config` disables them; `doctor` validates their rule ids alongside `ignoreRules`. Nothing changes for a project without the key: the engines stamp nothing, the CLI prints nothing, the config writer does not add an empty `ignoreSelectors`, and the per-instance attribute keeps working exactly as before. Coverage: `crates/html/tests/selector_ignores.rs` (component, subtree, wrong-rule, `*`, attribute parity), driver tests over the fake DOM, `crates/detect` config tests (normalize, merge, per-target narrowing, the tally), and oracle cases `detect-selector-ignore-*` / `ignores-selector-*` over a new workspace. The eight re-recorded context/doctor goldens differ only in the recognized-detector-keys sentence, which now lists the new key. Assisted-by: Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
127 lines
9.1 KiB
JavaScript
127 lines
9.1 KiB
JavaScript
/**
|
|
* `impeccable detect` corpus.
|
|
*
|
|
* Every antipattern fixture is scanned individually in JSON and text mode with
|
|
* --no-config (the repo's own .impeccable config ignores tests/fixtures), plus
|
|
* directory scans, project-config / DESIGN.md / inline-ignore behaviour from
|
|
* the detect-config workspace, and the flag surface (help, scope, quiet,
|
|
* no-advisory, errors).
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { REPO_ROOT } from '../lib.mjs';
|
|
|
|
const FIXTURES = path.join(REPO_ROOT, 'tests', 'fixtures', 'antipatterns');
|
|
|
|
export default function cases() {
|
|
const out = [];
|
|
const entries = fs.readdirSync(FIXTURES, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
for (const ent of entries) {
|
|
const rel = `tests/fixtures/antipatterns/${ent.name}`;
|
|
const id = ent.name.replace(/[^a-z0-9]+/gi, '-').toLowerCase();
|
|
out.push({
|
|
id: `detect-fixture-json-${id}`,
|
|
verb: 'detect',
|
|
args: ['--no-config', '--json', `<REPO>/${rel}`],
|
|
isolateHome: false,
|
|
});
|
|
out.push({
|
|
id: `detect-fixture-text-${id}`,
|
|
verb: 'detect',
|
|
args: ['--no-config', `<REPO>/${rel}`],
|
|
isolateHome: false,
|
|
});
|
|
}
|
|
|
|
out.push(
|
|
{ id: 'detect-dir-json-all-fixtures', verb: 'detect', args: ['--no-config', '--json', `<REPO>/tests/fixtures/antipatterns`], isolateHome: false, timeoutMs: 180_000 },
|
|
{ id: 'detect-dir-text-all-fixtures', verb: 'detect', args: ['--no-config', `<REPO>/tests/fixtures/antipatterns`], isolateHome: false, timeoutMs: 180_000 },
|
|
{ id: 'detect-dir-quiet-all-fixtures', verb: 'detect', args: ['--no-config', '--quiet', `<REPO>/tests/fixtures/antipatterns`], isolateHome: false, timeoutMs: 180_000 },
|
|
{ id: 'detect-scope-type', verb: 'detect', args: ['--no-config', '--json', '--scope', 'type', `<REPO>/tests/fixtures/antipatterns`], isolateHome: false, timeoutMs: 180_000 },
|
|
{ id: 'detect-scope-layout-text', verb: 'detect', args: ['--no-config', '--scope', 'layout', `<REPO>/tests/fixtures/antipatterns`], isolateHome: false, timeoutMs: 180_000 },
|
|
{ id: 'detect-scope-both', verb: 'detect', args: ['--no-config', '--json', '--scope', 'type,layout', `<REPO>/tests/fixtures/antipatterns`], isolateHome: false, timeoutMs: 180_000 },
|
|
{ id: 'detect-scope-unknown', verb: 'detect', args: ['--no-config', '--json', '--scope', 'nope', `<REPO>/tests/fixtures/antipatterns/blinking-cursor.html`], isolateHome: false },
|
|
{ id: 'detect-no-advisory-json', verb: 'detect', args: ['--no-config', '--no-advisory', '--json', `<REPO>/tests/fixtures/antipatterns`], isolateHome: false, timeoutMs: 180_000 },
|
|
{ id: 'detect-no-advisory-text', verb: 'detect', args: ['--no-config', '--no-advisory', `<REPO>/tests/fixtures/antipatterns`], isolateHome: false, timeoutMs: 180_000 },
|
|
{ id: 'detect-multifile-json', verb: 'detect', args: ['--no-config', '--json', `<REPO>/tests/fixtures/antipatterns/multifile`], isolateHome: false },
|
|
{ id: 'detect-multifile-text', verb: 'detect', args: ['--no-config', `<REPO>/tests/fixtures/antipatterns/multifile`], isolateHome: false },
|
|
{ id: 'detect-framework-vite-json', verb: 'detect', args: ['--no-config', '--json', `<REPO>/tests/fixtures/antipatterns/framework-vite`], isolateHome: false },
|
|
{ id: 'detect-framework-next-tailwind-json', verb: 'detect', args: ['--no-config', '--json', `<REPO>/tests/fixtures/antipatterns/framework-next-tailwind`], isolateHome: false },
|
|
{ id: 'detect-framework-next-modules-text', verb: 'detect', args: ['--no-config', `<REPO>/tests/fixtures/antipatterns/framework-next-modules`], isolateHome: false },
|
|
{ id: 'detect-framework-next-cssinjs-json', verb: 'detect', args: ['--no-config', '--json', `<REPO>/tests/fixtures/antipatterns/framework-next-cssinjs`], isolateHome: false },
|
|
|
|
// Flag surface and errors
|
|
{ id: 'detect-help', verb: 'detect', args: ['--help'] },
|
|
{ id: 'detect-no-args', verb: 'detect', args: [] },
|
|
{ id: 'detect-missing-file', verb: 'detect', args: ['--no-config', 'does-not-exist.html'] },
|
|
{ id: 'detect-missing-file-json', verb: 'detect', args: ['--no-config', '--json', 'does-not-exist.html'] },
|
|
// #711: a target that cannot be scanned forces exit 1, and that takes
|
|
// precedence over findings from the targets that did scan.
|
|
{
|
|
id: 'detect-missing-file-with-findings', verb: 'detect',
|
|
args: ['--no-config', '--json', `<REPO>/tests/fixtures/antipatterns/layout.html`, 'does-not-exist.html'],
|
|
isolateHome: false,
|
|
},
|
|
{
|
|
id: 'detect-unreadable-file-json', verb: 'detect',
|
|
setup: (ws) => {
|
|
const p = path.join(ws, 'locked.html');
|
|
fs.writeFileSync(p, '<div style="border-left: 4px solid #ff0000">x</div>\n');
|
|
fs.chmodSync(p, 0o000);
|
|
},
|
|
args: ['--no-config', '--json', 'locked.html'],
|
|
},
|
|
{
|
|
id: 'detect-unreadable-file-in-dir', verb: 'detect',
|
|
setup: (ws) => {
|
|
fs.writeFileSync(path.join(ws, 'a.html'), '<div style="border-left: 4px solid #ff0000">x</div>\n');
|
|
const p = path.join(ws, 'b.html');
|
|
fs.writeFileSync(p, '<div style="border-left: 4px solid #ff0000">x</div>\n');
|
|
fs.chmodSync(p, 0o000);
|
|
},
|
|
args: ['--no-config', '--json', '.'],
|
|
},
|
|
{ id: 'detect-unknown-flag', verb: 'detect', args: ['--bogus', `<REPO>/tests/fixtures/antipatterns/blinking-cursor.html`], isolateHome: false },
|
|
{ id: 'detect-bad-viewport', verb: 'detect', args: ['--viewport', 'wide', `<REPO>/tests/fixtures/antipatterns/blinking-cursor.html`], isolateHome: false },
|
|
{ id: 'cli-help', verb: 'cli-help', args: [] },
|
|
{ id: 'cli-version', verb: 'cli-version', args: [] },
|
|
|
|
// Project config, DESIGN.md, inline ignores (detect-config workspace)
|
|
{ id: 'detect-config-page-json', verb: 'detect', workspace: 'detect-config', args: ['--json', 'src/page.html'] },
|
|
{ id: 'detect-config-page-text', verb: 'detect', workspace: 'detect-config', args: ['src/page.html'] },
|
|
{ id: 'detect-config-page-no-config', verb: 'detect', workspace: 'detect-config', args: ['--no-config', '--json', 'src/page.html'] },
|
|
{ id: 'detect-config-page-no-design-system', verb: 'detect', workspace: 'detect-config', args: ['--no-design-system', '--json', 'src/page.html'] },
|
|
{ id: 'detect-config-dir-json', verb: 'detect', workspace: 'detect-config', args: ['--json', 'src'] },
|
|
{ id: 'detect-config-dir-text', verb: 'detect', workspace: 'detect-config', args: ['src'] },
|
|
{ id: 'detect-config-dir-dot', verb: 'detect', workspace: 'detect-config', args: ['--json', '.'] },
|
|
{ id: 'detect-config-inline-json', verb: 'detect', workspace: 'detect-config', args: ['--json', 'src/inline.html'] },
|
|
{ id: 'detect-config-inline-disabled', verb: 'detect', workspace: 'detect-config', args: ['--no-inline-ignores', '--json', 'src/inline.html'] },
|
|
{ id: 'detect-config-css-json', verb: 'detect', workspace: 'detect-config', args: ['--json', 'src/styles.css'] },
|
|
{ id: 'detect-config-css-text', verb: 'detect', workspace: 'detect-config', args: ['src/styles.css'] },
|
|
{ id: 'detect-config-vendor-ignored', verb: 'detect', workspace: 'detect-config', args: ['--json', 'src/vendor/ignored.html'] },
|
|
{ id: 'detect-config-from-subdir', verb: 'detect', workspace: 'detect-config', cwd: 'src', args: ['--json', 'page.html'] },
|
|
// A file in one project must not pick up another project's DESIGN.md
|
|
{ id: 'detect-config-cross-project', verb: 'detect', workspace: 'detect-config', args: ['--json', `<REPO>/tests/fixtures/antipatterns/blinking-cursor.html`], isolateHome: false },
|
|
|
|
// Component-level opt-outs (detector.ignoreSelectors): one entry waives a
|
|
// rule for every instance of a component, and the scan says how many hits
|
|
// it suppressed instead of going quiet. The fourth label is outside the
|
|
// component and stays reported.
|
|
{ id: 'detect-selector-ignore-text', verb: 'detect', workspace: 'detect-selector-ignores', args: ['src/page.html'] },
|
|
{ id: 'detect-selector-ignore-json', verb: 'detect', workspace: 'detect-selector-ignores', args: ['--json', 'src/page.html'] },
|
|
{ id: 'detect-selector-ignore-quiet', verb: 'detect', workspace: 'detect-selector-ignores', args: ['--quiet', 'src/page.html'] },
|
|
{ id: 'detect-selector-ignore-no-config', verb: 'detect', workspace: 'detect-selector-ignores', args: ['--no-config', '--json', 'src/page.html'] },
|
|
|
|
// `impeccable ignores` CRUD for the same entries.
|
|
{ id: 'ignores-selector-list', verb: 'ignores', workspace: 'detect-selector-ignores', args: ['list'] },
|
|
{ id: 'ignores-selector-add', verb: 'ignores', workspace: 'detect-selector-ignores', args: ['add-selector', 'undersized-ui-text', '.free-label', '--reason', 'timestamp column'] },
|
|
{ id: 'ignores-selector-remove', verb: 'ignores', workspace: 'detect-selector-ignores', args: ['remove-selector', 'undersized-ui-text', '.ks-tag'] },
|
|
{ id: 'ignores-selector-missing-args', verb: 'ignores', workspace: 'detect-selector-ignores', args: ['add-selector', 'undersized-ui-text'] },
|
|
{ id: 'ignores-selector-star-refused', verb: 'ignores', workspace: 'detect-selector-ignores', args: ['add-selector', 'undersized-ui-text', '*'] },
|
|
{ id: 'ignores-help', verb: 'ignores', args: ['--help'] },
|
|
);
|
|
|
|
return out;
|
|
}
|