@ztd-me/frontend-checks 0.0.0-stage → 0.1.0
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 +73 -2
- package/docs/publishing.md +40 -0
- package/package.json +62 -3
- package/src/css-cli.mjs +27 -0
- package/src/css-config.mjs +32 -0
- package/src/css-tokens.mjs +77 -0
- package/src/css.d.ts +22 -0
- package/src/css.mjs +53 -0
- package/src/playwright.d.ts +11 -0
- package/src/playwright.mjs +50 -0
package/README.md
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ztd-me/frontend-checks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native CSS and Playwright accessibility checks. Requires Node >=22.14 and Playwright Test ^1.62.0. Version `0.1.0` is prepared for public npm publication through automatic staging and owner 2FA promotion. See [publishing](docs/publishing.md) and [CLI integration](https://github.com/zeithrold/tools/blob/main/docs/frontend-tooling.md). Consumers retain `@ztd-me/eslint@0.1.1` and its strict standards.
|
|
4
|
+
|
|
5
|
+
After promotion and verified public installation:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add -D --save-exact @ztd-me/frontend-checks@0.1.0 @playwright/test@1.62.0
|
|
9
|
+
pnpm exec playwright install chromium
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Retain the consuming project's release-age, trust and build policy. Wait if a newly promoted release is not yet eligible. The `0.0.0-stage` placeholder for a new package is not a usable helper release.
|
|
13
|
+
|
|
14
|
+
## CSS
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
// css-check.config.mjs
|
|
18
|
+
export default {
|
|
19
|
+
files: ['src/**/*.css'],
|
|
20
|
+
tokenFiles: ['node_modules/tailwindcss/theme.css'],
|
|
21
|
+
externalCustomProperties: [],
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm exec ztd-css ./css-check.config.mjs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The config module is reviewed project code and executes during import. `checkCss(options)` is also exported from `@ztd-me/frontend-checks/css`. Every supplied glob must match; missing files and syntax/configuration errors fail. JSON findings print to stdout; a nonzero exit fails the gate. Under `zt check`, the CLI also writes `css.json` to `ZT_ARTIFACTS_DIR`.
|
|
30
|
+
|
|
31
|
+
Stylelint 17.15 / standard 40 provide native CSS validation; only documented Tailwind directives and `--alpha`/`--spacing` functions receive syntax allowances. Undefined `var()` references fail across the supplied files/declaration sources, even with fallbacks. Exact runtime-generated variables can be declared in `externalCustomProperties`; document their owner in the project contract. Imports are not resolved automatically. Neither Sass/Less nor embedded Vue styles are parsed by this CSS-only entrypoint.
|
|
32
|
+
|
|
33
|
+
Hex, named and CSS color-function paint literals outside custom-property definitions fail. Token definitions retain each project's values. `transparent`, `currentColor`, inheritance and URLs are allowed. This check covers declarations, not Tailwind arbitrary-value classes, inline JS styles or every possible CSS color expression. An inventory definition is not proof of cascade/theme availability; render the relevant states. No autofix rewrites token values.
|
|
34
|
+
|
|
35
|
+
## Playwright
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { defineConfig } from '@playwright/test'
|
|
39
|
+
import { verificationArtifacts } from '@ztd-me/frontend-checks/playwright'
|
|
40
|
+
|
|
41
|
+
const artifacts = verificationArtifacts()
|
|
42
|
+
export default defineConfig({
|
|
43
|
+
...artifacts,
|
|
44
|
+
webServer: existingWebServer,
|
|
45
|
+
projects: existingProjects,
|
|
46
|
+
use: { ...artifacts.use, baseURL: existingBaseURL },
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { test } from '@playwright/test'
|
|
52
|
+
import { assertAccessible, captureState } from '@ztd-me/frontend-checks/playwright'
|
|
53
|
+
|
|
54
|
+
test('translated dialog', async ({ page }, info) => {
|
|
55
|
+
await page.goto('/settings')
|
|
56
|
+
// Establish the real dialog state using project locators and interactions.
|
|
57
|
+
await assertAccessible(page, info, { label: 'settings-dialog' })
|
|
58
|
+
await captureState(page, info, 'settings-dialog')
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`assertAccessible` uses Axe 4.13 and defaults to WCAG 2 A/AA, 2.1 AA and 2.2 AA tags. It attaches the complete scan before asserting zero violations. Optional `include` scopes a supplemental scan; optional nonempty `tags` changes the selected coverage and must be justified locally. Full-page and keyboard/focus tests remain necessary. Projects own routes, states, browser matrices, service mocks and the built-Worker server. This helper does not provision or start them.
|
|
63
|
+
|
|
64
|
+
`verificationArtifacts(root?)` provides HTML/JSON reports, test attachments, failure traces/screenshots/videos. The root defaults to `ZT_ARTIFACTS_DIR`, otherwise `.zt/browser`. Merge with existing config; preserve native server/projects/use settings. `captureState` attaches a named PNG for review without maintaining screenshot baselines.
|
|
65
|
+
|
|
66
|
+
## Verification
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
pnpm install --frozen-lockfile
|
|
70
|
+
pnpm exec playwright install chromium
|
|
71
|
+
pnpm run check
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Tests execute native CSS parsing/lint, real Chromium Axe/keyboard/dialog behavior, a deliberate accessibility failure, typed imports, fresh tarball installation/CLI use, and stage authorization/duplicate guards. The workspace keeps release-age/trust policy and only the previously approved exact ESLint 0.1.1 / semver 6.3.1 exceptions. Fresh consumers use strict 24-hour release age with no exceptions. After promotion, `test:registry` verifies registry integrity against the reviewed tarball and repeats consumer import/type/CLI/browser checks; a metadata lookup alone is insufficient.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Public stage-only release
|
|
2
|
+
|
|
3
|
+
`@ztd-me/frontend-checks@0.1.0` is prepared for authorized public publication. `.github/workflows/publish-frontend.yml` runs automatically on relevant pushes to `main`. It uses pnpm 11.22.0, frozen installation, strict lint, declarations, CSS fixtures, Chromium tests and fresh packed-consumer checks on Node 22 and 24. The Node 24 job packs the tested bytes and records the immutable source SHA and checksums. A separate job verifies those checksums and source identity before staging that exact tarball. It never directly publishes or promotes a version.
|
|
4
|
+
|
|
5
|
+
The existing repository `NPM_TOKEN` secret is the authorized route. It must permit Read and write (stage only) for this package under `@ztd-me`, with Bypass 2FA disabled. Its actual scope for this new package cannot be inferred from its success publishing ESLint. If permission is missing, the package owner must supply the narrowly authorized access; the workflow fails without creating credentials or expanding rights. Organization administration rights alone do not grant package publication rights. The token is bound to the registry in memory in the stage step only; installation, checks and packing receive no token.
|
|
6
|
+
|
|
7
|
+
Before upload, the shared stage guard checks public versions and the authenticated pending-stage list. Only pnpm's structured public-metadata HTTP 404 is interpreted as a package without published versions. Missing tokens, stage-list authorization failures, network failures and malformed responses remain blockers. An existing stage is preserved and its ID recorded rather than uploading a duplicate. An ambiguous upload outcome requires owner reconciliation before retry. No manual workflow trigger, authentication selector or OIDC fallback is configured.
|
|
8
|
+
|
|
9
|
+
## Owner review and promotion
|
|
10
|
+
|
|
11
|
+
1. Review and merge the tested draft PR to `main` using the repository's normal owner-controlled review process.
|
|
12
|
+
2. Let the automatic workflow finish. Check the Actions source SHA, `frontend-package-<SHA>` artifact, `SHA256SUMS` and `frontend-stage-result-<SHA>` receipt. Review the pending stage's package name, version and contents against that exact artifact.
|
|
13
|
+
3. Promote the stage in npm's website with the owner's 2FA, or use an already-authenticated `pnpm stage approve <stage-id>` session and complete proof of presence. Do not share the credential or OTP in chat.
|
|
14
|
+
4. Verify the public registry bytes and fresh consumer as described below before dispatching consumer migrations.
|
|
15
|
+
|
|
16
|
+
First-package staging creates a public `0.0.0-stage` placeholder so package settings can be accessed. This placeholder does not make `0.1.0` installable. A successful stage receipt is also not a completed public release.
|
|
17
|
+
|
|
18
|
+
## Exact registry verification
|
|
19
|
+
|
|
20
|
+
From the reviewed source checkout, download the automatic release run's exact `frontend-package-<SHA>` artifact and verify `SHA256SUMS` and `source-commit.txt`. Once the owner has promoted `0.1.0`, run:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pnpm exec playwright install chromium
|
|
24
|
+
pnpm run test:registry 0.1.0 /path/to/verified/package.tgz
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The script checks the exact public package/version and registry SHA-512 integrity against the reviewed tarball, then creates a fresh pnpm consumer. It installs the exact registry version, reruns frozen installation, imports both `/css` and `/playwright`, invokes `ztd-css`, checks TypeScript declarations with strict settings and executes a deliberately failing browser Axe scan that must retain the violation evidence. Browser infrastructure errors cannot satisfy that assertion.
|
|
28
|
+
|
|
29
|
+
The consumer uses `minimumReleaseAge: 1440`, `minimumReleaseAgeStrict: true` and `trustPolicy: no-downgrade` without exceptions. A newly promoted release may require waiting 24 hours before this check is eligible. Wait and retry if the policy blocks it; do not lower policy or add an exception to force publication verification. Consumers retain their own approved policies and pin the verified version:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm add -D --save-exact @ztd-me/frontend-checks@0.1.0 @playwright/test@1.62.0
|
|
33
|
+
pnpm install --frozen-lockfile
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Optional future Trusted Publisher
|
|
37
|
+
|
|
38
|
+
An npm trust grant would be package-specific: GitHub owner `zeithrold`, repository `tools`, workflow filename `publish-frontend.yml`, no environment, stage publishing only. The owner must configure it separately; this release does not create one. OIDC supports stage upload but cannot authenticate the pending-stage lookup. Switching requires a reviewed guard redesign and job-scoped `id-token: write`, rather than removing the token from the current job or adding a fallback path. Owner proof of presence still controls promotion.
|
|
39
|
+
|
|
40
|
+
References: [pnpm stage](https://pnpm.io/cli/stage), [npm staged publishing](https://docs.npmjs.com/staged-publishing/), [token permissions](https://docs.npmjs.com/creating-and-viewing-access-tokens/), [Trusted Publishers](https://docs.npmjs.com/trusted-publishers/).
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ztd-me/frontend-checks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"description": "Native CSS and Playwright accessibility checks for zt frontend profiles",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.14.0"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
"./css": {
|
|
12
|
+
"types": "./src/css.d.ts",
|
|
13
|
+
"import": "./src/css.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./playwright": {
|
|
16
|
+
"types": "./src/playwright.d.ts",
|
|
17
|
+
"import": "./src/playwright.mjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"ztd-css": "./src/css-cli.mjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"README.md",
|
|
26
|
+
"docs"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@axe-core/playwright": "4.13.0",
|
|
30
|
+
"fast-glob": "3.3.3",
|
|
31
|
+
"postcss": "8.5.27",
|
|
32
|
+
"postcss-value-parser": "4.2.0",
|
|
33
|
+
"stylelint": "17.15.0",
|
|
34
|
+
"stylelint-config-standard": "40.0.0",
|
|
35
|
+
"color-name": "2.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@playwright/test": "^1.62.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "24.12.0",
|
|
42
|
+
"@playwright/test": "1.62.0",
|
|
43
|
+
"eslint": "10.11.0",
|
|
44
|
+
"typescript": "6.0.3",
|
|
45
|
+
"@ztd-me/eslint": "0.1.1"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/zeithrold/tools.git",
|
|
50
|
+
"directory": "packages/frontend-checks"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public",
|
|
54
|
+
"registry": "https://registry.npmjs.org/"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"lint": "eslint src scripts test playwright.config.mjs eslint.config.js --max-warnings 0",
|
|
58
|
+
"typecheck": "tsc -p test/types/tsconfig.json",
|
|
59
|
+
"test": "node --test test/*.test.mjs",
|
|
60
|
+
"test:browser": "playwright test",
|
|
61
|
+
"test:pack": "node scripts/pack-smoke.mjs",
|
|
62
|
+
"check": "pnpm run lint && pnpm run typecheck && pnpm test && pnpm run test:browser && pnpm run test:pack",
|
|
63
|
+
"test:registry": "node scripts/registry-smoke.mjs"
|
|
64
|
+
}
|
|
6
65
|
}
|
package/src/css-cli.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { checkCss } from './css.mjs'
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
if (process.argv.length !== 3) {
|
|
10
|
+
throw new Error('usage: ztd-css ./css-check.config.mjs; see README for the config schema')
|
|
11
|
+
}
|
|
12
|
+
const loaded = await import(pathToFileURL(path.resolve(process.argv[2])).href)
|
|
13
|
+
const report = await checkCss(loaded.default)
|
|
14
|
+
const data = `${JSON.stringify(report, null, 2)}\n`
|
|
15
|
+
if (process.env.ZT_ARTIFACTS_DIR) {
|
|
16
|
+
await writeFile(path.join(process.env.ZT_ARTIFACTS_DIR, 'css.json'), data)
|
|
17
|
+
}
|
|
18
|
+
process.stdout.write(data)
|
|
19
|
+
if (report.status !== 'passed') {
|
|
20
|
+
process.exitCode = 1
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
main().catch((error) => {
|
|
25
|
+
process.stderr.write(`${error.message}\n`)
|
|
26
|
+
process.exitCode = 1
|
|
27
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import standard from 'stylelint-config-standard'
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url)
|
|
5
|
+
|
|
6
|
+
export const cssConfig = {
|
|
7
|
+
...standard,
|
|
8
|
+
extends: require.resolve('stylelint-config-standard'),
|
|
9
|
+
rules: {
|
|
10
|
+
...standard.rules,
|
|
11
|
+
'at-rule-no-unknown': [
|
|
12
|
+
true,
|
|
13
|
+
{
|
|
14
|
+
ignoreAtRules: [
|
|
15
|
+
'theme',
|
|
16
|
+
'utility',
|
|
17
|
+
'variant',
|
|
18
|
+
'custom-variant',
|
|
19
|
+
'apply',
|
|
20
|
+
'reference',
|
|
21
|
+
'source',
|
|
22
|
+
'config',
|
|
23
|
+
'plugin',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
'function-no-unknown': [
|
|
28
|
+
true,
|
|
29
|
+
{ ignoreFunctions: ['--alpha', '--spacing'] },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import colors from 'color-name'
|
|
2
|
+
import valueParser from 'postcss-value-parser'
|
|
3
|
+
|
|
4
|
+
const colorFunctions = /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)$/i
|
|
5
|
+
const colorProperties = /^color$|color$|^background(?:$|-)|^border(?:$|-)|^outline(?:$|-)|shadow$|^fill$|^stroke$/i
|
|
6
|
+
|
|
7
|
+
function references(value) {
|
|
8
|
+
const names = []
|
|
9
|
+
valueParser(value).walk((node) => {
|
|
10
|
+
if (node.type !== 'function' || node.value !== 'var') {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
const first = node.nodes.find(child => child.type !== 'space' && child.type !== 'comment')
|
|
14
|
+
if (first?.type === 'word' && first.value.startsWith('--')) {
|
|
15
|
+
names.push(first.value)
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
return names
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasLiteralColor(value) {
|
|
22
|
+
let found = false
|
|
23
|
+
valueParser(value).walk((node) => {
|
|
24
|
+
if (node.type === 'function' && node.value === 'url') {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
if (node.type === 'function' && colorFunctions.test(node.value)) {
|
|
28
|
+
found = true
|
|
29
|
+
}
|
|
30
|
+
const literalWord = node.type === 'word'
|
|
31
|
+
&& (/^#[\da-f]{3,8}$/i.test(node.value) || Object.hasOwn(colors, node.value.toLowerCase()))
|
|
32
|
+
if (literalWord) {
|
|
33
|
+
found = true
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
return found
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// This is a closed declaration inventory, not a proof of the runtime cascade.
|
|
40
|
+
export function tokenWarnings(sources, definitions, options) {
|
|
41
|
+
const known = new Set(options.externalCustomProperties ?? [])
|
|
42
|
+
for (const root of [
|
|
43
|
+
...sources,
|
|
44
|
+
...definitions,
|
|
45
|
+
]) {
|
|
46
|
+
root.walkDecls((declaration) => {
|
|
47
|
+
if (declaration.prop.startsWith('--') && !declaration.prop.includes('*')) {
|
|
48
|
+
known.add(declaration.prop)
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
root.walkAtRules('property', rule => known.add(rule.params.trim()))
|
|
52
|
+
}
|
|
53
|
+
const warnings = []
|
|
54
|
+
for (const root of sources) {
|
|
55
|
+
root.walkDecls((declaration) => {
|
|
56
|
+
const issue = (rule, text) => warnings.push({
|
|
57
|
+
file: root.source.input.file,
|
|
58
|
+
line: declaration.source.start.line,
|
|
59
|
+
column: declaration.source.start.column,
|
|
60
|
+
rule,
|
|
61
|
+
text,
|
|
62
|
+
})
|
|
63
|
+
for (const name of references(declaration.value)) {
|
|
64
|
+
if (!known.has(name)) {
|
|
65
|
+
issue('ztd/defined-custom-property', `Undefined custom property ${name}; include its declaration source`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (declaration.prop.startsWith('--') || !colorProperties.test(declaration.prop)) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (hasLiteralColor(declaration.value)) {
|
|
72
|
+
issue('ztd/semantic-color', 'Move literal colors into project-owned semantic custom properties')
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
return warnings
|
|
77
|
+
}
|
package/src/css.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface CssOptions {
|
|
2
|
+
files: string[]
|
|
3
|
+
cwd?: string
|
|
4
|
+
/** External CSS declaration sources. Imports are not resolved implicitly. */
|
|
5
|
+
tokenFiles?: string[]
|
|
6
|
+
/** Exact runtime-provided names, reviewed in the project design contract. */
|
|
7
|
+
externalCustomProperties?: string[]
|
|
8
|
+
}
|
|
9
|
+
export interface CssWarning {
|
|
10
|
+
file: string
|
|
11
|
+
line: number
|
|
12
|
+
column: number
|
|
13
|
+
rule: string
|
|
14
|
+
text: string
|
|
15
|
+
}
|
|
16
|
+
export interface CssReport {
|
|
17
|
+
schemaVersion: 1
|
|
18
|
+
status: 'passed' | 'failed'
|
|
19
|
+
files: string[]
|
|
20
|
+
warnings: CssWarning[]
|
|
21
|
+
}
|
|
22
|
+
export function checkCss(options: CssOptions): Promise<CssReport>
|
package/src/css.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import glob from 'fast-glob'
|
|
5
|
+
import postcss from 'postcss'
|
|
6
|
+
import stylelint from 'stylelint'
|
|
7
|
+
import { cssConfig } from './css-config.mjs'
|
|
8
|
+
import { tokenWarnings } from './css-tokens.mjs'
|
|
9
|
+
|
|
10
|
+
async function sources(patterns, cwd) {
|
|
11
|
+
const matches = await Promise.all(patterns.map(async (pattern) => {
|
|
12
|
+
const selected = await glob(pattern, { cwd, absolute: true, onlyFiles: true, followSymbolicLinks: false })
|
|
13
|
+
if (!selected.length) {
|
|
14
|
+
throw new Error(`CSS pattern matched no files: ${pattern}`)
|
|
15
|
+
}
|
|
16
|
+
return selected
|
|
17
|
+
}))
|
|
18
|
+
const files = [
|
|
19
|
+
...new Set(matches.flat()),
|
|
20
|
+
]
|
|
21
|
+
return Promise.all(files.sort().map(async (file) => {
|
|
22
|
+
const code = await readFile(file, 'utf8')
|
|
23
|
+
return postcss.parse(code, { from: file })
|
|
24
|
+
}))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validate(options) {
|
|
28
|
+
if (!options || !Array.isArray(options.files) || !options.files.length) {
|
|
29
|
+
throw new TypeError('CSS files must be a nonempty explicit array of paths/globs')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function checkCss(options) {
|
|
34
|
+
validate(options)
|
|
35
|
+
const cwd = path.resolve(options.cwd ?? process.cwd())
|
|
36
|
+
const roots = await sources(options.files, cwd)
|
|
37
|
+
const definitions = await sources(options.tokenFiles ?? [], cwd)
|
|
38
|
+
const lint = await stylelint.lint({ files: roots.map(root => root.source.input.file), config: cssConfig, cwd })
|
|
39
|
+
const warnings = lint.results.flatMap(result => result.warnings.map(warning => ({
|
|
40
|
+
file: result.source,
|
|
41
|
+
line: warning.line,
|
|
42
|
+
column: warning.column,
|
|
43
|
+
rule: warning.rule,
|
|
44
|
+
text: warning.text,
|
|
45
|
+
})))
|
|
46
|
+
warnings.push(...tokenWarnings(roots, definitions, options))
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
status: lint.errored || warnings.length ? 'failed' : 'passed',
|
|
50
|
+
files: roots.map(root => root.source.input.file),
|
|
51
|
+
warnings,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Page, TestInfo, PlaywrightTestConfig } from '@playwright/test'
|
|
2
|
+
import type { AxeBuilder } from '@axe-core/playwright'
|
|
3
|
+
export interface AccessibilityOptions {
|
|
4
|
+
label?: string
|
|
5
|
+
/** Restricts the scan; full-page scans remain necessary for page-level checks. */
|
|
6
|
+
include?: string
|
|
7
|
+
tags?: string[]
|
|
8
|
+
}
|
|
9
|
+
export function assertAccessible(page: Page, testInfo: TestInfo, options?: AccessibilityOptions): ReturnType<AxeBuilder['analyze']>
|
|
10
|
+
export function captureState(page: Page, testInfo: TestInfo, label: string): Promise<void>
|
|
11
|
+
export function verificationArtifacts(root?: string): Pick<PlaywrightTestConfig, 'outputDir' | 'reporter' | 'use'>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import AxeBuilder from '@axe-core/playwright'
|
|
4
|
+
|
|
5
|
+
/** Scan after each relevant UI state is established, not only the initial route. */
|
|
6
|
+
export async function assertAccessible(page, testInfo, options = {}) {
|
|
7
|
+
if (options.tags && !options.tags.length) {
|
|
8
|
+
throw new TypeError('Accessibility tags must not be empty')
|
|
9
|
+
}
|
|
10
|
+
let builder = new AxeBuilder({ page }).withTags(options.tags ?? [
|
|
11
|
+
'wcag2a',
|
|
12
|
+
'wcag2aa',
|
|
13
|
+
'wcag21aa',
|
|
14
|
+
'wcag22aa',
|
|
15
|
+
])
|
|
16
|
+
if (options.include) {
|
|
17
|
+
builder = builder.include(options.include)
|
|
18
|
+
}
|
|
19
|
+
const results = await builder.analyze()
|
|
20
|
+
await testInfo.attach(`a11y-${options.label ?? 'state'}`, {
|
|
21
|
+
body: JSON.stringify(results, null, 2),
|
|
22
|
+
contentType: 'application/json',
|
|
23
|
+
})
|
|
24
|
+
assert.equal(results.violations.length, 0, JSON.stringify(results.violations, null, 2))
|
|
25
|
+
return results
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Screenshots are captured evidence; this helper does not maintain baselines. */
|
|
29
|
+
export async function captureState(page, testInfo, label) {
|
|
30
|
+
await testInfo.attach(label, { body: await page.screenshot({ fullPage: true }), contentType: 'image/png' })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Apply these paths to the project's existing config; retain its webServer/projects. */
|
|
34
|
+
export function verificationArtifacts(root = process.env.ZT_ARTIFACTS_DIR ?? '.zt/browser') {
|
|
35
|
+
return {
|
|
36
|
+
outputDir: `${root}/test-results`,
|
|
37
|
+
reporter: [
|
|
38
|
+
['list'],
|
|
39
|
+
[
|
|
40
|
+
'html',
|
|
41
|
+
{ outputFolder: `${root}/playwright-report`, open: 'never' },
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
'json',
|
|
45
|
+
{ outputFile: `${root}/playwright.json` },
|
|
46
|
+
],
|
|
47
|
+
],
|
|
48
|
+
use: { trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure' },
|
|
49
|
+
}
|
|
50
|
+
}
|