@aerokit/sdk 14.73.0 → 14.75.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerokit/sdk",
3
- "version": "14.73.0",
3
+ "version": "14.75.0",
4
4
  "description": "Unified TypeScript SDK for modern cloud platforms.",
5
5
  "scripts": {
6
6
  "clean": "rm -rf dist",
@@ -59,11 +59,15 @@
59
59
  }
60
60
  },
61
61
  "peerDependencies": {
62
- "@playwright/test": ">=1.45"
62
+ "@playwright/test": ">=1.45",
63
+ "@axe-core/playwright": ">=4.10"
63
64
  },
64
65
  "peerDependenciesMeta": {
65
66
  "@playwright/test": {
66
67
  "optional": true
68
+ },
69
+ "@axe-core/playwright": {
70
+ "optional": true
67
71
  }
68
72
  }
69
73
  }
@@ -0,0 +1,86 @@
1
+ import { expect, test } from '../fixtures.js';
2
+ import { CALENDAR_OR_SLOTS, listCrumb } from './list.js';
3
+
4
+ // The WCAG 2.1 level AA conformance rules, and the level A ones it includes (dirigible #7645).
5
+ const WCAG_21_AA = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'];
6
+ const ENFORCED_IMPACTS = new Set(['serious', 'critical']);
7
+
8
+ // report (default): attach every page's axe result to the test report and pass - the first
9
+ // release of a module. strict: also fail on a serious or critical violation. off: no scan.
10
+ const MODE = process.env.APPTEST_A11Y ?? 'report';
11
+
12
+ // Harmonia's default palette fails contrast against its own --primary (white on it, and it as link
13
+ // text in dark mode); that is fixed in the component library, so strict mode does not fail an app
14
+ // for it. Any other contrast failure still counts.
15
+ const PRIMARY_COLOR = () => {
16
+ const probe = document.createElement('div');
17
+ probe.style.color = 'var(--primary)';
18
+ document.body.appendChild(probe);
19
+ const color = getComputedStyle(probe).color;
20
+ probe.remove();
21
+ const context = document.createElement('canvas').getContext('2d');
22
+ context.fillStyle = color;
23
+ context.fillRect(0, 0, 1, 1);
24
+ const [r, g, b] = context.getImageData(0, 0, 1, 1).data;
25
+ return '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
26
+ };
27
+
28
+ function sameColor(measured, expected) {
29
+ if (typeof measured !== 'string' || measured.length !== 7 || !expected) return false;
30
+ for (let channel = 1; channel < 7; channel += 2) {
31
+ const a = parseInt(measured.slice(channel, channel + 2), 16);
32
+ const b = parseInt(expected.slice(channel, channel + 2), 16);
33
+ if (Math.abs(a - b) > 2) return false;
34
+ }
35
+ return true;
36
+ }
37
+
38
+ function onPrimary(node, primary) {
39
+ return (node.any ?? []).some((check) => sameColor(check.data?.fgColor, primary) || sameColor(check.data?.bgColor, primary));
40
+ }
41
+
42
+ async function axeBuilder() {
43
+ try {
44
+ return (await import('@axe-core/playwright')).default;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ // The list page and the create form of every entity with a page of its own, scanned by axe-core.
51
+ export function a11yFlow(manifest, entity) {
52
+ if (MODE === 'off' || !entity.route) return;
53
+ test(`${entity.name}: list and form pages pass axe-core (WCAG 2.1 AA)`, async ({ page }, testInfo) => {
54
+ const AxeBuilder = await axeBuilder();
55
+ test.skip(!AxeBuilder, 'add @axe-core/playwright to the harness devDependencies to run the accessibility flow');
56
+
57
+ const calendar = entity.layout === 'calendar' || entity.layout === 'slots';
58
+ const pages = [['list', entity.route, () => (calendar ? page.locator(CALENDAR_OR_SLOTS).first() : listCrumb(page, entity))]];
59
+ if (!calendar && !entity.hierarchy) {
60
+ pages.push(['form', entity.route + '/create', () => page.locator('input, select, textarea').filter({ visible: true }).first()]);
61
+ }
62
+
63
+ const findings = [];
64
+ for (const [name, route, ready] of pages) {
65
+ await page.goto(manifest.standaloneShell + route);
66
+ await expect(ready()).toBeVisible();
67
+ const results = await new AxeBuilder({ page }).withTags(WCAG_21_AA).analyze();
68
+ await testInfo.attach(`axe-${entity.name}-${name}.json`, {
69
+ body: JSON.stringify(results, null, 2),
70
+ contentType: 'application/json',
71
+ });
72
+ const primary = await page.evaluate(PRIMARY_COLOR);
73
+ for (const violation of results.violations) {
74
+ if (!ENFORCED_IMPACTS.has(violation.impact)) continue;
75
+ const nodes = violation.nodes.filter((node) => !(violation.id === 'color-contrast' && onPrimary(node, primary)));
76
+ if (nodes.length) {
77
+ const targets = nodes.slice(0, 5).map((node) => node.target.join(' ')).join(', ');
78
+ findings.push(`${name}: [${violation.impact}] ${violation.id} - ${violation.help} (${nodes.length} elements: ${targets})`);
79
+ }
80
+ }
81
+ }
82
+ if (MODE === 'strict') {
83
+ expect(findings, 'serious or critical WCAG 2.1 AA violations:\n' + findings.join('\n')).toEqual([]);
84
+ }
85
+ });
86
+ }
package/test/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import { test } from './fixtures.js';
3
+ import { a11yFlow } from './flows/a11y.js';
3
4
  import { crudFlow } from './flows/crud.js';
4
5
  import { listFlow } from './flows/list.js';
5
6
  import { multilingualFlow } from './flows/multilingual.js';
@@ -20,6 +21,7 @@ export function runTest(manifestRef, opts = {}) {
20
21
  myFlow(manifest, entity, opts);
21
22
  multilingualFlow(manifest, entity, opts);
22
23
  shellFlow(manifest, entity, opts);
24
+ a11yFlow(manifest, entity, opts);
23
25
  });
24
26
  }
25
27
  }