@owlmeans/web-consent 0.1.18-rc.24 → 0.1.18-rc.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-consent/SKILL.md +13 -1
- package/build/consent/toggle.d.ts +6 -0
- package/build/consent/toggle.d.ts.map +1 -1
- package/build/consent/toggle.js +13 -1
- package/build/consent/toggle.js.map +1 -1
- package/package.json +3 -2
- package/src/consent/toggle.tsx +49 -32
- package/tests/a11y.spec.ts +117 -0
- package/tests/harness/mount.tsx +9 -0
- package/tests/harness/styled.css +54 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
6
6
|
your project's skill store (`.agents/skills/`):
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
9
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.32
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-consent",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.26",
|
|
5
|
+
"generatedAt": "2026-09-22T12:36:21.844Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/web-consent
|
|
9
9
|
|
|
10
10
|
**Layer:** Web (React)
|
|
11
|
-
**Install:** `"@owlmeans/web-consent": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/web-consent": "^0.1.18-rc.26"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
The browser components of the consent set. The model — categories, storage, migration, the store,
|
|
14
14
|
Consent Mode signalling — is `@owlmeans/consent`, and this package re-exports a **named selection**
|
|
@@ -96,6 +96,18 @@ Every control shows a 3px `outline-ring` focus outline 3px off the element (`out
|
|
|
96
96
|
outline-offset-3`; the switch draws it on its track through `peer-focus-visible`). An outline, not
|
|
97
97
|
a ring, so a host that zeroes shadows keeps it. Never reintroduce a decorative class here — the
|
|
98
98
|
package tests fail on `bg-gradient`, `backdrop-`, `blur`, `shadow` or `ring-primary` in the dialog.
|
|
99
|
+
|
|
100
|
+
**Each switch is named by reference.** The `<label>` around a switch's checkbox holds only the
|
|
101
|
+
drawn track, and the category's words sit in a sibling column it cannot reach, so the checkbox
|
|
102
|
+
carries `aria-labelledby="<id>-label"` (the category label) and `aria-describedby` pointing at
|
|
103
|
+
`<id>-required` and `<id>-desc`. Drop those and axe reports three unnamed checkboxes (`label`,
|
|
104
|
+
critical). A required category stays `disabled` and keeps its name.
|
|
105
|
+
|
|
106
|
+
`tests/a11y.spec.ts` runs axe-core (a dev dependency) against the open dialog in both schemes, the
|
|
107
|
+
re-open button, and the policy page with services, and fails on any serious or critical
|
|
108
|
+
violation. It loads the harness with `?styled=1` (plus `&theme=dark`), which compiles the package
|
|
109
|
+
sources with a generated app's neutral tokens — contrast cannot be judged on the unstyled harness
|
|
110
|
+
the other specs need.
|
|
99
111
|
- `silent` skips every `dataLayer` and global write. It is for tests and for an app that runs no
|
|
100
112
|
tags at all.
|
|
101
113
|
- The draft is **re-seeded from storage every time the dialog opens**, not from the last render — a
|
|
@@ -17,6 +17,12 @@ export interface ConsentToggleProps {
|
|
|
17
17
|
* switch's hit area is 44px square around the visible 24×44 track, and it shows the dialog's 3px
|
|
18
18
|
* focus ring when reached from the keyboard (the real checkbox is visually hidden, so the ring is
|
|
19
19
|
* drawn on the track through `peer-focus-visible`).
|
|
20
|
+
*
|
|
21
|
+
* The checkbox takes its NAME from the category label and its description from the `Required`
|
|
22
|
+
* marker and the description text, by id. The `<label>` around it holds only the drawn track, so
|
|
23
|
+
* without those references a screen reader announces an unnamed checkbox three times over — the
|
|
24
|
+
* visible words sit in a sibling column the label cannot reach. A required category keeps its name
|
|
25
|
+
* while disabled: it is still disclosed, only not a choice.
|
|
20
26
|
*/
|
|
21
27
|
export declare const ConsentToggle: FC<ConsentToggleProps>;
|
|
22
28
|
//# sourceMappingURL=toggle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toggle.d.ts","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAG/B,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED
|
|
1
|
+
{"version":3,"file":"toggle.d.ts","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAG/B,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CA+ChD,CAAA"}
|
package/build/consent/toggle.js
CHANGED
|
@@ -8,6 +8,18 @@ import { cn } from '../lib/utils.js';
|
|
|
8
8
|
* switch's hit area is 44px square around the visible 24×44 track, and it shows the dialog's 3px
|
|
9
9
|
* focus ring when reached from the keyboard (the real checkbox is visually hidden, so the ring is
|
|
10
10
|
* drawn on the track through `peer-focus-visible`).
|
|
11
|
+
*
|
|
12
|
+
* The checkbox takes its NAME from the category label and its description from the `Required`
|
|
13
|
+
* marker and the description text, by id. The `<label>` around it holds only the drawn track, so
|
|
14
|
+
* without those references a screen reader announces an unnamed checkbox three times over — the
|
|
15
|
+
* visible words sit in a sibling column the label cannot reach. A required category keeps its name
|
|
16
|
+
* while disabled: it is still disclosed, only not a choice.
|
|
11
17
|
*/
|
|
12
|
-
export const ConsentToggle = ({ id, label, description, checked, required, requiredLabel, onChange }) =>
|
|
18
|
+
export const ConsentToggle = ({ id, label, description, checked, required, requiredLabel, onChange }) => {
|
|
19
|
+
const described = [
|
|
20
|
+
...(required === true ? [`${id}-required`] : []),
|
|
21
|
+
...(description !== '' ? [`${id}-desc`] : []),
|
|
22
|
+
];
|
|
23
|
+
return (_jsxs("div", { className: "flex items-center justify-between gap-4 rounded-2xl border border-border p-4", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "mb-0.5 flex flex-wrap items-center gap-x-2 text-sm font-bold text-foreground", children: [_jsx("span", { id: `${id}-label`, children: label }), required === true && _jsx("span", { id: `${id}-required`, className: "text-xs font-bold uppercase tracking-wide text-muted-foreground", children: requiredLabel })] }), description !== '' && _jsx("p", { id: `${id}-desc`, className: "text-pretty text-xs leading-relaxed text-muted-foreground", children: description })] }), _jsxs("label", { className: cn('relative inline-flex h-11 w-14 flex-shrink-0 items-center justify-center', required === true ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'), children: [_jsx("input", { id: id, type: "checkbox", "aria-labelledby": `${id}-label`, ...(described.length > 0 ? { 'aria-describedby': described.join(' ') } : {}), className: "peer sr-only", checked: checked || required === true, disabled: required === true, onChange: event => onChange(event.target.checked) }), _jsx("span", { className: cn('relative h-6 w-11 rounded-full transition-colors duration-300 peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-ring', checked || required === true ? 'bg-primary' : 'bg-muted-foreground/40'), children: _jsx("span", { className: cn('absolute top-0.5 left-0 h-5 w-5 rounded-full bg-white transition-transform duration-300', checked || required === true ? 'translate-x-5' : 'translate-x-0.5') }) })] })] }));
|
|
24
|
+
};
|
|
13
25
|
//# sourceMappingURL=toggle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAapC
|
|
1
|
+
{"version":3,"file":"toggle.js","sourceRoot":"","sources":["../../src/consent/toggle.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAA;AAapC;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,aAAa,GAA2B,CACnD,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,EACtE,EAAE;IACF,MAAM,SAAS,GAAG;QAChB,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9C,CAAA;IAED,OAAO,CACL,eAAK,SAAS,EAAC,8EAA8E,aAC3F,eAAK,SAAS,EAAC,gBAAgB,aAC7B,eAAK,SAAS,EAAC,8EAA8E,aAC3F,eAAM,EAAE,EAAE,GAAG,EAAE,QAAQ,YAAG,KAAK,GAAQ,EACtC,QAAQ,KAAK,IAAI,IAAI,eACpB,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,SAAS,EAAC,iEAAiE,YACjG,aAAa,GAAQ,IACnB,EACL,WAAW,KAAK,EAAE,IAAI,YACrB,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,EAAC,2DAA2D,YACvF,WAAW,GAAK,IACd,EACN,iBAAO,SAAS,EAAE,EAAE,CAClB,0EAA0E,EAC1E,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,+BAA+B,CAAC,CAAC,CAAC,gBAAgB,CACvE,aACC,gBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAC,UAAU,qBACE,GAAG,EAAE,QAAQ,KAC1B,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAC7E,SAAS,EAAC,cAAc,EACxB,OAAO,EAAE,OAAO,IAAI,QAAQ,KAAK,IAAI,EACrC,QAAQ,EAAE,QAAQ,KAAK,IAAI,EAC3B,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GACjD,EACF,eAAM,SAAS,EAAE,EAAE,CACjB,gKAAgK,EAChK,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,wBAAwB,CACvE,YACC,eAAM,SAAS,EAAE,EAAE,CACjB,yFAAyF,EACzF,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,iBAAiB,CACnE,GAAI,GACA,IACD,IACJ,CACP,CAAA;AACH,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/web-consent",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.26",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -33,12 +33,13 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@owlmeans/dep-config": "workspace:*",
|
|
36
|
-
"@owlmeans/test-ui": "^0.1.18-rc.
|
|
36
|
+
"@owlmeans/test-ui": "^0.1.18-rc.33",
|
|
37
37
|
"@tailwindcss/vite": "^4.0.0",
|
|
38
38
|
"@types/bun": "^1.4.0",
|
|
39
39
|
"@types/react": "^19.2.17",
|
|
40
40
|
"@types/react-dom": "^19.2.3",
|
|
41
41
|
"@vitejs/plugin-react": "^6.0.3",
|
|
42
|
+
"axe-core": "^4.13.0",
|
|
42
43
|
"clsx": "^2.1.1",
|
|
43
44
|
"lucide-react": "^0.550.0",
|
|
44
45
|
"nodemon": "^3.1.14",
|
package/src/consent/toggle.tsx
CHANGED
|
@@ -20,41 +20,58 @@ export interface ConsentToggleProps {
|
|
|
20
20
|
* switch's hit area is 44px square around the visible 24×44 track, and it shows the dialog's 3px
|
|
21
21
|
* focus ring when reached from the keyboard (the real checkbox is visually hidden, so the ring is
|
|
22
22
|
* drawn on the track through `peer-focus-visible`).
|
|
23
|
+
*
|
|
24
|
+
* The checkbox takes its NAME from the category label and its description from the `Required`
|
|
25
|
+
* marker and the description text, by id. The `<label>` around it holds only the drawn track, so
|
|
26
|
+
* without those references a screen reader announces an unnamed checkbox three times over — the
|
|
27
|
+
* visible words sit in a sibling column the label cannot reach. A required category keeps its name
|
|
28
|
+
* while disabled: it is still disclosed, only not a choice.
|
|
23
29
|
*/
|
|
24
30
|
export const ConsentToggle: FC<ConsentToggleProps> = (
|
|
25
31
|
{ id, label, description, checked, required, requiredLabel, onChange }
|
|
26
|
-
) =>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
) => {
|
|
33
|
+
const described = [
|
|
34
|
+
...(required === true ? [`${id}-required`] : []),
|
|
35
|
+
...(description !== '' ? [`${id}-desc`] : []),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex items-center justify-between gap-4 rounded-2xl border border-border p-4">
|
|
40
|
+
<div className="min-w-0 flex-1">
|
|
41
|
+
<div className="mb-0.5 flex flex-wrap items-center gap-x-2 text-sm font-bold text-foreground">
|
|
42
|
+
<span id={`${id}-label`}>{label}</span>
|
|
43
|
+
{required === true && <span
|
|
44
|
+
id={`${id}-required`} className="text-xs font-bold uppercase tracking-wide text-muted-foreground"
|
|
45
|
+
>{requiredLabel}</span>}
|
|
46
|
+
</div>
|
|
47
|
+
{description !== '' && <p
|
|
48
|
+
id={`${id}-desc`} className="text-pretty text-xs leading-relaxed text-muted-foreground"
|
|
49
|
+
>{description}</p>}
|
|
34
50
|
</div>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'relative inline-flex h-11 w-14 flex-shrink-0 items-center justify-center',
|
|
39
|
-
required === true ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
|
|
40
|
-
)}>
|
|
41
|
-
<input
|
|
42
|
-
id={id}
|
|
43
|
-
type="checkbox"
|
|
44
|
-
className="peer sr-only"
|
|
45
|
-
checked={checked || required === true}
|
|
46
|
-
disabled={required === true}
|
|
47
|
-
onChange={event => onChange(event.target.checked)}
|
|
48
|
-
/>
|
|
49
|
-
<span className={cn(
|
|
50
|
-
'relative h-6 w-11 rounded-full transition-colors duration-300 peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-ring',
|
|
51
|
-
checked || required === true ? 'bg-primary' : 'bg-muted-foreground/40'
|
|
51
|
+
<label className={cn(
|
|
52
|
+
'relative inline-flex h-11 w-14 flex-shrink-0 items-center justify-center',
|
|
53
|
+
required === true ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
|
|
52
54
|
)}>
|
|
55
|
+
<input
|
|
56
|
+
id={id}
|
|
57
|
+
type="checkbox"
|
|
58
|
+
aria-labelledby={`${id}-label`}
|
|
59
|
+
{...(described.length > 0 ? { 'aria-describedby': described.join(' ') } : {})}
|
|
60
|
+
className="peer sr-only"
|
|
61
|
+
checked={checked || required === true}
|
|
62
|
+
disabled={required === true}
|
|
63
|
+
onChange={event => onChange(event.target.checked)}
|
|
64
|
+
/>
|
|
53
65
|
<span className={cn(
|
|
54
|
-
'
|
|
55
|
-
checked || required === true ? '
|
|
56
|
-
)}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
66
|
+
'relative h-6 w-11 rounded-full transition-colors duration-300 peer-focus-visible:outline-3 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-ring',
|
|
67
|
+
checked || required === true ? 'bg-primary' : 'bg-muted-foreground/40'
|
|
68
|
+
)}>
|
|
69
|
+
<span className={cn(
|
|
70
|
+
'absolute top-0.5 left-0 h-5 w-5 rounded-full bg-white transition-transform duration-300',
|
|
71
|
+
checked || required === true ? 'translate-x-5' : 'translate-x-0.5'
|
|
72
|
+
)} />
|
|
73
|
+
</span>
|
|
74
|
+
</label>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import type { Page } from 'playwright'
|
|
4
|
+
import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
|
|
5
|
+
import { CONSENT_KEY, CONSENT_SCHEMA_VERSION } from '@owlmeans/consent'
|
|
6
|
+
import { HARNESS_URL } from './context.js'
|
|
7
|
+
|
|
8
|
+
const TIMEOUT = 60_000
|
|
9
|
+
|
|
10
|
+
afterAll(async () => {
|
|
11
|
+
await closeBrowser()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const base = HARNESS_URL.replace(/\/$/, '')
|
|
15
|
+
const AXE = createRequire(import.meta.url).resolve('axe-core/axe.min.js')
|
|
16
|
+
|
|
17
|
+
interface Violation { id: string, impact: string | null, help: string, targets: string[] }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every serious or critical axe violation on the page as it stands.
|
|
21
|
+
*
|
|
22
|
+
* Run against the THEMED harness (`styled=1`), because the rule that matters most after naming —
|
|
23
|
+
* colour contrast — means nothing on an unstyled page, where every text is black on white.
|
|
24
|
+
*/
|
|
25
|
+
const serious = async (page: Page): Promise<Violation[]> => {
|
|
26
|
+
await page.addScriptTag({ path: AXE })
|
|
27
|
+
const violations = await page.evaluate(async () => {
|
|
28
|
+
const axe = (window as never as {
|
|
29
|
+
axe: { run: (context: Document) => Promise<{ violations: {
|
|
30
|
+
id: string, impact: string | null, help: string, nodes: { target: string[] }[]
|
|
31
|
+
}[] }> }
|
|
32
|
+
}).axe
|
|
33
|
+
const result = await axe.run(document)
|
|
34
|
+
|
|
35
|
+
return result.violations.map(violation => ({
|
|
36
|
+
id: violation.id,
|
|
37
|
+
impact: violation.impact,
|
|
38
|
+
help: violation.help,
|
|
39
|
+
targets: violation.nodes.map(node => node.target.join(' ')),
|
|
40
|
+
}))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return violations.filter(violation => violation.impact === 'serious' || violation.impact === 'critical')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('@owlmeans/web-consent — accessibility', () => {
|
|
47
|
+
for (const theme of ['light', 'dark']) {
|
|
48
|
+
test(`the open dialog has no serious or critical axe violation (${theme})`, async () => {
|
|
49
|
+
// A first visit, so the dialog is open — which is what every new visitor of a generated app
|
|
50
|
+
// meets before anything else on the page.
|
|
51
|
+
const { page, close } = await mountComponent({ url: `${base}/?styled=1&theme=${theme}` })
|
|
52
|
+
try {
|
|
53
|
+
await page.waitForSelector('[data-consent-dialog]')
|
|
54
|
+
|
|
55
|
+
expect(await serious(page)).toEqual([])
|
|
56
|
+
} finally {
|
|
57
|
+
await close()
|
|
58
|
+
}
|
|
59
|
+
}, TIMEOUT)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('each switch is named by its category and described by its text', async () => {
|
|
63
|
+
// The label element around a switch holds only the drawn track, so its NAME has to come from
|
|
64
|
+
// the category label by reference — the defect this pins was three unnamed checkboxes.
|
|
65
|
+
const { page, close } = await mountComponent({ url: `${base}/?styled=1` })
|
|
66
|
+
try {
|
|
67
|
+
await page.waitForSelector('[data-consent-dialog]')
|
|
68
|
+
|
|
69
|
+
const essential = page.getByRole('checkbox', { name: 'Essential Cookies' })
|
|
70
|
+
expect(await essential.isDisabled()).toBe(true)
|
|
71
|
+
expect(await essential.isChecked()).toBe(true)
|
|
72
|
+
expect(await essential.getAttribute('aria-describedby')).toBe('cc-essential-required cc-essential-desc')
|
|
73
|
+
|
|
74
|
+
const analytics = page.getByRole('checkbox', { name: 'Analytics Cookies' })
|
|
75
|
+
expect(await analytics.isDisabled()).toBe(false)
|
|
76
|
+
expect(await analytics.getAttribute('aria-describedby')).toBe('cc-analytics-desc')
|
|
77
|
+
await page.getByRole('checkbox', { name: 'Marketing Cookies' }).waitFor({ state: 'attached' })
|
|
78
|
+
} finally {
|
|
79
|
+
await close()
|
|
80
|
+
}
|
|
81
|
+
}, TIMEOUT)
|
|
82
|
+
|
|
83
|
+
test('the re-open button has no serious or critical axe violation', async () => {
|
|
84
|
+
const { page, close } = await mountComponent({ url: `${base}/?styled=1`, waitUntil: 'commit' })
|
|
85
|
+
try {
|
|
86
|
+
await page.evaluate(([key, value]) => window.localStorage.setItem(key, value), [
|
|
87
|
+
CONSENT_KEY,
|
|
88
|
+
JSON.stringify({ essential: true, analytics: false, marketing: false, v: CONSENT_SCHEMA_VERSION }),
|
|
89
|
+
] as [string, string])
|
|
90
|
+
await page.goto(`${base}/?styled=1`, { waitUntil: 'domcontentloaded' })
|
|
91
|
+
await page.waitForSelector('[data-consent-reopen]')
|
|
92
|
+
|
|
93
|
+
expect(await serious(page)).toEqual([])
|
|
94
|
+
} finally {
|
|
95
|
+
await close()
|
|
96
|
+
}
|
|
97
|
+
}, TIMEOUT)
|
|
98
|
+
|
|
99
|
+
for (const theme of ['light', 'dark']) {
|
|
100
|
+
test(`the policy page with services has no serious or critical axe violation (${theme})`, async () => {
|
|
101
|
+
const { page, close } = await mountComponent({ url: `${base}/?styled=1&theme=${theme}`, waitUntil: 'commit' })
|
|
102
|
+
try {
|
|
103
|
+
// With a decision stored, so the dialog does not cover the page being checked.
|
|
104
|
+
await page.evaluate(([key, value]) => window.localStorage.setItem(key, value), [
|
|
105
|
+
CONSENT_KEY,
|
|
106
|
+
JSON.stringify({ essential: true, analytics: true, marketing: false, v: CONSENT_SCHEMA_VERSION }),
|
|
107
|
+
] as [string, string])
|
|
108
|
+
await page.goto(`${base}/?view=policy&services=1&styled=1&theme=${theme}`, { waitUntil: 'domcontentloaded' })
|
|
109
|
+
await page.waitForSelector('[data-cookie-policy-service]')
|
|
110
|
+
|
|
111
|
+
expect(await serious(page)).toEqual([])
|
|
112
|
+
} finally {
|
|
113
|
+
await close()
|
|
114
|
+
}
|
|
115
|
+
}, TIMEOUT)
|
|
116
|
+
}
|
|
117
|
+
})
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -12,9 +12,18 @@ import type { ConsentCategory, ConsentService } from '../../src/index.js'
|
|
|
12
12
|
* - `?categories=custom` — a category set that is NOT the default, with its own global var.
|
|
13
13
|
* - `?view=policy` — the cookie-policy page instead of the dialog.
|
|
14
14
|
* - `?services=1` — with `view=policy`, disclose {@link SERVICES} on it.
|
|
15
|
+
* - `?styled=1` — compile a real theme (`styled.css`), for the accessibility spec only.
|
|
16
|
+
* - `?theme=dark` — with `styled=1`, the dark scheme.
|
|
15
17
|
*/
|
|
16
18
|
const params = new URLSearchParams(window.location.search)
|
|
17
19
|
|
|
20
|
+
if (params.get('styled') != null) {
|
|
21
|
+
await import('./styled.css')
|
|
22
|
+
if (params.get('theme') === 'dark') {
|
|
23
|
+
document.documentElement.classList.add('dark')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
28
|
* A deliberately non-default set: a different key, a different global var, and a signal list that
|
|
20
29
|
* does not match the built-in one. A component that quietly fell back to the defaults would still
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A real theme for the accessibility spec, loaded only when the URL asks for it (`?styled=1`).
|
|
3
|
+
|
|
4
|
+
Every other spec asserts on class lists and needs the page unstyled: a styled dialog is a fixed,
|
|
5
|
+
full-screen overlay, and the specs that click past it would be clicking through it. Contrast,
|
|
6
|
+
on the other hand, cannot be judged on an unstyled page — so this sheet compiles the package's
|
|
7
|
+
own sources with the neutral tokens a generated app ships (ground, ink, muted, hairline, tile,
|
|
8
|
+
one accent) in both schemes.
|
|
9
|
+
*/
|
|
10
|
+
@import "tailwindcss";
|
|
11
|
+
|
|
12
|
+
@source "../../src";
|
|
13
|
+
|
|
14
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
--background: #ffffff;
|
|
18
|
+
--foreground: #0a2540;
|
|
19
|
+
--muted: #f5f7fa;
|
|
20
|
+
--muted-foreground: #51606f;
|
|
21
|
+
--border: #e6e6ea;
|
|
22
|
+
--primary: #8a3f1c;
|
|
23
|
+
--primary-foreground: #ffffff;
|
|
24
|
+
--ring: #0a2540;
|
|
25
|
+
color-scheme: light;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.dark {
|
|
29
|
+
--background: #000000;
|
|
30
|
+
--foreground: #f5f7fa;
|
|
31
|
+
--muted: #0e1318;
|
|
32
|
+
--muted-foreground: #9aa7b4;
|
|
33
|
+
--border: #1f2630;
|
|
34
|
+
--primary: #d9895f;
|
|
35
|
+
--primary-foreground: #000000;
|
|
36
|
+
--ring: #f5f7fa;
|
|
37
|
+
color-scheme: dark;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@theme inline {
|
|
41
|
+
--color-background: var(--background);
|
|
42
|
+
--color-foreground: var(--foreground);
|
|
43
|
+
--color-muted: var(--muted);
|
|
44
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
45
|
+
--color-border: var(--border);
|
|
46
|
+
--color-primary: var(--primary);
|
|
47
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
48
|
+
--color-ring: var(--ring);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
body {
|
|
52
|
+
background: var(--background);
|
|
53
|
+
color: var(--foreground);
|
|
54
|
+
}
|