Complete training workflow and acceptance hardening

This commit is contained in:
andreysk0304 2026-09-26 18:12:27 +03:00 • committed by gglamer
commit 7237265833
243 changed files with 17014 additions and 1500 deletions

View file

@ -0,0 +1,80 @@
import fs from "node:fs";
import path from "node:path";
import ts from "typescript";
const root = path.resolve("src");
const files = [];
function collect(directory) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) collect(target);
else if (/\.tsx?$/.test(entry.name)) files.push(target);
}
}
collect(root);
const issues = [];
for (const file of files) {
const source = fs.readFileSync(file, "utf8");
const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const labelIds = new Set();
function findLabels(node) {
if (ts.isJsxOpeningElement(node) && node.tagName.getText(tree) === "label") {
const htmlFor = node.attributes.properties.find((item) =>
ts.isJsxAttribute(item) && item.name.getText(tree) === "htmlFor");
if (htmlFor?.initializer && ts.isStringLiteral(htmlFor.initializer)) {
labelIds.add(htmlFor.initializer.text);
}
}
ts.forEachChild(node, findLabels);
}
findLabels(tree);
function inspect(node) {
let opening = null;
if (ts.isJsxSelfClosingElement(node)) opening = node;
else if (ts.isJsxElement(node)) opening = node.openingElement;
if (opening) {
const tag = opening.tagName.getText(tree);
if (["input", "select", "textarea"].includes(tag)) {
const attrs = opening.attributes.properties.filter(ts.isJsxAttribute);
const hasAccessibleName = attrs.some((attr) => {
const name = attr.name.getText(tree);
if (!["aria-label", "aria-labelledby", "title"].includes(name)) return false;
return Boolean(attr.initializer && (
(ts.isStringLiteral(attr.initializer) && attr.initializer.text.trim())
|| ts.isJsxExpression(attr.initializer)
));
});
const idAttr = attrs.find((attr) => attr.name.getText(tree) === "id");
const id = idAttr?.initializer && ts.isStringLiteral(idAttr.initializer)
? idAttr.initializer.text : null;
let parent = node.parent;
let wrappedByLabel = false;
while (parent) {
if (ts.isJsxElement(parent) && parent.openingElement.tagName.getText(tree) === "label") {
wrappedByLabel = true;
break;
}
parent = parent.parent;
}
const typeAttr = attrs.find((attr) => attr.name.getText(tree) === "type");
const hidden = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer)
&& typeAttr.initializer.text === "hidden";
if (!hidden && !hasAccessibleName && !wrappedByLabel && !(id && labelIds.has(id))) {
const { line } = tree.getLineAndCharacterOfPosition(opening.getStart(tree));
issues.push(`${path.relative(root, file)}:${line + 1} <${tag}>`);
}
}
}
ts.forEachChild(node, inspect);
}
inspect(tree);
}
if (issues.length) {
console.error(`Элементы формы без доступного имени (${issues.length}):\n${issues.join("\n")}`);
process.exitCode = 1;
} else {
console.log("Доступные имена input/select/textarea: проверены.");
}