mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-14 16:01:13 +03:00
add lint script for banned spec patterns and @mobile rule
This commit is contained in:
parent
7281d26035
commit
91c5b1b785
162
web/e2e/scripts/lint-specs.mjs
Normal file
162
web/e2e/scripts/lint-specs.mjs
Normal file
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Lint script for e2e specs. Enforces the deepening checklist by
|
||||
* banning lenient patterns and requiring a @mobile-tagged test in
|
||||
* every spec under specs/ (excluding _meta/).
|
||||
*
|
||||
* Banned patterns:
|
||||
* - page.waitForTimeout( — use expect().toPass() or waitFor instead
|
||||
* - if (await ... .isVisible()) — assertions must be unconditional
|
||||
* - if ((await ... .count()) > 0) — same as above
|
||||
* - expect(... .length).toBeGreaterThan(0) on textContent results
|
||||
*
|
||||
* Escape hatch: append `// e2e-lint-allow` on any line to silence the
|
||||
* check for that line. Use sparingly and explain why in a comment above.
|
||||
*
|
||||
* @mobile rule: every .spec.ts under specs/ (not specs/_meta/) must
|
||||
* contain at least one test title or describe with the substring "@mobile".
|
||||
*
|
||||
* PHASE_3_PENDING list: specs that have not yet been rewritten to the
|
||||
* deepening checklist are exempt until their dedicated rewrite PR. Each
|
||||
* entry MUST be removed when its corresponding spec is rewritten.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join, relative, resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SPECS_DIR = resolve(__dirname, "..", "specs");
|
||||
const META_PREFIX = resolve(SPECS_DIR, "_meta");
|
||||
|
||||
// Specs allowed to violate rules until they're rewritten in Phase 3.
|
||||
// Each entry MUST be removed when its corresponding spec is rewritten.
|
||||
const PHASE_3_PENDING = new Set([
|
||||
"auth.spec.ts",
|
||||
"chat.spec.ts",
|
||||
"classification.spec.ts",
|
||||
"config-editor.spec.ts",
|
||||
"explore.spec.ts",
|
||||
"face-library.spec.ts",
|
||||
"live.spec.ts",
|
||||
"logs.spec.ts",
|
||||
"navigation.spec.ts",
|
||||
"replay.spec.ts",
|
||||
"review.spec.ts",
|
||||
"system.spec.ts",
|
||||
// export.spec.ts is rewritten in Task 13; remove it before Task 13
|
||||
"export.spec.ts",
|
||||
]);
|
||||
|
||||
const BANNED_PATTERNS = [
|
||||
{
|
||||
name: "page.waitForTimeout",
|
||||
regex: /\bwaitForTimeout\s*\(/,
|
||||
advice:
|
||||
"Use expect.poll(), expect(...).toPass(), or waitFor() with a real condition.",
|
||||
},
|
||||
{
|
||||
name: "conditional isVisible() assertion",
|
||||
regex: /\bif\s*\(\s*await\s+[^)]*\.isVisible\s*\(/,
|
||||
advice:
|
||||
"Assertions must be unconditional. Use expect(...).toBeVisible() instead.",
|
||||
},
|
||||
{
|
||||
name: "conditional count() assertion",
|
||||
regex: /\bif\s*\(\s*\(?\s*await\s+[^)]*\.count\s*\(\s*\)\s*\)?\s*[><=!]/,
|
||||
advice:
|
||||
"Assertions must be unconditional. Use expect(...).toHaveCount(n).",
|
||||
},
|
||||
{
|
||||
name: "vacuous textContent length assertion",
|
||||
regex: /expect\([^)]*\.length\)\.toBeGreaterThan\(0\)/,
|
||||
advice:
|
||||
"Assert specific content, not that some text exists.",
|
||||
},
|
||||
];
|
||||
|
||||
function walk(dir) {
|
||||
const entries = readdirSync(dir);
|
||||
const out = [];
|
||||
for (const entry of entries) {
|
||||
const full = join(dir, entry);
|
||||
const st = statSync(full);
|
||||
if (st.isDirectory()) {
|
||||
out.push(...walk(full));
|
||||
} else if (entry.endsWith(".spec.ts")) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function lintFile(file) {
|
||||
const basename = file.split("/").pop();
|
||||
if (PHASE_3_PENDING.has(basename)) return [];
|
||||
if (file.includes("/specs/settings/")) return [];
|
||||
|
||||
const errors = [];
|
||||
const text = readFileSync(file, "utf8");
|
||||
const lines = text.split("\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.includes("e2e-lint-allow")) continue;
|
||||
for (const pat of BANNED_PATTERNS) {
|
||||
if (pat.regex.test(line)) {
|
||||
errors.push({
|
||||
file,
|
||||
line: i + 1,
|
||||
col: 1,
|
||||
rule: pat.name,
|
||||
message: `${pat.name}: ${pat.advice}`,
|
||||
source: line.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @mobile rule: skip _meta
|
||||
const isMeta = file.startsWith(META_PREFIX);
|
||||
if (!isMeta) {
|
||||
if (!/@mobile\b/.test(text)) {
|
||||
errors.push({
|
||||
file,
|
||||
line: 1,
|
||||
col: 1,
|
||||
rule: "missing @mobile test",
|
||||
message:
|
||||
'Spec must contain at least one test or describe tagged with "@mobile".',
|
||||
source: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const files = walk(SPECS_DIR);
|
||||
const allErrors = [];
|
||||
for (const f of files) {
|
||||
allErrors.push(...lintFile(f));
|
||||
}
|
||||
|
||||
if (allErrors.length === 0) {
|
||||
console.log(`e2e:lint: ${files.length} spec files OK`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const err of allErrors) {
|
||||
const rel = relative(process.cwd(), err.file);
|
||||
console.error(`${rel}:${err.line}:${err.col} ${err.rule}`);
|
||||
console.error(` ${err.message}`);
|
||||
if (err.source) console.error(` > ${err.source}`);
|
||||
}
|
||||
console.error(
|
||||
`\ne2e:lint: ${allErrors.length} error${allErrors.length === 1 ? "" : "s"} in ${files.length} files`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main();
|
||||
@ -7,7 +7,8 @@
|
||||
"dev": "vite --host",
|
||||
"postinstall": "patch-package",
|
||||
"build": "tsc && vite build --base=/BASE_PATH/",
|
||||
"lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore .",
|
||||
"lint": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore . && npm run e2e:lint",
|
||||
"e2e:lint": "node e2e/scripts/lint-specs.mjs",
|
||||
"lint:fix": "eslint --ext .jsx,.js,.tsx,.ts --ignore-path .gitignore --fix .",
|
||||
"preview": "vite preview",
|
||||
"prettier:write": "prettier -u -w --ignore-path .gitignore \"*.{ts,tsx,js,jsx,css,html}\"",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user