@kinora/reporter 0.6.0 → 0.8.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 CHANGED
@@ -37,8 +37,9 @@ Create an API token in the kinora dashboard (Settings → Workspace). Self-hosti
37
37
 
38
38
  ## Videos and screenshots without tracing
39
39
 
40
- Traces are uploaded by default, and Playwright already embeds a test's screenshots and video
41
- inside its trace.zip. If you run without tracing, upload them on their own:
40
+ Traces are uploaded by default, along with the expected / actual / diff images of a failed
41
+ `toHaveScreenshot`, which kinora shows as a comparison. Playwright already embeds a test's other
42
+ screenshots and its video inside the trace.zip. If you run without tracing, upload them on their own:
42
43
 
43
44
  ```ts
44
45
  reporter: [['@kinora/reporter', {
@@ -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). */
@@ -23,13 +23,14 @@ interface KinoraReporterOptions {
23
23
  policy?: 'always' | 'on-failure';
24
24
  };
25
25
  /**
26
- * Which attachment kinds to upload. Defaults to `['trace']`. Add `'video'` / `'screenshot'`
26
+ * Which attachment kinds to upload. Defaults to `['trace', 'snapshot']` (`snapshot` = the images
27
+ * of a failed screenshot assertion, shown as a comparison). Add `'video'` / `'screenshot'`
27
28
  * to host them on their own, which is what you want when `trace` is off (with tracing on,
28
29
  * Playwright already embeds them in the trace.zip).
29
30
  */
30
31
  uploadAttachments?: AttachmentKind[];
31
32
  }
32
- declare class KinoraReporter implements Reporter {
33
+ export default class KinoraReporter implements Reporter {
33
34
  private readonly options;
34
35
  private suite;
35
36
  private config;
@@ -38,5 +39,4 @@ declare class KinoraReporter implements Reporter {
38
39
  onEnd(result: FullResult): Promise<void>;
39
40
  private maybePostPrComment;
40
41
  }
41
- //#endregion
42
- 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.6.0",
4
+ "version": "0.8.0",
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.6.0"
47
+ "@kinora/core": "0.10.0"
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.3",
54
+ "tsdown": "^0.23.1",
47
55
  "typescript": "~6.0.3",
48
- "vitest": "^4.1.11"
56
+ "vitest": "^5.0.3"
49
57
  },
50
58
  "scripts": {
51
59
  "build": "tsdown",