@kinora/reporter 0.7.0 → 0.8.1

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 CHANGED
@@ -71,6 +71,28 @@ Each mounted story id is recorded on the test as a `kinora:story` annotation. Al
71
71
 
72
72
  More in the [component testing guide](https://docs.kinora.dev/guides/component-testing/).
73
73
 
74
+ ## Accessibility
75
+
76
+ kinora records the axe scans attached to a test: the rules broken, their impact and the elements
77
+ they hit. `scanA11y` runs the scan with `@axe-core/playwright` (add it to your dev dependencies)
78
+ and attaches the result:
79
+
80
+ ```ts
81
+ import { scanA11y } from '@kinora/reporter/a11y'
82
+ import { expect, test } from '@playwright/test'
83
+
84
+ test('checkout is accessible', async ({ page }) => {
85
+ await page.goto('/checkout')
86
+ const { violations } = await scanA11y(page)
87
+ expect(violations).toEqual([]) // leave it out to record the violations as warnings
88
+ })
89
+ ```
90
+
91
+ A scan you build yourself works too, as long as it is attached to the test with
92
+ `testInfo.attach(name, { body: JSON.stringify(results), contentType: 'application/json' })`.
93
+ An assertion alone is invisible to a reporter. More in the
94
+ [accessibility guide](https://docs.kinora.dev/guides/reporter/#accessibility).
95
+
74
96
  ## Documentation
75
97
 
76
98
  Full reporter options, PR/MR comments, CI examples, and self-hosting are in the docs:
@@ -0,0 +1,15 @@
1
+ import { Page } from "@playwright/test";
2
+ import AxeBuilder from "@axe-core/playwright";
3
+ //#region src/a11y.d.ts
4
+ export type AxeResults = Awaited<ReturnType<AxeBuilder['analyze']>>;
5
+ export declare const A11Y_ATTACHMENT = "accessibility-scan-results";
6
+ /**
7
+ * Scans the page with axe (`@axe-core/playwright`) and attaches the result to the running test,
8
+ * which is where kinora reads it: without the attachment, a reporter only sees the test fail.
9
+ *
10
+ * Returns axe's result. Assert on `violations` to fail the test on them, or leave them be to
11
+ * record them as warnings. `configure` narrows or widens the scan, e.g.
12
+ * `axe => axe.withTags(['wcag2a', 'wcag2aa'])`.
13
+ */
14
+ export declare function scanA11y(page: Page, configure?: (axe: AxeBuilder) => AxeBuilder): Promise<AxeResults>;
15
+ //#endregion
package/dist/a11y.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import { test } from "@playwright/test";
2
+ //#region src/a11y.ts
3
+ const A11Y_ATTACHMENT = "accessibility-scan-results";
4
+ /**
5
+ * Scans the page with axe (`@axe-core/playwright`) and attaches the result to the running test,
6
+ * which is where kinora reads it: without the attachment, a reporter only sees the test fail.
7
+ *
8
+ * Returns axe's result. Assert on `violations` to fail the test on them, or leave them be to
9
+ * record them as warnings. `configure` narrows or widens the scan, e.g.
10
+ * `axe => axe.withTags(['wcag2a', 'wcag2aa'])`.
11
+ */
12
+ async function scanA11y(page, configure) {
13
+ const { default: Axe } = await import("@axe-core/playwright").catch(() => {
14
+ throw new Error("scanA11y needs @axe-core/playwright: add it to your dev dependencies.");
15
+ });
16
+ const axe = new Axe({ page });
17
+ const results = await (configure?.(axe) ?? axe).analyze();
18
+ await test.info().attach(A11Y_ATTACHMENT, {
19
+ body: JSON.stringify(results),
20
+ contentType: "application/json"
21
+ });
22
+ return results;
23
+ }
24
+ //#endregion
25
+ export { A11Y_ATTACHMENT, scanA11y };
package/dist/ct.d.mts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { TestInfo, expect } from "@playwright/test";
2
2
  //#region src/ct.d.ts
3
3
  type Annotations = TestInfo['annotations'];
4
- declare function recordStory(annotations: Annotations, storyId: string): void;
5
- declare function withStoryAnnotations<M extends (storyId: string, ...rest: any[]) => unknown>(mount: M, annotations: Annotations): M;
4
+ export declare function recordStory(annotations: Annotations, storyId: string): void;
5
+ export declare function withStoryAnnotations<M extends (storyId: string, ...rest: any[]) => unknown>(mount: M, annotations: Annotations): M;
6
6
  /**
7
7
  * Drop-in replacement for `test` from `@playwright/test` in component tests: `mount` records
8
8
  * the mounted story id on the test, which lets kinora group results by component and story.
9
9
  */
10
- declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
10
+ export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
11
11
  //#endregion
12
- export { expect, recordStory, test, withStoryAnnotations };
12
+ export { expect };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AttachmentKind, CiMeta, GitMeta } from "@kinora/core";
2
2
  import { FullConfig, FullResult, Reporter, Suite } from "@playwright/test/reporter";
3
3
  //#region src/index.d.ts
4
- interface KinoraReporterOptions {
4
+ export interface KinoraReporterOptions {
5
5
  /** kinora server base URL. Defaults to env KINORA_URL, then the hosted cloud. Set for self-host. */
6
6
  url?: string;
7
7
  /** Project API token. Defaults to env KINORA_TOKEN (keep it out of the config file). */
@@ -30,7 +30,7 @@ interface KinoraReporterOptions {
30
30
  */
31
31
  uploadAttachments?: AttachmentKind[];
32
32
  }
33
- declare class KinoraReporter implements Reporter {
33
+ export default class KinoraReporter implements Reporter {
34
34
  private readonly options;
35
35
  private suite;
36
36
  private config;
@@ -39,5 +39,4 @@ declare class KinoraReporter implements Reporter {
39
39
  onEnd(result: FullResult): Promise<void>;
40
40
  private maybePostPrComment;
41
41
  }
42
- //#endregion
43
- export { KinoraReporterOptions, KinoraReporter as default };
42
+ //#endregion
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { readFileSync } from "node:fs";
1
+ import { readFileSync, statSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import process from "node:process";
4
- import { DEFAULT_KINORA_URL, DEFAULT_UPLOAD_ATTACHMENTS, IngestError, createIngestClient, detectCiEnv, effectiveAttachments, isUploadableAttachment, makeTestKey, postPrComment, resolvePrContext } from "@kinora/core";
4
+ import { DEFAULT_KINORA_URL, DEFAULT_UPLOAD_ATTACHMENTS, IngestError, a11yFromScans, createIngestClient, detectCiEnv, effectiveAttachments, isUploadableAttachment, makeTestKey, parseAxeScan, postPrComment, resolvePrContext } from "@kinora/core";
5
5
  //#region src/index.ts
6
6
  function identity(test) {
7
7
  let file = "";
@@ -24,9 +24,24 @@ function identity(test) {
24
24
  projectName
25
25
  };
26
26
  }
27
+ const MAX_SCAN_BYTES = 20971520;
28
+ function axeScans(attachments) {
29
+ return attachments.flatMap((a) => {
30
+ if (!a.contentType.includes("json")) return [];
31
+ try {
32
+ const text = a.body ? a.body.length <= MAX_SCAN_BYTES ? a.body.toString("utf8") : void 0 : a.path && statSync(a.path).size <= MAX_SCAN_BYTES ? readFileSync(a.path, "utf8") : void 0;
33
+ const scan = text ? parseAxeScan(text) : void 0;
34
+ return scan ? [scan] : [];
35
+ } catch {
36
+ return [];
37
+ }
38
+ });
39
+ }
27
40
  function toNormTest(test) {
28
41
  const { file, titlePath, projectName } = identity(test);
29
42
  const last = test.results.at(-1);
43
+ const attachments = effectiveAttachments(test.results);
44
+ const a11y = a11yFromScans(axeScans(attachments), test.ok());
30
45
  return {
31
46
  testKey: makeTestKey(file, titlePath, projectName),
32
47
  title: test.title,
@@ -49,12 +64,13 @@ function toNormTest(test) {
49
64
  stack: e.stack,
50
65
  location: e.location
51
66
  }] : []),
52
- attachments: effectiveAttachments(test.results).map((a) => ({
67
+ attachments: attachments.map((a) => ({
53
68
  name: a.name,
54
69
  contentType: a.contentType,
55
70
  path: a.path,
56
71
  hasBody: a.body != null
57
- }))
72
+ })),
73
+ ...a11y && { a11y }
58
74
  };
59
75
  }
60
76
  function countsOf(tests) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kinora/reporter",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.8.1",
5
5
  "description": "Playwright reporter that uploads test results to a kinora server",
6
6
  "author": "Joris Gallot",
7
7
  "license": "MIT",
@@ -21,31 +21,39 @@
21
21
  ],
22
22
  "exports": {
23
23
  ".": "./dist/index.mjs",
24
- "./ct": "./dist/ct.mjs"
24
+ "./ct": "./dist/ct.mjs",
25
+ "./a11y": "./dist/a11y.mjs"
25
26
  },
26
27
  "main": "./dist/index.mjs",
27
28
  "files": [
28
29
  "dist"
29
30
  ],
30
31
  "engines": {
31
- "node": ">=20"
32
+ "node": ">=20.20.2"
32
33
  },
33
34
  "publishConfig": {
34
35
  "access": "public"
35
36
  },
36
37
  "peerDependencies": {
38
+ "@axe-core/playwright": ">=4.7.0",
37
39
  "@playwright/test": ">=1.40.0"
38
40
  },
41
+ "peerDependenciesMeta": {
42
+ "@axe-core/playwright": {
43
+ "optional": true
44
+ }
45
+ },
39
46
  "dependencies": {
40
- "@kinora/core": "0.7.0"
47
+ "@kinora/core": "0.10.1"
41
48
  },
42
49
  "devDependencies": {
43
- "@playwright/test": "^1.63.0",
44
- "@types/node": "^26.5.1",
45
- "@vitest/coverage-v8": "^4.1.11",
46
- "tsdown": "^0.22.14",
50
+ "@axe-core/playwright": "^4.13.0",
51
+ "@playwright/test": "^1.64.0",
52
+ "@types/node": "^26.6.5",
53
+ "@vitest/coverage-v8": "^5.0.4",
54
+ "tsdown": "^0.23.1",
47
55
  "typescript": "~6.0.3",
48
- "vitest": "^4.1.11"
56
+ "vitest": "^5.0.4"
49
57
  },
50
58
  "scripts": {
51
59
  "build": "tsdown",