mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-11 17:47:37 +03:00
161 lines
4.5 KiB
JavaScript
161 lines
4.5 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* Lint script for e2e specs. Bans lenient test patterns and requires
|
||
|
|
* 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".
|
||
|
|
*
|
||
|
|
* Specs in PENDING_REWRITE are exempt from all rules until they are
|
||
|
|
* rewritten with proper assertions and mobile coverage. Remove each
|
||
|
|
* entry when its spec is updated.
|
||
|
|
*/
|
||
|
|
|
||
|
|
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 exempt from lint rules until they are rewritten with proper
|
||
|
|
// assertions and mobile coverage. Remove each entry when its spec is updated.
|
||
|
|
const PENDING_REWRITE = new Set([
|
||
|
|
"auth.spec.ts",
|
||
|
|
"chat.spec.ts",
|
||
|
|
"classification.spec.ts",
|
||
|
|
"config-editor.spec.ts",
|
||
|
|
"explore.spec.ts",
|
||
|
|
"export.spec.ts",
|
||
|
|
"face-library.spec.ts",
|
||
|
|
"live.spec.ts",
|
||
|
|
"logs.spec.ts",
|
||
|
|
"navigation.spec.ts",
|
||
|
|
"replay.spec.ts",
|
||
|
|
"review.spec.ts",
|
||
|
|
"system.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 (PENDING_REWRITE.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();
|