@ultimat3/cli 20.2.1 → 22.0.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.
Files changed (237) hide show
  1. package/CLAUDE.md +84 -1582
  2. package/README.md +40 -2
  3. package/package.json +31 -31
  4. package/src/api-registration.ts +124 -0
  5. package/src/app-artifacts.ts +39 -0
  6. package/src/app-boundaries.ts +5 -10
  7. package/src/app-env.ts +2 -2
  8. package/src/app-load.ts +37 -5
  9. package/src/app-openapi.ts +27 -0
  10. package/src/app-permissions.ts +0 -0
  11. package/src/app-root.ts +1 -1
  12. package/src/async-pages.ts +32 -0
  13. package/src/bin.ts +7 -1
  14. package/src/boundary-findings.ts +36 -0
  15. package/src/browser-launcher-fake-html.ts +80 -0
  16. package/src/browser-launcher-fake.ts +165 -0
  17. package/src/browser-launcher-port.ts +159 -0
  18. package/src/browser-launcher.ts +26 -133
  19. package/src/budgets.ts +56 -13
  20. package/src/build-errors.ts +54 -0
  21. package/src/cdp-shot-a11y.ts +77 -0
  22. package/src/cdp-shot-clock.ts +14 -0
  23. package/src/cdp-shot-driver.ts +150 -0
  24. package/src/cdp-shot-element.ts +147 -0
  25. package/src/cdp-shot-errors.ts +62 -0
  26. package/src/cdp-shot-keys.ts +152 -0
  27. package/src/cdp-shot-page.ts +230 -0
  28. package/src/cdp-shot-watch.ts +241 -0
  29. package/src/cmd-affected-spec.ts +24 -0
  30. package/src/cmd-affected.ts +3 -19
  31. package/src/cmd-build-spec.ts +16 -0
  32. package/src/cmd-build.ts +46 -20
  33. package/src/cmd-ci-spec.ts +24 -0
  34. package/src/cmd-ci.ts +17 -21
  35. package/src/cmd-db-branch.ts +2 -2
  36. package/src/cmd-db-spec.ts +99 -0
  37. package/src/cmd-db.ts +15 -94
  38. package/src/cmd-deploy-helm.ts +136 -0
  39. package/src/cmd-deploy-spec.ts +40 -0
  40. package/src/cmd-deploy.ts +117 -42
  41. package/src/cmd-dev-spec.ts +25 -0
  42. package/src/cmd-dev.ts +78 -122
  43. package/src/cmd-docs-spec.ts +16 -0
  44. package/src/cmd-docs.ts +2 -11
  45. package/src/cmd-doctor-spec.ts +21 -0
  46. package/src/cmd-doctor.ts +4 -18
  47. package/src/cmd-env-spec.ts +18 -0
  48. package/src/cmd-env.ts +4 -13
  49. package/src/cmd-errors-spec.ts +23 -0
  50. package/src/cmd-errors.ts +3 -17
  51. package/src/cmd-fix-spec.ts +18 -0
  52. package/src/cmd-fix.ts +4 -13
  53. package/src/cmd-generate-spec.ts +31 -0
  54. package/src/cmd-generate.ts +46 -42
  55. package/src/cmd-i18n-spec.ts +16 -0
  56. package/src/cmd-i18n.ts +7 -14
  57. package/src/cmd-jobs-spec.ts +56 -0
  58. package/src/cmd-jobs.ts +3 -50
  59. package/src/cmd-manifest-spec.ts +15 -0
  60. package/src/cmd-manifest.ts +19 -20
  61. package/src/cmd-mcp-spec.ts +21 -0
  62. package/src/cmd-mcp.ts +2 -15
  63. package/src/cmd-new-spec.ts +33 -0
  64. package/src/cmd-new.ts +38 -33
  65. package/src/cmd-policy-spec.ts +13 -0
  66. package/src/cmd-policy.ts +2 -9
  67. package/src/cmd-pr-spec.ts +39 -0
  68. package/src/cmd-pr.ts +5 -65
  69. package/src/cmd-registries-spec.ts +32 -0
  70. package/src/cmd-registries.ts +4 -25
  71. package/src/cmd-routes-spec.ts +12 -0
  72. package/src/cmd-routes.ts +2 -7
  73. package/src/cmd-secrets-spec.ts +19 -0
  74. package/src/cmd-secrets.ts +22 -34
  75. package/src/cmd-shot-island.ts +12 -13
  76. package/src/cmd-shot-spec.ts +53 -0
  77. package/src/cmd-shot.ts +11 -60
  78. package/src/cmd-tasks-spec.ts +21 -0
  79. package/src/cmd-tasks.ts +2 -16
  80. package/src/cmd-test-spec.ts +54 -0
  81. package/src/cmd-test.ts +4 -49
  82. package/src/cmd-verify-spec.ts +28 -0
  83. package/src/cmd-verify.ts +3 -23
  84. package/src/db-seed.ts +4 -2
  85. package/src/dev-dashboard.ts +4 -4
  86. package/src/dev-lock.ts +18 -2
  87. package/src/dev-port.ts +21 -0
  88. package/src/dev-route-table.ts +119 -0
  89. package/src/dispatch.ts +12 -5
  90. package/src/document-styles.ts +1 -1
  91. package/src/drift.ts +4 -18
  92. package/src/error-catalog.ts +16 -9
  93. package/src/error-codes.ts +33 -28
  94. package/src/error-pages.ts +19 -7
  95. package/src/error-unthrown.ts +130 -0
  96. package/src/errors.ts +22 -29
  97. package/src/favicon.ts +2 -2
  98. package/src/fix-imports.ts +1 -1
  99. package/src/fix-scan.ts +2 -9
  100. package/src/flag-reads.ts +1 -2
  101. package/src/foreign-text.ts +36 -0
  102. package/src/framework-schema.ts +5 -6
  103. package/src/generate-feature.ts +42 -0
  104. package/src/generate-files.ts +7 -3
  105. package/src/generate-grants.ts +83 -0
  106. package/src/generate-kinds.ts +56 -4
  107. package/src/i18n-index.ts +59 -10
  108. package/src/icon-assets.ts +1 -1
  109. package/src/image-prepare.ts +14 -0
  110. package/src/import-scan.ts +63 -0
  111. package/src/index.ts +67 -334
  112. package/src/invocation-flags.ts +26 -0
  113. package/src/island-bundle.ts +38 -11
  114. package/src/island-capture.ts +6 -6
  115. package/src/island-harness-script.ts +6 -2
  116. package/src/island-realtime.ts +98 -0
  117. package/src/island-shot.ts +5 -0
  118. package/src/island-store.ts +131 -0
  119. package/src/island-verdict.ts +2 -2
  120. package/src/job-registration.ts +42 -0
  121. package/src/jobs-driver.ts +2 -2
  122. package/src/live-routes.ts +82 -42
  123. package/src/load-findings.ts +51 -0
  124. package/src/mcp-db-target.ts +1 -1
  125. package/src/mcp-errors.ts +30 -26
  126. package/src/mcp-host.ts +27 -12
  127. package/src/mcp-ui-diff.ts +27 -0
  128. package/src/mcp-ui-inspect.ts +4 -4
  129. package/src/mcp-ui-interact.ts +21 -15
  130. package/src/mcp-ui.ts +19 -15
  131. package/src/measure-database.ts +73 -0
  132. package/src/measure-paths.ts +74 -0
  133. package/src/measure-scope.ts +74 -0
  134. package/src/messages.ts +1 -3
  135. package/src/metrics-endpoint.ts +1 -1
  136. package/src/otlp-export.ts +1 -1
  137. package/src/output.ts +6 -0
  138. package/src/page-sync.ts +54 -0
  139. package/src/permission-grants.ts +86 -0
  140. package/src/prerender-out.ts +25 -0
  141. package/src/prerender.ts +121 -102
  142. package/src/pwa-artifacts.ts +3 -3
  143. package/src/realtime-browser-probe-fixture.ts +2 -2
  144. package/src/reexport-manifest.ts +2 -1
  145. package/src/registry.ts +80 -56
  146. package/src/role-realtime.ts +36 -0
  147. package/src/{dev-replicator.ts → role-replicator.ts} +1 -1
  148. package/src/{dev-roles-fixture.ts → role-start-fixture.ts} +9 -5
  149. package/src/role-start-types.ts +112 -0
  150. package/src/{dev-roles.ts → role-start.ts} +42 -115
  151. package/src/{dev-sync.ts → role-sync.ts} +23 -11
  152. package/src/root-env.ts +67 -0
  153. package/src/{dev-assets.ts → runtime-assets.ts} +7 -7
  154. package/src/{dev-services.ts → runtime-bindings.ts} +43 -17
  155. package/src/{dev-cache.ts → runtime-cache.ts} +2 -2
  156. package/src/runtime-jobs.ts +87 -0
  157. package/src/{dev-live-feed.ts → runtime-live-feed.ts} +20 -5
  158. package/src/{dev-notify-retention.ts → runtime-notify-retention.ts} +1 -1
  159. package/src/{dev-purge.ts → runtime-purge.ts} +2 -2
  160. package/src/{dev-queue.ts → runtime-queue.ts} +4 -4
  161. package/src/runtime-realtime.ts +55 -0
  162. package/src/{dev-render.ts → runtime-render.ts} +119 -20
  163. package/src/{dev-replica.ts → runtime-replica.ts} +2 -2
  164. package/src/{dev-runtime.ts → runtime-services.ts} +41 -17
  165. package/src/{dev-storage.ts → runtime-storage.ts} +4 -4
  166. package/src/scaffold-fixture.ts +28 -6
  167. package/src/scaffold-typecheck.ts +6 -3
  168. package/src/schema-drift.ts +7 -1
  169. package/src/script-csp.ts +5 -2
  170. package/src/secrets-rotation.ts +59 -0
  171. package/src/serve-boot.ts +192 -0
  172. package/src/serve-drain.ts +24 -0
  173. package/src/serve-entry.ts +6 -0
  174. package/src/serve-env.ts +116 -0
  175. package/src/serve-types.ts +55 -0
  176. package/src/serve.ts +44 -338
  177. package/src/shot-server.ts +2 -2
  178. package/src/shot-settle.ts +10 -1
  179. package/src/shot-theme.ts +3 -3
  180. package/src/shot-verdict.ts +16 -7
  181. package/src/signal-shred.ts +27 -0
  182. package/src/solid-loader.ts +26 -2
  183. package/src/static-report.ts +8 -1
  184. package/src/sw-artifacts.ts +13 -3
  185. package/src/sync-url.ts +31 -0
  186. package/src/templates/action.ts +30 -16
  187. package/src/templates/entity.ts +12 -7
  188. package/src/templates/index.ts +1 -1
  189. package/src/templates/job.ts +10 -7
  190. package/src/templates/policy.ts +20 -2
  191. package/src/templates/resource-create.ts +127 -0
  192. package/src/templates/resource-form-island.ts +76 -30
  193. package/src/templates/resource.ts +10 -4
  194. package/src/templates/route.ts +3 -0
  195. package/src/templates/scaffold-app.ts +4 -1
  196. package/src/templates/scaffold-auth.ts +3 -1
  197. package/src/templates/scaffold-container-compose.ts +184 -0
  198. package/src/templates/scaffold-container.ts +25 -145
  199. package/src/templates/scaffold-dashboard-example.ts +2 -2
  200. package/src/templates/scaffold-db-package.ts +16 -3
  201. package/src/templates/scaffold-demo-org.ts +41 -0
  202. package/src/templates/scaffold-entries.ts +1 -1
  203. package/src/templates/scaffold-env.ts +6 -0
  204. package/src/templates/scaffold-helm-templates.ts +66 -7
  205. package/src/templates/scaffold-helm.ts +27 -5
  206. package/src/templates/scaffold-i18n.ts +15 -10
  207. package/src/templates/scaffold-repo.ts +13 -10
  208. package/src/templates/scaffold-roles.ts +38 -10
  209. package/src/templates/slice-foundation.ts +1 -1
  210. package/src/templates/wrap.ts +4 -1
  211. package/src/test-passes.ts +2 -1
  212. package/src/test-workers.ts +26 -0
  213. package/src/ts-scan.ts +3 -6
  214. package/src/tsconfig-references.ts +1 -2
  215. package/src/verify-checks.ts +41 -39
  216. package/src/verify-e2e.ts +41 -0
  217. package/src/verify-run.ts +115 -50
  218. package/src/verify-step.ts +3 -3
  219. package/src/verify-tests.ts +22 -30
  220. package/src/verify-typecheck.ts +28 -0
  221. package/src/web-binding.ts +2 -2
  222. package/src/worker-bundle.ts +192 -0
  223. package/src/workspace-graph.ts +10 -33
  224. package/src/cdp-browser.ts +0 -100
  225. package/src/cdp-connection.ts +0 -211
  226. package/src/cdp-e2e-page.ts +0 -209
  227. package/src/cdp-errors.ts +0 -56
  228. package/src/cdp-launch.ts +0 -139
  229. package/src/e2e-dom-fixture.ts +0 -117
  230. package/src/e2e-driver.ts +0 -97
  231. package/src/e2e-errors.ts +0 -103
  232. package/src/e2e-evaluate.ts +0 -156
  233. package/src/e2e-locator.ts +0 -86
  234. package/src/e2e-page.ts +0 -150
  235. package/src/e2e-selection.ts +0 -182
  236. package/src/measurement-actor.ts +0 -26
  237. /package/src/{dev-hooks.ts → runtime-hooks.ts} +0 -0
@@ -1,86 +0,0 @@
1
- // `LocatorLike` over a browser page: a handle that holds a SELECTION and resolves nothing until
2
- // something asks. One round trip per question, and no waiting of its own — `toBeVisible()` owns
3
- // the only retry budget in the harness and drives this by looking again.
4
-
5
- import type { LocatorLike } from '@ultimat3/testing';
6
- import { E2eLocatorAmbiguousError, E2eLocatorEmptyError } from './e2e-errors';
7
- import type { E2eResolution, E2eSelection } from './e2e-selection';
8
- import { markSelector, selectionExpression, unmarkExpression } from './e2e-selection';
9
-
10
- /**
11
- * What a locator needs from the browser. Narrower than `ScrapePage` on purpose: three methods is
12
- * what a test has to stand up to prove the mapping, and `click` is taken from the page rather than
13
- * re-derived here so the actionability wait `@ultimat3/scraping` already performs is the one that
14
- * runs.
15
- */
16
- export interface LocatablePage {
17
- evaluate(expression: string): Promise<unknown>;
18
- click(selector: string): Promise<void>;
19
- url(): string;
20
- }
21
-
22
- const readResolution = (raw: unknown): E2eResolution => {
23
- const decoded = typeof raw === 'string' ? (JSON.parse(raw) as unknown) : raw;
24
- const held = decoded as { count?: unknown; visible?: unknown; marked?: unknown };
25
- return {
26
- count: typeof held?.count === 'number' ? held.count : 0,
27
- visible: held?.visible === true,
28
- marked: held?.marked === true,
29
- };
30
- };
31
-
32
- /**
33
- * One mark per click, never a constant: two locators marking the same page at once would each
34
- * click whichever element the other had just tagged. Seeded from a counter and not from
35
- * `Math.random()`, which `bun run flight-copies` refuses in shipped source and which would make
36
- * two runs of one test address two different elements.
37
- */
38
- let marks = 0;
39
- const nextMark = (): string => {
40
- marks += 1;
41
- return `m${String(marks)}`;
42
- };
43
-
44
- /** Test seam: the counter is process-global, so a test that asserts a mark resets it first. */
45
- export const resetLocatorMarks = (): void => {
46
- marks = 0;
47
- };
48
-
49
- /**
50
- * Nothing is cached. A locator taken before a navigation must address the page that is there when
51
- * it is USED — the same rule `ScrapeFrame` states for a frame handle — so every method below
52
- * re-resolves, and a handle can never go stale behind the caller's back.
53
- */
54
- export function e2eLocator(page: LocatablePage, selection: E2eSelection): LocatorLike {
55
- const resolve = async (mark?: string): Promise<E2eResolution> =>
56
- readResolution(await page.evaluate(selectionExpression(selection, mark)));
57
-
58
- return {
59
- count: async () => (await resolve()).count,
60
- /**
61
- * A point-in-time look, and deliberately not an ambiguity check: an assertion handed a locator
62
- * that matched three elements has an ANSWER — was the first one visible — and a refusal there
63
- * would turn `expect(...).toBeVisible()` into a crash rather than a verdict. Only `click`
64
- * refuses, because only `click` has to pick one.
65
- */
66
- isVisible: async () => (await resolve()).visible,
67
- first: () => e2eLocator(page, { ...selection, first: true }),
68
- click: async () => {
69
- const mark = nextMark();
70
- const resolution = await resolve(mark);
71
- if (resolution.count === 0) {
72
- throw new E2eLocatorEmptyError({ selection, url: page.url() });
73
- }
74
- if (resolution.count > 1 && !selection.first) {
75
- throw new E2eLocatorAmbiguousError({ selection, count: resolution.count });
76
- }
77
- try {
78
- await page.click(markSelector(mark));
79
- } finally {
80
- // Best effort, and never allowed to replace the click's own failure: a click that
81
- // navigated took the whole document — attribute included — with it.
82
- await page.evaluate(unmarkExpression(mark)).catch(() => undefined);
83
- }
84
- },
85
- };
86
- }
package/src/e2e-page.ts DELETED
@@ -1,150 +0,0 @@
1
- // `PageLike` over a browser page. The adapter itself: `@ultimat3/testing` declares the surface an
2
- // e2e test drives and `@ultimat3/scraping` owns the only driver that can drive one, and neither
3
- // may import the other — so the join is here, in the one package allowed to know about both.
4
-
5
- import { finiteCount } from '@ultimat3/core';
6
- import type { LocatorLike, PageLike } from '@ultimat3/testing';
7
- import { E2eServiceWorkerAbsentError } from './e2e-errors';
8
- import { evaluateClosure } from './e2e-evaluate';
9
- import { e2eLocator } from './e2e-locator';
10
- import type { E2eSelection } from './e2e-selection';
11
-
12
- /**
13
- * What this adapter needs of a browser: four required members, every one of them on `ScrapePage`.
14
- * Declared structurally rather than as `ScrapePage` so a test can stand one up in six lines — the
15
- * same bargain `cdp-port.ts` makes about puppeteer, one layer up.
16
- */
17
- export interface E2eBrowserPage {
18
- url(): string;
19
- goto(url: string, options?: { readonly timeout?: number | undefined }): Promise<unknown>;
20
- evaluate(expression: string): Promise<unknown>;
21
- click(selector: string): Promise<void>;
22
- /**
23
- * The browser's own network condition, which `E2eFixtures.offline()`/`online()` forward to.
24
- * `ScrapePage` has it (`page-over-target.ts`), reaching `CdpPageLike.setOfflineMode` through
25
- * `cdp-target.ts`'s guard.
26
- *
27
- * OPTIONAL for the reason the four above are structural: this port is the shape of somebody
28
- * ELSE's object, and requiring it would cost every six-line double a type error for a capability
29
- * a test that never goes offline does not need. Absent, `e2e-driver.ts` keeps refusing by name —
30
- * a coded refusal, never a silent no-op, because an `offline()` that did nothing would let the
31
- * app's ONLINE page pass an offline test.
32
- */
33
- offline?(enabled: boolean): Promise<void>;
34
- }
35
-
36
- export interface E2ePageOptions {
37
- readonly page: E2eBrowserPage;
38
- /** Every `goto('/feed')` in an e2e suite is app-relative; this is what makes one absolute. */
39
- readonly baseUrl: string;
40
- /** Per-navigation deadline, handed to the driver rather than enforced here. */
41
- readonly timeoutMs?: number | undefined;
42
- /** How long `waitForServiceWorker()` waits before refusing. Bounded IN THE PAGE. */
43
- readonly serviceWorkerTimeoutMs?: number | undefined;
44
- }
45
-
46
- /** Named once, so both refusals below name the call an app's test preload actually makes. */
47
- const SUBJECT = 'installE2eDriver';
48
-
49
- export const DEFAULT_E2E_TIMEOUT_MS = 30_000;
50
- export const DEFAULT_SERVICE_WORKER_TIMEOUT_MS = 10_000;
51
-
52
- /** Every in-page expression here answers JSON text, for the reason `cdp-snapshot.ts` states. */
53
- const readField = (raw: unknown, key: string): unknown => {
54
- const decoded = typeof raw === 'string' ? (JSON.parse(raw) as unknown) : raw;
55
- return typeof decoded === 'object' && decoded !== null
56
- ? (decoded as Record<string, unknown>)[key]
57
- : undefined;
58
- };
59
-
60
- const TITLE = '(() => JSON.stringify({ title: document.title }))()';
61
-
62
- /**
63
- * The first flush, read by a `fetch` INSIDE the page after the navigation, because neither
64
- * `ScrapePage` nor `CdpPageLike` exposes a response body — puppeteer's `page.on('response')` is
65
- * not on the port and adding it would be an edit to `@ultimat3/scraping`.
66
- *
67
- * The cost, stated rather than hidden: this is a SECOND request to the same route, so what it
68
- * measures is that route's streaming behaviour and not the byte-for-byte first chunk the open
69
- * document received. It runs in the page, so it carries the page's cookies and its origin — a
70
- * `fetch` from the test process would carry neither.
71
- */
72
- const firstFlushExpression = (url: string): string =>
73
- `(() => fetch(${JSON.stringify(url)}, { credentials: 'same-origin' })
74
- .then((response) => response.body.getReader().read())
75
- .then((chunk) => JSON.stringify({ html: new TextDecoder().decode(chunk.value || new Uint8Array()) })))()`;
76
-
77
- /**
78
- * `ready` alone is not control: a first load activates a worker that is not yet the page's
79
- * controller, and an offline assertion made in that window tests nothing. So this waits for
80
- * `controllerchange` too — an EVENT, not a poll, so the harness still has exactly one retry loop.
81
- */
82
- const serviceWorkerExpression = (timeoutMs: number): string =>
83
- `(() => {
84
- if (!navigator.serviceWorker) return JSON.stringify({ controlled: false });
85
- const controlled = new Promise((resolve) => {
86
- if (navigator.serviceWorker.controller) { resolve(true); return; }
87
- navigator.serviceWorker.addEventListener('controllerchange', () => resolve(true), { once: true });
88
- });
89
- const deadline = new Promise((resolve) => setTimeout(() => resolve(false), ${String(timeoutMs)}));
90
- return Promise.race([navigator.serviceWorker.ready.then(() => controlled), deadline])
91
- .catch(() => false)
92
- .then((ok) => JSON.stringify({ controlled: ok === true }));
93
- })()`;
94
-
95
- /**
96
- * The adapter. Every member re-reads the live page: a `PageLike` handed to a test outlives every
97
- * navigation the test makes, so nothing here may capture a URL, a document or an element.
98
- */
99
- export function e2ePage(options: E2ePageOptions): PageLike {
100
- const page = options.page;
101
- // Both are screened HERE, at construction, and not where they land: `timeout` is handed to a
102
- // driver that reads it as a deadline, and `swTimeout` is INTERPOLATED into the in-page source,
103
- // where `setTimeout(fn, NaN)` is `setTimeout(fn, 0)` — so a NaN budget makes every
104
- // `waitForServiceWorker()` refuse a worker that really did take control. A misdiagnosis reported
105
- // as a test failure is worse than the failure. Floor 0, because a driver reads `timeout: 0` as
106
- // "no deadline" and that is a value an app is entitled to declare.
107
- const timeout = finiteCount(SUBJECT, 'timeoutMs', options.timeoutMs ?? DEFAULT_E2E_TIMEOUT_MS);
108
- const swTimeout = finiteCount(
109
- SUBJECT,
110
- 'serviceWorkerTimeoutMs',
111
- options.serviceWorkerTimeoutMs ?? DEFAULT_SERVICE_WORKER_TIMEOUT_MS,
112
- );
113
- const absolute = (url: string): string => new URL(url, options.baseUrl).toString();
114
- const locate = (selection: E2eSelection): LocatorLike => e2eLocator(page, selection);
115
-
116
- return {
117
- url: () => page.url(),
118
- goto: (url) => page.goto(absolute(url), { timeout }),
119
- // The page's OWN url, re-read at call time: a reload of the url the adapter was built with
120
- // would navigate away from wherever the test had got to.
121
- reload: () => page.goto(page.url(), { timeout }),
122
- title: async () => {
123
- const title = readField(await page.evaluate(TITLE), 'title');
124
- return typeof title === 'string' ? title : '';
125
- },
126
- gotoStreamed: async (url) => {
127
- const target = absolute(url);
128
- await page.goto(target, { timeout });
129
- const html = readField(await page.evaluate(firstFlushExpression(target)), 'html');
130
- return { html: typeof html === 'string' ? html : '' };
131
- },
132
- waitForServiceWorker: async () => {
133
- const raw = await page.evaluate(serviceWorkerExpression(swTimeout));
134
- if (readField(raw, 'controlled') !== true) {
135
- throw new E2eServiceWorkerAbsentError({ url: page.url(), timeoutMs: swTimeout });
136
- }
137
- },
138
- evaluate: <T>(fn: () => T): Promise<T> => evaluateClosure(page, fn) as unknown as Promise<T>,
139
- locator: (selector) => locate({ kind: 'css', selector, first: false }),
140
- getByRole: (role, roleOptions) =>
141
- locate({
142
- kind: 'role',
143
- role,
144
- first: false,
145
- ...(roleOptions?.name === undefined ? {} : { name: roleOptions.name }),
146
- ...(roleOptions?.level === undefined ? {} : { level: roleOptions.level }),
147
- }),
148
- getByText: (text) => locate({ kind: 'text', text, first: false }),
149
- };
150
- }
@@ -1,182 +0,0 @@
1
- // What an e2e locator SELECTS, as a value, plus the one in-page expression that resolves it.
2
- // `locator`/`getByRole`/`getByText` are lazy handles, so the selection has to survive as data
3
- // until something asks a question about it — and only then does it become a string the browser
4
- // can run.
5
-
6
- /** Where the driver parks its click target. Removed again as soon as the click has been made. */
7
- export const MARK_ATTRIBUTE = 'data-x-e2e';
8
-
9
- /** One selection, exactly as the test spelled it. `first` is `.first()`, applied at resolve time. */
10
- export type E2eSelection =
11
- | { readonly kind: 'css'; readonly selector: string; readonly first: boolean }
12
- | {
13
- readonly kind: 'role';
14
- readonly role: string;
15
- readonly name?: string | undefined;
16
- readonly level?: number | undefined;
17
- readonly first: boolean;
18
- }
19
- | { readonly kind: 'text'; readonly text: string; readonly first: boolean };
20
-
21
- /**
22
- * What the page answers for one selection. `count` is how many elements matched AFTER `first`
23
- * narrowed it, `visible` is about the first match alone, and `marked` says the click target now
24
- * carries `MARK_ATTRIBUTE` — three facts in one round trip, because a locator that asked twice
25
- * could be answered about two different renders.
26
- */
27
- export interface E2eResolution {
28
- readonly count: number;
29
- readonly visible: boolean;
30
- readonly marked: boolean;
31
- }
32
-
33
- /**
34
- * The elements that carry a role IMPLICITLY, so `getByRole('button')` finds a `<button>` that
35
- * never wrote the attribute. A `Map` and not an object literal: the key is a role a test typed,
36
- * and a computed read of a `Record` answers `Object.prototype` members — the defect
37
- * `bun run proto-index` exists for.
38
- *
39
- * Deliberately not the whole of WAI-ARIA. A role absent from this table still resolves through
40
- * its explicit `[role="…"]` attribute, which is why an unknown role is not refused: refusing
41
- * `role="feed"` because a table in the framework is short would be the framework deciding an app's
42
- * markup is wrong.
43
- */
44
- const IMPLICIT_ROLE_ELEMENTS = new Map<string, readonly string[]>([
45
- ['banner', ['header']],
46
- ['button', ['button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]']],
47
- ['checkbox', ['input[type="checkbox"]']],
48
- ['combobox', ['select']],
49
- ['contentinfo', ['footer']],
50
- ['dialog', ['dialog']],
51
- ['form', ['form']],
52
- ['heading', ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']],
53
- ['img', ['img']],
54
- ['link', ['a[href]']],
55
- ['list', ['ul', 'ol']],
56
- ['listitem', ['li']],
57
- ['main', ['main']],
58
- ['navigation', ['nav']],
59
- ['option', ['option']],
60
- ['radio', ['input[type="radio"]']],
61
- ['table', ['table']],
62
- ['textbox', ['input[type="text"]', 'input[type="email"]', 'input[type="search"]', 'textarea']],
63
- ]);
64
-
65
- /** Elements whose text is markup rather than page copy — `getByText` must never land on one. */
66
- const TEXT_SKIP_TAGS = ['SCRIPT', 'STYLE', 'HEAD', 'TITLE', 'META', 'LINK', 'NOSCRIPT'];
67
-
68
- /** A CSS attribute selector takes a double-quoted string, which is what `JSON.stringify` writes. */
69
- const roleSelector = (role: string): string =>
70
- [`[role=${JSON.stringify(role)}]`, ...(IMPLICIT_ROLE_ELEMENTS.get(role) ?? [])].join(',');
71
-
72
- /**
73
- * The call a test wrote, rebuilt from the selection. Every refusal below quotes it, because
74
- * "an e2e locator matched nothing" names no line of the test file and this does.
75
- */
76
- export function selectionCall(selection: E2eSelection): string {
77
- const tail = selection.first ? '.first()' : '';
78
- if (selection.kind === 'css') return `page.locator(${JSON.stringify(selection.selector)})${tail}`;
79
- if (selection.kind === 'text') return `page.getByText(${JSON.stringify(selection.text)})${tail}`;
80
- const options = [
81
- ...(selection.name === undefined ? [] : [`name: ${JSON.stringify(selection.name)}`]),
82
- ...(selection.level === undefined ? [] : [`level: ${String(selection.level)}`]),
83
- ];
84
- const role = JSON.stringify(selection.role);
85
- const suffix = options.length === 0 ? '' : `, { ${options.join(', ')} }`;
86
- return `page.getByRole(${role}${suffix})${tail}`;
87
- }
88
-
89
- /**
90
- * The candidate list, as a JS expression evaluating to an array of elements.
91
- *
92
- * `getByRole` and `getByText` are not CSS and cannot be: a role is implicit in a tag name, an
93
- * accessible name comes off four different attributes before it comes off the text, and
94
- * `getByText` has to pick the INNERMOST element that contains the string. So the union selector
95
- * is only the cheap first pass and every rule after it runs in JS, in the page.
96
- */
97
- function candidateSource(selection: E2eSelection): string {
98
- if (selection.kind === 'css') return `all(${JSON.stringify(selection.selector)})`;
99
- if (selection.kind === 'text') {
100
- const needle = JSON.stringify(selection.text.replace(/\s+/g, ' ').trim().toLowerCase());
101
- // Innermost FIRST and the skip list second, in that order. Reversed, `<html>` becomes the
102
- // innermost survivor of a page whose only copy of the string is inside a `<script>` — the
103
- // ancestor inherits the match its own excluded child made, which is worse than not filtering
104
- // at all: it reports one match, at the document root, for text no reader can see.
105
- return `innermost(all('*').filter((el) => norm(el.textContent).toLowerCase().indexOf(${needle}) !== -1)).filter((el) => ${JSON.stringify(TEXT_SKIP_TAGS)}.indexOf(el.tagName) === -1)`;
106
- }
107
- const byRole = `all(${JSON.stringify(roleSelector(selection.role))}).filter((el) => { const own = el.getAttribute('role'); return own === null || norm(own).toLowerCase() === ${JSON.stringify(selection.role.toLowerCase())}; })`;
108
- const byLevel =
109
- selection.level === undefined
110
- ? byRole
111
- : `${byRole}.filter((el) => level(el) === ${String(selection.level)})`;
112
- return selection.name === undefined
113
- ? byLevel
114
- : `${byLevel}.filter((el) => accName(el).toLowerCase() === ${JSON.stringify(selection.name.replace(/\s+/g, ' ').trim().toLowerCase())})`;
115
- }
116
-
117
- /**
118
- * The helpers the candidate source above calls, defined once inside the IIFE.
119
- *
120
- * `visible` is `display`/`visibility`/`opacity`, which is character for character what
121
- * `@ultimat3/scraping`'s `snapshotExpression` computes for `ElementSnapshot.visible`. That is a
122
- * COPY and it is the wrong shape: `ScrapeTarget.query()` already answers a snapshot per element
123
- * and `ScrapeFrame` does not expose it, so this driver cannot reach the framework's one definition
124
- * of visible without an edit to `packages/scraping/src/page.ts`. Mirrored deliberately rather than
125
- * invented, so the two cannot disagree about a page until that edit lands.
126
- */
127
- const HELPERS = `
128
- const norm = (s) => (s || '').replace(/\\s+/g, ' ').trim();
129
- const all = (sel) => Array.prototype.slice.call(document.querySelectorAll(sel));
130
- const innermost = (found) => found.filter((el) => !found.some((other) => other !== el && el.contains(other)));
131
- const level = (el) => {
132
- const aria = el.getAttribute('aria-level');
133
- if (aria !== null) return Number(aria);
134
- const tag = el.tagName.toLowerCase();
135
- return /^h[1-6]$/.test(tag) ? Number(tag.slice(1)) : undefined;
136
- };
137
- const accName = (el) => {
138
- const label = el.getAttribute('aria-label');
139
- if (label !== null && norm(label) !== '') return norm(label);
140
- const by = el.getAttribute('aria-labelledby');
141
- if (by !== null) {
142
- const parts = norm(by).split(' ').map((id) => document.getElementById(id)).filter((n) => n).map((n) => norm(n.textContent));
143
- if (norm(parts.join(' ')) !== '') return norm(parts.join(' '));
144
- }
145
- const tag = el.tagName.toLowerCase();
146
- if (tag === 'input') { const v = el.getAttribute('value'); if (v !== null && norm(v) !== '') return norm(v); }
147
- if (tag === 'img') { const a = el.getAttribute('alt'); if (a !== null) return norm(a); }
148
- const text = norm(el.textContent);
149
- if (text !== '') return text;
150
- const title = el.getAttribute('title');
151
- return title !== null ? norm(title) : '';
152
- };`;
153
-
154
- /**
155
- * Returns JSON TEXT rather than an object, the same bargain `cdp-snapshot.ts` makes: a CDP round
156
- * trip serialises the answer anyway, and a string has ONE deserialiser — a schema parse — instead
157
- * of an implicit one inside the browser library plus a cast on this side.
158
- */
159
- export function selectionExpression(selection: E2eSelection, mark?: string): string {
160
- const marker = mark === undefined ? 'null' : JSON.stringify(mark);
161
- return `(() => {${HELPERS}
162
- const found = ${candidateSource(selection)};
163
- const matches = ${selection.first ? 'found.slice(0, 1)' : 'found'};
164
- const el = matches[0];
165
- let visible = false;
166
- if (el) {
167
- const style = getComputedStyle(el);
168
- visible = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
169
- }
170
- const mark = ${marker};
171
- let marked = false;
172
- if (mark !== null && el) { el.setAttribute(${JSON.stringify(MARK_ATTRIBUTE)}, mark); marked = true; }
173
- return JSON.stringify({ count: matches.length, visible: visible, marked: marked });
174
- })()`;
175
- }
176
-
177
- /** The CSS the marked element answers to — what `ScrapePage.click` is handed once one is marked. */
178
- export const markSelector = (mark: string): string => `[${MARK_ATTRIBUTE}=${JSON.stringify(mark)}]`;
179
-
180
- /** Undo. Best effort by design: a click that navigated took the whole document with it. */
181
- export const unmarkExpression = (mark: string): string =>
182
- `(() => { const el = document.querySelector(${JSON.stringify(markSelector(mark))}); if (el) el.removeAttribute(${JSON.stringify(MARK_ATTRIBUTE)}); return JSON.stringify(true); })()`;
@@ -1,26 +0,0 @@
1
- // The actor a route is rendered AS when the render exists only to be weighed. `app/` pages are
2
- // authed by construction: a `load` that calls a policy-guarded query denies an anonymous actor with
3
- // `X_UNAUTHENTICATED`, so under the anonymous build context every authed page was reported
4
- // `X_BUDGET_UNMEASURED` and a real app could not pass the `budgets` step (measured 2026-09-05).
5
- // Weighing bytes needs no data authority — the rendered document is discarded — so this actor holds
6
- // every permission. It is handed ONLY to the weigh-and-discard branch of `prerender.ts`, never to
7
- // `renderStatic`: a `site/` artifact is published to everyone, and a guarded query inside its `load`
8
- // must keep failing the build rather than rendering another actor's rows into a file.
9
-
10
- import type { Actor } from '@ultimat3/core';
11
-
12
- /** The id every trace and log line under a measurement render carries, so it is recognisable. */
13
- export const MEASUREMENT_ACTOR_ID = 'x-build-measure';
14
-
15
- /**
16
- * `kind: 'service'` and not `'user'`: an app's own `requireMember()`-style helper that resolves a
17
- * user to a row has nothing to resolve here, and the honest kind says so. `'*'` is the grant
18
- * `actorHas` reads as everything — the same spelling a role map uses for a superuser.
19
- */
20
- export const measurementActor = (): Actor => ({
21
- kind: 'service',
22
- id: MEASUREMENT_ACTOR_ID,
23
- roles: [],
24
- scopes: [],
25
- permissions: ['*'],
26
- });
File without changes