@noego/wood 0.4.8 → 0.4.10

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 (149) hide show
  1. package/bin/axe.js +313 -98
  2. package/bin/wood.js +7 -1
  3. package/dist/automation/actions.cjs +17 -0
  4. package/dist/automation/actions.cjs.map +1 -0
  5. package/dist/automation/actions.d.cts +57 -0
  6. package/dist/automation/actions.d.ts +57 -0
  7. package/dist/automation/actions.js +1 -0
  8. package/dist/automation/actions.js.map +1 -0
  9. package/dist/automation/actions_executor.cjs +101 -0
  10. package/dist/automation/actions_executor.cjs.map +1 -0
  11. package/dist/automation/actions_executor.d.cts +16 -0
  12. package/dist/automation/actions_executor.d.ts +16 -0
  13. package/dist/automation/actions_executor.js +76 -0
  14. package/dist/automation/actions_executor.js.map +1 -0
  15. package/dist/automation/connection.cjs +86 -0
  16. package/dist/automation/connection.cjs.map +1 -0
  17. package/dist/automation/connection.d.cts +18 -0
  18. package/dist/automation/connection.d.ts +18 -0
  19. package/dist/automation/connection.js +51 -0
  20. package/dist/automation/connection.js.map +1 -0
  21. package/dist/automation/errors.cjs +50 -0
  22. package/dist/automation/errors.cjs.map +1 -0
  23. package/dist/automation/errors.d.cts +31 -0
  24. package/dist/automation/errors.d.ts +31 -0
  25. package/dist/automation/errors.js +24 -0
  26. package/dist/automation/errors.js.map +1 -0
  27. package/dist/automation/index.cjs +49 -0
  28. package/dist/automation/index.cjs.map +1 -0
  29. package/dist/automation/index.d.cts +16 -0
  30. package/dist/automation/index.d.ts +16 -0
  31. package/dist/automation/index.js +15 -0
  32. package/dist/automation/index.js.map +1 -0
  33. package/dist/automation/introspection_reader.cjs +75 -0
  34. package/dist/automation/introspection_reader.cjs.map +1 -0
  35. package/dist/automation/introspection_reader.d.cts +20 -0
  36. package/dist/automation/introspection_reader.d.ts +20 -0
  37. package/dist/automation/introspection_reader.js +48 -0
  38. package/dist/automation/introspection_reader.js.map +1 -0
  39. package/dist/automation/locator.cjs +102 -0
  40. package/dist/automation/locator.cjs.map +1 -0
  41. package/dist/automation/locator.d.cts +51 -0
  42. package/dist/automation/locator.d.ts +51 -0
  43. package/dist/automation/locator.js +75 -0
  44. package/dist/automation/locator.js.map +1 -0
  45. package/dist/automation/observation.cjs +17 -0
  46. package/dist/automation/observation.cjs.map +1 -0
  47. package/dist/automation/observation.d.cts +44 -0
  48. package/dist/automation/observation.d.ts +44 -0
  49. package/dist/automation/observation.js +1 -0
  50. package/dist/automation/observation.js.map +1 -0
  51. package/dist/automation/receipts.cjs +17 -0
  52. package/dist/automation/receipts.cjs.map +1 -0
  53. package/dist/automation/receipts.d.cts +27 -0
  54. package/dist/automation/receipts.d.ts +27 -0
  55. package/dist/automation/receipts.js +1 -0
  56. package/dist/automation/receipts.js.map +1 -0
  57. package/dist/automation/scenario.cjs +696 -0
  58. package/dist/automation/scenario.cjs.map +1 -0
  59. package/dist/automation/scenario.d.cts +89 -0
  60. package/dist/automation/scenario.d.ts +89 -0
  61. package/dist/automation/scenario.js +660 -0
  62. package/dist/automation/scenario.js.map +1 -0
  63. package/dist/automation/scrolling.cjs +17 -0
  64. package/dist/automation/scrolling.cjs.map +1 -0
  65. package/dist/automation/scrolling.d.cts +51 -0
  66. package/dist/automation/scrolling.d.ts +51 -0
  67. package/dist/automation/scrolling.js +1 -0
  68. package/dist/automation/scrolling.js.map +1 -0
  69. package/dist/automation/scrolling_executor.cjs +109 -0
  70. package/dist/automation/scrolling_executor.cjs.map +1 -0
  71. package/dist/automation/scrolling_executor.d.cts +13 -0
  72. package/dist/automation/scrolling_executor.d.ts +13 -0
  73. package/dist/automation/scrolling_executor.js +84 -0
  74. package/dist/automation/scrolling_executor.js.map +1 -0
  75. package/dist/automation/target_identity.cjs +17 -0
  76. package/dist/automation/target_identity.cjs.map +1 -0
  77. package/dist/automation/target_identity.d.cts +28 -0
  78. package/dist/automation/target_identity.d.ts +28 -0
  79. package/dist/automation/target_identity.js +1 -0
  80. package/dist/automation/target_identity.js.map +1 -0
  81. package/dist/automation/target_registry.cjs +148 -0
  82. package/dist/automation/target_registry.cjs.map +1 -0
  83. package/dist/automation/target_registry.d.cts +32 -0
  84. package/dist/automation/target_registry.d.ts +32 -0
  85. package/dist/automation/target_registry.js +122 -0
  86. package/dist/automation/target_registry.js.map +1 -0
  87. package/dist/automation/waits.cjs +17 -0
  88. package/dist/automation/waits.cjs.map +1 -0
  89. package/dist/automation/waits.d.cts +44 -0
  90. package/dist/automation/waits.d.ts +44 -0
  91. package/dist/automation/waits.js +1 -0
  92. package/dist/automation/waits.js.map +1 -0
  93. package/dist/automation/waits_executor.cjs +104 -0
  94. package/dist/automation/waits_executor.cjs.map +1 -0
  95. package/dist/automation/waits_executor.d.cts +14 -0
  96. package/dist/automation/waits_executor.d.ts +14 -0
  97. package/dist/automation/waits_executor.js +79 -0
  98. package/dist/automation/waits_executor.js.map +1 -0
  99. package/dist/client/index.d.cts +1 -0
  100. package/dist/client/index.d.ts +1 -0
  101. package/dist/codegen/renderer_app_generator.cjs +16 -1
  102. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  103. package/dist/codegen/renderer_app_generator.js +16 -1
  104. package/dist/codegen/renderer_app_generator.js.map +1 -1
  105. package/dist/index.d.cts +1 -0
  106. package/dist/index.d.ts +1 -0
  107. package/dist/navigation/automation_introspection.cjs +110 -0
  108. package/dist/navigation/automation_introspection.cjs.map +1 -0
  109. package/dist/navigation/automation_introspection.d.cts +60 -0
  110. package/dist/navigation/automation_introspection.d.ts +60 -0
  111. package/dist/navigation/automation_introspection.js +85 -0
  112. package/dist/navigation/automation_introspection.js.map +1 -0
  113. package/dist/navigation/index.cjs +7 -2
  114. package/dist/navigation/index.cjs.map +1 -1
  115. package/dist/navigation/index.d.cts +2 -0
  116. package/dist/navigation/index.d.ts +2 -0
  117. package/dist/navigation/index.js +4 -1
  118. package/dist/navigation/index.js.map +1 -1
  119. package/dist/testing/automation_assertions.cjs +417 -0
  120. package/dist/testing/automation_assertions.cjs.map +1 -0
  121. package/dist/testing/automation_assertions.d.cts +44 -0
  122. package/dist/testing/automation_assertions.d.ts +44 -0
  123. package/dist/testing/automation_assertions.js +385 -0
  124. package/dist/testing/automation_assertions.js.map +1 -0
  125. package/dist/testing/browser.cjs +26 -34
  126. package/dist/testing/browser.cjs.map +1 -1
  127. package/dist/testing/browser.d.cts +2 -1
  128. package/dist/testing/browser.d.ts +2 -1
  129. package/dist/testing/browser.js +30 -31
  130. package/dist/testing/browser.js.map +1 -1
  131. package/dist/testing/create_controller_harness.d.cts +1 -0
  132. package/dist/testing/create_controller_harness.d.ts +1 -0
  133. package/dist/testing/geometry.cjs +109 -0
  134. package/dist/testing/geometry.cjs.map +1 -0
  135. package/dist/testing/geometry.d.cts +45 -0
  136. package/dist/testing/geometry.d.ts +45 -0
  137. package/dist/testing/geometry.js +79 -0
  138. package/dist/testing/geometry.js.map +1 -0
  139. package/dist/testing/index.cjs +31 -2
  140. package/dist/testing/index.cjs.map +1 -1
  141. package/dist/testing/index.d.cts +4 -0
  142. package/dist/testing/index.d.ts +4 -0
  143. package/dist/testing/index.js +31 -1
  144. package/dist/testing/index.js.map +1 -1
  145. package/dist/tracing/trace_hub.cjs +3 -0
  146. package/dist/tracing/trace_hub.cjs.map +1 -1
  147. package/dist/tracing/trace_hub.js +3 -0
  148. package/dist/tracing/trace_hub.js.map +1 -1
  149. package/package.json +7 -1
package/bin/axe.js CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Thin CLI adapter over @noego/wood/automation (AXE-WP-01).
4
+ // Argument parsing and presentation only — connection, target resolution,
5
+ // locator handling, and error classification live in typed source.
6
+
7
+ import { readFile } from 'node:fs/promises';
3
8
  import path from 'node:path';
4
9
  import process from 'node:process';
5
- import { readFileSync } from 'node:fs';
6
10
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
11
 
8
12
  const __filename = fileURLToPath(import.meta.url);
@@ -10,53 +14,95 @@ const __dirname = path.dirname(__filename);
10
14
  const distDir = path.resolve(__dirname, '..', 'dist');
11
15
 
12
16
  // Node's ESM loader requires file:// URLs for absolute paths on Windows;
13
- // raw `C:\...` strings throw ERR_UNSUPPORTED_ESM_URL_SCHEME.
14
- const { readWoodDevMetadata } = await import(pathToFileURL(path.join(distDir, 'dev', 'dev_metadata.js')).href);
15
-
16
- const INTERNAL_BRIDGE_KEY = '__NOEGO_WOOD_BRIDGE__';
17
+ // raw `C:\\...` strings throw ERR_UNSUPPORTED_ESM_URL_SCHEME.
18
+ const automation = await import(pathToFileURL(path.join(distDir, 'automation', 'index.js')).href);
17
19
 
18
20
  function printUsage() {
19
21
  console.log('Usage: axe <command> [options]');
20
22
  console.log('');
21
23
  console.log('Commands:');
22
- console.log(' targets [--window <windowId>] List attachable Wood windows');
23
- console.log(' js [--window <windowId>] Run JavaScript from stdin in the selected renderer');
24
- console.log(' click <selector> [--window <id>] Click an element');
25
- console.log(' type <selector> <text> [--window <id>] Fill an element');
26
- console.log(' screenshot [path] [--window <id>] Save a screenshot');
24
+ console.log(' targets List attachable Wood windows');
25
+ console.log(' run <scenario.json> | run --stdin Run a persistent version-1 scenario');
26
+ console.log(' observe [--window <id>] Print a JSON target/route observation');
27
+ console.log(' route [--window <id>] Print the current route or null');
28
+ console.log(' wait --visible <locator> [--timeout <ms>] Wait for an element to be visible');
29
+ console.log(' wait --hidden <locator> [--timeout <ms>] Wait for an element to be hidden');
30
+ console.log(' wait --route <page> [--timeout <ms>] Wait for the current route');
31
+ console.log(' wait --url <value> [--timeout <ms>] Wait for an exact URL');
32
+ console.log(' scroll [--viewport|--container <locator>] --by <dx,dy>');
33
+ console.log(' scroll [--viewport|--container <locator>] --to <top|bottom|left|right>');
34
+ console.log(' scroll [--viewport|--container <locator>] --until <locator> [--step-px <n>] [--max-steps <n>]');
35
+ console.log(' scroll --into-view <locator> Scroll an element into view');
36
+ console.log(' click <locator> [--then-visible <locator>] [--then-route <page>]');
37
+ console.log(' type <locator> <text> [--then-visible <locator>] [--then-route <page>]');
38
+ console.log(' fill <locator> --value <text> Fill an element (Playwright fill semantics)');
39
+ console.log(' press --keys <k1,k2> Press one or more key chords');
40
+ console.log(' debug js [--window <windowId>] Run JavaScript from stdin (explicit UI bypass; alias: js)');
41
+ console.log(' screenshot [path] [--window <id>] Save a screenshot');
42
+ console.log('');
43
+ console.log('Global options: --window <windowId> --json');
44
+ console.log('');
45
+ console.log('Locators: bare strings are CSS; prefixes select semantic locators:');
46
+ console.log(' test_id=<id> role=<role>[:<name>] label=<text> placeholder=<text> text=<text> css=<selector>');
47
+ console.log('');
48
+ console.log('When multiple Wood windows are open, --window <windowId> is required.');
27
49
  }
28
50
 
51
+ const VALUE_FLAGS = new Set([
52
+ '--window',
53
+ '--visible',
54
+ '--hidden',
55
+ '--route',
56
+ '--url',
57
+ '--timeout',
58
+ '--container',
59
+ '--by',
60
+ '--to',
61
+ '--until',
62
+ '--step-px',
63
+ '--max-steps',
64
+ '--into-view',
65
+ '--then-visible',
66
+ '--then-route',
67
+ '--keys',
68
+ '--value',
69
+ ]);
70
+
29
71
  function parseArgs(argv) {
30
72
  const positional = [];
73
+ const flags = new Map();
31
74
  let windowId = null;
75
+ let json = false;
32
76
 
33
77
  for (let i = 0; i < argv.length; i += 1) {
34
78
  const arg = argv[i];
35
- if (arg === '--window') {
36
- windowId = argv[i + 1] ?? null;
37
- i += 1;
79
+ if (arg === '--json') {
80
+ json = true;
81
+ continue;
82
+ }
83
+ if (arg === '--stdin' || arg === '--viewport') {
84
+ flags.set(arg, true);
85
+ continue;
86
+ }
87
+ if (VALUE_FLAGS.has(arg)) {
88
+ const value = argv[i + 1];
89
+ if (value == null) {
90
+ flags.set(arg, null);
91
+ } else {
92
+ flags.set(arg, value);
93
+ i += 1;
94
+ }
95
+ if (arg === '--window') windowId = value ?? null;
38
96
  continue;
39
97
  }
40
98
  positional.push(arg);
41
99
  }
42
100
 
43
- return { positional, windowId };
44
- }
45
-
46
- async function loadPlaywright() {
47
- try {
48
- return await import('playwright');
49
- } catch (error) {
50
- const message = error instanceof Error ? error.message : String(error);
51
- throw new Error(`axe requires Playwright to be installed in this environment. ${message}`);
52
- }
101
+ return { positional, flags, windowId, json };
53
102
  }
54
103
 
55
104
  async function readStdin() {
56
- if (process.stdin.isTTY) {
57
- return '';
58
- }
59
-
105
+ if (process.stdin.isTTY) return '';
60
106
  const chunks = [];
61
107
  for await (const chunk of process.stdin) {
62
108
  chunks.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
@@ -64,63 +110,163 @@ async function readStdin() {
64
110
  return chunks.join('');
65
111
  }
66
112
 
67
- async function connectBrowser(browserUrl) {
68
- const playwright = await loadPlaywright();
69
- return playwright.chromium.connectOverCDP(browserUrl);
70
- }
71
-
72
- async function listTargets(browser) {
73
- const results = [];
74
- for (const context of browser.contexts()) {
75
- for (const page of context.pages()) {
76
- let currentWindow = null;
77
- try {
78
- currentWindow = await page.evaluate(async (bridgeKey) => {
79
- const bridge = globalThis[bridgeKey];
80
- if (!bridge || typeof bridge !== 'object') {
81
- return null;
82
- }
83
- const windowApi = bridge.__window;
84
- if (!windowApi || typeof windowApi.current !== 'function') {
85
- return null;
86
- }
87
- return await windowApi.current();
88
- }, INTERNAL_BRIDGE_KEY);
89
- } catch {
90
- currentWindow = null;
91
- }
113
+ function fail(error) {
114
+ console.error(`[axe] ${automation.describeAutomationError(error)}`);
115
+ process.exit(1);
116
+ }
92
117
 
93
- results.push({
94
- page,
95
- windowId: currentWindow?.ok ? currentWindow.windowId ?? null : null,
96
- defaultRoute: currentWindow?.ok ? currentWindow.defaultRoute ?? null : null,
97
- title: await page.title().catch(() => ''),
98
- url: page.url(),
99
- });
100
- }
118
+ function unwrap(result) {
119
+ if (!result.ok) fail(result.error);
120
+ return result.value;
121
+ }
122
+
123
+ function flagValue(flags, name) {
124
+ const value = flags.get(name);
125
+ if (typeof value !== 'string') {
126
+ fail({ code: 'ACTION_FAILED', message: `${name} requires a value.` });
127
+ }
128
+ return value;
129
+ }
130
+
131
+ function optionalPositiveNumber(flags, name, fallback) {
132
+ if (!flags.has(name)) return fallback;
133
+ const raw = flagValue(flags, name);
134
+ const value = Number(raw);
135
+ if (!Number.isFinite(value) || value <= 0) {
136
+ fail({ code: 'ACTION_FAILED', message: `${name} must be a positive number.` });
137
+ }
138
+ return value;
139
+ }
140
+
141
+ function locator(raw) {
142
+ return unwrap(automation.parseCliLocator(raw));
143
+ }
144
+
145
+ async function resolveTargetPage(browser, windowId) {
146
+ const entries = await automation.discoverTargets(browser);
147
+ return unwrap(automation.selectTarget(entries, windowId));
148
+ }
149
+
150
+ function printReceipt(receipt, json, human) {
151
+ process.stdout.write(json ? `${JSON.stringify(receipt)}\n` : `${human}\n`);
152
+ }
153
+
154
+ function postconditions(flags) {
155
+ const conditions = [];
156
+ if (flags.has('--then-route')) {
157
+ conditions.push({ kind: 'route', page: flagValue(flags, '--then-route') });
158
+ }
159
+ if (flags.has('--then-visible')) {
160
+ conditions.push({ kind: 'visible', target: locator(flagValue(flags, '--then-visible')) });
161
+ }
162
+ return conditions;
163
+ }
164
+
165
+ async function executeWithPostconditions(page, action, flags) {
166
+ const conditions = postconditions(flags);
167
+ const result = await automation.executeAction(page, action, {
168
+ ...(conditions[0] ? { wait: conditions[0] } : {}),
169
+ });
170
+ const receipt = unwrap(result);
171
+ for (const condition of conditions.slice(1)) {
172
+ unwrap(await automation.awaitCondition(page, condition, { timeoutMs: 5_000 }));
173
+ }
174
+ return receipt;
175
+ }
176
+
177
+ function scrollContainer(flags) {
178
+ if (flags.has('--container')) {
179
+ return { kind: 'locator', target: locator(flagValue(flags, '--container')) };
101
180
  }
102
- return results;
181
+ return { kind: 'viewport' };
103
182
  }
104
183
 
105
- async function resolveTarget(browser, requestedWindowId) {
106
- const targets = await listTargets(browser);
107
- if (targets.length === 0) {
108
- throw new Error('No renderer targets found in the running Wood app.');
184
+ function scrollCommand(flags) {
185
+ if (flags.has('--into-view')) {
186
+ return { mode: 'into_view', target: locator(flagValue(flags, '--into-view')) };
109
187
  }
110
188
 
111
- if (requestedWindowId) {
112
- const match = targets.find((target) => target.windowId === requestedWindowId);
113
- if (!match) {
114
- throw new Error(`No Wood window target found for "${requestedWindowId}".`);
189
+ const container = scrollContainer(flags);
190
+ if (flags.has('--by')) {
191
+ const raw = flagValue(flags, '--by');
192
+ const parts = raw.split(',');
193
+ const deltaX = Number(parts[0]);
194
+ const deltaY = Number(parts[1]);
195
+ if (parts.length !== 2 || !Number.isFinite(deltaX) || !Number.isFinite(deltaY)) {
196
+ fail({ code: 'ACTION_FAILED', message: '--by must be two finite numbers formatted as dx,dy.' });
197
+ }
198
+ return { mode: 'wheel', container, deltaX, deltaY };
199
+ }
200
+ if (flags.has('--to')) {
201
+ const edge = flagValue(flags, '--to');
202
+ if (!['top', 'bottom', 'left', 'right'].includes(edge)) {
203
+ fail({ code: 'ACTION_FAILED', message: '--to must be top, bottom, left, or right.' });
115
204
  }
116
- return match;
205
+ return { mode: 'position', container, edge };
206
+ }
207
+ if (flags.has('--until')) {
208
+ return {
209
+ mode: 'until',
210
+ container,
211
+ target: locator(flagValue(flags, '--until')),
212
+ direction: 'down',
213
+ stepPx: optionalPositiveNumber(flags, '--step-px', 400),
214
+ maxSteps: optionalPositiveNumber(flags, '--max-steps', 20),
215
+ };
117
216
  }
217
+ fail({ code: 'ACTION_FAILED', message: 'axe scroll requires exactly one of --by, --to, --until, or --into-view.' });
218
+ }
118
219
 
119
- return targets[0];
220
+ function waitCondition(flags) {
221
+ const choices = ['--visible', '--hidden', '--route', '--url'].filter((name) => flags.has(name));
222
+ if (choices.length !== 1) {
223
+ fail({ code: 'ACTION_FAILED', message: 'axe wait requires exactly one of --visible, --hidden, --route, or --url.' });
224
+ }
225
+ const choice = choices[0];
226
+ const value = flagValue(flags, choice);
227
+ if (choice === '--visible' || choice === '--hidden') {
228
+ return { kind: choice.slice(2), target: locator(value) };
229
+ }
230
+ if (choice === '--route') return { kind: 'route', page: value };
231
+ return { kind: 'url', value };
232
+ }
233
+
234
+ async function runScenarioCommand(connection, positional, flags) {
235
+ const fromStdin = flags.has('--stdin');
236
+ const fileName = positional[1];
237
+ if (fromStdin === Boolean(fileName)) {
238
+ fail({ code: 'ACTION_FAILED', message: 'axe run requires exactly one scenario file or --stdin.' });
239
+ }
240
+ let source;
241
+ try {
242
+ source = fromStdin ? await readStdin() : await readFile(path.resolve(process.cwd(), fileName), 'utf8');
243
+ } catch (error) {
244
+ fail({
245
+ code: 'ACTION_FAILED',
246
+ message: 'Could not read scenario JSON.',
247
+ details: { cause: error instanceof Error ? error.message : String(error) },
248
+ });
249
+ }
250
+ let json;
251
+ try {
252
+ json = JSON.parse(source);
253
+ } catch (error) {
254
+ fail({
255
+ code: 'ACTION_FAILED',
256
+ message: 'Scenario input is not valid JSON.',
257
+ details: { cause: error instanceof Error ? error.message : String(error) },
258
+ });
259
+ }
260
+ const scenario = unwrap(automation.parseScenario(json));
261
+ const result = await automation.runScenario(connection, scenario, {
262
+ onEvent: (event) => process.stdout.write(`${JSON.stringify(event)}\n`),
263
+ evidenceDir: path.resolve(process.cwd(), 'axe-evidence'),
264
+ });
265
+ if (!result.ok) fail(result.error);
120
266
  }
121
267
 
122
268
  async function main() {
123
- const { positional, windowId } = parseArgs(process.argv.slice(2));
269
+ const { positional, flags, windowId, json } = parseArgs(process.argv.slice(2));
124
270
  const command = positional[0];
125
271
 
126
272
  if (!command) {
@@ -128,57 +274,126 @@ async function main() {
128
274
  process.exit(0);
129
275
  }
130
276
 
131
- const metadata = readWoodDevMetadata(process.cwd());
132
- const browser = await connectBrowser(metadata.browserUrl);
277
+ const connection = unwrap(await automation.connectToWoodApp(process.cwd()));
133
278
 
134
279
  try {
135
280
  if (command === 'targets') {
136
- const targets = await listTargets(browser);
137
- const lines = targets.map((target, index) => JSON.stringify({
281
+ const entries = await automation.discoverTargets(connection.browser);
282
+ const lines = entries.map((entry, index) => JSON.stringify({
283
+ // index is diagnostic only — windowId/targetId are the identity contract.
138
284
  index,
139
- windowId: target.windowId,
140
- defaultRoute: target.defaultRoute,
141
- title: target.title,
142
- url: target.url,
285
+ targetId: entry.target.targetId,
286
+ windowId: entry.target.windowId,
287
+ defaultRoute: entry.target.configuredDefaultRoute ?? null,
288
+ eligible: entry.target.eligible,
289
+ title: entry.target.title,
290
+ url: entry.target.url,
143
291
  }));
144
292
  process.stdout.write(`${lines.join('\n')}\n`);
145
293
  return;
146
294
  }
147
295
 
148
- const target = await resolveTarget(browser, windowId);
149
- const page = target.page;
296
+ if (command === 'run') {
297
+ await runScenarioCommand(connection, positional, flags);
298
+ return;
299
+ }
300
+
301
+ const { page, target } = await resolveTargetPage(connection.browser, windowId);
302
+
303
+ if (command === 'observe') {
304
+ process.stdout.write(`${JSON.stringify({ target, route: target.currentRoute ?? null })}\n`);
305
+ return;
306
+ }
307
+
308
+ if (command === 'route') {
309
+ process.stdout.write(`${target.currentRoute ?? 'null'}\n`);
310
+ return;
311
+ }
312
+
313
+ if (command === 'wait') {
314
+ const condition = waitCondition(flags);
315
+ const timeoutMs = optionalPositiveNumber(flags, '--timeout', 5_000);
316
+ const waited = unwrap(await automation.awaitCondition(page, condition, { timeoutMs }));
317
+ process.stdout.write(json ? `${JSON.stringify(waited)}\n` : `condition satisfied after ${waited.waitedMs}ms\n`);
318
+ return;
319
+ }
320
+
321
+ if (command === 'scroll') {
322
+ const receipt = unwrap(await automation.executeAction(page, { kind: 'scroll', command: scrollCommand(flags) }));
323
+ printReceipt(receipt, json, 'scrolled');
324
+ return;
325
+ }
150
326
 
151
- if (command === 'js') {
327
+ if (command === 'js' || (command === 'debug' && positional[1] === 'js')) {
152
328
  const source = await readStdin();
153
329
  if (!source.trim()) {
154
- throw new Error('axe js expects JavaScript via stdin.');
330
+ fail({ code: 'ACTION_FAILED', message: 'axe debug js expects JavaScript via stdin.' });
155
331
  }
332
+ // Explicit diagnostic escape hatch: bypasses the real UI path (bypassedUi: true)
333
+ // and is excluded from release qualification scenarios.
334
+ process.stderr.write('[axe] debug js is a UI bypass (bypassedUi: true); not valid release-qualification evidence.\n');
156
335
  const result = await page.evaluate(async (userSource) => {
157
336
  const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
158
- return await new AsyncFunction(userSource)();
337
+ // Bare expressions ("2+2", "document.title") are not valid as a
338
+ // function *body* result — wrap them in an implicit return first;
339
+ // fall back to body form for statement programs ("const x = ...; return x").
340
+ let fn;
341
+ try {
342
+ fn = new AsyncFunction(`return (${userSource}\n);`);
343
+ } catch {
344
+ fn = new AsyncFunction(userSource);
345
+ }
346
+ return await fn();
159
347
  }, source);
160
348
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
161
349
  return;
162
350
  }
163
351
 
164
352
  if (command === 'click') {
165
- const selector = positional[1];
166
- if (!selector) {
167
- throw new Error('axe click requires a selector.');
168
- }
169
- await page.locator(selector).click();
170
- process.stdout.write(`clicked ${selector}\n`);
353
+ const rawLocator = positional[1];
354
+ if (!rawLocator) fail({ code: 'LOCATOR_INVALID', message: 'axe click requires a locator.' });
355
+ const targetLocator = locator(rawLocator);
356
+ const receipt = await executeWithPostconditions(page, { kind: 'click', target: targetLocator }, flags);
357
+ printReceipt(receipt, json, `clicked ${automation.describeLocator(targetLocator)}`);
171
358
  return;
172
359
  }
173
360
 
174
361
  if (command === 'type') {
175
- const selector = positional[1];
362
+ const rawLocator = positional[1];
176
363
  const text = positional[2];
177
- if (!selector || text == null) {
178
- throw new Error('axe type requires a selector and text.');
364
+ if (!rawLocator || text == null) {
365
+ fail({ code: 'LOCATOR_INVALID', message: 'axe type requires a locator and text.' });
366
+ }
367
+ const targetLocator = locator(rawLocator);
368
+ const receipt = await executeWithPostconditions(
369
+ page,
370
+ { kind: 'type', target: targetLocator, value: text },
371
+ flags,
372
+ );
373
+ printReceipt(receipt, json, `typed into ${automation.describeLocator(targetLocator)}`);
374
+ return;
375
+ }
376
+
377
+ if (command === 'fill') {
378
+ const rawLocator = positional[1];
379
+ if (!rawLocator || !flags.has('--value')) {
380
+ fail({ code: 'LOCATOR_INVALID', message: 'axe fill requires a locator and --value <text>.' });
179
381
  }
180
- await page.locator(selector).fill(text);
181
- process.stdout.write(`typed into ${selector}\n`);
382
+ const targetLocator = locator(rawLocator);
383
+ const receipt = unwrap(await automation.executeAction(page, {
384
+ kind: 'fill',
385
+ target: targetLocator,
386
+ value: flagValue(flags, '--value'),
387
+ }));
388
+ printReceipt(receipt, json, `filled ${automation.describeLocator(targetLocator)}`);
389
+ return;
390
+ }
391
+
392
+ if (command === 'press') {
393
+ const keys = flagValue(flags, '--keys').split(',').map((key) => key.trim()).filter(Boolean);
394
+ if (keys.length === 0) fail({ code: 'ACTION_FAILED', message: '--keys must contain at least one key.' });
395
+ const receipt = unwrap(await automation.executeAction(page, { kind: 'press', keys }));
396
+ printReceipt(receipt, json, `pressed ${keys.join(', ')}`);
182
397
  return;
183
398
  }
184
399
 
@@ -191,9 +406,9 @@ async function main() {
191
406
  return;
192
407
  }
193
408
 
194
- throw new Error(`Unknown axe command "${command}".`);
409
+ fail({ code: 'UNSUPPORTED', message: `Unknown axe command "${command}".` });
195
410
  } finally {
196
- await browser.close();
411
+ await connection.close();
197
412
  }
198
413
  }
199
414
 
package/bin/wood.js CHANGED
@@ -40,7 +40,7 @@ const {
40
40
  const command = process.argv[2];
41
41
  const extraArgs = process.argv.slice(3);
42
42
 
43
- const COMMANDS = ['dev', 'build'];
43
+ const COMMANDS = ['dev', 'build', 'codegen'];
44
44
 
45
45
  if (!command || !COMMANDS.includes(command)) {
46
46
  console.log('Usage: wood <command>');
@@ -48,6 +48,7 @@ if (!command || !COMMANDS.includes(command)) {
48
48
  console.log('Commands:');
49
49
  console.log(' dev Generate code and start electron-vite in development mode');
50
50
  console.log(' build Generate code and run electron-vite production build');
51
+ console.log(' codegen Generate code only, then exit (no electron-vite)');
51
52
  console.log('');
52
53
  console.log('Options are passed through to electron-vite.');
53
54
  process.exit(command ? 1 : 0);
@@ -448,6 +449,11 @@ try {
448
449
  process.exit(1);
449
450
  }
450
451
 
452
+ if (command === 'codegen') {
453
+ console.log('[wood] Codegen complete');
454
+ process.exit(0);
455
+ }
456
+
451
457
  // ---------------------------------------------------------------------------
452
458
  // 2. Optional dev watcher (YAML + controllers)
453
459
  // ---------------------------------------------------------------------------
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+ var actions_exports = {};
16
+ module.exports = __toCommonJS(actions_exports);
17
+ //# sourceMappingURL=actions.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/automation/actions.ts"],"sourcesContent":["/**\n * Canonical action model (frozen — Axe.md \"Canonical action model\").\n * Type surface only in WP-00/01; the executor lands in AXE-WP-03.\n *\n * `fill` and `type` are distinct: `fill` sets the field value through\n * Playwright's fill semantics; `type` emits keyboard input.\n */\n\nimport type { WoodLocator } from './locator.cjs';\nimport type { WoodScrollCommand } from './scrolling.cjs';\nimport type { WoodWaitCondition } from './waits.cjs';\n\nexport type WoodAutomationAction =\n | { kind: 'click'; target: WoodLocator; button?: 'left' | 'middle' }\n | { kind: 'double_click'; target: WoodLocator }\n | { kind: 'fill'; target: WoodLocator; value: string }\n | { kind: 'type'; target: WoodLocator; value: string; delayMs?: number }\n | { kind: 'press'; target?: WoodLocator; keys: string[] }\n | { kind: 'select'; target: WoodLocator; option: string }\n | { kind: 'toggle'; target: WoodLocator; checked: boolean }\n | { kind: 'scroll'; command: WoodScrollCommand }\n | { kind: 'back' }\n | { kind: 'forward' }\n | { kind: 'reload' }\n | { kind: 'wait'; condition: WoodWaitCondition };\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -0,0 +1,57 @@
1
+ import { WoodLocator } from './locator.cjs';
2
+ import { WoodScrollCommand } from './scrolling.cjs';
3
+ import { WoodWaitCondition } from './waits.cjs';
4
+ import 'playwright';
5
+ import './errors.cjs';
6
+
7
+ /**
8
+ * Canonical action model (frozen — Axe.md "Canonical action model").
9
+ * Type surface only in WP-00/01; the executor lands in AXE-WP-03.
10
+ *
11
+ * `fill` and `type` are distinct: `fill` sets the field value through
12
+ * Playwright's fill semantics; `type` emits keyboard input.
13
+ */
14
+
15
+ type WoodAutomationAction = {
16
+ kind: 'click';
17
+ target: WoodLocator;
18
+ button?: 'left' | 'middle';
19
+ } | {
20
+ kind: 'double_click';
21
+ target: WoodLocator;
22
+ } | {
23
+ kind: 'fill';
24
+ target: WoodLocator;
25
+ value: string;
26
+ } | {
27
+ kind: 'type';
28
+ target: WoodLocator;
29
+ value: string;
30
+ delayMs?: number;
31
+ } | {
32
+ kind: 'press';
33
+ target?: WoodLocator;
34
+ keys: string[];
35
+ } | {
36
+ kind: 'select';
37
+ target: WoodLocator;
38
+ option: string;
39
+ } | {
40
+ kind: 'toggle';
41
+ target: WoodLocator;
42
+ checked: boolean;
43
+ } | {
44
+ kind: 'scroll';
45
+ command: WoodScrollCommand;
46
+ } | {
47
+ kind: 'back';
48
+ } | {
49
+ kind: 'forward';
50
+ } | {
51
+ kind: 'reload';
52
+ } | {
53
+ kind: 'wait';
54
+ condition: WoodWaitCondition;
55
+ };
56
+
57
+ export type { WoodAutomationAction };
@@ -0,0 +1,57 @@
1
+ import { WoodLocator } from './locator.js';
2
+ import { WoodScrollCommand } from './scrolling.js';
3
+ import { WoodWaitCondition } from './waits.js';
4
+ import 'playwright';
5
+ import './errors.js';
6
+
7
+ /**
8
+ * Canonical action model (frozen — Axe.md "Canonical action model").
9
+ * Type surface only in WP-00/01; the executor lands in AXE-WP-03.
10
+ *
11
+ * `fill` and `type` are distinct: `fill` sets the field value through
12
+ * Playwright's fill semantics; `type` emits keyboard input.
13
+ */
14
+
15
+ type WoodAutomationAction = {
16
+ kind: 'click';
17
+ target: WoodLocator;
18
+ button?: 'left' | 'middle';
19
+ } | {
20
+ kind: 'double_click';
21
+ target: WoodLocator;
22
+ } | {
23
+ kind: 'fill';
24
+ target: WoodLocator;
25
+ value: string;
26
+ } | {
27
+ kind: 'type';
28
+ target: WoodLocator;
29
+ value: string;
30
+ delayMs?: number;
31
+ } | {
32
+ kind: 'press';
33
+ target?: WoodLocator;
34
+ keys: string[];
35
+ } | {
36
+ kind: 'select';
37
+ target: WoodLocator;
38
+ option: string;
39
+ } | {
40
+ kind: 'toggle';
41
+ target: WoodLocator;
42
+ checked: boolean;
43
+ } | {
44
+ kind: 'scroll';
45
+ command: WoodScrollCommand;
46
+ } | {
47
+ kind: 'back';
48
+ } | {
49
+ kind: 'forward';
50
+ } | {
51
+ kind: 'reload';
52
+ } | {
53
+ kind: 'wait';
54
+ condition: WoodWaitCondition;
55
+ };
56
+
57
+ export type { WoodAutomationAction };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}