jsgui3-server 0.0.144 → 0.0.146

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 (71) hide show
  1. package/docs/jsgui3-html-improvement-ideas.md +162 -0
  2. package/docs/jsgui3-html-improvement-ideas.svg +151 -0
  3. package/docs/jsgui3-sass-patterns-book/01-vision-and-goals.md +31 -0
  4. package/docs/jsgui3-sass-patterns-book/02-stack-map.md +40 -0
  5. package/docs/jsgui3-sass-patterns-book/03-control-local-sass.md +60 -0
  6. package/docs/jsgui3-sass-patterns-book/04-extension-and-variants.md +76 -0
  7. package/docs/jsgui3-sass-patterns-book/05-theming-and-tokens.md +54 -0
  8. package/docs/jsgui3-sass-patterns-book/06-workspace-overrides.md +45 -0
  9. package/docs/jsgui3-sass-patterns-book/07-resource-and-bundling.md +46 -0
  10. package/docs/jsgui3-sass-patterns-book/08-examples.md +62 -0
  11. package/docs/jsgui3-sass-patterns-book/09-testing-and-adoption.md +27 -0
  12. package/docs/jsgui3-sass-patterns-book/README.md +23 -0
  13. package/docs/troubleshooting.md +44 -4
  14. package/examples/controls/14) window, canvas/client.js +1 -1
  15. package/examples/controls/14b) window, canvas (improved renderer)/client.js +1 -1
  16. package/examples/controls/14d) window, canvas globe/EarthGlobeRenderer.js +19 -14
  17. package/examples/controls/14d) window, canvas globe/client.js +1 -1
  18. package/examples/controls/14d) window, canvas globe/pipeline/TransformStage.js +5 -5
  19. package/examples/controls/14e) window, canvas multithreaded/client.js +1 -1
  20. package/examples/controls/14f) window, canvas polyglobe/client.js +1 -1
  21. package/examples/controls/16) window, form container/client.js +254 -0
  22. package/examples/controls/16) window, form container/server.js +20 -0
  23. package/examples/controls/17) window, mvvm binding/client.js +530 -0
  24. package/examples/controls/17) window, mvvm binding/server.js +20 -0
  25. package/examples/jsgui3-html/01) mvvm-counter/client.js +648 -0
  26. package/examples/jsgui3-html/01) mvvm-counter/server.js +21 -0
  27. package/examples/jsgui3-html/02) date-transform/client.js +764 -0
  28. package/examples/jsgui3-html/02) date-transform/server.js +21 -0
  29. package/examples/jsgui3-html/03) form-validation/client.js +1045 -0
  30. package/examples/jsgui3-html/03) form-validation/server.js +21 -0
  31. package/examples/jsgui3-html/04) data-grid/client.js +738 -0
  32. package/examples/jsgui3-html/04) data-grid/server.js +21 -0
  33. package/examples/jsgui3-html/05) master-detail/client.js +649 -0
  34. package/examples/jsgui3-html/05) master-detail/server.js +21 -0
  35. package/examples/jsgui3-html/06) theming/client.js +514 -0
  36. package/examples/jsgui3-html/06) theming/server.js +21 -0
  37. package/examples/jsgui3-html/07) mixins/client.js +465 -0
  38. package/examples/jsgui3-html/07) mixins/server.js +21 -0
  39. package/examples/jsgui3-html/08) router/client.js +372 -0
  40. package/examples/jsgui3-html/08) router/server.js +21 -0
  41. package/examples/jsgui3-html/09) resource-transform/client.js +692 -0
  42. package/examples/jsgui3-html/09) resource-transform/server.js +21 -0
  43. package/examples/jsgui3-html/10) binding-debugger/client.js +810 -0
  44. package/examples/jsgui3-html/10) binding-debugger/server.js +21 -0
  45. package/examples/jsgui3-html/README.md +48 -0
  46. package/http/responders/static/Static_Route_HTTP_Responder.js +25 -20
  47. package/package.json +3 -3
  48. package/publishers/http-webpageorsite-publisher.js +3 -1
  49. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +87 -100
  50. package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +89 -60
  51. package/serve-factory.js +12 -5
  52. package/server.js +103 -85
  53. package/tests/README.md +52 -9
  54. package/tests/bundlers.test.js +53 -47
  55. package/tests/end-to-end.test.js +336 -365
  56. package/tests/examples-controls.e2e.test.js +15 -1
  57. package/tests/fixtures/end-to-end-client.js +54 -0
  58. package/tests/fixtures/jsgui3-html/binding_debugger_expectations.json +15 -0
  59. package/tests/fixtures/jsgui3-html/counter_expectations.json +31 -0
  60. package/tests/fixtures/jsgui3-html/data_grid_expectations.json +26 -0
  61. package/tests/fixtures/jsgui3-html/date_transform_expectations.json +26 -0
  62. package/tests/fixtures/jsgui3-html/form_validation_expectations.json +27 -0
  63. package/tests/fixtures/jsgui3-html/master_detail_expectations.json +15 -0
  64. package/tests/fixtures/jsgui3-html/mixins_expectations.json +10 -0
  65. package/tests/fixtures/jsgui3-html/resource_transform_expectations.json +11 -0
  66. package/tests/fixtures/jsgui3-html/router_expectations.json +10 -0
  67. package/tests/fixtures/jsgui3-html/theming_expectations.json +10 -0
  68. package/tests/jsgui3-html-examples.puppeteer.test.js +537 -0
  69. package/tests/sass-controls.e2e.test.js +123 -9
  70. package/tests/test-runner.js +1 -0
  71. package/tests/window-examples.puppeteer.test.js +217 -1
@@ -26,6 +26,7 @@ const sass_mix_client_js = [
26
26
  "const jsgui = require('jsgui3-html');",
27
27
  "const { Control } = jsgui;",
28
28
  "const { controls } = jsgui;",
29
+ "const Active_HTML_Document = controls.Active_HTML_Document;",
29
30
  "",
30
31
  "class Sass_Mix_Control extends Control {",
31
32
  " constructor(spec = {}) {",
@@ -51,7 +52,20 @@ const sass_mix_client_js = [
51
52
  "}",
52
53
  "`;",
53
54
  "",
55
+ "class Demo_UI extends Active_HTML_Document {",
56
+ " constructor(spec = {}) {",
57
+ " spec.__type_name = spec.__type_name || 'demo_ui';",
58
+ " super(spec);",
59
+ " const { context } = this;",
60
+ " if (!spec.el) {",
61
+ " const control = new Sass_Mix_Control({ context });",
62
+ " this.body.add(control);",
63
+ " }",
64
+ " }",
65
+ "}",
66
+ "",
54
67
  "controls.Sass_Mix_Control = Sass_Mix_Control;",
68
+ "controls.Demo_UI = Demo_UI;",
55
69
  "module.exports = jsgui;",
56
70
  ""
57
71
  ].join('\n');
@@ -60,6 +74,7 @@ const sass_only_client_js = [
60
74
  "const jsgui = require('jsgui3-html');",
61
75
  "const { Control } = jsgui;",
62
76
  "const { controls } = jsgui;",
77
+ "const Active_HTML_Document = controls.Active_HTML_Document;",
63
78
  "",
64
79
  "class Sass_Only_Control extends Control {",
65
80
  " constructor(spec = {}) {",
@@ -80,7 +95,20 @@ const sass_only_client_js = [
80
95
  " color: #000000",
81
96
  "`;",
82
97
  "",
98
+ "class Demo_UI extends Active_HTML_Document {",
99
+ " constructor(spec = {}) {",
100
+ " spec.__type_name = spec.__type_name || 'demo_ui';",
101
+ " super(spec);",
102
+ " const { context } = this;",
103
+ " if (!spec.el) {",
104
+ " const control = new Sass_Only_Control({ context });",
105
+ " this.body.add(control);",
106
+ " }",
107
+ " }",
108
+ "}",
109
+ "",
83
110
  "controls.Sass_Only_Control = Sass_Only_Control;",
111
+ "controls.Demo_UI = Demo_UI;",
84
112
  "module.exports = jsgui;",
85
113
  ""
86
114
  ].join('\n');
@@ -89,6 +117,7 @@ const sass_css_mix_client_js = [
89
117
  "const jsgui = require('jsgui3-html');",
90
118
  "const { Control } = jsgui;",
91
119
  "const { controls } = jsgui;",
120
+ "const Active_HTML_Document = controls.Active_HTML_Document;",
92
121
  "",
93
122
  "class Sass_Css_Mix_Control extends Control {",
94
123
  " constructor(spec = {}) {",
@@ -111,7 +140,61 @@ const sass_css_mix_client_js = [
111
140
  " color: $mix_color",
112
141
  "`;",
113
142
  "",
143
+ "class Demo_UI extends Active_HTML_Document {",
144
+ " constructor(spec = {}) {",
145
+ " spec.__type_name = spec.__type_name || 'demo_ui';",
146
+ " super(spec);",
147
+ " const { context } = this;",
148
+ " if (!spec.el) {",
149
+ " const control = new Sass_Css_Mix_Control({ context });",
150
+ " this.body.add(control);",
151
+ " }",
152
+ " }",
153
+ "}",
154
+ "",
114
155
  "controls.Sass_Css_Mix_Control = Sass_Css_Mix_Control;",
156
+ "controls.Demo_UI = Demo_UI;",
157
+ "module.exports = jsgui;",
158
+ ""
159
+ ].join('\n');
160
+
161
+ const sass_theme_client_js = [
162
+ "const jsgui = require('jsgui3-html');",
163
+ "const { Control } = jsgui;",
164
+ "const { controls } = jsgui;",
165
+ "const Active_HTML_Document = controls.Active_HTML_Document;",
166
+ "",
167
+ "class Sass_Theme_Control extends Control {",
168
+ " constructor(spec = {}) {",
169
+ " super(spec);",
170
+ " const { context } = this;",
171
+ " if (!spec.el) {",
172
+ " const container = new controls.div({ context, class: 'sass-theme-control' });",
173
+ " this.add(container);",
174
+ " }",
175
+ " }",
176
+ "}",
177
+ "",
178
+ "Sass_Theme_Control.scss = `",
179
+ ".sass-theme-control {",
180
+ " color: var(--accent-color);",
181
+ "}",
182
+ "`;",
183
+ "",
184
+ "class Demo_UI extends Active_HTML_Document {",
185
+ " constructor(spec = {}) {",
186
+ " spec.__type_name = spec.__type_name || 'demo_ui';",
187
+ " super(spec);",
188
+ " const { context } = this;",
189
+ " if (!spec.el) {",
190
+ " const control = new Sass_Theme_Control({ context });",
191
+ " this.body.add(control);",
192
+ " }",
193
+ " }",
194
+ "}",
195
+ "",
196
+ "controls.Sass_Theme_Control = Sass_Theme_Control;",
197
+ "controls.Demo_UI = Demo_UI;",
115
198
  "module.exports = jsgui;",
116
199
  ""
117
200
  ].join('\n');
@@ -167,7 +250,17 @@ const remove_file_if_exists = async (file_path) => {
167
250
  }
168
251
  };
169
252
 
170
- const start_test_server = async ({ client_path, control_name }) => {
253
+ const merge_style_config = (overrides = {}) => {
254
+ const merged = Object.assign({}, style_config, overrides);
255
+ merged.sourcemaps = Object.assign(
256
+ {},
257
+ style_config.sourcemaps || {},
258
+ overrides.sourcemaps || {}
259
+ );
260
+ return merged;
261
+ };
262
+
263
+ const start_test_server = async ({ client_path, control_name, style_overrides }) => {
171
264
  delete require.cache[require.resolve(client_path)];
172
265
  const client_module = require(client_path);
173
266
  const ctrl = client_module.controls && client_module.controls[control_name];
@@ -178,7 +271,7 @@ const start_test_server = async ({ client_path, control_name }) => {
178
271
  src_path_client_js: client_path,
179
272
  name: `tests/${control_name}`,
180
273
  debug: true,
181
- style: style_config
274
+ style: merge_style_config(style_overrides)
182
275
  });
183
276
 
184
277
  server.allowed_addresses = ['127.0.0.1'];
@@ -228,7 +321,7 @@ describe('Sass/CSS Control E2E Tests', function() {
228
321
  const client_path = await write_temp_client_file('temp_sass_mix_client.js', sass_mix_client_js);
229
322
  const { server, port } = await start_test_server({
230
323
  client_path,
231
- control_name: 'Sass_Mix_Control'
324
+ control_name: 'Demo_UI'
232
325
  });
233
326
 
234
327
  try {
@@ -266,7 +359,7 @@ describe('Sass/CSS Control E2E Tests', function() {
266
359
  const client_path = await write_temp_client_file('temp_sass_only_client.js', sass_only_client_js);
267
360
  const { server, port } = await start_test_server({
268
361
  client_path,
269
- control_name: 'Sass_Only_Control'
362
+ control_name: 'Demo_UI'
270
363
  });
271
364
 
272
365
  try {
@@ -280,10 +373,7 @@ describe('Sass/CSS Control E2E Tests', function() {
280
373
  assert(css_text.includes('color: #336699'), 'Expected Sass variable compilation');
281
374
  assert(css_text.includes('.sass-only-control:hover'), 'Expected nested Sass selector output');
282
375
 
283
- const css_sourcemap = extract_inline_sourcemap(css_text);
284
- assert(Array.isArray(css_sourcemap.sources), 'Expected sourcemap sources array');
285
- assert(Array.isArray(css_sourcemap.sourcesContent), 'Expected sourcemap sourcesContent array');
286
- assert(sourcemap_contains(css_sourcemap, '$primary_color'), 'Expected sourcemap to include Sass source content');
376
+ assert(!css_text.includes('/*# sourceMappingURL='), 'Expected no inline sourcemap with multiple style segments');
287
377
 
288
378
  const js_response = await make_request(`${base_url}/js/js.js`);
289
379
  assert.strictEqual(js_response.status_code, 200);
@@ -298,7 +388,7 @@ describe('Sass/CSS Control E2E Tests', function() {
298
388
  const client_path = await write_temp_client_file('temp_sass_css_mix_client.js', sass_css_mix_client_js);
299
389
  const { server, port } = await start_test_server({
300
390
  client_path,
301
- control_name: 'Sass_Css_Mix_Control'
391
+ control_name: 'Demo_UI'
302
392
  });
303
393
 
304
394
  try {
@@ -324,4 +414,28 @@ describe('Sass/CSS Control E2E Tests', function() {
324
414
  await remove_file_if_exists(client_path);
325
415
  }
326
416
  });
417
+
418
+ it('should apply scss_sources overrides from server config', async function() {
419
+ const client_path = await write_temp_client_file('temp_sass_theme_client.js', sass_theme_client_js);
420
+ const { server, port } = await start_test_server({
421
+ client_path,
422
+ control_name: 'Demo_UI',
423
+ style_overrides: {
424
+ scss_sources: [':root { --accent-color: #00aa88; }']
425
+ }
426
+ });
427
+
428
+ try {
429
+ const base_url = `http://127.0.0.1:${port}`;
430
+ const css_response = await make_request(`${base_url}/css/css.css`);
431
+ assert.strictEqual(css_response.status_code, 200);
432
+ assert((css_response.headers['content-type'] || '').includes('css'), 'Expected CSS content-type');
433
+
434
+ const css_text = css_response.body;
435
+ assert(css_text.includes('--accent-color: #00aa88'), 'Expected scss_sources to compile into CSS');
436
+ } finally {
437
+ await close_server(server);
438
+ await remove_file_if_exists(client_path);
439
+ }
440
+ });
327
441
  });
@@ -33,6 +33,7 @@ class TestRunner {
33
33
  'error-handling.test.js',
34
34
  'examples-controls.e2e.test.js',
35
35
  'sass-controls.e2e.test.js',
36
+ 'jsgui3-html-examples.puppeteer.test.js',
36
37
  'window-examples.puppeteer.test.js'
37
38
  ];
38
39
  }
@@ -1,4 +1,5 @@
1
1
  const assert = require('assert');
2
+ const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { describe, it, before, after } = require('mocha');
4
5
 
@@ -7,6 +8,7 @@ const { get_free_port } = require('../port-utils');
7
8
 
8
9
  const repo_root_path = path.join(__dirname, '..');
9
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');
10
12
 
11
13
  let puppeteer;
12
14
  let browser;
@@ -65,11 +67,38 @@ const open_example_page = async (port) => {
65
67
  await page.setViewport({ width: 1280, height: 720 });
66
68
 
67
69
  const base_url = `http://127.0.0.1:${port}/`;
68
- await page.goto(base_url, { waitUntil: 'domcontentloaded' });
70
+ await page.goto(base_url, { waitUntil: 'load' });
69
71
 
70
72
  return page;
71
73
  };
72
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
+
73
102
  const wait_for_class_state = async (page, selector, class_name, expected) => {
74
103
  await page.waitForFunction(
75
104
  (selector_arg, class_arg, expected_arg) => {
@@ -85,6 +114,46 @@ const wait_for_class_state = async (page, selector, class_name, expected) => {
85
114
  );
86
115
  };
87
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
+
88
157
  const get_tab_page_display = async (page) => {
89
158
  return page.evaluate(() => {
90
159
  return Array.from(document.querySelectorAll('.tab-page')).map((page_el) => {
@@ -137,11 +206,158 @@ describe('Window Examples Puppeteer Tests', function () {
137
206
  const button_handles = await page.$$('.window .title.bar button.button');
138
207
  assert.strictEqual(button_handles.length, 3, 'Expected minimize/maximize/close buttons');
139
208
 
209
+ await capture_screenshot(page, '1) window', 'before_minimize');
140
210
  await button_handles[0].click();
141
211
  await wait_for_class_state(page, '.window', 'minimized', true);
212
+ await capture_screenshot(page, '1) window', 'after_minimize');
142
213
 
143
214
  await button_handles[0].click();
144
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');
145
361
  } finally {
146
362
  if (page) {
147
363
  await page.close();