jsgui3-server 0.0.151 → 0.0.152

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 (77) 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/api-reference.md +120 -2
  5. package/docs/books/website-design/01-introduction.md +73 -0
  6. package/docs/books/website-design/02-current-state.md +195 -0
  7. package/docs/books/website-design/03-base-class.md +181 -0
  8. package/docs/books/website-design/04-webpage.md +307 -0
  9. package/docs/books/website-design/05-website.md +456 -0
  10. package/docs/books/website-design/06-pages-storage.md +170 -0
  11. package/docs/books/website-design/07-api-layer.md +285 -0
  12. package/docs/books/website-design/08-server-integration.md +271 -0
  13. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  14. package/docs/books/website-design/10-open-questions.md +196 -0
  15. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  16. package/docs/books/website-design/12-content-model.md +395 -0
  17. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  18. package/docs/books/website-design/14-website-module-spec.md +541 -0
  19. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  20. package/docs/books/website-design/16-minimal-first.md +203 -0
  21. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  22. package/docs/books/website-design/README.md +43 -0
  23. package/docs/configuration-reference.md +54 -0
  24. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  25. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  26. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  27. package/docs/swagger.md +316 -0
  28. package/examples/controls/1) window/server.js +6 -1
  29. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  30. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  31. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  32. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  33. declarative api/e2e-screenshot-1-name-change.png +0 -0
  34. declarative api/e2e-screenshot-2-toggled.png +0 -0
  35. declarative api/e2e-screenshot-3-final.png +0 -0
  36. declarative api/e2e-screenshot-final.png +0 -0
  37. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  38. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  39. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  40. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  41. package/examples/data-views/01) query-endpoint/server.js +61 -0
  42. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  43. package/labs/website-design/002-pages-storage/check.js +244 -0
  44. package/labs/website-design/002-pages-storage/results.txt +0 -0
  45. package/labs/website-design/003-type-detection/check.js +193 -0
  46. package/labs/website-design/003-type-detection/results.txt +0 -0
  47. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  48. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  49. package/labs/website-design/005-normalize-input/check.js +303 -0
  50. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  51. package/labs/website-design/README.md +34 -0
  52. package/labs/website-design/manifest.json +68 -0
  53. package/labs/website-design/run-all.js +60 -0
  54. package/middleware/json-body.js +126 -0
  55. package/openapi.js +474 -0
  56. package/package.json +11 -8
  57. package/publishers/Publishers.js +6 -5
  58. package/publishers/http-function-publisher.js +135 -126
  59. package/publishers/http-webpage-publisher.js +89 -11
  60. package/publishers/query-publisher.js +116 -0
  61. package/publishers/swagger-publisher.js +203 -0
  62. package/publishers/swagger-ui.js +578 -0
  63. package/resources/adapters/array-adapter.js +143 -0
  64. package/resources/query-resource.js +131 -0
  65. package/serve-factory.js +728 -18
  66. package/server.js +421 -103
  67. package/tests/README.md +23 -1
  68. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  69. package/tests/helpers/playwright-e2e-harness.js +326 -0
  70. package/tests/openapi.test.js +319 -0
  71. package/tests/playwright-smoke.test.js +134 -0
  72. package/tests/publish-enhancements.test.js +673 -0
  73. package/tests/query-publisher.test.js +430 -0
  74. package/tests/quick-json-body-test.js +169 -0
  75. package/tests/serve.test.js +425 -122
  76. package/tests/swagger-publisher.test.js +1076 -0
  77. package/tests/test-runner.js +1 -0
package/tests/README.md CHANGED
@@ -36,11 +36,13 @@ tests/
36
36
  ├── control-optimizer-cache-behavior.test.js # Optimizer cache enable/disable behavior
37
37
  ├── examples-controls.e2e.test.js # Example apps regression (controls)
38
38
  ├── sass-controls.e2e.test.js # Sass/CSS controls E2E coverage
39
+ ├── playwright-smoke.test.js # Playwright browser smoke test for local page serving
39
40
  ├── jsgui3-html-examples.puppeteer.test.js # Puppeteer interaction tests (jsgui3-html examples)
40
41
  ├── bundling-default-control-elimination.puppeteer.test.js # Puppeteer: default control elimination bundle checks
41
42
  ├── window-examples.puppeteer.test.js # Puppeteer interaction tests (window examples)
42
43
  ├── window-resource-integration.puppeteer.test.js # Browser E2E: controls + resource APIs + SSE
43
44
  ├── helpers/puppeteer-e2e-harness.js # Shared Puppeteer story runner + probes
45
+ ├── helpers/playwright-e2e-harness.js # Shared Playwright story runner + probes
44
46
  ├── test-runner.js # Custom test runner with reporting
45
47
  └── README.md # This file
46
48
  ```
@@ -87,6 +89,16 @@ npm run test:puppeteer:bundling
87
89
  npm run test:puppeteer:resources
88
90
  ```
89
91
 
92
+ ### Install Playwright Browser Runtime
93
+ ```bash
94
+ npm run test:playwright:install
95
+ ```
96
+
97
+ ### Run Playwright Smoke Test
98
+ ```bash
99
+ npm run test:playwright:smoke
100
+ ```
101
+
90
102
  ### Run Tests with Options
91
103
  ```bash
92
104
  # Debug mode (enables sourcemaps)
@@ -105,7 +117,8 @@ node tests/test-runner.js --test=end-to-end.test.js --debug
105
117
  2. Run the example regression suite next (HTML/CSS/JS smoke checks).
106
118
  3. Run Puppeteer interaction tests last (heavier, requires a browser).
107
119
  4. Run the resource integration Puppeteer suite when changing resources/SSE APIs.
108
- 5. Run the full suite only when changes are broad or before release.
120
+ 5. Run the Playwright smoke test to validate alternate browser automation coverage.
121
+ 6. Run the full suite only when changes are broad or before release.
109
122
 
110
123
  Suggested sequence:
111
124
  ```bash
@@ -114,6 +127,7 @@ npm run test:examples:controls
114
127
  npm run test:puppeteer:bundling
115
128
  npm run test:puppeteer:windows
116
129
  npm run test:puppeteer:resources
130
+ npm run test:playwright:smoke
117
131
  npm test
118
132
  ```
119
133
 
@@ -133,6 +147,14 @@ Key patterns:
133
147
  - SSE propagation (`/events`) reflected in client UI/debug state.
134
148
  - Keep selectors stable (`id` or `data-test`) so interaction tests remain robust.
135
149
 
150
+ ## Playwright Support
151
+
152
+ Playwright support follows the same server-first test model as Puppeteer:
153
+
154
+ - `tests/playwright-smoke.test.js` validates that Playwright can open a served page and load JS/CSS bundles.
155
+ - `tests/helpers/playwright-e2e-harness.js` mirrors probe/story helpers so new Playwright interaction tests can be added with minimal boilerplate.
156
+ - Use `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` if you need to force a specific Chromium binary.
157
+
136
158
  ## Patterns That Work
137
159
 
138
160
  - Use per-test temporary client files and delete them in `finally`.
@@ -154,7 +154,18 @@ describe('Admin UI jsgui control integration', function() {
154
154
  it('renders settings snapshot with key-value rows and logout button control', async () => {
155
155
  const admin_shell = create_admin_shell();
156
156
  admin_shell._fetch_json = () => Promise.resolve({
157
- server: { name: 'Example Server' },
157
+ server: {
158
+ name: 'Example Server',
159
+ primary_endpoint: 'http://127.0.0.1:52000/',
160
+ listening_endpoints: [
161
+ { url: 'http://127.0.0.1:52000/' },
162
+ { url: 'http://192.168.1.2:52000/' }
163
+ ],
164
+ startup_diagnostics: {
165
+ requested_port: 52000,
166
+ addresses_attempted: ['127.0.0.1', '192.168.1.2']
167
+ }
168
+ },
158
169
  process: {
159
170
  node_version: 'v22.0.0',
160
171
  platform: 'linux',
@@ -169,6 +180,10 @@ describe('Admin UI jsgui control integration', function() {
169
180
  assert(dynamic_html.includes('Example Server'));
170
181
  assert(dynamic_html.includes('v22.0.0'));
171
182
  assert(dynamic_html.includes('as-logout-btn'));
183
+ assert(dynamic_html.includes('Startup & Network'));
184
+ assert(dynamic_html.includes('Primary Endpoint'));
185
+ assert(dynamic_html.includes('127.0.0.1:52000'));
186
+ assert(dynamic_html.includes('Requested Port'));
172
187
  });
173
188
 
174
189
  it('renders custom section data for array, object, and scalar payloads', () => {
@@ -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
+ };