jsgui3-server 0.0.143 → 0.0.145

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 (67) hide show
  1. package/docs/comprehensive-documentation.md +25 -6
  2. package/docs/configuration-reference.md +46 -11
  3. package/docs/controls-development.md +54 -26
  4. package/docs/jsgui3-html-improvement-ideas.md +162 -0
  5. package/docs/jsgui3-html-improvement-ideas.svg +151 -0
  6. package/docs/troubleshooting.md +9 -8
  7. package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +19 -14
  8. package/examples/controls/14d) window, canvas globe/pipeline/TransformStage.js +5 -5
  9. package/examples/jsgui3-html/01) mvvm-counter/client.js +648 -0
  10. package/examples/jsgui3-html/01) mvvm-counter/server.js +21 -0
  11. package/examples/jsgui3-html/02) date-transform/client.js +764 -0
  12. package/examples/jsgui3-html/02) date-transform/server.js +21 -0
  13. package/examples/jsgui3-html/03) form-validation/client.js +1045 -0
  14. package/examples/jsgui3-html/03) form-validation/server.js +21 -0
  15. package/examples/jsgui3-html/04) data-grid/client.js +738 -0
  16. package/examples/jsgui3-html/04) data-grid/server.js +21 -0
  17. package/examples/jsgui3-html/05) master-detail/client.js +649 -0
  18. package/examples/jsgui3-html/05) master-detail/server.js +21 -0
  19. package/examples/jsgui3-html/06) theming/client.js +514 -0
  20. package/examples/jsgui3-html/06) theming/server.js +21 -0
  21. package/examples/jsgui3-html/07) mixins/client.js +465 -0
  22. package/examples/jsgui3-html/07) mixins/server.js +21 -0
  23. package/examples/jsgui3-html/08) router/client.js +372 -0
  24. package/examples/jsgui3-html/08) router/server.js +21 -0
  25. package/examples/jsgui3-html/09) resource-transform/client.js +692 -0
  26. package/examples/jsgui3-html/09) resource-transform/server.js +21 -0
  27. package/examples/jsgui3-html/10) binding-debugger/client.js +810 -0
  28. package/examples/jsgui3-html/10) binding-debugger/server.js +21 -0
  29. package/examples/jsgui3-html/README.md +48 -0
  30. package/http/responders/static/Static_Route_HTTP_Responder.js +25 -20
  31. package/lab/README.md +19 -0
  32. package/lab/experiments/window_examples_dom_audit.js +241 -0
  33. package/lab/results/window_examples_dom_audit.json +131 -0
  34. package/lab/results/window_examples_dom_audit.md +46 -0
  35. package/package.json +8 -3
  36. package/publishers/http-webpageorsite-publisher.js +8 -4
  37. package/resources/processors/bundlers/css-bundler.js +28 -173
  38. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +32 -20
  39. package/resources/processors/bundlers/style-bundler.js +288 -0
  40. package/resources/processors/compilers/SASS_Compiler.js +88 -0
  41. package/resources/processors/extractors/js/css_and_js/AST_Node/CSS_And_JS_From_JS_String_Using_AST_Node_Extractor.js +64 -68
  42. package/resources/website-css-resource.js +24 -20
  43. package/resources/website-javascript-resource-processor.js +17 -57
  44. package/resources/website-javascript-resource.js +17 -57
  45. package/serve-factory.js +38 -24
  46. package/server.js +116 -92
  47. package/tests/README.md +38 -3
  48. package/tests/bundlers.test.js +41 -32
  49. package/tests/content-analysis.test.js +19 -18
  50. package/tests/end-to-end.test.js +336 -365
  51. package/tests/error-handling.test.js +13 -11
  52. package/tests/examples-controls.e2e.test.js +13 -1
  53. package/tests/fixtures/end-to-end-client.js +54 -0
  54. package/tests/fixtures/jsgui3-html/binding_debugger_expectations.json +15 -0
  55. package/tests/fixtures/jsgui3-html/counter_expectations.json +31 -0
  56. package/tests/fixtures/jsgui3-html/data_grid_expectations.json +26 -0
  57. package/tests/fixtures/jsgui3-html/date_transform_expectations.json +26 -0
  58. package/tests/fixtures/jsgui3-html/form_validation_expectations.json +27 -0
  59. package/tests/fixtures/jsgui3-html/master_detail_expectations.json +15 -0
  60. package/tests/fixtures/jsgui3-html/mixins_expectations.json +10 -0
  61. package/tests/fixtures/jsgui3-html/resource_transform_expectations.json +11 -0
  62. package/tests/fixtures/jsgui3-html/router_expectations.json +10 -0
  63. package/tests/fixtures/jsgui3-html/theming_expectations.json +10 -0
  64. package/tests/jsgui3-html-examples.puppeteer.test.js +537 -0
  65. package/tests/sass-controls.e2e.test.js +327 -0
  66. package/tests/test-runner.js +4 -1
  67. package/tests/window-examples.puppeteer.test.js +455 -0
@@ -0,0 +1,455 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { describe, it, before, after } = require('mocha');
5
+
6
+ const Server = require('../server');
7
+ const { get_free_port } = require('../port-utils');
8
+
9
+ const repo_root_path = path.join(__dirname, '..');
10
+ const examples_controls_root_path = path.join(repo_root_path, 'examples', 'controls');
11
+ const screenshots_root_path = path.join(repo_root_path, 'tests', 'screenshots', 'windows');
12
+
13
+ let puppeteer;
14
+ let browser;
15
+
16
+ const launch_puppeteer_browser = async () => {
17
+ const launch_options = {
18
+ headless: true,
19
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
20
+ };
21
+
22
+ if (process.env.PUPPETEER_EXECUTABLE_PATH) {
23
+ launch_options.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
24
+ }
25
+
26
+ return puppeteer.launch(launch_options);
27
+ };
28
+
29
+ const start_example_server = async ({ dir_name, ctrl_name }) => {
30
+ const example_dir_path = path.join(examples_controls_root_path, dir_name);
31
+ const example_client_path = path.join(example_dir_path, 'client.js');
32
+
33
+ const jsgui = require(example_client_path);
34
+ const ctrl = jsgui.controls && jsgui.controls[ctrl_name];
35
+ assert(ctrl, `Missing exported control jsgui.controls.${ctrl_name} in ${example_client_path}`);
36
+
37
+ const server = new Server({
38
+ Ctrl: ctrl,
39
+ src_path_client_js: example_client_path,
40
+ name: `examples/controls/${dir_name}`
41
+ });
42
+
43
+ server.allowed_addresses = ['127.0.0.1'];
44
+
45
+ await new Promise((resolve, reject) => {
46
+ const timeout = setTimeout(() => reject(new Error('Publisher ready timeout')), 60000);
47
+ server.on('ready', () => {
48
+ clearTimeout(timeout);
49
+ resolve();
50
+ });
51
+ });
52
+
53
+ const port = await get_free_port();
54
+ await new Promise((resolve, reject) => {
55
+ server.start(port, (err) => (err ? reject(err) : resolve()));
56
+ });
57
+
58
+ return { server, port };
59
+ };
60
+
61
+ const stop_example_server = async (server) => {
62
+ await new Promise((resolve) => server.close(resolve));
63
+ };
64
+
65
+ const open_example_page = async (port) => {
66
+ const page = await browser.newPage();
67
+ await page.setViewport({ width: 1280, height: 720 });
68
+
69
+ const base_url = `http://127.0.0.1:${port}/`;
70
+ await page.goto(base_url, { waitUntil: 'load' });
71
+
72
+ return page;
73
+ };
74
+
75
+ const normalize_screenshot_label = (value) => {
76
+ return String(value)
77
+ .toLowerCase()
78
+ .replace(/[^a-z0-9]+/g, '_')
79
+ .replace(/^_+|_+$/g, '');
80
+ };
81
+
82
+ const build_screenshot_name = (story_name, step_name) => {
83
+ const story_label = normalize_screenshot_label(story_name);
84
+ const step_label = normalize_screenshot_label(step_name);
85
+ return `${story_label}__${step_label}`;
86
+ };
87
+
88
+ const ensure_screenshots_dir = () => {
89
+ fs.mkdirSync(screenshots_root_path, { recursive: true });
90
+ };
91
+
92
+ const capture_screenshot = async (page, story_name, step_name) => {
93
+ ensure_screenshots_dir();
94
+ const screenshot_name = build_screenshot_name(story_name, step_name);
95
+ const file_path = path.join(screenshots_root_path, `${screenshot_name}.png`);
96
+ await page.screenshot({ path: file_path, fullPage: true });
97
+ const stats = fs.statSync(file_path);
98
+ assert.ok(stats.size > 0, `Expected screenshot to be written: ${file_path}`);
99
+ return file_path;
100
+ };
101
+
102
+ const wait_for_class_state = async (page, selector, class_name, expected) => {
103
+ await page.waitForFunction(
104
+ (selector_arg, class_arg, expected_arg) => {
105
+ const element = document.querySelector(selector_arg);
106
+ if (!element) return false;
107
+ const has_class = element.classList.contains(class_arg);
108
+ return expected_arg ? has_class : !has_class;
109
+ },
110
+ {},
111
+ selector,
112
+ class_name,
113
+ expected
114
+ );
115
+ };
116
+
117
+ const get_window_bcr = async (page, selector = '.window') => {
118
+ return page.$eval(selector, (el) => {
119
+ const rect = el.getBoundingClientRect();
120
+ return {
121
+ left: rect.left,
122
+ top: rect.top,
123
+ width: rect.width,
124
+ height: rect.height,
125
+ right: rect.right,
126
+ bottom: rect.bottom
127
+ };
128
+ });
129
+ };
130
+
131
+ const drag_window_by = async (page, selector, delta_x, delta_y) => {
132
+ const handle = await page.$(selector);
133
+ assert(handle, `Missing drag handle for selector: ${selector}`);
134
+ const box = await handle.boundingBox();
135
+ assert(box, `Missing bounding box for selector: ${selector}`);
136
+ const start_x = box.x + box.width / 2;
137
+ const start_y = box.y + box.height / 2;
138
+ await page.mouse.move(start_x, start_y);
139
+ await page.mouse.down();
140
+ await page.mouse.move(start_x + delta_x, start_y + delta_y, { steps: 10 });
141
+ await page.mouse.up();
142
+ };
143
+
144
+ const resize_window_by = async (page, selector, delta_x, delta_y) => {
145
+ const handle = await page.$(selector);
146
+ assert(handle, `Missing resize handle for selector: ${selector}`);
147
+ const box = await handle.boundingBox();
148
+ assert(box, `Missing bounding box for selector: ${selector}`);
149
+ const start_x = box.x + box.width / 2;
150
+ const start_y = box.y + box.height / 2;
151
+ await page.mouse.move(start_x, start_y);
152
+ await page.mouse.down();
153
+ await page.mouse.move(start_x + delta_x, start_y + delta_y, { steps: 10 });
154
+ await page.mouse.up();
155
+ };
156
+
157
+ const get_tab_page_display = async (page) => {
158
+ return page.evaluate(() => {
159
+ return Array.from(document.querySelectorAll('.tab-page')).map((page_el) => {
160
+ return window.getComputedStyle(page_el).display;
161
+ });
162
+ });
163
+ };
164
+
165
+ const get_checkbox_state = async (page) => {
166
+ return page.$eval('.checkbox input[type="checkbox"]', (el) => el.checked);
167
+ };
168
+
169
+ describe('Window Examples Puppeteer Tests', function () {
170
+ this.timeout(180000);
171
+
172
+ before(async function () {
173
+ this.timeout(60000);
174
+ try {
175
+ puppeteer = require('puppeteer');
176
+ } catch (error) {
177
+ this.skip();
178
+ return;
179
+ }
180
+
181
+ try {
182
+ browser = await launch_puppeteer_browser();
183
+ } catch (error) {
184
+ this.skip();
185
+ }
186
+ });
187
+
188
+ after(async function () {
189
+ if (browser) {
190
+ await browser.close();
191
+ browser = null;
192
+ }
193
+ });
194
+
195
+ it('minimize button toggles window state in "1) window"', async function () {
196
+ const { server, port } = await start_example_server({
197
+ dir_name: '1) window',
198
+ ctrl_name: 'Demo_UI'
199
+ });
200
+
201
+ let page;
202
+ try {
203
+ page = await open_example_page(port);
204
+ await page.waitForSelector('.window .title.bar');
205
+
206
+ const button_handles = await page.$$('.window .title.bar button.button');
207
+ assert.strictEqual(button_handles.length, 3, 'Expected minimize/maximize/close buttons');
208
+
209
+ await capture_screenshot(page, '1) window', 'before_minimize');
210
+ await button_handles[0].click();
211
+ await wait_for_class_state(page, '.window', 'minimized', true);
212
+ await capture_screenshot(page, '1) window', 'after_minimize');
213
+
214
+ await button_handles[0].click();
215
+ await wait_for_class_state(page, '.window', 'minimized', false);
216
+ await capture_screenshot(page, '1) window', 'after_restore');
217
+ } finally {
218
+ if (page) {
219
+ await page.close();
220
+ }
221
+ await stop_example_server(server);
222
+ }
223
+ });
224
+
225
+ it('maximize button toggles window state in "1) window"', async function () {
226
+ const { server, port } = await start_example_server({
227
+ dir_name: '1) window',
228
+ ctrl_name: 'Demo_UI'
229
+ });
230
+
231
+ let page;
232
+ try {
233
+ page = await open_example_page(port);
234
+ await page.waitForSelector('.window .title.bar');
235
+
236
+ const button_handles = await page.$$('.window .title.bar button.button');
237
+ assert.strictEqual(button_handles.length, 3, 'Expected minimize/maximize/close buttons');
238
+
239
+ await capture_screenshot(page, '1) window', 'before_maximize');
240
+ await button_handles[1].click();
241
+ await wait_for_class_state(page, '.window', 'maximized', true);
242
+ await capture_screenshot(page, '1) window', 'after_maximize');
243
+
244
+ await button_handles[1].click();
245
+ await wait_for_class_state(page, '.window', 'maximized', false);
246
+ await capture_screenshot(page, '1) window', 'after_unmaximize');
247
+ } finally {
248
+ if (page) {
249
+ await page.close();
250
+ }
251
+ await stop_example_server(server);
252
+ }
253
+ });
254
+
255
+ it('dragging the title bar moves the window in "1) window"', async function () {
256
+ const { server, port } = await start_example_server({
257
+ dir_name: '1) window',
258
+ ctrl_name: 'Demo_UI'
259
+ });
260
+
261
+ let page;
262
+ try {
263
+ page = await open_example_page(port);
264
+ await page.waitForSelector('.window .title.bar');
265
+
266
+ const initial_bcr = await get_window_bcr(page);
267
+ await capture_screenshot(page, '1) window', 'before_drag');
268
+
269
+ await drag_window_by(page, '.window .title.bar', 120, 80);
270
+ await page.waitForTimeout(150);
271
+
272
+ const moved_bcr = await get_window_bcr(page);
273
+ assert.ok(
274
+ moved_bcr.left > initial_bcr.left + 10,
275
+ `Expected window to move right (from ${initial_bcr.left} to ${moved_bcr.left})`
276
+ );
277
+ assert.ok(
278
+ moved_bcr.top > initial_bcr.top + 10,
279
+ `Expected window to move down (from ${initial_bcr.top} to ${moved_bcr.top})`
280
+ );
281
+ await capture_screenshot(page, '1) window', 'after_drag');
282
+ } finally {
283
+ if (page) {
284
+ await page.close();
285
+ }
286
+ await stop_example_server(server);
287
+ }
288
+ });
289
+
290
+ it('resizing updates window bounds in "1) window"', async function () {
291
+ const { server, port } = await start_example_server({
292
+ dir_name: '1) window',
293
+ ctrl_name: 'Demo_UI'
294
+ });
295
+
296
+ let page;
297
+ try {
298
+ page = await open_example_page(port);
299
+ await page.waitForSelector('.window .bottom-right.resize-handle');
300
+
301
+ const initial_bcr = await get_window_bcr(page);
302
+ await capture_screenshot(page, '1) window', 'before_resize');
303
+
304
+ await resize_window_by(page, '.window .bottom-right.resize-handle', 120, 80);
305
+ await page.waitForTimeout(150);
306
+
307
+ const resized_bcr = await get_window_bcr(page);
308
+ assert.ok(
309
+ resized_bcr.width > initial_bcr.width + 20,
310
+ `Expected width to increase (from ${initial_bcr.width} to ${resized_bcr.width})`
311
+ );
312
+ assert.ok(
313
+ resized_bcr.height > initial_bcr.height + 20,
314
+ `Expected height to increase (from ${initial_bcr.height} to ${resized_bcr.height})`
315
+ );
316
+ await capture_screenshot(page, '1) window', 'after_resize');
317
+ } finally {
318
+ if (page) {
319
+ await page.close();
320
+ }
321
+ await stop_example_server(server);
322
+ }
323
+ });
324
+
325
+ it('focus and close behaviors work in "2) two windows"', async function () {
326
+ const { server, port } = await start_example_server({
327
+ dir_name: '2) two windows',
328
+ ctrl_name: 'Demo_UI'
329
+ });
330
+
331
+ let page;
332
+ try {
333
+ page = await open_example_page(port);
334
+ await page.waitForSelector('.window .title.bar');
335
+
336
+ const window_handles = await page.$$('.window');
337
+ assert.strictEqual(window_handles.length, 2, 'Expected two windows');
338
+
339
+ await capture_screenshot(page, '2) two windows', 'initial');
340
+
341
+ const title_handle = await window_handles[1].$('.title.bar');
342
+ assert(title_handle, 'Missing title bar for second window');
343
+ await title_handle.click();
344
+
345
+ await page.waitForFunction(() => {
346
+ const windows = document.querySelectorAll('.window');
347
+ if (windows.length < 2) return false;
348
+ const z_first = parseInt(window.getComputedStyle(windows[0]).zIndex, 10) || 0;
349
+ const z_second = parseInt(window.getComputedStyle(windows[1]).zIndex, 10) || 0;
350
+ return z_second > z_first;
351
+ });
352
+
353
+ await capture_screenshot(page, '2) two windows', 'after_focus');
354
+
355
+ const button_handles = await window_handles[1].$$('.title.bar button.button');
356
+ assert.strictEqual(button_handles.length, 3, 'Expected minimize/maximize/close buttons');
357
+ await button_handles[2].click();
358
+
359
+ await page.waitForFunction(() => document.querySelectorAll('.window').length === 1);
360
+ await capture_screenshot(page, '2) two windows', 'after_close');
361
+ } finally {
362
+ if (page) {
363
+ await page.close();
364
+ }
365
+ await stop_example_server(server);
366
+ }
367
+ });
368
+
369
+ it('tab switching updates visible page in "4) window, tabbed panel"', async function () {
370
+ const { server, port } = await start_example_server({
371
+ dir_name: '4) window, tabbed panel',
372
+ ctrl_name: 'Demo_UI'
373
+ });
374
+
375
+ let page;
376
+ try {
377
+ page = await open_example_page(port);
378
+ await page.waitForSelector('.tab-container');
379
+
380
+ const tab_labels = await page.$$('.tab-label');
381
+ assert.strictEqual(tab_labels.length, 2, 'Expected two tab labels');
382
+
383
+ const display_before = await get_tab_page_display(page);
384
+ assert.notStrictEqual(display_before[0], 'none', 'First tab should be visible');
385
+ assert.strictEqual(display_before[1], 'none', 'Second tab should be hidden initially');
386
+
387
+ await tab_labels[1].click();
388
+ const display_after = await get_tab_page_display(page);
389
+ assert.strictEqual(display_after[0], 'none', 'First tab should be hidden after switch');
390
+ assert.notStrictEqual(display_after[1], 'none', 'Second tab should be visible after switch');
391
+ } finally {
392
+ if (page) {
393
+ await page.close();
394
+ }
395
+ await stop_example_server(server);
396
+ }
397
+ });
398
+
399
+ it('checkbox label toggles input in "8) window, checkbox/a)"', async function () {
400
+ const { server, port } = await start_example_server({
401
+ dir_name: '8) window, checkbox/a)',
402
+ ctrl_name: 'Demo_UI'
403
+ });
404
+
405
+ let page;
406
+ try {
407
+ page = await open_example_page(port);
408
+ await page.waitForSelector('.checkbox input[type="checkbox"]');
409
+
410
+ const label_text = await page.$eval('.checkbox label', (el) => el.textContent.trim());
411
+ assert.strictEqual(label_text, 'A checkbox');
412
+
413
+ const initial_checked = await get_checkbox_state(page);
414
+ assert.strictEqual(initial_checked, false, 'Checkbox should start unchecked');
415
+
416
+ await page.click('.checkbox label');
417
+ await page.waitForFunction(() => {
418
+ const el = document.querySelector('.checkbox input[type="checkbox"]');
419
+ return el && el.checked === true;
420
+ });
421
+
422
+ await page.click('.checkbox label');
423
+ await page.waitForFunction(() => {
424
+ const el = document.querySelector('.checkbox input[type="checkbox"]');
425
+ return el && el.checked === false;
426
+ });
427
+ } finally {
428
+ if (page) {
429
+ await page.close();
430
+ }
431
+ await stop_example_server(server);
432
+ }
433
+ });
434
+
435
+ it('date picker renders native input in "9) window, date picker"', async function () {
436
+ const { server, port } = await start_example_server({
437
+ dir_name: '9) window, date picker',
438
+ ctrl_name: 'Demo_UI'
439
+ });
440
+
441
+ let page;
442
+ try {
443
+ page = await open_example_page(port);
444
+ await page.waitForSelector('input.date-picker[type="date"]');
445
+
446
+ const input_type = await page.$eval('input.date-picker', (el) => el.getAttribute('type'));
447
+ assert.strictEqual(input_type, 'date', 'Expected native date input');
448
+ } finally {
449
+ if (page) {
450
+ await page.close();
451
+ }
452
+ await stop_example_server(server);
453
+ }
454
+ });
455
+ });