Revisionen: m-4 och m-6 — de två som bara går att stänga delvis

m-6 · Tillgänglighetstester med axe på verkstadsgränssnittets
grundkomponenter: knapp, textfält, panel och bild. Gränssnittet var
genomtänkt för handskar, buller och solljus men obevisat — ingenting
kontrollerade att en knapp har ett tillgängligt namn eller att ett
formulärfält har en etikett.

Att bilder bär alternativtext är i den här produkten inte bara ett
tillgänglighetskrav: bilden *är* bevisningen, och beskrivningen är det
som säger vad den visar.

Ungefär en tredjedel av WCAG går att kontrollera maskinellt. Testet
säger det rakt ut, och den manuella granskningen står kvar som öppen —
automatik hittar inte om ett gränssnitt är användbart, bara om det är
felformat.

m-4 · Paritetstestet mellan orkesterkopiorna jämför nu innehåll i
stället för att kontrollera att strängar förekommer: grundreglerna ord
för ord, metodikkatalogens id i ordning, och modellvalet per uppgift.
Ett substrängtest hade inte fångat att en grundregel ändrats i ena
kopian.

Den riktiga åtgärden är att avveckla Supabase-vägen. Det är ett
driftbeslut, inte en kodändring, och står kvar som öppet.

Revisionsdokumentet har fått en avdelning med utfall per fynd, så att en
omgranskning kan verifiera i stället för att lita på ordet. Två punkter
är ärligt öppna och ingen av dem är kod: personuppgiftsbiträdesavtalet
med konsekvensbedömning, och avvecklingen av den äldre orkestern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
This commit is contained in:
Claude
2026-08-05 13:48:06 +00:00
parent fbc034a280
commit 03fbea8fac
5 changed files with 221 additions and 4 deletions
+52 -3
View File
@@ -72,6 +72,7 @@
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
"axe-core": "^4.12.1",
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
@@ -83,7 +84,8 @@
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
"vitest": "^3.2.4"
"vitest": "^3.2.4",
"vitest-axe": "^0.1.0"
}
},
"node_modules/@adobe/css-tools": {
@@ -3914,6 +3916,16 @@
"postcss": "^8.1.0"
}
},
"node_modules/axe-core": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz",
"integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==",
"dev": true,
"license": "MPL-2.0",
"engines": {
"node": ">=4"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -4537,8 +4549,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -5677,6 +5688,13 @@
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -8007,6 +8025,37 @@
}
}
},
"node_modules/vitest-axe": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/vitest-axe/-/vitest-axe-0.1.0.tgz",
"integrity": "sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"aria-query": "^5.0.0",
"axe-core": "^4.4.2",
"chalk": "^5.0.1",
"dom-accessibility-api": "^0.5.14",
"lodash-es": "^4.17.21",
"redent": "^3.0.0"
},
"peerDependencies": {
"vitest": ">=0.16.0"
}
},
"node_modules/vitest-axe/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+3 -1
View File
@@ -78,6 +78,7 @@
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"autoprefixer": "^10.4.21",
"axe-core": "^4.12.1",
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
@@ -89,7 +90,8 @@
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
"vitest": "^3.2.4"
"vitest": "^3.2.4",
"vitest-axe": "^0.1.0"
},
"description": "Guidad Felsökning — evidensbaserat diagnossystem för fordonsverkstäder."
}
@@ -201,3 +201,44 @@ describe("arbetsorderskanning", () => {
}
});
});
// m-4: de två orkesterkopiorna hålls i synk av test, inte av delad kod.
// Den riktiga åtgärden är att avveckla Supabase-vägen; tills dess måste
// paritetstestet jämföra *innehåll*, inte bara att en sträng finns
// någonstans i filen. Ett substrängtest hade inte fångat att en
// grundregel ändrats i ena kopian.
describe("orkesterkopiorna är semantiskt identiska", () => {
const edge = readFileSync("../supabase/functions/felsokning-ai/index.ts", "utf8");
const tjanst = readFileSync("../services/ai-orkester/server.mjs", "utf8");
/** Plockar ut ett block mellan två markörer och normaliserar blanksteg. */
const block = (kalla: string, start: string, slut: string) => {
const i = kalla.indexOf(start);
if (i < 0) return null;
const j = kalla.indexOf(slut, i + start.length);
return kalla.slice(i, j < 0 ? undefined : j).replace(/\s+/g, " ").trim();
};
it("grundreglerna är ord för ord desamma", () => {
const a = block(edge, "Absoluta regler:", "`;");
const b = block(tjanst, "Absoluta regler:", "`;");
expect(a).not.toBeNull();
expect(a).toBe(b);
});
it("metodikkatalogen har samma id i samma ordning", () => {
const idn = (kalla: string) => {
const k = block(kalla, "const METODIK_KATALOG", "];");
return [...(k ?? "").matchAll(/\["([a-z_]+)",/g)].map((m) => m[1]);
};
expect(idn(edge)).toEqual(idn(tjanst));
expect(idn(edge).length).toBe(16);
});
it("modellvalet per uppgift är detsamma", () => {
const routing = (kalla: string) =>
[...kalla.matchAll(/^\s{2}(\w+): \{\n\s+modell: "([^"]+)"/gm)].map((m) => `${m[1]}=${m[2]}`);
expect(routing(edge).sort()).toEqual(routing(tjanst).sort());
expect(routing(tjanst).length).toBeGreaterThanOrEqual(6);
});
});
@@ -0,0 +1,72 @@
// Tillgänglighet (QUALITY-AUDIT m-6).
//
// Gränssnittet är designat för handskar, buller och starkt solljus —
// stora knappar, hög kontrast, få val per skärm. Det är genomtänkt, men
// det var obevisat: ingenting testade att en knapp har ett tillgängligt
// namn, att färg inte är enda informationsbäraren eller att formulärfält
// har etiketter.
//
// EN 301 549 gäller vid offentlig upphandling, och en verkstadskedja med
// kommunala kunder kommer att fråga. Viktigare i praktiken: en tekniker
// med nedsatt syn ska kunna använda verktyget.
//
// Testet är automatiskt och fångar därför bara en del — ungefär en
// tredjedel av WCAG-kriterierna går att kontrollera maskinellt. Det som
// återstår är manuell granskning, och det står i revisionen.
import { render } from "@testing-library/react";
import { axe } from "vitest-axe";
import { describe, expect, it } from "vitest";
import { Panel, StorKnapp, TextFalt } from "../ui";
import { Bild } from "../Bilagevisning";
async function utanFel(ui: React.ReactElement) {
const { container } = render(ui);
const resultat = await axe(container);
return resultat.violations.map((v) => `${v.id}: ${v.help}`);
}
describe("verkstadsgränssnittets grundkomponenter", () => {
it("knappen har ett tillgängligt namn och går att nå med tangentbord", async () => {
expect(await utanFel(<StorKnapp onClick={() => {}}>Avsluta felsökning</StorKnapp>)).toEqual([]);
});
it("textfältet har en etikett kopplad till inmatningen", async () => {
expect(
await utanFel(<TextFalt label="Felbeskrivning" varde="" satt={() => {}} />),
).toEqual([]);
});
it("panelen har en rubrikstruktur som går att navigera", async () => {
expect(
await utanFel(
<Panel rubrik="Utförda kontroller">
<p>Lufttryck kontrollerat</p>
</Panel>,
),
).toEqual([]);
});
it("bilder bär alternativtext — evidensen måste gå att förstå utan att se den", async () => {
// En bild utan alternativtext är i den här produkten inte bara ett
// tillgänglighetsfel: bilden *är* bevisningen, och beskrivningen är
// det som säger vad den visar.
expect(await utanFel(<Bild bilaga={{ dataUrl: "data:image/gif;base64,R0lGOD" }} alt="Höger framhjul" />)).toEqual(
[],
);
});
});
describe("färg är aldrig enda informationsbäraren", () => {
it("status anges med text eller symbol, inte bara med färgpunkt", () => {
// Tillförlitlighets- och statusnivåer visas som färgpunkter. En
// tekniker med färgblindhet ska ändå kunna skilja verifierat från
// hypotes — därför måste texten finnas.
const { container } = render(
<Panel rubrik="Tillförlitlighet">
<p>🔴 Hypotes kräver verifiering</p>
</Panel>,
);
expect(container.textContent).toMatch(/Hypotes/);
expect(container.textContent).toMatch(/verifiering/);
});
});