jsgui3-server 0.0.151 → 0.0.155

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 (109) hide show
  1. package/README.md +21 -0
  2. package/admin-ui/v1/controls/admin_shell.js +33 -0
  3. package/admin-ui/v1/server.js +14 -1
  4. package/docs/agi/skills/README.md +23 -0
  5. package/docs/agi/skills/agent-output-control/SKILL.md +56 -0
  6. package/docs/agi/skills/ai-deep-research/SKILL.md +52 -0
  7. package/docs/agi/skills/autonomous-ui-inspection/SKILL.md +102 -0
  8. package/docs/agi/skills/deep-research/SKILL.md +156 -0
  9. package/docs/agi/skills/endurance/SKILL.md +53 -0
  10. package/docs/agi/skills/exploring-other-codebases/SKILL.md +56 -0
  11. package/docs/agi/skills/instruction-adherence/SKILL.md +73 -0
  12. package/docs/agi/skills/jsgui3-activation-debug/SKILL.md +94 -0
  13. package/docs/agi/skills/jsgui3-context-menu-patterns/SKILL.md +94 -0
  14. package/docs/agi/skills/puppeteer-efficient-ui-verification/SKILL.md +65 -0
  15. package/docs/agi/skills/runaway-process-guard/SKILL.md +49 -0
  16. package/docs/agi/skills/session-discipline/SKILL.md +40 -0
  17. package/docs/agi/skills/skill-writing/SKILL.md +211 -0
  18. package/docs/agi/skills/static-analysis/SKILL.md +58 -0
  19. package/docs/agi/skills/targeted-testing/SKILL.md +63 -0
  20. package/docs/agi/skills/understanding-jsgui3/SKILL.md +85 -0
  21. package/docs/api-reference.md +120 -2
  22. package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +1 -0
  23. package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +33 -0
  24. package/docs/books/website-design/01-introduction.md +73 -0
  25. package/docs/books/website-design/02-current-state.md +195 -0
  26. package/docs/books/website-design/03-base-class.md +181 -0
  27. package/docs/books/website-design/04-webpage.md +307 -0
  28. package/docs/books/website-design/05-website.md +456 -0
  29. package/docs/books/website-design/06-pages-storage.md +170 -0
  30. package/docs/books/website-design/07-api-layer.md +285 -0
  31. package/docs/books/website-design/08-server-integration.md +271 -0
  32. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  33. package/docs/books/website-design/10-open-questions.md +196 -0
  34. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  35. package/docs/books/website-design/12-content-model.md +395 -0
  36. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  37. package/docs/books/website-design/14-website-module-spec.md +541 -0
  38. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  39. package/docs/books/website-design/16-minimal-first.md +203 -0
  40. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  41. package/docs/books/website-design/README.md +43 -0
  42. package/docs/bundling-system-deep-dive.md +112 -3
  43. package/docs/configuration-reference.md +84 -0
  44. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  45. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  46. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  47. package/docs/swagger.md +316 -0
  48. package/examples/controls/1) window/server.js +6 -1
  49. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  50. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  51. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  52. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  53. declarative api/e2e-screenshot-1-name-change.png +0 -0
  54. declarative api/e2e-screenshot-2-toggled.png +0 -0
  55. declarative api/e2e-screenshot-3-final.png +0 -0
  56. declarative api/e2e-screenshot-final.png +0 -0
  57. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  58. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  59. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  60. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  61. package/examples/data-views/01) query-endpoint/server.js +61 -0
  62. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  63. package/labs/website-design/002-pages-storage/check.js +244 -0
  64. package/labs/website-design/002-pages-storage/results.txt +0 -0
  65. package/labs/website-design/003-type-detection/check.js +193 -0
  66. package/labs/website-design/003-type-detection/results.txt +0 -0
  67. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  68. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  69. package/labs/website-design/005-normalize-input/check.js +303 -0
  70. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  71. package/labs/website-design/README.md +34 -0
  72. package/labs/website-design/manifest.json +68 -0
  73. package/labs/website-design/run-all.js +60 -0
  74. package/middleware/json-body.js +126 -0
  75. package/openapi.js +474 -0
  76. package/package.json +13 -7
  77. package/publishers/Publishers.js +6 -5
  78. package/publishers/http-function-publisher.js +135 -126
  79. package/publishers/http-webpage-publisher.js +89 -11
  80. package/publishers/query-publisher.js +116 -0
  81. package/publishers/swagger-publisher.js +203 -0
  82. package/publishers/swagger-ui.js +578 -0
  83. package/resources/adapters/array-adapter.js +143 -0
  84. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +90 -22
  85. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +50 -14
  86. package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +48 -14
  87. package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +396 -44
  88. package/resources/query-resource.js +131 -0
  89. package/serve-factory.js +677 -18
  90. package/server.js +585 -167
  91. package/tests/README.md +86 -2
  92. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  93. package/tests/bundling-default-control-elimination.puppeteer.test.js +32 -1
  94. package/tests/control-elimination-root-feature-pruning.test.js +440 -0
  95. package/tests/control-elimination-static-bracket-access.test.js +245 -0
  96. package/tests/control-scan-manifest-regression.test.js +2 -0
  97. package/tests/end-to-end.test.js +22 -21
  98. package/tests/fixtures/control_scan_manifest_expectations.json +4 -2
  99. package/tests/helpers/playwright-e2e-harness.js +326 -0
  100. package/tests/helpers/puppeteer-e2e-harness.js +62 -1
  101. package/tests/openapi.test.js +319 -0
  102. package/tests/playwright-smoke.test.js +134 -0
  103. package/tests/project-local-controls-bundling.puppeteer.test.js +462 -0
  104. package/tests/publish-enhancements.test.js +673 -0
  105. package/tests/query-publisher.test.js +430 -0
  106. package/tests/quick-json-body-test.js +169 -0
  107. package/tests/serve.test.js +425 -122
  108. package/tests/swagger-publisher.test.js +1076 -0
  109. package/tests/test-runner.js +4 -0
@@ -16,8 +16,10 @@ const normalize_manifest = (manifest) => {
16
16
  entry_file: manifest && manifest.entry_file_path ? path.basename(manifest.entry_file_path) : null,
17
17
  uses_jsgui3_html: Boolean(manifest && manifest.uses_jsgui3_html),
18
18
  dynamic_control_access_detected: Boolean(manifest && manifest.dynamic_control_access_detected),
19
+ dynamic_resource_access_detected: Boolean(manifest && manifest.dynamic_resource_access_detected),
19
20
  reachable_files: safe_paths(manifest && manifest.reachable_files),
20
21
  used_identifiers: safe_array(manifest && manifest.used_identifiers),
22
+ selected_root_features: safe_array(manifest && manifest.selected_root_features),
21
23
  selected_controls: safe_array(manifest && manifest.selected_controls),
22
24
  unmatched_identifiers: safe_array(manifest && manifest.unmatched_identifiers),
23
25
  package_aliases: safe_array(manifest && manifest.package_aliases),
@@ -12,6 +12,7 @@ describe('End-to-End Integration Tests', function() {
12
12
 
13
13
  let server;
14
14
  let serverPort = 3001; // Use a different port for testing
15
+ const test_host = '127.0.0.1';
15
16
  let testControl;
16
17
  let test_client_path;
17
18
  let base_serve_options;
@@ -36,7 +37,7 @@ describe('End-to-End Integration Tests', function() {
36
37
  const server_instance = await Server.serve(serve_options);
37
38
  const port = server_instance.port || serve_options.port;
38
39
  if (port) {
39
- await wait_for_route(`http://localhost:${port}/js/js.js`);
40
+ await wait_for_route(`http://${test_host}:${port}/js/js.js`);
40
41
  }
41
42
  return server_instance;
42
43
  };
@@ -86,7 +87,7 @@ describe('End-to-End Integration Tests', function() {
86
87
  }
87
88
  });
88
89
 
89
- const js_identity_response = await makeRequest(`http://localhost:${serverPort}/js/js.js`, {
90
+ const js_identity_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
90
91
  'Accept-Encoding': 'identity'
91
92
  });
92
93
 
@@ -97,7 +98,7 @@ describe('End-to-End Integration Tests', function() {
97
98
  );
98
99
 
99
100
  // Test gzip compressed JavaScript
100
- const js_gzip_response = await makeRequest(`http://localhost:${serverPort}/js/js.js`, {
101
+ const js_gzip_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
101
102
  'Accept-Encoding': 'gzip'
102
103
  });
103
104
 
@@ -112,7 +113,7 @@ describe('End-to-End Integration Tests', function() {
112
113
  );
113
114
 
114
115
  // Test brotli compressed JavaScript
115
- const br_js_response = await makeRequest(`http://localhost:${serverPort}/js/js.js`, {
116
+ const br_js_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
116
117
  'Accept-Encoding': 'br'
117
118
  });
118
119
 
@@ -148,7 +149,7 @@ describe('End-to-End Integration Tests', function() {
148
149
  });
149
150
 
150
151
  // Test JavaScript with sourcemaps
151
- const js_gzip_response = await makeRequest(`http://localhost:${serverPort}/js/js.js`, {
152
+ const js_gzip_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
152
153
  'Accept-Encoding': 'gzip'
153
154
  });
154
155
 
@@ -179,7 +180,7 @@ describe('End-to-End Integration Tests', function() {
179
180
  }
180
181
  });
181
182
 
182
- const css_identity_response = await makeRequest(`http://localhost:${serverPort}/css/css.css`, {
183
+ const css_identity_response = await makeRequest(`http://${test_host}:${serverPort}/css/css.css`, {
183
184
  'Accept-Encoding': 'identity'
184
185
  });
185
186
 
@@ -190,7 +191,7 @@ describe('End-to-End Integration Tests', function() {
190
191
  );
191
192
 
192
193
  // Test gzip compressed CSS
193
- const css_gzip_response = await makeRequest(`http://localhost:${serverPort}/css/css.css`, {
194
+ const css_gzip_response = await makeRequest(`http://${test_host}:${serverPort}/css/css.css`, {
194
195
  'Accept-Encoding': 'gzip'
195
196
  });
196
197
 
@@ -223,9 +224,9 @@ describe('End-to-End Integration Tests', function() {
223
224
  });
224
225
 
225
226
  // Test HTML (should not be compressed due to threshold)
226
- const htmlResponse = await makeRequest(`http://localhost:${serverPort}/`, {
227
- 'Accept-Encoding': 'gzip'
228
- });
227
+ const htmlResponse = await makeRequest(`http://${test_host}:${serverPort}/`, {
228
+ 'Accept-Encoding': 'gzip'
229
+ });
229
230
 
230
231
  assert.strictEqual(htmlResponse.statusCode, 200);
231
232
  assert(!htmlResponse.headers['content-encoding'], 'HTML should not be compressed due to threshold');
@@ -250,7 +251,7 @@ describe('End-to-End Integration Tests', function() {
250
251
  }
251
252
  });
252
253
 
253
- const baseline_response = await makeRequest(`http://localhost:${serverPort}/js/js.js`, {
254
+ const baseline_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
254
255
  'Accept-Encoding': 'identity'
255
256
  });
256
257
  assert.strictEqual(baseline_response.statusCode, 200);
@@ -276,7 +277,7 @@ describe('End-to-End Integration Tests', function() {
276
277
  });
277
278
 
278
279
  // Test JavaScript
279
- const minified_response = await makeRequest(`http://localhost:${serverPort}/js/js.js`, {
280
+ const minified_response = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
280
281
  'Accept-Encoding': 'identity'
281
282
  });
282
283
 
@@ -305,9 +306,9 @@ describe('End-to-End Integration Tests', function() {
305
306
  });
306
307
 
307
308
  // Test with gzip request but compression disabled
308
- const jsResponse = await makeRequest(`http://localhost:${serverPort}/js/js.js`, {
309
- 'Accept-Encoding': 'gzip'
310
- });
309
+ const jsResponse = await makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
310
+ 'Accept-Encoding': 'gzip'
311
+ });
311
312
 
312
313
  assert.strictEqual(jsResponse.statusCode, 200);
313
314
  assert(!jsResponse.headers['content-encoding'], 'Should not have content-encoding when compression disabled');
@@ -332,11 +333,11 @@ describe('End-to-End Integration Tests', function() {
332
333
 
333
334
  // Make multiple concurrent requests
334
335
  const requests = [];
335
- for (let i = 0; i < 5; i++) {
336
- requests.push(makeRequest(`http://localhost:${serverPort}/js/js.js`, {
337
- 'Accept-Encoding': 'gzip'
338
- }));
339
- }
336
+ for (let i = 0; i < 5; i++) {
337
+ requests.push(makeRequest(`http://${test_host}:${serverPort}/js/js.js`, {
338
+ 'Accept-Encoding': 'gzip'
339
+ }));
340
+ }
340
341
 
341
342
  const responses = await Promise.all(requests);
342
343
 
@@ -366,7 +367,7 @@ describe('End-to-End Integration Tests', function() {
366
367
  ];
367
368
 
368
369
  for (const test of tests) {
369
- const response = await makeRequest(`http://localhost:${serverPort}${test.path}`);
370
+ const response = await makeRequest(`http://${test_host}:${serverPort}${test.path}`);
370
371
  assert.strictEqual(response.statusCode, 200);
371
372
  assert(response.headers['content-type'].includes(test.expectedType),
372
373
  `Should serve correct content type for ${test.path}`);
@@ -3,6 +3,7 @@
3
3
  "entry_file": "temp_control_scan_static_alias_client.js",
4
4
  "uses_jsgui3_html": true,
5
5
  "dynamic_control_access_detected": false,
6
+ "dynamic_resource_access_detected": false,
6
7
  "reachable_files": [
7
8
  "temp_control_scan_static_alias_client.js"
8
9
  ],
@@ -10,6 +11,7 @@
10
11
  "Active_HTML_Document",
11
12
  "Button"
12
13
  ],
14
+ "selected_root_features": [],
13
15
  "selected_controls": [
14
16
  "Active_HTML_Document",
15
17
  "Button"
@@ -19,7 +21,6 @@
19
21
  "ui"
20
22
  ],
21
23
  "controls_aliases": [
22
- "controls",
23
24
  "ui_controls"
24
25
  ]
25
26
  },
@@ -27,12 +28,14 @@
27
28
  "entry_file": "temp_control_scan_dynamic_alias_client.js",
28
29
  "uses_jsgui3_html": true,
29
30
  "dynamic_control_access_detected": true,
31
+ "dynamic_resource_access_detected": false,
30
32
  "reachable_files": [
31
33
  "temp_control_scan_dynamic_alias_client.js"
32
34
  ],
33
35
  "used_identifiers": [
34
36
  "Active_HTML_Document"
35
37
  ],
38
+ "selected_root_features": [],
36
39
  "selected_controls": [
37
40
  "Active_HTML_Document"
38
41
  ],
@@ -41,7 +44,6 @@
41
44
  "ui"
42
45
  ],
43
46
  "controls_aliases": [
44
- "controls",
45
47
  "ui_controls"
46
48
  ]
47
49
  }
@@ -0,0 +1,326 @@
1
+ const assert = require('assert');
2
+ const path = require('path');
3
+
4
+ const Server = require('../../server');
5
+ const { get_free_port } = require('../../port-utils');
6
+
7
+ const default_viewport = {
8
+ width: 1366,
9
+ height: 900
10
+ };
11
+
12
+ const ensure_playwright_module = () => {
13
+ try {
14
+ return require('playwright');
15
+ } catch {
16
+ return null;
17
+ }
18
+ };
19
+
20
+ const launch_playwright_browser = async (playwright_module, launch_options = {}) => {
21
+ const resolved_playwright = playwright_module || ensure_playwright_module();
22
+ if (!resolved_playwright || !resolved_playwright.chromium) {
23
+ throw new Error('Playwright Chromium is unavailable in this environment.');
24
+ }
25
+
26
+ const normalized_options = {
27
+ headless: true,
28
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
29
+ ...launch_options
30
+ };
31
+
32
+ if (!normalized_options.executablePath && process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
33
+ normalized_options.executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
34
+ }
35
+
36
+ return resolved_playwright.chromium.launch(normalized_options);
37
+ };
38
+
39
+ const start_control_example_server = async ({
40
+ examples_root_path,
41
+ dir_name,
42
+ ctrl_name,
43
+ server_name_prefix = 'examples/controls'
44
+ }) => {
45
+ const example_dir_path = path.join(examples_root_path, dir_name);
46
+ const example_client_path = path.join(example_dir_path, 'client.js');
47
+
48
+ const jsgui_module = require(example_client_path);
49
+ const ctrl_constructor = jsgui_module.controls && jsgui_module.controls[ctrl_name];
50
+ assert(ctrl_constructor, `Missing exported control jsgui.controls.${ctrl_name} in ${example_client_path}`);
51
+
52
+ const server_instance = new Server({
53
+ Ctrl: ctrl_constructor,
54
+ src_path_client_js: example_client_path,
55
+ name: `${server_name_prefix}/${dir_name}`
56
+ });
57
+
58
+ server_instance.allowed_addresses = ['127.0.0.1'];
59
+
60
+ await new Promise((resolve, reject) => {
61
+ const timeout_handle = setTimeout(() => reject(new Error('Publisher ready timeout')), 60000);
62
+ server_instance.on('ready', () => {
63
+ clearTimeout(timeout_handle);
64
+ resolve();
65
+ });
66
+ });
67
+
68
+ const port = await get_free_port();
69
+
70
+ await new Promise((resolve, reject) => {
71
+ server_instance.start(port, (error) => {
72
+ if (error) reject(error);
73
+ else resolve();
74
+ });
75
+ });
76
+
77
+ return {
78
+ server_instance,
79
+ port,
80
+ example_client_path
81
+ };
82
+ };
83
+
84
+ const stop_server_instance = async (server_instance) => {
85
+ if (!server_instance) {
86
+ return;
87
+ }
88
+
89
+ await new Promise((resolve) => {
90
+ server_instance.close(() => resolve());
91
+ });
92
+ };
93
+
94
+ const attach_page_probe = (page) => {
95
+ const probe = {
96
+ console_errors: [],
97
+ page_errors: [],
98
+ request_failures: []
99
+ };
100
+
101
+ const console_handler = (message) => {
102
+ if (message.type() === 'error') {
103
+ probe.console_errors.push(message.text());
104
+ }
105
+ };
106
+
107
+ const page_error_handler = (error) => {
108
+ probe.page_errors.push(error instanceof Error ? error.message : String(error));
109
+ };
110
+
111
+ const request_failed_handler = (request) => {
112
+ const failure = request.failure();
113
+ probe.request_failures.push({
114
+ url: request.url(),
115
+ method: request.method(),
116
+ error_text: failure ? failure.errorText : 'unknown'
117
+ });
118
+ };
119
+
120
+ page.on('console', console_handler);
121
+ page.on('pageerror', page_error_handler);
122
+ page.on('requestfailed', request_failed_handler);
123
+
124
+ probe.detach = () => {
125
+ page.off('console', console_handler);
126
+ page.off('pageerror', page_error_handler);
127
+ page.off('requestfailed', request_failed_handler);
128
+ };
129
+
130
+ return probe;
131
+ };
132
+
133
+ const assert_clean_page_probe = (probe, {
134
+ allowed_console_error_patterns = [],
135
+ allowed_page_error_patterns = [],
136
+ allowed_request_failure_patterns = []
137
+ } = {}) => {
138
+ const is_allowed_message = (message, allowed_patterns) => {
139
+ return allowed_patterns.some((pattern) => {
140
+ if (pattern instanceof RegExp) {
141
+ return pattern.test(message);
142
+ }
143
+ return String(message).includes(String(pattern));
144
+ });
145
+ };
146
+
147
+ const blocked_console_errors = probe.console_errors.filter((message) => {
148
+ return !is_allowed_message(message, allowed_console_error_patterns);
149
+ });
150
+
151
+ const blocked_page_errors = probe.page_errors.filter((message) => {
152
+ return !is_allowed_message(message, allowed_page_error_patterns);
153
+ });
154
+
155
+ const blocked_request_failures = probe.request_failures.filter((failure_item) => {
156
+ const label = `${failure_item.method} ${failure_item.url} ${failure_item.error_text}`;
157
+ return !is_allowed_message(label, allowed_request_failure_patterns);
158
+ });
159
+
160
+ assert.strictEqual(
161
+ blocked_console_errors.length,
162
+ 0,
163
+ `Unexpected browser console errors: ${blocked_console_errors.join(' | ')}`
164
+ );
165
+ assert.strictEqual(
166
+ blocked_page_errors.length,
167
+ 0,
168
+ `Unexpected browser page errors: ${blocked_page_errors.join(' | ')}`
169
+ );
170
+ assert.strictEqual(
171
+ blocked_request_failures.length,
172
+ 0,
173
+ `Unexpected browser request failures: ${JSON.stringify(blocked_request_failures)}`
174
+ );
175
+ };
176
+
177
+ const open_page = async (browser_instance, target_url, options = {}) => {
178
+ const viewport = options.viewport || default_viewport;
179
+
180
+ const page = await browser_instance.newPage({ viewport });
181
+
182
+ const page_probe = attach_page_probe(page);
183
+
184
+ await page.goto(target_url, {
185
+ waitUntil: options.wait_until || 'load'
186
+ });
187
+
188
+ return {
189
+ page,
190
+ page_probe
191
+ };
192
+ };
193
+
194
+ const close_page_with_probe = async (page, page_probe) => {
195
+ if (page_probe && typeof page_probe.detach === 'function') {
196
+ page_probe.detach();
197
+ }
198
+
199
+ if (page && !page.isClosed()) {
200
+ await page.close();
201
+ }
202
+ };
203
+
204
+ const set_input_value_with_events = async (
205
+ page,
206
+ selector,
207
+ next_value,
208
+ event_names = ['input', 'change']
209
+ ) => {
210
+ await page.$eval(selector, (element, value, events) => {
211
+ element.value = String(value);
212
+ for (const event_name of events) {
213
+ element.dispatchEvent(new Event(event_name, { bubbles: true }));
214
+ }
215
+ }, next_value, event_names);
216
+ };
217
+
218
+ const wait_for_text_content = async (page, selector, expected_text_or_regex, timeout_ms = 6000) => {
219
+ if (expected_text_or_regex instanceof RegExp) {
220
+ await page.waitForFunction(
221
+ (selector_arg, regex_source, regex_flags) => {
222
+ const element = document.querySelector(selector_arg);
223
+ if (!element) return false;
224
+ const value = (element.textContent || '').trim();
225
+ const expected_regex = new RegExp(regex_source, regex_flags);
226
+ return expected_regex.test(value);
227
+ },
228
+ { timeout: timeout_ms },
229
+ selector,
230
+ expected_text_or_regex.source,
231
+ expected_text_or_regex.flags
232
+ );
233
+ return;
234
+ }
235
+
236
+ const expected_text = String(expected_text_or_regex);
237
+ await page.waitForFunction(
238
+ (selector_arg, expected_text_arg) => {
239
+ const element = document.querySelector(selector_arg);
240
+ if (!element) return false;
241
+ return (element.textContent || '').trim() === expected_text_arg;
242
+ },
243
+ { timeout: timeout_ms },
244
+ selector,
245
+ expected_text
246
+ );
247
+ };
248
+
249
+ const drag_by = async (page, selector, delta_x, delta_y) => {
250
+ const locator = page.locator(selector).first();
251
+ const box = await locator.boundingBox();
252
+ assert(box, `Missing bounding box for selector: ${selector}`);
253
+
254
+ const start_x = box.x + box.width / 2;
255
+ const start_y = box.y + box.height / 2;
256
+
257
+ await page.mouse.move(start_x, start_y);
258
+ await page.mouse.down();
259
+ await page.mouse.move(start_x + delta_x, start_y + delta_y, { steps: 12 });
260
+ await page.mouse.up();
261
+ };
262
+
263
+ const run_interaction_story = async ({
264
+ page,
265
+ story_name,
266
+ steps
267
+ }) => {
268
+ assert(Array.isArray(steps) && steps.length > 0, 'Interaction story requires at least one step');
269
+
270
+ const step_results = [];
271
+
272
+ for (let index = 0; index < steps.length; index += 1) {
273
+ const step_spec = steps[index] || {};
274
+ const step_name = step_spec.name || `step_${index + 1}`;
275
+ const started_at = Date.now();
276
+
277
+ try {
278
+ if (typeof step_spec.run === 'function') {
279
+ await step_spec.run(page);
280
+ }
281
+ if (typeof step_spec.assert === 'function') {
282
+ await step_spec.assert(page);
283
+ }
284
+
285
+ step_results.push({
286
+ step_name,
287
+ duration_ms: Date.now() - started_at
288
+ });
289
+ } catch (error) {
290
+ error.message = `[${story_name} :: ${step_name}] ${error.message}`;
291
+ throw error;
292
+ }
293
+ }
294
+
295
+ return step_results;
296
+ };
297
+
298
+ const wait_for_condition = async (condition_fn, timeout_ms = 6000, interval_ms = 25) => {
299
+ const started_at = Date.now();
300
+
301
+ while ((Date.now() - started_at) <= timeout_ms) {
302
+ const condition_result = await condition_fn();
303
+ if (condition_result) {
304
+ return true;
305
+ }
306
+ await new Promise((resolve) => setTimeout(resolve, interval_ms));
307
+ }
308
+
309
+ return false;
310
+ };
311
+
312
+ module.exports = {
313
+ ensure_playwright_module,
314
+ launch_playwright_browser,
315
+ start_control_example_server,
316
+ stop_server_instance,
317
+ open_page,
318
+ close_page_with_probe,
319
+ attach_page_probe,
320
+ assert_clean_page_probe,
321
+ set_input_value_with_events,
322
+ wait_for_text_content,
323
+ drag_by,
324
+ run_interaction_story,
325
+ wait_for_condition
326
+ };
@@ -301,6 +301,64 @@ const wait_for_condition = async (condition_fn, timeout_ms = 6000, interval_ms =
301
301
  return false;
302
302
  };
303
303
 
304
+ const extract_esbuild_warning_headers_from_warning_records = (warning_records = []) => {
305
+ if (!Array.isArray(warning_records)) {
306
+ return [];
307
+ }
308
+
309
+ return warning_records.map((warning_record) => {
310
+ if (!warning_record || typeof warning_record !== 'object') {
311
+ return '';
312
+ }
313
+ const warning_text = String(warning_record.text || '').trim();
314
+ const warning_id = warning_record.id ? `[${String(warning_record.id)}]` : '';
315
+ return warning_id ? `${warning_text} ${warning_id}`.trim() : warning_text;
316
+ }).filter((warning_header) => warning_header.length > 0);
317
+ };
318
+
319
+ const extract_esbuild_warning_headers_from_bundle = (bundle) => {
320
+ const warning_records = bundle &&
321
+ bundle.bundle_analysis &&
322
+ bundle.bundle_analysis.esbuild_warnings;
323
+ return extract_esbuild_warning_headers_from_warning_records(warning_records);
324
+ };
325
+
326
+ const assert_no_unexpected_esbuild_warning_headers = (warning_headers, {
327
+ allowed_warning_patterns = [],
328
+ required_warning_patterns = []
329
+ } = {}) => {
330
+ const headers = Array.isArray(warning_headers) ? warning_headers : [];
331
+ const is_pattern_match = (value, pattern) => {
332
+ if (pattern instanceof RegExp) {
333
+ return pattern.test(value);
334
+ }
335
+ return value.includes(String(pattern));
336
+ };
337
+ const is_allowed_warning = (warning_header) => {
338
+ return allowed_warning_patterns.some((pattern) => is_pattern_match(warning_header, pattern));
339
+ };
340
+ const unexpected_warning_headers = headers.filter((warning_header) => !is_allowed_warning(warning_header));
341
+ assert.strictEqual(
342
+ unexpected_warning_headers.length,
343
+ 0,
344
+ `Unexpected esbuild warnings detected:\n${unexpected_warning_headers.join('\n')}`
345
+ );
346
+
347
+ const missing_required_patterns = required_warning_patterns.filter((pattern) => {
348
+ return !headers.some((warning_header) => is_pattern_match(warning_header, pattern));
349
+ });
350
+ assert.strictEqual(
351
+ missing_required_patterns.length,
352
+ 0,
353
+ `Missing required esbuild warning patterns: ${missing_required_patterns.map((pattern) => String(pattern)).join(', ')}`
354
+ );
355
+
356
+ return {
357
+ warning_headers: headers,
358
+ unexpected_warning_headers
359
+ };
360
+ };
361
+
304
362
  module.exports = {
305
363
  ensure_puppeteer_module,
306
364
  launch_puppeteer_browser,
@@ -313,5 +371,8 @@ module.exports = {
313
371
  wait_for_text_content,
314
372
  drag_by,
315
373
  run_interaction_story,
316
- wait_for_condition
374
+ wait_for_condition,
375
+ extract_esbuild_warning_headers_from_warning_records,
376
+ extract_esbuild_warning_headers_from_bundle,
377
+ assert_no_unexpected_esbuild_warning_headers
317
378
  };