jsgui3-server 0.0.148 → 0.0.149

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 (134) hide show
  1. package/.github/workflows/control-scan-manifest-check.yml +31 -0
  2. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-071799b982906680f5fd699d.js +40 -0
  3. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-07352945ad5c92654fcb8b65.js +39 -0
  4. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-138a601fadb6191ea314c6fd.js +39 -0
  5. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-171f6c381c2cadf2e9fa7087.js +39 -0
  6. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-1d973388156b84a04373fac9.js +39 -0
  7. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-20e117bc8a10d2cd16234bbe.js +40 -0
  8. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-2b028a82b0e5efddba42425f.js +39 -0
  9. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-4518556cd5c7e059e82b22b8.js +40 -0
  10. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5bac1aa0f213902f718ed74f.js +40 -0
  11. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5f9996ac7822caf777d92f56.js +39 -0
  12. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-60a92c702e65fd9cf748e3ec.js +39 -0
  13. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6164c1f8f738995c541895d2.js +44 -0
  14. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6718a85eb9e5aa782dd47a05.js +45 -0
  15. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-69e280f14e37aee76a1d4675.js +39 -0
  16. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7570d1b030d44b111ed59c4c.js +39 -0
  17. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7798c9bbd55e510d5039f936.js +42 -0
  18. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-78cd511ea1ef18ecb03d1be5.js +40 -0
  19. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7d482e0b95bcb5e3c543118b.js +43 -0
  20. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-80e9476d1127c55b40fdb36f.js +40 -0
  21. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-810ced55d5320a3088a05b13.js +40 -0
  22. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-8423565f1a40e329afc8c6cf.js +40 -0
  23. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-900bef783b8cee36506ec282.js +39 -0
  24. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-a1a37aff6416fdad74040ddf.js +39 -0
  25. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-ad48d5e8eda40f175b4df090.js +39 -0
  26. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-aec5a2d963015528c9099462.js +39 -0
  27. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-af9d34e0f1722fab9e28c269.js +39 -0
  28. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-b818e4015e2f1fe86280b5ab.js +41 -0
  29. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bcb2541adc70b7aba61768c5.js +44 -0
  30. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bfe89d2c78ed44f95ed7dd73.js +40 -0
  31. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c06f04806a1e688e1187110c.js +40 -0
  32. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c3f3adf904f585afc544b96a.js +39 -0
  33. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-d45acb873e1d8e32d5e60f2e.js +39 -0
  34. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-db06f132533706f4a0163b8c.js +39 -0
  35. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f660f40d78b135fc8560a862.js +39 -0
  36. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f9dee4ec18a96e09bee06bae.js +39 -0
  37. package/README.md +85 -3
  38. package/admin-ui/client.js +8 -8
  39. package/dev-status.svg +139 -0
  40. package/docs/api-reference.md +301 -43
  41. package/docs/books/jsgui3-bundling-research-book/00-table-of-contents.md +35 -0
  42. package/docs/books/jsgui3-bundling-research-book/01-pipeline-and-runtime-semantics.md +34 -0
  43. package/docs/books/jsgui3-bundling-research-book/02-javascript-bundling-core.md +36 -0
  44. package/docs/books/jsgui3-bundling-research-book/03-style-extraction-and-css-compilation.md +35 -0
  45. package/docs/books/jsgui3-bundling-research-book/04-static-publishing-and-delivery.md +39 -0
  46. package/docs/books/jsgui3-bundling-research-book/05-current-limits-and-size-bloat-vectors.md +25 -0
  47. package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +77 -0
  48. package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +63 -0
  49. package/docs/books/jsgui3-bundling-research-book/08-test-and-verification-methodology.md +43 -0
  50. package/docs/books/jsgui3-bundling-research-book/09-roadmap-and-rollout.md +42 -0
  51. package/docs/books/jsgui3-bundling-research-book/10-further-research-strategies-and-upgrades.md +211 -0
  52. package/docs/books/jsgui3-bundling-research-book/README.md +35 -0
  53. package/docs/bundling-system-deep-dive.md +9 -4
  54. package/docs/comprehensive-documentation.md +49 -18
  55. package/docs/configuration-reference.md +152 -27
  56. package/docs/core/README.md +19 -0
  57. package/docs/core/jsgui3-server-core-book/00-table-of-contents.md +21 -0
  58. package/docs/core/jsgui3-server-core-book/01-startup-readiness-state-machine.md +41 -0
  59. package/docs/core/jsgui3-server-core-book/02-resource-abstraction-and-lifecycle.md +92 -0
  60. package/docs/core/jsgui3-server-core-book/03-resource-pool-and-event-topology.md +47 -0
  61. package/docs/core/jsgui3-server-core-book/04-sse-publisher-semantics.md +41 -0
  62. package/docs/core/jsgui3-server-core-book/05-serve-factory-resource-wiring.md +46 -0
  63. package/docs/core/jsgui3-server-core-book/06-e2e-testing-methodology.md +48 -0
  64. package/docs/core/jsgui3-server-core-book/07-defect-detection-and-hardening-loop.md +47 -0
  65. package/docs/publishers-guide.md +59 -4
  66. package/docs/resources-guide.md +184 -35
  67. package/docs/simple-server-api-design.md +72 -17
  68. package/docs/system-architecture.md +18 -14
  69. package/examples/controls/15) window, observable SSE/server.js +6 -1
  70. package/examples/controls/19) window, auto observable ui/server.js +9 -0
  71. package/examples/controls/20) window, task manager app/README.md +133 -0
  72. package/examples/controls/20) window, task manager app/client.js +797 -0
  73. package/examples/controls/20) window, task manager app/server.js +178 -0
  74. package/examples/controls/6) window, color_palette/client.js +165 -68
  75. package/examples/controls/9) window, date picker/client.js +362 -76
  76. package/examples/controls/9b) window, shared data.model mirrored date pickers/client.js +104 -83
  77. package/examples/jsgui3-html/06) theming/client.js +22 -1
  78. package/examples/jsgui3-html/10) binding-debugger/client.js +137 -1
  79. package/http/responders/static/Static_Route_HTTP_Responder.js +52 -34
  80. package/lab/experiments/capture-color-controls.js +196 -0
  81. package/lab/results/screenshots/color-controls/full_page.png +0 -0
  82. package/lab/results/screenshots/color-controls/section_1_color_grid_12x12.png +0 -0
  83. package/lab/results/screenshots/color-controls/section_2_color_grid_4x2.png +0 -0
  84. package/lab/results/screenshots/color-controls/section_3_color_palette.png +0 -0
  85. package/lab/results/screenshots/color-controls/section_4_palette_comparison.png +0 -0
  86. package/lab/results/screenshots/color-controls/section_5_raw_swatches.png +0 -0
  87. package/lab/results/screenshots/color-controls/section_6_optimized_crayola.png +0 -0
  88. package/lab/results/screenshots/color-controls/section_7_pastel_palette.png +0 -0
  89. package/lab/results/screenshots/color-controls/section_8_extended_144.png +0 -0
  90. package/lab/screenshot-utils.js +248 -0
  91. package/module.js +11 -4
  92. package/package.json +12 -2
  93. package/publishers/Publishers.js +4 -3
  94. package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +5 -5
  95. package/publishers/http-sse-publisher.js +341 -0
  96. package/resources/process-resource.js +950 -0
  97. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +129 -33
  98. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +18 -7
  99. package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +829 -0
  100. package/resources/remote-process-resource.js +355 -0
  101. package/resources/server-resource-pool.js +354 -41
  102. package/serve-factory.js +441 -259
  103. package/server.js +89 -13
  104. package/tests/README.md +66 -4
  105. package/tests/admin-ui-render.test.js +24 -0
  106. package/tests/assigners.test.js +56 -40
  107. package/tests/bundling-default-control-elimination.puppeteer.test.js +260 -0
  108. package/tests/configuration-validation.test.js +21 -18
  109. package/tests/content-analysis.test.js +7 -6
  110. package/tests/control-optimizer-cache-behavior.test.js +52 -0
  111. package/tests/control-scan-manifest-regression.test.js +144 -0
  112. package/tests/end-to-end.test.js +15 -14
  113. package/tests/error-handling.test.js +222 -179
  114. package/tests/fixtures/bundling-default-button-client.js +37 -0
  115. package/tests/fixtures/bundling-default-window-client.js +34 -0
  116. package/tests/fixtures/control_scan_manifest_expectations.json +48 -0
  117. package/tests/fixtures/resource-monitor-client.js +319 -0
  118. package/tests/helpers/puppeteer-e2e-harness.js +317 -0
  119. package/tests/http-sse-publisher.test.js +136 -0
  120. package/tests/performance.test.js +69 -65
  121. package/tests/process-resource.test.js +138 -0
  122. package/tests/publishers.test.js +7 -7
  123. package/tests/remote-process-resource.test.js +160 -0
  124. package/tests/sass-controls.e2e.test.js +7 -1
  125. package/tests/serve-resources.test.js +270 -0
  126. package/tests/serve.test.js +120 -50
  127. package/tests/server-resource-pool.test.js +106 -0
  128. package/tests/small-controls-bundle-size.test.js +252 -0
  129. package/tests/test-runner.js +13 -1
  130. package/tests/window-examples.puppeteer.test.js +204 -1
  131. package/tests/window-resource-integration.puppeteer.test.js +585 -0
  132. package/tests/temp_invalid.js +0 -7
  133. package/tests/temp_invalid_utf8.js +0 -1
  134. package/tests/temp_malformed.js +0 -10
@@ -0,0 +1,319 @@
1
+ const jsgui = require('jsgui3-client');
2
+ const Active_HTML_Document = require('../../controls/Active_HTML_Document');
3
+
4
+ const { controls } = jsgui;
5
+ const { Button } = controls;
6
+
7
+ class Resource_Monitor_App extends Active_HTML_Document {
8
+ constructor(spec = {}) {
9
+ spec.__type_name = spec.__type_name || 'resource_monitor_app';
10
+ super(spec);
11
+
12
+ const { context } = this;
13
+
14
+ this._event_source = null;
15
+ this._events_count = 0;
16
+
17
+ const compose = () => {
18
+ const monitor_window = new controls.Window({
19
+ context,
20
+ title: 'Resource Monitor',
21
+ pos: [24, 24]
22
+ });
23
+ monitor_window.size = [560, 360];
24
+
25
+ const root = new controls.div({ context });
26
+ root.dom.attributes.id = 'resource-monitor-root';
27
+
28
+ const heading = new controls.h3({ context });
29
+ heading.add('Server Resource Control');
30
+ root.add(heading);
31
+
32
+ const controls_row = new controls.div({ context });
33
+ controls_row.dom.attributes.class = 'controls-row';
34
+
35
+ const start_button = new Button({ context, text: 'Start' });
36
+ start_button.dom.attributes.id = 'resource-start-btn';
37
+ controls_row.add(start_button);
38
+
39
+ const stop_button = new Button({ context, text: 'Stop' });
40
+ stop_button.dom.attributes.id = 'resource-stop-btn';
41
+ controls_row.add(stop_button);
42
+
43
+ const restart_button = new Button({ context, text: 'Restart' });
44
+ restart_button.dom.attributes.id = 'resource-restart-btn';
45
+ controls_row.add(restart_button);
46
+
47
+ const refresh_button = new Button({ context, text: 'Refresh' });
48
+ refresh_button.dom.attributes.id = 'resource-refresh-btn';
49
+ controls_row.add(refresh_button);
50
+
51
+ root.add(controls_row);
52
+
53
+ const state_line = new controls.div({ context });
54
+ state_line.dom.attributes.class = 'state-line';
55
+
56
+ const state_label = new controls.span({ context });
57
+ state_label.add('State: ');
58
+ state_line.add(state_label);
59
+
60
+ const state_value = new controls.span({ context });
61
+ state_value.dom.attributes.id = 'resource-state';
62
+ state_value.dom.attributes.class = 'resource-state-value';
63
+ state_value.add('unknown');
64
+ state_line.add(state_value);
65
+
66
+ root.add(state_line);
67
+
68
+ const events_line = new controls.div({ context });
69
+ events_line.dom.attributes.class = 'events-line';
70
+
71
+ const events_count_label = new controls.span({ context });
72
+ events_count_label.add('Events: ');
73
+ events_line.add(events_count_label);
74
+
75
+ const events_count_value = new controls.span({ context });
76
+ events_count_value.dom.attributes.id = 'resource-events-count';
77
+ events_count_value.dom.attributes.class = 'resource-events-count-value';
78
+ events_count_value.add('0');
79
+ events_line.add(events_count_value);
80
+
81
+ root.add(events_line);
82
+
83
+ const events_title = new controls.h4({ context });
84
+ events_title.add('Latest Events');
85
+ root.add(events_title);
86
+
87
+ const events_list = new controls.ul({ context });
88
+ events_list.dom.attributes.id = 'resource-events-list';
89
+ root.add(events_list);
90
+
91
+ monitor_window.inner.add(root);
92
+ this.body.add(monitor_window);
93
+ };
94
+
95
+ if (!spec.el) {
96
+ compose();
97
+ }
98
+ }
99
+
100
+ _set_state_text(next_state) {
101
+ const state_element = document.getElementById('resource-state');
102
+ if (state_element) {
103
+ state_element.textContent = String(next_state || 'unknown');
104
+ }
105
+ }
106
+
107
+ _append_event(event_name, payload = {}) {
108
+ this._events_count += 1;
109
+
110
+ const events_count_element = document.getElementById('resource-events-count');
111
+ if (events_count_element) {
112
+ events_count_element.textContent = String(this._events_count);
113
+ }
114
+
115
+ const events_list_element = document.getElementById('resource-events-list');
116
+ if (!events_list_element) {
117
+ return;
118
+ }
119
+
120
+ const list_item = document.createElement('li');
121
+ const summary = payload && typeof payload === 'object'
122
+ ? `${event_name}: ${payload.resourceName || payload.name || ''} ${payload.from || ''} -> ${payload.to || ''}`.trim()
123
+ : event_name;
124
+ list_item.textContent = summary;
125
+ events_list_element.prepend(list_item);
126
+
127
+ while (events_list_element.childElementCount > 12) {
128
+ events_list_element.removeChild(events_list_element.lastChild);
129
+ }
130
+ }
131
+
132
+ async _read_status() {
133
+ const response = await fetch('/api/resource/status');
134
+ const status_result = await response.json();
135
+ if (status_result && status_result.status && status_result.status.state) {
136
+ this._set_state_text(status_result.status.state);
137
+ }
138
+
139
+ window.__resource_client_debug = window.__resource_client_debug || {
140
+ api_calls: [],
141
+ sse_events: []
142
+ };
143
+ window.__resource_client_debug.api_calls.push({
144
+ route: '/api/resource/status',
145
+ timestamp: Date.now(),
146
+ state: status_result && status_result.status ? status_result.status.state : null
147
+ });
148
+
149
+ return status_result;
150
+ }
151
+
152
+ async _invoke_action(action_name) {
153
+ const response = await fetch(`/api/resource/${action_name}`, {
154
+ method: 'POST',
155
+ headers: {
156
+ 'Content-Type': 'application/json'
157
+ },
158
+ body: JSON.stringify({})
159
+ });
160
+
161
+ const action_result = await response.json();
162
+
163
+ window.__resource_client_debug = window.__resource_client_debug || {
164
+ api_calls: [],
165
+ sse_events: []
166
+ };
167
+ window.__resource_client_debug.api_calls.push({
168
+ route: `/api/resource/${action_name}`,
169
+ timestamp: Date.now(),
170
+ response_state: action_result && action_result.status ? action_result.status.state : null
171
+ });
172
+
173
+ await this._read_status();
174
+ return action_result;
175
+ }
176
+
177
+ _bind_buttons() {
178
+ const start_button = document.getElementById('resource-start-btn');
179
+ if (start_button) {
180
+ start_button.addEventListener('click', () => this._invoke_action('start').catch(console.error));
181
+ }
182
+
183
+ const stop_button = document.getElementById('resource-stop-btn');
184
+ if (stop_button) {
185
+ stop_button.addEventListener('click', () => this._invoke_action('stop').catch(console.error));
186
+ }
187
+
188
+ const restart_button = document.getElementById('resource-restart-btn');
189
+ if (restart_button) {
190
+ restart_button.addEventListener('click', () => this._invoke_action('restart').catch(console.error));
191
+ }
192
+
193
+ const refresh_button = document.getElementById('resource-refresh-btn');
194
+ if (refresh_button) {
195
+ refresh_button.addEventListener('click', () => this._read_status().catch(console.error));
196
+ }
197
+ }
198
+
199
+ _bind_sse() {
200
+ this._event_source = new EventSource('/events');
201
+
202
+ const observed_event_names = [
203
+ 'resource_state_change',
204
+ 'crashed',
205
+ 'unhealthy',
206
+ 'unreachable',
207
+ 'recovered'
208
+ ];
209
+
210
+ for (const event_name of observed_event_names) {
211
+ this._event_source.addEventListener(event_name, (event) => {
212
+ let payload = {};
213
+ if (event && typeof event.data === 'string' && event.data.length > 0) {
214
+ try {
215
+ payload = JSON.parse(event.data);
216
+ } catch {
217
+ payload = { raw: event.data };
218
+ }
219
+ }
220
+
221
+ if (event_name === 'resource_state_change' && payload.to) {
222
+ this._set_state_text(payload.to);
223
+ }
224
+
225
+ this._append_event(event_name, payload);
226
+
227
+ window.__resource_client_debug = window.__resource_client_debug || {
228
+ api_calls: [],
229
+ sse_events: []
230
+ };
231
+ window.__resource_client_debug.sse_events.push({
232
+ event_name,
233
+ payload,
234
+ timestamp: Date.now()
235
+ });
236
+ });
237
+ }
238
+ }
239
+
240
+ activate() {
241
+ if (this.__active) {
242
+ return;
243
+ }
244
+
245
+ super.activate();
246
+
247
+ this._bind_buttons();
248
+ this._bind_sse();
249
+
250
+ this._read_status().catch(console.error);
251
+
252
+ window.addEventListener('beforeunload', () => {
253
+ if (this._event_source) {
254
+ this._event_source.close();
255
+ this._event_source = null;
256
+ }
257
+ });
258
+ }
259
+ }
260
+
261
+ Resource_Monitor_App.css = `
262
+ body {
263
+ margin: 0;
264
+ padding: 0;
265
+ background: linear-gradient(145deg, #1f2937, #111827);
266
+ color: #f9fafb;
267
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
268
+ }
269
+
270
+ #resource-monitor-root {
271
+ padding: 14px;
272
+ }
273
+
274
+ #resource-monitor-root h3,
275
+ #resource-monitor-root h4 {
276
+ margin: 0 0 10px 0;
277
+ }
278
+
279
+ .controls-row {
280
+ display: flex;
281
+ gap: 8px;
282
+ margin-bottom: 10px;
283
+ }
284
+
285
+ .controls-row button {
286
+ min-width: 86px;
287
+ }
288
+
289
+ .state-line,
290
+ .events-line {
291
+ margin-bottom: 8px;
292
+ font-size: 14px;
293
+ }
294
+
295
+ #resource-events-list {
296
+ list-style: none;
297
+ margin: 0;
298
+ padding: 0;
299
+ max-height: 180px;
300
+ overflow: auto;
301
+ border: 1px solid rgba(255, 255, 255, 0.2);
302
+ border-radius: 6px;
303
+ background: rgba(17, 24, 39, 0.8);
304
+ }
305
+
306
+ #resource-events-list li {
307
+ font-size: 12px;
308
+ line-height: 1.4;
309
+ padding: 6px 8px;
310
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
311
+ }
312
+
313
+ #resource-events-list li:last-child {
314
+ border-bottom: none;
315
+ }
316
+ `;
317
+
318
+ controls.Resource_Monitor_App = Resource_Monitor_App;
319
+ module.exports = jsgui;
@@ -0,0 +1,317 @@
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_puppeteer_module = () => {
13
+ try {
14
+ return require('puppeteer');
15
+ } catch {
16
+ return null;
17
+ }
18
+ };
19
+
20
+ const launch_puppeteer_browser = async (puppeteer_module, launch_options = {}) => {
21
+ const resolved_puppeteer = puppeteer_module || ensure_puppeteer_module();
22
+ if (!resolved_puppeteer) {
23
+ throw new Error('Puppeteer 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.PUPPETEER_EXECUTABLE_PATH) {
33
+ normalized_options.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
34
+ }
35
+
36
+ return resolved_puppeteer.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();
181
+ await page.setViewport(viewport);
182
+
183
+ const page_probe = attach_page_probe(page);
184
+
185
+ await page.goto(target_url, {
186
+ waitUntil: options.wait_until || 'load'
187
+ });
188
+
189
+ return {
190
+ page,
191
+ page_probe
192
+ };
193
+ };
194
+
195
+ const set_input_value_with_events = async (
196
+ page,
197
+ selector,
198
+ next_value,
199
+ event_names = ['input', 'change']
200
+ ) => {
201
+ await page.$eval(selector, (element, value, events) => {
202
+ element.value = String(value);
203
+ for (const event_name of events) {
204
+ element.dispatchEvent(new Event(event_name, { bubbles: true }));
205
+ }
206
+ }, next_value, event_names);
207
+ };
208
+
209
+ const wait_for_text_content = async (page, selector, expected_text_or_regex, timeout_ms = 6000) => {
210
+ if (expected_text_or_regex instanceof RegExp) {
211
+ await page.waitForFunction(
212
+ (selector_arg, regex_source, regex_flags) => {
213
+ const element = document.querySelector(selector_arg);
214
+ if (!element) return false;
215
+ const value = (element.textContent || '').trim();
216
+ const expected_regex = new RegExp(regex_source, regex_flags);
217
+ return expected_regex.test(value);
218
+ },
219
+ { timeout: timeout_ms },
220
+ selector,
221
+ expected_text_or_regex.source,
222
+ expected_text_or_regex.flags
223
+ );
224
+ return;
225
+ }
226
+
227
+ const expected_text = String(expected_text_or_regex);
228
+ await page.waitForFunction(
229
+ (selector_arg, expected_text_arg) => {
230
+ const element = document.querySelector(selector_arg);
231
+ if (!element) return false;
232
+ return (element.textContent || '').trim() === expected_text_arg;
233
+ },
234
+ { timeout: timeout_ms },
235
+ selector,
236
+ expected_text
237
+ );
238
+ };
239
+
240
+ const drag_by = async (page, selector, delta_x, delta_y) => {
241
+ const handle = await page.$(selector);
242
+ assert(handle, `Missing drag handle for selector: ${selector}`);
243
+ const box = await handle.boundingBox();
244
+ assert(box, `Missing bounding box for selector: ${selector}`);
245
+
246
+ const start_x = box.x + box.width / 2;
247
+ const start_y = box.y + box.height / 2;
248
+
249
+ await page.mouse.move(start_x, start_y);
250
+ await page.mouse.down();
251
+ await page.mouse.move(start_x + delta_x, start_y + delta_y, { steps: 12 });
252
+ await page.mouse.up();
253
+ };
254
+
255
+ const run_interaction_story = async ({
256
+ page,
257
+ story_name,
258
+ steps
259
+ }) => {
260
+ assert(Array.isArray(steps) && steps.length > 0, 'Interaction story requires at least one step');
261
+
262
+ const step_results = [];
263
+
264
+ for (let index = 0; index < steps.length; index += 1) {
265
+ const step_spec = steps[index] || {};
266
+ const step_name = step_spec.name || `step_${index + 1}`;
267
+ const started_at = Date.now();
268
+
269
+ try {
270
+ if (typeof step_spec.run === 'function') {
271
+ await step_spec.run(page);
272
+ }
273
+ if (typeof step_spec.assert === 'function') {
274
+ await step_spec.assert(page);
275
+ }
276
+
277
+ step_results.push({
278
+ step_name,
279
+ duration_ms: Date.now() - started_at
280
+ });
281
+ } catch (error) {
282
+ error.message = `[${story_name} :: ${step_name}] ${error.message}`;
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ return step_results;
288
+ };
289
+
290
+ const wait_for_condition = async (condition_fn, timeout_ms = 6000, interval_ms = 25) => {
291
+ const started_at = Date.now();
292
+
293
+ while ((Date.now() - started_at) <= timeout_ms) {
294
+ const condition_result = await condition_fn();
295
+ if (condition_result) {
296
+ return true;
297
+ }
298
+ await new Promise((resolve) => setTimeout(resolve, interval_ms));
299
+ }
300
+
301
+ return false;
302
+ };
303
+
304
+ module.exports = {
305
+ ensure_puppeteer_module,
306
+ launch_puppeteer_browser,
307
+ start_control_example_server,
308
+ stop_server_instance,
309
+ open_page,
310
+ attach_page_probe,
311
+ assert_clean_page_probe,
312
+ set_input_value_with_events,
313
+ wait_for_text_content,
314
+ drag_by,
315
+ run_interaction_story,
316
+ wait_for_condition
317
+ };