xbintsc 0.3.25 → 0.3.35

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 (154) hide show
  1. package/README.md +33 -0
  2. package/dist/src/cli/main.js +124 -30
  3. package/dist/src/cli/main.js.map +1 -1
  4. package/dist/src/codegen/generator/calls/access.js +8 -1
  5. package/dist/src/codegen/generator/calls/access.js.map +1 -1
  6. package/dist/src/codegen/generator/state.js +20 -0
  7. package/dist/src/codegen/generator/state.js.map +1 -1
  8. package/dist/src/codegen/generator/tables.d.ts +6 -0
  9. package/dist/src/codegen/generator/tables.js +38 -0
  10. package/dist/src/codegen/generator/tables.js.map +1 -1
  11. package/dist/src/driver/bundler/commonjs.d.ts +52 -0
  12. package/dist/src/driver/bundler/commonjs.js +493 -0
  13. package/dist/src/driver/bundler/commonjs.js.map +1 -0
  14. package/dist/src/driver/bundler/graph.js +14 -0
  15. package/dist/src/driver/bundler/graph.js.map +1 -1
  16. package/dist/src/driver/bundler/merge.js +14 -0
  17. package/dist/src/driver/bundler/merge.js.map +1 -1
  18. package/dist/src/driver/bundler/types.d.ts +8 -0
  19. package/dist/src/driver/compiler.d.ts +8 -1
  20. package/dist/src/driver/compiler.js +73 -5
  21. package/dist/src/driver/compiler.js.map +1 -1
  22. package/dist/src/driver/config.d.ts +53 -0
  23. package/dist/src/driver/config.js +141 -0
  24. package/dist/src/driver/config.js.map +1 -0
  25. package/dist/src/driver/icon.d.ts +62 -0
  26. package/dist/src/driver/icon.js +151 -0
  27. package/dist/src/driver/icon.js.map +1 -0
  28. package/dist/src/driver/index.d.ts +4 -0
  29. package/dist/src/driver/index.js +4 -0
  30. package/dist/src/driver/index.js.map +1 -1
  31. package/dist/src/driver/mac-bundle.d.ts +41 -0
  32. package/dist/src/driver/mac-bundle.js +123 -0
  33. package/dist/src/driver/mac-bundle.js.map +1 -0
  34. package/dist/src/driver/win-icon.d.ts +27 -0
  35. package/dist/src/driver/win-icon.js +80 -0
  36. package/dist/src/driver/win-icon.js.map +1 -0
  37. package/dist/src/parser/statements.js +17 -4
  38. package/dist/src/parser/statements.js.map +1 -1
  39. package/dist/tests/cli/main.test.js +46 -1
  40. package/dist/tests/cli/main.test.js.map +1 -1
  41. package/dist/tests/driver/config.test.d.ts +1 -0
  42. package/dist/tests/driver/config.test.js +124 -0
  43. package/dist/tests/driver/config.test.js.map +1 -0
  44. package/dist/tests/driver/icon.test.d.ts +1 -0
  45. package/dist/tests/driver/icon.test.js +421 -0
  46. package/dist/tests/driver/icon.test.js.map +1 -0
  47. package/dist/tests/driver/modules.test.js +109 -0
  48. package/dist/tests/driver/modules.test.js.map +1 -1
  49. package/dist/tests/e2e/fetch.test.d.ts +12 -0
  50. package/dist/tests/e2e/fetch.test.js +272 -0
  51. package/dist/tests/e2e/fetch.test.js.map +1 -0
  52. package/dist/tests/e2e/gui-dom.test.d.ts +5 -0
  53. package/dist/tests/e2e/gui-dom.test.js +102 -0
  54. package/dist/tests/e2e/gui-dom.test.js.map +1 -0
  55. package/dist/tests/e2e/gui-events.test.d.ts +5 -0
  56. package/dist/tests/e2e/gui-events.test.js +60 -0
  57. package/dist/tests/e2e/gui-events.test.js.map +1 -0
  58. package/dist/tests/e2e/gui-helpers.d.ts +31 -0
  59. package/dist/tests/e2e/gui-helpers.js +74 -0
  60. package/dist/tests/e2e/gui-helpers.js.map +1 -0
  61. package/dist/tests/e2e/gui-layout.test.d.ts +4 -0
  62. package/dist/tests/e2e/gui-layout.test.js +88 -0
  63. package/dist/tests/e2e/gui-layout.test.js.map +1 -0
  64. package/dist/tests/e2e/gui-lifecycle.test.d.ts +5 -0
  65. package/dist/tests/e2e/gui-lifecycle.test.js +58 -0
  66. package/dist/tests/e2e/gui-lifecycle.test.js.map +1 -0
  67. package/dist/tests/e2e/gui-paint.test.d.ts +5 -0
  68. package/dist/tests/e2e/gui-paint.test.js +111 -0
  69. package/dist/tests/e2e/gui-paint.test.js.map +1 -0
  70. package/dist/tests/e2e/gui-script.test.d.ts +5 -0
  71. package/dist/tests/e2e/gui-script.test.js +78 -0
  72. package/dist/tests/e2e/gui-script.test.js.map +1 -0
  73. package/dist/tests/e2e/gui-style.test.d.ts +5 -0
  74. package/dist/tests/e2e/gui-style.test.js +175 -0
  75. package/dist/tests/e2e/gui-style.test.js.map +1 -0
  76. package/dist/tests/e2e/icon.test.d.ts +7 -0
  77. package/dist/tests/e2e/icon.test.js +80 -0
  78. package/dist/tests/e2e/icon.test.js.map +1 -0
  79. package/dist/tests/e2e/npm-modules.test.d.ts +4 -1
  80. package/dist/tests/e2e/npm-modules.test.js +47 -1
  81. package/dist/tests/e2e/npm-modules.test.js.map +1 -1
  82. package/dist/tests/e2e/runtime-coverage-typed-array.test.d.ts +9 -0
  83. package/dist/tests/e2e/runtime-coverage-typed-array.test.js +140 -0
  84. package/dist/tests/e2e/runtime-coverage-typed-array.test.js.map +1 -0
  85. package/dist/tests/parser/statements.test.js +11 -0
  86. package/dist/tests/parser/statements.test.js.map +1 -1
  87. package/package.json +1 -1
  88. package/runtime/ext_gui/css.cpp +4 -517
  89. package/runtime/ext_gui/css_internal.h +27 -0
  90. package/runtime/ext_gui/css_match.cpp +195 -0
  91. package/runtime/ext_gui/css_selector.cpp +207 -0
  92. package/runtime/ext_gui/css_sheet.cpp +129 -0
  93. package/runtime/ext_gui/dom.cpp +7 -403
  94. package/runtime/ext_gui/dom_api.cpp +63 -1146
  95. package/runtime/ext_gui/dom_api_document.cpp +96 -0
  96. package/runtime/ext_gui/dom_api_events.cpp +123 -0
  97. package/runtime/ext_gui/dom_api_internal.h +135 -0
  98. package/runtime/ext_gui/dom_api_node.cpp +329 -0
  99. package/runtime/ext_gui/dom_api_node_read.cpp +294 -0
  100. package/runtime/ext_gui/dom_api_proto.cpp +130 -0
  101. package/runtime/ext_gui/dom_api_style.cpp +192 -0
  102. package/runtime/ext_gui/dom_internal.h +39 -0
  103. package/runtime/ext_gui/dom_parse.cpp +348 -0
  104. package/runtime/ext_gui/dom_serialize.cpp +54 -0
  105. package/runtime/ext_gui/gui.cpp +56 -286
  106. package/runtime/ext_gui/gui_events.cpp +294 -0
  107. package/runtime/ext_gui/gui_internal.h +29 -0
  108. package/runtime/ext_gui/layout.cpp +6 -597
  109. package/runtime/ext_gui/layout_flex.cpp +200 -0
  110. package/runtime/ext_gui/layout_flow.cpp +191 -0
  111. package/runtime/ext_gui/layout_flow_internal.h +149 -0
  112. package/runtime/ext_gui/layout_inline.cpp +181 -0
  113. package/runtime/ext_gui/layout_internal.h +20 -0
  114. package/runtime/ext_gui/renderer.cpp +15 -679
  115. package/runtime/ext_gui/renderer_image.cpp +233 -0
  116. package/runtime/ext_gui/renderer_internal.h +112 -0
  117. package/runtime/ext_gui/renderer_shaders.h +108 -0
  118. package/runtime/ext_gui/renderer_text.cpp +342 -0
  119. package/runtime/ext_gui/style.cpp +12 -660
  120. package/runtime/ext_gui/style_internal.h +33 -0
  121. package/runtime/ext_gui/style_print.cpp +113 -0
  122. package/runtime/ext_gui/style_ua.cpp +98 -0
  123. package/runtime/ext_gui/style_values.cpp +468 -0
  124. package/runtime/rt.h +6 -0
  125. package/runtime/rt_internal.h +4 -0
  126. package/runtime/xt_builtins.c +1 -0
  127. package/runtime/xt_containers/access.inc +32 -1
  128. package/runtime/xt_fetch/buffer.inc +64 -0
  129. package/runtime/xt_fetch/headers.inc +312 -0
  130. package/runtime/xt_fetch/request.inc +386 -0
  131. package/runtime/xt_fetch/response.inc +107 -0
  132. package/runtime/xt_fetch/socket.inc +72 -0
  133. package/runtime/xt_fetch/url.inc +108 -0
  134. package/runtime/xt_fetch.c +39 -0
  135. package/runtime/xt_stdlib2/set.inc +1 -1
  136. package/runtime/xt_typed_array.c +614 -0
  137. package/src/cli/main.ts +137 -29
  138. package/src/codegen/generator/calls/access.ts +9 -1
  139. package/src/codegen/generator/state.ts +20 -0
  140. package/src/codegen/generator/tables.ts +40 -0
  141. package/src/driver/bundler/commonjs.ts +553 -0
  142. package/src/driver/bundler/graph.ts +14 -0
  143. package/src/driver/bundler/merge.ts +15 -0
  144. package/src/driver/bundler/types.ts +8 -0
  145. package/src/driver/compiler.ts +83 -5
  146. package/src/driver/config.ts +173 -0
  147. package/src/driver/icon.ts +184 -0
  148. package/src/driver/index.ts +30 -0
  149. package/src/driver/mac-bundle.ts +156 -0
  150. package/src/driver/win-icon.ts +96 -0
  151. package/src/parser/statements.ts +17 -4
  152. package/dist/tests/e2e/gui.test.d.ts +0 -12
  153. package/dist/tests/e2e/gui.test.js +0 -702
  154. package/dist/tests/e2e/gui.test.js.map +0 -1
@@ -1,702 +0,0 @@
1
- /**
2
- * End-to-end tests for the `gui` extension.
3
- *
4
- * The suite compiles TypeScript programs that import `gui`, links them against
5
- * the prebuilt `runtime/lib/<os>-<arch>/gui.a` and runs them. Programs close
6
- * themselves through the `XT_GUI_AUTOCLOSE_MS` hook, so the tests never block on
7
- * a real window.
8
- *
9
- * It is skipped when the GUI archive has not been built (run `npm run gui`
10
- * first) and on Windows, where the archive is not produced yet.
11
- */
12
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { join } from "node:path";
15
- import { spawnSync } from "node:child_process";
16
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
17
- import { build } from "../../src/driver/compiler.js";
18
- import { createDefaultRegistry } from "../../src/extensions/registry.js";
19
- import { guiExtension } from "../../src/extensions/gui/index.js";
20
- import { findRuntimeDir, platformSlug } from "../../src/driver/paths.js";
21
- const archivePath = join(findRuntimeDir(), "lib", platformSlug(), "gui.a");
22
- const available = existsSync(archivePath) && process.platform !== "win32";
23
- describe.skipIf(!available)("gui extension", () => {
24
- let workdir;
25
- let logoPath;
26
- beforeAll(() => {
27
- workdir = mkdtempSync(join(tmpdir(), "xbintsc-gui-"));
28
- // A 4x4 PNG: left half red, right half blue (base64-encoded inline).
29
- logoPath = join(workdir, "logo.png");
30
- writeFileSync(logoPath, Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAFElEQVR42mP4z8DwH4Sh1H8G0gUALFAf4eNWqTEAAAAASUVORK5CYII=", "base64"));
31
- });
32
- afterAll(() => {
33
- rmSync(workdir, { recursive: true, force: true });
34
- });
35
- /** Compile a program and run it with the auto-close hook, returning stdout. */
36
- function compileAndRun(name, source, extraFiles = {}) {
37
- for (const [fileName, contents] of Object.entries(extraFiles)) {
38
- writeFileSync(join(workdir, fileName), contents);
39
- }
40
- const entry = join(workdir, `${name}.ts`);
41
- writeFileSync(entry, source);
42
- const extensions = createDefaultRegistry().register(guiExtension);
43
- const result = build(entry, {
44
- emit: "exe",
45
- outDir: join(workdir, `out-${name}`),
46
- cacheDir: join(workdir, `.cache-${name}`),
47
- extensions,
48
- });
49
- expect(result.diagnostics.filter((diagnostic) => diagnostic.category === "error")).toEqual([]);
50
- const executed = spawnSync(result.outputPath, [], {
51
- encoding: "utf8",
52
- env: { ...process.env, XT_GUI_AUTOCLOSE_MS: "400" },
53
- });
54
- expect(executed.status, executed.stderr).toBe(0);
55
- return { stdout: executed.stdout, status: executed.status };
56
- }
57
- it("opens a GPU-backed window and delivers lifecycle events", () => {
58
- const { stdout } = compileAndRun("window", `
59
- import { createWindow, run } from "gui";
60
- const win = createWindow({ title: "test window", width: 320, height: 240 });
61
- win.on("ready", () => console.log("gui ready"));
62
- win.on("close", () => console.log("gui closed"));
63
- win.setBackground("#101010");
64
- win.loadHTML("<h1>hi</h1>");
65
- run();
66
- console.log("gui exited");
67
- `);
68
- const lines = stdout.trim().split("\n");
69
- expect(lines).toContain("gui ready");
70
- expect(lines).toContain("gui closed");
71
- expect(lines).toContain("gui exited");
72
- });
73
- it("parses HTML/CSS and cascades computed styles", () => {
74
- const { stdout } = compileAndRun("cascade", `
75
- import { createWindow, run } from "gui";
76
-
77
- const HTML = \`
78
- <html>
79
- <head>
80
- <style>
81
- body { font-size: 16px; color: #111111; }
82
- div { width: 10%; }
83
- .box { width: 25%; }
84
- #main { width: 60%; color: #222222; }
85
- .box > .inner { padding-top: 3px; }
86
- .box .inner { padding-top: 5px; }
87
- li:first-child { color: #555555; }
88
- li:nth-child(2) { color: #666666; }
89
- li:not(.skip) { font-weight: bold; }
90
- [data-role="lead"] { text-align: center; }
91
- .em { font-size: 2em; }
92
- .rem { font-size: 2rem; }
93
- .imp { color: #000000; }
94
- .imp { color: #ff0000 !important; }
95
- </style>
96
- </head>
97
- <body>
98
- <div id="main" class="box">
99
- <div class="inner" style="color: #00ff00">x</div>
100
- <div class="inner">y</div>
101
- </div>
102
- <ul>
103
- <li>one</li>
104
- <li>two</li>
105
- <li class="skip">three</li>
106
- </ul>
107
- <p data-role="lead">lead</p>
108
- <div class="em">em</div>
109
- <div class="rem">rem</div>
110
- <div class="imp" style="color: #0000ff">important</div>
111
- </body>
112
- </html>
113
- \`;
114
-
115
- const win = createWindow({ title: "cascade", width: 800, height: 600 });
116
- win.on("ready", () => {
117
- const out = (label, value) => console.log(label + "=" + value);
118
- out("main-width", win.computedStyle("#main", "width"));
119
- out("inner-padding", win.computedStyle(".inner", "padding-top"));
120
- out("inner-color", win.computedStyle(".inner", "color"));
121
- out("body-color", win.computedStyle("body", "color"));
122
- out("li1-color", win.computedStyle("li:first-child", "color"));
123
- out("li2-color", win.computedStyle("li:nth-child(2)", "color"));
124
- out("li2-weight", win.computedStyle("li:nth-child(2)", "font-weight"));
125
- out("skip-weight", win.computedStyle("li.skip", "font-weight"));
126
- out("lead-align", win.computedStyle("[data-role=\\"lead\\"]", "text-align"));
127
- out("em-size", win.computedStyle(".em", "font-size"));
128
- out("rem-size", win.computedStyle(".rem", "font-size"));
129
- out("imp-color", win.computedStyle(".imp", "color"));
130
- out("li-count", win.queryCount("li"));
131
- out("lead-count", win.queryCount("[data-role=\\"lead\\"]"));
132
- });
133
- win.loadHTML(HTML);
134
- run();
135
- `);
136
- const value = (label) => {
137
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
138
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
139
- return line.slice(label.length + 1).trim();
140
- };
141
- expect(value("main-width")).toBe("60%"); // id beats class and tag
142
- expect(value("inner-padding")).toBe("5px"); // equal specificity, later rule wins
143
- expect(value("inner-color")).toBe("rgb(0, 255, 0)"); // inline beats the sheet
144
- expect(value("body-color")).toBe("rgb(17, 17, 17)");
145
- expect(value("li1-color")).toBe("rgb(85, 85, 85)"); // :first-child
146
- expect(value("li2-color")).toBe("rgb(102, 102, 102)"); // :nth-child(2)
147
- expect(value("li2-weight")).toBe("700"); // :not(.skip)
148
- expect(value("skip-weight")).toBe("400"); // :not(.skip) excluded it
149
- expect(value("lead-align")).toBe("center"); // [data-role="lead"]
150
- expect(value("em-size")).toBe("32px"); // 2em of the 16px parent
151
- expect(value("rem-size")).toBe("32px"); // 2rem of the 16px root
152
- expect(value("imp-color")).toBe("rgb(255, 0, 0)"); // !important beats inline
153
- expect(value("li-count")).toBe("3");
154
- expect(value("lead-count")).toBe("1");
155
- });
156
- it("lays out block, inline and flex boxes", () => {
157
- const { stdout } = compileAndRun("layout", `
158
- import { createWindow, run } from "gui";
159
-
160
- const HTML = \`
161
- <html><head><style>
162
- body { margin: 0; }
163
- #box { width: 200px; height: 100px; padding: 10px; border: 2px solid #000; margin: 5px; }
164
- #row { display: flex; width: 300px; gap: 10px; }
165
- #row .a { width: 50px; height: 20px; }
166
- #row .b { flex: 1; height: 20px; }
167
- #wrap { width: 60px; font-size: 10px; }
168
- </style></head><body>
169
- <div id="box">hello</div>
170
- <div id="row"><div class="a"></div><div class="b"></div></div>
171
- <div id="wrap">aaaa bbbb cccc dddd</div>
172
- </body></html>
173
- \`;
174
-
175
- const win = createWindow({ title: "layout", width: 800, height: 600 });
176
- win.on("ready", () => {
177
- const rect = (selector) => {
178
- const r = win.getBoundingClientRect(selector);
179
- return r.x + "," + r.y + "," + r.width + "," + r.height;
180
- };
181
- console.log("box=" + rect("#box"));
182
- console.log("a=" + rect("#row .a"));
183
- console.log("b=" + rect("#row .b"));
184
- console.log("wrap=" + rect("#wrap"));
185
- console.log("lh=" + win.fontMetrics(10).lineHeight);
186
- });
187
- win.loadHTML(HTML);
188
- run();
189
- `);
190
- const value = (label) => {
191
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
192
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
193
- return line.slice(label.length + 1).trim();
194
- };
195
- // Border box = content + padding (20) + border (4); margin offsets by 5.
196
- expect(value("box")).toBe("5,5,224,124");
197
- // Flex row: `.a` keeps 50px, `.b` grows into the remaining 240px after a 10px gap.
198
- expect(value("a")).toBe("0,134,50,20");
199
- expect(value("b")).toBe("60,134,240,20");
200
- // 60px-wide box wraps "aaaa bbbb" / "cccc dddd" onto two lines. The line
201
- // height comes from the real font metrics, so assert against them rather
202
- // than hard-coding a pixel value (fonts differ per platform).
203
- const wrap = value("wrap").split(",").map(Number);
204
- expect(wrap.slice(0, 3)).toEqual([0, 154, 60]);
205
- expect(wrap[3]).toBeCloseTo(2 * Number(value("lh")), 1);
206
- });
207
- it("sizes and paints images (intrinsic and explicit)", () => {
208
- const { stdout } = compileAndRun("image", `
209
- import { createWindow, run } from "gui";
210
-
211
- const HTML = \`
212
- <html><head><style>
213
- body { margin: 0; }
214
- #logo { width: 40px; height: 40px; }
215
- </style></head><body>
216
- <img id="logo" src="${logoPath}">
217
- <img id="natural" src="${logoPath}">
218
- </body></html>
219
- \`;
220
-
221
- const win = createWindow({ title: "image", width: 400, height: 300 });
222
- win.on("ready", () => {
223
- const logo = win.getBoundingClientRect("#logo");
224
- console.log("logo=" + logo.width + "x" + logo.height);
225
- const natural = win.getBoundingClientRect("#natural");
226
- console.log("natural=" + natural.width + "x" + natural.height);
227
- console.log(win.paintList().trim());
228
- win.close();
229
- });
230
- win.loadHTML(HTML);
231
- run();
232
- `);
233
- // Explicit CSS size wins; without one the intrinsic 4x4 size is used.
234
- expect(stdout).toContain("logo=40x40");
235
- expect(stdout).toContain("natural=4x4");
236
- expect(stdout).toContain(`image x=0.0 y=0.0 w=40.0 h=40.0 src=${logoPath}`);
237
- expect(stdout).toContain(`image x=0.0 y=40.0 w=4.0 h=4.0 src=${logoPath}`);
238
- });
239
- it("builds a display list of backgrounds and borders", () => {
240
- const { stdout } = compileAndRun("paint", `
241
- import { createWindow, run } from "gui";
242
-
243
- const HTML = \`
244
- <html><head><style>
245
- body { margin: 0; background-color: #123456; }
246
- #box { width: 100px; height: 40px; background: #ff0000; border: 4px solid #00ff00; border-radius: 8px; }
247
- </style></head><body>
248
- <div id="box">x</div>
249
- </body></html>
250
- \`;
251
-
252
- const win = createWindow({ title: "paint", width: 800, height: 600 });
253
- win.on("ready", () => {
254
- const r = win.getBoundingClientRect("#box");
255
- console.log("box-rect=" + r.x + "," + r.y + "," + r.width + "," + r.height);
256
- console.log("paint-count=" + win.paintCount());
257
- console.log("paint-list-start");
258
- console.log(win.paintList());
259
- console.log("paint-list-end");
260
- });
261
- win.loadHTML(HTML);
262
- run();
263
- `);
264
- const value = (label) => {
265
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
266
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
267
- return line.slice(label.length + 1).trim();
268
- };
269
- const list = stdout.slice(stdout.indexOf("paint-list-start"), stdout.indexOf("paint-list-end"));
270
- const occurrences = (needle) => list.split(needle).length - 1;
271
- // Border box = 100x40 content + 4px border on every side.
272
- expect(value("box-rect")).toBe("0,0,108,48");
273
- // body background + #box background + 4 border edges.
274
- expect(value("paint-count")).toBe("6");
275
- expect(occurrences("#123456ff")).toBe(1); // body background
276
- expect(occurrences("#ff0000ff")).toBe(1); // #box background
277
- expect(occurrences("#00ff00ff")).toBe(4); // four border edges
278
- // The background rectangle carries the 8px corner radius.
279
- expect(list).toContain("r=8.0 color=#ff0000ff");
280
- });
281
- it("emits shaped text runs into the display list", () => {
282
- const { stdout } = compileAndRun("text-runs", `
283
- import { createWindow, run } from "gui";
284
-
285
- const HTML = \`
286
- <html><head><style>
287
- body { margin: 0; }
288
- #t { font-size: 20px; color: #0000ff; }
289
- </style></head><body>
290
- <div id="t">Hi</div>
291
- </body></html>
292
- \`;
293
-
294
- const win = createWindow({ title: "text-runs", width: 400, height: 300 });
295
- win.on("ready", () => {
296
- const r = win.getBoundingClientRect("#t");
297
- console.log("rect=" + r.x + "," + r.y + "," + r.width + "," + r.height);
298
- console.log("paint-list-start");
299
- console.log(win.paintList());
300
- console.log("paint-list-end");
301
- });
302
- win.loadHTML(HTML);
303
- run();
304
- `);
305
- const list = stdout.slice(stdout.indexOf("paint-list-start"), stdout.indexOf("paint-list-end"));
306
- // The text fragment carries the resolved font size and colour.
307
- expect(list).toContain('text x=0.0');
308
- expect(list).toContain('size=20.0');
309
- expect(list).toContain('color=#0000ffff "Hi"');
310
- });
311
- it("hit-tests elements and delivers input events to handlers", () => {
312
- const { stdout } = compileAndRun("input", `
313
- import { createWindow, run } from "gui";
314
-
315
- const HTML = \`
316
- <html><head><style>
317
- body { margin: 0; }
318
- #box { width: 100px; height: 50px; }
319
- #inner { width: 20px; height: 20px; }
320
- </style></head><body>
321
- <div id="box"><div id="inner">x</div></div>
322
- </body></html>
323
- \`;
324
-
325
- const win = createWindow({ title: "input", width: 400, height: 300 });
326
- const log = [];
327
- win.on("mousemove", (e) => log.push("move:" + e.target));
328
- win.on("mousedown", (e) => log.push("down:" + e.button + ":" + e.target));
329
- win.on("click", (e) => log.push("click:" + e.x + "," + e.y + ":" + e.target));
330
- win.on("wheel", (e) => log.push("wheel:" + e.deltaX + "," + e.deltaY));
331
- win.on("keydown", (e) => log.push("key:" + e.key + ":" + e.shift));
332
-
333
- win.on("ready", () => {
334
- console.log("hit-inner=" + win.hitTest(5, 5));
335
- console.log("hit-box=" + win.hitTest(50, 40));
336
- console.log("hit-none=[" + win.hitTest(390, 290) + "]");
337
- win.sendEvent("mousemove", { x: 5, y: 5 });
338
- win.sendEvent("mousedown", { x: 5, y: 5, button: 0 });
339
- win.sendEvent("click", { x: 5, y: 5, button: 0 });
340
- win.sendEvent("wheel", { x: 5, y: 5, deltaX: 1, deltaY: 2 });
341
- win.sendEvent("keydown", { key: "a", code: "KeyA" });
342
- for (const line of log) console.log("event=" + line);
343
- win.close();
344
- });
345
- win.loadHTML(HTML);
346
- run();
347
- `);
348
- const value = (label) => {
349
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
350
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
351
- return line.slice(label.length + 1).trim();
352
- };
353
- // Hit testing returns the deepest element under the point.
354
- expect(value("hit-inner")).toBe("div#inner");
355
- expect(value("hit-box")).toBe("div#box");
356
- expect(value("hit-none")).toBe("[]");
357
- // Handlers receive the payload (coordinates, button, target, deltas, key).
358
- expect(stdout).toContain("event=move:div#inner");
359
- expect(stdout).toContain("event=down:0:div#inner");
360
- expect(stdout).toContain("event=click:5,5:div#inner");
361
- expect(stdout).toContain("event=wheel:1,2");
362
- expect(stdout).toContain("event=key:a:false");
363
- });
364
- it("applies :hover and :focus state to selector matching", () => {
365
- const { stdout } = compileAndRun("state", `
366
- import { createWindow, run } from "gui";
367
-
368
- const HTML = \`
369
- <html><head><style>
370
- body { margin: 0; }
371
- #box { width: 100px; height: 50px; background: #000000; }
372
- #box:hover { background: #ff0000; }
373
- #box:focus { background: #00ff00; }
374
- #box:hover > #inner { color: #0000ff; }
375
- #inner { width: 20px; height: 20px; color: #888888; }
376
- </style></head><body>
377
- <div id="box"><div id="inner">x</div></div>
378
- </body></html>
379
- \`;
380
-
381
- const win = createWindow({ title: "state", width: 400, height: 300 });
382
- win.on("ready", () => {
383
- console.log("initial-bg=" + win.computedStyle("#box", "background-color"));
384
- console.log("initial-color=" + win.computedStyle("#inner", "color"));
385
- win.sendEvent("mousemove", { x: 50, y: 40 });
386
- console.log("hover-bg=" + win.computedStyle("#box", "background-color"));
387
- win.sendEvent("mousemove", { x: 5, y: 5 });
388
- console.log("inner-hover-color=" + win.computedStyle("#inner", "color"));
389
- win.sendEvent("mousedown", { x: 50, y: 40, button: 0 });
390
- console.log("focus-bg=" + win.computedStyle("#box", "background-color"));
391
- win.close();
392
- });
393
- win.loadHTML(HTML);
394
- run();
395
- `);
396
- const value = (label) => {
397
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
398
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
399
- return line.slice(label.length + 1).trim();
400
- };
401
- expect(value("initial-bg")).toBe("rgb(0, 0, 0)"); // no :hover rule match
402
- expect(value("initial-color")).toBe("rgb(136, 136, 136)");
403
- expect(value("hover-bg")).toBe("rgb(255, 0, 0)"); // hovering #box
404
- // Hover also applies to ancestors of the hovered element.
405
- expect(value("inner-hover-color")).toBe("rgb(0, 0, 255)");
406
- expect(value("focus-bg")).toBe("rgb(0, 255, 0)"); // :focus wins over :hover
407
- });
408
- it("interpolates CSS transitions as the clock advances", () => {
409
- const { stdout } = compileAndRun("transition", `
410
- import { createWindow, run } from "gui";
411
-
412
- const HTML = \`
413
- <html><head><style>
414
- body { margin: 0; }
415
- #box {
416
- width: 100px; height: 50px; background-color: #000000; color: #888888;
417
- border-radius: 0px;
418
- transition: background-color 1000ms linear, color 500ms linear, border-radius 1000ms linear;
419
- }
420
- #box:hover { background-color: #ffffff; color: #0000ff; border-radius: 10px; }
421
- </style></head><body>
422
- <div id="box">hi</div>
423
- </body></html>
424
- \`;
425
-
426
- const win = createWindow({ title: "transition", width: 400, height: 300 });
427
- win.on("ready", () => {
428
- win.sendEvent("mousemove", { x: 50, y: 25 });
429
- console.log("start=" + win.computedStyle("#box", "background-color"));
430
- win.advance(250);
431
- console.log("q1-bg=" + win.computedStyle("#box", "background-color"));
432
- console.log("q1-color=" + win.computedStyle("#box", "color"));
433
- win.advance(250);
434
- console.log("q2-bg=" + win.computedStyle("#box", "background-color"));
435
- console.log("q2-color=" + win.computedStyle("#box", "color"));
436
- console.log("q2-radius=" + win.computedStyle("#box", "border-radius"));
437
- win.advance(500);
438
- console.log("end-bg=" + win.computedStyle("#box", "background-color"));
439
- console.log("end-radius=" + win.computedStyle("#box", "border-radius"));
440
- win.close();
441
- });
442
- win.loadHTML(HTML);
443
- run();
444
- `);
445
- const value = (label) => {
446
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
447
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
448
- return line.slice(label.length + 1).trim();
449
- };
450
- expect(value("start")).toBe("rgb(0, 0, 0)"); // 0% progress at t=0
451
- expect(value("q1-bg")).toBe("rgb(64, 64, 64)"); // 25% of black->white
452
- expect(value("q1-color")).toBe("rgb(68, 68, 196)"); // 50% of the 500ms color run
453
- expect(value("q2-bg")).toBe("rgb(128, 128, 128)"); // halfway
454
- expect(value("q2-color")).toBe("rgb(0, 0, 255)"); // finished
455
- expect(value("q2-radius")).toBe("5px");
456
- expect(value("end-bg")).toBe("rgb(255, 255, 255)"); // finished
457
- expect(value("end-radius")).toBe("10px");
458
- });
459
- it("shapes and measures text with the font stack", () => {
460
- const { stdout } = compileAndRun("text", `
461
- import { createWindow, run } from "gui";
462
-
463
- const win = createWindow({ title: "text", width: 400, height: 300 });
464
- win.on("ready", () => {
465
- const fm16 = win.fontMetrics(16);
466
- console.log("ready=" + fm16.ready);
467
- console.log("lh16=" + fm16.lineHeight);
468
- console.log("ascent16=" + fm16.ascent);
469
- console.log("m16=" + win.measureText("Hello", 16));
470
- console.log("m32=" + win.measureText("Hello", 32));
471
- console.log("empty=" + win.measureText("", 16));
472
- console.log("wide=" + (win.measureText("WWWW", 16) > win.measureText("iiii", 16)));
473
- win.close();
474
- });
475
- win.loadHTML("<html><body></body></html>");
476
- run();
477
- `);
478
- const value = (label) => {
479
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
480
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
481
- return line.slice(label.length + 1).trim();
482
- };
483
- // Metrics are positive and scale ~linearly with the font size (this holds
484
- // for both the real shaper and the no-font fallback).
485
- const m16 = Number(value("m16"));
486
- const m32 = Number(value("m32"));
487
- expect(m16).toBeGreaterThan(0);
488
- expect(m32).toBeGreaterThan(m16 * 1.8);
489
- expect(m32).toBeLessThan(m16 * 2.2);
490
- expect(Number(value("lh16"))).toBeGreaterThan(0);
491
- expect(Number(value("ascent16"))).toBeGreaterThan(0);
492
- expect(value("empty")).toBe("0");
493
- expect(value("wide")).toBe("true");
494
- });
495
- it("exposes DOM element handles, mutation and element events", () => {
496
- const { stdout } = compileAndRun("dom", `
497
- import { createWindow, run } from "gui";
498
-
499
- const HTML = \`
500
- <html><head><style>
501
- body { margin: 0; }
502
- #box { width: 100px; height: 50px; }
503
- #inner { width: 20px; height: 20px; color: #888888; }
504
- </style></head><body>
505
- <div id="box"><div id="inner">x</div></div>
506
- </body></html>\`;
507
-
508
- const win = createWindow({ title: "dom", width: 400, height: 300 });
509
- const log = [];
510
- win.on("click", (e) => log.push("win:" + e.target));
511
- win.on("ready", () => {
512
- const doc = win.document;
513
- const box = doc.querySelector("#box");
514
- const inner = doc.querySelector("#inner");
515
- console.log("tag=" + inner.tagName + ":" + inner.nodeType);
516
- console.log("text=" + inner.textContent);
517
- console.log("same=" + (doc.querySelector("#box") === box));
518
- console.log("parent=" + inner.parentNode.id + ":" + inner.parentElement.tagName);
519
- console.log("count=" + doc.querySelectorAll("div").length);
520
- console.log("body=" + (doc.body != null) + ":" + (doc.documentElement.tagName === "HTML"));
521
- console.log("byId=" + doc.getElementById("inner").id);
522
- inner.setAttribute("data-x", "1");
523
- console.log("attr=" + inner.getAttribute("data-x") + ":" + inner.hasAttribute("data-x"));
524
- inner.removeAttribute("data-x");
525
- console.log("attr2=" + inner.hasAttribute("data-x"));
526
- inner.classList.add("hot", "warm");
527
- console.log("class=" + inner.className + ":" + inner.classList.contains("warm"));
528
- console.log("toggle=" + inner.classList.toggle("hot") + ":" + inner.className);
529
- inner.style.setProperty("color", "#00ff00");
530
- console.log("style=" + inner.style.getPropertyValue("color") + ":" + win.computedStyle("#inner", "color"));
531
- console.log("offset=" + box.offsetWidth + "x" + box.offsetHeight);
532
- console.log("contains=" + box.contains(inner) + ":" + inner.contains(box));
533
- const created = doc.createElement("div");
534
- created.id = "new";
535
- created.textContent = "hi";
536
- box.appendChild(created);
537
- console.log("created=" + created.isConnected + ":" + doc.querySelector("#new").textContent + ":" + win.computedStyle("#new", "font-size"));
538
- const rect = doc.querySelector("#new").getBoundingClientRect();
539
- console.log("rect=" + rect.width + "x" + rect.height);
540
- box.removeChild(created);
541
- console.log("removed=" + created.isConnected + ":" + (doc.querySelector("#new") === undefined));
542
- // Element listeners bubble from target up to ancestors.
543
- box.addEventListener("click", (e) => log.push("box:" + e.target.id));
544
- inner.addEventListener("click", (e) => log.push("inner:" + e.target.id));
545
- inner.click();
546
- for (const line of log) console.log("event=" + line);
547
- // Window-level handlers keep their string target descriptor.
548
- win.sendEvent("click", { x: 5, y: 5, button: 0 });
549
- console.log("winCompat=" + log[log.length - 1]);
550
- // stopPropagation halts bubbling but the window handler still runs.
551
- const log2 = [];
552
- const box2 = doc.querySelector("#box");
553
- box2.addEventListener("click", () => log2.push("box"));
554
- inner.addEventListener("click", (e) => { log2.push("inner"); e.stopPropagation(); });
555
- inner.click();
556
- console.log("stopped=" + log2.join(","));
557
- win.close();
558
- });
559
- win.loadHTML(HTML);
560
- run();
561
- `);
562
- const value = (label) => {
563
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
564
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
565
- return line.slice(label.length + 1).trim();
566
- };
567
- expect(value("tag")).toBe("DIV:1");
568
- expect(value("text")).toBe("x");
569
- expect(value("same")).toBe("true"); // handle identity is stable
570
- expect(value("parent")).toBe("box:DIV");
571
- expect(value("count")).toBe("2");
572
- expect(value("body")).toBe("true:true");
573
- expect(value("byId")).toBe("inner");
574
- expect(value("attr")).toBe("1:true");
575
- expect(value("attr2")).toBe("false");
576
- expect(value("class")).toBe("hot warm:true");
577
- expect(value("toggle")).toBe("false:warm"); // toggle removed the existing class
578
- expect(value("style")).toBe("#00ff00:rgb(0, 255, 0)");
579
- expect(value("offset")).toBe("100x50");
580
- expect(value("contains")).toBe("true:false");
581
- expect(value("created")).toBe("true:hi:16px");
582
- expect(value("removed")).toBe("false:true");
583
- // Element listeners fire target-first, then bubble to the ancestor. The
584
- // legacy window handler runs last and still receives a descriptor string.
585
- expect(stdout).toContain("event=inner:inner");
586
- expect(stdout).toContain("event=box:inner");
587
- expect(value("winCompat")).toBe("win:div#inner.warm");
588
- expect(value("stopped")).toBe("inner");
589
- });
590
- it("compiles inline <script> bodies from an imported .html asset", () => {
591
- const { stdout } = compileAndRun("script", `
592
- import { createWindow, run } from "gui";
593
- import page from "./page.html";
594
-
595
- const win = createWindow({ title: "script", width: 320, height: 240 });
596
- win.on("ready", () => {
597
- const button = win.document.getElementById("b");
598
- button.click();
599
- button.click();
600
- console.log("after=" + win.document.getElementById("count").textContent);
601
- console.log("button=" + button.textContent);
602
- win.close();
603
- });
604
- win.loadHTML(page);
605
- run();
606
- `, {
607
- "page.html": `<html><body>
608
- <div id="count">0</div>
609
- <button id="b">0</button>
610
- <script lang="ts">
611
- const button = document.getElementById("b");
612
- const label = document.getElementById("count");
613
- let n = 0;
614
- button.addEventListener("click", () => {
615
- n = n + 1;
616
- button.textContent = String(n);
617
- label.textContent = String(n);
618
- });
619
- </script>
620
- </body></html>`,
621
- });
622
- const value = (label) => {
623
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
624
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
625
- return line.slice(label.length + 1).trim();
626
- };
627
- expect(value("after")).toBe("2");
628
- expect(value("button")).toBe("2");
629
- });
630
- it("runs external <script src> modules with imports at load time", () => {
631
- const { stdout } = compileAndRun("scriptsrc", `
632
- import { createWindow, run } from "gui";
633
- import page from "./page-src.html";
634
-
635
- const win = createWindow({ title: "scriptsrc", width: 320, height: 240 });
636
- win.on("ready", () => {
637
- const button = win.document.getElementById("b");
638
- button.click();
639
- button.click();
640
- console.log("srcAfter=" + win.document.getElementById("b").textContent);
641
- win.close();
642
- });
643
- win.loadHTML(page);
644
- run();
645
- `, {
646
- "page-src.html": `<html><body><button id="b">0</button><script src="./counter.ts"></script></body></html>`,
647
- "counter.ts": `
648
- import { double } from "./helper";
649
- const button = document.getElementById("b");
650
- let n = 0;
651
- button.addEventListener("click", () => {
652
- n = double(n) + 1;
653
- button.textContent = String(n);
654
- });
655
- `,
656
- "helper.ts": "export function double(n: number) { return n * 2; }",
657
- });
658
- const value = (label) => {
659
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
660
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
661
- return line.slice(label.length + 1).trim();
662
- };
663
- /* 0 -> 1 -> 3 proves the hoisted import (double) is linked. */
664
- expect(value("srcAfter")).toBe("3");
665
- });
666
- it("runs requestAnimationFrame callbacks each frame and honours cancelAnimationFrame", () => {
667
- const { stdout } = compileAndRun("raf", `
668
- import { createWindow, run } from "gui";
669
-
670
- const win = createWindow({ title: "raf", width: 320, height: 240 });
671
- let frames = 0;
672
- const tick = (t: number) => {
673
- frames = frames + 1;
674
- win.document.getElementById("label").textContent = String(frames);
675
- if (frames < 3) win.requestAnimationFrame(tick);
676
- else {
677
- console.log("frames=" + frames + ":" + (t > 0));
678
- console.log("label=" + win.document.getElementById("label").textContent);
679
- win.close();
680
- }
681
- };
682
- win.on("ready", () => {
683
- const cancelled = win.requestAnimationFrame(() => console.log("cancelled-ran"));
684
- win.cancelAnimationFrame(cancelled);
685
- const id = win.requestAnimationFrame(tick);
686
- console.log("id=" + (id > 0));
687
- });
688
- win.loadHTML("<div id='label'>0</div>");
689
- run();
690
- `);
691
- const value = (label) => {
692
- const line = stdout.split("\n").find((entry) => entry.startsWith(`${label}=`));
693
- expect(line, `missing ${label} in:\n${stdout}`).toBeDefined();
694
- return line.slice(label.length + 1).trim();
695
- };
696
- expect(value("id")).toBe("true");
697
- expect(value("frames")).toBe("3:true"); // callback receives a timestamp
698
- expect(value("label")).toBe("3"); // the DOM write was visible on the next read
699
- expect(stdout).not.toContain("cancelled-ran");
700
- });
701
- });
702
- //# sourceMappingURL=gui.test.js.map