From 9d3a6ea7823e2d0dc3e4ecd54b1714c72ffb3c65 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 18:28:35 +0000 Subject: [PATCH 1/5] feat: --fast, where the changelog implies it already is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flag landed on the CLI and nowhere else. A build plugin and the GitHub Action are how most people run this in CI, and neither could reach the feature the changelog sells them — a plugin option that does not exist and an action input that is not there are not something a reader discovers before trying. IntegrationOptions gains `fast`, which is all five plugins: auditBuild spreads everything but its own two keys into the audit command, so naming the flag once is naming it everywhere. That pass-through was untested, and is the whole mechanism, so it is asserted now rather than rediscovered the next time a flag is added. The action gets a `fast` input threaded through EAA_FAST, built into the argument list the way allow-remote already is. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013WKUrBVgwFFGLbsfN46MBF --- action.yml | 14 +++++++++++++ docs/integrations.md | 5 +++-- src/integration/run.ts | 7 +++++++ tests/integrations/options.test.ts | 33 ++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/integrations/options.test.ts diff --git a/action.yml b/action.yml index 1955f7f..6da1dbc 100644 --- a/action.yml +++ b/action.yml @@ -83,6 +83,16 @@ inputs: required: false default: '' + fast: + description: >- + Skip the rules the browserless engine cannot decide, rather than running + them and discarding the answer. Those rules are still reported as not + evaluated, so the verdict does not move; what is given up is the list of + elements under each, which are the ones somebody checks by hand. No effect + on a browser run. + required: false + default: 'false' + version: description: Version of eaa-kit to run, as an npm dist-tag or version. required: false @@ -129,6 +139,7 @@ runs: EAA_BASE_URL: ${{ inputs.base-url }} EAA_VERSION: ${{ inputs.version }} EAA_CONCURRENCY: ${{ inputs.concurrency }} + EAA_FAST: ${{ inputs.fast }} EAA_BASELINE: ${{ inputs.baseline }} run: | set -o pipefail @@ -160,6 +171,9 @@ runs: if [ -n "$EAA_CONCURRENCY" ]; then args+=(--concurrency "$EAA_CONCURRENCY") fi + if [ "$EAA_FAST" = "true" ]; then + args+=(--fast) + fi if [ -n "$EAA_BASELINE" ]; then args+=(--baseline "$EAA_BASELINE") fi diff --git a/docs/integrations.md b/docs/integrations.md index 6dfdd67..c2d53c5 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -40,7 +40,7 @@ prints, and fails the build on violations at or above the threshold. | `enabled` | `true` | Set false to skip the audit entirely | | `baseline` | — | Accept the violations in this file; fail only on new ones | | `format`, `output` | — | Also write a report, as `--format` and `--output` do | -| `include`, `exclude`, `baseUrl`, `browser`, `concurrency` | | As for `audit` | +| `include`, `exclude`, `baseUrl`, `browser`, `fast`, `concurrency` | | As for `audit` | Failing the build by default is the point: an auditor that only ever prints is one nobody reads. `failBuild: false` exists for the week it takes to adopt the tool on a site that @@ -156,7 +156,7 @@ absence. | `failBuild: false` | report without failing — for the week it takes to adopt this on a site that already exists | | `enabled: false` | skip entirely, for turning it off per environment without unwiring it | | `directory` | audit somewhere other than the build's `outDir` | -| `browser`, `baseline`, `include`, `exclude`, `format`, `output` | as the CLI | +| `browser`, `fast`, `baseline`, `include`, `exclude`, `format`, `output` | as the CLI | `outDir` is read from the resolved Vite config, so a project that moved its output needs no second place to say so. @@ -276,6 +276,7 @@ watching is the wrong default for something whose job is to fail that build. | `sitemap` | — | Where the site lists its pages, if not `/sitemap.xml`; with `url` only | | `baseline` | — | Path to a baseline file; fail only on violations it does not list | | `concurrency` | from page and core count | Worker threads for the browserless engine; `1` for none | +| `fast` | `false` | Skip the rules the browserless engine cannot decide instead of running them and discarding the answer | | `version` | `latest` | Version of eaa-kit to run | ### Outputs diff --git a/src/integration/run.ts b/src/integration/run.ts index 20b0a3f..3f8817d 100644 --- a/src/integration/run.ts +++ b/src/integration/run.ts @@ -18,6 +18,13 @@ export interface IntegrationOptions { baseUrl?: string /** Audit in real Chromium. Needs the playwright peer. */ browser?: boolean + /** + * Skip the rules the browserless engine cannot decide rather than running + * them and discarding the answer. The verdict does not move — those rules are + * still reported as not evaluated — so what a build gives up is the list of + * elements a person would check by hand. No effect under `browser`. + */ + fast?: boolean concurrency?: number baseline?: string format?: OutputFormat diff --git a/tests/integrations/options.test.ts b/tests/integrations/options.test.ts new file mode 100644 index 0000000..964cc2b --- /dev/null +++ b/tests/integrations/options.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from 'vitest' +import { auditBuild } from '../../src/integration/run.ts' + +/** + * What the integrations hand over to the audit. + * + * Every option but `enabled` and `failBuild` is the audit command's own and is + * passed through untouched, which is what lets a flag reach all five plugins by + * being named in one interface. That is only true while nothing in between + * rebuilds the object key by key, so it is asserted rather than assumed: a flag + * the changelog says exists and the plugins cannot pass is the failure this + * catches. + */ + +const runAuditCommand = vi.hoisted(() => vi.fn(async () => ({ audits: [], exitCode: 0 }))) + +vi.mock('../../src/cli/audit.ts', () => ({ runAuditCommand })) + +describe('auditBuild', () => { + it('passes the audit options through, and keeps its own two', async () => { + await auditBuild( + 'dist', + { fast: true, failOn: 'critical', browser: false, enabled: true, failBuild: false }, + { info: () => {}, warn: () => {}, error: () => {} }, + ) + + expect(runAuditCommand).toHaveBeenCalledWith('dist', { + fast: true, + failOn: 'critical', + browser: false, + }) + }) +}) From 5d62880118fed932e1eb488f01f0df016c30a832 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 18:38:32 +0000 Subject: [PATCH 2/5] feat: audit defaults in eaa.config, with the flags still winning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config file has served the statement alone since 0.2, so a project that audits with the same six flags on every run has had nowhere to write them down but the build script that repeats them. An `audit` block is that list, said once. Everything in it is a default and every typed flag beats it: the file is what a project usually wants, and a flag is what somebody wants on this run. A config nobody could override would make a one-off --browser check impossible without editing a committed file. Two things this needed. --fail-on and --format lose their commander defaults, because commander writes a default into the parsed options whether or not the flag was typed, and that would have silently overruled the file on the two flags most worth putting in it; both still default in the command, to the values the help text names. And the block is read by a schema of its own rather than through parseConfig, so a project wanting audit defaults does not have to write a whole statement config to get them — s.object drops what it does not know, so one file still serves both readers. `baseline` reads the keys that mean the same thing to it. `output` is the report's path for one command and the baseline's for the other, so it is never carried across; format, failOn and baseline describe a verdict baseline does not reach. The merge is a function rather than four lines in the action, because the precedence is the whole feature and commander's actions are the one part of this CLI a test cannot call. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013WKUrBVgwFFGLbsfN46MBF --- docs/audit.md | 48 +++++++++ docs/statement.md | 4 + examples/eaa.config.json | 4 + src/cli/command.ts | 129 +++++++++++++++++++++++ src/cli/index.ts | 59 +++++++---- src/cli/init.ts | 6 ++ src/config/define.ts | 113 +++++++++++++++++++- src/config/load.ts | 46 +++++++- src/schema.ts | 21 ++++ tests/cli/config-defaults.test.ts | 167 ++++++++++++++++++++++++++++++ 10 files changed, 575 insertions(+), 22 deletions(-) create mode 100644 tests/cli/config-defaults.test.ts diff --git a/docs/audit.md b/docs/audit.md index 8d19daa..e49bdbf 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -185,10 +185,58 @@ chatter coming along. | `--concurrency ` | from page and core count | Pages to audit at once — threads without `--browser`, tabs with it; `1` turns both off | | `--fast` | off | Skip the rules the browserless engine cannot decide, rather than running them and discarding the answer | | `--baseline ` | — | Accept the violations recorded in this file; fail only on new ones | +| `--config ` | searched for | Take defaults from this config file rather than the one found by searching | Dot directories such as build caches are skipped by default. `--include` and `--exclude` replace the defaults rather than adding to them. +## Defaults from eaa.config + +A project that runs the same six flags on every invocation can write them down once. The +config file [the statement command already uses](statement.md#the-config-file) takes an +`audit` block, found by walking up from the working directory, and `baseline` reads it too: + +```jsonc +{ + "audit": { + "dir": "build", + "include": ["**/*.html"], + "failOn": "critical", + "browser": true, + "concurrency": 4 + } +} +``` + +```bash +eaa-kit audit # build/, in Chromium, failing on critical +eaa-kit audit --fail-on serious # the same run, at a threshold you asked for now +``` + +**A flag typed on the command line wins.** The file is what the project usually wants; a +flag is what somebody wants on this run, and a config that could not be overridden would +make a one-off `--browser` check impossible without editing a committed file. + +Everything in the block is optional, and the whole block is: `audit` works in a project +that has no config file, as it always has. A file written for the statement alone is not +required to grow one, and a file with nothing but an `audit` block is valid — the rest of +the schema is required by `statement`, which is the command that publishes a document. + +| Key | Same as | +| --- | --- | +| `dir` | the positional argument, which wins over it | +| `include`, `exclude`, `baseUrl`, `url`, `sitemap`, `maxPages`, `maxDepth` | the flags of those names | +| `allowRemote`, `ignoreRobots` | `--allow-remote`, `--ignore-robots` | +| `failOn`, `format`, `output`, `baseline` | `--fail-on`, `--format`, `--output`, `--baseline` | +| `browser`, `fast`, `concurrency` | the engine flags | +| `perPage`, `manual`, `coverage` | the console report's three extra sections | +| `build` | `false` is `--no-build` | + +`baseline` reads the keys that mean the same thing to it — the page selection and the +engine — and not the ones that do not. `output` is where the report goes for one command +and where the baseline goes for the other, so it is never carried across; `format`, +`failOn` and `baseline` describe a verdict `baseline` does not reach. + ### `--fail-on ` `minor`, `moderate`, `serious` (default) or `critical`. Any violation at or above the diff --git a/docs/statement.md b/docs/statement.md index 703f375..5b3074d 100644 --- a/docs/statement.md +++ b/docs/statement.md @@ -66,6 +66,10 @@ generator emitting an inaccessible statement has failed at the one job it has. directory. TypeScript configs are read directly — Node strips the types, so there is no build step and no loader dependency. +The same file can carry an [`audit` block](audit.md#defaults-from-eaaconfig) of defaults +for `eaa-kit audit`. Nothing in it reaches the statement, and nothing here reaches an +audit; they share a file, not a meaning. + ```ts import { defineConfig } from 'eaa-kit' diff --git a/examples/eaa.config.json b/examples/eaa.config.json index f99b40d..309681f 100644 --- a/examples/eaa.config.json +++ b/examples/eaa.config.json @@ -28,5 +28,9 @@ }, "enforcement": { "country": "AT" + }, + "audit": { + "include": ["**/*.html"], + "failOn": "serious" } } diff --git a/src/cli/command.ts b/src/cli/command.ts index 2af0be7..5a1cadc 100644 --- a/src/cli/command.ts +++ b/src/cli/command.ts @@ -3,6 +3,9 @@ import path from 'node:path' import pc from 'picocolors' import type { CollectedPage } from '../audit/collect.ts' import type { PageAudit } from '../audit/runners/jsdom.ts' +import type { AuditConfig } from '../config/define.ts' +import type { AuditCommandOptions } from './audit.ts' +import type { BaselineCommandOptions } from './baseline.ts' /** * What every command does around the audit itself: say what is happening, run @@ -119,3 +122,129 @@ export async function emitDocument( await mkdir(path.dirname(target), { recursive: true }) await writeFile(target, body, 'utf8') } + +/** + * Audit defaults from the project's config file. + * + * `audit` and `baseline` are run from a build script over and over with the + * same six flags, and the flags are the only place to say them: the config file + * has existed since 0.2 and served the statement alone. An `audit` block there + * is that list written once. + * + * Everything it returns is a default. The flags are merged over it by the + * caller, because the file is the project's usual answer and a flag is somebody + * asking for something else on this run. + * + * No config file at all is not an error, unlike for `statement`: this command + * has always run against projects that have never heard of one. A file that + * exists and cannot be read is exit 2 — it was written to be used, and running + * on different settings than it names would be worse than stopping. + */ +export async function auditDefaults( + options: { + cwd?: string + /** Explicit path, skipping the search. */ + config?: string + } = {}, +): Promise { + // Imported here so that a run with no config file, and `--help`, never load + // the module that reads one. + const { loadAuditConfig } = await import('../config/load.ts') + + const loaded = await loadAuditConfig({ + ...(options.cwd ? { cwd: options.cwd } : {}), + ...(options.config ? { path: options.config } : {}), + }) + + if (!loaded?.audit) return {} + note(`Defaults from ${path.basename(loaded.path)}`) + return loaded.audit +} + +/** + * What the audit command was asked to do, from the config file and the flags. + * + * A function rather than four lines in the action, because the precedence is + * the whole feature and the actions are the one part of this CLI a test cannot + * call: commander owns them. + */ +export type AuditFlags = Omit & { + /** commander's form of `--no-build`: true unless somebody typed the flag. */ + build?: boolean + /** Where to read defaults from. Consumed before this point. */ + config?: string +} + +export function auditInvocation( + dir: string | undefined, + defaults: AuditConfig, + flags: AuditFlags, +): { dir: string | undefined; options: AuditCommandOptions } { + const { build, config: _config, ...typed } = flags + const { dir: configDir, build: configBuild, ...fromConfig } = defaults + + return { + // The positional argument wins, and neither one given still means "work it + // out from the project", as it always has. + dir: dir ?? configDir, + options: { + ...fromConfig, + ...typed, + // `--no-build` reaches commander as build: true when nobody typed it, so + // it cannot be merged like the rest. Either source asking for no build is + // asking for no build. + ...(build === false || configBuild === false ? { noBuild: true } : {}), + }, + } +} + +export type BaselineFlags = Omit & { config?: string } + +/** + * The same, for `baseline`, which reads the defaults that mean the same thing + * to it. + * + * Deliberately a subset. `output` names where the report goes for one command + * and where the baseline goes for the other, so carrying it across would write + * a baseline over the path somebody set aside for a report; `format`, `failOn` + * and `baseline` all describe a verdict this command does not reach. + */ +export function baselineInvocation( + dir: string | undefined, + defaults: AuditConfig, + flags: BaselineFlags, +): { dir: string; options: BaselineCommandOptions } { + const { config: _config, ...typed } = flags + + return { + // This command has no auto-detection, so something has to be named: the + // argument, then the config file, then the directory most builds write to. + dir: dir ?? defaults.dir ?? './dist', + options: { ...baselineDefaults(defaults), ...typed }, + } +} + +function baselineDefaults(config: AuditConfig) { + return pick(config, [ + 'include', + 'exclude', + 'baseUrl', + 'url', + 'allowRemote', + 'ignoreRobots', + 'sitemap', + 'maxPages', + 'maxDepth', + 'browser', + 'concurrency', + ]) +} + +/** Copies the keys that are actually set, so nothing spreads an undefined over a real value. */ +function pick(source: T, keys: readonly K[]): Pick { + const out: Partial> = {} + for (const key of keys) { + if (source[key] !== undefined) out[key] = source[key] + } + return out as Pick +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 079894f..f12d3a9 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,16 +2,25 @@ import { enableCompileCache } from 'node:module' import { Command, InvalidArgumentError } from 'commander' import { DEFAULT_BASELINE_FILE } from '../audit/baseline.ts' -import { DEFAULT_FAIL_ON, IMPACT_LEVELS, type ImpactLevel } from '../audit/impact.ts' +import { DEFAULT_FAIL_ON, IMPACT_LEVELS } from '../audit/impact.ts' import { COUNTRIES, - type Country, + ConfigError, STATEMENT_LOCALES, type StatementLocale, } from '../config/define.ts' import { TOOL_VERSION } from '../version.ts' -import { type AuditCommandOptions, OUTPUT_FORMATS, runAuditCommand } from './audit.ts' -import { type BaselineCommandOptions, runBaselineCommand } from './baseline.ts' +import { OUTPUT_FORMATS, runAuditCommand } from './audit.ts' +import { runBaselineCommand } from './baseline.ts' +import { + type AuditFlags, + auditDefaults, + auditInvocation, + type BaselineFlags, + baselineInvocation, + fail, + note, +} from './command.ts' import { DIFF_FORMATS, type DiffCommandOptions, runDiffCommand } from './diff.ts' import { runStatementCommand, @@ -55,8 +64,6 @@ useCompileCache() * they occur: `--no-build`, which commander reports as `build`, and `--lang`, * which the statement command calls `locale`. */ -type AuditFlags = Omit & { build: boolean } -type BaselineFlags = Omit type DiffFlags = Omit type StatementFlags = Omit & { lang?: StatementLocale } @@ -137,17 +144,20 @@ program .option('--sitemap ', 'where the site lists its pages, if not /sitemap.xml') .option('--max-pages ', 'stop the crawl after this many pages', parsePositive) .option('--max-depth ', 'how far from the entry URL to follow links', parseDepth) + // Neither of these carries a commander default any more. Commander writes a + // default into the parsed options whether or not the flag was typed, and the + // flags are merged over the config file's `audit` block — so a default here + // would silently overrule what a project wrote down. Both still default in + // the command itself, to the same values the help text names. .option( '--fail-on ', - `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join('|')})`, + `exit 1 on violations at or above this impact (${IMPACT_LEVELS.join('|')}; default: ${DEFAULT_FAIL_ON})`, parseImpact, - DEFAULT_FAIL_ON, ) .option( '--format ', - `output format (${OUTPUT_FORMATS.join('|')})`, + `output format (${OUTPUT_FORMATS.join('|')}; default: console)`, parseFormat, - 'console', ) .option('--output ', 'write the report to a file instead of stdout') .option('--browser', 'audit in real Chromium, covering the rules jsdom cannot evaluate') @@ -161,19 +171,18 @@ program parseConcurrency, ) .option('--baseline ', 'accept the violations recorded in this file; fail only on new ones') + .option('--config ', 'take defaults from this config file, otherwise it is searched for') .action(async (dir: string | undefined, flags: AuditFlags) => { - const { build, ...options } = flags - const { exitCode } = await runAuditCommand(dir, { - ...options, - ...(build === false ? { noBuild: true } : {}), - }) + const defaults = await auditDefaults({ ...(flags.config ? { config: flags.config } : {}) }) + const invocation = auditInvocation(dir, defaults, flags) + const { exitCode } = await runAuditCommand(invocation.dir, invocation.options) process.exitCode = exitCode }) program .command('baseline') .description('Record the violations a build already has, so later runs fail only on new ones') - .argument('[dir]', 'directory holding the built site', './dist') + .argument('[dir]', 'directory holding the built site (default: ./dist)') .option('--include ', 'glob patterns to audit, relative to dir') .option('--exclude ', 'glob patterns to skip') .option('--base-url ', 'audit pages under their real site URL') @@ -188,8 +197,11 @@ program .option('--expires-on ', 'ISO date after which the entries stop suppressing', parseDate) .option('--browser', 'audit in real Chromium instead of jsdom') .option('--concurrency ', 'pages to audit at once, or 1 for none', parseConcurrency) - .action(async (dir: string, flags: BaselineFlags) => { - const { exitCode } = await runBaselineCommand(dir, flags) + .option('--config ', 'take defaults from this config file, otherwise it is searched for') + .action(async (dir: string | undefined, flags: BaselineFlags) => { + const defaults = await auditDefaults({ ...(flags.config ? { config: flags.config } : {}) }) + const invocation = baselineInvocation(dir, defaults, flags) + const { exitCode } = await runBaselineCommand(invocation.dir, invocation.options) process.exitCode = exitCode }) @@ -252,6 +264,17 @@ program try { await program.parseAsync(process.argv) } catch (cause) { + // A config file that exists and cannot be read: the same report `statement` + // gives, since it is the same file and the same mistake. + if (cause instanceof ConfigError) { + fail(cause.message) + for (const issue of cause.issues) note(` ${issue}`) + process.exitCode = 2 + // Commander's own errors carry an exitCode; this one does not, and the + // branch below would print a stack trace for a typo in a config file. + process.exit(2) + } + // --help and --version land here too, with exitCode 0; everything else is a // usage error, which this CLI reports as 2. const error = cause as { exitCode?: number; message?: string } diff --git a/src/cli/init.ts b/src/cli/init.ts index af8d6df..1f63aae 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -2,6 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { createInterface } from 'node:readline/promises' import pc from 'picocolors' +import { DEFAULT_FAIL_ON } from '../audit/impact.ts' import { COUNTRIES, type Country } from '../config/define.ts' import { CONFIG_FILENAMES } from '../config/load.ts' import { exists } from '../fs.ts' @@ -167,6 +168,11 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise< knownIssues: [], }, enforcement: { country }, + // Defaults for `eaa-kit audit`, so a project says once what every + // invocation would otherwise repeat. This one restates the built-in + // threshold rather than changing anything: it is here to be found and + // edited, since a block nobody knows about is a feature nobody has. + audit: { failOn: DEFAULT_FAIL_ON }, } try { diff --git a/src/config/define.ts b/src/config/define.ts index 28b36cb..33625ed 100644 --- a/src/config/define.ts +++ b/src/config/define.ts @@ -1,3 +1,4 @@ +import { IMPACT_LEVELS } from '../audit/impact.ts' import * as s from '../schema.ts' /** Countries with their own supervisory body and statute text. */ @@ -55,6 +56,74 @@ const knownIssueSchema = s.union( 'expected a description, or an object with one', ) +/** + * Report formats the `audit` block accepts. + * + * Spelled out here rather than imported from `src/cli/audit.ts`, which pulls + * the console reporter in statically: this module is reachable from the + * package entry point and from every build integration, and none of them + * should pay for a reporter to read a config file. A test asserts the two + * lists agree, so a format cannot be added to one and not the other. + */ +export const AUDIT_FORMATS = ['console', 'json', 'sarif', 'html'] as const +export type AuditFormat = (typeof AUDIT_FORMATS)[number] + +/** + * Defaults for `eaa-kit audit` and `eaa-kit baseline`, so a project says once + * what every invocation would otherwise repeat. + * + * Every field is optional and every one is a default: a flag actually typed on + * the command line wins, because the file is the project's usual answer and the + * flag is somebody asking for something else right now. + * + * `baseline` reads the subset that means the same thing to it. `output`, + * `format`, `failOn` and `baseline` are audit-only on purpose — a baseline + * written to the report's path would overwrite the report, and a threshold for + * failing a run means nothing to a command that records what it finds. + */ +const auditSchema = s.object({ + /** Build directory. The positional argument wins over it. */ + dir: s.optional(s.string({ min: 1 })), + include: s.optional(s.array(s.string({ min: 1 }))), + exclude: s.optional(s.array(s.string({ min: 1 }))), + /** Audit pages under their real site URL instead of file://. */ + baseUrl: s.optional(s.url()), + /** Audit a running site instead of a directory. */ + url: s.optional(s.url()), + /** Crawl a host that is not loopback. Off unless a project says otherwise. */ + allowRemote: s.optional(s.boolean()), + ignoreRobots: s.optional(s.boolean()), + /** Where the site lists its pages, when that is not /sitemap.xml. */ + sitemap: s.optional(s.string({ min: 1 })), + maxPages: s.optional(s.integer({ min: 1 })), + /** 0 audits the entry page alone. */ + maxDepth: s.optional(s.integer({ min: 0 })), + /** Lowest impact that exits 1. */ + failOn: s.optional(s.enumeration(IMPACT_LEVELS)), + format: s.optional(s.enumeration(AUDIT_FORMATS)), + /** Write the report here instead of stdout. */ + output: s.optional(s.string({ min: 1 })), + /** Audit in real Chromium. Needs the playwright peer. */ + browser: s.optional(s.boolean()), + /** Skip the rules the browserless engine cannot decide. No effect with `browser`. */ + fast: s.optional(s.boolean()), + concurrency: s.optional(s.integer({ min: 1 })), + /** Path to a baseline; violations it accounts for do not fail the run. */ + baseline: s.optional(s.string({ min: 1 })), + /** List every page and its result under the issues. */ + perPage: s.optional(s.boolean()), + /** Print the manual check for each rule the engine could not evaluate. */ + manual: s.optional(s.boolean()), + /** List every WCAG 2.2 A/AA criterion and what the run reached on it. */ + coverage: s.optional(s.boolean()), + /** + * False is `--no-build`: never run the project's build or start its server to + * find something to audit. Written in the positive because that is the state + * being described, and because a config file has no flags to negate. + */ + build: s.optional(s.boolean()), +}) + export const configSchema = s.object({ site: s.object({ name: s.string({ min: 1 }), @@ -101,8 +170,20 @@ export const configSchema = s.object({ /** Drives which supervisory body and statute the template names. */ country: s.enumeration(COUNTRIES), }), + /** Defaults for the audit commands. Nothing here reaches the statement. */ + audit: s.optional(auditSchema), }) +/** + * The `audit` block on its own. + * + * A project that only wants audit defaults should not have to write a complete + * statement config to get them, and `s.object` drops the keys it does not know, + * so the same file satisfies both readers: `statement` demands the whole + * document, `audit` reads this and ignores the rest. + */ +const auditConfigSchema = s.object({ audit: s.optional(auditSchema) }) + /** * What an author writes in `eaa.config.ts`. * @@ -129,6 +210,7 @@ export interface EaaConfigInput { auditReason?: IssueReason } enforcement: { country: Country } + audit?: AuditConfig } /** One barrier, as written in a config file. */ @@ -141,6 +223,17 @@ export interface KnownIssueInput { } export type EaaConfig = s.Infer export type KnownIssue = EaaConfig['compliance']['knownIssues'][number] +/** + * The `audit` block, as written and as parsed — every field is optional. + * + * `undefined` is mapped out of the value types rather than left in them: + * `s.object` never writes a key it did not parse, so an absent field is an + * absent key, and the commands spread this over their own options where a + * present-but-undefined key would overwrite a real value. + */ +export type AuditConfig = { + [K in keyof s.Infer]?: Exclude[K], undefined> +} /** * Identity function that gives `eaa.config.ts` its types. Deliberately does not @@ -165,7 +258,25 @@ export class ConfigError extends Error { /** Validate an already-loaded config object. */ export function parseConfig(value: unknown, source = 'config'): EaaConfig { - const result = s.safeParse(configSchema, value) + return parse(configSchema, value, source) +} + +/** + * Read only the `audit` block, ignoring whatever else the file holds. + * + * Returns undefined where there is no block, which is the common case: most + * config files exist for the statement alone, and finding one is not a reason + * to change how an audit runs. + */ +export function parseAuditConfig(value: unknown, source = 'config'): AuditConfig | undefined { + // The cast drops `| undefined` from each field's type, which the parser has + // already dropped from the value: `s.object` writes a key only when it read + // one. See AuditConfig for why that distinction is worth keeping. + return parse(auditConfigSchema, value, source).audit as AuditConfig | undefined +} + +function parse(schema: s.Schema, value: unknown, source: string): T { + const result = s.safeParse(schema, value) if (result.success) return result.data const issues = result.error.issues.map((issue) => { diff --git a/src/config/load.ts b/src/config/load.ts index 097a9c0..4eacd2d 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -2,7 +2,13 @@ import { readFile } from 'node:fs/promises' import path from 'node:path' import { pathToFileURL } from 'node:url' import { isFile } from '../fs.ts' -import { ConfigError, type EaaConfig, parseConfig } from './define.ts' +import { + type AuditConfig, + ConfigError, + type EaaConfig, + parseAuditConfig, + parseConfig, +} from './define.ts' /** Checked in this order, first match wins. */ export const CONFIG_FILENAMES = [ @@ -50,8 +56,42 @@ export async function loadConfig(options: LoadConfigOptions = {}): Promise { + const cwd = path.resolve(options.cwd ?? process.cwd()) + const file = options.path ? path.resolve(cwd, options.path) : await findConfigFile(cwd) + + if (!file) return undefined + if (!(await isFile(file))) { + throw new ConfigError(`Config file not found: ${file}`) + } + + return { audit: parseAuditConfig(await readConfigFile(file), path.basename(file)), path: file } +} + +function readConfigFile(file: string): Promise { + return file.endsWith('.json') ? importJson(file) : importModule(file) } /** Walks up from `cwd`, so the CLI works from a subdirectory of the project. */ diff --git a/src/schema.ts b/src/schema.ts index 035c92e..9ba7d20 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -167,6 +167,27 @@ export function number(): Schema { } } +/** + * A whole number, with an optional floor. + * + * `number` would accept 2.5 pages and a concurrency of 0, both of which the + * CLI's own parsers refuse. A config file is the same instruction typed + * somewhere else, so it is held to the same rule. + */ +export function integer(options: { min?: number } = {}): Schema { + return { + read: (value, path, issues) => { + if (typeof value !== 'number' || !Number.isInteger(value)) { + return fail(issues, path, 'expected a whole number') + } + if (options.min !== undefined && value < options.min) { + return fail(issues, path, `must be ${options.min} or more`) + } + return value + }, + } +} + /** One of a fixed set. The message lists them, since that is the useful part. */ export function enumeration(values: T): Schema { return { diff --git a/tests/cli/config-defaults.test.ts b/tests/cli/config-defaults.test.ts new file mode 100644 index 0000000..c7b0acf --- /dev/null +++ b/tests/cli/config-defaults.test.ts @@ -0,0 +1,167 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { OUTPUT_FORMATS } from '../../src/cli/audit.ts' +import { auditInvocation, baselineInvocation } from '../../src/cli/command.ts' +import { AUDIT_FORMATS, ConfigError, parseAuditConfig } from '../../src/config/define.ts' +import { loadAuditConfig } from '../../src/config/load.ts' + +/** + * Defaults from the project's config file, and what the flags do to them. + * + * The precedence is the whole feature: a file says what a project usually + * wants, and a flag says what somebody wants on this run. Getting it backwards + * would make `--browser` unusable for a one-off check on a project whose config + * says otherwise, which is the shape of mistake nobody notices until they hit + * it. + */ + +const dirs: string[] = [] + +afterEach(async () => { + vi.restoreAllMocks() + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +async function project(config?: unknown, name = 'eaa.config.json'): Promise { + const dir = await mkdtemp(path.join(tmpdir(), 'eaa-kit-config-')) + dirs.push(dir) + if (config !== undefined) { + await writeFile(path.join(dir, name), JSON.stringify(config, null, 2)) + } + return dir +} + +describe('the audit block', () => { + it('is read without the statement config around it', async () => { + // Most of the config file is required, and none of it is required to say + // how an audit should run. A project that wants defaults and no statement + // writes the block alone. + const dir = await project({ audit: { failOn: 'critical', browser: true } }) + + await expect(loadAuditConfig({ cwd: dir })).resolves.toMatchObject({ + audit: { failOn: 'critical', browser: true }, + }) + }) + + it('is absent from a config file written for the statement alone', async () => { + const dir = await project({ site: { name: 'x', url: 'https://x.test', locale: 'de-AT' } }) + + await expect(loadAuditConfig({ cwd: dir })).resolves.toMatchObject({ audit: undefined }) + }) + + it('is not required to exist at all', async () => { + // `eaa-kit audit` predates the config file and still runs against projects + // that have never had one; finding nothing is not a failure. + const dir = await project() + + await expect(loadAuditConfig({ cwd: dir })).resolves.toBeUndefined() + }) + + it('refuses a path that names a file that is not there', async () => { + const dir = await project() + + await expect(loadAuditConfig({ cwd: dir, path: 'nope.json' })).rejects.toThrow(ConfigError) + }) + + it('names the field a reader has to fix', () => { + expect(() => + parseAuditConfig({ audit: { failOn: 'catastrophic' } }, 'eaa.config.json'), + ).toThrow(ConfigError) + + try { + parseAuditConfig({ audit: { concurrency: 0 } }, 'eaa.config.json') + expect.unreachable() + } catch (cause) { + expect((cause as ConfigError).issues).toEqual(['audit.concurrency: must be 1 or more']) + } + }) + + it('holds a count to the rule the flag parser holds it to', () => { + try { + parseAuditConfig({ audit: { maxPages: 2.5 } }) + expect.unreachable() + } catch (cause) { + expect((cause as ConfigError).issues).toEqual(['audit.maxPages: expected a whole number']) + } + // Depth 0 is the entry page alone, as it is on the command line. + expect(parseAuditConfig({ audit: { maxDepth: 0 } })).toEqual({ maxDepth: 0 }) + }) + + it('accepts the formats the audit command accepts, and no others', () => { + expect([...AUDIT_FORMATS]).toEqual([...OUTPUT_FORMATS]) + }) +}) + +describe('auditInvocation', () => { + it('takes its defaults from the config and lets a typed flag win', () => { + const invocation = auditInvocation( + undefined, + { failOn: 'minor', browser: true, include: ['about/**'] }, + { failOn: 'critical', build: true }, + ) + + expect(invocation.options).toMatchObject({ + failOn: 'critical', + browser: true, + include: ['about/**'], + }) + }) + + it('prefers the directory somebody typed over the one the file names', () => { + expect(auditInvocation('build', { dir: 'dist' }, { build: true }).dir).toBe('build') + expect(auditInvocation(undefined, { dir: 'dist' }, { build: true }).dir).toBe('dist') + // Neither: auto-detection, which is what an argument-less run has always + // done and what the config file must not quietly take away. + expect(auditInvocation(undefined, {}, { build: true }).dir).toBeUndefined() + }) + + it('takes no build from either side', () => { + // commander reports build: true for a flag nobody typed, so this one cannot + // be merged like the rest. + expect(auditInvocation(undefined, { build: false }, { build: true }).options.noBuild).toBe(true) + expect(auditInvocation(undefined, {}, { build: false }).options.noBuild).toBe(true) + expect( + auditInvocation(undefined, { build: true }, { build: true }).options.noBuild, + ).toBeUndefined() + }) + + it('does not pass its own two keys on to the audit', () => { + const { options } = auditInvocation('dist', { dir: 'dist' }, { build: true, config: 'x.json' }) + + expect(options).not.toHaveProperty('config') + expect(options).not.toHaveProperty('dir') + expect(options).not.toHaveProperty('build') + }) +}) + +describe('baselineInvocation', () => { + it('reads only the defaults that mean the same thing to it', () => { + const { options } = baselineInvocation( + 'dist', + { + include: ['about/**'], + browser: true, + concurrency: 2, + // Audit-only. `output` is the report's path here and the baseline's + // path there, so carrying it across would write the baseline over the + // report somebody set aside. + output: 'report.json', + format: 'json', + failOn: 'critical', + baseline: 'eaa-baseline.json', + fast: true, + }, + {}, + ) + + expect(options).toEqual({ include: ['about/**'], browser: true, concurrency: 2 }) + }) + + it('falls back through the argument, the file, and ./dist', () => { + expect(baselineInvocation('build', { dir: 'out' }, {}).dir).toBe('build') + expect(baselineInvocation(undefined, { dir: 'out' }, {}).dir).toBe('out') + expect(baselineInvocation(undefined, {}, {}).dir).toBe('./dist') + }) +}) From 25b269feab006ac2e05b4aef5918f441b3b9fe38 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 18:47:04 +0000 Subject: [PATCH 3/5] feat: statements for Spain, France, Italy and the Netherlands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three countries was the DACH region and the tool is named after an EU directive. These four are the largest markets it applies in, and each one gets a document under its own law rather than a translation of the Austrian one — the rule Switzerland already set here. That means four more languages. site.locale now picks any of the six rather than testing for a German prefix, and each has a region for its dates, because a bare language tag leaves the format to whatever ICU picks and these documents have always been dated the European way. The matrix is sparse on purpose: a country has the language its law is administered in, and English. Asking for a combination nobody wrote is an error naming what that country does have, not a fall back to another language — publishing somebody's legal document in a language their readers may not have, quietly, is worse than stopping. Where a national regime prescribes a declaration of its own, the template says so rather than letting a generated file look like it discharges the obligation: France's RGAA declaration and multi-year plan under art. 47 of loi 2005-102, Italy's dichiarazione filed on AgID's model by 23 September each year, and Spain's RD 1112/2018 statement for the public sector. Where supervision is genuinely split — Spain between the state and the autonomous communities, the Netherlands between six authorities — the template says that too rather than naming one body and sounding certain. Statutes and authorities, verified against: ES Ley 11/2023, de 8 de mayo (BOE-A-2023-11022) FR ordonnance n° 2023-859; art. 47 loi n° 2005-102; Arcom IT d.lgs. 82/2022 art. 21, amending l. 4/2004; AgID NL Implementatiewet toegankelijkheidsvoorschriften producten en diensten, in force 28 June 2025; ACM for services, RDI for products Every template ships with a whole-document snapshot, and a test now asserts the snapshot matrix covers the template directory, so a country added without one cannot ship prose no reader has seen. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013WKUrBVgwFFGLbsfN46MBF --- README.md | 7 +- docs/statement.md | 49 ++++++- src/cli/init.ts | 4 + src/config/define.ts | 13 +- src/statement/render.ts | 64 +++++++-- src/statement/templates/es.en.md | 125 +++++++++++++++++ src/statement/templates/es.es.md | 127 +++++++++++++++++ src/statement/templates/fr.en.md | 128 +++++++++++++++++ src/statement/templates/fr.fr.md | 131 ++++++++++++++++++ src/statement/templates/it.en.md | 127 +++++++++++++++++ src/statement/templates/it.it.md | 130 +++++++++++++++++ src/statement/templates/nl.en.md | 125 +++++++++++++++++ src/statement/templates/nl.nl.md | 127 +++++++++++++++++ tests/cli/statement.test.ts | 5 +- tests/config/load.test.ts | 2 +- .../__snapshots__/statement.es.en.html | 83 +++++++++++ .../__snapshots__/statement.es.en.md | 84 +++++++++++ .../__snapshots__/statement.es.es.html | 83 +++++++++++ .../__snapshots__/statement.es.es.md | 84 +++++++++++ .../__snapshots__/statement.fr.en.html | 89 ++++++++++++ .../__snapshots__/statement.fr.en.md | 87 ++++++++++++ .../__snapshots__/statement.fr.fr.html | 89 ++++++++++++ .../__snapshots__/statement.fr.fr.md | 89 ++++++++++++ .../__snapshots__/statement.it.en.html | 84 +++++++++++ .../__snapshots__/statement.it.en.md | 86 ++++++++++++ .../__snapshots__/statement.it.it.html | 84 +++++++++++ .../__snapshots__/statement.it.it.md | 87 ++++++++++++ .../__snapshots__/statement.nl.en.html | 83 +++++++++++ .../__snapshots__/statement.nl.en.md | 84 +++++++++++ .../__snapshots__/statement.nl.nl.html | 83 +++++++++++ .../__snapshots__/statement.nl.nl.md | 84 +++++++++++ tests/statement/render.test.ts | 25 +++- tests/statement/snapshot.test.ts | 33 ++++- 33 files changed, 2555 insertions(+), 30 deletions(-) create mode 100644 src/statement/templates/es.en.md create mode 100644 src/statement/templates/es.es.md create mode 100644 src/statement/templates/fr.en.md create mode 100644 src/statement/templates/fr.fr.md create mode 100644 src/statement/templates/it.en.md create mode 100644 src/statement/templates/it.it.md create mode 100644 src/statement/templates/nl.en.md create mode 100644 src/statement/templates/nl.nl.md create mode 100644 tests/statement/__snapshots__/statement.es.en.html create mode 100644 tests/statement/__snapshots__/statement.es.en.md create mode 100644 tests/statement/__snapshots__/statement.es.es.html create mode 100644 tests/statement/__snapshots__/statement.es.es.md create mode 100644 tests/statement/__snapshots__/statement.fr.en.html create mode 100644 tests/statement/__snapshots__/statement.fr.en.md create mode 100644 tests/statement/__snapshots__/statement.fr.fr.html create mode 100644 tests/statement/__snapshots__/statement.fr.fr.md create mode 100644 tests/statement/__snapshots__/statement.it.en.html create mode 100644 tests/statement/__snapshots__/statement.it.en.md create mode 100644 tests/statement/__snapshots__/statement.it.it.html create mode 100644 tests/statement/__snapshots__/statement.it.it.md create mode 100644 tests/statement/__snapshots__/statement.nl.en.html create mode 100644 tests/statement/__snapshots__/statement.nl.en.md create mode 100644 tests/statement/__snapshots__/statement.nl.nl.html create mode 100644 tests/statement/__snapshots__/statement.nl.nl.md diff --git a/README.md b/README.md index d6bfe53..4e270dd 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,10 @@ export, Nuxt, SvelteKit, anything behind a CMS — are audited running instead: eaa-kit audit --url http://localhost:3000 ``` -**Writes the statement.** A Barrierefreiheitserklärung from one config file, in German or -English, as Markdown or HTML, naming the statute and supervisory body of Austria, -Switzerland or Germany — and optionally listing the barriers a real audit found. +**Writes the statement.** A Barrierefreiheitserklärung from one config file, as Markdown or +HTML, naming the statute and supervisory body of Austria, Germany, Switzerland, Spain, +France, Italy or the Netherlands — in that country's language or in English — and +optionally listing the barriers a real audit found. ```bash eaa-kit statement --output src/content/a11y.md diff --git a/docs/statement.md b/docs/statement.md index 5b3074d..ea1f5e7 100644 --- a/docs/statement.md +++ b/docs/statement.md @@ -1,8 +1,8 @@ # The statement command Generates an accessibility statement (Barrierefreiheitserklärung) from a config file and, -optionally, from an audit report — in German or English, as Markdown or HTML, with the -statute and supervisory body of the country you name. +optionally, from an audit report — in the language of the country you name or in English, +as Markdown or HTML, with that country's statute and supervisory body. ```bash eaa-kit statement # to stdout @@ -15,8 +15,8 @@ eaa-kit statement --audit a11y.json # list what the audit found | Flag | Default | Meaning | | --- | --- | --- | | `--config ` | searched for | Path to the config file | -| `--lang ` | from `site.locale` | `de` or `en` | -| `--country ` | from `enforcement.country` | `AT`, `CH` or `DE` | +| `--lang ` | from `site.locale` | `de`, `en`, `es`, `fr`, `it` or `nl` — see the table below for which countries have which | +| `--country ` | from `enforcement.country` | `AT`, `CH`, `DE`, `ES`, `FR`, `IT` or `NL` | | `--audit ` | — | A report from `eaa-kit audit --format json`; its violations are listed as non-accessible content | | `--format ` | from `--output` | `markdown` or `html` | | `--output ` | stdout | Write to a file; parent directories are created | @@ -104,7 +104,7 @@ export default defineConfig({ ], }, enforcement: { - country: 'AT', // AT, CH or DE + country: 'AT', // AT, CH, DE, ES, FR, IT or NL }, }) ``` @@ -144,6 +144,45 @@ The `AT` template names the Barrierefreiheitsgesetz and the Sozialministeriumser `DE` template names the Barrierefreiheitsstärkungsgesetz and the Marktüberwachungsstelle der Länder (MLBF). Both statutes transpose Directive (EU) 2019/882. +## The countries, and what each template names + +Each country's statement is a document under its own law, written in the language that law +is administered in, plus English. It is not a translation of another country's: `--lang fr` +is the French statement, and there is no French rendering of the Austrian one. + +| Country | Languages | Statute | Enforcement named | +| --- | --- | --- | --- | +| `AT` | `de`, `en` | Barrierefreiheitsgesetz (BaFG) | Sozialministeriumservice | +| `DE` | `de`, `en` | Barrierefreiheitsstärkungsgesetz (BFSG) | Marktüberwachungsstelle der Länder (MLBF) | +| `CH` | `de`, `en` | Behindertengleichstellungsgesetz (BehiG) — [not an EAA transposition](#switzerland-is-not-the-eu) | the courts; there is no supervisory body | +| `ES` | `es`, `en` | [Ley 11/2023, de 8 de mayo](https://www.boe.es/buscar/act.php?id=BOE-A-2023-11022) | the competent market surveillance authority, usually your autonomous community's consumer body | +| `FR` | `fr`, `en` | Ordonnance n° 2023-859 du 6 septembre 2023, and art. 47 of loi n° 2005-102 | the Défenseur des droits, and [Arcom](https://www.arcom.fr) | +| `IT` | `it`, `en` | D.lgs. 27 maggio 2022, n. 82, amending the legge Stanca (l. 4/2004) | [AgID](https://www.agid.gov.it) | +| `NL` | `nl`, `en` | Implementatiewet toegankelijkheidsvoorschriften producten en diensten | [ACM](https://www.acm.nl) for services, RDI for products | + +Asking for a language a country does not have is an error naming the ones it does, rather +than a fall back to another language: a legal document silently published in a language +your readers may not have is worse than a run that stops. + +### Three regimes ask for more than this document + +Where a country's own regime prescribes a declaration with a form of its own, the template +says so rather than letting a generated file look like it discharges the obligation. + +- **France.** An online public service, or a company whose average French turnover over the + last three closed financial years exceeds €250 million, falls under art. 47 of loi + n° 2005-102: the declaration must follow the RGAA model, rest on an RGAA audit, and come + with a multi-year accessibility plan. Arcom supervises this, with fines of up to €50,000 + per non-compliant service and €25,000 for the documentary failings. +- **Italy.** Private providers of public-facing services with average turnover above + €500 million over the last three years file a *dichiarazione di accessibilità* on AgID's + own model, by 23 September each year. +- **Spain.** Public-sector websites and apps are governed by Real Decreto 1112/2018, whose + accessibility statement has content of its own. + +In each case the generated statement is the document you publish beside the service, not +the one you file. + ## Switzerland is not the EU `CH` is a different document, not a translation of the other two, because Swiss law is diff --git a/src/cli/init.ts b/src/cli/init.ts index 1f63aae..ed00e6c 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -57,6 +57,10 @@ const COUNTRY_LOCALES: Record = { AT: 'de-AT', DE: 'de-DE', CH: 'de-CH', + ES: 'es-ES', + FR: 'fr-FR', + IT: 'it-IT', + NL: 'nl-NL', } /** diff --git a/src/config/define.ts b/src/config/define.ts index 33625ed..d5a6588 100644 --- a/src/config/define.ts +++ b/src/config/define.ts @@ -2,11 +2,18 @@ import { IMPACT_LEVELS } from '../audit/impact.ts' import * as s from '../schema.ts' /** Countries with their own supervisory body and statute text. */ -export const COUNTRIES = ['AT', 'DE', 'CH'] as const +export const COUNTRIES = ['AT', 'DE', 'CH', 'ES', 'FR', 'IT', 'NL'] as const export type Country = (typeof COUNTRIES)[number] -/** Languages a statement can be rendered in. */ -export const STATEMENT_LOCALES = ['de', 'en'] as const +/** + * Languages a statement can be rendered in. + * + * Not every country has every one: a statement is a document under a particular + * legal regime, not a translation of a document under another, so each country + * has the language it is published in and English. `renderStatement` says which + * ones a country has when asked for one it does not. + */ +export const STATEMENT_LOCALES = ['de', 'en', 'es', 'fr', 'it', 'nl'] as const export type StatementLocale = (typeof STATEMENT_LOCALES)[number] /** diff --git a/src/statement/render.ts b/src/statement/render.ts index 1a8c163..ceab416 100644 --- a/src/statement/render.ts +++ b/src/statement/render.ts @@ -1,7 +1,13 @@ import { readdir, readFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' -import type { Country, EaaConfig, KnownIssue, StatementLocale } from '../config/define.ts' +import { + type Country, + type EaaConfig, + type KnownIssue, + STATEMENT_LOCALES, + type StatementLocale, +} from '../config/define.ts' import { isDirectory } from '../fs.ts' import { standardsReference } from '../text.ts' import { StatementError } from './error.ts' @@ -51,16 +57,23 @@ export async function renderStatement( const locale = options.locale ?? defaultLocale(config) const template = `${country.toLowerCase()}.${locale}` - const source = await loadTemplate(template) + const source = await loadTemplate(country, locale) const markdown = tidy(renderTemplate(source, buildScope(config, locale, options.audit))) const html = toHtmlDocument(markdown, { lang: locale, fallbackTitle: config.site.name }) return { markdown, html, locale, country, template } } -/** A German-language site gets a German statement unless told otherwise. */ +/** + * A site gets its statement in its own language where there is one for it. + * + * From `site.locale`, which is a BCP 47 tag: `de-AT` and `de` both mean the + * German document. English is the fallback because every country has an English + * template, being the language a statement is most often also published in. + */ function defaultLocale(config: EaaConfig): StatementLocale { - return config.site.locale.toLowerCase().startsWith('de') ? 'de' : 'en' + const language = config.site.locale.toLowerCase().split('-')[0] + return STATEMENT_LOCALES.find((candidate) => candidate === language) ?? 'en' } /** @@ -183,6 +196,23 @@ function reasonScope(reason: KnownIssue['reason']): TemplateScope { } } +/** + * Where each statement language formats its dates. + * + * A region is named for every one, because a bare language tag leaves the + * format to whatever ICU picks: `de` is de-DE, and this tool's German documents + * have always been dated the Austrian way. `en-GB` for the same reason — + * 20 August 2026, not August 20, 2026, in a European legal document. + */ +const DATE_LOCALES: Record = { + de: 'de-AT', + en: 'en-GB', + es: 'es-ES', + fr: 'fr-FR', + it: 'it-IT', + nl: 'nl-NL', +} + /** * 2026-08-20 becomes 20. August 2026 or 20 August 2026. * @@ -196,7 +226,7 @@ function formatDate(iso: string, locale: StatementLocale): string { const date = new Date(`${iso}T00:00:00Z`) if (Number.isNaN(date.getTime())) return iso - return new Intl.DateTimeFormat(locale === 'de' ? 'de-AT' : 'en-GB', { + return new Intl.DateTimeFormat(DATE_LOCALES[locale], { day: 'numeric', month: 'long', year: 'numeric', @@ -218,20 +248,38 @@ function tidy(markdown: string): string { let templateDirectory: string | undefined -async function loadTemplate(name: string): Promise { +/** + * The document for a country in a language, if there is one. + * + * The matrix is deliberately sparse: a country's statement is written under its + * own law and published in the language the law is administered in, plus + * English. Asking for a combination nobody wrote is an error naming the + * languages that country does have — not a fall back to another language, which + * would hand somebody a document in a language their readers may not have and + * do it quietly. + */ +async function loadTemplate(country: Country, locale: StatementLocale): Promise { templateDirectory ??= await findTemplateDirectory() const directory = templateDirectory + const name = `${country.toLowerCase()}.${locale}` const file = path.join(directory, `${name}.md`) try { return await readFile(file, 'utf8') } catch { - const available = (await readdir(directory)) + const templates = (await readdir(directory)) .filter((entry) => entry.endsWith('.md')) .map((entry) => entry.replace(/\.md$/, '')) .sort() + const prefix = `${country.toLowerCase()}.` + const forCountry = templates + .filter((entry) => entry.startsWith(prefix)) + .map((entry) => entry.slice(prefix.length)) + throw new StatementError( - `No statement template for ${name}. Available: ${available.join(', ')}`, + forCountry.length > 0 + ? `No ${country} statement in ${locale}. ${country} has: ${forCountry.join(', ')}` + : `No statement template for ${name}. Available: ${templates.join(', ')}`, ) } } diff --git a/src/statement/templates/es.en.md b/src/statement/templates/es.en.md new file mode 100644 index 0000000..04f9028 --- /dev/null +++ b/src/statement/templates/es.en.md @@ -0,0 +1,125 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with Ley 11/2023 of 8 May 2023, which transposes Directive (EU) 2019/882 (the +European Accessibility Act) into Spanish law. Its obligations for products and services +apply from 28 June 2025. + +This accessibility statement applies to {{ site.url }}. + +## Compliance status + +{{#if compliance.isCompliant}} +This website is fully compliant with {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +This website is partially compliant with {{ compliance.standard }}. The content listed in +the following section is not accessible, for the reasons given. +{{/if}} +{{#if compliance.isNonCompliant}} +This website is not compliant with {{ compliance.standard }}. The content listed in the +following section is not accessible, for the reasons given. +{{/if}} + +## Non-accessible content + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requirement affected: {{ standards }} +{{/if}} +{{#if pageList}} + Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reason: disproportionate burden. +{{/if}} +{{#if isOutOfScope}} + Reason: the content falls outside the scope of Ley 11/2023. +{{/if}} +{{#if isFixPlanned}} + Reason: the barrier is known and is being addressed. +{{/if}} +{{#if remedyByFormatted}} + Expected to be resolved by: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No non-accessible content was known at the time of assessment. +{{/if}} + +## Preparation of this statement + +This statement was prepared on {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +It is based on a self-assessment carried out by {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +It is based on an assessment carried out by a third party. +{{/if}} + +{{#if audit.isSinglePage}} +The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website. +{{/if}} +{{#if audit.isMultiPage}} +The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }} +pages of this website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +One further rule check requires a manual decision. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} further rule checks require a manual decision. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +One rule check could not be decided by the tool that was used; it is not reported as met. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they +are not reported as met. +{{/if}} +{{#if hasAudit}} + +{{/if}} +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contact form: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Phone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Address: {{ provider.address }} +{{/if}} + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can complain to the competent market +surveillance authority. Ley 11/2023 divides that surveillance between the national +administration and the autonomous communities, so for a digital service the usual route is +the consumer authority of your autonomous community. + +Public-sector websites and mobile applications are additionally governed by Real Decreto +1112/2018, which requires an accessibility statement with content of its own. This document +is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/src/statement/templates/es.es.md b/src/statement/templates/es.es.md new file mode 100644 index 0000000..3e3bacf --- /dev/null +++ b/src/statement/templates/es.es.md @@ -0,0 +1,127 @@ +# Declaración de accesibilidad + +{{ provider.legalName }} se compromete a hacer accesible el sitio web {{ site.name }} +conforme a la Ley 11/2023, de 8 de mayo, que traspone al ordenamiento español la Directiva +(UE) 2019/882 (European Accessibility Act). Sus obligaciones sobre productos y servicios +son aplicables desde el 28 de junio de 2025. + +Esta declaración de accesibilidad se refiere a {{ site.url }}. + +## Situación de cumplimiento + +{{#if compliance.isCompliant}} +Este sitio web es plenamente conforme con {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Este sitio web es parcialmente conforme con {{ compliance.standard }}. Los contenidos que +se enumeran a continuación no son accesibles, por los motivos indicados. +{{/if}} +{{#if compliance.isNonCompliant}} +Este sitio web no es conforme con {{ compliance.standard }}. Los contenidos que se enumeran +a continuación no son accesibles, por los motivos indicados. +{{/if}} + +## Contenido no accesible + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requisito afectado: {{ standards }} +{{/if}} +{{#if pageList}} + Páginas afectadas: {{ pageList }}{{#if hasMorePages}} y {{ morePages }} más{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Motivo: carga desproporcionada. +{{/if}} +{{#if isOutOfScope}} + Motivo: el contenido queda fuera del ámbito de aplicación de la Ley 11/2023. +{{/if}} +{{#if isFixPlanned}} + Motivo: la barrera es conocida y está en vías de corrección. +{{/if}} +{{#if remedyByFormatted}} + Corrección prevista antes del: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detectado mediante análisis automático (axe-core, regla {{ ruleId }}); descríbalo con sus propias palabras. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +En el momento de la evaluación no constaba contenido no accesible. +{{/if}} + +## Preparación de la presente declaración + +Esta declaración se preparó el {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Se basa en una autoevaluación realizada por {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Se basa en una evaluación realizada por un tercero. +{{/if}} + +{{#if audit.isSinglePage}} +El análisis automático del {{ audit.checkedOnFormatted }} abarcó una página de este sitio +web. +{{/if}} +{{#if audit.isMultiPage}} +El análisis automático del {{ audit.checkedOnFormatted }} abarcó {{ audit.pages }} páginas +de este sitio web. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Otra comprobación de regla requiere una valoración manual. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Otras {{ audit.needsReview }} comprobaciones de reglas requieren una valoración manual. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +En una comprobación de regla la herramienta empleada no alcanzó un resultado; no se +presenta como cumplida. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +En {{ audit.notEvaluated }} comprobaciones de reglas la herramienta empleada no alcanzó un +resultado; no se presentan como cumplidas. +{{/if}} +{{#if hasAudit}} + +{{/if}} +La evaluación se apoya en parte en análisis automáticos. Las herramientas automáticas solo +detectan una parte de las barreras posibles; no sustituyen a una revisión manual ni a una +prueba con tecnologías de apoyo. + +## Observaciones y datos de contacto + +¿Ha encontrado una barrera, o necesita información en un formato accesible? Escríbanos: + +- Correo electrónico: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Formulario de contacto: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Teléfono: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Dirección: {{ provider.address }} +{{/if}} + +Procuramos responder a su comunicación con prontitud. + +## Procedimiento de aplicación + +Si nuestra respuesta no le resulta satisfactoria, puede presentar una reclamación ante la +autoridad de vigilancia del mercado competente. La Ley 11/2023 reparte esa vigilancia entre +las administraciones competentes del Estado y de las comunidades autónomas, por lo que la +vía habitual para un servicio digital es el organismo de consumo de su comunidad autónoma. + +Los sitios web y las aplicaciones móviles del sector público se rigen además por el Real +Decreto 1112/2018, que exige una declaración de accesibilidad de contenido propio. Este +documento no es esa declaración. + +--- + +Esta declaración se ha generado con eaa-kit y no constituye asesoramiento jurídico. +Revísela antes de publicarla y, en caso de duda, sométala a revisión jurídica. diff --git a/src/statement/templates/fr.en.md b/src/statement/templates/fr.en.md new file mode 100644 index 0000000..5dc9128 --- /dev/null +++ b/src/statement/templates/fr.en.md @@ -0,0 +1,128 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with Ordonnance n° 2023-859 of 6 September 2023, which transposes Directive (EU) +2019/882 (the European Accessibility Act) into French law, and with Article 47 of Loi +n° 2005-102 of 11 February 2005. + +This accessibility statement applies to {{ site.url }}. + +## Compliance status + +{{#if compliance.isCompliant}} +This website is fully compliant with {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +This website is partially compliant with {{ compliance.standard }}. The content listed in +the following section is not accessible, for the reasons given. +{{/if}} +{{#if compliance.isNonCompliant}} +This website is not compliant with {{ compliance.standard }}. The content listed in the +following section is not accessible, for the reasons given. +{{/if}} + +## Non-accessible content + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requirement affected: {{ standards }} +{{/if}} +{{#if pageList}} + Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reason: disproportionate burden. +{{/if}} +{{#if isOutOfScope}} + Reason: the content falls outside the scope of these obligations. +{{/if}} +{{#if isFixPlanned}} + Reason: the barrier is known and is being addressed. +{{/if}} +{{#if remedyByFormatted}} + Expected to be resolved by: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No non-accessible content was known at the time of assessment. +{{/if}} + +## Preparation of this statement + +This statement was prepared on {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +It is based on a self-assessment carried out by {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +It is based on an assessment carried out by a third party. +{{/if}} + +{{#if audit.isSinglePage}} +The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website. +{{/if}} +{{#if audit.isMultiPage}} +The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }} +pages of this website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +One further rule check requires a manual decision. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} further rule checks require a manual decision. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +One rule check could not be decided by the tool that was used; it is not reported as met. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they +are not reported as met. +{{/if}} +{{#if hasAudit}} + +{{/if}} +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contact form: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Phone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Address: {{ provider.address }} +{{/if}} + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can: + +- write to the Défenseur des droits, or contact its delegate in your département: + https://www.defenseurdesdroits.fr +- report the failing to Arcom, which supervises the digital accessibility obligations of + Article 47 of Loi n° 2005-102: https://www.arcom.fr + +If Article 47 applies to you — an online public service, or a company whose average +turnover in France over the last three closed financial years exceeds €250 million — your +accessibility statement must follow the RGAA model, rest on an RGAA audit, and be +accompanied by a multi-year accessibility plan. This document does not stand in for that. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/src/statement/templates/fr.fr.md b/src/statement/templates/fr.fr.md new file mode 100644 index 0000000..ff64d61 --- /dev/null +++ b/src/statement/templates/fr.fr.md @@ -0,0 +1,131 @@ +# Déclaration d'accessibilité + +{{ provider.legalName }} s'engage à rendre le site {{ site.name }} accessible, conformément +à l'ordonnance n° 2023-859 du 6 septembre 2023, qui transpose en droit français la +directive (UE) 2019/882 (European Accessibility Act), et à l'article 47 de la loi +n° 2005-102 du 11 février 2005. + +Cette déclaration d'accessibilité s'applique à {{ site.url }}. + +## État de conformité + +{{#if compliance.isCompliant}} +Ce site est totalement conforme à {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Ce site est partiellement conforme à {{ compliance.standard }}. Les contenus énumérés +ci-dessous ne sont pas accessibles, pour les motifs indiqués. +{{/if}} +{{#if compliance.isNonCompliant}} +Ce site n'est pas conforme à {{ compliance.standard }}. Les contenus énumérés ci-dessous ne +sont pas accessibles, pour les motifs indiqués. +{{/if}} + +## Contenus non accessibles + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Exigence concernée : {{ standards }} +{{/if}} +{{#if pageList}} + Pages concernées : {{ pageList }}{{#if hasMorePages}} et {{ morePages }} autres{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Motif : charge disproportionnée. +{{/if}} +{{#if isOutOfScope}} + Motif : le contenu n'entre pas dans le champ d'application de ces obligations. +{{/if}} +{{#if isFixPlanned}} + Motif : la barrière est connue et sa correction est engagée. +{{/if}} +{{#if remedyByFormatted}} + Correction prévue avant le : {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Détecté par un test automatisé (axe-core, règle {{ ruleId }}) ; à reformuler dans vos propres mots. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Aucun contenu non accessible n'était connu au moment de l'évaluation. +{{/if}} + +## Établissement de cette déclaration + +Cette déclaration a été établie le {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Elle repose sur une auto-évaluation réalisée par {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Elle repose sur une évaluation réalisée par un tiers. +{{/if}} + +{{#if audit.isSinglePage}} +Le test automatisé du {{ audit.checkedOnFormatted }} a porté sur une page de ce site. +{{/if}} +{{#if audit.isMultiPage}} +Le test automatisé du {{ audit.checkedOnFormatted }} a porté sur {{ audit.pages }} pages de +ce site. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Une autre vérification de règle demande une appréciation humaine. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} autres vérifications de règles demandent une appréciation humaine. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Une vérification de règle n'a pu être tranchée par l'outil utilisé ; elle n'est pas +présentée comme satisfaite. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} vérifications de règles n'ont pu être tranchées par l'outil +utilisé ; elles ne sont pas présentées comme satisfaites. +{{/if}} +{{#if hasAudit}} + +{{/if}} +L'évaluation repose en partie sur des tests automatisés. Les outils automatisés ne +détectent qu'une partie des barrières possibles ; ils ne remplacent ni un test manuel ni un +test avec des technologies d'assistance. + +## Retour d'information et contact + +Vous avez rencontré une barrière, ou vous avez besoin d'une information sous une forme +accessible ? Écrivez-nous : + +- Courriel : {{ provider.email }} +{{#if provider.feedbackUrl}} +- Formulaire de contact : {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Téléphone : {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Adresse : {{ provider.address }} +{{/if}} + +Nous nous efforçons de répondre à votre message dans les meilleurs délais. + +## Voies de recours + +Si votre demande reste sans réponse satisfaisante, vous pouvez : + +- écrire au Défenseur des droits, ou contacter le délégué du Défenseur des droits de votre + département : https://www.defenseurdesdroits.fr +- signaler le manquement à l'Arcom, qui contrôle les obligations d'accessibilité numérique + de l'article 47 de la loi n° 2005-102 : https://www.arcom.fr + +Si vous relevez de cet article 47 — service public en ligne, ou entreprise dont le chiffre +d'affaires moyen réalisé en France sur les trois derniers exercices clos dépasse +250 millions d'euros — votre déclaration d'accessibilité doit suivre le modèle du RGAA, +s'appuyer sur un audit RGAA et s'accompagner d'un schéma pluriannuel de mise en +accessibilité. Le présent document n'en tient pas lieu. + +--- + +Cette déclaration a été générée avec eaa-kit et ne constitue pas un conseil juridique. +Relisez-la avant publication et faites-la vérifier par un juriste en cas de doute. diff --git a/src/statement/templates/it.en.md b/src/statement/templates/it.en.md new file mode 100644 index 0000000..a320fb0 --- /dev/null +++ b/src/statement/templates/it.en.md @@ -0,0 +1,127 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with Legislative Decree 82 of 27 May 2022, which transposes Directive (EU) +2019/882 (the European Accessibility Act) into Italian law and amends Law 4 of 9 January +2004 (the Stanca Act). Its provisions apply from 28 June 2025. + +This accessibility statement applies to {{ site.url }}. + +## Compliance status + +{{#if compliance.isCompliant}} +This website is fully compliant with {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +This website is partially compliant with {{ compliance.standard }}. The content listed in +the following section is not accessible, for the reasons given. +{{/if}} +{{#if compliance.isNonCompliant}} +This website is not compliant with {{ compliance.standard }}. The content listed in the +following section is not accessible, for the reasons given. +{{/if}} + +## Non-accessible content + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requirement affected: {{ standards }} +{{/if}} +{{#if pageList}} + Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reason: disproportionate burden. +{{/if}} +{{#if isOutOfScope}} + Reason: the content falls outside the scope of Legislative Decree 82/2022. +{{/if}} +{{#if isFixPlanned}} + Reason: the barrier is known and is being addressed. +{{/if}} +{{#if remedyByFormatted}} + Expected to be resolved by: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No non-accessible content was known at the time of assessment. +{{/if}} + +## Preparation of this statement + +This statement was prepared on {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +It is based on a self-assessment carried out by {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +It is based on an assessment carried out by a third party. +{{/if}} + +{{#if audit.isSinglePage}} +The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website. +{{/if}} +{{#if audit.isMultiPage}} +The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }} +pages of this website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +One further rule check requires a manual decision. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} further rule checks require a manual decision. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +One rule check could not be decided by the tool that was used; it is not reported as met. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they +are not reported as met. +{{/if}} +{{#if hasAudit}} + +{{/if}} +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contact form: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Phone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Address: {{ provider.address }} +{{/if}} + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can report the barrier to the Agency for +Digital Italy (AgID), the authority supervising the accessibility of services within the +scope of Legislative Decree 82/2022. + +Agenzia per l'Italia digitale +https://www.agid.gov.it + +Private providers of public-facing services whose average turnover over the last three +years exceeds €500 million publish and update an accessibility declaration on AgID's own +model, by 23 September each year. This document does not stand in for that declaration. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/src/statement/templates/it.it.md b/src/statement/templates/it.it.md new file mode 100644 index 0000000..d91d6c2 --- /dev/null +++ b/src/statement/templates/it.it.md @@ -0,0 +1,130 @@ +# Dichiarazione di accessibilità + +{{ provider.legalName }} si impegna a rendere accessibile il sito {{ site.name }} in +conformità al decreto legislativo 27 maggio 2022, n. 82, che recepisce la direttiva (UE) +2019/882 (European Accessibility Act) e modifica la legge 9 gennaio 2004, n. 4 (legge +Stanca). Le relative disposizioni si applicano dal 28 giugno 2025. + +Questa dichiarazione di accessibilità si riferisce a {{ site.url }}. + +## Stato di conformità + +{{#if compliance.isCompliant}} +Questo sito è pienamente conforme a {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Questo sito è parzialmente conforme a {{ compliance.standard }}. I contenuti elencati nella +sezione seguente non sono accessibili, per i motivi indicati. +{{/if}} +{{#if compliance.isNonCompliant}} +Questo sito non è conforme a {{ compliance.standard }}. I contenuti elencati nella sezione +seguente non sono accessibili, per i motivi indicati. +{{/if}} + +## Contenuti non accessibili + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requisito interessato: {{ standards }} +{{/if}} +{{#if pageList}} + Pagine interessate: {{ pageList }}{{#if hasMorePages}} e altre {{ morePages }}{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Motivo: onere sproporzionato. +{{/if}} +{{#if isOutOfScope}} + Motivo: il contenuto non rientra nell'ambito di applicazione del d.lgs. 82/2022. +{{/if}} +{{#if isFixPlanned}} + Motivo: la barriera è nota ed è in corso di correzione. +{{/if}} +{{#if remedyByFormatted}} + Correzione prevista entro il: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Rilevato da un test automatico (axe-core, regola {{ ruleId }}); da riformulare con parole proprie. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Al momento della valutazione non risultavano contenuti non accessibili. +{{/if}} + +## Redazione della presente dichiarazione + +Questa dichiarazione è stata redatta il {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Si basa su un'autovalutazione svolta da {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Si basa su una valutazione svolta da terzi. +{{/if}} + +{{#if audit.isSinglePage}} +Il test automatico del {{ audit.checkedOnFormatted }} ha riguardato una pagina di questo +sito. +{{/if}} +{{#if audit.isMultiPage}} +Il test automatico del {{ audit.checkedOnFormatted }} ha riguardato {{ audit.pages }} +pagine di questo sito. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Un'ulteriore verifica di regola richiede una valutazione manuale. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Altre {{ audit.needsReview }} verifiche di regole richiedono una valutazione manuale. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Per una verifica di regola lo strumento utilizzato non ha raggiunto un esito; non viene +presentata come soddisfatta. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +Per {{ audit.notEvaluated }} verifiche di regole lo strumento utilizzato non ha raggiunto +un esito; non vengono presentate come soddisfatte. +{{/if}} +{{#if hasAudit}} + +{{/if}} +La valutazione si basa anche su test automatici. Gli strumenti automatici rilevano solo una +parte delle barriere possibili; non sostituiscono né una verifica manuale né una verifica +con tecnologie assistive. + +## Riscontri e contatti + +Ha incontrato una barriera, o le serve un'informazione in forma accessibile? Ci scriva: + +- E-mail: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Modulo di contatto: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefono: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Indirizzo: {{ provider.address }} +{{/if}} + +Ci impegniamo a rispondere in tempi brevi. + +## Procedura di attuazione + +Se la risposta non è soddisfacente, può segnalare la barriera all'Agenzia per l'Italia +digitale (AgID), autorità di vigilanza sull'accessibilità dei servizi che rientrano +nell'ambito del d.lgs. 82/2022. + +Agenzia per l'Italia digitale +https://www.agid.gov.it + +I soggetti privati che offrono servizi al pubblico con un fatturato medio, negli ultimi tre +anni di attività, superiore a 500 milioni di euro pubblicano e aggiornano ogni anno, entro +il 23 settembre, una dichiarazione di accessibilità secondo il modello AgID. Questo +documento non sostituisce quella dichiarazione. + +--- + +Questa dichiarazione è stata generata con eaa-kit e non costituisce consulenza legale. La +rilegga prima di pubblicarla e, in caso di dubbio, la faccia verificare da un legale. diff --git a/src/statement/templates/nl.en.md b/src/statement/templates/nl.en.md new file mode 100644 index 0000000..4328d3c --- /dev/null +++ b/src/statement/templates/nl.en.md @@ -0,0 +1,125 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with the Implementatiewet toegankelijkheidsvoorschriften producten en diensten, +which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Dutch law. +It has been in force since 28 June 2025. + +This accessibility statement applies to {{ site.url }}. + +## Compliance status + +{{#if compliance.isCompliant}} +This website is fully compliant with {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +This website is partially compliant with {{ compliance.standard }}. The content listed in +the following section is not accessible, for the reasons given. +{{/if}} +{{#if compliance.isNonCompliant}} +This website is not compliant with {{ compliance.standard }}. The content listed in the +following section is not accessible, for the reasons given. +{{/if}} + +## Non-accessible content + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Requirement affected: {{ standards }} +{{/if}} +{{#if pageList}} + Pages affected: {{ pageList }}{{#if hasMorePages}} and {{ morePages }} more{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reason: disproportionate burden. +{{/if}} +{{#if isOutOfScope}} + Reason: the content falls outside the scope of this Act. +{{/if}} +{{#if isFixPlanned}} + Reason: the barrier is known and is being addressed. +{{/if}} +{{#if remedyByFormatted}} + Expected to be resolved by: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Detected by automated testing (axe-core, rule {{ ruleId }}); describe it in your own words. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +No non-accessible content was known at the time of assessment. +{{/if}} + +## Preparation of this statement + +This statement was prepared on {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +It is based on a self-assessment carried out by {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +It is based on an assessment carried out by a third party. +{{/if}} + +{{#if audit.isSinglePage}} +The automated test run of {{ audit.checkedOnFormatted }} covered one page of this website. +{{/if}} +{{#if audit.isMultiPage}} +The automated test run of {{ audit.checkedOnFormatted }} covered {{ audit.pages }} +pages of this website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +One further rule check requires a manual decision. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} further rule checks require a manual decision. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +One rule check could not be decided by the tool that was used; it is not reported as met. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} rule checks could not be decided by the tool that was used; they +are not reported as met. +{{/if}} +{{#if hasAudit}} + +{{/if}} +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contact form: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Phone: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Address: {{ provider.address }} +{{/if}} + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can report the matter to the supervisor. +Supervision is split between several authorities: for services such as web shops and +customer support it is the Autoriteit Consument & Markt (ACM), and for products such as +smartphones, e-readers and payment terminals the Rijksinspectie Digitale Infrastructuur +(RDI). + +Autoriteit Consument & Markt +https://www.acm.nl + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/src/statement/templates/nl.nl.md b/src/statement/templates/nl.nl.md new file mode 100644 index 0000000..e2a77ce --- /dev/null +++ b/src/statement/templates/nl.nl.md @@ -0,0 +1,127 @@ +# Toegankelijkheidsverklaring + +{{ provider.legalName }} zet zich in om de website {{ site.name }} toegankelijk te maken, +in overeenstemming met de Implementatiewet toegankelijkheidsvoorschriften producten en +diensten, waarmee richtlijn (EU) 2019/882 (European Accessibility Act) in Nederlands recht +is omgezet. De wet geldt sinds 28 juni 2025. + +Deze toegankelijkheidsverklaring geldt voor {{ site.url }}. + +## Nalevingsstatus + +{{#if compliance.isCompliant}} +Deze website voldoet volledig aan {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Deze website voldoet gedeeltelijk aan {{ compliance.standard }}. De inhoud die hieronder +staat is niet toegankelijk, om de genoemde redenen. +{{/if}} +{{#if compliance.isNonCompliant}} +Deze website voldoet niet aan {{ compliance.standard }}. De inhoud die hieronder staat is +niet toegankelijk, om de genoemde redenen. +{{/if}} + +## Niet-toegankelijke inhoud + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Betrokken eis: {{ standards }} +{{/if}} +{{#if pageList}} + Betrokken pagina's: {{ pageList }}{{#if hasMorePages}} en {{ morePages }} andere{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Reden: onevenredige last. +{{/if}} +{{#if isOutOfScope}} + Reden: de inhoud valt buiten het toepassingsgebied van deze wet. +{{/if}} +{{#if isFixPlanned}} + Reden: de drempel is bekend en wordt verholpen. +{{/if}} +{{#if remedyByFormatted}} + Verwacht verholpen op: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Vastgesteld met een geautomatiseerde test (axe-core, regel {{ ruleId }}); beschrijf dit in eigen woorden. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Op het moment van de beoordeling was er geen niet-toegankelijke inhoud bekend. +{{/if}} + +## Opstelling van deze verklaring + +Deze verklaring is opgesteld op {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Zij berust op een zelfbeoordeling door {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Zij berust op een beoordeling door een derde partij. +{{/if}} + +{{#if audit.isSinglePage}} +De geautomatiseerde test van {{ audit.checkedOnFormatted }} betrof één pagina van deze +website. +{{/if}} +{{#if audit.isMultiPage}} +De geautomatiseerde test van {{ audit.checkedOnFormatted }} betrof {{ audit.pages }} +pagina's van deze website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Voor één andere regelcontrole is een menselijke beoordeling nodig. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Voor {{ audit.needsReview }} andere regelcontroles is een menselijke beoordeling nodig. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Bij één regelcontrole kwam het gebruikte gereedschap niet tot een uitkomst; die wordt niet +als voldaan gepresenteerd. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +Bij {{ audit.notEvaluated }} regelcontroles kwam het gebruikte gereedschap niet tot een +uitkomst; die worden niet als voldaan gepresenteerd. +{{/if}} +{{#if hasAudit}} + +{{/if}} +De beoordeling berust mede op geautomatiseerd testen. Geautomatiseerde gereedschappen +vinden maar een deel van de mogelijke drempels; zij vervangen geen handmatige test en geen +test met hulptechnologie. + +## Reactie en contact + +Een drempel tegengekomen, of informatie nodig in een toegankelijke vorm? Laat het ons +weten: + +- E-mail: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Contactformulier: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefoon: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Adres: {{ provider.address }} +{{/if}} + +Wij streven ernaar snel te reageren. + +## Handhavingsprocedure + +Bent u niet tevreden met onze reactie, dan kunt u een melding doen bij de toezichthouder. +Het toezicht is over meerdere toezichthouders verdeeld: voor diensten zoals webwinkels en +klantenservice is dat de Autoriteit Consument & Markt (ACM), voor producten zoals +smartphones, e-readers en betaalautomaten de Rijksinspectie Digitale Infrastructuur (RDI). + +Autoriteit Consument & Markt +https://www.acm.nl + +--- + +Deze verklaring is gemaakt met eaa-kit en is geen juridisch advies. Lees haar na voordat u +haar publiceert en laat haar bij twijfel juridisch toetsen. diff --git a/tests/cli/statement.test.ts b/tests/cli/statement.test.ts index 92a0074..e8f0d16 100644 --- a/tests/cli/statement.test.ts +++ b/tests/cli/statement.test.ts @@ -10,7 +10,8 @@ import { SUPPORTED_REPORT_SCHEMA } from '../../src/statement/findings.ts' * Stands in for a country added to COUNTRIES before its template is written. * AT, CH and DE all have one, so the guard needs a country that does not. */ -const UNWRITTEN = 'FR' as Country +/** A country in COUNTRIES whose templates nobody has written. There is none today. */ +const UNWRITTEN = 'JP' as Country const AUDIT_FIXTURE = path.join(import.meta.dirname, '../fixtures/statement/audit.json') @@ -150,7 +151,7 @@ describe('runStatementCommand', () => { const { exitCode } = await runStatementCommand({ cwd: dir, country: UNWRITTEN }) expect(exitCode).toBe(2) - expect(stderr.join('')).toContain('No statement template for fr.de') + expect(stderr.join('')).toContain('No statement template for jp.de') expect(stdout.join('')).toBe('') }) }) diff --git a/tests/config/load.test.ts b/tests/config/load.test.ts index bb22218..1193728 100644 --- a/tests/config/load.test.ts +++ b/tests/config/load.test.ts @@ -99,7 +99,7 @@ describe('parseConfig', () => { it.each([ ['a missing contact address', { provider: { legalName: 'X' } }, 'provider.email'], ['an unroutable site url', { site: { ...VALID.site, url: 'not-a-url' } }, 'site.url'], - ['an unknown country', { enforcement: { country: 'FR' } }, 'enforcement.country'], + ['an unknown country', { enforcement: { country: 'JP' } }, 'enforcement.country'], [ 'a status outside the three the regime recognises', { compliance: { ...VALID.compliance, status: 'mostly-fine' } }, diff --git a/tests/statement/__snapshots__/statement.es.en.html b/tests/statement/__snapshots__/statement.es.en.html new file mode 100644 index 0000000..d2f5ab3 --- /dev/null +++ b/tests/statement/__snapshots__/statement.es.en.html @@ -0,0 +1,83 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with Ley 11/2023 of 8 May 2023, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Spanish law. Its obligations for products and services apply from 28 June 2025.

+

This accessibility statement applies to https://example.at.

+

Compliance status

+

This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in the following section is not accessible, for the reasons given.

+

Non-accessible content

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reason: the barrier is known and is being addressed.
    + Expected to be resolved by: 31 December 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reason: disproportionate burden.
  • +
  • Form field must not have multiple label elements
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words.
  • +
  • Images must have alternative text
    + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule image-alt); describe it in your own words.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words.
  • +
  • Document should have one main landmark
    + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages affected: kontakt.html, team.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words.
  • +
+

Preparation of this statement

+

This statement was prepared on 21 August 2026.

+

It is based on a self-assessment carried out by Musterbetrieb GmbH.

+

The automated test run of 21 August 2026 covered 8 pages of this website. 2 further rule checks require a manual decision. 12 rule checks could not be decided by the tool that was used; they are not reported as met.

+

The assessment relies in part on automated testing. Automated tools detect only a subset of possible barriers; they are not a substitute for manual testing or for testing with assistive technologies.

+

Feedback and contact

+

Found a barrier, or need information in an accessible format? Please get in touch:

+ +

We aim to respond to your feedback promptly.

+

Enforcement procedure

+

If you are not satisfied with our response, you can complain to the competent market surveillance authority. Ley 11/2023 divides that surveillance between the national administration and the autonomous communities, so for a digital service the usual route is the consumer authority of your autonomous community.

+

Public-sector websites and mobile applications are additionally governed by Real Decreto 1112/2018, which requires an accessibility statement with content of its own. This document is not that statement.

+
+

This statement was generated with eaa-kit and is not legal advice. Review it before publishing, and have it checked by a lawyer if in doubt.

+
+ + diff --git a/tests/statement/__snapshots__/statement.es.en.md b/tests/statement/__snapshots__/statement.es.en.md new file mode 100644 index 0000000..6f9aacd --- /dev/null +++ b/tests/statement/__snapshots__/statement.es.en.md @@ -0,0 +1,84 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with Ley 11/2023 of 8 May 2023, which transposes Directive (EU) 2019/882 (the +European Accessibility Act) into Spanish law. Its obligations for products and services +apply from 28 June 2025. + +This accessibility statement applies to https://example.at. + +## Compliance status + +This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in +the following section is not accessible, for the reasons given. + +## Non-accessible content + +- Die eingebettete Karte hat keinen Titel. + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reason: the barrier is known and is being addressed. + Expected to be resolved by: 31 December 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reason: disproportionate burden. +- Form field must not have multiple label elements + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words. +- Images must have alternative text + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule image-alt); describe it in your own words. +- Elements must meet minimum color contrast ratio thresholds + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words. +- Document should have one main landmark + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages affected: kontakt.html, team.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words. + +## Preparation of this statement + +This statement was prepared on 21 August 2026. + +It is based on a self-assessment carried out by Musterbetrieb GmbH. + +The automated test run of 21 August 2026 covered 8 +pages of this website. +2 further rule checks require a manual decision. +12 rule checks could not be decided by the tool that was used; they +are not reported as met. + +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: office@example.at +- Contact form: https://example.at/kontakt +- Phone: +43 1 2345678 +- Address: Hauptstraße 1, 1010 Wien + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can complain to the competent market +surveillance authority. Ley 11/2023 divides that surveillance between the national +administration and the autonomous communities, so for a digital service the usual route is +the consumer authority of your autonomous community. + +Public-sector websites and mobile applications are additionally governed by Real Decreto +1112/2018, which requires an accessibility statement with content of its own. This document +is not that statement. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/tests/statement/__snapshots__/statement.es.es.html b/tests/statement/__snapshots__/statement.es.es.html new file mode 100644 index 0000000..08dc575 --- /dev/null +++ b/tests/statement/__snapshots__/statement.es.es.html @@ -0,0 +1,83 @@ + + + + + + +Declaración de accesibilidad + + + +
+

Declaración de accesibilidad

+

Musterbetrieb GmbH se compromete a hacer accesible el sitio web Musterbetrieb conforme a la Ley 11/2023, de 8 de mayo, que traspone al ordenamiento español la Directiva (UE) 2019/882 (European Accessibility Act). Sus obligaciones sobre productos y servicios son aplicables desde el 28 de junio de 2025.

+

Esta declaración de accesibilidad se refiere a https://example.at.

+

Situación de cumplimiento

+

Este sitio web es parcialmente conforme con EN 301 549 V3.2.1 (WCAG 2.2 AA). Los contenidos que se enumeran a continuación no son accesibles, por los motivos indicados.

+

Contenido no accesible

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requisito afectado: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Motivo: la barrera es conocida y está en vías de corrección.
    + Corrección prevista antes del: 31 de diciembre de 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Motivo: carga desproporcionada.
  • +
  • Form field must not have multiple label elements
    + Páginas afectadas: index.html
    + Motivo: la barrera es conocida y está en vías de corrección.
    + Detectado mediante análisis automático (axe-core, regla form-field-multiple-labels); descríbalo con sus propias palabras.
  • +
  • Images must have alternative text
    + Requisito afectado: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Páginas afectadas: index.html
    + Motivo: la barrera es conocida y está en vías de corrección.
    + Detectado mediante análisis automático (axe-core, regla image-alt); descríbalo con sus propias palabras.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requisito afectado: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Páginas afectadas: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html y 2 más
    + Motivo: la barrera es conocida y está en vías de corrección.
    + Detectado mediante análisis automático (axe-core, regla color-contrast); descríbalo con sus propias palabras.
  • +
  • Document should have one main landmark
    + Requisito afectado: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Páginas afectadas: kontakt.html, team.html
    + Motivo: la barrera es conocida y está en vías de corrección.
    + Detectado mediante análisis automático (axe-core, regla landmark-one-main); descríbalo con sus propias palabras.
  • +
+

Preparación de la presente declaración

+

Esta declaración se preparó el 21 de agosto de 2026.

+

Se basa en una autoevaluación realizada por Musterbetrieb GmbH.

+

El análisis automático del 21 de agosto de 2026 abarcó 8 páginas de este sitio web. Otras 2 comprobaciones de reglas requieren una valoración manual. En 12 comprobaciones de reglas la herramienta empleada no alcanzó un resultado; no se presentan como cumplidas.

+

La evaluación se apoya en parte en análisis automáticos. Las herramientas automáticas solo detectan una parte de las barreras posibles; no sustituyen a una revisión manual ni a una prueba con tecnologías de apoyo.

+

Observaciones y datos de contacto

+

¿Ha encontrado una barrera, o necesita información en un formato accesible? Escríbanos:

+ +

Procuramos responder a su comunicación con prontitud.

+

Procedimiento de aplicación

+

Si nuestra respuesta no le resulta satisfactoria, puede presentar una reclamación ante la autoridad de vigilancia del mercado competente. La Ley 11/2023 reparte esa vigilancia entre las administraciones competentes del Estado y de las comunidades autónomas, por lo que la vía habitual para un servicio digital es el organismo de consumo de su comunidad autónoma.

+

Los sitios web y las aplicaciones móviles del sector público se rigen además por el Real Decreto 1112/2018, que exige una declaración de accesibilidad de contenido propio. Este documento no es esa declaración.

+
+

Esta declaración se ha generado con eaa-kit y no constituye asesoramiento jurídico. Revísela antes de publicarla y, en caso de duda, sométala a revisión jurídica.

+
+ + diff --git a/tests/statement/__snapshots__/statement.es.es.md b/tests/statement/__snapshots__/statement.es.es.md new file mode 100644 index 0000000..0f3b591 --- /dev/null +++ b/tests/statement/__snapshots__/statement.es.es.md @@ -0,0 +1,84 @@ +# Declaración de accesibilidad + +Musterbetrieb GmbH se compromete a hacer accesible el sitio web Musterbetrieb +conforme a la Ley 11/2023, de 8 de mayo, que traspone al ordenamiento español la Directiva +(UE) 2019/882 (European Accessibility Act). Sus obligaciones sobre productos y servicios +son aplicables desde el 28 de junio de 2025. + +Esta declaración de accesibilidad se refiere a https://example.at. + +## Situación de cumplimiento + +Este sitio web es parcialmente conforme con EN 301 549 V3.2.1 (WCAG 2.2 AA). Los contenidos que +se enumeran a continuación no son accesibles, por los motivos indicados. + +## Contenido no accesible + +- Die eingebettete Karte hat keinen Titel. + Requisito afectado: WCAG 4.1.2, EN 301 549 9.4.1.2 + Motivo: la barrera es conocida y está en vías de corrección. + Corrección prevista antes del: 31 de diciembre de 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Motivo: carga desproporcionada. +- Form field must not have multiple label elements + Páginas afectadas: index.html + Motivo: la barrera es conocida y está en vías de corrección. + Detectado mediante análisis automático (axe-core, regla form-field-multiple-labels); descríbalo con sus propias palabras. +- Images must have alternative text + Requisito afectado: WCAG 1.1.1, EN 301 549 9.1.1.1 + Páginas afectadas: index.html + Motivo: la barrera es conocida y está en vías de corrección. + Detectado mediante análisis automático (axe-core, regla image-alt); descríbalo con sus propias palabras. +- Elements must meet minimum color contrast ratio thresholds + Requisito afectado: WCAG 1.4.3, EN 301 549 9.1.4.3 + Páginas afectadas: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html y 2 más + Motivo: la barrera es conocida y está en vías de corrección. + Detectado mediante análisis automático (axe-core, regla color-contrast); descríbalo con sus propias palabras. +- Document should have one main landmark + Requisito afectado: WCAG 1.3.1, EN 301 549 9.1.3.1 + Páginas afectadas: kontakt.html, team.html + Motivo: la barrera es conocida y está en vías de corrección. + Detectado mediante análisis automático (axe-core, regla landmark-one-main); descríbalo con sus propias palabras. + +## Preparación de la presente declaración + +Esta declaración se preparó el 21 de agosto de 2026. + +Se basa en una autoevaluación realizada por Musterbetrieb GmbH. + +El análisis automático del 21 de agosto de 2026 abarcó 8 páginas +de este sitio web. +Otras 2 comprobaciones de reglas requieren una valoración manual. +En 12 comprobaciones de reglas la herramienta empleada no alcanzó un +resultado; no se presentan como cumplidas. + +La evaluación se apoya en parte en análisis automáticos. Las herramientas automáticas solo +detectan una parte de las barreras posibles; no sustituyen a una revisión manual ni a una +prueba con tecnologías de apoyo. + +## Observaciones y datos de contacto + +¿Ha encontrado una barrera, o necesita información en un formato accesible? Escríbanos: + +- Correo electrónico: office@example.at +- Formulario de contacto: https://example.at/kontakt +- Teléfono: +43 1 2345678 +- Dirección: Hauptstraße 1, 1010 Wien + +Procuramos responder a su comunicación con prontitud. + +## Procedimiento de aplicación + +Si nuestra respuesta no le resulta satisfactoria, puede presentar una reclamación ante la +autoridad de vigilancia del mercado competente. La Ley 11/2023 reparte esa vigilancia entre +las administraciones competentes del Estado y de las comunidades autónomas, por lo que la +vía habitual para un servicio digital es el organismo de consumo de su comunidad autónoma. + +Los sitios web y las aplicaciones móviles del sector público se rigen además por el Real +Decreto 1112/2018, que exige una declaración de accesibilidad de contenido propio. Este +documento no es esa declaración. + +--- + +Esta declaración se ha generado con eaa-kit y no constituye asesoramiento jurídico. +Revísela antes de publicarla y, en caso de duda, sométala a revisión jurídica. diff --git a/tests/statement/__snapshots__/statement.fr.en.html b/tests/statement/__snapshots__/statement.fr.en.html new file mode 100644 index 0000000..65de458 --- /dev/null +++ b/tests/statement/__snapshots__/statement.fr.en.html @@ -0,0 +1,89 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with Ordonnance n° 2023-859 of 6 September 2023, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into French law, and with Article 47 of Loi n° 2005-102 of 11 February 2005.

+

This accessibility statement applies to https://example.at.

+

Compliance status

+

This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in the following section is not accessible, for the reasons given.

+

Non-accessible content

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reason: the barrier is known and is being addressed.
    + Expected to be resolved by: 31 December 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reason: disproportionate burden.
  • +
  • Form field must not have multiple label elements
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words.
  • +
  • Images must have alternative text
    + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule image-alt); describe it in your own words.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words.
  • +
  • Document should have one main landmark
    + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages affected: kontakt.html, team.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words.
  • +
+

Preparation of this statement

+

This statement was prepared on 21 August 2026.

+

It is based on a self-assessment carried out by Musterbetrieb GmbH.

+

The automated test run of 21 August 2026 covered 8 pages of this website. 2 further rule checks require a manual decision. 12 rule checks could not be decided by the tool that was used; they are not reported as met.

+

The assessment relies in part on automated testing. Automated tools detect only a subset of possible barriers; they are not a substitute for manual testing or for testing with assistive technologies.

+

Feedback and contact

+

Found a barrier, or need information in an accessible format? Please get in touch:

+ +

We aim to respond to your feedback promptly.

+

Enforcement procedure

+

If you are not satisfied with our response, you can:

+
    +
  • write to the Défenseur des droits, or contact its delegate in your département:
    + https://www.defenseurdesdroits.fr
  • +
  • report the failing to Arcom, which supervises the digital accessibility obligations of
    + Article 47 of Loi n° 2005-102: https://www.arcom.fr
  • +
+

If Article 47 applies to you — an online public service, or a company whose average turnover in France over the last three closed financial years exceeds €250 million — your accessibility statement must follow the RGAA model, rest on an RGAA audit, and be accompanied by a multi-year accessibility plan. This document does not stand in for that.

+
+

This statement was generated with eaa-kit and is not legal advice. Review it before publishing, and have it checked by a lawyer if in doubt.

+
+ + diff --git a/tests/statement/__snapshots__/statement.fr.en.md b/tests/statement/__snapshots__/statement.fr.en.md new file mode 100644 index 0000000..f543557 --- /dev/null +++ b/tests/statement/__snapshots__/statement.fr.en.md @@ -0,0 +1,87 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with Ordonnance n° 2023-859 of 6 September 2023, which transposes Directive (EU) +2019/882 (the European Accessibility Act) into French law, and with Article 47 of Loi +n° 2005-102 of 11 February 2005. + +This accessibility statement applies to https://example.at. + +## Compliance status + +This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in +the following section is not accessible, for the reasons given. + +## Non-accessible content + +- Die eingebettete Karte hat keinen Titel. + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reason: the barrier is known and is being addressed. + Expected to be resolved by: 31 December 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reason: disproportionate burden. +- Form field must not have multiple label elements + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words. +- Images must have alternative text + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule image-alt); describe it in your own words. +- Elements must meet minimum color contrast ratio thresholds + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words. +- Document should have one main landmark + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages affected: kontakt.html, team.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words. + +## Preparation of this statement + +This statement was prepared on 21 August 2026. + +It is based on a self-assessment carried out by Musterbetrieb GmbH. + +The automated test run of 21 August 2026 covered 8 +pages of this website. +2 further rule checks require a manual decision. +12 rule checks could not be decided by the tool that was used; they +are not reported as met. + +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: office@example.at +- Contact form: https://example.at/kontakt +- Phone: +43 1 2345678 +- Address: Hauptstraße 1, 1010 Wien + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can: + +- write to the Défenseur des droits, or contact its delegate in your département: + https://www.defenseurdesdroits.fr +- report the failing to Arcom, which supervises the digital accessibility obligations of + Article 47 of Loi n° 2005-102: https://www.arcom.fr + +If Article 47 applies to you — an online public service, or a company whose average +turnover in France over the last three closed financial years exceeds €250 million — your +accessibility statement must follow the RGAA model, rest on an RGAA audit, and be +accompanied by a multi-year accessibility plan. This document does not stand in for that. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/tests/statement/__snapshots__/statement.fr.fr.html b/tests/statement/__snapshots__/statement.fr.fr.html new file mode 100644 index 0000000..f9f30c2 --- /dev/null +++ b/tests/statement/__snapshots__/statement.fr.fr.html @@ -0,0 +1,89 @@ + + + + + + +Déclaration d'accessibilité + + + +
+

Déclaration d'accessibilité

+

Musterbetrieb GmbH s'engage à rendre le site Musterbetrieb accessible, conformément à l'ordonnance n° 2023-859 du 6 septembre 2023, qui transpose en droit français la directive (UE) 2019/882 (European Accessibility Act), et à l'article 47 de la loi n° 2005-102 du 11 février 2005.

+

Cette déclaration d'accessibilité s'applique à https://example.at.

+

État de conformité

+

Ce site est partiellement conforme à EN 301 549 V3.2.1 (WCAG 2.2 AA). Les contenus énumérés ci-dessous ne sont pas accessibles, pour les motifs indiqués.

+

Contenus non accessibles

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Exigence concernée : WCAG 4.1.2, EN 301 549 9.4.1.2
    + Motif : la barrière est connue et sa correction est engagée.
    + Correction prévue avant le : 31 décembre 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Motif : charge disproportionnée.
  • +
  • Form field must not have multiple label elements
    + Pages concernées : index.html
    + Motif : la barrière est connue et sa correction est engagée.
    + Détecté par un test automatisé (axe-core, règle form-field-multiple-labels) ; à reformuler dans vos propres mots.
  • +
  • Images must have alternative text
    + Exigence concernée : WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages concernées : index.html
    + Motif : la barrière est connue et sa correction est engagée.
    + Détecté par un test automatisé (axe-core, règle image-alt) ; à reformuler dans vos propres mots.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Exigence concernée : WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages concernées : blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html et 2 autres
    + Motif : la barrière est connue et sa correction est engagée.
    + Détecté par un test automatisé (axe-core, règle color-contrast) ; à reformuler dans vos propres mots.
  • +
  • Document should have one main landmark
    + Exigence concernée : WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages concernées : kontakt.html, team.html
    + Motif : la barrière est connue et sa correction est engagée.
    + Détecté par un test automatisé (axe-core, règle landmark-one-main) ; à reformuler dans vos propres mots.
  • +
+

Établissement de cette déclaration

+

Cette déclaration a été établie le 21 août 2026.

+

Elle repose sur une auto-évaluation réalisée par Musterbetrieb GmbH.

+

Le test automatisé du 21 août 2026 a porté sur 8 pages de ce site. 2 autres vérifications de règles demandent une appréciation humaine. 12 vérifications de règles n'ont pu être tranchées par l'outil utilisé ; elles ne sont pas présentées comme satisfaites.

+

L'évaluation repose en partie sur des tests automatisés. Les outils automatisés ne détectent qu'une partie des barrières possibles ; ils ne remplacent ni un test manuel ni un test avec des technologies d'assistance.

+

Retour d'information et contact

+

Vous avez rencontré une barrière, ou vous avez besoin d'une information sous une forme accessible ? Écrivez-nous :

+ +

Nous nous efforçons de répondre à votre message dans les meilleurs délais.

+

Voies de recours

+

Si votre demande reste sans réponse satisfaisante, vous pouvez :

+
    +
  • écrire au Défenseur des droits, ou contacter le délégué du Défenseur des droits de votre
    + département : https://www.defenseurdesdroits.fr
  • +
  • signaler le manquement à l'Arcom, qui contrôle les obligations d'accessibilité numérique
    + de l'article 47 de la loi n° 2005-102 : https://www.arcom.fr
  • +
+

Si vous relevez de cet article 47 — service public en ligne, ou entreprise dont le chiffre d'affaires moyen réalisé en France sur les trois derniers exercices clos dépasse 250 millions d'euros — votre déclaration d'accessibilité doit suivre le modèle du RGAA, s'appuyer sur un audit RGAA et s'accompagner d'un schéma pluriannuel de mise en accessibilité. Le présent document n'en tient pas lieu.

+
+

Cette déclaration a été générée avec eaa-kit et ne constitue pas un conseil juridique. Relisez-la avant publication et faites-la vérifier par un juriste en cas de doute.

+
+ + diff --git a/tests/statement/__snapshots__/statement.fr.fr.md b/tests/statement/__snapshots__/statement.fr.fr.md new file mode 100644 index 0000000..8176850 --- /dev/null +++ b/tests/statement/__snapshots__/statement.fr.fr.md @@ -0,0 +1,89 @@ +# Déclaration d'accessibilité + +Musterbetrieb GmbH s'engage à rendre le site Musterbetrieb accessible, conformément +à l'ordonnance n° 2023-859 du 6 septembre 2023, qui transpose en droit français la +directive (UE) 2019/882 (European Accessibility Act), et à l'article 47 de la loi +n° 2005-102 du 11 février 2005. + +Cette déclaration d'accessibilité s'applique à https://example.at. + +## État de conformité + +Ce site est partiellement conforme à EN 301 549 V3.2.1 (WCAG 2.2 AA). Les contenus énumérés +ci-dessous ne sont pas accessibles, pour les motifs indiqués. + +## Contenus non accessibles + +- Die eingebettete Karte hat keinen Titel. + Exigence concernée : WCAG 4.1.2, EN 301 549 9.4.1.2 + Motif : la barrière est connue et sa correction est engagée. + Correction prévue avant le : 31 décembre 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Motif : charge disproportionnée. +- Form field must not have multiple label elements + Pages concernées : index.html + Motif : la barrière est connue et sa correction est engagée. + Détecté par un test automatisé (axe-core, règle form-field-multiple-labels) ; à reformuler dans vos propres mots. +- Images must have alternative text + Exigence concernée : WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages concernées : index.html + Motif : la barrière est connue et sa correction est engagée. + Détecté par un test automatisé (axe-core, règle image-alt) ; à reformuler dans vos propres mots. +- Elements must meet minimum color contrast ratio thresholds + Exigence concernée : WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages concernées : blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html et 2 autres + Motif : la barrière est connue et sa correction est engagée. + Détecté par un test automatisé (axe-core, règle color-contrast) ; à reformuler dans vos propres mots. +- Document should have one main landmark + Exigence concernée : WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages concernées : kontakt.html, team.html + Motif : la barrière est connue et sa correction est engagée. + Détecté par un test automatisé (axe-core, règle landmark-one-main) ; à reformuler dans vos propres mots. + +## Établissement de cette déclaration + +Cette déclaration a été établie le 21 août 2026. + +Elle repose sur une auto-évaluation réalisée par Musterbetrieb GmbH. + +Le test automatisé du 21 août 2026 a porté sur 8 pages de +ce site. +2 autres vérifications de règles demandent une appréciation humaine. +12 vérifications de règles n'ont pu être tranchées par l'outil +utilisé ; elles ne sont pas présentées comme satisfaites. + +L'évaluation repose en partie sur des tests automatisés. Les outils automatisés ne +détectent qu'une partie des barrières possibles ; ils ne remplacent ni un test manuel ni un +test avec des technologies d'assistance. + +## Retour d'information et contact + +Vous avez rencontré une barrière, ou vous avez besoin d'une information sous une forme +accessible ? Écrivez-nous : + +- Courriel : office@example.at +- Formulaire de contact : https://example.at/kontakt +- Téléphone : +43 1 2345678 +- Adresse : Hauptstraße 1, 1010 Wien + +Nous nous efforçons de répondre à votre message dans les meilleurs délais. + +## Voies de recours + +Si votre demande reste sans réponse satisfaisante, vous pouvez : + +- écrire au Défenseur des droits, ou contacter le délégué du Défenseur des droits de votre + département : https://www.defenseurdesdroits.fr +- signaler le manquement à l'Arcom, qui contrôle les obligations d'accessibilité numérique + de l'article 47 de la loi n° 2005-102 : https://www.arcom.fr + +Si vous relevez de cet article 47 — service public en ligne, ou entreprise dont le chiffre +d'affaires moyen réalisé en France sur les trois derniers exercices clos dépasse +250 millions d'euros — votre déclaration d'accessibilité doit suivre le modèle du RGAA, +s'appuyer sur un audit RGAA et s'accompagner d'un schéma pluriannuel de mise en +accessibilité. Le présent document n'en tient pas lieu. + +--- + +Cette déclaration a été générée avec eaa-kit et ne constitue pas un conseil juridique. +Relisez-la avant publication et faites-la vérifier par un juriste en cas de doute. diff --git a/tests/statement/__snapshots__/statement.it.en.html b/tests/statement/__snapshots__/statement.it.en.html new file mode 100644 index 0000000..9dddefc --- /dev/null +++ b/tests/statement/__snapshots__/statement.it.en.html @@ -0,0 +1,84 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with Legislative Decree 82 of 27 May 2022, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Italian law and amends Law 4 of 9 January 2004 (the Stanca Act). Its provisions apply from 28 June 2025.

+

This accessibility statement applies to https://example.at.

+

Compliance status

+

This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in the following section is not accessible, for the reasons given.

+

Non-accessible content

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reason: the barrier is known and is being addressed.
    + Expected to be resolved by: 31 December 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reason: disproportionate burden.
  • +
  • Form field must not have multiple label elements
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words.
  • +
  • Images must have alternative text
    + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule image-alt); describe it in your own words.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words.
  • +
  • Document should have one main landmark
    + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages affected: kontakt.html, team.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words.
  • +
+

Preparation of this statement

+

This statement was prepared on 21 August 2026.

+

It is based on a self-assessment carried out by Musterbetrieb GmbH.

+

The automated test run of 21 August 2026 covered 8 pages of this website. 2 further rule checks require a manual decision. 12 rule checks could not be decided by the tool that was used; they are not reported as met.

+

The assessment relies in part on automated testing. Automated tools detect only a subset of possible barriers; they are not a substitute for manual testing or for testing with assistive technologies.

+

Feedback and contact

+

Found a barrier, or need information in an accessible format? Please get in touch:

+ +

We aim to respond to your feedback promptly.

+

Enforcement procedure

+

If you are not satisfied with our response, you can report the barrier to the Agency for Digital Italy (AgID), the authority supervising the accessibility of services within the scope of Legislative Decree 82/2022.

+

Agenzia per l'Italia digitale https://www.agid.gov.it

+

Private providers of public-facing services whose average turnover over the last three years exceeds €500 million publish and update an accessibility declaration on AgID's own model, by 23 September each year. This document does not stand in for that declaration.

+
+

This statement was generated with eaa-kit and is not legal advice. Review it before publishing, and have it checked by a lawyer if in doubt.

+
+ + diff --git a/tests/statement/__snapshots__/statement.it.en.md b/tests/statement/__snapshots__/statement.it.en.md new file mode 100644 index 0000000..a3a7e8b --- /dev/null +++ b/tests/statement/__snapshots__/statement.it.en.md @@ -0,0 +1,86 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with Legislative Decree 82 of 27 May 2022, which transposes Directive (EU) +2019/882 (the European Accessibility Act) into Italian law and amends Law 4 of 9 January +2004 (the Stanca Act). Its provisions apply from 28 June 2025. + +This accessibility statement applies to https://example.at. + +## Compliance status + +This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in +the following section is not accessible, for the reasons given. + +## Non-accessible content + +- Die eingebettete Karte hat keinen Titel. + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reason: the barrier is known and is being addressed. + Expected to be resolved by: 31 December 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reason: disproportionate burden. +- Form field must not have multiple label elements + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words. +- Images must have alternative text + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule image-alt); describe it in your own words. +- Elements must meet minimum color contrast ratio thresholds + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words. +- Document should have one main landmark + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages affected: kontakt.html, team.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words. + +## Preparation of this statement + +This statement was prepared on 21 August 2026. + +It is based on a self-assessment carried out by Musterbetrieb GmbH. + +The automated test run of 21 August 2026 covered 8 +pages of this website. +2 further rule checks require a manual decision. +12 rule checks could not be decided by the tool that was used; they +are not reported as met. + +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: office@example.at +- Contact form: https://example.at/kontakt +- Phone: +43 1 2345678 +- Address: Hauptstraße 1, 1010 Wien + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can report the barrier to the Agency for +Digital Italy (AgID), the authority supervising the accessibility of services within the +scope of Legislative Decree 82/2022. + +Agenzia per l'Italia digitale +https://www.agid.gov.it + +Private providers of public-facing services whose average turnover over the last three +years exceeds €500 million publish and update an accessibility declaration on AgID's own +model, by 23 September each year. This document does not stand in for that declaration. + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/tests/statement/__snapshots__/statement.it.it.html b/tests/statement/__snapshots__/statement.it.it.html new file mode 100644 index 0000000..a3de0f9 --- /dev/null +++ b/tests/statement/__snapshots__/statement.it.it.html @@ -0,0 +1,84 @@ + + + + + + +Dichiarazione di accessibilità + + + +
+

Dichiarazione di accessibilità

+

Musterbetrieb GmbH si impegna a rendere accessibile il sito Musterbetrieb in conformità al decreto legislativo 27 maggio 2022, n. 82, che recepisce la direttiva (UE) 2019/882 (European Accessibility Act) e modifica la legge 9 gennaio 2004, n. 4 (legge Stanca). Le relative disposizioni si applicano dal 28 giugno 2025.

+

Questa dichiarazione di accessibilità si riferisce a https://example.at.

+

Stato di conformità

+

Questo sito è parzialmente conforme a EN 301 549 V3.2.1 (WCAG 2.2 AA). I contenuti elencati nella sezione seguente non sono accessibili, per i motivi indicati.

+

Contenuti non accessibili

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requisito interessato: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Motivo: la barriera è nota ed è in corso di correzione.
    + Correzione prevista entro il: 31 dicembre 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Motivo: onere sproporzionato.
  • +
  • Form field must not have multiple label elements
    + Pagine interessate: index.html
    + Motivo: la barriera è nota ed è in corso di correzione.
    + Rilevato da un test automatico (axe-core, regola form-field-multiple-labels); da riformulare con parole proprie.
  • +
  • Images must have alternative text
    + Requisito interessato: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pagine interessate: index.html
    + Motivo: la barriera è nota ed è in corso di correzione.
    + Rilevato da un test automatico (axe-core, regola image-alt); da riformulare con parole proprie.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requisito interessato: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pagine interessate: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html e altre 2
    + Motivo: la barriera è nota ed è in corso di correzione.
    + Rilevato da un test automatico (axe-core, regola color-contrast); da riformulare con parole proprie.
  • +
  • Document should have one main landmark
    + Requisito interessato: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pagine interessate: kontakt.html, team.html
    + Motivo: la barriera è nota ed è in corso di correzione.
    + Rilevato da un test automatico (axe-core, regola landmark-one-main); da riformulare con parole proprie.
  • +
+

Redazione della presente dichiarazione

+

Questa dichiarazione è stata redatta il 21 agosto 2026.

+

Si basa su un'autovalutazione svolta da Musterbetrieb GmbH.

+

Il test automatico del 21 agosto 2026 ha riguardato 8 pagine di questo sito. Altre 2 verifiche di regole richiedono una valutazione manuale. Per 12 verifiche di regole lo strumento utilizzato non ha raggiunto un esito; non vengono presentate come soddisfatte.

+

La valutazione si basa anche su test automatici. Gli strumenti automatici rilevano solo una parte delle barriere possibili; non sostituiscono né una verifica manuale né una verifica con tecnologie assistive.

+

Riscontri e contatti

+

Ha incontrato una barriera, o le serve un'informazione in forma accessibile? Ci scriva:

+ +

Ci impegniamo a rispondere in tempi brevi.

+

Procedura di attuazione

+

Se la risposta non è soddisfacente, può segnalare la barriera all'Agenzia per l'Italia digitale (AgID), autorità di vigilanza sull'accessibilità dei servizi che rientrano nell'ambito del d.lgs. 82/2022.

+

Agenzia per l'Italia digitale https://www.agid.gov.it

+

I soggetti privati che offrono servizi al pubblico con un fatturato medio, negli ultimi tre anni di attività, superiore a 500 milioni di euro pubblicano e aggiornano ogni anno, entro il 23 settembre, una dichiarazione di accessibilità secondo il modello AgID. Questo documento non sostituisce quella dichiarazione.

+
+

Questa dichiarazione è stata generata con eaa-kit e non costituisce consulenza legale. La rilegga prima di pubblicarla e, in caso di dubbio, la faccia verificare da un legale.

+
+ + diff --git a/tests/statement/__snapshots__/statement.it.it.md b/tests/statement/__snapshots__/statement.it.it.md new file mode 100644 index 0000000..b112e62 --- /dev/null +++ b/tests/statement/__snapshots__/statement.it.it.md @@ -0,0 +1,87 @@ +# Dichiarazione di accessibilità + +Musterbetrieb GmbH si impegna a rendere accessibile il sito Musterbetrieb in +conformità al decreto legislativo 27 maggio 2022, n. 82, che recepisce la direttiva (UE) +2019/882 (European Accessibility Act) e modifica la legge 9 gennaio 2004, n. 4 (legge +Stanca). Le relative disposizioni si applicano dal 28 giugno 2025. + +Questa dichiarazione di accessibilità si riferisce a https://example.at. + +## Stato di conformità + +Questo sito è parzialmente conforme a EN 301 549 V3.2.1 (WCAG 2.2 AA). I contenuti elencati nella +sezione seguente non sono accessibili, per i motivi indicati. + +## Contenuti non accessibili + +- Die eingebettete Karte hat keinen Titel. + Requisito interessato: WCAG 4.1.2, EN 301 549 9.4.1.2 + Motivo: la barriera è nota ed è in corso di correzione. + Correzione prevista entro il: 31 dicembre 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Motivo: onere sproporzionato. +- Form field must not have multiple label elements + Pagine interessate: index.html + Motivo: la barriera è nota ed è in corso di correzione. + Rilevato da un test automatico (axe-core, regola form-field-multiple-labels); da riformulare con parole proprie. +- Images must have alternative text + Requisito interessato: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pagine interessate: index.html + Motivo: la barriera è nota ed è in corso di correzione. + Rilevato da un test automatico (axe-core, regola image-alt); da riformulare con parole proprie. +- Elements must meet minimum color contrast ratio thresholds + Requisito interessato: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pagine interessate: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html e altre 2 + Motivo: la barriera è nota ed è in corso di correzione. + Rilevato da un test automatico (axe-core, regola color-contrast); da riformulare con parole proprie. +- Document should have one main landmark + Requisito interessato: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pagine interessate: kontakt.html, team.html + Motivo: la barriera è nota ed è in corso di correzione. + Rilevato da un test automatico (axe-core, regola landmark-one-main); da riformulare con parole proprie. + +## Redazione della presente dichiarazione + +Questa dichiarazione è stata redatta il 21 agosto 2026. + +Si basa su un'autovalutazione svolta da Musterbetrieb GmbH. + +Il test automatico del 21 agosto 2026 ha riguardato 8 +pagine di questo sito. +Altre 2 verifiche di regole richiedono una valutazione manuale. +Per 12 verifiche di regole lo strumento utilizzato non ha raggiunto +un esito; non vengono presentate come soddisfatte. + +La valutazione si basa anche su test automatici. Gli strumenti automatici rilevano solo una +parte delle barriere possibili; non sostituiscono né una verifica manuale né una verifica +con tecnologie assistive. + +## Riscontri e contatti + +Ha incontrato una barriera, o le serve un'informazione in forma accessibile? Ci scriva: + +- E-mail: office@example.at +- Modulo di contatto: https://example.at/kontakt +- Telefono: +43 1 2345678 +- Indirizzo: Hauptstraße 1, 1010 Wien + +Ci impegniamo a rispondere in tempi brevi. + +## Procedura di attuazione + +Se la risposta non è soddisfacente, può segnalare la barriera all'Agenzia per l'Italia +digitale (AgID), autorità di vigilanza sull'accessibilità dei servizi che rientrano +nell'ambito del d.lgs. 82/2022. + +Agenzia per l'Italia digitale +https://www.agid.gov.it + +I soggetti privati che offrono servizi al pubblico con un fatturato medio, negli ultimi tre +anni di attività, superiore a 500 milioni di euro pubblicano e aggiornano ogni anno, entro +il 23 settembre, una dichiarazione di accessibilità secondo il modello AgID. Questo +documento non sostituisce quella dichiarazione. + +--- + +Questa dichiarazione è stata generata con eaa-kit e non costituisce consulenza legale. La +rilegga prima di pubblicarla e, in caso di dubbio, la faccia verificare da un legale. diff --git a/tests/statement/__snapshots__/statement.nl.en.html b/tests/statement/__snapshots__/statement.nl.en.html new file mode 100644 index 0000000..7806d3c --- /dev/null +++ b/tests/statement/__snapshots__/statement.nl.en.html @@ -0,0 +1,83 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with the Implementatiewet toegankelijkheidsvoorschriften producten en diensten, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Dutch law. It has been in force since 28 June 2025.

+

This accessibility statement applies to https://example.at.

+

Compliance status

+

This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in the following section is not accessible, for the reasons given.

+

Non-accessible content

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reason: the barrier is known and is being addressed.
    + Expected to be resolved by: 31 December 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reason: disproportionate burden.
  • +
  • Form field must not have multiple label elements
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words.
  • +
  • Images must have alternative text
    + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Pages affected: index.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule image-alt); describe it in your own words.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words.
  • +
  • Document should have one main landmark
    + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Pages affected: kontakt.html, team.html
    + Reason: the barrier is known and is being addressed.
    + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words.
  • +
+

Preparation of this statement

+

This statement was prepared on 21 August 2026.

+

It is based on a self-assessment carried out by Musterbetrieb GmbH.

+

The automated test run of 21 August 2026 covered 8 pages of this website. 2 further rule checks require a manual decision. 12 rule checks could not be decided by the tool that was used; they are not reported as met.

+

The assessment relies in part on automated testing. Automated tools detect only a subset of possible barriers; they are not a substitute for manual testing or for testing with assistive technologies.

+

Feedback and contact

+

Found a barrier, or need information in an accessible format? Please get in touch:

+ +

We aim to respond to your feedback promptly.

+

Enforcement procedure

+

If you are not satisfied with our response, you can report the matter to the supervisor. Supervision is split between several authorities: for services such as web shops and customer support it is the Autoriteit Consument & Markt (ACM), and for products such as smartphones, e-readers and payment terminals the Rijksinspectie Digitale Infrastructuur (RDI).

+

Autoriteit Consument & Markt https://www.acm.nl

+
+

This statement was generated with eaa-kit and is not legal advice. Review it before publishing, and have it checked by a lawyer if in doubt.

+
+ + diff --git a/tests/statement/__snapshots__/statement.nl.en.md b/tests/statement/__snapshots__/statement.nl.en.md new file mode 100644 index 0000000..c51536e --- /dev/null +++ b/tests/statement/__snapshots__/statement.nl.en.md @@ -0,0 +1,84 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with the Implementatiewet toegankelijkheidsvoorschriften producten en diensten, +which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Dutch law. +It has been in force since 28 June 2025. + +This accessibility statement applies to https://example.at. + +## Compliance status + +This website is partially compliant with EN 301 549 V3.2.1 (WCAG 2.2 AA). The content listed in +the following section is not accessible, for the reasons given. + +## Non-accessible content + +- Die eingebettete Karte hat keinen Titel. + Requirement affected: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reason: the barrier is known and is being addressed. + Expected to be resolved by: 31 December 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reason: disproportionate burden. +- Form field must not have multiple label elements + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule form-field-multiple-labels); describe it in your own words. +- Images must have alternative text + Requirement affected: WCAG 1.1.1, EN 301 549 9.1.1.1 + Pages affected: index.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule image-alt); describe it in your own words. +- Elements must meet minimum color contrast ratio thresholds + Requirement affected: WCAG 1.4.3, EN 301 549 9.1.4.3 + Pages affected: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html and 2 more + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule color-contrast); describe it in your own words. +- Document should have one main landmark + Requirement affected: WCAG 1.3.1, EN 301 549 9.1.3.1 + Pages affected: kontakt.html, team.html + Reason: the barrier is known and is being addressed. + Detected by automated testing (axe-core, rule landmark-one-main); describe it in your own words. + +## Preparation of this statement + +This statement was prepared on 21 August 2026. + +It is based on a self-assessment carried out by Musterbetrieb GmbH. + +The automated test run of 21 August 2026 covered 8 +pages of this website. +2 further rule checks require a manual decision. +12 rule checks could not be decided by the tool that was used; they +are not reported as met. + +The assessment relies in part on automated testing. Automated tools detect only a subset +of possible barriers; they are not a substitute for manual testing or for testing with +assistive technologies. + +## Feedback and contact + +Found a barrier, or need information in an accessible format? Please get in touch: + +- Email: office@example.at +- Contact form: https://example.at/kontakt +- Phone: +43 1 2345678 +- Address: Hauptstraße 1, 1010 Wien + +We aim to respond to your feedback promptly. + +## Enforcement procedure + +If you are not satisfied with our response, you can report the matter to the supervisor. +Supervision is split between several authorities: for services such as web shops and +customer support it is the Autoriteit Consument & Markt (ACM), and for products such as +smartphones, e-readers and payment terminals the Rijksinspectie Digitale Infrastructuur +(RDI). + +Autoriteit Consument & Markt +https://www.acm.nl + +--- + +This statement was generated with eaa-kit and is not legal advice. Review it before +publishing, and have it checked by a lawyer if in doubt. diff --git a/tests/statement/__snapshots__/statement.nl.nl.html b/tests/statement/__snapshots__/statement.nl.nl.html new file mode 100644 index 0000000..1487e43 --- /dev/null +++ b/tests/statement/__snapshots__/statement.nl.nl.html @@ -0,0 +1,83 @@ + + + + + + +Toegankelijkheidsverklaring + + + +
+

Toegankelijkheidsverklaring

+

Musterbetrieb GmbH zet zich in om de website Musterbetrieb toegankelijk te maken, in overeenstemming met de Implementatiewet toegankelijkheidsvoorschriften producten en diensten, waarmee richtlijn (EU) 2019/882 (European Accessibility Act) in Nederlands recht is omgezet. De wet geldt sinds 28 juni 2025.

+

Deze toegankelijkheidsverklaring geldt voor https://example.at.

+

Nalevingsstatus

+

Deze website voldoet gedeeltelijk aan EN 301 549 V3.2.1 (WCAG 2.2 AA). De inhoud die hieronder staat is niet toegankelijk, om de genoemde redenen.

+

Niet-toegankelijke inhoud

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Betrokken eis: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Reden: de drempel is bekend en wordt verholpen.
    + Verwacht verholpen op: 31 december 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Reden: onevenredige last.
  • +
  • Form field must not have multiple label elements
    + Betrokken pagina's: index.html
    + Reden: de drempel is bekend en wordt verholpen.
    + Vastgesteld met een geautomatiseerde test (axe-core, regel form-field-multiple-labels); beschrijf dit in eigen woorden.
  • +
  • Images must have alternative text
    + Betrokken eis: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Betrokken pagina's: index.html
    + Reden: de drempel is bekend en wordt verholpen.
    + Vastgesteld met een geautomatiseerde test (axe-core, regel image-alt); beschrijf dit in eigen woorden.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Betrokken eis: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Betrokken pagina's: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html en 2 andere
    + Reden: de drempel is bekend en wordt verholpen.
    + Vastgesteld met een geautomatiseerde test (axe-core, regel color-contrast); beschrijf dit in eigen woorden.
  • +
  • Document should have one main landmark
    + Betrokken eis: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Betrokken pagina's: kontakt.html, team.html
    + Reden: de drempel is bekend en wordt verholpen.
    + Vastgesteld met een geautomatiseerde test (axe-core, regel landmark-one-main); beschrijf dit in eigen woorden.
  • +
+

Opstelling van deze verklaring

+

Deze verklaring is opgesteld op 21 augustus 2026.

+

Zij berust op een zelfbeoordeling door Musterbetrieb GmbH.

+

De geautomatiseerde test van 21 augustus 2026 betrof 8 pagina's van deze website. Voor 2 andere regelcontroles is een menselijke beoordeling nodig. Bij 12 regelcontroles kwam het gebruikte gereedschap niet tot een uitkomst; die worden niet als voldaan gepresenteerd.

+

De beoordeling berust mede op geautomatiseerd testen. Geautomatiseerde gereedschappen vinden maar een deel van de mogelijke drempels; zij vervangen geen handmatige test en geen test met hulptechnologie.

+

Reactie en contact

+

Een drempel tegengekomen, of informatie nodig in een toegankelijke vorm? Laat het ons weten:

+ +

Wij streven ernaar snel te reageren.

+

Handhavingsprocedure

+

Bent u niet tevreden met onze reactie, dan kunt u een melding doen bij de toezichthouder. Het toezicht is over meerdere toezichthouders verdeeld: voor diensten zoals webwinkels en klantenservice is dat de Autoriteit Consument & Markt (ACM), voor producten zoals smartphones, e-readers en betaalautomaten de Rijksinspectie Digitale Infrastructuur (RDI).

+

Autoriteit Consument & Markt https://www.acm.nl

+
+

Deze verklaring is gemaakt met eaa-kit en is geen juridisch advies. Lees haar na voordat u haar publiceert en laat haar bij twijfel juridisch toetsen.

+
+ + diff --git a/tests/statement/__snapshots__/statement.nl.nl.md b/tests/statement/__snapshots__/statement.nl.nl.md new file mode 100644 index 0000000..ed93940 --- /dev/null +++ b/tests/statement/__snapshots__/statement.nl.nl.md @@ -0,0 +1,84 @@ +# Toegankelijkheidsverklaring + +Musterbetrieb GmbH zet zich in om de website Musterbetrieb toegankelijk te maken, +in overeenstemming met de Implementatiewet toegankelijkheidsvoorschriften producten en +diensten, waarmee richtlijn (EU) 2019/882 (European Accessibility Act) in Nederlands recht +is omgezet. De wet geldt sinds 28 juni 2025. + +Deze toegankelijkheidsverklaring geldt voor https://example.at. + +## Nalevingsstatus + +Deze website voldoet gedeeltelijk aan EN 301 549 V3.2.1 (WCAG 2.2 AA). De inhoud die hieronder +staat is niet toegankelijk, om de genoemde redenen. + +## Niet-toegankelijke inhoud + +- Die eingebettete Karte hat keinen Titel. + Betrokken eis: WCAG 4.1.2, EN 301 549 9.4.1.2 + Reden: de drempel is bekend en wordt verholpen. + Verwacht verholpen op: 31 december 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Reden: onevenredige last. +- Form field must not have multiple label elements + Betrokken pagina's: index.html + Reden: de drempel is bekend en wordt verholpen. + Vastgesteld met een geautomatiseerde test (axe-core, regel form-field-multiple-labels); beschrijf dit in eigen woorden. +- Images must have alternative text + Betrokken eis: WCAG 1.1.1, EN 301 549 9.1.1.1 + Betrokken pagina's: index.html + Reden: de drempel is bekend en wordt verholpen. + Vastgesteld met een geautomatiseerde test (axe-core, regel image-alt); beschrijf dit in eigen woorden. +- Elements must meet minimum color contrast ratio thresholds + Betrokken eis: WCAG 1.4.3, EN 301 549 9.1.4.3 + Betrokken pagina's: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html en 2 andere + Reden: de drempel is bekend en wordt verholpen. + Vastgesteld met een geautomatiseerde test (axe-core, regel color-contrast); beschrijf dit in eigen woorden. +- Document should have one main landmark + Betrokken eis: WCAG 1.3.1, EN 301 549 9.1.3.1 + Betrokken pagina's: kontakt.html, team.html + Reden: de drempel is bekend en wordt verholpen. + Vastgesteld met een geautomatiseerde test (axe-core, regel landmark-one-main); beschrijf dit in eigen woorden. + +## Opstelling van deze verklaring + +Deze verklaring is opgesteld op 21 augustus 2026. + +Zij berust op een zelfbeoordeling door Musterbetrieb GmbH. + +De geautomatiseerde test van 21 augustus 2026 betrof 8 +pagina's van deze website. +Voor 2 andere regelcontroles is een menselijke beoordeling nodig. +Bij 12 regelcontroles kwam het gebruikte gereedschap niet tot een +uitkomst; die worden niet als voldaan gepresenteerd. + +De beoordeling berust mede op geautomatiseerd testen. Geautomatiseerde gereedschappen +vinden maar een deel van de mogelijke drempels; zij vervangen geen handmatige test en geen +test met hulptechnologie. + +## Reactie en contact + +Een drempel tegengekomen, of informatie nodig in een toegankelijke vorm? Laat het ons +weten: + +- E-mail: office@example.at +- Contactformulier: https://example.at/kontakt +- Telefoon: +43 1 2345678 +- Adres: Hauptstraße 1, 1010 Wien + +Wij streven ernaar snel te reageren. + +## Handhavingsprocedure + +Bent u niet tevreden met onze reactie, dan kunt u een melding doen bij de toezichthouder. +Het toezicht is over meerdere toezichthouders verdeeld: voor diensten zoals webwinkels en +klantenservice is dat de Autoriteit Consument & Markt (ACM), voor producten zoals +smartphones, e-readers en betaalautomaten de Rijksinspectie Digitale Infrastructuur (RDI). + +Autoriteit Consument & Markt +https://www.acm.nl + +--- + +Deze verklaring is gemaakt met eaa-kit en is geen juridisch advies. Lees haar na voordat u +haar publiceert en laat haar bij twijfel juridisch toetsen. diff --git a/tests/statement/render.test.ts b/tests/statement/render.test.ts index 26b1f25..5675792 100644 --- a/tests/statement/render.test.ts +++ b/tests/statement/render.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import type { Country, EaaConfigInput } from '../../src/config/define.ts' +import type { Country, EaaConfigInput, StatementLocale } from '../../src/config/define.ts' import { parseConfig } from '../../src/config/define.ts' import type { AuditFinding, AuditSummary } from '../../src/statement/findings.ts' import { renderStatement, StatementError } from '../../src/statement/render.ts' @@ -92,15 +92,28 @@ describe('template selection', () => { expect(statement.markdown).not.toContain('BaFG') }) + it('refuses a language a country does not have, naming the ones it does', async () => { + // The matrix is sparse: France's statement exists in French and English, + // and a German rendering of it is not a thing anybody wrote. Falling back + // to another language would hand somebody a legal document in a language + // their readers may not have, and do it quietly. + const missing = { country: 'FR' as Country, locale: 'de' as StatementLocale } + + await expect(renderStatement(config(), missing)).rejects.toThrow(StatementError) + await expect(renderStatement(config(), missing)).rejects.toThrow( + /No FR statement in de\. FR has: en, fr/, + ) + }) + it('refuses a country whose template does not exist yet, naming what does', async () => { - // The cast stands in for a country added to COUNTRIES before its template is - // written. Rendering a placeholder as somebody's legal document would be far - // worse than failing. - const missing = { country: 'FR' as Country } + // The cast stands in for a country added to COUNTRIES before its templates + // are written. Rendering a placeholder as somebody's legal document would be + // far worse than failing. + const missing = { country: 'JP' as Country } await expect(renderStatement(config(), missing)).rejects.toThrow(StatementError) await expect(renderStatement(config(), missing)).rejects.toThrow( - /Available: at\.de, at\.en, ch\.de, ch\.en, de\.de, de\.en/, + /Available: at\.de, at\.en, ch\.de, ch\.en, de\.de, de\.en, es\.en/, ) }) }) diff --git a/tests/statement/snapshot.test.ts b/tests/statement/snapshot.test.ts index 4f10474..ee06dce 100644 --- a/tests/statement/snapshot.test.ts +++ b/tests/statement/snapshot.test.ts @@ -1,4 +1,4 @@ -import { readFile } from 'node:fs/promises' +import { readdir, readFile } from 'node:fs/promises' import path from 'node:path' import { describe, expect, it } from 'vitest' import { parseConfig } from '../../src/config/define.ts' @@ -17,10 +17,11 @@ import { TOOL_VERSION } from '../../src/version.ts' */ const FIXTURES = path.join(import.meta.dirname, '../fixtures/statement') +const TEMPLATES = path.join(import.meta.dirname, '../../src/statement/templates') /** * The generator meta tag carries the package version, which changes on every - * release and would otherwise rewrite all six HTML snapshots for a reason that + * release and would otherwise rewrite every HTML snapshot for a reason that * has nothing to do with what these files are for. The version is asserted on * its own below; here it is held still so the diff is the prose. */ @@ -28,6 +29,12 @@ function stable(html: string): string { return html.replaceAll(TOOL_VERSION, '0.0.0-test') } +/** + * Every template there is, which is a sparse matrix: each country has the + * language its law is administered in, and English. A country added without a + * line here would have no snapshot and no reviewable form, so the count is + * asserted against the template directory below. + */ const COMBINATIONS = [ { country: 'AT', locale: 'de' }, { country: 'AT', locale: 'en' }, @@ -35,6 +42,14 @@ const COMBINATIONS = [ { country: 'CH', locale: 'en' }, { country: 'DE', locale: 'de' }, { country: 'DE', locale: 'en' }, + { country: 'ES', locale: 'en' }, + { country: 'ES', locale: 'es' }, + { country: 'FR', locale: 'en' }, + { country: 'FR', locale: 'fr' }, + { country: 'IT', locale: 'en' }, + { country: 'IT', locale: 'it' }, + { country: 'NL', locale: 'en' }, + { country: 'NL', locale: 'nl' }, ] as const async function fixtures() { @@ -70,6 +85,20 @@ describe('statement snapshots', () => { ) }) + it('covers every template that ships', async () => { + // A country added without a line in COMBINATIONS would ship prose no + // snapshot has ever shown a reader, which is the one thing these files + // exist to prevent. + const shipped = (await readdir(TEMPLATES)) + .filter((entry) => entry.endsWith('.md')) + .map((entry) => entry.replace(/\.md$/, '')) + .sort() + + expect(shipped).toEqual( + COMBINATIONS.map(({ country, locale }) => `${country.toLowerCase()}.${locale}`).sort(), + ) + }) + it('stamps the real package version into the generator meta tag', async () => { // What the snapshots above deliberately hold still, asserted once here so // normalising it cannot hide a version that stopped being written at all. From 9b675728d3ad8b15dcc0795be9c572be7892a8fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 18:48:55 +0000 Subject: [PATCH 4/5] chore: 0.5.0 v0.4.0 was tagged before PRs #21 and #22 merged, so master has carried three Unreleased sections since: the fingerprint fix that moves both file contracts to schemaVersion 2, --fast, the browser and start-up work, and six bugs found by using 0.4.0 on real builds. Folded into one section with this release's own three additions. The JSON report and baseline schemaVersions are the entry to read. docs/reports.md gets a "Coming from 0.4.0" note saying what refuses what and why, since a consumer that never touches fingerprint needs no change at all and should not have to work that out from the changelog. The Action example and the integrations doc pinned @v0.4.0; examples/ is regenerated so the version it stamps is the one being published. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013WKUrBVgwFFGLbsfN46MBF --- .github/workflows/accessibility.yml | 2 +- CHANGELOG.md | 181 ++++++++++++++++------------ docs/baseline.md | 10 +- docs/integrations.md | 2 +- docs/reports.md | 17 +++ examples/report.html | 4 +- examples/report.json | 2 +- examples/report.sarif | 2 +- examples/statement.de.html | 2 +- package.json | 2 +- 10 files changed, 137 insertions(+), 87 deletions(-) diff --git a/.github/workflows/accessibility.yml b/.github/workflows/accessibility.yml index 501cfd4..7e3db93 100644 --- a/.github/workflows/accessibility.yml +++ b/.github/workflows/accessibility.yml @@ -28,7 +28,7 @@ jobs: node-version: 22 # In your own repository this becomes: - # uses: likeBloodMoon/eaa-kit@v0.4.0 + # uses: likeBloodMoon/eaa-kit@v0.5.0 # # An exact release tag. There is deliberately no moving v0 tag to follow: # this is a 0.x package, the flags and the JSON contract can still move diff --git a/CHANGELOG.md b/CHANGELOG.md index be29c20..4b19652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,113 @@ move: the JSON report's `schemaVersion` and the baseline file's. Both are bumped a field is removed, renamed, or changes meaning — new fields may appear without one, so consumers must ignore what they do not recognise. -## Unreleased +## 0.5.0 — 2026-09-01 + +### Added + +- **Statements for Spain, France, Italy and the Netherlands**, in each country's own + language as well as English. Three countries was the DACH region; the tool is named after + a directive that applies across the EU, and these four are the largest markets it applies + in. + + Each is a document under its own law rather than a translation of the Austrian one, which + is the rule Switzerland already set here. `ES` names Ley 11/2023; `FR` the ordonnance + n° 2023-859 and article 47 of loi n° 2005-102; `IT` the d.lgs. 82/2022 that amended the + legge Stanca; `NL` the Implementatiewet toegankelijkheidsvoorschriften producten en + diensten. Where a national regime prescribes a declaration with a form of its own — the + RGAA declaration and multi-year plan in France, the dichiarazione filed on AgID's model in + Italy, RD 1112/2018 for the Spanish public sector — the template says so, rather than + letting a generated file look as though it discharges the obligation. Where supervision is + genuinely split, between the Spanish state and the autonomous communities or across six + Dutch authorities, it says that too rather than naming one body and sounding certain. + + The language matrix is sparse on purpose: a country has the language its law is + administered in, and English. `--country AT --lang fr` is an error naming the languages + Austria does have, not a fall back to another language — a legal document published + quietly in a language its readers may not have is worse than a run that stops. + +- **Audit defaults in `eaa.config`.** The config file has served the statement alone since + 0.2, so a project that audits with the same six flags on every run had nowhere to write + them down but the build script that repeats them. An `audit` block is that list, said + once, and `baseline` reads the keys that mean the same thing to it. + + Everything in it is a default and every typed flag beats it: the file is what a project + usually wants, and a flag is what somebody wants on this run. A block nobody could + override would make a one-off `--browser` check impossible without editing a committed + file. The block is read by a schema of its own, so a project wanting audit defaults does + not have to write a whole statement config to get them; `--config ` names a file + instead of searching for one. No config file at all still runs exactly as before. + +- **`--fast`**, which skips the rules the browserless engine cannot decide rather than + running them and discarding the answer. Colour contrast is computed against a stylesheet + jsdom never fetched and target size against boxes that are all 0x0; both verdicts are + thrown away as untrustworthy, and the work to produce them is not cheap — colour contrast + is the most expensive rule axe-core has. Skipping the set is 14-19% of a page and 8-10% + of a whole run once start-up is counted. + + The verdict does not move. Every skipped rule is still reported as not evaluated with the + same reason, no skipped rule ever becomes a pass, and the coverage view is + criterion-for-criterion identical — all three asserted. What is given up is the element + list: a rule that ran can name the elements it could not decide, which are the ones a + person then checks by hand, and a rule that never ran cannot. That is the whole of the + trade, which is why it is a flag and not the default. No effect under `--browser`, which + can decide those rules for real, and it says so rather than ignoring the flag quietly. + + Available to the build plugins as `fast` and to the GitHub Action as the `fast` input, + which is where most runs of this tool happen: a flag the changelog sells and CI cannot + reach is not a feature anyone has. + +### Changed + +- **The baseline file's `schemaVersion` moves to 2** and the JSON report's to 2, because + `fingerprint` changed meaning in both. Neither is read across the boundary: a baseline + written by 0.4.0 records identities under the old rule, and matching them against the new + one would suppress nothing while looking as though it had, so it is refused with the + command that rewrites it. `diff` likewise refuses to compare a 0.4.0 report against a + newer one — that comparison is precisely the one that reports every document-level + barrier as both new and fixed. SARIF's partial fingerprint key moves to `eaaKit/v2` for + the same reason, which is what tells code scanning these are a new scheme rather than + defects that moved. + + Re-record a baseline with `eaa-kit baseline`, and read the new file before committing it: + it lists what this run found, which is not necessarily what the old one accepted. + +- **`--browser` audits four pages at once instead of one.** This runner took pages strictly + one at a time while the browserless one had a whole measured worker pool, which had it + backwards: the browser is the slow engine, and it spends most of a page waiting on the + stylesheets and images it fetches rather than on the CPU. Over 24 pages of a styled site, + 17.9 s became 7.7 s. `--concurrency` now sets this too — threads without `--browser`, + open tabs with it — and its help text says so. Four is the default because each open page + holds a document tree, its decoded images and its own copy of axe-core, so Chromium's + memory is the limit rather than cores. Results are placed by position rather than pushed + as they arrive, and a test asserts that runs at 1, 4 and 8 tabs produce the same report + page for page. + +- **A run with nothing to fix no longer reads the project's source.** The component index + exists to answer one question — which file a failing element was written in — and a clean + run never asks it, but it was built anyway before either report rendered. Measured on a + 1500-file project auditing one clean page: ~450 ms and ~20 MB spent on lookups that never + happened, scaling with the source tree rather than with anything the run did. It is now + built only when there is an element to attribute. + +- The per-page timeout constant moved to `result.ts` so the worker pool can read it without + pulling 630 ms of jsdom into a module that deliberately avoids it — the same move + `ENGINE_BLIND_RULES` already made. + +- **Compiled bytecode is reused between runs.** Importing jsdom is ~700 ms and axe-core + another ~130 ms to import and compile, and V8 paid to compile both from source on every + invocation of a CLI that build scripts run over and over against unchanged code. Node's + compile cache is enabled in the CLI entry and again in the audit worker, since the cache + is per-thread and each worker compiles its own copy. A one-page audit goes 1397 ms to + 1256 ms; a fifty-page one is unchanged, which is the expected shape for a fixed cost. + Best-effort: a read-only or sandboxed cache directory makes a run slightly slower and + nothing else. + +- **`--fail-on` and `--format` no longer carry a commander default.** Commander writes a + default into the parsed options whether or not the flag was typed, which would have + silently overruled the config file on the two flags most worth putting in it. Both still + default in the command itself, to the same values the help text names, so nothing about a + run changes. ### Fixed @@ -66,25 +172,6 @@ consumers must ignore what they do not recognise. - One unreachable page was counted in the singular and conjugated in the plural: "1 page could not be reached, and were not audited". -### Changed - -- **The baseline file's `schemaVersion` moves to 2** and the JSON report's to 2, because - `fingerprint` changed meaning in both. Neither is read across the boundary: a baseline - written by 0.4.0 records identities under the old rule, and matching them against the new - one would suppress nothing while looking as though it had, so it is refused with the - command that rewrites it. `diff` likewise refuses to compare a 0.4.0 report against a - newer one — that comparison is precisely the one that reports every document-level - barrier as both new and fixed. SARIF's partial fingerprint key moves to `eaaKit/v2` for - the same reason, which is what tells code scanning these are a new scheme rather than - defects that moved. - - Re-record a baseline with `eaa-kit baseline`, and read the new file before committing it: - it lists what this run found, which is not necessarily what the old one accepted. - -## Unreleased - -### Fixed - - **The per-page timeout could not stop the thing it was written for.** It is a `Promise.race`, and a race cannot interrupt synchronous work: neither jsdom's parse nor axe-core's walk of the tree yields, so the timer meant to stop them never gets to run. @@ -113,60 +200,6 @@ consumers must ignore what they do not recognise. They now share the request timeout, refuse a redirect that leaves the origin as `fetchPage` already does, and are capped like any other body. -### Changed - -- **`--browser` audits four pages at once instead of one.** This runner took pages strictly - one at a time while the browserless one had a whole measured worker pool, which had it - backwards: the browser is the slow engine, and it spends most of a page waiting on the - stylesheets and images it fetches rather than on the CPU. Over 24 pages of a styled site, - 17.9 s became 7.7 s. `--concurrency` now sets this too — threads without `--browser`, - open tabs with it — and its help text says so. Four is the default because each open page - holds a document tree, its decoded images and its own copy of axe-core, so Chromium's - memory is the limit rather than cores. Results are placed by position rather than pushed - as they arrive, and a test asserts that runs at 1, 4 and 8 tabs produce the same report - page for page. - -- **A run with nothing to fix no longer reads the project's source.** The component index - exists to answer one question — which file a failing element was written in — and a clean - run never asks it, but it was built anyway before either report rendered. Measured on a - 1500-file project auditing one clean page: ~450 ms and ~20 MB spent on lookups that never - happened, scaling with the source tree rather than with anything the run did. It is now - built only when there is an element to attribute. - -- The per-page timeout constant moved to `result.ts` so the worker pool can read it without - pulling 630 ms of jsdom into a module that deliberately avoids it — the same move - `ENGINE_BLIND_RULES` already made. - -## Unreleased - -### Added - -- **`--fast`**, which skips the rules the browserless engine cannot decide rather than - running them and discarding the answer. Colour contrast is computed against a stylesheet - jsdom never fetched and target size against boxes that are all 0x0; both verdicts are - thrown away as untrustworthy, and the work to produce them is not cheap — colour contrast - is the most expensive rule axe-core has. Skipping the set is 14-19% of a page and 8-10% - of a whole run once start-up is counted. - - The verdict does not move. Every skipped rule is still reported as not evaluated with the - same reason, no skipped rule ever becomes a pass, and the coverage view is - criterion-for-criterion identical — all three asserted. What is given up is the element - list: a rule that ran can name the elements it could not decide, which are the ones a - person then checks by hand, and a rule that never ran cannot. That is the whole of the - trade, which is why it is a flag and not the default. No effect under `--browser`, which - can decide those rules for real, and it says so rather than ignoring the flag quietly. - -### Changed - -- **Compiled bytecode is reused between runs.** Importing jsdom is ~700 ms and axe-core - another ~130 ms to import and compile, and V8 paid to compile both from source on every - invocation of a CLI that build scripts run over and over against unchanged code. Node's - compile cache is enabled in the CLI entry and again in the audit worker, since the cache - is per-thread and each worker compiles its own copy. A one-page audit goes 1397 ms to - 1256 ms; a fifty-page one is unchanged, which is the expected shape for a fixed cost. - Best-effort: a read-only or sandboxed cache directory makes a run slightly slower and - nothing else. - ## 0.4.0 — 2026-09-01 ### Added diff --git a/docs/baseline.md b/docs/baseline.md index e9bf61c..4e581a9 100644 --- a/docs/baseline.md +++ b/docs/baseline.md @@ -75,11 +75,11 @@ document-level rule such as `html-has-lang` survives an edit elsewhere on the pa Entries are sorted, so the file diffs cleanly and two people regenerating it get the same result. -A baseline written by an earlier version is refused rather than read: `schemaVersion` 1 -recorded fingerprints under the old rule, and matching them against the new one would -suppress nothing while looking as though it had. Record it again with `eaa-kit baseline`, -then read the new file before committing it — it lists what this run found, which is not -necessarily what the old one accepted. +A baseline written by an earlier version is refused rather than read: `schemaVersion` 1, +which is what 0.4.0 and older wrote, recorded fingerprints under the old rule, and matching +them against the new one would suppress nothing while looking as though it had. Record it +again with `eaa-kit baseline`, then read the new file before committing it — it lists what +this run found, which is not necessarily what the old one accepted. ## In code scanning diff --git a/docs/integrations.md b/docs/integrations.md index c2d53c5..147b91b 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -246,7 +246,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 22 - - uses: likeBloodMoon/eaa-kit@v0.4.0 + - uses: likeBloodMoon/eaa-kit@v0.5.0 with: install-command: npm ci build-command: npm run build diff --git a/docs/reports.md b/docs/reports.md index 4f720c3..d674663 100644 --- a/docs/reports.md +++ b/docs/reports.md @@ -17,6 +17,23 @@ shareable ones. - Rule ids, WCAG success criteria and EN 301 549 clauses come from axe-core and may change when its major version changes; `tool.axeCore` records which version produced the report. +#### Coming from 0.4.0 + +`schemaVersion` moved from `1` to `2` in 0.5.0, because [`fingerprint`](#fingerprint) +changed meaning: it hashed the failing element's whole outer markup and now hashes its +opening tag. Three things follow, and none of them is silent. + +- **`eaa-kit diff` refuses a 0.4.0 report against a newer one.** That comparison is exactly + the one that reports every document-level barrier as both new and fixed. Regenerate the + earlier report from the same commit if you need the comparison. +- **A 0.4.0 baseline is refused** rather than read, with the command that rewrites it. See + [baselines](baseline.md). +- **SARIF's partial fingerprint key is now `eaaKit/v2`.** Code scanning treats the two as + different schemes, which is what stops it reading a re-identified alert as a defect that + moved. + +A consumer that reads the JSON report and does not touch `fingerprint` needs no change. + Deliberately **not** in the document, and not coming later: absolute filesystem paths (they leak the build machine into anything you commit), per-page timings (they would make two reports of the same build differ), and raw axe-core tags (promising those would tie diff --git a/examples/report.html b/examples/report.html index c77c5a3..45ae5ea 100644 --- a/examples/report.html +++ b/examples/report.html @@ -3,7 +3,7 @@ - + Accessibility audit · tests/fixtures/site