diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf32b7..2ed4cc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,64 @@ 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.9.0 + +### Added + +- **`init` sets up the baseline and the CI workflow as well as the config.** After the + config it offers a baseline, when a built site is already there, and points the + config's `audit` block at it. It also offers a GitHub Actions workflow, when the project + is in a git repository. The workflow is written for the project: package manager, build + script, build directory, baseline, `working-directory` in a monorepo, and the action + pinned to this release. An existing workflow is never overwritten. `--no-baseline` and + `--no-ci` turn either offer off. +- **Errors say what to type next.** The exit-2 paths a new user is likely to meet end with + the command that fixes them, on a line of their own: + - a missing config points at `eaa-kit init`; + - a language the country does not have points at `--lang` with one it does; + - an unknown country points at `eaa-kit countries`; + - a missing report, baseline or review record points at the command that writes one; + - a mistyped flag points at that command's `--help`. + + Errors carry this as a `next` field, so the message still says only what went wrong. +- **Five more statement templates: Belgium in German, and Czechia, Denmark, Finland and + Sweden.** Each new country has its own language and English; `cs`, `da`, `fi` and `sv` + are new `--lang` values. That makes fifteen countries and thirty-one templates. + `init` now reads `sv`, `da` and `cs` as their countries too. + + **Given up, as in 0.8.0:** these citations come from regulators' pages, government + portals and law firms, because the official gazettes could not be reached. They are + marked unverified and cite no article numbers and no fines. Finland has no Swedish + rendering yet. + +- **Redirects are found before the crawl, and a redirect to another site is never + followed without agreement.** The entry URL is followed one hop at a time first. When + it leads to another site, as `www.gtainside.de` does to `www.gtainside.com`, the new + `--redirects` option decides what happens: + - `ask`, the default, asks at the terminal, and stops when there is nobody to ask; + - `follow` goes on; + - `stop` never does. + + Stopping names the two commands that go on. A redirect within the same site (`www.`, + http to https) is followed without a question. A followed redirect is recorded in every + report: two console lines above the counts, a *Redirected* row in the HTML report, + `completeness.entryRedirect` in the JSON report and an `entryRedirect` property in SARIF. + There is a `redirects` config key and a `redirects` input on the GitHub Action. +- **A sign-in wall stops the run instead of being audited as the site.** It is caught + from evidence the site gives: a 401 or 403, a redirect to an identity provider, a + redirect to a sign-in path, or a redirect to a page with a password field. The run + exits 2 with the credentials flag to use. Credentials are no longer sent past a hop + that leaves the entry's origin. Pages that all land on one address during the crawl are + now called a sign-in page when that page has a password field. + +### Changed + +- A crawl whose entry URL redirects to another site used to fail every page as + "redirected off" and end with "Could not fetch". It now stops before crawling, says + where the site went, and names the commands to go on. The exit code is 2 either way. +- The baseline and review-record errors no longer put their fix in the message text. It + moved to the error's `next` field, which the CLI prints under the message. + ## Unreleased — 0.8.0 ### Added diff --git a/README.md b/README.md index 0e886d2..6cf3cce 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Build-time WCAG 2.2 AA auditor and EU accessibility statement generator for stat built for the freelancers and small agencies who have to comply with the European Accessibility Act (in force since 28 June 2025) without an accessibility budget. It started in the DACH region — the BFSG in Germany, the BaFG in Austria — and the statement now names -the statute and supervisory body of **eleven countries**: Austria, Belgium, Germany, -Switzerland, Spain, France, Ireland, Italy, the Netherlands, Poland and Portugal, each in -its own language as well as English. +the statute and supervisory body of **fifteen countries**: Austria, Belgium, Czechia, +Denmark, Finland, France, Germany, Ireland, Italy, the Netherlands, Poland, Portugal, +Spain, Sweden and Switzerland, each in its own language as well as English. 0.7.0 makes a run cost what it should. A page that has not changed byte for byte is not audited again, and a run with nothing to re-audit never loads an engine at all: twenty @@ -28,8 +28,8 @@ Sites behind a login or a preview protection are auditable too. npx eaa-kit # nothing to set up: finds your site, audits it, writes a report npx eaa-kit audit # WCAG 2.2 AA report; finds your build itself npx eaa-kit diff a.json b.json # what a change made worse, and what it fixed -npx eaa-kit init # write an eaa.config.json -npx eaa-kit statement # accessibility statement, in one of eleven countries +npx eaa-kit init # the config, a baseline and a CI workflow +npx eaa-kit statement # accessibility statement, in one of fifteen countries npx eaa-kit countries # which ones, in which languages, under which law npx eaa-kit checklist # the manual review no engine can do for you ``` @@ -75,7 +75,7 @@ listing the barriers a real audit found. ```bash eaa-kit statement --output src/content/a11y.md -eaa-kit statement --country PL --lang pl # eaa-kit countries lists all eleven +eaa-kit statement --country PL --lang pl # eaa-kit countries lists all fifteen ``` Each country's statement is a document under its own law rather than a translation of @@ -193,7 +193,7 @@ eaa-kit audit --url https://preview.example.com --basic-auth user:password | --- | --- | | [Auditing a build](docs/audit.md) | The `audit` command, both engines, exit codes, and what an automated run can and cannot tell you | | [Defaults from eaa.config](docs/audit.md#defaults-from-eaaconfig) | Writing the flags down once, and what still overrides them | -| [The statement command](docs/statement.md) | The config file, the eleven countries, and filling a statement from audit results | +| [The statement command](docs/statement.md) | The config file, the fifteen countries, and filling a statement from audit results | | [Baselines](docs/baseline.md) | Adopting the tool on a site that already has violations | | [Comparing two runs](docs/reports.md#comparing-two-runs) | The `diff` command, and what it refuses to call fixed | | [Coverage of WCAG](docs/audit.md#how-much-of-wcag-a-run-reaches) | What an automated engine can reach at all, and what it cannot | diff --git a/ROADMAP.md b/ROADMAP.md index 4b967af..8765a46 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,12 +19,105 @@ having to learn the tool first. Three releases get there: a result made short enough to use while working rather than after. - **0.9.0 — the first ten minutes.** Everything a new user meets before their first useful result: `init` that sets up CI and a baseline as well as a config, errors that say what to - type next, and a German rendering for Belgium's third language community. The countries - after this release's four — Sweden, Denmark, Finland, Czechia — go here, with the same rule. + type next, a German rendering for Belgium's third language community, and Sweden, + Denmark, Finland and Czechia. - **1.0.0 — the promise.** The JSON report, the review record, the baseline and the config file frozen as documented contracts under semver, with a migration note for anything that changed on the way. No new surface: 1.0 is 0.9 with the guarantees written down. +## 0.9.0 — the first ten minutes + +0.8.0 made the first command do the useful thing. 0.9.0 is about the next few: the ones a +new user runs after the first report, and what they read when one of those goes wrong. + +### 1. `init` sets up the project, not only the config + +A config file is one of three things a project needs before the tool is doing its job. The +other two are a baseline, so CI fails on new barriers rather than on every existing one, +and the CI job itself. `init` now offers both after writing the config: + +- **A baseline**, when there is a built site to record it from. `init` never runs a build + to get one. Recording it says how many barriers it accepts, the config's `audit` block + points at it so a local `eaa-kit audit` reads it too, and the accepted barriers are still + reported on every run, as they always have been. +- **A GitHub Actions workflow** at `.github/workflows/accessibility.yml`, when the project + is in a git repository. It is written for this project: the package manager from the + lockfile, the build script if there is one, the build directory `init` found, the + baseline if one was just recorded, and the action pinned to this exact release. + +The refusals: an existing workflow file is never overwritten. Nothing is set up that was +not offered. `--no-ci` and `--no-baseline` answer for a script. With `--yes` the defaults +apply, which means a workflow only inside a git repository and a baseline only when a +build is already there. + +### 2. Errors say what to type next + +Every exit-2 path a new user is likely to hit ends with the command that fixes it: + +- No config: `eaa-kit init`. +- No template in that language: `--lang` with the languages the country has. +- An unknown country: `eaa-kit countries`. +- A missing or outdated report, baseline or review record: the command that writes one. +- A mistyped flag: the command's `--help`. + +The rule is the one `start` already follows. An error that the reader cannot act on +without opening the docs is half an error. + +### 3. Belgium in German, and four more countries: Sweden, Denmark, Finland, Czechia + +| | Statute | Supervision named | Languages | +| --- | --- | --- | --- | +| `BE` | as 0.8.0 | as 0.8.0, in German | `de` added | +| `CZ` | Zákon č. 424/2023 Sb., o požadavcích na přístupnost některých výrobků a služeb | Česká obchodní inspekce (ČOI) | `cs`, `en` | +| `DK` | Lov nr. 801 af 7. juni 2022 om tilgængelighedskrav for produkter og tjenester | Sikkerhedsstyrelsen for e-commerce; supervision is split | `da`, `en` | +| `FI` | Laki digitaalisten palvelujen tarjoamisesta (306/2019), as amended for the Directive | Traficom | `fi`, `en` | +| `SE` | Lag (2023:254) om vissa produkters och tjänsters tillgänglighet | Post- och telestyrelsen (PTS) | `sv`, `en` | + +**Written from secondary sources, like 0.8.0's four.** The official gazettes are still not +reachable from where this work is done, and the decision was to go ahead rather than wait. +All five are marked unverified in the registry, in `eaa-kit countries` and in the docs. +Their citations are kept to the statute and the supervisor, with no article numbers and +no fines, for the same reason as 0.8.0's. Nine countries' citations now need checking +against the primary text before the release that carries them is tagged. + +Finland's Swedish rendering is not in this release. Swedish is an official language there +and the law exists in Swedish, but a Finnish statement in Swedish is a document of its +own, and it waits for a source text. + +### 4. Redirects and sign-in walls, found before the crawl + +Two things put a crawl somewhere other than where it was sent. Before this release, one +was found too late and the other only by guessing. + +- **A redirect to another site.** `https://www.gtainside.de` answers with a 301 to + `https://www.gtainside.com`. The crawl refused every page as "redirected off" and ended + on an error that did not say what to do. Now the entry is followed one redirect at a + time before the crawl, and a redirect to another site stops the run with where it went + and the two commands that go on: audit the destination, or `--redirects follow`. In a + terminal the default, `ask`, puts the question instead. A redirect within the same site + (`www.`, http to https) is followed without a question. Every redirect the run followed + is written into all four report formats, because a reader who asked for one site and + is reading about another has to find that out before the first finding. +- **A sign-in wall.** A 401 or 403, a redirect to an identity provider, a redirect to a + page that is a sign-in page by name, or a redirect to a page with a password field + stops the run with exit 2 and the credentials flag to use. Before, the run audited the + login form and reported it as the site. The weaker signal found during the crawl, many + pages landing on one address, is now called a sign-in page when that page has a + password field, and "looks like one" otherwise. + +The refusals: a redirect to another site is never followed without a flag or a yes. The +destination passes the same `--allow-remote` gate as the entry. Credentials are only sent +while the redirect chain stays on the entry's origin. A redirect the run did not follow +produces no report, because a report about the wrong site is the failure being +prevented. + +### Done means + +- `lint`, `typecheck`, `test` (with colour forced as well as without), `smoke` and the + packaged-CLI run green across the CI matrix. +- The citation check for 0.8.0's four countries and this release's five, recorded in the + changelog, before either version is tagged. + ## 0.8.0 — reach 0.7.0 made a run cost what it should. 0.8.0 spends that on two things: getting the tool to diff --git a/action.yml b/action.yml index 3238565..7233866 100644 --- a/action.yml +++ b/action.yml @@ -39,6 +39,14 @@ inputs: link following alone finds only what the navigation links to. required: false default: '' + redirects: + description: >- + What to do when "url" redirects to another site: ask, follow or stop. + A workflow has nobody to ask, so ask behaves as stop there, and the run + fails with where the site went. Set follow to go on and have the SARIF + log record the redirect. Only used with "url". + required: false + default: '' max-pages: description: Stop the crawl after this many pages. required: false @@ -177,6 +185,7 @@ runs: EAA_URL: ${{ inputs.url }} EAA_ALLOW_REMOTE: ${{ inputs.allow-remote }} EAA_SITEMAP: ${{ inputs.sitemap }} + EAA_REDIRECTS: ${{ inputs.redirects }} EAA_MAX_PAGES: ${{ inputs.max-pages }} EAA_FAIL_ON: ${{ inputs.fail-on }} EAA_SARIF: ${{ inputs.sarif-file }} @@ -203,6 +212,9 @@ runs: if [ -n "$EAA_SITEMAP" ]; then args+=(--sitemap "$EAA_SITEMAP") fi + if [ -n "$EAA_REDIRECTS" ]; then + args+=(--redirects "$EAA_REDIRECTS") + fi if [ -n "$EAA_MAX_PAGES" ]; then args+=(--max-pages "$EAA_MAX_PAGES") fi diff --git a/docs/audit.md b/docs/audit.md index ee8ba06..8035b82 100644 --- a/docs/audit.md +++ b/docs/audit.md @@ -88,6 +88,42 @@ a crawl that stopped early says so. | `--max-depth ` | how far from the entry URL to follow links (default 3) | | `--allow-remote` | crawl a host that is not localhost | | `--ignore-robots` | crawl paths `robots.txt` disallows | +| `--redirects ` | when the URL [redirects to another site](#when-the-url-redirects-somewhere-else): `ask` (default), `follow` or `stop` | + +### When the URL redirects somewhere else + +Before crawling, the entry URL is fetched one redirect at a time, so the run knows where it +really leads before it audits anything. A site that redirects to another one is the +case this is for: + +``` +$ eaa-kit audit --url https://www.gtainside.de --allow-remote +error https://www.gtainside.de/ redirects to https://www.gtainside.com/ (301), which is a + different site, so nothing was audited. + → eaa-kit audit --url https://www.gtainside.com/ --allow-remote audit the address it leads to + → eaa-kit audit --url https://www.gtainside.de/ --allow-remote --redirects follow or follow it on every run +``` + +- **`ask`**, the default, asks at the terminal whether to audit the new address instead. + With nobody at the terminal, in CI for example, the answer is no, and the run stops as + above. +- **`follow`** goes on to the new address without asking. +- **`stop`** never goes on, not even to the same site at another address. + +A redirect within the same site is followed under `ask` and `follow` without a question. +The same site means the same host, give or take a leading `www.`, over http or https. + +**A redirect the run followed is in every report.** Four report formats say it: + +- the console report lists both addresses above the counts; +- the HTML report has a *Redirected* row under what was audited; +- the JSON report records it as `completeness.entryRedirect`; +- the SARIF log records it as the `entryRedirect` run property. + +A reader who asked for one site and is reading about another finds that out before the +first finding. The destination has to pass the same `--allow-remote` gate as the entry, so +a local URL cannot redirect its way onto the internet. `redirects` in the config's `audit` +block sets the mode for every run. ### Sites behind a login or a preview protection @@ -108,25 +144,39 @@ three, and a crawl that loses the sitemap quietly audits less. Under `--browser` on the browser context, so stylesheets and images load too; a protected page audited without its CSS is a page audited wrong. -**A sign-in page in front of the site is caught rather than audited.** The dangerous shape -is not the one that answers 401 — that fails loudly. It is the site whose unauthenticated -requests are *redirected* to a login form, which answers 200: every request succeeds, and a -tool that is not looking will audit the login page and report it as the site. When several -requested URLs all answer at one address, or a whole crawl comes back as the single page it -was redirected to, the run says so and records those URLs as pages it never reached — so the -report cannot come back complete: +**A sign-in page in front of the site stops the run instead of being audited.** A tool +that is not looking audits the login form, reports it as the site, and hands back a clean +result for pages it never saw. So the entry URL is checked before the crawl, and the run +stops, with exit 2, when any of these shows that a sign-in wall is there: + +- the server answers **401** or **403**; +- the entry redirects to a **known identity provider**, such as Google, Microsoft, Okta, + Auth0, Cognito or Apple; +- it redirects to a page that is a **sign-in page by name**: `/login`, `/sign-in`, `/sso`, + `/wp-login.php`, `/users/sign_in`, `/anmelden` and the like; +- it redirects to a page with a **password field** on it. ``` -warning Every page requested answered at https://staging.example.com/login, which is not - where it was asked. - A sign-in page in front of the site looks like this. - If it is behind one, pass --basic-auth user:password or --header "Cookie: …". +error https://staging.example.com/ is behind a sign-in wall: it sends visitors to a + sign-in page at https://staging.example.com/users/sign_in. + eaa-kit will not audit a sign-in form as though it were the site, so nothing was audited. + → eaa-kit audit --url https://staging.example.com/ --header "Cookie: " ``` +A 401 points at `--basic-auth` instead. When credentials were sent and the wall is still +there, the message says they were not accepted. On the way through the redirects, the +credentials are only sent while the chain stays on the entry's origin, so a hop to an +identity provider never receives them. + +The walls that only show once the crawl is under way are still caught: several requested +pages all answering at one address, or a whole crawl coming back as the one page it was +redirected to. Those pages are recorded as never reached, so the report cannot come back +complete. When the page they landed on has a password field, the warning and the report +call it a sign-in page. Otherwise they say it only looks like one, because a one-page +site looks the same from here. + Ordinary redirects are left alone: a locale prefix on the entry, trailing-slash -normalisation, anything where each URL lands somewhere of its own. A redirect that leaves -the origin — an identity provider, as Cloudflare Access uses — was already refused, since -auditing somebody else's sign-in page is not auditing your site. +normalisation, anything where each URL lands somewhere of its own. **These are credentials, and the tool never writes one down.** Nothing reaches a report, a baseline, a SARIF log or the completeness record, and a malformed `--header` is reported by @@ -313,7 +363,7 @@ the schema is required by `statement`, which is the command that publishes a doc | Key | Same as | | --- | --- | | `dir` | the positional argument, which wins over it | -| `include`, `exclude`, `baseUrl`, `url`, `sitemap`, `maxPages`, `maxDepth` | the flags of those names | +| `include`, `exclude`, `baseUrl`, `url`, `sitemap`, `redirects`, `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 | diff --git a/docs/integrations.md b/docs/integrations.md index 3e94f39..44d741a 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -278,6 +278,7 @@ watching is the wrong default for something whose job is to fail that build. | `sarif-file` | `eaa-kit.sarif` | Where to write the SARIF log | | `upload-sarif` | `true` | Upload to GitHub code scanning | | `sitemap` | — | Where the site lists its pages, if not `/sitemap.xml`; with `url` only | +| `redirects` | — | When `url` redirects to another site: `ask`, `follow` or `stop`. A workflow has nobody to ask, so the default stops the run; `follow` goes on and records the redirect in the SARIF log | | `baseline` | — | Path to a baseline file; fail only on violations it does not list | | `headers` | — | Extra request headers for a protected site, one `Name: value` per line. Read from `secrets` | | `basic-auth` | — | `user:password` for a site behind basic auth. Read from `secrets` | diff --git a/docs/reports.md b/docs/reports.md index a03f90b..d27f8b7 100644 --- a/docs/reports.md +++ b/docs/reports.md @@ -240,6 +240,14 @@ verdict; `audited` alone is what was measured today, which is what a consumer de current a report is needs. Reuse does not make a run incomplete — `complete` answers whether any of the site was missed, and nothing was. +`entryRedirect` is there only when the crawl's entry URL redirected to another address and +the run followed it: `requested` is the URL the run was given, `auditedFrom` is where it +started instead, `statuses` lists each hop's HTTP status, and `followedBecause` is +`same-site`, `flag` or `prompt`. Every page in the report belongs to `auditedFrom`, so a +consumer comparing reports over time should check it before treating two runs as the same +site. A redirect the run did not follow never reaches a report, because that run stops +first. + `completeness` was added without moving `schemaVersion`: new fields may appear without a bump, and consumers must ignore what they do not recognise. A consumer written against an earlier version that has never seen this field should treat its absence diff --git a/docs/statement.md b/docs/statement.md index f3742b6..8879a42 100644 --- a/docs/statement.md +++ b/docs/statement.md @@ -17,8 +17,8 @@ eaa-kit statement --review eaa-review.json # say what a person checked | Flag | Default | Meaning | | --- | --- | --- | | `--config ` | searched for | Path to the config file | -| `--lang ` | from `site.locale` | `de`, `en`, `es`, `fr`, `it`, `nl`, `pl` or `pt` — see the table below for which countries have which | -| `--country ` | from `enforcement.country` | `AT`, `BE`, `CH`, `DE`, `ES`, `FR`, `IE`, `IT`, `NL`, `PL` or `PT` — `eaa-kit countries` lists them | +| `--lang ` | from `site.locale` | `cs`, `da`, `de`, `en`, `es`, `fi`, `fr`, `it`, `nl`, `pl`, `pt` or `sv` — see the table below for which countries have which | +| `--country ` | from `enforcement.country` | `AT`, `BE`, `CH`, `CZ`, `DE`, `DK`, `ES`, `FI`, `FR`, `IE`, `IT`, `NL`, `PL`, `PT` or `SE` — `eaa-kit countries` lists them | | `--audit ` | — | A report from `eaa-kit audit --format json`; its violations are listed as non-accessible content | | `--review ` | — | A [review record](review.md); the statement says how many criteria a person checked, and refuses a false claim of conformance | | `--format ` | from `--output` | `markdown` or `html` | @@ -38,11 +38,25 @@ enforcement procedure, and when the statement was prepared. eaa-kit init ``` -Asks for the few things it cannot work out — who is answerable for the site, where -feedback goes, whose law applies — takes the site name and URL from `package.json` where -they are stated, and writes an `eaa.config.json` the loader accepts. It refuses to -overwrite a config that is already there without `--force`, and `--yes` takes every -default without asking. +Asks for the few things it cannot work out: who is answerable for the site, where +feedback goes, and whose law applies. It takes the site name, URL and language from what +the project and its built site already state, and writes an `eaa.config.json` the loader +accepts. It refuses to overwrite a config that is already there without `--force`, and +`--yes` takes every default without asking. + +Then it offers the other two things a project needs: + +- **A baseline**, when a built site is already there. `init` never runs a build to get + one. The config's `audit` block points at the baseline, so `eaa-kit audit` reads it + without being told. +- **A GitHub Actions workflow** at `.github/workflows/accessibility.yml`, when the project + is in a git repository. It is written for the project: the package manager from the + lockfile, the `build` script, the build directory, the baseline, and the action pinned + to the release that wrote it. It sits in a subdirectory's `working-directory` when the + project is below the repository root. + +An existing workflow is never overwritten. `--no-baseline` and `--no-ci` keep `init` from +offering either one. What it writes is `partially-compliant`, never `compliant`. The file is written before any audit has run, and a statement claiming full conformance for a site nobody has assessed is @@ -107,7 +121,7 @@ export default defineConfig({ ], }, enforcement: { - country: 'AT', // AT, BE, CH, DE, ES, FR, IE, IT, NL, PL or PT + country: 'AT', // any code eaa-kit countries lists }, }) ``` @@ -162,24 +176,28 @@ is the French statement, and there is no French rendering of the Austrian one. | `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 | -| `BE` † | `fr`, `nl`, `en` | Code de droit économique / Wetboek van economisch recht, as amended by the law of 5 November 2023 | [SPF Économie / FOD Economie](https://economie.fgov.be), Economic Inspection, and says supervision is split | +| `BE` † | `fr`, `nl`, `de`, `en` | Code de droit économique / Wetboek van economisch recht, as amended by the law of 5 November 2023 | [SPF Économie / FOD Economie](https://economie.fgov.be), Economic Inspection, and says supervision is split | | `IE` † | `en` | European Union (Accessibility Requirements of Products and Services) Regulations 2023 (S.I. No. 636 of 2023) | [CCPC](https://www.ccpc.ie); ComReg and the Central Bank for their sectors | | `PL` † | `pl`, `en` | Ustawa z dnia 26 kwietnia 2024 r. (Dz.U. 2024 poz. 731) | [Prezes Zarządu PFRON](https://www.pfron.org.pl), who passes e-commerce reports to the minister for digital affairs | | `PT` † | `pt`, `en` | Decreto-Lei n.º 82/2022, de 6 de dezembro | [ANACOM](https://www.anacom.pt) for e-commerce, and says supervision is split | +| `CZ` † | `cs`, `en` | Zákon č. 424/2023 Sb., o požadavcích na přístupnost některých výrobků a služeb | [Česká obchodní inspekce](https://coi.gov.cz) (ČOI) | +| `DK` † | `da`, `en` | Lov nr. 801 af 7. juni 2022 om tilgængelighedskrav for produkter og tjenester | [Sikkerhedsstyrelsen](https://www.sik.dk) for e-commerce, and says supervision is split | +| `FI` † | `fi`, `en` | Laki digitaalisten palvelujen tarjoamisesta (306/2019), as amended for the Directive | [Traficom](https://www.traficom.fi) | +| `SE` † | `sv`, `en` | Lag (2023:254) om vissa produkters och tjänsters tillgänglighet | [Post- och telestyrelsen](https://pts.se) (PTS) | `eaa-kit countries` prints the same list in the terminal, and `--json` prints it for anything that wants to build on it. -† **New in 0.8.0, and checked less than the others.** The statute, the authority and the -enforcement route for these four were established from regulators' own pages, government -portals and law firms. The official gazettes could not be reached when these templates were -written. For that reason these templates cite less than the older seven: no article -numbers and no fine amounts, which are the details a secondary source most often gets wrong. -They will be checked against the primary text before 0.8.0 is released. Until then, read -the enforcement section with that in mind, and if you find something wrong, -[open an issue](https://github.com/likeBloodMoon/eaa-kit/issues). -Belgium has no German rendering yet: the federal law is published in French and Dutch, and -a German one written without a German source text would be a translation. +† **New in 0.8.0 or 0.9.0, and checked less than the others.** The statute, the authority +and the enforcement route for these nine were established from regulators' own pages, +government portals and law firms. The official gazettes could not be reached when these +templates were written. For that reason these templates cite less than the older ones: no +article numbers and no fine amounts, which are the details a secondary source most often +gets wrong. They will be checked against the primary text before the release that carries +them is tagged. Until then, read the enforcement section with that in mind, and if you find +something wrong, [open an issue](https://github.com/likeBloodMoon/eaa-kit/issues). +Finland has no Swedish rendering yet, although Swedish is an official language there: a +Finnish statement in Swedish is a document of its own, and it waits for a source text. 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 diff --git a/package.json b/package.json index 6a667fc..1db67c1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "eaa-kit", "version": "0.7.0", - "description": "WCAG 2.2 AA auditor and EU accessibility statement generator for eleven countries (AT, BE, CH, DE, ES, FR, IE, IT, NL, PL, PT). Audits a static build or a running site from the command line.", + "description": "WCAG 2.2 AA auditor and EU accessibility statement generator for fifteen countries (AT, BE, CH, CZ, DE, DK, ES, FI, FR, IE, IT, NL, PL, PT, SE). Audits a static build or a running site from the command line.", "type": "module", "license": "MIT", "repository": { diff --git a/src/audit/baseline.ts b/src/audit/baseline.ts index a0cee73..6d5bd26 100644 --- a/src/audit/baseline.ts +++ b/src/audit/baseline.ts @@ -1,5 +1,6 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' +import type { NextStep } from '../next.ts' import * as s from '../schema.ts' import { isoDate } from '../text.ts' import { elementFingerprint } from './fingerprint.ts' @@ -74,6 +75,14 @@ export type Baseline = s.Infer export class BaselineError extends Error { override readonly name = 'BaselineError' + + constructor( + message: string, + /** The command that fixes it, printed under the message. */ + readonly next?: NextStep, + ) { + super(message) + } } /** What applying a baseline to a run did. */ @@ -279,9 +288,10 @@ export async function readBaseline(file: string, cwd = process.cwd()): Promise host === provider || host.endsWith(`.${provider}`)) +} + +export function isSignInPath(url: URL): boolean { + return SIGN_IN_PATH.test(url.pathname) +} + +/** A form somebody types a password into. The shape every sign-in page has. */ +export function hasPasswordField(html: string): boolean { + return /]*\btype\s*=\s*["']?password\b/i.test(html) +} + +/** + * Whether two URLs are the same site in the sense a reader means it: the same + * host, give or take a leading `www.`, over http or https. A redirect between + * two of these is the site tidying its own address, not a different site. + */ +export function isSameSite(a: URL, b: URL): boolean { + const host = (url: URL): string => url.hostname.toLowerCase().replace(/^www\./, '') + return host(a) === host(b) && a.port === b.port +} + +export interface ProbeOptions { + fetchImpl?: typeof fetch + timeoutMs: number + /** + * The caller's credentials. Sent only while a hop stays on the entry's + * origin: `fetch` drops an Authorization header on a cross-origin redirect + * for the same reason, and a manual chain has to do it itself. + */ + headers?: Record + maxBodyBytes: number +} + +/** Follow the entry's redirects one at a time, and look at where they end. */ +export async function probeEntry(entry: URL, options: ProbeOptions): Promise { + const impl = options.fetchImpl ?? fetch + const hops: Hop[] = [] + let current = new URL(entry.href) + + for (let hop = 0; hop <= MAX_HOPS; hop += 1) { + let response: Response + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), options.timeoutMs) + try { + response = await impl(current.href, { + signal: controller.signal, + redirect: 'manual', + headers: { + accept: 'text/html,application/xhtml+xml', + 'user-agent': 'eaa-kit', + ...(current.origin === entry.origin ? options.headers : {}), + }, + }) + } catch (cause) { + clearTimeout(timer) + const reason = controller.signal.aborted + ? `timed out after ${options.timeoutMs}ms` + : cause instanceof Error + ? cause.message + : String(cause) + return { hops, final: current, status: 0, error: reason } + } + clearTimeout(timer) + hops.push({ url: current.href, status: response.status }) + + const location = response.headers.get('location') + if (response.status >= 300 && response.status < 400 && location !== null) { + await discard(response) + current = new URL(location, current) + current.hash = '' + continue + } + + const result: EntryProbe = { hops, final: current, status: response.status } + const signIn = await signInEvidence(entry, current, response, options.maxBodyBytes) + if (signIn !== undefined) result.signIn = signIn + await discard(response) + return result + } + + return { + hops, + final: current, + status: 0, + error: `more than ${MAX_HOPS} redirects, which is a loop or close enough to one`, + } +} + +async function signInEvidence( + entry: URL, + final: URL, + response: Response, + maxBodyBytes: number, +): Promise { + if (response.status === 401) return 'http-401' + if (response.status === 403) return 'http-403' + + // Only a redirect can put a sign-in page where the site was asked for. An + // entry that answers at its own address with a form on it may simply be a + // site whose home page has a login box, and that is the site. + if (final.href === entry.href) return undefined + if (isIdentityProvider(final)) return 'identity-provider' + if (isSignInPath(final)) return 'sign-in-path' + if (!response.ok) return undefined + + const type = response.headers.get('content-type') ?? '' + if (!/\b(?:text\/html|application\/xhtml\+xml)\b/i.test(type)) return undefined + const text = await readUpTo(response, maxBodyBytes) + return hasPasswordField(text) ? 'password-field' : undefined +} + +/** + * Let go of a body that was not read, so the connection is released. One that + * was read is locked to its reader and already finished with. + */ +async function discard(response: Response): Promise { + if (response.body === null || response.body.locked) return + await response.body.cancel() +} + +/** A body, cut off at a limit rather than refused: this only looks for a form. */ +async function readUpTo(response: Response, limit: number): Promise { + const reader = response.body?.getReader() + if (reader === undefined) return '' + const decoder = new TextDecoder() + let text = '' + let size = 0 + for (;;) { + const { done, value } = await reader.read() + if (done) break + size += value.byteLength + text += decoder.decode(value, { stream: true }) + if (size >= limit) { + await reader.cancel() + break + } + } + return text +} + +/** What each piece of evidence is, in words, for the message and the report. */ +export function describeSignIn(evidence: SignInEvidence, at: string): string { + switch (evidence) { + case 'http-401': + return `${at} answered 401: it asks for credentials before it shows any page` + case 'http-403': + return `${at} answered 403: it refuses requests that are not signed in` + case 'identity-provider': + return `it sends visitors to sign in at ${new URL(at).origin}` + case 'sign-in-path': + return `it sends visitors to a sign-in page at ${at}` + case 'password-field': + return `it sends visitors to ${at}, a page with a password field on it` + } +} diff --git a/src/audit/report/console.ts b/src/audit/report/console.ts index 9d34236..3604818 100644 --- a/src/audit/report/console.ts +++ b/src/audit/report/console.ts @@ -479,6 +479,21 @@ function completenessLines(ctx: Context): string[] { // below should be read. const reused = reusedPart(completeness) const lines: string[] = reused === undefined ? [] : [line(ctx, ` ${reused}`, ctx.c.dim)] + // Before anything else: every number below is about the site the redirect + // led to, and a reader who asked for another one has to know that first. + const redirect = completeness.entryRedirect + if (redirect !== undefined) { + const why = { 'same-site': 'same site', flag: '--redirects follow', prompt: 'approved' }[ + redirect.followedBecause + ] + // An address per line, so the terminal's width cuts neither of them off. + lines.unshift( + line(ctx, ` Redirected (${redirect.statuses.join(' → ')}, ${why}):`, ctx.c.yellow), + line(ctx, ` from ${redirect.requested}`, ctx.c.yellow), + line(ctx, ` to ${redirect.auditedFrom}`, ctx.c.yellow), + line(ctx, ' Everything below is about the second address.', ctx.c.dim), + ) + } if (completeness.complete) return lines diff --git a/src/audit/report/html.ts b/src/audit/report/html.ts index a31eac3..552ead2 100644 --- a/src/audit/report/html.ts +++ b/src/audit/report/html.ts @@ -1,7 +1,13 @@ import axe from 'axe-core' import { collapse, count, escapeAttribute, escapeText, standardsReference } from '../../text.ts' import { TOOL_VERSION } from '../../version.ts' -import { discoveryLabel, missedParts, type RunCompleteness, reusedPart } from '../completeness.ts' +import { + discoveryLabel, + missedParts, + type RunCompleteness, + redirectPart, + reusedPart, +} from '../completeness.ts' import { type ComponentLocation, componentPath } from '../component.ts' import { buildCoverage, type CriterionCoverage, reviewSummary } from '../coverage.ts' import { byImpactThenRule, type ImpactLevel, impactLabel } from '../impact.ts' @@ -192,6 +198,11 @@ function runDetails( // still has to be able to tell which pages were measured today. const reused = options.completeness ? reusedPart(options.completeness) : undefined if (reused !== undefined) rows.push(['Reused', reused]) + // Second, straight under what was audited: the address the reader typed is + // not the site this document describes, and they will not find that out from + // the list of pages. + const redirected = options.completeness ? redirectPart(options.completeness) : undefined + if (redirected !== undefined) rows.splice(1, 0, ['Redirected', redirected]) const body = rows .map( diff --git a/src/audit/report/sarif.ts b/src/audit/report/sarif.ts index 390b027..a7c06e5 100644 --- a/src/audit/report/sarif.ts +++ b/src/audit/report/sarif.ts @@ -296,6 +296,9 @@ function summaryProperties( pagesErrored: completeness.errored, pagesUnreachable: completeness.unreachable.length, truncated: completeness.truncated, + ...(completeness.entryRedirect === undefined + ? {} + : { entryRedirect: completeness.entryRedirect }), } : {}), } diff --git a/src/audit/review.ts b/src/audit/review.ts index fbfeb13..ee03d43 100644 --- a/src/audit/review.ts +++ b/src/audit/review.ts @@ -1,5 +1,6 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' +import type { NextStep } from '../next.ts' import * as s from '../schema.ts' import { isoDate } from '../text.ts' @@ -76,6 +77,14 @@ export type ReviewRecord = s.Infer export class ReviewError extends Error { override readonly name = 'ReviewError' + + constructor( + message: string, + /** The command that fixes it, printed under the message. */ + readonly next?: NextStep, + ) { + super(message) + } } /** How a review record is applied to a run. */ @@ -246,9 +255,10 @@ export async function readReview(file: string, cwd = process.cwd()): Promise `. @@ -66,7 +66,7 @@ export async function runDiffCommand( diff = diffReports(before, after) } catch (cause) { if (cause instanceof DiffError) { - fail(cause.message) + failWith(cause) return { exitCode: 2 } } throw cause diff --git a/src/cli/index.ts b/src/cli/index.ts index ae4f144..b689406 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -8,6 +8,7 @@ import { DEFAULT_REVIEW_FILE } from '../audit/review.ts' import { COUNTRIES, ConfigError, + REDIRECT_MODES, STATEMENT_LOCALES, type StatementLocale, } from '../config/define.ts' @@ -22,6 +23,7 @@ import { type BaselineFlags, baselineInvocation, fail, + nextStep, note, } from './command.ts' import { DIFF_FORMATS, type DiffCommandOptions, runDiffCommand } from './diff.ts' @@ -173,6 +175,11 @@ program .option('--allow-remote', 'allow --url to crawl a host that is not localhost') .option('--ignore-robots', 'crawl paths robots.txt disallows') .option('--sitemap ', 'where the site lists its pages, if not /sitemap.xml') + .option( + '--redirects ', + `when the URL redirects to another site: ${REDIRECT_MODES.join('|')} (default: ask)`, + oneOf(REDIRECT_MODES), + ) .option('--max-pages ', 'stop the crawl after this many pages', parsePositive) .option('--max-depth ', 'how far from the entry URL to follow links', parseDepth) .option( @@ -229,7 +236,10 @@ program // Refused rather than turned into a poll: a running site changes without // writing anything this process can see. if (invocation.options.url !== undefined) { - fail('--watch watches a build directory, and --url audits a running site.') + fail('--watch watches a build directory, and --url audits a running site.', { + command: 'eaa-kit audit ./dist --watch', + why: 'watch the build instead, with your build directory in place of ./dist', + }) process.exitCode = 2 return } @@ -262,6 +272,11 @@ program .option('--allow-remote', 'allow --url to crawl a host that is not localhost') .option('--ignore-robots', 'crawl paths robots.txt disallows') .option('--sitemap ', 'where the site lists its pages, if not /sitemap.xml') + .option( + '--redirects ', + `when the URL redirects to another site: ${REDIRECT_MODES.join('|')} (default: ask)`, + oneOf(REDIRECT_MODES), + ) .option('--max-pages ', 'stop the crawl after this many pages', parsePositive) .option('--max-depth ', 'how far from the entry URL to follow links', parseDepth) .option( @@ -336,11 +351,28 @@ program .option('--output ', 'write here instead of eaa.config.json') .option('--force', 'overwrite a config that is already there') .option('-y, --yes', 'take every default without asking') - .action(async (flags: { output?: string; force?: true; yes?: true }) => { - const { runInitCommand } = await import('./init.ts') - const { exitCode } = await runInitCommand(flags) - process.exitCode = exitCode - }) + .option('--no-ci', 'never offer the GitHub Actions workflow') + .option('--no-baseline', 'never offer to record a baseline') + .action( + async (flags: { + output?: string + force?: true + yes?: true + ci: boolean + baseline: boolean + }) => { + const { ci, baseline, ...rest } = flags + const { runInitCommand } = await import('./init.ts') + // Commander sets both to true unless the --no- form was typed, and true + // means "ask", which is what init does anyway. + const { exitCode } = await runInitCommand({ + ...rest, + ...(ci ? {} : { ci: false as const }), + ...(baseline ? {} : { baseline: false as const }), + }) + process.exitCode = exitCode + }, + ) program .command('statement') @@ -390,6 +422,7 @@ try { if (cause instanceof ConfigError) { fail(cause.message) for (const issue of cause.issues) note(` ${issue}`) + if (cause.next !== undefined) nextStep(cause.next) 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. @@ -398,9 +431,25 @@ try { // --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 } + const error = cause as { exitCode?: number; message?: string; code?: string } if (typeof error.exitCode === 'number') { process.exitCode = error.exitCode === 0 ? 0 : 2 + // Commander has printed what was wrong. What it does not say is where the + // right spelling is, which for a mistyped flag is that command's help. + // An unknown command is left alone: commander already suggests the + // nearest one, and a second suggestion would compete with it. + if (error.exitCode !== 0 && error.code !== 'commander.unknownCommand') { + const typed = process.argv[2] + const known = program.commands.find((command) => command.name() === typed) + nextStep( + // A country nobody has written a statement for has its own list. + error.message?.includes("'--country") === true + ? { command: 'eaa-kit countries', why: 'the countries a statement can be written for' } + : known === undefined + ? { command: 'eaa-kit --help', why: 'the commands, and what each one does' } + : { command: `eaa-kit ${known.name()} --help`, why: 'every flag it takes' }, + ) + } } else { process.stderr.write(`${cause instanceof Error ? cause.stack : String(cause)}\n`) process.exitCode = 2 diff --git a/src/cli/init.ts b/src/cli/init.ts index 9dc84e9..1b509fc 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -1,13 +1,15 @@ -import { readFile, writeFile } from 'node:fs/promises' +import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { createInterface } from 'node:readline/promises' import pc from 'picocolors' +import { DEFAULT_BASELINE_FILE } from '../audit/baseline.ts' import { DEFAULT_FAIL_ON } from '../audit/impact.ts' import { COUNTRY_INFO, countryForLocale, findCountry } from '../config/countries.ts' import { COUNTRIES, type Country } from '../config/define.ts' import { CONFIG_FILENAMES } from '../config/load.ts' import { exists } from '../fs.ts' -import { fail, note, warn } from './command.ts' +import { fail, nextStep, note, warn } from './command.ts' +import { findGitRoot, WORKFLOW_FILE, workflowFor } from './setup.ts' /** * `eaa-kit init`. @@ -37,6 +39,10 @@ export interface InitCommandOptions { yes?: boolean /** Injectable so the prompts can be tested without a terminal. */ ask?: (question: string, fallback: string) => Promise + /** `--no-ci`: never offer the GitHub Actions workflow. */ + ci?: false + /** `--no-baseline`: never offer to record a baseline. */ + baseline?: false } export interface InitCommandResult { @@ -140,7 +146,8 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise< const already = await existingConfig(cwd) if (already !== undefined && !options.force) { - warn(`${already} already exists. Pass --force to overwrite it.`) + warn(`${already} already exists, and init never overwrites one without being told to.`) + nextStep({ command: 'eaa-kit init --force', why: `start ${already} again from scratch` }) return { exitCode: 1 } } @@ -174,10 +181,43 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise< const legalName = await ask('Legal entity answerable for the site', name) const email = await ask('Feedback email', '') const feedbackUrl = await ask('Feedback or contact form URL (optional)', '') + + // The other two things a project needs before the tool is doing its job. + // Each is only offered where it can work: a baseline needs a build that is + // already there, since init never runs one, and a workflow needs a git + // repository and no workflow of the same name, which is never overwritten. + const site = options.baseline === false ? undefined : await builtSite(cwd) + const recordBaseline = + site !== undefined && + isYes( + await ask( + 'Record a baseline of the barriers the site has today, so CI fails only on new ones? (y/n)', + 'y', + ), + ) + const root = options.ci === false ? undefined : await findGitRoot(cwd) + const workflowPath = root === undefined ? undefined : path.join(root, WORKFLOW_FILE) + const writeWorkflow = + workflowPath !== undefined && + !(await exists(workflowPath)) && + isYes(await ask('Add a GitHub Actions workflow that audits every push? (y/n)', 'y')) + // Before any writing: an open stdin handle keeps the process alive after the // file is written, and the reader is left looking at a prompt that has gone. terminal?.close() + // Before the config, so the config can point at it: a baseline the project + // records is one `eaa-kit audit` should read without being told. + let baseline: string | undefined + if (recordBaseline && site !== undefined) { + const { runBaselineCommand } = await import('./baseline.ts') + const recorded = await runBaselineCommand(path.relative(cwd, site) || '.', { cwd }) + if (recorded.exitCode === 0) baseline = DEFAULT_BASELINE_FILE + } else if (await exists(path.join(cwd, DEFAULT_BASELINE_FILE))) { + // One recorded earlier is still one the workflow and the audit should read. + baseline = DEFAULT_BASELINE_FILE + } + const config = { site: { name, url, locale }, provider: { @@ -199,7 +239,7 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise< // 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 }, + audit: { failOn: DEFAULT_FAIL_ON, ...(baseline === undefined ? {} : { baseline }) }, } try { @@ -223,14 +263,40 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise< } note( 'Read it before publishing anything from it: status is partially-compliant,\n' + - 'which is the honest default before an audit has run.\n' + - '\n' + - 'Next: eaa-kit audit · eaa-kit statement', + 'which is the honest default before an audit has run.', ) + if (writeWorkflow && root !== undefined && workflowPath !== undefined) { + const workflow = await workflowFor({ + cwd, + root, + ...(site === undefined ? {} : { site }), + ...(baseline === undefined ? {} : { baseline }), + failOn: DEFAULT_FAIL_ON, + }) + try { + await mkdir(path.dirname(workflowPath), { recursive: true }) + await writeFile(workflowPath, workflow, 'utf8') + process.stderr.write(`Wrote ${path.relative(cwd, workflowPath)}\n`) + } catch (cause) { + // The config is written and is the part that matters; a workflow that + // could not be is reported, not turned into a failed init. + warn( + `Could not write ${WORKFLOW_FILE}: ${cause instanceof Error ? cause.message : String(cause)}`, + ) + } + } + + note( + `\nNext: eaa-kit statement${writeWorkflow ? ' · commit and push to run the workflow' : ' · eaa-kit audit'}`, + ) return { file: target, exitCode: 0 } } +function isYes(answer: string): boolean { + return /^y(es)?$/i.test(answer.trim()) +} + /** How often an answer that is not a country is asked again before giving up on it. */ const COUNTRY_ATTEMPTS = 3 diff --git a/src/cli/pages.ts b/src/cli/pages.ts index 6a9b156..f8942f1 100644 --- a/src/cli/pages.ts +++ b/src/cli/pages.ts @@ -6,9 +6,10 @@ import { emptyDirectoryHint, holdsHtml, } from '../audit/collect.ts' -import type { Collection, Unmeasured } from '../audit/completeness.ts' +import type { Collection, EntryRedirect, Unmeasured } from '../audit/completeness.ts' +import type { RedirectMode } from '../config/define.ts' import { count } from '../text.ts' -import { fail, note, warn } from './command.ts' +import { fail, failWith, nextStep, note, warn } from './command.ts' /** * Where the pages a command audits come from. @@ -39,6 +40,16 @@ export interface CrawlCommandOptions { * Sent by the crawl and by the browser runner, and never written down. */ headers?: Record + /** What to do when the entry URL redirects to another site. Defaults to `ask`. */ + redirects?: RedirectMode + /** + * Asks whether to follow a redirect to another site. Defaults to a prompt on + * the terminal when there is one, and to nothing when there is not, which + * `ask` then treats as a no. + */ + confirm?: (question: string) => Promise + /** Injectable for tests. Defaults to global fetch. */ + fetchImpl?: typeof fetch } export interface ResolvePagesOptions extends CrawlCommandOptions { @@ -141,7 +152,7 @@ export async function resolvePages( // mistake to whoever typed the path, so they get the same advice. This is // what somebody sees pointing the tool at ./dist in a Next.js project, // which is the commonest way to arrive here at all. - fail(cause.message) + failWith(cause) note(await emptyDirectoryHint(shown, cwd)) return undefined } @@ -220,7 +231,7 @@ async function crawlPages( entry = parseEntryUrl(url, options.allowRemote ?? false) } catch (cause) { if (cause instanceof CrawlError) { - fail(cause.message) + failWith(cause) return undefined } throw cause @@ -228,6 +239,10 @@ async function crawlPages( note(`Crawling ${entry.origin}…`) + const followed = await followEntry(entry, options) + if (followed === undefined) return undefined + entry = followed.entry + const result = await crawlSite(entry, { ...(options.allowRemote ? { allowRemote: true } : {}), ...(options.ignoreRobots ? { ignoreRobots: true } : {}), @@ -236,6 +251,7 @@ async function crawlPages( ...(options.maxDepth === undefined ? {} : { maxDepth: options.maxDepth }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.headers === undefined ? {} : { headers: options.headers }), + ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }), }) if (result.pages.length === 0 && result.failures.length > 0) { @@ -269,11 +285,26 @@ async function crawlPages( // and reports it as the site. Said here, and carried into the report below, // because somebody reading an HTML report was never at this terminal. const collapsed = collapsedOnto(result) + // A password field on the page everything landed on turns "looks like" into + // "is": that is a sign-in form, and the requested pages are behind it. + const { hasPasswordField } = await import('../audit/entry.ts') + const landedOn = collapsed[0]?.landedOn ?? '' + const signInForm = + collapsed.length > 0 && + result.pages.some((page) => page.absolutePath === landedOn && hasPasswordField(page.html)) if (collapsed.length > 0) { - const landedOn = collapsed[0]?.landedOn ?? '' - warn(`Every page requested answered at ${landedOn}, which is not where it was asked.`) - note(' A sign-in page in front of the site looks like this.') - note(' If it is behind one, pass --basic-auth user:password or --header "Cookie: …".') + if (signInForm) { + warn( + `Every page requested answered at ${landedOn}, which has a password field on it: a sign-in page stands in front of the site.`, + ) + } else { + warn(`Every page requested answered at ${landedOn}, which is not where it was asked.`) + note(' A sign-in page in front of the site looks like this, and so does a one-page site.') + } + nextStep({ + command: `eaa-kit audit --url ${entry.href} --basic-auth user:password`, + why: 'or --header "Cookie: …" with a signed-in session; neither is written into a report', + }) } return { @@ -288,14 +319,161 @@ async function crawlPages( // about the page it was sent to, and none about the page it asked for. ...collapsed.map((redirect) => ({ location: redirect.requested, - reason: `answered at ${redirect.landedOn} instead, so this page was not audited`, + reason: signInForm + ? `answered at ${redirect.landedOn} instead, a sign-in page, so this page was not audited` + : `answered at ${redirect.landedOn} instead, so this page was not audited`, })), ], truncated: result.truncated, + ...(followed.redirect === undefined ? {} : { entryRedirect: followed.redirect }), }, } } +/** + * Where the crawl should start, once the entry's redirects are known. + * + * Returns undefined when the run must stop, having said why and what to type: + * a sign-in wall, or a redirect to another site nobody agreed to follow. Those + * are the two ways a run can audit something other than what it was sent to, + * and each one ends here rather than in a report about the wrong pages. + */ +async function followEntry( + entry: URL, + options: CrawlCommandOptions, +): Promise<{ entry: URL; redirect?: EntryRedirect } | undefined> { + const { describeSignIn, isSameSite, probeEntry } = await import('../audit/entry.ts') + const { CrawlError, DEFAULT_REQUEST_TIMEOUT_MS, MAX_BODY_BYTES, parseEntryUrl } = await import( + '../audit/crawl.ts' + ) + + const probe = await probeEntry(entry, { + timeoutMs: options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, + maxBodyBytes: MAX_BODY_BYTES, + ...(options.headers === undefined ? {} : { headers: options.headers }), + ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }), + }) + + // Not reachable at all is the crawl's to report, as it always has been. + if (probe.error !== undefined && probe.hops.length === 0) return { entry } + if (probe.error !== undefined) { + fail(`${entry.href} did not settle on a page: ${probe.error}`) + return undefined + } + + if (probe.signIn !== undefined) { + const at = probe.final.href + fail(`${entry.href} is behind a sign-in wall: ${describeSignIn(probe.signIn, at)}.`) + note( + options.headers !== undefined + ? ' The credentials that were sent were not accepted, so nothing was audited.' + : probe.signIn === 'http-401' || probe.signIn === 'http-403' + ? ' Nothing was audited: there is no page to audit until the site lets the run in.' + : ' eaa-kit will not audit a sign-in form as though it were the site, so nothing was audited.', + ) + nextStep( + probe.signIn === 'http-401' + ? { + command: `eaa-kit audit --url ${entry.href} --basic-auth user:password`, + why: 'send the credentials it asks for; they are never written into a report', + } + : { + command: `eaa-kit audit --url ${entry.href} --header "Cookie: "`, + why: 'send a signed-in session, copied from your browser; never written into a report', + }, + ) + return undefined + } + + const destination = probe.final + if (destination.origin === entry.origin) return { entry } + + const statuses = probe.hops.slice(0, -1).map((hop) => hop.status) + const chain = `${statuses.join(' → ')}` + const sameSite = isSameSite(entry, destination) + const mode = options.redirects ?? 'ask' + + let because: EntryRedirect['followedBecause'] | undefined + if (mode !== 'stop' && sameSite) because = 'same-site' + else if (mode === 'follow') because = 'flag' + else if (mode === 'ask') { + const confirm = options.confirm ?? terminalConfirm() + if ( + confirm !== undefined && + (await confirm( + `${entry.href} redirects to ${destination.href} (${chain}). Audit ${destination.href} instead?`, + )) + ) { + because = 'prompt' + } + } + + if (because === undefined) { + fail( + `${entry.href} redirects to ${destination.href} (${chain}), ` + + `${sameSite ? 'the same site at another address' : 'which is a different site'}, so nothing was audited.`, + ) + note( + mode === 'stop' + ? ' --redirects stop was asked for, so the redirect was not followed.' + : ' eaa-kit only follows a redirect to another site when you agree to it: otherwise the\n' + + ' report would describe a site you did not name.', + ) + const remote = options.allowRemote ? ' --allow-remote' : '' + nextStep({ + command: `eaa-kit audit --url ${destination.href}${remote}`, + why: 'audit the address it leads to', + }) + nextStep({ + command: `eaa-kit audit --url ${entry.href}${remote} --redirects follow`, + why: 'or follow it on every run, and have each report say so', + }) + return undefined + } + + // The destination has to pass the same gate the entry did: a local entry + // redirected to the internet is a remote crawl nobody allowed. + let target: URL + try { + target = parseEntryUrl(destination.href, options.allowRemote ?? false) + } catch (cause) { + if (cause instanceof CrawlError) { + fail(`${entry.href} redirects to ${destination.href}.`) + failWith(cause) + return undefined + } + throw cause + } + + warn( + `${entry.href} redirects to ${target.href} (${chain}). Auditing ${target.href}; every report says so.`, + ) + return { + entry: target, + redirect: { + requested: entry.href, + auditedFrom: target.href, + statuses, + followedBecause: because, + }, + } +} + +/** A yes/no question on the terminal, or nothing when there is no terminal to ask at. */ +function terminalConfirm(): ((question: string) => Promise) | undefined { + if (process.stdin.isTTY !== true || process.stderr.isTTY !== true) return undefined + return async (question) => { + const { createInterface } = await import('node:readline/promises') + const rl = createInterface({ input: process.stdin, output: process.stderr }) + try { + const answer = await rl.question(`${question} (y/N) `) + return /^y(es)?$/i.test(answer.trim()) + } finally { + rl.close() + } + } +} + /** * No directory and no URL: work out what this project needs. * diff --git a/src/cli/setup.ts b/src/cli/setup.ts new file mode 100644 index 0000000..a635159 --- /dev/null +++ b/src/cli/setup.ts @@ -0,0 +1,147 @@ +import { readFile, stat } from 'node:fs/promises' +import path from 'node:path' +import { detectPackageManager, readPackageJson } from '../audit/project.ts' +import { TOOL_VERSION } from '../version.ts' + +/** + * The CI half of `eaa-kit init`: a GitHub Actions workflow written for this + * project rather than copied from the docs and edited until it works. + * + * Everything in it is read from the project. The package manager comes from + * the lockfile, the build from the `build` script, the directory from what + * `init` found, and the baseline from whether one was just recorded. Anything + * it cannot read is left to the action's own detection rather than guessed, + * because a workflow that fails on its first push teaches somebody to switch + * it off. + */ + +/** Where the workflow goes, relative to the repository root. */ +export const WORKFLOW_FILE = path.join('.github', 'workflows', 'accessibility.yml') + +/** The repository this project is in, found the way git finds it: upwards. */ +export async function findGitRoot(cwd: string): Promise { + let current = path.resolve(cwd) + for (;;) { + try { + // A directory in a normal checkout, a file in a worktree or submodule. + await stat(path.join(current, '.git')) + return current + } catch { + const parent = path.dirname(current) + if (parent === current) return undefined + current = parent + } + } +} + +/** The branch HEAD points at, which is the one worth auditing on push. */ +async function currentBranch(root: string): Promise { + try { + const head = await readFile(path.join(root, '.git', 'HEAD'), 'utf8') + return /^ref: refs\/heads\/(.+)$/m.exec(head)?.[1]?.trim() + } catch { + return undefined + } +} + +export interface WorkflowInputs { + /** The project, which may be below the repository root in a monorepo. */ + cwd: string + root: string + /** The built site `init` found, absolute. Undefined leaves it to the action. */ + site?: string + /** A baseline file relative to `cwd`, when there is one. */ + baseline?: string + failOn: string +} + +/** + * The workflow, as text. YAML is written by hand rather than through a + * library: it is one fixed shape with a few values in it, and the comments in + * it are the documentation somebody reads when it first fails. + */ +export async function workflowFor(inputs: WorkflowInputs): Promise { + const pkg = await readPackageJson(inputs.cwd) + const manager = pkg === undefined ? undefined : await detectPackageManager(inputs.cwd) + const branch = (await currentBranch(inputs.root)) ?? 'main' + const workingDirectory = toPosix(path.relative(inputs.root, inputs.cwd)) + const directory = + inputs.site === undefined ? '' : toPosix(path.relative(inputs.cwd, inputs.site)) || '.' + + const setup: string[] = [] + if (manager === 'pnpm') { + setup.push( + ' - uses: pnpm/action-setup@v4', + // action-setup reads the version from packageManager; without that field + // it stops with "No pnpm version is specified". + ...(typeof (pkg as { packageManager?: unknown } | undefined)?.packageManager === 'string' + ? [] + : [' with:', ' version: 10']), + ) + } + if (manager === 'bun') setup.push(' - uses: oven-sh/setup-bun@v2') + + const install = + manager === undefined + ? undefined + : { + npm: 'npm ci', + pnpm: 'pnpm install --frozen-lockfile', + yarn: 'yarn install --frozen-lockfile', + bun: 'bun install --frozen-lockfile', + }[manager] + const build = + manager !== undefined && pkg?.scripts?.['build'] !== undefined + ? `${manager} run build` + : undefined + + const withLines = [ + ...(workingDirectory === '' ? [] : [`working-directory: ${workingDirectory}`]), + ...(install === undefined ? [] : [`install-command: ${install}`]), + ...(build === undefined ? [] : [`build-command: ${build}`]), + // Empty lets the action work the directory out, as `eaa-kit audit` does. + `directory: ${directory === '' ? "''" : directory}`, + `fail-on: ${inputs.failOn}`, + ...(inputs.baseline === undefined ? [] : [`baseline: ${toPosix(inputs.baseline)}`]), + ].map((line) => ` ${line}`) + + return [ + '# Written by eaa-kit init. Audits the built site against WCAG 2.2 AA on every', + '# push and pull request, uploads what it finds to GitHub code scanning, and', + '# fails the job on barriers at or above fail-on.', + '#', + '# The inputs are documented at', + '# https://github.com/likeBloodMoon/eaa-kit/blob/master/docs/integrations.md#github-actions', + 'name: Accessibility', + '', + 'on:', + ' push:', + ` branches: [${branch}]`, + ' pull_request:', + '', + 'permissions:', + ' contents: read', + ' # Required by the SARIF upload to code scanning.', + ' security-events: write', + '', + 'jobs:', + ' audit:', + ' runs-on: ubuntu-latest', + ' steps:', + ' - uses: actions/checkout@v4', + ...setup, + ' - uses: actions/setup-node@v4', + ' with:', + ' node-version: 22', + // The exact release that wrote this file. There is no moving tag to + // follow, for the reason docs/integrations.md gives. + ` - uses: likeBloodMoon/eaa-kit@v${TOOL_VERSION}`, + ' with:', + ...withLines, + '', + ].join('\n') +} + +function toPosix(value: string): string { + return value.split(path.sep).join('/') +} diff --git a/src/cli/statement.ts b/src/cli/statement.ts index 5343171..db68e35 100644 --- a/src/cli/statement.ts +++ b/src/cli/statement.ts @@ -6,7 +6,7 @@ import { checkStatementEvidence, refuses } from '../statement/evidence.ts' import { type AuditSummary, readAuditReport } from '../statement/findings.ts' import { renderStatement } from '../statement/render.ts' import { count } from '../text.ts' -import { advise, emitDocument, fail, note } from './command.ts' +import { advise, emitDocument, fail, nextStep, note } from './command.ts' /** Markdown for a content directory, HTML for dropping straight onto a site. */ export const STATEMENT_FORMATS = ['markdown', 'html'] as const @@ -131,6 +131,7 @@ export async function runStatementCommand( if (cause instanceof ConfigError) { for (const issue of cause.issues) note(` ${issue}`) } + if (cause.next !== undefined) nextStep(cause.next) return { document: '', format, exitCode: 2 } } throw cause diff --git a/src/config/countries.ts b/src/config/countries.ts index c93420b..90fdede 100644 --- a/src/config/countries.ts +++ b/src/config/countries.ts @@ -44,7 +44,7 @@ export const COUNTRY_INFO: Record = { }, BE: { name: 'Belgium', - languages: ['fr', 'nl', 'en'], + languages: ['fr', 'nl', 'de', 'en'], siteLocale: 'fr-BE', statute: 'Loi du 5 novembre 2023 / wet van 5 november 2023 (Code de droit économique)', authority: 'SPF Économie / FOD Economie, Economic Inspection', @@ -57,6 +57,14 @@ export const COUNTRY_INFO: Record = { statute: 'Behindertengleichstellungsgesetz (BehiG), not an EAA transposition', authority: 'the courts; there is no supervisory body', }, + CZ: { + name: 'Czechia', + languages: ['cs', 'en'], + siteLocale: 'cs-CZ', + statute: 'Zákon č. 424/2023 Sb., o požadavcích na přístupnost některých výrobků a služeb', + authority: 'Česká obchodní inspekce (ČOI)', + unverified: true, + }, DE: { name: 'Germany', languages: ['de', 'en'], @@ -64,6 +72,14 @@ export const COUNTRY_INFO: Record = { statute: 'Barrierefreiheitsstärkungsgesetz (BFSG)', authority: 'Marktüberwachungsstelle der Länder (MLBF)', }, + DK: { + name: 'Denmark', + languages: ['da', 'en'], + siteLocale: 'da-DK', + statute: 'Lov nr. 801 af 7. juni 2022 om tilgængelighedskrav for produkter og tjenester', + authority: 'Sikkerhedsstyrelsen for e-commerce; supervision is split', + unverified: true, + }, ES: { name: 'Spain', languages: ['es', 'en'], @@ -72,6 +88,14 @@ export const COUNTRY_INFO: Record = { authority: "the competent market surveillance authority, usually the autonomous community's consumer body", }, + FI: { + name: 'Finland', + languages: ['fi', 'en'], + siteLocale: 'fi-FI', + statute: 'Laki digitaalisten palvelujen tarjoamisesta (306/2019), as amended', + authority: 'Liikenne- ja viestintävirasto Traficom', + unverified: true, + }, FR: { name: 'France', languages: ['fr', 'en'], @@ -117,6 +141,14 @@ export const COUNTRY_INFO: Record = { authority: 'ANACOM for e-commerce services', unverified: true, }, + SE: { + name: 'Sweden', + languages: ['sv', 'en'], + siteLocale: 'sv-SE', + statute: 'Lag (2023:254) om vissa produkters och tjänsters tillgänglighet', + authority: 'Post- och telestyrelsen (PTS)', + unverified: true, + }, } /** @@ -153,7 +185,18 @@ export function countryForLocale(tag: string): Country | undefined { // A region this tool has no country for is a site aimed somewhere else, and // `fr-CA` is not a reason to offer France. if (region !== undefined) return codes.find((code) => code.toLowerCase() === region) - return codes.find( + + // `fr` is France and `de` is Germany: the country the language is named for. + const own = codes.find( (code) => COUNTRY_INFO[code].siteLocale.toLowerCase() === `${language}-${language}`, ) + if (own !== undefined) return own + + // `sv`, `da` and `cs` are not spelled like their countries, so a language + // only one listed country is written in stands for that country. English is + // left out: every country here has an English statement, so an English site + // says nothing about where it sells. + if (language === 'en') return undefined + const speakers = codes.filter((code) => COUNTRY_INFO[code].siteLocale.startsWith(`${language}-`)) + return speakers.length === 1 ? speakers[0] : undefined } diff --git a/src/config/define.ts b/src/config/define.ts index fa31555..4f7095d 100644 --- a/src/config/define.ts +++ b/src/config/define.ts @@ -1,11 +1,28 @@ import { IMPACT_LEVELS } from '../audit/impact.ts' +import type { NextStep } from '../next.ts' import * as s from '../schema.ts' /** * Countries with their own supervisory body and statute text. What is known * about each one is in `countries.ts`. */ -export const COUNTRIES = ['AT', 'BE', 'CH', 'DE', 'ES', 'FR', 'IE', 'IT', 'NL', 'PL', 'PT'] as const +export const COUNTRIES = [ + 'AT', + 'BE', + 'CH', + 'CZ', + 'DE', + 'DK', + 'ES', + 'FI', + 'FR', + 'IE', + 'IT', + 'NL', + 'PL', + 'PT', + 'SE', +] as const export type Country = (typeof COUNTRIES)[number] /** @@ -16,7 +33,20 @@ export type Country = (typeof COUNTRIES)[number] * 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', 'pl', 'pt'] as const +export const STATEMENT_LOCALES = [ + 'cs', + 'da', + 'de', + 'en', + 'es', + 'fi', + 'fr', + 'it', + 'nl', + 'pl', + 'pt', + 'sv', +] as const export type StatementLocale = (typeof STATEMENT_LOCALES)[number] /** @@ -66,6 +96,18 @@ const knownIssueSchema = s.union( 'expected a description, or an object with one', ) +/** + * What a crawl does when its entry URL redirects to another site. + * + * `ask` puts the question to whoever is at the terminal and, with nobody there, + * stops. `follow` goes on to the new address, and every report says so. `stop` + * never goes on, not even to the same site at another address. A redirect + * within the same host, give or take `www.` and http or https, is followed + * under `ask` and `follow` without a question, and is still reported. + */ +export const REDIRECT_MODES = ['ask', 'follow', 'stop'] as const +export type RedirectMode = (typeof REDIRECT_MODES)[number] + /** * Report formats the `audit` block accepts. * @@ -105,6 +147,8 @@ const auditSchema = s.object({ ignoreRobots: s.optional(s.boolean()), /** Where the site lists its pages, when that is not /sitemap.xml. */ sitemap: s.optional(s.string({ min: 1 })), + /** What to do when the entry URL redirects to another site. */ + redirects: s.optional(s.enumeration(REDIRECT_MODES)), maxPages: s.optional(s.integer({ min: 1 })), /** 0 audits the entry page alone. */ maxDepth: s.optional(s.integer({ min: 0 })), @@ -270,6 +314,8 @@ export class ConfigError extends Error { constructor( message: string, readonly issues: string[] = [], + /** The command that fixes it, printed under the message and the issues. */ + readonly next?: NextStep, ) { super(message) } diff --git a/src/config/load.ts b/src/config/load.ts index 4eacd2d..cc5577b 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -49,11 +49,15 @@ export async function loadConfig(options: LoadConfigOptions = {}): Promise `looked for ${name}`), + { command: 'eaa-kit init', why: 'write one, filled in from what the site already states' }, ) } if (!(await isFile(file))) { - throw new ConfigError(`Config file not found: ${file}`) + throw new ConfigError(`Config file not found: ${file}`, [], { + command: `eaa-kit init --output ${path.relative(cwd, file) || path.basename(file)}`, + why: 'write it there', + }) } return { config: parseConfig(await readConfigFile(file), path.basename(file)), path: file } @@ -84,7 +88,10 @@ export async function loadAuditConfig( if (!file) return undefined if (!(await isFile(file))) { - throw new ConfigError(`Config file not found: ${file}`) + throw new ConfigError(`Config file not found: ${file}`, [], { + command: `eaa-kit init --output ${path.relative(cwd, file) || path.basename(file)}`, + why: 'write it there', + }) } return { audit: parseAuditConfig(await readConfigFile(file), path.basename(file)), path: file } diff --git a/src/next.ts b/src/next.ts new file mode 100644 index 0000000..f89d77e --- /dev/null +++ b/src/next.ts @@ -0,0 +1,28 @@ +/** + * The command that gets somebody past an error. + * + * An error the reader cannot act on without opening the docs is half an error. + * Every error class a command can stop on carries one of these where there is a + * command that fixes it, and `fail` prints it under the message. That keeps + * "what went wrong" and "what to type" apart, so the second one can be copied + * as it is. + * + * Only where the fix is a command. An invalid field in a config file is fixed + * by editing the field, and the issues printed under that error already name + * it, so pointing at a command there would send somebody somewhere else. + */ +export interface NextStep { + /** Exactly what to type. */ + command: string + /** What it does, in a few words. */ + why: string +} + +/** The next step an error carries, if it carries one. */ +export function nextStepOf(cause: unknown): NextStep | undefined { + if (cause === null || typeof cause !== 'object') return undefined + const next = (cause as { next?: unknown }).next + if (next === null || typeof next !== 'object') return undefined + const { command, why } = next as Partial + return typeof command === 'string' && typeof why === 'string' ? { command, why } : undefined +} diff --git a/src/statement/error.ts b/src/statement/error.ts index 2158375..f4778fc 100644 --- a/src/statement/error.ts +++ b/src/statement/error.ts @@ -1,3 +1,5 @@ +import type { NextStep } from '../next.ts' + /** * Lives in its own module so that both the renderer and the audit-report reader * can throw it without importing each other. @@ -9,4 +11,12 @@ */ export class StatementError extends Error { override readonly name = 'StatementError' + + constructor( + message: string, + /** The command that fixes it, printed under the message. */ + readonly next?: NextStep, + ) { + super(message) + } } diff --git a/src/statement/findings.ts b/src/statement/findings.ts index 0f159ec..e6ac47c 100644 --- a/src/statement/findings.ts +++ b/src/statement/findings.ts @@ -107,6 +107,10 @@ export function summariseAuditReport(value: unknown, source = 'audit report'): A if (report.schemaVersion !== SUPPORTED_REPORT_SCHEMA) { throw new StatementError( `${source} has schemaVersion ${report.schemaVersion}; this version of eaa-kit reads ${SUPPORTED_REPORT_SCHEMA}`, + { + command: `eaa-kit audit --format json --output ${source}`, + why: 'write it again with this version', + }, ) } @@ -167,7 +171,10 @@ export async function readAuditReport(file: string, cwd = process.cwd()): Promis try { raw = await readFile(target, 'utf8') } catch { - throw new StatementError(`Could not read the audit report at ${file}`) + throw new StatementError(`Could not read the audit report at ${file}`, { + command: `eaa-kit audit --format json --output ${file}`, + why: 'write the report the statement reads its barriers from', + }) } let value: unknown diff --git a/src/statement/render.ts b/src/statement/render.ts index 71f2862..a516aad 100644 --- a/src/statement/render.ts +++ b/src/statement/render.ts @@ -267,14 +267,18 @@ function reasonScope(reason: KnownIssue['reason']): TemplateScope { * 20 August 2026, not August 20, 2026, in a European legal document. */ const DATE_LOCALES: Record = { + cs: 'cs-CZ', + da: 'da-DK', de: 'de-AT', en: 'en-GB', es: 'es-ES', + fi: 'fi-FI', fr: 'fr-FR', it: 'it-IT', nl: 'nl-NL', pl: 'pl-PL', pt: 'pt-PT', + sv: 'sv-SE', } /** @@ -340,10 +344,17 @@ async function loadTemplate(country: Country, locale: StatementLocale): Promise< .filter((entry) => entry.startsWith(prefix)) .map((entry) => entry.slice(prefix.length)) + const first = forCountry[0] throw new StatementError( - forCountry.length > 0 + first !== undefined ? `No ${country} statement in ${locale}. ${country} has: ${forCountry.join(', ')}` : `No statement template for ${name}. Available: ${templates.join(', ')}`, + first !== undefined + ? { + command: `eaa-kit statement --lang ${first}`, + why: `the ${country} statement in ${first}`, + } + : { command: 'eaa-kit countries', why: 'the countries a statement can be written for' }, ) } } diff --git a/src/statement/templates/be.de.md b/src/statement/templates/be.de.md new file mode 100644 index 0000000..71325a3 --- /dev/null +++ b/src/statement/templates/be.de.md @@ -0,0 +1,148 @@ +# Erklärung zur Barrierefreiheit + +{{ provider.legalName }} ist bemüht, die Website {{ site.name }} im Einklang mit dem +Wirtschaftsgesetzbuch in der durch das Gesetz vom 5. November 2023 geänderten Fassung +barrierefrei zugänglich zu machen. Mit diesem Gesetz wird die Richtlinie (EU) 2019/882 +(European Accessibility Act) für Dienstleistungen im elektronischen Geschäftsverkehr und +Bankdienstleistungen für Verbraucher in belgisches Recht umgesetzt. Diese Pflichten gelten +seit dem 28. Juni 2025. + +Diese Erklärung zur Barrierefreiheit gilt für {{ site.url }}. + +## Stand der Vereinbarkeit mit den Anforderungen + +{{#if compliance.isCompliant}} +Diese Website ist mit {{ compliance.standard }} vollständig vereinbar. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Diese Website ist mit {{ compliance.standard }} teilweise vereinbar. Die im folgenden +Abschnitt aufgeführten Inhalte sind aus den jeweils genannten Gründen nicht barrierefrei. +{{/if}} +{{#if compliance.isNonCompliant}} +Diese Website ist mit {{ compliance.standard }} nicht vereinbar. Die im folgenden Abschnitt +aufgeführten Inhalte sind aus den jeweils genannten Gründen nicht barrierefrei. +{{/if}} + +## Nicht barrierefreie Inhalte + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Betroffene Anforderung: {{ standards }} +{{/if}} +{{#if pageList}} + Betroffene Seiten: {{ pageList }}{{#if hasMorePages}} und {{ morePages }} weitere{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Grund: unverhältnismäßige Belastung. +{{/if}} +{{#if isOutOfScope}} + Grund: der Inhalt fällt nicht in den Anwendungsbereich dieser Pflichten. +{{/if}} +{{#if isFixPlanned}} + Grund: die Barriere ist bekannt und wird behoben. +{{/if}} +{{#if remedyByFormatted}} + Geplante Behebung bis: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Automatisiert erkannt (axe-core, Regel {{ ruleId }}); bitte in eigenen Worten beschreiben. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Zum Zeitpunkt der Prüfung sind keine nicht barrierefreien Inhalte bekannt. +{{/if}} + +## Erstellung dieser Erklärung + +Diese Erklärung wurde am {{ compliance.assessedOnFormatted }} erstellt. + +{{#if compliance.isSelfAssessment}} +Grundlage ist eine Selbstbewertung durch {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Grundlage ist eine Prüfung durch Dritte. +{{/if}} + +{{#if review.isSingle}} +Eines der {{ review.total }} Erfolgskriterien der WCAG 2.2 (Stufen A und AA) wurde manuell +geprüft. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} der {{ review.total }} Erfolgskriterien der WCAG 2.2 (Stufen A und AA) +wurden manuell geprüft. +{{/if}} +{{#if review.hasDate}} +Die jüngste dieser manuellen Prüfungen erfolgte am {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +Die automatisierte Prüfung vom {{ audit.checkedOnFormatted }} umfasste eine Seite dieser +Website. +{{/if}} +{{#if audit.isMultiPage}} +Die automatisierte Prüfung vom {{ audit.checkedOnFormatted }} umfasste {{ audit.pages }} +Seiten dieser Website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Bei einer weiteren Regelprüfung ist eine manuelle Beurteilung erforderlich. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Bei {{ audit.needsReview }} weiteren Regelprüfungen ist eine manuelle Beurteilung +erforderlich. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Bei einer Regelprüfung erreichte das verwendete Werkzeug kein Ergebnis; sie wird nicht als +erfüllt ausgewiesen. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +Bei {{ audit.notEvaluated }} Regelprüfungen erreichte das verwendete Werkzeug kein +Ergebnis; sie werden nicht als erfüllt ausgewiesen. +{{/if}} +{{#if hasAudit}} + +{{/if}} +Die Bewertung stützt sich unter anderem auf eine automatisierte Prüfung. Automatisierte +Werkzeuge erkennen nur einen Teil der möglichen Barrieren; sie ersetzen keine manuelle +Prüfung und keine Prüfung mit assistiven Technologien. + +## Feedback und Kontaktangaben + +Sie haben eine Barriere gefunden oder benötigen Informationen in einer barrierefreien +Form? Melden Sie sich bitte bei uns: + +- E-Mail: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Kontaktformular: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefon: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Anschrift: {{ provider.address }} +{{/if}} + +Wir bemühen uns, Ihre Rückmeldung zeitnah zu beantworten. + +## Beschwerdeverfahren + +Wenn Sie mit unserer Antwort nicht zufrieden sind, können Sie das Problem dem FÖD Wirtschaft +melden, dessen Generaldirektion Wirtschaftsinspektion die Einhaltung dieser Pflichten durch +Dienstleistungen im elektronischen Geschäftsverkehr überwacht. + +FÖD Wirtschaft, K.M.B., Mittelstand und Energie +https://economie.fgov.be + +Die Aufsicht ist in Belgien aufgeteilt: Für andere von der Richtlinie erfasste +Dienstleistungen, etwa die elektronische Kommunikation, sind andere Behörden zuständig. +Websites und mobile Anwendungen öffentlicher Stellen unterliegen einer eigenen Regelung mit +einer eigenen Erklärung zur Barrierefreiheit; dieses Dokument ersetzt sie nicht. + +--- + +Diese Erklärung wurde mit eaa-kit erstellt und ist keine Rechtsberatung. Prüfen Sie den +Inhalt vor der Veröffentlichung und lassen Sie ihn im Zweifel rechtlich prüfen. diff --git a/src/statement/templates/cz.cs.md b/src/statement/templates/cz.cs.md new file mode 100644 index 0000000..10c77a7 --- /dev/null +++ b/src/statement/templates/cz.cs.md @@ -0,0 +1,137 @@ +# Prohlášení o přístupnosti + +{{ provider.legalName }} usiluje o to, aby webové stránky {{ site.name }} byly přístupné v +souladu se zákonem č. 424/2023 Sb., o požadavcích na přístupnost některých výrobků a služeb, +kterým se do českého práva provádí směrnice (EU) 2019/882 (evropský akt o přístupnosti). +Požadavky zákona se uplatňují od 28. června 2025. + +Toto prohlášení o přístupnosti se vztahuje na {{ site.url }}. + +## Stav souladu + +{{#if compliance.isCompliant}} +Tyto webové stránky jsou plně v souladu s normou {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Tyto webové stránky jsou částečně v souladu s normou {{ compliance.standard }}. Níže uvedený +obsah není přístupný z uvedených důvodů. +{{/if}} +{{#if compliance.isNonCompliant}} +Tyto webové stránky nejsou v souladu s normou {{ compliance.standard }}. Níže uvedený obsah +není přístupný z uvedených důvodů. +{{/if}} + +## Nepřístupný obsah + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Dotčený požadavek: {{ standards }} +{{/if}} +{{#if pageList}} + Dotčené stránky: {{ pageList }}{{#if hasMorePages}} a další (počet: {{ morePages }}){{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Důvod: nepřiměřená zátěž. +{{/if}} +{{#if isOutOfScope}} + Důvod: obsah nespadá do působnosti zákona. +{{/if}} +{{#if isFixPlanned}} + Důvod: nedostatek je známý a odstraňuje se. +{{/if}} +{{#if remedyByFormatted}} + Předpokládané odstranění do: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Zjištěno automatizovaným testem (axe-core, pravidlo {{ ruleId }}); popište vlastními slovy. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +V době posouzení nebyl znám žádný nepřístupný obsah. +{{/if}} + +## Vypracování tohoto prohlášení + +Toto prohlášení bylo vypracováno dne {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Vychází z vlastního posouzení, které provedl subjekt {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Vychází z posouzení provedeného třetí stranou. +{{/if}} + +{{#if review.isSingle}} +Jedno z {{ review.total }} kritérií úspěšnosti WCAG 2.2 (úrovně A a AA) bylo ověřeno ručně. +{{/if}} +{{#if review.isPlural}} +Počet kritérií úspěšnosti WCAG 2.2 (úrovně A a AA) ověřených ručně: {{ review.answered }} z +{{ review.total }}. +{{/if}} +{{#if review.hasDate}} +Poslední z těchto ručních ověření bylo zaznamenáno dne {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +Automatizovaný test ze dne {{ audit.checkedOnFormatted }} zahrnul jednu stránku tohoto webu. +{{/if}} +{{#if audit.isMultiPage}} +Automatizovaný test ze dne {{ audit.checkedOnFormatted }} zahrnul následující počet stránek +tohoto webu: {{ audit.pages }}. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Jedna další kontrola pravidla vyžaduje posouzení člověkem. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Počet dalších kontrol pravidel, které vyžadují posouzení člověkem: {{ audit.needsReview }}. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +U jedné kontroly pravidla použitý nástroj nedospěl k výsledku; není uváděna jako splněná. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +Počet kontrol pravidel, u nichž použitý nástroj nedospěl k výsledku: {{ audit.notEvaluated +}}; nejsou uváděny jako splněné. +{{/if}} +{{#if hasAudit}} + +{{/if}} +Posouzení se zčásti opírá o automatizované testování. Automatizované nástroje odhalí jen +část možných bariér; nenahrazují ruční testování ani testování s asistivními technologiemi. + +## Zpětná vazba a kontaktní údaje + +Narazili jste na bariéru, nebo potřebujete informace v přístupné podobě? Kontaktujte nás: + +- E-mail: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Kontaktní formulář: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefon: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Adresa: {{ provider.address }} +{{/if}} + +Snažíme se odpovídat co nejdříve. + +## Dozor + +Pokud nejste s naší odpovědí spokojeni, můžete se obrátit na Českou obchodní inspekci (ČOI), +která vykonává dozor nad dodržováním zákona. + +Česká obchodní inspekce (ČOI) +https://coi.gov.cz + +Webové stránky a mobilní aplikace subjektů veřejného sektoru upravuje jiný zákon, který +vyžaduje vlastní prohlášení o přístupnosti. Tento dokument je nenahrazuje. + +--- + +Toto prohlášení bylo vytvořeno nástrojem eaa-kit a nepředstavuje právní radu. Před +zveřejněním je zkontrolujte, a v případě pochybností je nechte posoudit právníkem. diff --git a/src/statement/templates/cz.en.md b/src/statement/templates/cz.en.md new file mode 100644 index 0000000..b1eff78 --- /dev/null +++ b/src/statement/templates/cz.en.md @@ -0,0 +1,139 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with Act No. 424/2023 Coll., on accessibility requirements for certain products +and services, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into +Czech law. Its requirements 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 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 review.isSingle}} +One of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and AA was checked +manually. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and +AA were checked manually. +{{/if}} +{{#if review.hasDate}} +The most recent of those manual checks was recorded on {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/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 contact the Czech Trade Inspection +Authority (ČOI), which supervises compliance with the Act. + +Česká obchodní inspekce (ČOI) +https://coi.gov.cz + +Websites and mobile applications of public sector bodies fall under a separate act, which +requires an accessibility statement 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/dk.da.md b/src/statement/templates/dk.da.md new file mode 100644 index 0000000..04023b3 --- /dev/null +++ b/src/statement/templates/dk.da.md @@ -0,0 +1,141 @@ +# Tilgængelighedserklæring + +{{ provider.legalName }} arbejder på at gøre websitet {{ site.name }} tilgængeligt i +overensstemmelse med lov nr. 801 af 7. juni 2022 om tilgængelighedskrav for produkter og +tjenester, som gennemfører direktiv (EU) 2019/882 (tilgængelighedsdirektivet) i dansk ret. +Lovens krav gælder for tjenester, der leveres fra den 28. juni 2025. + +Denne tilgængelighedserklæring gælder for {{ site.url }}. + +## Overholdelsesstatus + +{{#if compliance.isCompliant}} +Dette website overholder fuldt ud {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Dette website overholder delvist {{ compliance.standard }}. Det indhold, der er angivet +nedenfor, er ikke tilgængeligt af de anførte grunde. +{{/if}} +{{#if compliance.isNonCompliant}} +Dette website overholder ikke {{ compliance.standard }}. Det indhold, der er angivet +nedenfor, er ikke tilgængeligt af de anførte grunde. +{{/if}} + +## Indhold, der ikke er tilgængeligt + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Berørt krav: {{ standards }} +{{/if}} +{{#if pageList}} + Berørte sider: {{ pageList }}{{#if hasMorePages}} og {{ morePages }} andre{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Begrundelse: uforholdsmæssig stor byrde. +{{/if}} +{{#if isOutOfScope}} + Begrundelse: indholdet er ikke omfattet af lovens anvendelsesområde. +{{/if}} +{{#if isFixPlanned}} + Begrundelse: barrieren er kendt og ved at blive udbedret. +{{/if}} +{{#if remedyByFormatted}} + Forventes udbedret senest: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Fundet ved automatisk test (axe-core, regel {{ ruleId }}); beskriv det med dine egne ord. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Da vurderingen blev foretaget, var der ikke kendskab til indhold, der ikke er tilgængeligt. +{{/if}} + +## Udarbejdelse af denne erklæring + +Erklæringen blev udarbejdet den {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Den bygger på en selvevaluering foretaget af {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Den bygger på en vurdering foretaget af en tredjepart. +{{/if}} + +{{#if review.isSingle}} +Et af de {{ review.total }} succeskriterier i WCAG 2.2 (niveau A og AA) er kontrolleret +manuelt. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} af de {{ review.total }} succeskriterier i WCAG 2.2 (niveau A og AA) +er kontrolleret manuelt. +{{/if}} +{{#if review.hasDate}} +Den seneste af disse manuelle kontroller blev registreret den {{ review.checkedOnFormatted +}}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +Den automatiske test den {{ audit.checkedOnFormatted }} omfattede én side på dette website. +{{/if}} +{{#if audit.isMultiPage}} +Den automatiske test den {{ audit.checkedOnFormatted }} omfattede {{ audit.pages }} sider på +dette website. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Yderligere én regelkontrol kræver en manuel vurdering. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Yderligere {{ audit.needsReview }} regelkontroller kræver en manuel vurdering. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Én regelkontrol kunne ikke afgøres af det anvendte værktøj; den angives ikke som opfyldt. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} regelkontroller kunne ikke afgøres af det anvendte værktøj; de +angives ikke som opfyldt. +{{/if}} +{{#if hasAudit}} + +{{/if}} +Vurderingen bygger delvis på automatisk test. Automatiske værktøjer finder kun en del af de +mulige barrierer; de erstatter ikke manuel test eller test med hjælpemidler. + +## Feedback og kontaktoplysninger + +Er du stødt på en barriere, eller har du brug for oplysninger i et tilgængeligt format? +Kontakt os: + +- E-mail: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Kontaktformular: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefon: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Adresse: {{ provider.address }} +{{/if}} + +Vi bestræber os på at svare hurtigst muligt. + +## Klageadgang + +Hvis du ikke er tilfreds med vores svar, kan du henvende dig til Sikkerhedsstyrelsen, som +fører tilsyn med e-handelstjenester efter loven. Tilsynet i Danmark er fordelt på flere +myndigheder: med finansielle tjenester fører Finanstilsynet for eksempel tilsyn. + +Sikkerhedsstyrelsen +https://www.sik.dk + +Offentlige myndigheders websteder og mobilapplikationer er omfattet af en anden lov, som +kræver en særskilt tilgængelighedserklæring. Dette dokument erstatter ikke den. + +--- + +Denne erklæring er udarbejdet med eaa-kit og er ikke juridisk rådgivning. Gennemgå den, før +den offentliggøres, og få den vurderet af en jurist, hvis du er i tvivl. diff --git a/src/statement/templates/dk.en.md b/src/statement/templates/dk.en.md new file mode 100644 index 0000000..408fed1 --- /dev/null +++ b/src/statement/templates/dk.en.md @@ -0,0 +1,141 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with Act No. 801 of 7 June 2022 on accessibility requirements for products and +services, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into +Danish law. Its requirements apply to services provided 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 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 review.isSingle}} +One of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and AA was checked +manually. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and +AA were checked manually. +{{/if}} +{{#if review.hasDate}} +The most recent of those manual checks was recorded on {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/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 contact the Danish Safety Technology +Authority (Sikkerhedsstyrelsen), which supervises e-commerce services under the Act. +Supervision in Denmark is split between several authorities: financial services, for +example, are supervised by the Danish Financial Supervisory Authority (Finanstilsynet). + +Sikkerhedsstyrelsen +https://www.sik.dk + +Websites and mobile applications of public sector bodies fall under a separate act, which +requires an accessibility statement 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/fi.en.md b/src/statement/templates/fi.en.md new file mode 100644 index 0000000..2805161 --- /dev/null +++ b/src/statement/templates/fi.en.md @@ -0,0 +1,139 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with the Act on the Provision of Digital Services (306/2019), as amended to +transpose Directive (EU) 2019/882 (the European Accessibility Act) into Finnish law. Its +accessibility requirements for e-commerce 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 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 review.isSingle}} +One of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and AA was checked +manually. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and +AA were checked manually. +{{/if}} +{{#if review.hasDate}} +The most recent of those manual checks was recorded on {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/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 contact the Finnish Transport and +Communications Agency (Traficom), which supervises the accessibility of digital services. + +Liikenne- ja viestintävirasto Traficom +https://www.traficom.fi + +The Act sets content requirements of its own for the accessibility statements of public +sector bodies. 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/fi.fi.md b/src/statement/templates/fi.fi.md new file mode 100644 index 0000000..799b4e2 --- /dev/null +++ b/src/statement/templates/fi.fi.md @@ -0,0 +1,141 @@ +# Saavutettavuusseloste + +{{ provider.legalName }} pyrkii siihen, että verkkosivusto {{ site.name }} on saavutettava +lain digitaalisten palvelujen tarjoamisesta (306/2019) mukaisesti. Lakia on muutettu +direktiivin (EU) 2019/882 (esteettömyysdirektiivi) panemiseksi täytäntöön Suomessa. +Verkkokauppapalveluja koskevia saavutettavuusvaatimuksia sovelletaan 28.6.2025 alkaen. + +Tämä saavutettavuusseloste koskee sivustoa {{ site.url }}. + +## Vaatimustenmukaisuuden tila + +{{#if compliance.isCompliant}} +Tämä verkkosivusto on kokonaan yhdenmukainen standardin {{ compliance.standard }} kanssa. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Tämä verkkosivusto on osittain yhdenmukainen standardin {{ compliance.standard }} kanssa. +Alla luetellut sisällöt eivät ole saavutettavia mainituista syistä. +{{/if}} +{{#if compliance.isNonCompliant}} +Tämä verkkosivusto ei ole yhdenmukainen standardin {{ compliance.standard }} kanssa. Alla +luetellut sisällöt eivät ole saavutettavia mainituista syistä. +{{/if}} + +## Sisältö, joka ei ole saavutettavaa + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Koskee vaatimusta: {{ standards }} +{{/if}} +{{#if pageList}} + Koskee sivuja: {{ pageList }}{{#if hasMorePages}} ja {{ morePages }} muuta{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Syy: kohtuuton rasite. +{{/if}} +{{#if isOutOfScope}} + Syy: sisältö ei kuulu lain soveltamisalaan. +{{/if}} +{{#if isFixPlanned}} + Syy: puute tiedetään ja sitä korjataan. +{{/if}} +{{#if remedyByFormatted}} + Korjataan viimeistään: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Havaittu automaattisessa testauksessa (axe-core, sääntö {{ ruleId }}); kuvaa puute omin sanoin. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Arvioinnin aikaan ei ollut tiedossa sisältöä, joka ei olisi saavutettavaa. +{{/if}} + +## Selosteen laatiminen + +Tämä seloste on laadittu {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Se perustuu itsearviointiin, jonka on tehnyt {{ provider.legalName }}. +{{/if}} +{{#if compliance.isExternalAudit}} +Se perustuu kolmannen osapuolen tekemään arviointiin. +{{/if}} + +{{#if review.isSingle}} +Yksi WCAG 2.2:n {{ review.total }} onnistumiskriteeristä (tasot A ja AA) on tarkistettu +manuaalisesti. +{{/if}} +{{#if review.isPlural}} +Manuaalisesti on tarkistettu {{ review.answered }}/{{ review.total }} WCAG 2.2:n +onnistumiskriteeristä (tasot A ja AA). +{{/if}} +{{#if review.hasDate}} +Viimeisin näistä manuaalisista tarkistuksista kirjattiin {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +Automaattinen testaus {{ audit.checkedOnFormatted }} kattoi yhden sivun tältä sivustolta. +{{/if}} +{{#if audit.isMultiPage}} +Automaattinen testaus {{ audit.checkedOnFormatted }} kattoi {{ audit.pages }} sivua tältä +sivustolta. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Yksi muu sääntötarkistus edellyttää ihmisen arviota. +{{/if}} +{{#if audit.needsReviewIsPlural}} +{{ audit.needsReview }} muuta sääntötarkistusta edellyttää ihmisen arviota. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +Käytetty työkalu ei pystynyt ratkaisemaan yhtä sääntötarkistusta; sitä ei esitetä +täyttyneenä. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +Käytetty työkalu ei pystynyt ratkaisemaan {{ audit.notEvaluated }} sääntötarkistusta; niitä +ei esitetä täyttyneinä. +{{/if}} +{{#if hasAudit}} + +{{/if}} +Arviointi perustuu osittain automaattiseen testaukseen. Automaattiset työkalut löytävät vain +osan mahdollisista puutteista; ne eivät korvaa manuaalista testausta eikä testausta +avustavilla teknologioilla. + +## Palaute ja yhteystiedot + +Huomasitko saavutettavuuspuutteen, tai tarvitsetko tietoa saavutettavassa muodossa? Ota +yhteyttä: + +- Sähköposti: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Yhteydenottolomake: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Puhelin: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Osoite: {{ provider.address }} +{{/if}} + +Pyrimme vastaamaan mahdollisimman pian. + +## Valvontaviranomainen + +Jos et ole tyytyväinen vastaukseemme, voit ottaa yhteyttä Liikenne- ja viestintävirasto +Traficomiin, joka valvoo digitaalisten palvelujen saavutettavuutta. + +Liikenne- ja viestintävirasto Traficom +https://www.traficom.fi + +Laissa on omat sisältövaatimuksensa julkisen sektorin toimijoiden saavutettavuusselosteille. +Tämä asiakirja ei korvaa sellaista selostetta. + +--- + +Tämä seloste on laadittu eaa-kit-työkalulla, eikä se ole oikeudellista neuvontaa. Tarkista +se ennen julkaisua, ja pyydä epäselvissä tapauksissa juristin arvio. diff --git a/src/statement/templates/se.en.md b/src/statement/templates/se.en.md new file mode 100644 index 0000000..0342244 --- /dev/null +++ b/src/statement/templates/se.en.md @@ -0,0 +1,139 @@ +# Accessibility Statement + +{{ provider.legalName }} is committed to making the website {{ site.name }} accessible in +accordance with the Act on the Accessibility of Certain Products and Services (SFS +2023:254), which transposes Directive (EU) 2019/882 (the European Accessibility Act) into +Swedish law. Its requirements 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 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 review.isSingle}} +One of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and AA was checked +manually. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} of the {{ review.total }} success criteria in WCAG 2.2 at Levels A and +AA were checked manually. +{{/if}} +{{#if review.hasDate}} +The most recent of those manual checks was recorded on {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/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 Swedish Post +and Telecom Authority (PTS), which supervises e-commerce services under the Act. + +Post- och telestyrelsen (PTS) +https://pts.se + +Websites and mobile applications of public sector bodies fall under a separate act, which +requires an accessibility statement 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/se.sv.md b/src/statement/templates/se.sv.md new file mode 100644 index 0000000..4aa56ed --- /dev/null +++ b/src/statement/templates/se.sv.md @@ -0,0 +1,142 @@ +# Tillgänglighetsredogörelse + +{{ provider.legalName }} strävar efter att göra webbplatsen {{ site.name }} tillgänglig i +enlighet med lagen (2023:254) om vissa produkters och tjänsters tillgänglighet, som genomför +direktiv (EU) 2019/882 (tillgänglighetsdirektivet) i svensk rätt. Lagens krav gäller från +den 28 juni 2025. + +Den här tillgänglighetsredogörelsen gäller {{ site.url }}. + +## Efterlevnadsstatus + +{{#if compliance.isCompliant}} +Webbplatsen är helt förenlig med {{ compliance.standard }}. +{{/if}} +{{#if compliance.isPartiallyCompliant}} +Webbplatsen är delvis förenlig med {{ compliance.standard }}. Innehållet som anges nedan är +inte tillgängligt, av de skäl som anges. +{{/if}} +{{#if compliance.isNonCompliant}} +Webbplatsen är inte förenlig med {{ compliance.standard }}. Innehållet som anges nedan är +inte tillgängligt, av de skäl som anges. +{{/if}} + +## Innehåll som inte är tillgängligt + +{{#if hasKnownIssues}} +{{#each compliance.knownIssues}} +- {{ description }} +{{#if standards}} + Berört krav: {{ standards }} +{{/if}} +{{#if pageList}} + Berörda sidor: {{ pageList }}{{#if hasMorePages}} och {{ morePages }} till{{/if}} +{{/if}} +{{#if isDisproportionateBurden}} + Skäl: oskälig börda. +{{/if}} +{{#if isOutOfScope}} + Skäl: innehållet omfattas inte av lagens tillämpningsområde. +{{/if}} +{{#if isFixPlanned}} + Skäl: bristen är känd och håller på att åtgärdas. +{{/if}} +{{#if remedyByFormatted}} + Planeras vara åtgärdat senast: {{ remedyByFormatted }} +{{/if}} +{{#if isFromAudit}} + Upptäckt vid automatiserad testning (axe-core, regel {{ ruleId }}); beskriv det med egna ord. +{{/if}} +{{/each}} +{{/if}} +{{#if hasNoKnownIssues}} +Vid bedömningen var inget otillgängligt innehåll känt. +{{/if}} + +## Hur redogörelsen har tagits fram + +Redogörelsen upprättades den {{ compliance.assessedOnFormatted }}. + +{{#if compliance.isSelfAssessment}} +Den bygger på en självskattning som {{ provider.legalName }} har gjort. +{{/if}} +{{#if compliance.isExternalAudit}} +Den bygger på en bedömning som en tredje part har gjort. +{{/if}} + +{{#if review.isSingle}} +Ett av de {{ review.total }} framgångskriterierna i WCAG 2.2 (nivå A och AA) har +kontrollerats manuellt. +{{/if}} +{{#if review.isPlural}} +{{ review.answered }} av de {{ review.total }} framgångskriterierna i WCAG 2.2 (nivå A och +AA) har kontrollerats manuellt. +{{/if}} +{{#if review.hasDate}} +Den senaste av dessa manuella kontroller registrerades den {{ review.checkedOnFormatted }}. +{{/if}} +{{#if hasReview}} + +{{/if}} +{{#if audit.isSinglePage}} +Den automatiserade testningen den {{ audit.checkedOnFormatted }} omfattade en sida på +webbplatsen. +{{/if}} +{{#if audit.isMultiPage}} +Den automatiserade testningen den {{ audit.checkedOnFormatted }} omfattade {{ audit.pages }} +sidor på webbplatsen. +{{/if}} +{{#if audit.needsReviewIsSingle}} +Ytterligare en regelkontroll kräver en manuell bedömning. +{{/if}} +{{#if audit.needsReviewIsPlural}} +Ytterligare {{ audit.needsReview }} regelkontroller kräver en manuell bedömning. +{{/if}} +{{#if audit.notEvaluatedIsSingle}} +En regelkontroll kunde inte avgöras av verktyget som användes; den redovisas inte som +uppfylld. +{{/if}} +{{#if audit.notEvaluatedIsPlural}} +{{ audit.notEvaluated }} regelkontroller kunde inte avgöras av verktyget som användes; de +redovisas inte som uppfyllda. +{{/if}} +{{#if hasAudit}} + +{{/if}} +Bedömningen bygger delvis på automatiserad testning. Automatiserade verktyg hittar bara en +del av de möjliga bristerna; de ersätter inte manuell testning eller testning med +hjälpmedel. + +## Återkoppling och kontaktuppgifter + +Har du hittat en brist, eller behöver du information i ett tillgängligt format? Kontakta +oss: + +- E-post: {{ provider.email }} +{{#if provider.feedbackUrl}} +- Kontaktformulär: {{ provider.feedbackUrl }} +{{/if}} +{{#if provider.phone}} +- Telefon: {{ provider.phone }} +{{/if}} +{{#if provider.address}} +- Adress: {{ provider.address }} +{{/if}} + +Vi strävar efter att svara så snart som möjligt. + +## Tillsyn + +Om du inte är nöjd med vårt svar kan du anmäla saken till Post- och telestyrelsen (PTS), som +har tillsyn över e-handelstjänster enligt lagen. + +Post- och telestyrelsen (PTS) +https://pts.se + +Offentliga aktörers webbplatser och mobila applikationer omfattas av en annan lag, som +kräver en egen tillgänglighetsredogörelse. Det här dokumentet ersätter inte den. + +--- + +Redogörelsen har tagits fram med eaa-kit och är inte juridisk rådgivning. Granska den innan +den publiceras, och låt en jurist granska den om du är osäker. diff --git a/tests/audit/baseline.test.ts b/tests/audit/baseline.test.ts index 04448b0..091b689 100644 --- a/tests/audit/baseline.test.ts +++ b/tests/audit/baseline.test.ts @@ -308,9 +308,13 @@ describe('the file', () => { it('says how to make one when it is not there', async () => { const dir = await project() - await expect(readBaseline('missing.json', dir)).rejects.toThrow( - /Could not read the baseline at missing\.json.*eaa-kit baseline/s, - ) + // The how is the error's next step, which the CLI prints under the message + // as a line somebody can copy. + const error = await readBaseline('missing.json', dir).catch((cause: unknown) => cause) + expect(error).toMatchObject({ + message: expect.stringMatching(/Could not read the baseline at missing\.json/), + next: { command: 'eaa-kit baseline --output missing.json' }, + }) }) it('rejects a file that is not JSON', async () => { diff --git a/tests/audit/review.test.ts b/tests/audit/review.test.ts index 6266050..bf6c1e5 100644 --- a/tests/audit/review.test.ts +++ b/tests/audit/review.test.ts @@ -230,7 +230,11 @@ describe('the file', () => { it('says how to make one when there is none', async () => { const dir = await workspace() - await expect(readReview(DEFAULT_REVIEW_FILE, dir)).rejects.toThrow(/eaa-kit checklist/) + // The how is the error's next step, printed by the CLI under the message. + const error = await readReview(DEFAULT_REVIEW_FILE, dir).catch((cause: unknown) => cause) + expect(error).toMatchObject({ + next: { command: expect.stringMatching(/^eaa-kit checklist --record /) }, + }) }) it('refuses a result it does not recognise rather than treating it as met', async () => { diff --git a/tests/audit/site.test.ts b/tests/audit/site.test.ts index c1865f1..c5d1a17 100644 --- a/tests/audit/site.test.ts +++ b/tests/audit/site.test.ts @@ -63,11 +63,16 @@ describe('countryForLocale', () => { ['pl', 'PL'], ['pt-PT', 'PT'], ['en-IE', 'IE'], + ['sv', 'SE'], + ['da', 'DK'], + ['cs', 'CZ'], + ['fi', 'FI'], + ['de-BE', 'BE'], ])('reads %s as %s', (tag, country) => { expect(countryForLocale(tag)).toBe(country) }) - it.each(['en', 'en-GB', 'fr-CA', 'pt-BR', 'sv-SE'])('offers nothing for %s', (tag) => { + it.each(['en', 'en-GB', 'fr-CA', 'pt-BR', 'nb-NO'])('offers nothing for %s', (tag) => { // English has no country of its own here, and a region this tool has no // country for is a site aimed somewhere else. expect(countryForLocale(tag)).toBeUndefined() diff --git a/tests/cli/next.test.ts b/tests/cli/next.test.ts new file mode 100644 index 0000000..ead6a3d --- /dev/null +++ b/tests/cli/next.test.ts @@ -0,0 +1,120 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { stripVTControlCharacters } from 'node:util' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { runAuditCommand } from '../../src/cli/audit.ts' +import { runDiffCommand } from '../../src/cli/diff.ts' +import { runInitCommand } from '../../src/cli/init.ts' +import { runStatementCommand } from '../../src/cli/statement.ts' +import { nextStepOf } from '../../src/next.ts' + +/** + * Every exit-2 path a new user is likely to meet ends with the command that + * fixes it. These run each one and read the line under the error, with the + * colour codes taken out so the assertion holds on a terminal that has them. + */ + +const dirs: string[] = [] +let stderr: string[] = [] + +beforeEach(() => { + stderr = [] + vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => { + stderr.push(String(chunk)) + return true + }) + vi.spyOn(process.stdout, 'write').mockImplementation(() => true) +}) + +afterEach(async () => { + vi.restoreAllMocks() + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +async function folder(files: Record = {}): Promise { + const dir = await mkdtemp(path.join(tmpdir(), 'eaa-kit-next-')) + dirs.push(dir) + for (const [name, body] of Object.entries(files)) await writeFile(path.join(dir, name), body) + return dir +} + +const said = (): string => stripVTControlCharacters(stderr.join('')) + +const CONFIG = JSON.stringify({ + site: { name: 'S', url: 'https://s.example', locale: 'de-AT' }, + provider: { legalName: 'S GmbH', email: 'a@s.example' }, + compliance: { status: 'partially-compliant', assessedOn: '2026-09-01' }, + enforcement: { country: 'FR' }, +}) + +describe('what to type next', () => { + it('points a statement with no config at init', async () => { + const cwd = await folder() + + expect((await runStatementCommand({ cwd })).exitCode).toBe(2) + expect(said()).toContain('→ eaa-kit init') + }) + + it('points a missing language at one the country has', async () => { + const cwd = await folder({ 'eaa.config.json': CONFIG }) + + expect((await runStatementCommand({ cwd, locale: 'de' })).exitCode).toBe(2) + expect(said()).toContain('→ eaa-kit statement --lang en') + }) + + it('points a missing audit report at the command that writes one', async () => { + const cwd = await folder({ 'eaa.config.json': CONFIG }) + + await runStatementCommand({ cwd, audit: 'a11y.json' }) + + expect(said()).toContain('→ eaa-kit audit --format json --output a11y.json') + }) + + it('points a missing baseline at the command that records one', async () => { + const cwd = await folder({ 'index.html': 'x' }) + + const { exitCode } = await runAuditCommand(cwd, { cwd, baseline: 'base.json' }) + + expect(exitCode).toBe(2) + expect(said()).toContain('→ eaa-kit baseline --output base.json') + }) + + it('points a missing review record at checklist', async () => { + const cwd = await folder({ 'index.html': 'x' }) + + const { exitCode } = await runAuditCommand(cwd, { cwd, review: 'review.json' }) + + expect(exitCode).toBe(2) + expect(said()).toContain('→ eaa-kit checklist --record review.json') + }) + + it('points a missing report in a diff at the audit that writes it', async () => { + const cwd = await folder() + + const { exitCode } = await runDiffCommand('before.json', 'after.json', { cwd }) + + expect(exitCode).toBe(2) + expect(said()).toContain('→ eaa-kit audit --format json --output before.json') + }) + + it('points a config that is already there at --force', async () => { + const cwd = await folder({ 'eaa.config.json': CONFIG }) + + await runInitCommand({ cwd, yes: true }) + + expect(said()).toContain('→ eaa-kit init --force') + }) +}) + +describe('nextStepOf', () => { + it('reads a next step off anything that carries one, and nothing else', () => { + expect(nextStepOf({ next: { command: 'eaa-kit init', why: 'w' } })).toEqual({ + command: 'eaa-kit init', + why: 'w', + }) + expect(nextStepOf(new Error('plain'))).toBeUndefined() + expect(nextStepOf({ next: { command: 3 } })).toBeUndefined() + expect(nextStepOf(undefined)).toBeUndefined() + }) +}) diff --git a/tests/cli/redirects.test.ts b/tests/cli/redirects.test.ts new file mode 100644 index 0000000..e103072 --- /dev/null +++ b/tests/cli/redirects.test.ts @@ -0,0 +1,339 @@ +import { stripVTControlCharacters } from 'node:util' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { runAuditCommand } from '../../src/cli/audit.ts' +import { resolvePages } from '../../src/cli/pages.ts' + +/** + * Where a crawl's entry URL actually leads, and what the run does about it. + * + * The case this was written for: `www.gtainside.de` answers with a 301 to + * `www.gtainside.com`. That is a different site from the one somebody named, + * so the run stops and says so unless they agree to go on, and when they do, + * every report says which site it is about. + */ + +interface Route { + status?: number + location?: string + body?: string + type?: string +} + +const PAGE = (title: string, extra = ''): string => + `${title}

${title}

${extra}
` + +/** + * A fake network. It honours `redirect: 'manual'`, which the entry probe uses + * to see each hop, and follows redirects itself otherwise, the way fetch does + * for the crawl, setting the response's url to where it ended up. + */ +function network(routes: Record) { + const requests: Array<{ url: string; headers: Record }> = [] + const answer = (href: string): Response => { + const route = routes[href] ?? { status: 404, body: 'gone' } + const headers: Record = { 'content-type': route.type ?? 'text/html' } + if (route.location !== undefined) headers.location = route.location + return new Response(route.body ?? '', { status: route.status ?? 200, headers }) + } + const fetchImpl = (async (input: string | URL, init?: RequestInit) => { + let href = String(input) + requests.push({ url: href, headers: { ...(init?.headers as Record) } }) + if (init?.redirect === 'manual') return answer(href) + for (let hop = 0; hop < 10; hop += 1) { + const route = routes[href] + if (route?.location === undefined) break + href = new URL(route.location, href).href + } + const response = answer(href) + Object.defineProperty(response, 'url', { value: href }) + return response + }) as typeof fetch + return { fetchImpl, requests } +} + +let stderr: string[] = [] +beforeEach(() => { + stderr = [] + vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => { + stderr.push(String(chunk)) + return true + }) +}) +afterEach(() => vi.restoreAllMocks()) + +const said = (): string => stripVTControlCharacters(stderr.join('')) + +const GTA = { + 'https://www.gtainside.de/': { status: 301, location: 'https://www.gtainside.com/' }, + 'https://www.gtainside.com/': { body: PAGE('GTA Inside') }, +} + +describe('an entry that redirects to another site', () => { + it('stops, says where it went, and names both ways on', async () => { + const { fetchImpl } = network(GTA) + + const resolved = await resolvePages(undefined, { + url: 'https://www.gtainside.de/', + allowRemote: true, + fetchImpl, + }) + + expect(resolved).toBeUndefined() + expect(said()).toContain( + 'https://www.gtainside.de/ redirects to https://www.gtainside.com/ (301), which is a different site, so nothing was audited.', + ) + expect(said()).toContain('→ eaa-kit audit --url https://www.gtainside.com/ --allow-remote') + expect(said()).toContain('--redirects follow') + }) + + it('asks, and stops on a no', async () => { + const { fetchImpl } = network(GTA) + const questions: string[] = [] + + const resolved = await resolvePages(undefined, { + url: 'https://www.gtainside.de/', + allowRemote: true, + fetchImpl, + confirm: async (question) => { + questions.push(question) + return false + }, + }) + + expect(resolved).toBeUndefined() + expect(questions[0]).toContain('Audit https://www.gtainside.com/ instead?') + }) + + it('goes on when approved, and records that it did and why', async () => { + const { fetchImpl } = network(GTA) + + const resolved = await resolvePages(undefined, { + url: 'https://www.gtainside.de/', + allowRemote: true, + fetchImpl, + confirm: async () => true, + }) + + expect(resolved?.pages.map((page) => page.absolutePath)).toEqual(['https://www.gtainside.com/']) + expect(resolved?.completeness.entryRedirect).toEqual({ + requested: 'https://www.gtainside.de/', + auditedFrom: 'https://www.gtainside.com/', + statuses: [301], + followedBecause: 'prompt', + }) + }) + + it('goes on without asking under --redirects follow', async () => { + const { fetchImpl } = network(GTA) + + const resolved = await resolvePages(undefined, { + url: 'https://www.gtainside.de/', + allowRemote: true, + redirects: 'follow', + fetchImpl, + confirm: async () => { + throw new Error('should not ask') + }, + }) + + expect(resolved?.completeness.entryRedirect?.followedBecause).toBe('flag') + }) + + it('never lets a local entry redirect its way onto the internet', async () => { + const { fetchImpl } = network({ + 'http://localhost:3000/': { status: 302, location: 'https://example.com/' }, + 'https://example.com/': { body: PAGE('Elsewhere') }, + }) + + const resolved = await resolvePages(undefined, { + url: 'http://localhost:3000/', + redirects: 'follow', + fetchImpl, + }) + + expect(resolved).toBeUndefined() + expect(said()).toContain('does not crawl remote hosts by default') + }) +}) + +describe('an entry that redirects within the same site', () => { + const HTTPS = { + 'http://example.com/': { status: 301, location: 'https://www.example.com/' }, + 'https://www.example.com/': { body: PAGE('Example') }, + } + + it('is followed without a question, and still recorded', async () => { + const { fetchImpl } = network(HTTPS) + + const resolved = await resolvePages(undefined, { + url: 'http://example.com/', + allowRemote: true, + fetchImpl, + }) + + expect(resolved?.completeness.entryRedirect).toMatchObject({ + auditedFrom: 'https://www.example.com/', + followedBecause: 'same-site', + }) + }) + + it('is refused under --redirects stop', async () => { + const { fetchImpl } = network(HTTPS) + + const resolved = await resolvePages(undefined, { + url: 'http://example.com/', + allowRemote: true, + redirects: 'stop', + fetchImpl, + }) + + expect(resolved).toBeUndefined() + expect(said()).toContain('the same site at another address') + expect(said()).toContain('--redirects stop was asked for') + }) +}) + +describe('a sign-in wall in front of the entry', () => { + it.each([ + [ + '401', + { 'https://staging.example.com/': { status: 401, body: 'no' } }, + 'answered 401: it asks for credentials', + '--basic-auth user:password', + ], + [ + 'an identity provider', + { + 'https://staging.example.com/': { + status: 302, + location: 'https://accounts.google.com/o/oauth2/auth?x=1', + }, + 'https://accounts.google.com/o/oauth2/auth?x=1': { body: PAGE('Sign in') }, + }, + 'it sends visitors to sign in at https://accounts.google.com', + '--header "Cookie: "', + ], + [ + 'a login page on the site', + { + 'https://staging.example.com/': { status: 302, location: '/users/sign_in' }, + 'https://staging.example.com/users/sign_in': { body: PAGE('Sign in') }, + }, + 'a sign-in page at https://staging.example.com/users/sign_in', + '--header "Cookie: "', + ], + [ + 'a page with a password field', + { + 'https://staging.example.com/': { status: 302, location: '/gate' }, + 'https://staging.example.com/gate': { + body: PAGE('Gate', '
'), + }, + }, + 'a page with a password field on it', + '--header "Cookie: "', + ], + ] as Array<[string, Record, string, string]>)( + 'stops rather than auditing the form: %s', + async (_name, routes, evidence, fix) => { + const { fetchImpl } = network(routes) + + const resolved = await resolvePages(undefined, { + url: 'https://staging.example.com/', + allowRemote: true, + redirects: 'follow', + fetchImpl, + }) + + expect(resolved).toBeUndefined() + expect(said()).toContain('is behind a sign-in wall') + expect(said()).toContain(evidence) + expect(said()).toContain(fix) + }, + ) + + it('never sends the credentials to another origin on the way', async () => { + const { fetchImpl, requests } = network({ + 'https://staging.example.com/': { status: 302, location: 'https://sso.other.example/login' }, + 'https://sso.other.example/login': { body: PAGE('Sign in') }, + }) + + await resolvePages(undefined, { + url: 'https://staging.example.com/', + allowRemote: true, + headers: { authorization: 'Basic c2VjcmV0' }, + fetchImpl, + }) + + const offOrigin = requests.filter((request) => request.url.startsWith('https://sso.')) + expect(offOrigin.length).toBeGreaterThan(0) + for (const request of offOrigin) expect(request.headers.authorization).toBeUndefined() + // And with credentials sent, the message says they were not accepted. + expect(said()).toContain('The credentials that were sent were not accepted') + }) +}) + +describe('the reports', () => { + it('say which site they are about when the run followed a redirect', async () => { + const { fetchImpl } = network(GTA) + const stdout: string[] = [] + vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { + stdout.push(String(chunk)) + return true + }) + + for (const format of ['console', 'html', 'json', 'sarif'] as const) { + stdout.length = 0 + await runAuditCommand(undefined, { + url: 'https://www.gtainside.de/', + allowRemote: true, + redirects: 'follow', + noCache: true, + format, + fetchImpl, + }) + const text = stripVTControlCharacters(stdout.join('')) + expect(text, format).toContain('https://www.gtainside.com/') + if (format === 'console') { + expect(text).toContain('Redirected (301, --redirects follow):') + expect(text).toContain('from https://www.gtainside.de/') + expect(text).toContain('to https://www.gtainside.com/') + } else if (format === 'html') { + expect(text).toContain( + 'https://www.gtainside.de/ redirected to https://www.gtainside.com/ (301', + ) + } else { + expect(text, format).toContain('"requested": "https://www.gtainside.de/"') + } + } + }) +}) + +describe('pages that all land on one sign-in form', () => { + it('says it is a sign-in page when the page they land on has a password field', async () => { + // The entry answers where it was asked, so the probe has nothing to say; + // it is the pages behind it that are walled off. + const { fetchImpl } = network({ + 'https://example.com/': { body: PAGE('Home', 'ab') }, + 'https://example.com/a': { status: 302, location: '/members' }, + 'https://example.com/b': { status: 302, location: '/members' }, + 'https://example.com/members': { + body: PAGE('Members', '
'), + }, + }) + + const resolved = await resolvePages(undefined, { + url: 'https://example.com/', + allowRemote: true, + fetchImpl, + }) + + expect(said()).toContain( + 'which has a password field on it: a sign-in page stands in front of the site', + ) + expect(resolved?.completeness.unreachable.map((item) => item.reason)).toEqual([ + 'answered at https://example.com/members instead, a sign-in page, so this page was not audited', + 'answered at https://example.com/members instead, a sign-in page, so this page was not audited', + ]) + }) +}) diff --git a/tests/cli/setup.test.ts b/tests/cli/setup.test.ts new file mode 100644 index 0000000..b59ccb5 --- /dev/null +++ b/tests/cli/setup.test.ts @@ -0,0 +1,162 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { runInitCommand } from '../../src/cli/init.ts' +import { findGitRoot, WORKFLOW_FILE, workflowFor } from '../../src/cli/setup.ts' +import { TOOL_VERSION } from '../../src/version.ts' + +const dirs: string[] = [] + +beforeEach(() => { + vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + vi.spyOn(process.stdout, 'write').mockImplementation(() => true) +}) + +afterEach(async () => { + vi.restoreAllMocks() + await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +async function repo(files: Record): Promise { + const dir = await mkdtemp(path.join(tmpdir(), 'eaa-kit-setup-')) + dirs.push(dir) + for (const [name, body] of Object.entries(files)) { + await mkdir(path.join(dir, path.dirname(name)), { recursive: true }) + await writeFile(path.join(dir, name), body) + } + return dir +} + +const PAGE = + 'S

S

' + +/** Answers in order; anything not given takes the default, as enter does. */ +function answers(...given: string[]): (question: string, fallback: string) => Promise { + let next = 0 + return async (_question, fallback) => { + const answer = given[next++] + return answer === undefined || answer === '' ? fallback : answer + } +} + +describe('workflowFor', () => { + it('is written for the project: its package manager, build, directory and baseline', async () => { + const root = await repo({ + '.git/HEAD': 'ref: refs/heads/trunk\n', + 'package.json': JSON.stringify({ scripts: { build: 'astro build' } }), + 'pnpm-lock.yaml': '', + }) + + const yaml = await workflowFor({ + cwd: root, + root, + site: path.join(root, 'dist'), + baseline: 'eaa-baseline.json', + failOn: 'serious', + }) + + expect(yaml).toContain('branches: [trunk]') + expect(yaml).toContain('uses: pnpm/action-setup@v4') + // No packageManager field, so the version action-setup needs is given. + expect(yaml).toContain('version: 10') + expect(yaml).toContain('install-command: pnpm install --frozen-lockfile') + expect(yaml).toContain('build-command: pnpm run build') + expect(yaml).toContain('directory: dist') + expect(yaml).toContain('baseline: eaa-baseline.json') + expect(yaml).toContain(`uses: likeBloodMoon/eaa-kit@v${TOOL_VERSION}`) + expect(yaml).toContain('security-events: write') + }) + + it('runs from the package in a monorepo, and leaves an unknown directory to the action', async () => { + const root = await repo({ + '.git/HEAD': 'ref: refs/heads/main\n', + 'apps/web/package.json': JSON.stringify({ scripts: { build: 'next build' } }), + 'apps/web/package-lock.json': '{}', + }) + + const yaml = await workflowFor({ + cwd: path.join(root, 'apps', 'web'), + root, + failOn: 'critical', + }) + + expect(yaml).toContain('working-directory: apps/web') + expect(yaml).toContain('install-command: npm ci') + expect(yaml).not.toContain('pnpm/action-setup') + expect(yaml).toContain("directory: ''") + expect(yaml).toContain('fail-on: critical') + }) + + it('installs and builds nothing for a site written by hand', async () => { + const root = await repo({ '.git/HEAD': 'ref: refs/heads/main\n', 'index.html': PAGE }) + + const yaml = await workflowFor({ cwd: root, root, site: root, failOn: 'serious' }) + + expect(yaml).not.toContain('install-command') + expect(yaml).not.toContain('build-command') + expect(yaml).toContain('directory: .') + }) +}) + +describe('findGitRoot', () => { + it('walks up to the repository', async () => { + const root = await repo({ '.git/HEAD': 'ref: refs/heads/main\n', 'a/b/c.txt': '' }) + + expect(await findGitRoot(path.join(root, 'a', 'b'))).toBe(root) + }) +}) + +describe('eaa-kit init sets up the project', () => { + it('records a baseline and writes a workflow that reads it', async () => { + const cwd = await repo({ '.git/HEAD': 'ref: refs/heads/main\n', 'index.html': PAGE }) + + const { exitCode } = await runInitCommand({ cwd, ask: answers('', '', '', '', '', 'a@s.at') }) + + expect(exitCode).toBe(0) + const baseline = JSON.parse(await readFile(path.join(cwd, 'eaa-baseline.json'), 'utf8')) + expect(baseline.entries.length).toBeGreaterThan(0) + const config = JSON.parse(await readFile(path.join(cwd, 'eaa.config.json'), 'utf8')) + expect(config.audit).toMatchObject({ baseline: 'eaa-baseline.json' }) + const workflow = await readFile(path.join(cwd, WORKFLOW_FILE), 'utf8') + expect(workflow).toContain('baseline: eaa-baseline.json') + }) + + it('does neither when told no', async () => { + const cwd = await repo({ '.git/HEAD': 'ref: refs/heads/main\n', 'index.html': PAGE }) + + await runInitCommand({ cwd, ask: answers('', '', '', '', '', 'a@s.at', '', 'n', 'n') }) + + await expect(readFile(path.join(cwd, 'eaa-baseline.json'), 'utf8')).rejects.toThrow() + await expect(readFile(path.join(cwd, WORKFLOW_FILE), 'utf8')).rejects.toThrow() + }) + + it('does not offer them under --no-ci and --no-baseline', async () => { + const cwd = await repo({ '.git/HEAD': 'ref: refs/heads/main\n', 'index.html': PAGE }) + + await runInitCommand({ cwd, yes: true, ci: false, baseline: false }) + + await expect(readFile(path.join(cwd, 'eaa-baseline.json'), 'utf8')).rejects.toThrow() + await expect(readFile(path.join(cwd, WORKFLOW_FILE), 'utf8')).rejects.toThrow() + }) + + it('never overwrites a workflow that is already there', async () => { + const cwd = await repo({ + '.git/HEAD': 'ref: refs/heads/main\n', + 'index.html': PAGE, + [WORKFLOW_FILE]: '# mine\n', + }) + + await runInitCommand({ cwd, yes: true, baseline: false }) + + expect(await readFile(path.join(cwd, WORKFLOW_FILE), 'utf8')).toBe('# mine\n') + }) + + it('offers no workflow outside a git repository', async () => { + const cwd = await repo({ 'index.html': PAGE }) + + await runInitCommand({ cwd, yes: true, baseline: false }) + + await expect(readFile(path.join(cwd, WORKFLOW_FILE), 'utf8')).rejects.toThrow() + }) +}) diff --git a/tests/statement/__snapshots__/statement.be.de.html b/tests/statement/__snapshots__/statement.be.de.html new file mode 100644 index 0000000..8596d7c --- /dev/null +++ b/tests/statement/__snapshots__/statement.be.de.html @@ -0,0 +1,84 @@ + + + + + + +Erklärung zur Barrierefreiheit + + + +
+

Erklärung zur Barrierefreiheit

+

Musterbetrieb GmbH ist bemüht, die Website Musterbetrieb im Einklang mit dem Wirtschaftsgesetzbuch in der durch das Gesetz vom 5. November 2023 geänderten Fassung barrierefrei zugänglich zu machen. Mit diesem Gesetz wird die Richtlinie (EU) 2019/882 (European Accessibility Act) für Dienstleistungen im elektronischen Geschäftsverkehr und Bankdienstleistungen für Verbraucher in belgisches Recht umgesetzt. Diese Pflichten gelten seit dem 28. Juni 2025.

+

Diese Erklärung zur Barrierefreiheit gilt für https://example.at.

+

Stand der Vereinbarkeit mit den Anforderungen

+

Diese Website ist mit EN 301 549 V3.2.1 (WCAG 2.2 AA) teilweise vereinbar. Die im folgenden Abschnitt aufgeführten Inhalte sind aus den jeweils genannten Gründen nicht barrierefrei.

+

Nicht barrierefreie Inhalte

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Betroffene Anforderung: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Grund: die Barriere ist bekannt und wird behoben.
    + Geplante Behebung bis: 31. Dezember 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Grund: unverhältnismäßige Belastung.
  • +
  • Form field must not have multiple label elements
    + Betroffene Seiten: index.html
    + Grund: die Barriere ist bekannt und wird behoben.
    + Automatisiert erkannt (axe-core, Regel form-field-multiple-labels); bitte in eigenen Worten beschreiben.
  • +
  • Images must have alternative text
    + Betroffene Anforderung: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Betroffene Seiten: index.html
    + Grund: die Barriere ist bekannt und wird behoben.
    + Automatisiert erkannt (axe-core, Regel image-alt); bitte in eigenen Worten beschreiben.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Betroffene Anforderung: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Betroffene Seiten: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html und 2 weitere
    + Grund: die Barriere ist bekannt und wird behoben.
    + Automatisiert erkannt (axe-core, Regel color-contrast); bitte in eigenen Worten beschreiben.
  • +
  • Document should have one main landmark
    + Betroffene Anforderung: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Betroffene Seiten: kontakt.html, team.html
    + Grund: die Barriere ist bekannt und wird behoben.
    + Automatisiert erkannt (axe-core, Regel landmark-one-main); bitte in eigenen Worten beschreiben.
  • +
+

Erstellung dieser Erklärung

+

Diese Erklärung wurde am 21. August 2026 erstellt.

+

Grundlage ist eine Selbstbewertung durch Musterbetrieb GmbH.

+

Die automatisierte Prüfung vom 21. August 2026 umfasste 8 Seiten dieser Website. Bei 2 weiteren Regelprüfungen ist eine manuelle Beurteilung erforderlich. Bei 12 Regelprüfungen erreichte das verwendete Werkzeug kein Ergebnis; sie werden nicht als erfüllt ausgewiesen.

+

Die Bewertung stützt sich unter anderem auf eine automatisierte Prüfung. Automatisierte Werkzeuge erkennen nur einen Teil der möglichen Barrieren; sie ersetzen keine manuelle Prüfung und keine Prüfung mit assistiven Technologien.

+

Feedback und Kontaktangaben

+

Sie haben eine Barriere gefunden oder benötigen Informationen in einer barrierefreien Form? Melden Sie sich bitte bei uns:

+ +

Wir bemühen uns, Ihre Rückmeldung zeitnah zu beantworten.

+

Beschwerdeverfahren

+

Wenn Sie mit unserer Antwort nicht zufrieden sind, können Sie das Problem dem FÖD Wirtschaft melden, dessen Generaldirektion Wirtschaftsinspektion die Einhaltung dieser Pflichten durch Dienstleistungen im elektronischen Geschäftsverkehr überwacht.

+

FÖD Wirtschaft, K.M.B., Mittelstand und Energie https://economie.fgov.be

+

Die Aufsicht ist in Belgien aufgeteilt: Für andere von der Richtlinie erfasste Dienstleistungen, etwa die elektronische Kommunikation, sind andere Behörden zuständig. Websites und mobile Anwendungen öffentlicher Stellen unterliegen einer eigenen Regelung mit einer eigenen Erklärung zur Barrierefreiheit; dieses Dokument ersetzt sie nicht.

+
+

Diese Erklärung wurde mit eaa-kit erstellt und ist keine Rechtsberatung. Prüfen Sie den Inhalt vor der Veröffentlichung und lassen Sie ihn im Zweifel rechtlich prüfen.

+
+ + diff --git a/tests/statement/__snapshots__/statement.be.de.md b/tests/statement/__snapshots__/statement.be.de.md new file mode 100644 index 0000000..d209f45 --- /dev/null +++ b/tests/statement/__snapshots__/statement.be.de.md @@ -0,0 +1,91 @@ +# Erklärung zur Barrierefreiheit + +Musterbetrieb GmbH ist bemüht, die Website Musterbetrieb im Einklang mit dem +Wirtschaftsgesetzbuch in der durch das Gesetz vom 5. November 2023 geänderten Fassung +barrierefrei zugänglich zu machen. Mit diesem Gesetz wird die Richtlinie (EU) 2019/882 +(European Accessibility Act) für Dienstleistungen im elektronischen Geschäftsverkehr und +Bankdienstleistungen für Verbraucher in belgisches Recht umgesetzt. Diese Pflichten gelten +seit dem 28. Juni 2025. + +Diese Erklärung zur Barrierefreiheit gilt für https://example.at. + +## Stand der Vereinbarkeit mit den Anforderungen + +Diese Website ist mit EN 301 549 V3.2.1 (WCAG 2.2 AA) teilweise vereinbar. Die im folgenden +Abschnitt aufgeführten Inhalte sind aus den jeweils genannten Gründen nicht barrierefrei. + +## Nicht barrierefreie Inhalte + +- Die eingebettete Karte hat keinen Titel. + Betroffene Anforderung: WCAG 4.1.2, EN 301 549 9.4.1.2 + Grund: die Barriere ist bekannt und wird behoben. + Geplante Behebung bis: 31. Dezember 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Grund: unverhältnismäßige Belastung. +- Form field must not have multiple label elements + Betroffene Seiten: index.html + Grund: die Barriere ist bekannt und wird behoben. + Automatisiert erkannt (axe-core, Regel form-field-multiple-labels); bitte in eigenen Worten beschreiben. +- Images must have alternative text + Betroffene Anforderung: WCAG 1.1.1, EN 301 549 9.1.1.1 + Betroffene Seiten: index.html + Grund: die Barriere ist bekannt und wird behoben. + Automatisiert erkannt (axe-core, Regel image-alt); bitte in eigenen Worten beschreiben. +- Elements must meet minimum color contrast ratio thresholds + Betroffene Anforderung: WCAG 1.4.3, EN 301 549 9.1.4.3 + Betroffene Seiten: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html und 2 weitere + Grund: die Barriere ist bekannt und wird behoben. + Automatisiert erkannt (axe-core, Regel color-contrast); bitte in eigenen Worten beschreiben. +- Document should have one main landmark + Betroffene Anforderung: WCAG 1.3.1, EN 301 549 9.1.3.1 + Betroffene Seiten: kontakt.html, team.html + Grund: die Barriere ist bekannt und wird behoben. + Automatisiert erkannt (axe-core, Regel landmark-one-main); bitte in eigenen Worten beschreiben. + +## Erstellung dieser Erklärung + +Diese Erklärung wurde am 21. August 2026 erstellt. + +Grundlage ist eine Selbstbewertung durch Musterbetrieb GmbH. + +Die automatisierte Prüfung vom 21. August 2026 umfasste 8 +Seiten dieser Website. +Bei 2 weiteren Regelprüfungen ist eine manuelle Beurteilung +erforderlich. +Bei 12 Regelprüfungen erreichte das verwendete Werkzeug kein +Ergebnis; sie werden nicht als erfüllt ausgewiesen. + +Die Bewertung stützt sich unter anderem auf eine automatisierte Prüfung. Automatisierte +Werkzeuge erkennen nur einen Teil der möglichen Barrieren; sie ersetzen keine manuelle +Prüfung und keine Prüfung mit assistiven Technologien. + +## Feedback und Kontaktangaben + +Sie haben eine Barriere gefunden oder benötigen Informationen in einer barrierefreien +Form? Melden Sie sich bitte bei uns: + +- E-Mail: office@example.at +- Kontaktformular: https://example.at/kontakt +- Telefon: +43 1 2345678 +- Anschrift: Hauptstraße 1, 1010 Wien + +Wir bemühen uns, Ihre Rückmeldung zeitnah zu beantworten. + +## Beschwerdeverfahren + +Wenn Sie mit unserer Antwort nicht zufrieden sind, können Sie das Problem dem FÖD Wirtschaft +melden, dessen Generaldirektion Wirtschaftsinspektion die Einhaltung dieser Pflichten durch +Dienstleistungen im elektronischen Geschäftsverkehr überwacht. + +FÖD Wirtschaft, K.M.B., Mittelstand und Energie +https://economie.fgov.be + +Die Aufsicht ist in Belgien aufgeteilt: Für andere von der Richtlinie erfasste +Dienstleistungen, etwa die elektronische Kommunikation, sind andere Behörden zuständig. +Websites und mobile Anwendungen öffentlicher Stellen unterliegen einer eigenen Regelung mit +einer eigenen Erklärung zur Barrierefreiheit; dieses Dokument ersetzt sie nicht. + +--- + +Diese Erklärung wurde mit eaa-kit erstellt und ist keine Rechtsberatung. Prüfen Sie den +Inhalt vor der Veröffentlichung und lassen Sie ihn im Zweifel rechtlich prüfen. diff --git a/tests/statement/__snapshots__/statement.cz.cs.html b/tests/statement/__snapshots__/statement.cz.cs.html new file mode 100644 index 0000000..92b51dd --- /dev/null +++ b/tests/statement/__snapshots__/statement.cz.cs.html @@ -0,0 +1,84 @@ + + + + + + +Prohlášení o přístupnosti + + + +
+

Prohlášení o přístupnosti

+

Musterbetrieb GmbH usiluje o to, aby webové stránky Musterbetrieb byly přístupné v souladu se zákonem č. 424/2023 Sb., o požadavcích na přístupnost některých výrobků a služeb, kterým se do českého práva provádí směrnice (EU) 2019/882 (evropský akt o přístupnosti). Požadavky zákona se uplatňují od 28. června 2025.

+

Toto prohlášení o přístupnosti se vztahuje na https://example.at.

+

Stav souladu

+

Tyto webové stránky jsou částečně v souladu s normou EN 301 549 V3.2.1 (WCAG 2.2 AA). Níže uvedený obsah není přístupný z uvedených důvodů.

+

Nepřístupný obsah

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Dotčený požadavek: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Důvod: nedostatek je známý a odstraňuje se.
    + Předpokládané odstranění do: 31. prosince 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Důvod: nepřiměřená zátěž.
  • +
  • Form field must not have multiple label elements
    + Dotčené stránky: index.html
    + Důvod: nedostatek je známý a odstraňuje se.
    + Zjištěno automatizovaným testem (axe-core, pravidlo form-field-multiple-labels); popište vlastními slovy.
  • +
  • Images must have alternative text
    + Dotčený požadavek: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Dotčené stránky: index.html
    + Důvod: nedostatek je známý a odstraňuje se.
    + Zjištěno automatizovaným testem (axe-core, pravidlo image-alt); popište vlastními slovy.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Dotčený požadavek: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Dotčené stránky: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html a další (počet: 2)
    + Důvod: nedostatek je známý a odstraňuje se.
    + Zjištěno automatizovaným testem (axe-core, pravidlo color-contrast); popište vlastními slovy.
  • +
  • Document should have one main landmark
    + Dotčený požadavek: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Dotčené stránky: kontakt.html, team.html
    + Důvod: nedostatek je známý a odstraňuje se.
    + Zjištěno automatizovaným testem (axe-core, pravidlo landmark-one-main); popište vlastními slovy.
  • +
+

Vypracování tohoto prohlášení

+

Toto prohlášení bylo vypracováno dne 21. srpna 2026.

+

Vychází z vlastního posouzení, které provedl subjekt Musterbetrieb GmbH.

+

Automatizovaný test ze dne 21. srpna 2026 zahrnul následující počet stránek tohoto webu: 8. Počet dalších kontrol pravidel, které vyžadují posouzení člověkem: 2. Počet kontrol pravidel, u nichž použitý nástroj nedospěl k výsledku: 12; nejsou uváděny jako splněné.

+

Posouzení se zčásti opírá o automatizované testování. Automatizované nástroje odhalí jen část možných bariér; nenahrazují ruční testování ani testování s asistivními technologiemi.

+

Zpětná vazba a kontaktní údaje

+

Narazili jste na bariéru, nebo potřebujete informace v přístupné podobě? Kontaktujte nás:

+ +

Snažíme se odpovídat co nejdříve.

+

Dozor

+

Pokud nejste s naší odpovědí spokojeni, můžete se obrátit na Českou obchodní inspekci (ČOI), která vykonává dozor nad dodržováním zákona.

+

Česká obchodní inspekce (ČOI) https://coi.gov.cz

+

Webové stránky a mobilní aplikace subjektů veřejného sektoru upravuje jiný zákon, který vyžaduje vlastní prohlášení o přístupnosti. Tento dokument je nenahrazuje.

+
+

Toto prohlášení bylo vytvořeno nástrojem eaa-kit a nepředstavuje právní radu. Před zveřejněním je zkontrolujte, a v případě pochybností je nechte posoudit právníkem.

+
+ + diff --git a/tests/statement/__snapshots__/statement.cz.cs.md b/tests/statement/__snapshots__/statement.cz.cs.md new file mode 100644 index 0000000..32da81e --- /dev/null +++ b/tests/statement/__snapshots__/statement.cz.cs.md @@ -0,0 +1,82 @@ +# Prohlášení o přístupnosti + +Musterbetrieb GmbH usiluje o to, aby webové stránky Musterbetrieb byly přístupné v +souladu se zákonem č. 424/2023 Sb., o požadavcích na přístupnost některých výrobků a služeb, +kterým se do českého práva provádí směrnice (EU) 2019/882 (evropský akt o přístupnosti). +Požadavky zákona se uplatňují od 28. června 2025. + +Toto prohlášení o přístupnosti se vztahuje na https://example.at. + +## Stav souladu + +Tyto webové stránky jsou částečně v souladu s normou EN 301 549 V3.2.1 (WCAG 2.2 AA). Níže uvedený +obsah není přístupný z uvedených důvodů. + +## Nepřístupný obsah + +- Die eingebettete Karte hat keinen Titel. + Dotčený požadavek: WCAG 4.1.2, EN 301 549 9.4.1.2 + Důvod: nedostatek je známý a odstraňuje se. + Předpokládané odstranění do: 31. prosince 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Důvod: nepřiměřená zátěž. +- Form field must not have multiple label elements + Dotčené stránky: index.html + Důvod: nedostatek je známý a odstraňuje se. + Zjištěno automatizovaným testem (axe-core, pravidlo form-field-multiple-labels); popište vlastními slovy. +- Images must have alternative text + Dotčený požadavek: WCAG 1.1.1, EN 301 549 9.1.1.1 + Dotčené stránky: index.html + Důvod: nedostatek je známý a odstraňuje se. + Zjištěno automatizovaným testem (axe-core, pravidlo image-alt); popište vlastními slovy. +- Elements must meet minimum color contrast ratio thresholds + Dotčený požadavek: WCAG 1.4.3, EN 301 549 9.1.4.3 + Dotčené stránky: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html a další (počet: 2) + Důvod: nedostatek je známý a odstraňuje se. + Zjištěno automatizovaným testem (axe-core, pravidlo color-contrast); popište vlastními slovy. +- Document should have one main landmark + Dotčený požadavek: WCAG 1.3.1, EN 301 549 9.1.3.1 + Dotčené stránky: kontakt.html, team.html + Důvod: nedostatek je známý a odstraňuje se. + Zjištěno automatizovaným testem (axe-core, pravidlo landmark-one-main); popište vlastními slovy. + +## Vypracování tohoto prohlášení + +Toto prohlášení bylo vypracováno dne 21. srpna 2026. + +Vychází z vlastního posouzení, které provedl subjekt Musterbetrieb GmbH. + +Automatizovaný test ze dne 21. srpna 2026 zahrnul následující počet stránek +tohoto webu: 8. +Počet dalších kontrol pravidel, které vyžadují posouzení člověkem: 2. +Počet kontrol pravidel, u nichž použitý nástroj nedospěl k výsledku: 12; nejsou uváděny jako splněné. + +Posouzení se zčásti opírá o automatizované testování. Automatizované nástroje odhalí jen +část možných bariér; nenahrazují ruční testování ani testování s asistivními technologiemi. + +## Zpětná vazba a kontaktní údaje + +Narazili jste na bariéru, nebo potřebujete informace v přístupné podobě? Kontaktujte nás: + +- E-mail: office@example.at +- Kontaktní formulář: https://example.at/kontakt +- Telefon: +43 1 2345678 +- Adresa: Hauptstraße 1, 1010 Wien + +Snažíme se odpovídat co nejdříve. + +## Dozor + +Pokud nejste s naší odpovědí spokojeni, můžete se obrátit na Českou obchodní inspekci (ČOI), +která vykonává dozor nad dodržováním zákona. + +Česká obchodní inspekce (ČOI) +https://coi.gov.cz + +Webové stránky a mobilní aplikace subjektů veřejného sektoru upravuje jiný zákon, který +vyžaduje vlastní prohlášení o přístupnosti. Tento dokument je nenahrazuje. + +--- + +Toto prohlášení bylo vytvořeno nástrojem eaa-kit a nepředstavuje právní radu. Před +zveřejněním je zkontrolujte, a v případě pochybností je nechte posoudit právníkem. diff --git a/tests/statement/__snapshots__/statement.cz.en.html b/tests/statement/__snapshots__/statement.cz.en.html new file mode 100644 index 0000000..4fa4160 --- /dev/null +++ b/tests/statement/__snapshots__/statement.cz.en.html @@ -0,0 +1,84 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with Act No. 424/2023 Coll., on accessibility requirements for certain products and services, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Czech law. Its requirements 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 contact the Czech Trade Inspection Authority (ČOI), which supervises compliance with the Act.

+

Česká obchodní inspekce (ČOI) https://coi.gov.cz

+

Websites and mobile applications of public sector bodies fall under a separate act, which requires an accessibility statement 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.cz.en.md b/tests/statement/__snapshots__/statement.cz.en.md new file mode 100644 index 0000000..b89586e --- /dev/null +++ b/tests/statement/__snapshots__/statement.cz.en.md @@ -0,0 +1,84 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with Act No. 424/2023 Coll., on accessibility requirements for certain products +and services, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into +Czech law. Its requirements 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 contact the Czech Trade Inspection +Authority (ČOI), which supervises compliance with the Act. + +Česká obchodní inspekce (ČOI) +https://coi.gov.cz + +Websites and mobile applications of public sector bodies fall under a separate act, which +requires an accessibility statement 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.dk.da.html b/tests/statement/__snapshots__/statement.dk.da.html new file mode 100644 index 0000000..f2b3c9b --- /dev/null +++ b/tests/statement/__snapshots__/statement.dk.da.html @@ -0,0 +1,84 @@ + + + + + + +Tilgængelighedserklæring + + + +
+

Tilgængelighedserklæring

+

Musterbetrieb GmbH arbejder på at gøre websitet Musterbetrieb tilgængeligt i overensstemmelse med lov nr. 801 af 7. juni 2022 om tilgængelighedskrav for produkter og tjenester, som gennemfører direktiv (EU) 2019/882 (tilgængelighedsdirektivet) i dansk ret. Lovens krav gælder for tjenester, der leveres fra den 28. juni 2025.

+

Denne tilgængelighedserklæring gælder for https://example.at.

+

Overholdelsesstatus

+

Dette website overholder delvist EN 301 549 V3.2.1 (WCAG 2.2 AA). Det indhold, der er angivet nedenfor, er ikke tilgængeligt af de anførte grunde.

+

Indhold, der ikke er tilgængeligt

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Berørt krav: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Begrundelse: barrieren er kendt og ved at blive udbedret.
    + Forventes udbedret senest: 31. december 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Begrundelse: uforholdsmæssig stor byrde.
  • +
  • Form field must not have multiple label elements
    + Berørte sider: index.html
    + Begrundelse: barrieren er kendt og ved at blive udbedret.
    + Fundet ved automatisk test (axe-core, regel form-field-multiple-labels); beskriv det med dine egne ord.
  • +
  • Images must have alternative text
    + Berørt krav: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Berørte sider: index.html
    + Begrundelse: barrieren er kendt og ved at blive udbedret.
    + Fundet ved automatisk test (axe-core, regel image-alt); beskriv det med dine egne ord.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Berørt krav: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Berørte sider: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html og 2 andre
    + Begrundelse: barrieren er kendt og ved at blive udbedret.
    + Fundet ved automatisk test (axe-core, regel color-contrast); beskriv det med dine egne ord.
  • +
  • Document should have one main landmark
    + Berørt krav: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Berørte sider: kontakt.html, team.html
    + Begrundelse: barrieren er kendt og ved at blive udbedret.
    + Fundet ved automatisk test (axe-core, regel landmark-one-main); beskriv det med dine egne ord.
  • +
+

Udarbejdelse af denne erklæring

+

Erklæringen blev udarbejdet den 21. august 2026.

+

Den bygger på en selvevaluering foretaget af Musterbetrieb GmbH.

+

Den automatiske test den 21. august 2026 omfattede 8 sider på dette website. Yderligere 2 regelkontroller kræver en manuel vurdering. 12 regelkontroller kunne ikke afgøres af det anvendte værktøj; de angives ikke som opfyldt.

+

Vurderingen bygger delvis på automatisk test. Automatiske værktøjer finder kun en del af de mulige barrierer; de erstatter ikke manuel test eller test med hjælpemidler.

+

Feedback og kontaktoplysninger

+

Er du stødt på en barriere, eller har du brug for oplysninger i et tilgængeligt format? Kontakt os:

+ +

Vi bestræber os på at svare hurtigst muligt.

+

Klageadgang

+

Hvis du ikke er tilfreds med vores svar, kan du henvende dig til Sikkerhedsstyrelsen, som fører tilsyn med e-handelstjenester efter loven. Tilsynet i Danmark er fordelt på flere myndigheder: med finansielle tjenester fører Finanstilsynet for eksempel tilsyn.

+

Sikkerhedsstyrelsen https://www.sik.dk

+

Offentlige myndigheders websteder og mobilapplikationer er omfattet af en anden lov, som kræver en særskilt tilgængelighedserklæring. Dette dokument erstatter ikke den.

+
+

Denne erklæring er udarbejdet med eaa-kit og er ikke juridisk rådgivning. Gennemgå den, før den offentliggøres, og få den vurderet af en jurist, hvis du er i tvivl.

+
+ + diff --git a/tests/statement/__snapshots__/statement.dk.da.md b/tests/statement/__snapshots__/statement.dk.da.md new file mode 100644 index 0000000..c4ddc5a --- /dev/null +++ b/tests/statement/__snapshots__/statement.dk.da.md @@ -0,0 +1,85 @@ +# Tilgængelighedserklæring + +Musterbetrieb GmbH arbejder på at gøre websitet Musterbetrieb tilgængeligt i +overensstemmelse med lov nr. 801 af 7. juni 2022 om tilgængelighedskrav for produkter og +tjenester, som gennemfører direktiv (EU) 2019/882 (tilgængelighedsdirektivet) i dansk ret. +Lovens krav gælder for tjenester, der leveres fra den 28. juni 2025. + +Denne tilgængelighedserklæring gælder for https://example.at. + +## Overholdelsesstatus + +Dette website overholder delvist EN 301 549 V3.2.1 (WCAG 2.2 AA). Det indhold, der er angivet +nedenfor, er ikke tilgængeligt af de anførte grunde. + +## Indhold, der ikke er tilgængeligt + +- Die eingebettete Karte hat keinen Titel. + Berørt krav: WCAG 4.1.2, EN 301 549 9.4.1.2 + Begrundelse: barrieren er kendt og ved at blive udbedret. + Forventes udbedret senest: 31. december 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Begrundelse: uforholdsmæssig stor byrde. +- Form field must not have multiple label elements + Berørte sider: index.html + Begrundelse: barrieren er kendt og ved at blive udbedret. + Fundet ved automatisk test (axe-core, regel form-field-multiple-labels); beskriv det med dine egne ord. +- Images must have alternative text + Berørt krav: WCAG 1.1.1, EN 301 549 9.1.1.1 + Berørte sider: index.html + Begrundelse: barrieren er kendt og ved at blive udbedret. + Fundet ved automatisk test (axe-core, regel image-alt); beskriv det med dine egne ord. +- Elements must meet minimum color contrast ratio thresholds + Berørt krav: WCAG 1.4.3, EN 301 549 9.1.4.3 + Berørte sider: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html og 2 andre + Begrundelse: barrieren er kendt og ved at blive udbedret. + Fundet ved automatisk test (axe-core, regel color-contrast); beskriv det med dine egne ord. +- Document should have one main landmark + Berørt krav: WCAG 1.3.1, EN 301 549 9.1.3.1 + Berørte sider: kontakt.html, team.html + Begrundelse: barrieren er kendt og ved at blive udbedret. + Fundet ved automatisk test (axe-core, regel landmark-one-main); beskriv det med dine egne ord. + +## Udarbejdelse af denne erklæring + +Erklæringen blev udarbejdet den 21. august 2026. + +Den bygger på en selvevaluering foretaget af Musterbetrieb GmbH. + +Den automatiske test den 21. august 2026 omfattede 8 sider på +dette website. +Yderligere 2 regelkontroller kræver en manuel vurdering. +12 regelkontroller kunne ikke afgøres af det anvendte værktøj; de +angives ikke som opfyldt. + +Vurderingen bygger delvis på automatisk test. Automatiske værktøjer finder kun en del af de +mulige barrierer; de erstatter ikke manuel test eller test med hjælpemidler. + +## Feedback og kontaktoplysninger + +Er du stødt på en barriere, eller har du brug for oplysninger i et tilgængeligt format? +Kontakt os: + +- E-mail: office@example.at +- Kontaktformular: https://example.at/kontakt +- Telefon: +43 1 2345678 +- Adresse: Hauptstraße 1, 1010 Wien + +Vi bestræber os på at svare hurtigst muligt. + +## Klageadgang + +Hvis du ikke er tilfreds med vores svar, kan du henvende dig til Sikkerhedsstyrelsen, som +fører tilsyn med e-handelstjenester efter loven. Tilsynet i Danmark er fordelt på flere +myndigheder: med finansielle tjenester fører Finanstilsynet for eksempel tilsyn. + +Sikkerhedsstyrelsen +https://www.sik.dk + +Offentlige myndigheders websteder og mobilapplikationer er omfattet af en anden lov, som +kræver en særskilt tilgængelighedserklæring. Dette dokument erstatter ikke den. + +--- + +Denne erklæring er udarbejdet med eaa-kit og er ikke juridisk rådgivning. Gennemgå den, før +den offentliggøres, og få den vurderet af en jurist, hvis du er i tvivl. diff --git a/tests/statement/__snapshots__/statement.dk.en.html b/tests/statement/__snapshots__/statement.dk.en.html new file mode 100644 index 0000000..1e646e7 --- /dev/null +++ b/tests/statement/__snapshots__/statement.dk.en.html @@ -0,0 +1,84 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with Act No. 801 of 7 June 2022 on accessibility requirements for products and services, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Danish law. Its requirements apply to services provided 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 contact the Danish Safety Technology Authority (Sikkerhedsstyrelsen), which supervises e-commerce services under the Act. Supervision in Denmark is split between several authorities: financial services, for example, are supervised by the Danish Financial Supervisory Authority (Finanstilsynet).

+

Sikkerhedsstyrelsen https://www.sik.dk

+

Websites and mobile applications of public sector bodies fall under a separate act, which requires an accessibility statement 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.dk.en.md b/tests/statement/__snapshots__/statement.dk.en.md new file mode 100644 index 0000000..2b3a63d --- /dev/null +++ b/tests/statement/__snapshots__/statement.dk.en.md @@ -0,0 +1,86 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with Act No. 801 of 7 June 2022 on accessibility requirements for products and +services, which transposes Directive (EU) 2019/882 (the European Accessibility Act) into +Danish law. Its requirements apply to services provided 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 contact the Danish Safety Technology +Authority (Sikkerhedsstyrelsen), which supervises e-commerce services under the Act. +Supervision in Denmark is split between several authorities: financial services, for +example, are supervised by the Danish Financial Supervisory Authority (Finanstilsynet). + +Sikkerhedsstyrelsen +https://www.sik.dk + +Websites and mobile applications of public sector bodies fall under a separate act, which +requires an accessibility statement 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.fi.en.html b/tests/statement/__snapshots__/statement.fi.en.html new file mode 100644 index 0000000..da0b917 --- /dev/null +++ b/tests/statement/__snapshots__/statement.fi.en.html @@ -0,0 +1,84 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with the Act on the Provision of Digital Services (306/2019), as amended to transpose Directive (EU) 2019/882 (the European Accessibility Act) into Finnish law. Its accessibility requirements for e-commerce 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 contact the Finnish Transport and Communications Agency (Traficom), which supervises the accessibility of digital services.

+

Liikenne- ja viestintävirasto Traficom https://www.traficom.fi

+

The Act sets content requirements of its own for the accessibility statements of public sector bodies. 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.fi.en.md b/tests/statement/__snapshots__/statement.fi.en.md new file mode 100644 index 0000000..00230e4 --- /dev/null +++ b/tests/statement/__snapshots__/statement.fi.en.md @@ -0,0 +1,84 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with the Act on the Provision of Digital Services (306/2019), as amended to +transpose Directive (EU) 2019/882 (the European Accessibility Act) into Finnish law. Its +accessibility requirements for e-commerce 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 contact the Finnish Transport and +Communications Agency (Traficom), which supervises the accessibility of digital services. + +Liikenne- ja viestintävirasto Traficom +https://www.traficom.fi + +The Act sets content requirements of its own for the accessibility statements of public +sector bodies. 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.fi.fi.html b/tests/statement/__snapshots__/statement.fi.fi.html new file mode 100644 index 0000000..a51cadb --- /dev/null +++ b/tests/statement/__snapshots__/statement.fi.fi.html @@ -0,0 +1,84 @@ + + + + + + +Saavutettavuusseloste + + + +
+

Saavutettavuusseloste

+

Musterbetrieb GmbH pyrkii siihen, että verkkosivusto Musterbetrieb on saavutettava lain digitaalisten palvelujen tarjoamisesta (306/2019) mukaisesti. Lakia on muutettu direktiivin (EU) 2019/882 (esteettömyysdirektiivi) panemiseksi täytäntöön Suomessa. Verkkokauppapalveluja koskevia saavutettavuusvaatimuksia sovelletaan 28.6.2025 alkaen.

+

Tämä saavutettavuusseloste koskee sivustoa https://example.at.

+

Vaatimustenmukaisuuden tila

+

Tämä verkkosivusto on osittain yhdenmukainen standardin EN 301 549 V3.2.1 (WCAG 2.2 AA) kanssa. Alla luetellut sisällöt eivät ole saavutettavia mainituista syistä.

+

Sisältö, joka ei ole saavutettavaa

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Koskee vaatimusta: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Syy: puute tiedetään ja sitä korjataan.
    + Korjataan viimeistään: 31. joulukuuta 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Syy: kohtuuton rasite.
  • +
  • Form field must not have multiple label elements
    + Koskee sivuja: index.html
    + Syy: puute tiedetään ja sitä korjataan.
    + Havaittu automaattisessa testauksessa (axe-core, sääntö form-field-multiple-labels); kuvaa puute omin sanoin.
  • +
  • Images must have alternative text
    + Koskee vaatimusta: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Koskee sivuja: index.html
    + Syy: puute tiedetään ja sitä korjataan.
    + Havaittu automaattisessa testauksessa (axe-core, sääntö image-alt); kuvaa puute omin sanoin.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Koskee vaatimusta: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Koskee sivuja: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html ja 2 muuta
    + Syy: puute tiedetään ja sitä korjataan.
    + Havaittu automaattisessa testauksessa (axe-core, sääntö color-contrast); kuvaa puute omin sanoin.
  • +
  • Document should have one main landmark
    + Koskee vaatimusta: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Koskee sivuja: kontakt.html, team.html
    + Syy: puute tiedetään ja sitä korjataan.
    + Havaittu automaattisessa testauksessa (axe-core, sääntö landmark-one-main); kuvaa puute omin sanoin.
  • +
+

Selosteen laatiminen

+

Tämä seloste on laadittu 21. elokuuta 2026.

+

Se perustuu itsearviointiin, jonka on tehnyt Musterbetrieb GmbH.

+

Automaattinen testaus 21. elokuuta 2026 kattoi 8 sivua tältä sivustolta. 2 muuta sääntötarkistusta edellyttää ihmisen arviota. Käytetty työkalu ei pystynyt ratkaisemaan 12 sääntötarkistusta; niitä ei esitetä täyttyneinä.

+

Arviointi perustuu osittain automaattiseen testaukseen. Automaattiset työkalut löytävät vain osan mahdollisista puutteista; ne eivät korvaa manuaalista testausta eikä testausta avustavilla teknologioilla.

+

Palaute ja yhteystiedot

+

Huomasitko saavutettavuuspuutteen, tai tarvitsetko tietoa saavutettavassa muodossa? Ota yhteyttä:

+ +

Pyrimme vastaamaan mahdollisimman pian.

+

Valvontaviranomainen

+

Jos et ole tyytyväinen vastaukseemme, voit ottaa yhteyttä Liikenne- ja viestintävirasto Traficomiin, joka valvoo digitaalisten palvelujen saavutettavuutta.

+

Liikenne- ja viestintävirasto Traficom https://www.traficom.fi

+

Laissa on omat sisältövaatimuksensa julkisen sektorin toimijoiden saavutettavuusselosteille. Tämä asiakirja ei korvaa sellaista selostetta.

+
+

Tämä seloste on laadittu eaa-kit-työkalulla, eikä se ole oikeudellista neuvontaa. Tarkista se ennen julkaisua, ja pyydä epäselvissä tapauksissa juristin arvio.

+
+ + diff --git a/tests/statement/__snapshots__/statement.fi.fi.md b/tests/statement/__snapshots__/statement.fi.fi.md new file mode 100644 index 0000000..f926f3e --- /dev/null +++ b/tests/statement/__snapshots__/statement.fi.fi.md @@ -0,0 +1,85 @@ +# Saavutettavuusseloste + +Musterbetrieb GmbH pyrkii siihen, että verkkosivusto Musterbetrieb on saavutettava +lain digitaalisten palvelujen tarjoamisesta (306/2019) mukaisesti. Lakia on muutettu +direktiivin (EU) 2019/882 (esteettömyysdirektiivi) panemiseksi täytäntöön Suomessa. +Verkkokauppapalveluja koskevia saavutettavuusvaatimuksia sovelletaan 28.6.2025 alkaen. + +Tämä saavutettavuusseloste koskee sivustoa https://example.at. + +## Vaatimustenmukaisuuden tila + +Tämä verkkosivusto on osittain yhdenmukainen standardin EN 301 549 V3.2.1 (WCAG 2.2 AA) kanssa. +Alla luetellut sisällöt eivät ole saavutettavia mainituista syistä. + +## Sisältö, joka ei ole saavutettavaa + +- Die eingebettete Karte hat keinen Titel. + Koskee vaatimusta: WCAG 4.1.2, EN 301 549 9.4.1.2 + Syy: puute tiedetään ja sitä korjataan. + Korjataan viimeistään: 31. joulukuuta 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Syy: kohtuuton rasite. +- Form field must not have multiple label elements + Koskee sivuja: index.html + Syy: puute tiedetään ja sitä korjataan. + Havaittu automaattisessa testauksessa (axe-core, sääntö form-field-multiple-labels); kuvaa puute omin sanoin. +- Images must have alternative text + Koskee vaatimusta: WCAG 1.1.1, EN 301 549 9.1.1.1 + Koskee sivuja: index.html + Syy: puute tiedetään ja sitä korjataan. + Havaittu automaattisessa testauksessa (axe-core, sääntö image-alt); kuvaa puute omin sanoin. +- Elements must meet minimum color contrast ratio thresholds + Koskee vaatimusta: WCAG 1.4.3, EN 301 549 9.1.4.3 + Koskee sivuja: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html ja 2 muuta + Syy: puute tiedetään ja sitä korjataan. + Havaittu automaattisessa testauksessa (axe-core, sääntö color-contrast); kuvaa puute omin sanoin. +- Document should have one main landmark + Koskee vaatimusta: WCAG 1.3.1, EN 301 549 9.1.3.1 + Koskee sivuja: kontakt.html, team.html + Syy: puute tiedetään ja sitä korjataan. + Havaittu automaattisessa testauksessa (axe-core, sääntö landmark-one-main); kuvaa puute omin sanoin. + +## Selosteen laatiminen + +Tämä seloste on laadittu 21. elokuuta 2026. + +Se perustuu itsearviointiin, jonka on tehnyt Musterbetrieb GmbH. + +Automaattinen testaus 21. elokuuta 2026 kattoi 8 sivua tältä +sivustolta. +2 muuta sääntötarkistusta edellyttää ihmisen arviota. +Käytetty työkalu ei pystynyt ratkaisemaan 12 sääntötarkistusta; niitä +ei esitetä täyttyneinä. + +Arviointi perustuu osittain automaattiseen testaukseen. Automaattiset työkalut löytävät vain +osan mahdollisista puutteista; ne eivät korvaa manuaalista testausta eikä testausta +avustavilla teknologioilla. + +## Palaute ja yhteystiedot + +Huomasitko saavutettavuuspuutteen, tai tarvitsetko tietoa saavutettavassa muodossa? Ota +yhteyttä: + +- Sähköposti: office@example.at +- Yhteydenottolomake: https://example.at/kontakt +- Puhelin: +43 1 2345678 +- Osoite: Hauptstraße 1, 1010 Wien + +Pyrimme vastaamaan mahdollisimman pian. + +## Valvontaviranomainen + +Jos et ole tyytyväinen vastaukseemme, voit ottaa yhteyttä Liikenne- ja viestintävirasto +Traficomiin, joka valvoo digitaalisten palvelujen saavutettavuutta. + +Liikenne- ja viestintävirasto Traficom +https://www.traficom.fi + +Laissa on omat sisältövaatimuksensa julkisen sektorin toimijoiden saavutettavuusselosteille. +Tämä asiakirja ei korvaa sellaista selostetta. + +--- + +Tämä seloste on laadittu eaa-kit-työkalulla, eikä se ole oikeudellista neuvontaa. Tarkista +se ennen julkaisua, ja pyydä epäselvissä tapauksissa juristin arvio. diff --git a/tests/statement/__snapshots__/statement.se.en.html b/tests/statement/__snapshots__/statement.se.en.html new file mode 100644 index 0000000..c0984e8 --- /dev/null +++ b/tests/statement/__snapshots__/statement.se.en.html @@ -0,0 +1,84 @@ + + + + + + +Accessibility Statement + + + +
+

Accessibility Statement

+

Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in accordance with the Act on the Accessibility of Certain Products and Services (SFS 2023:254), which transposes Directive (EU) 2019/882 (the European Accessibility Act) into Swedish law. Its requirements 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 matter to the Swedish Post and Telecom Authority (PTS), which supervises e-commerce services under the Act.

+

Post- och telestyrelsen (PTS) https://pts.se

+

Websites and mobile applications of public sector bodies fall under a separate act, which requires an accessibility statement 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.se.en.md b/tests/statement/__snapshots__/statement.se.en.md new file mode 100644 index 0000000..dad3187 --- /dev/null +++ b/tests/statement/__snapshots__/statement.se.en.md @@ -0,0 +1,84 @@ +# Accessibility Statement + +Musterbetrieb GmbH is committed to making the website Musterbetrieb accessible in +accordance with the Act on the Accessibility of Certain Products and Services (SFS +2023:254), which transposes Directive (EU) 2019/882 (the European Accessibility Act) into +Swedish law. Its requirements 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 matter to the Swedish Post +and Telecom Authority (PTS), which supervises e-commerce services under the Act. + +Post- och telestyrelsen (PTS) +https://pts.se + +Websites and mobile applications of public sector bodies fall under a separate act, which +requires an accessibility statement 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.se.sv.html b/tests/statement/__snapshots__/statement.se.sv.html new file mode 100644 index 0000000..3f538a0 --- /dev/null +++ b/tests/statement/__snapshots__/statement.se.sv.html @@ -0,0 +1,84 @@ + + + + + + +Tillgänglighetsredogörelse + + + +
+

Tillgänglighetsredogörelse

+

Musterbetrieb GmbH strävar efter att göra webbplatsen Musterbetrieb tillgänglig i enlighet med lagen (2023:254) om vissa produkters och tjänsters tillgänglighet, som genomför direktiv (EU) 2019/882 (tillgänglighetsdirektivet) i svensk rätt. Lagens krav gäller från den 28 juni 2025.

+

Den här tillgänglighetsredogörelsen gäller https://example.at.

+

Efterlevnadsstatus

+

Webbplatsen är delvis förenlig med EN 301 549 V3.2.1 (WCAG 2.2 AA). Innehållet som anges nedan är inte tillgängligt, av de skäl som anges.

+

Innehåll som inte är tillgängligt

+
    +
  • Die eingebettete Karte hat keinen Titel.
    + Berört krav: WCAG 4.1.2, EN 301 549 9.4.1.2
    + Skäl: bristen är känd och håller på att åtgärdas.
    + Planeras vara åtgärdat senast: 31 december 2026
  • +
  • Ältere PDF-Dokumente sind nicht barrierefrei.
    + Skäl: oskälig börda.
  • +
  • Form field must not have multiple label elements
    + Berörda sidor: index.html
    + Skäl: bristen är känd och håller på att åtgärdas.
    + Upptäckt vid automatiserad testning (axe-core, regel form-field-multiple-labels); beskriv det med egna ord.
  • +
  • Images must have alternative text
    + Berört krav: WCAG 1.1.1, EN 301 549 9.1.1.1
    + Berörda sidor: index.html
    + Skäl: bristen är känd och håller på att åtgärdas.
    + Upptäckt vid automatiserad testning (axe-core, regel image-alt); beskriv det med egna ord.
  • +
  • Elements must meet minimum color contrast ratio thresholds
    + Berört krav: WCAG 1.4.3, EN 301 549 9.1.4.3
    + Berörda sidor: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html och 2 till
    + Skäl: bristen är känd och håller på att åtgärdas.
    + Upptäckt vid automatiserad testning (axe-core, regel color-contrast); beskriv det med egna ord.
  • +
  • Document should have one main landmark
    + Berört krav: WCAG 1.3.1, EN 301 549 9.1.3.1
    + Berörda sidor: kontakt.html, team.html
    + Skäl: bristen är känd och håller på att åtgärdas.
    + Upptäckt vid automatiserad testning (axe-core, regel landmark-one-main); beskriv det med egna ord.
  • +
+

Hur redogörelsen har tagits fram

+

Redogörelsen upprättades den 21 augusti 2026.

+

Den bygger på en självskattning som Musterbetrieb GmbH har gjort.

+

Den automatiserade testningen den 21 augusti 2026 omfattade 8 sidor på webbplatsen. Ytterligare 2 regelkontroller kräver en manuell bedömning. 12 regelkontroller kunde inte avgöras av verktyget som användes; de redovisas inte som uppfyllda.

+

Bedömningen bygger delvis på automatiserad testning. Automatiserade verktyg hittar bara en del av de möjliga bristerna; de ersätter inte manuell testning eller testning med hjälpmedel.

+

Återkoppling och kontaktuppgifter

+

Har du hittat en brist, eller behöver du information i ett tillgängligt format? Kontakta oss:

+ +

Vi strävar efter att svara så snart som möjligt.

+

Tillsyn

+

Om du inte är nöjd med vårt svar kan du anmäla saken till Post- och telestyrelsen (PTS), som har tillsyn över e-handelstjänster enligt lagen.

+

Post- och telestyrelsen (PTS) https://pts.se

+

Offentliga aktörers webbplatser och mobila applikationer omfattas av en annan lag, som kräver en egen tillgänglighetsredogörelse. Det här dokumentet ersätter inte den.

+
+

Redogörelsen har tagits fram med eaa-kit och är inte juridisk rådgivning. Granska den innan den publiceras, och låt en jurist granska den om du är osäker.

+
+ + diff --git a/tests/statement/__snapshots__/statement.se.sv.md b/tests/statement/__snapshots__/statement.se.sv.md new file mode 100644 index 0000000..a7e6a70 --- /dev/null +++ b/tests/statement/__snapshots__/statement.se.sv.md @@ -0,0 +1,85 @@ +# Tillgänglighetsredogörelse + +Musterbetrieb GmbH strävar efter att göra webbplatsen Musterbetrieb tillgänglig i +enlighet med lagen (2023:254) om vissa produkters och tjänsters tillgänglighet, som genomför +direktiv (EU) 2019/882 (tillgänglighetsdirektivet) i svensk rätt. Lagens krav gäller från +den 28 juni 2025. + +Den här tillgänglighetsredogörelsen gäller https://example.at. + +## Efterlevnadsstatus + +Webbplatsen är delvis förenlig med EN 301 549 V3.2.1 (WCAG 2.2 AA). Innehållet som anges nedan är +inte tillgängligt, av de skäl som anges. + +## Innehåll som inte är tillgängligt + +- Die eingebettete Karte hat keinen Titel. + Berört krav: WCAG 4.1.2, EN 301 549 9.4.1.2 + Skäl: bristen är känd och håller på att åtgärdas. + Planeras vara åtgärdat senast: 31 december 2026 +- Ältere PDF-Dokumente sind nicht barrierefrei. + Skäl: oskälig börda. +- Form field must not have multiple label elements + Berörda sidor: index.html + Skäl: bristen är känd och håller på att åtgärdas. + Upptäckt vid automatiserad testning (axe-core, regel form-field-multiple-labels); beskriv det med egna ord. +- Images must have alternative text + Berört krav: WCAG 1.1.1, EN 301 549 9.1.1.1 + Berörda sidor: index.html + Skäl: bristen är känd och håller på att åtgärdas. + Upptäckt vid automatiserad testning (axe-core, regel image-alt); beskriv det med egna ord. +- Elements must meet minimum color contrast ratio thresholds + Berört krav: WCAG 1.4.3, EN 301 549 9.1.4.3 + Berörda sidor: blog/2026-06-eaa.html, blog/index.html, impressum.html, index.html, kontakt.html och 2 till + Skäl: bristen är känd och håller på att åtgärdas. + Upptäckt vid automatiserad testning (axe-core, regel color-contrast); beskriv det med egna ord. +- Document should have one main landmark + Berört krav: WCAG 1.3.1, EN 301 549 9.1.3.1 + Berörda sidor: kontakt.html, team.html + Skäl: bristen är känd och håller på att åtgärdas. + Upptäckt vid automatiserad testning (axe-core, regel landmark-one-main); beskriv det med egna ord. + +## Hur redogörelsen har tagits fram + +Redogörelsen upprättades den 21 augusti 2026. + +Den bygger på en självskattning som Musterbetrieb GmbH har gjort. + +Den automatiserade testningen den 21 augusti 2026 omfattade 8 +sidor på webbplatsen. +Ytterligare 2 regelkontroller kräver en manuell bedömning. +12 regelkontroller kunde inte avgöras av verktyget som användes; de +redovisas inte som uppfyllda. + +Bedömningen bygger delvis på automatiserad testning. Automatiserade verktyg hittar bara en +del av de möjliga bristerna; de ersätter inte manuell testning eller testning med +hjälpmedel. + +## Återkoppling och kontaktuppgifter + +Har du hittat en brist, eller behöver du information i ett tillgängligt format? Kontakta +oss: + +- E-post: office@example.at +- Kontaktformulär: https://example.at/kontakt +- Telefon: +43 1 2345678 +- Adress: Hauptstraße 1, 1010 Wien + +Vi strävar efter att svara så snart som möjligt. + +## Tillsyn + +Om du inte är nöjd med vårt svar kan du anmäla saken till Post- och telestyrelsen (PTS), som +har tillsyn över e-handelstjänster enligt lagen. + +Post- och telestyrelsen (PTS) +https://pts.se + +Offentliga aktörers webbplatser och mobila applikationer omfattas av en annan lag, som +kräver en egen tillgänglighetsredogörelse. Det här dokumentet ersätter inte den. + +--- + +Redogörelsen har tagits fram med eaa-kit och är inte juridisk rådgivning. Granska den innan +den publiceras, och låt en jurist granska den om du är osäker. diff --git a/tests/statement/render.test.ts b/tests/statement/render.test.ts index 2a99ef1..c27f473 100644 --- a/tests/statement/render.test.ts +++ b/tests/statement/render.test.ts @@ -114,7 +114,7 @@ describe('template selection', () => { await expect(renderStatement(config(), missing)).rejects.toThrow(StatementError) await expect(renderStatement(config(), missing)).rejects.toThrow( - /Available: at\.de, at\.en, be\.en, be\.fr, be\.nl, ch\.de, ch\.en, de\.de/, + /Available: at\.de, at\.en, be\.de, be\.en, be\.fr, be\.nl, ch\.de, ch\.en/, ) }) }) @@ -293,6 +293,15 @@ describe('enforcement body', () => { ['PL', 'en', 'State Fund for Rehabilitation of Disabled Persons (PFRON)'], ['PT', 'pt', 'Autoridade Nacional de Comunicações (ANACOM)'], ['PT', 'en', 'National Communications Authority (ANACOM)'], + ['BE', 'de', 'FÖD Wirtschaft'], + ['CZ', 'cs', 'Českou obchodní inspekci (ČOI)'], + ['CZ', 'en', 'Czech Trade Inspection Authority (ČOI)'], + ['DK', 'da', 'Sikkerhedsstyrelsen'], + ['DK', 'en', 'Danish Safety Technology Authority (Sikkerhedsstyrelsen)'], + ['FI', 'fi', 'Traficomiin'], + ['FI', 'en', 'Finnish Transport and Communications Agency (Traficom)'], + ['SE', 'sv', 'Post- och telestyrelsen (PTS)'], + ['SE', 'en', 'Swedish Post and Telecom Authority (PTS)'], ] as Array<[Country, StatementLocale, string]>)( 'names the %s authority in %s', async (country, locale, authority) => { @@ -307,6 +316,9 @@ describe('enforcement body', () => { ['BE', 'fr'], ['BE', 'nl'], ['BE', 'en'], + ['BE', 'de'], + ['DK', 'da'], + ['DK', 'en'], ['PT', 'pt'], ['PT', 'en'], ] as Array<[Country, StatementLocale]>)( @@ -314,7 +326,9 @@ describe('enforcement body', () => { async (country, locale) => { const statement = await renderStatement(config(), { country, locale }) - expect(flat(statement.markdown)).toMatch(/réparti|verdeeld|split|repartida/) + expect(flat(statement.markdown)).toMatch( + /réparti|verdeeld|split|repartida|aufgeteilt|fordelt/, + ) }, ) }) @@ -728,6 +742,15 @@ describe('what a person checked', () => { ['PL', 'en', 'success criteria in WCAG 2.2'], ['PT', 'pt', 'critérios de sucesso das WCAG 2.2'], ['PT', 'en', 'success criteria in WCAG 2.2'], + ['BE', 'de', 'Erfolgskriterien der WCAG 2.2'], + ['CZ', 'cs', 'kritérií úspěšnosti WCAG 2.2'], + ['CZ', 'en', 'success criteria in WCAG 2.2'], + ['DK', 'da', 'succeskriterier i WCAG 2.2'], + ['DK', 'en', 'success criteria in WCAG 2.2'], + ['FI', 'fi', 'WCAG 2.2:n onnistumiskriteeristä'], + ['FI', 'en', 'success criteria in WCAG 2.2'], + ['SE', 'sv', 'framgångskriterierna i WCAG 2.2'], + ['SE', 'en', 'success criteria in WCAG 2.2'], ] as Array<[Country, StatementLocale, string]>)( 'reaches the document in %s/%s', async (country, locale, phrase) => { diff --git a/tests/statement/snapshot.test.ts b/tests/statement/snapshot.test.ts index b4870d8..4c54bcc 100644 --- a/tests/statement/snapshot.test.ts +++ b/tests/statement/snapshot.test.ts @@ -38,15 +38,22 @@ function stable(html: string): string { const COMBINATIONS = [ { country: 'AT', locale: 'de' }, { country: 'AT', locale: 'en' }, + { country: 'BE', locale: 'de' }, { country: 'BE', locale: 'en' }, { country: 'BE', locale: 'fr' }, { country: 'BE', locale: 'nl' }, { country: 'CH', locale: 'de' }, { country: 'CH', locale: 'en' }, + { country: 'CZ', locale: 'cs' }, + { country: 'CZ', locale: 'en' }, { country: 'DE', locale: 'de' }, { country: 'DE', locale: 'en' }, + { country: 'DK', locale: 'da' }, + { country: 'DK', locale: 'en' }, { country: 'ES', locale: 'en' }, { country: 'ES', locale: 'es' }, + { country: 'FI', locale: 'en' }, + { country: 'FI', locale: 'fi' }, { country: 'FR', locale: 'en' }, { country: 'FR', locale: 'fr' }, { country: 'IE', locale: 'en' }, @@ -58,6 +65,8 @@ const COMBINATIONS = [ { country: 'PL', locale: 'pl' }, { country: 'PT', locale: 'en' }, { country: 'PT', locale: 'pt' }, + { country: 'SE', locale: 'en' }, + { country: 'SE', locale: 'sv' }, ] as const async function fixtures() {