@swifttui/web 0.0.14 → 0.0.15
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.
- package/README.md +24 -10
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +2 -0
- package/dist/src/AccessibilityTree.js +156 -0
- package/dist/src/AccessibilityTree.js.map +1 -0
- package/dist/src/BoxDrawingRenderer.js +1106 -0
- package/dist/src/BoxDrawingRenderer.js.map +1 -0
- package/dist/src/WebHostApp.d.ts +41 -0
- package/dist/src/WebHostApp.js +135 -0
- package/dist/src/WebHostApp.js.map +1 -0
- package/dist/src/WebHostSceneManifest.d.ts +18 -0
- package/dist/src/WebHostSceneManifest.js +70 -0
- package/dist/src/WebHostSceneManifest.js.map +1 -0
- package/dist/src/WebHostSceneRuntime.d.ts +112 -0
- package/dist/src/WebHostSceneRuntime.js +651 -0
- package/dist/src/WebHostSceneRuntime.js.map +1 -0
- package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
- package/dist/src/WebHostSurfaceTransport.js +252 -0
- package/dist/src/WebHostSurfaceTransport.js.map +1 -0
- package/dist/src/WebHostTerminalStyle.d.ts +92 -0
- package/dist/src/WebHostTerminalStyle.js +277 -0
- package/dist/src/WebHostTerminalStyle.js.map +1 -0
- package/dist/src/WebHostTestFixtures.d.ts +5 -0
- package/dist/src/WebHostTestFixtures.js +9 -0
- package/dist/src/WebHostTestFixtures.js.map +1 -0
- package/dist/src/WebSocketSceneBridge.d.ts +53 -0
- package/dist/src/WebSocketSceneBridge.js +124 -0
- package/dist/src/WebSocketSceneBridge.js.map +1 -0
- package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
- package/dist/src/wasi/BrowserWASIBridge.js +97 -0
- package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
- package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
- package/dist/src/wasi/SharedInputQueue.js +102 -0
- package/dist/src/wasi/SharedInputQueue.js.map +1 -0
- package/dist/src/wasi/StdIOPipe.d.ts +15 -0
- package/dist/src/wasi/StdIOPipe.js +56 -0
- package/dist/src/wasi/StdIOPipe.js.map +1 -0
- package/dist/src/wasi/WasiPollScheduler.js +114 -0
- package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
- package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
- package/dist/src/wasi/WasmSceneRuntime.js +119 -0
- package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
- package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
- package/dist/src/wasi/WasmSceneWorker.js +109 -0
- package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +2 -0
- package/dist/wasi-worker.d.ts +2 -0
- package/dist/wasi-worker.js +2 -0
- package/dist/wasi.d.ts +6 -0
- package/dist/wasi.js +6 -0
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +2 -0
- package/package.json +49 -18
- package/AGENTS.md +0 -52
- package/cli.ts +0 -168
- package/index.html +0 -50
- package/index.ts +0 -8
- package/manifest.ts +0 -1
- package/src/AccessibilityTree.ts +0 -262
- package/src/BoxDrawingRenderer.ts +0 -585
- package/src/PublicEntrypointBoundary.test.ts +0 -20
- package/src/WebHostApp.test.ts +0 -222
- package/src/WebHostApp.ts +0 -269
- package/src/WebHostSceneManifest.test.ts +0 -38
- package/src/WebHostSceneManifest.ts +0 -156
- package/src/WebHostSceneRuntime.test.ts +0 -1982
- package/src/WebHostSceneRuntime.ts +0 -1142
- package/src/WebHostSurfaceTransport.test.ts +0 -362
- package/src/WebHostSurfaceTransport.ts +0 -691
- package/src/WebHostTerminalStyle.test.ts +0 -123
- package/src/WebHostTerminalStyle.ts +0 -471
- package/src/WebHostTestFixtures.ts +0 -10
- package/src/WebSocketSceneBridge.test.ts +0 -198
- package/src/WebSocketSceneBridge.ts +0 -233
- package/src/browser.ts +0 -59
- package/src/wasi/BrowserWASIBridge.test.ts +0 -168
- package/src/wasi/BrowserWASIBridge.ts +0 -167
- package/src/wasi/SharedInputQueue.test.ts +0 -146
- package/src/wasi/SharedInputQueue.ts +0 -199
- package/src/wasi/StdIOPipe.ts +0 -72
- package/src/wasi/WasiPollScheduler.test.ts +0 -176
- package/src/wasi/WasiPollScheduler.ts +0 -305
- package/src/wasi/WasmSceneRuntime.ts +0 -205
- package/src/wasi/WasmSceneWorker.ts +0 -182
- package/testing.ts +0 -1
- package/tsconfig.json +0 -29
- package/wasi-worker.ts +0 -1
- package/wasi.ts +0 -4
- package/websocket.ts +0 -1
|
@@ -1,1982 +0,0 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
BrowserWASIBridge,
|
|
5
|
-
encodeRenderStyleControlMessage,
|
|
6
|
-
encodeResizeControlMessage,
|
|
7
|
-
} from "./wasi/BrowserWASIBridge.ts";
|
|
8
|
-
import { SharedInputQueueReader } from "./wasi/SharedInputQueue.ts";
|
|
9
|
-
import { createWasmSceneRuntimeFactory } from "./wasi/WasmSceneRuntime.ts";
|
|
10
|
-
import { WebHostSceneRuntime, type WheelMode } from "./WebHostSceneRuntime.ts";
|
|
11
|
-
import { transportFixture } from "./WebHostTestFixtures.ts";
|
|
12
|
-
|
|
13
|
-
const encoder = new TextEncoder();
|
|
14
|
-
const decoder = new TextDecoder();
|
|
15
|
-
|
|
16
|
-
test("hidden scenes stay out of layout even after style updates", () => {
|
|
17
|
-
const dom = installFakeDOM();
|
|
18
|
-
try {
|
|
19
|
-
const mount = new FakeElement("div");
|
|
20
|
-
const runtime = new WebHostSceneRuntime({
|
|
21
|
-
mount: mount as unknown as HTMLElement,
|
|
22
|
-
descriptor: { id: "details", title: "Details", isDefault: false },
|
|
23
|
-
style: {},
|
|
24
|
-
onInput: () => {},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
expect(runtime.element.hidden).toBe(true);
|
|
28
|
-
expect(runtime.element.style.getPropertyValue("display")).toBe("none");
|
|
29
|
-
expect(runtime.element.style.getPropertyPriority("display")).toBe("important");
|
|
30
|
-
|
|
31
|
-
runtime.setStyle({ fontSize: 18 });
|
|
32
|
-
expect(runtime.element.hidden).toBe(true);
|
|
33
|
-
expect(runtime.element.style.getPropertyValue("display")).toBe("none");
|
|
34
|
-
|
|
35
|
-
runtime.setVisible(true);
|
|
36
|
-
expect(runtime.element.hidden).toBe(false);
|
|
37
|
-
expect(runtime.element.style.getPropertyValue("display")).toBe("grid");
|
|
38
|
-
|
|
39
|
-
runtime.setVisible(false);
|
|
40
|
-
expect(runtime.element.hidden).toBe(true);
|
|
41
|
-
expect(runtime.element.style.getPropertyValue("display")).toBe("none");
|
|
42
|
-
} finally {
|
|
43
|
-
dom.restore();
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("runtime draws decoded surface frames into the canvas", async () => {
|
|
48
|
-
const dom = installFakeDOM({ devicePixelRatio: 2 });
|
|
49
|
-
try {
|
|
50
|
-
const bridge = new BrowserWASIBridge({
|
|
51
|
-
sceneId: "main",
|
|
52
|
-
columns: 4,
|
|
53
|
-
rows: 2,
|
|
54
|
-
});
|
|
55
|
-
const mount = new FakeElement("div");
|
|
56
|
-
const runtime = new WebHostSceneRuntime({
|
|
57
|
-
mount: mount as unknown as HTMLElement,
|
|
58
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
59
|
-
style: {
|
|
60
|
-
fontSize: 20,
|
|
61
|
-
fontFamily: "Test Mono",
|
|
62
|
-
theme: {
|
|
63
|
-
foreground: "#eeeeee",
|
|
64
|
-
background: "#101820",
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
bridge,
|
|
68
|
-
onInput: () => {},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
await runtime.mount();
|
|
72
|
-
|
|
73
|
-
expect(dom.canvases).toHaveLength(1);
|
|
74
|
-
const canvas = dom.canvases[0]!;
|
|
75
|
-
const context = canvas.context;
|
|
76
|
-
|
|
77
|
-
context.operations = [];
|
|
78
|
-
bridge.stdout.write(encoder.encode(transportFixture("web-surface-styled")));
|
|
79
|
-
|
|
80
|
-
expect(canvas.width).toBe(80);
|
|
81
|
-
expect(canvas.height).toBe(108);
|
|
82
|
-
expect(canvas.style.width).toBe("40px");
|
|
83
|
-
expect(canvas.style.height).toBe("54px");
|
|
84
|
-
|
|
85
|
-
expect(context.operations).toContainEqual({
|
|
86
|
-
type: "clearRect",
|
|
87
|
-
x: 0,
|
|
88
|
-
y: 0,
|
|
89
|
-
width: 40,
|
|
90
|
-
height: 54,
|
|
91
|
-
});
|
|
92
|
-
expect(context.operations).toContainEqual({
|
|
93
|
-
type: "fillRect",
|
|
94
|
-
x: 0,
|
|
95
|
-
y: 0,
|
|
96
|
-
width: 40,
|
|
97
|
-
height: 54,
|
|
98
|
-
fillStyle: "rgba(16, 24, 32, 1)",
|
|
99
|
-
globalAlpha: 1,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(fillTextOperations(context, "A")).toEqual([
|
|
103
|
-
{
|
|
104
|
-
type: "fillText",
|
|
105
|
-
text: "A",
|
|
106
|
-
x: 0,
|
|
107
|
-
y: 21,
|
|
108
|
-
fillStyle: "#000000FF",
|
|
109
|
-
font: "italic 700 20px Test Mono",
|
|
110
|
-
globalAlpha: 0.75,
|
|
111
|
-
},
|
|
112
|
-
]);
|
|
113
|
-
expect(fillTextOperations(context, "界")).toHaveLength(1);
|
|
114
|
-
expect(fillRectOperations(context, "#E05757FF")[0]).toMatchObject({
|
|
115
|
-
x: 0,
|
|
116
|
-
y: 0,
|
|
117
|
-
width: 10,
|
|
118
|
-
height: 27,
|
|
119
|
-
globalAlpha: 0.75,
|
|
120
|
-
});
|
|
121
|
-
expect(fillRectOperations(context, "#61C67BFF")[0]).toMatchObject({
|
|
122
|
-
x: 10,
|
|
123
|
-
y: 0,
|
|
124
|
-
width: 20,
|
|
125
|
-
height: 27,
|
|
126
|
-
globalAlpha: 0.5,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
const strokes = context.operations.filter((operation) => operation.type === "stroke");
|
|
130
|
-
expect(strokes).toContainEqual({
|
|
131
|
-
type: "stroke",
|
|
132
|
-
strokeStyle: "#EBB33CFF",
|
|
133
|
-
lineWidth: 1,
|
|
134
|
-
lineDash: [4, 3],
|
|
135
|
-
path: [["moveTo", 0, 25], ["lineTo", 10, 25]],
|
|
136
|
-
});
|
|
137
|
-
expect(strokes).toContainEqual({
|
|
138
|
-
type: "stroke",
|
|
139
|
-
strokeStyle: "#E05757FF",
|
|
140
|
-
lineWidth: 1,
|
|
141
|
-
lineDash: [1, 3],
|
|
142
|
-
path: [["moveTo", 0, 13], ["lineTo", 10, 13]],
|
|
143
|
-
});
|
|
144
|
-
expect(strokes.some((operation) => operation.lineWidth === 2)).toBe(true);
|
|
145
|
-
} finally {
|
|
146
|
-
dom.restore();
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("runtime redraws only damaged cells when a compatible frame includes damage", async () => {
|
|
151
|
-
const dom = installFakeDOM();
|
|
152
|
-
try {
|
|
153
|
-
const bridge = new BrowserWASIBridge({
|
|
154
|
-
sceneId: "main",
|
|
155
|
-
columns: 4,
|
|
156
|
-
rows: 2,
|
|
157
|
-
});
|
|
158
|
-
const mount = new FakeElement("div");
|
|
159
|
-
const runtime = new WebHostSceneRuntime({
|
|
160
|
-
mount: mount as unknown as HTMLElement,
|
|
161
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
162
|
-
style: {
|
|
163
|
-
fontSize: 20,
|
|
164
|
-
fontFamily: "Test Mono",
|
|
165
|
-
},
|
|
166
|
-
bridge,
|
|
167
|
-
onInput: () => {},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
await runtime.mount();
|
|
171
|
-
|
|
172
|
-
const canvas = dom.canvases[0]!;
|
|
173
|
-
const context = canvas.context;
|
|
174
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
175
|
-
version: 1,
|
|
176
|
-
width: 4,
|
|
177
|
-
height: 2,
|
|
178
|
-
styles: [null],
|
|
179
|
-
rows: [
|
|
180
|
-
[[0, "A", 1, 0], [1, "B", 1, 0]],
|
|
181
|
-
[[0, "C", 1, 0], [1, "D", 1, 0]],
|
|
182
|
-
],
|
|
183
|
-
images: [],
|
|
184
|
-
})));
|
|
185
|
-
|
|
186
|
-
context.operations = [];
|
|
187
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
188
|
-
version: 1,
|
|
189
|
-
width: 4,
|
|
190
|
-
height: 2,
|
|
191
|
-
styles: [null],
|
|
192
|
-
rows: [
|
|
193
|
-
[[0, "A", 1, 0], [1, "B", 1, 0]],
|
|
194
|
-
[[0, "X", 1, 0], [1, "D", 1, 0]],
|
|
195
|
-
],
|
|
196
|
-
images: [],
|
|
197
|
-
damage: {
|
|
198
|
-
textRows: [[1, [[0, 1]]]],
|
|
199
|
-
requiresFullTextRepaint: false,
|
|
200
|
-
requiresFullGraphicsReplay: false,
|
|
201
|
-
},
|
|
202
|
-
})));
|
|
203
|
-
|
|
204
|
-
expect(context.operations).toContainEqual({
|
|
205
|
-
type: "clearRect",
|
|
206
|
-
x: 0,
|
|
207
|
-
y: 27,
|
|
208
|
-
width: 10,
|
|
209
|
-
height: 27,
|
|
210
|
-
});
|
|
211
|
-
expect(fillTextOperations(context, "X")).toHaveLength(1);
|
|
212
|
-
expect(fillTextOperations(context, "A")).toEqual([]);
|
|
213
|
-
expect(fillTextOperations(context, "D")).toEqual([]);
|
|
214
|
-
} finally {
|
|
215
|
-
dom.restore();
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("runtime redraws spanning cells that overlap a dirty range", async () => {
|
|
220
|
-
const dom = installFakeDOM();
|
|
221
|
-
try {
|
|
222
|
-
const bridge = new BrowserWASIBridge({
|
|
223
|
-
sceneId: "main",
|
|
224
|
-
columns: 6,
|
|
225
|
-
rows: 1,
|
|
226
|
-
});
|
|
227
|
-
const mount = new FakeElement("div");
|
|
228
|
-
const runtime = new WebHostSceneRuntime({
|
|
229
|
-
mount: mount as unknown as HTMLElement,
|
|
230
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
231
|
-
style: {
|
|
232
|
-
fontSize: 20,
|
|
233
|
-
fontFamily: "Test Mono",
|
|
234
|
-
},
|
|
235
|
-
bridge,
|
|
236
|
-
onInput: () => {},
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
await runtime.mount();
|
|
240
|
-
|
|
241
|
-
const canvas = dom.canvases[0]!;
|
|
242
|
-
const context = canvas.context;
|
|
243
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
244
|
-
version: 1,
|
|
245
|
-
width: 6,
|
|
246
|
-
height: 1,
|
|
247
|
-
styles: [null],
|
|
248
|
-
rows: [
|
|
249
|
-
[[0, "Wide", 4, 0], [4, "Z", 1, 0]],
|
|
250
|
-
],
|
|
251
|
-
images: [],
|
|
252
|
-
})));
|
|
253
|
-
|
|
254
|
-
context.operations = [];
|
|
255
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
256
|
-
version: 1,
|
|
257
|
-
width: 6,
|
|
258
|
-
height: 1,
|
|
259
|
-
styles: [null],
|
|
260
|
-
rows: [
|
|
261
|
-
[[0, "wide", 4, 0], [4, "Z", 1, 0]],
|
|
262
|
-
],
|
|
263
|
-
images: [],
|
|
264
|
-
damage: {
|
|
265
|
-
textRows: [[0, [[2, 3]]]],
|
|
266
|
-
requiresFullTextRepaint: false,
|
|
267
|
-
requiresFullGraphicsReplay: false,
|
|
268
|
-
},
|
|
269
|
-
})));
|
|
270
|
-
|
|
271
|
-
expect(context.operations).toContainEqual({
|
|
272
|
-
type: "clearRect",
|
|
273
|
-
x: 20,
|
|
274
|
-
y: 0,
|
|
275
|
-
width: 10,
|
|
276
|
-
height: 27,
|
|
277
|
-
});
|
|
278
|
-
expect(fillTextOperations(context, "wide")).toHaveLength(1);
|
|
279
|
-
expect(fillTextOperations(context, "Z")).toEqual([]);
|
|
280
|
-
} finally {
|
|
281
|
-
dom.restore();
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test("runtime clears stale overlay text when dirty rects remove an overlay", async () => {
|
|
286
|
-
const dom = installFakeDOM();
|
|
287
|
-
try {
|
|
288
|
-
const bridge = new BrowserWASIBridge({
|
|
289
|
-
sceneId: "main",
|
|
290
|
-
columns: 24,
|
|
291
|
-
rows: 4,
|
|
292
|
-
});
|
|
293
|
-
const mount = new FakeElement("div");
|
|
294
|
-
const runtime = new WebHostSceneRuntime({
|
|
295
|
-
mount: mount as unknown as HTMLElement,
|
|
296
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
297
|
-
style: {
|
|
298
|
-
fontSize: 20,
|
|
299
|
-
fontFamily: "Test Mono",
|
|
300
|
-
},
|
|
301
|
-
bridge,
|
|
302
|
-
onInput: () => {},
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
await runtime.mount();
|
|
306
|
-
|
|
307
|
-
const canvas = dom.canvases[0]!;
|
|
308
|
-
const context = canvas.context;
|
|
309
|
-
context.operations = [];
|
|
310
|
-
|
|
311
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
312
|
-
version: 1,
|
|
313
|
-
width: 24,
|
|
314
|
-
height: 4,
|
|
315
|
-
styles: [null],
|
|
316
|
-
rows: [
|
|
317
|
-
[[0, "Base content", 12, 0]],
|
|
318
|
-
[],
|
|
319
|
-
[],
|
|
320
|
-
[],
|
|
321
|
-
],
|
|
322
|
-
images: [],
|
|
323
|
-
})));
|
|
324
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
325
|
-
version: 1,
|
|
326
|
-
width: 24,
|
|
327
|
-
height: 4,
|
|
328
|
-
styles: [null],
|
|
329
|
-
rows: [
|
|
330
|
-
[[0, "Base content", 12, 0]],
|
|
331
|
-
[[0, "Command palette", 15, 0]],
|
|
332
|
-
[[0, "Search actions", 14, 0]],
|
|
333
|
-
[],
|
|
334
|
-
],
|
|
335
|
-
images: [],
|
|
336
|
-
damage: {
|
|
337
|
-
textRows: [
|
|
338
|
-
[1, [[0, 24]]],
|
|
339
|
-
[2, [[0, 24]]],
|
|
340
|
-
],
|
|
341
|
-
requiresFullTextRepaint: false,
|
|
342
|
-
requiresFullGraphicsReplay: false,
|
|
343
|
-
},
|
|
344
|
-
})));
|
|
345
|
-
|
|
346
|
-
const overlayText = readCanvasTextLikePixels(canvas);
|
|
347
|
-
expect(overlayText).toContain("Command palette");
|
|
348
|
-
expect(overlayText).toContain("Search actions");
|
|
349
|
-
|
|
350
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
351
|
-
version: 1,
|
|
352
|
-
width: 24,
|
|
353
|
-
height: 4,
|
|
354
|
-
styles: [null],
|
|
355
|
-
rows: [
|
|
356
|
-
[[0, "Base content", 12, 0]],
|
|
357
|
-
[],
|
|
358
|
-
[],
|
|
359
|
-
[],
|
|
360
|
-
],
|
|
361
|
-
images: [],
|
|
362
|
-
damage: {
|
|
363
|
-
textRows: [
|
|
364
|
-
[1, [[0, 24]]],
|
|
365
|
-
[2, [[0, 24]]],
|
|
366
|
-
],
|
|
367
|
-
requiresFullTextRepaint: false,
|
|
368
|
-
requiresFullGraphicsReplay: false,
|
|
369
|
-
},
|
|
370
|
-
})));
|
|
371
|
-
|
|
372
|
-
const dismissedText = readCanvasTextLikePixels(canvas);
|
|
373
|
-
expect(dismissedText).not.toContain("Command palette");
|
|
374
|
-
expect(dismissedText).not.toContain("Search actions");
|
|
375
|
-
} finally {
|
|
376
|
-
dom.restore();
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
test("runtime skips canvas drawing for compatible empty damage", async () => {
|
|
381
|
-
const dom = installFakeDOM();
|
|
382
|
-
try {
|
|
383
|
-
const bridge = new BrowserWASIBridge({ sceneId: "main", columns: 4, rows: 2 });
|
|
384
|
-
const mount = new FakeElement("div");
|
|
385
|
-
const runtime = new WebHostSceneRuntime({
|
|
386
|
-
mount: mount as unknown as HTMLElement,
|
|
387
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
388
|
-
style: { fontSize: 20, fontFamily: "Test Mono" },
|
|
389
|
-
bridge,
|
|
390
|
-
onInput: () => {},
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
await runtime.mount();
|
|
394
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
395
|
-
version: 1,
|
|
396
|
-
width: 4,
|
|
397
|
-
height: 2,
|
|
398
|
-
styles: [null],
|
|
399
|
-
rows: [[[0, "A", 1, 0]], []],
|
|
400
|
-
images: [],
|
|
401
|
-
})));
|
|
402
|
-
|
|
403
|
-
const context = dom.canvases[0]!.context;
|
|
404
|
-
context.operations = [];
|
|
405
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
406
|
-
version: 1,
|
|
407
|
-
width: 4,
|
|
408
|
-
height: 2,
|
|
409
|
-
styles: [null],
|
|
410
|
-
rows: [[[0, "A", 1, 0]], []],
|
|
411
|
-
images: [],
|
|
412
|
-
damage: {
|
|
413
|
-
textRows: [],
|
|
414
|
-
requiresFullTextRepaint: false,
|
|
415
|
-
requiresFullGraphicsReplay: false,
|
|
416
|
-
},
|
|
417
|
-
})));
|
|
418
|
-
|
|
419
|
-
expect(context.operations).toEqual([]);
|
|
420
|
-
} finally {
|
|
421
|
-
dom.restore();
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test("runtime clears dirty rows when an image disappears", async () => {
|
|
426
|
-
const dom = installFakeDOM({
|
|
427
|
-
createImageBitmap: async () => ({ imageId: "decoded-image" }),
|
|
428
|
-
});
|
|
429
|
-
try {
|
|
430
|
-
const bridge = new BrowserWASIBridge({ sceneId: "main", columns: 4, rows: 2 });
|
|
431
|
-
const mount = new FakeElement("div");
|
|
432
|
-
const runtime = new WebHostSceneRuntime({
|
|
433
|
-
mount: mount as unknown as HTMLElement,
|
|
434
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
435
|
-
style: { fontSize: 20, fontFamily: "Test Mono" },
|
|
436
|
-
bridge,
|
|
437
|
-
onInput: () => {},
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
await runtime.mount();
|
|
441
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
442
|
-
version: 1,
|
|
443
|
-
width: 4,
|
|
444
|
-
height: 2,
|
|
445
|
-
styles: [null],
|
|
446
|
-
rows: [
|
|
447
|
-
[[0, "A", 1, 0]],
|
|
448
|
-
[[0, "B", 1, 0]],
|
|
449
|
-
],
|
|
450
|
-
images: [
|
|
451
|
-
{
|
|
452
|
-
id: "png:test",
|
|
453
|
-
format: "png",
|
|
454
|
-
bounds: [1, 1, 2, 1],
|
|
455
|
-
visibleBounds: [1, 1, 2, 1],
|
|
456
|
-
scalingMode: "stretch",
|
|
457
|
-
dataBase64: "iVBORw==",
|
|
458
|
-
},
|
|
459
|
-
],
|
|
460
|
-
})));
|
|
461
|
-
await flushPromises();
|
|
462
|
-
|
|
463
|
-
const context = dom.canvases[0]!.context;
|
|
464
|
-
context.operations = [];
|
|
465
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
466
|
-
version: 1,
|
|
467
|
-
width: 4,
|
|
468
|
-
height: 2,
|
|
469
|
-
styles: [null],
|
|
470
|
-
rows: [
|
|
471
|
-
[[0, "A", 1, 0]],
|
|
472
|
-
[[0, "B", 1, 0]],
|
|
473
|
-
],
|
|
474
|
-
images: [],
|
|
475
|
-
damage: {
|
|
476
|
-
textRows: [[1, [[1, 3]]]],
|
|
477
|
-
requiresFullTextRepaint: false,
|
|
478
|
-
requiresFullGraphicsReplay: false,
|
|
479
|
-
},
|
|
480
|
-
})));
|
|
481
|
-
|
|
482
|
-
expect(context.operations).toContainEqual({
|
|
483
|
-
type: "clearRect",
|
|
484
|
-
x: 10,
|
|
485
|
-
y: 27,
|
|
486
|
-
width: 20,
|
|
487
|
-
height: 27,
|
|
488
|
-
});
|
|
489
|
-
expect(drawImageOperations(context)).toEqual([]);
|
|
490
|
-
expect(fillTextOperations(context, "A")).toEqual([]);
|
|
491
|
-
} finally {
|
|
492
|
-
dom.restore();
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
test("WASI runtime forwards bridge control input into the worker queue", async () => {
|
|
497
|
-
const dom = installFakeDOM();
|
|
498
|
-
const previousWorker = globalThis.Worker;
|
|
499
|
-
const postedMessages: Array<{ inputQueue?: ConstructorParameters<typeof SharedInputQueueReader>[0] }> = [];
|
|
500
|
-
|
|
501
|
-
class FakeWorker {
|
|
502
|
-
constructor(
|
|
503
|
-
_url: string | URL,
|
|
504
|
-
_options?: WorkerOptions
|
|
505
|
-
) {}
|
|
506
|
-
|
|
507
|
-
addEventListener(
|
|
508
|
-
_type: string,
|
|
509
|
-
_listener: EventListener
|
|
510
|
-
): void {}
|
|
511
|
-
|
|
512
|
-
postMessage(
|
|
513
|
-
message: { inputQueue?: ConstructorParameters<typeof SharedInputQueueReader>[0] }
|
|
514
|
-
): void {
|
|
515
|
-
postedMessages.push(message);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
terminate(): void {}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
globalThis.Worker = FakeWorker as unknown as typeof Worker;
|
|
522
|
-
try {
|
|
523
|
-
const bridge = new BrowserWASIBridge({ sceneId: "main", columns: 4, rows: 2 });
|
|
524
|
-
const mount = new FakeElement("div");
|
|
525
|
-
const runtime = createWasmSceneRuntimeFactory(new URL("https://example.test/app.wasm"), {
|
|
526
|
-
workerModuleURL: "fake-worker.js",
|
|
527
|
-
})({
|
|
528
|
-
mount: mount as unknown as HTMLElement,
|
|
529
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
530
|
-
style: { fontSize: 20 },
|
|
531
|
-
bridge,
|
|
532
|
-
onInput: () => {},
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
await runtime.mount();
|
|
536
|
-
const inputQueue = postedMessages[0]?.inputQueue;
|
|
537
|
-
if (!inputQueue) {
|
|
538
|
-
throw new Error("worker did not receive an input queue");
|
|
539
|
-
}
|
|
540
|
-
const reader = new SharedInputQueueReader(inputQueue);
|
|
541
|
-
reader.readAvailable(reader.availableBytes());
|
|
542
|
-
|
|
543
|
-
const style = { cursorBlink: true };
|
|
544
|
-
bridge.updateRenderStyle(style);
|
|
545
|
-
const styleBytes = reader.readAvailable(reader.availableBytes());
|
|
546
|
-
expect(Array.from(styleBytes ?? [])).toEqual(
|
|
547
|
-
Array.from(encodeRenderStyleControlMessage(style))
|
|
548
|
-
);
|
|
549
|
-
|
|
550
|
-
bridge.resize(10, 4, 9, 18);
|
|
551
|
-
const resizeBytes = reader.readAvailable(reader.availableBytes());
|
|
552
|
-
expect(Array.from(resizeBytes ?? [])).toEqual(
|
|
553
|
-
Array.from(encodeResizeControlMessage(10, 4, 9, 18))
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
runtime.dispose();
|
|
557
|
-
} finally {
|
|
558
|
-
globalThis.Worker = previousWorker;
|
|
559
|
-
dom.restore();
|
|
560
|
-
}
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
test("runtime mounts accessibility tree and announces live-region changes", async () => {
|
|
564
|
-
const dom = installFakeDOM();
|
|
565
|
-
try {
|
|
566
|
-
const bridge = new BrowserWASIBridge({
|
|
567
|
-
sceneId: "main",
|
|
568
|
-
columns: 4,
|
|
569
|
-
rows: 2,
|
|
570
|
-
});
|
|
571
|
-
const mount = new FakeElement("div");
|
|
572
|
-
const runtime = new WebHostSceneRuntime({
|
|
573
|
-
mount: mount as unknown as HTMLElement,
|
|
574
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
575
|
-
style: {
|
|
576
|
-
fontSize: 20,
|
|
577
|
-
fontFamily: "Test Mono",
|
|
578
|
-
},
|
|
579
|
-
bridge,
|
|
580
|
-
onInput: () => {},
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
await runtime.mount();
|
|
584
|
-
|
|
585
|
-
const canvas = dom.canvases[0]!;
|
|
586
|
-
expect(canvas.getAttribute("aria-hidden")).toBe("true");
|
|
587
|
-
|
|
588
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
589
|
-
version: 2,
|
|
590
|
-
width: 4,
|
|
591
|
-
height: 2,
|
|
592
|
-
styles: [null],
|
|
593
|
-
rows: [[], []],
|
|
594
|
-
accessibilityTree: [
|
|
595
|
-
{
|
|
596
|
-
id: "root",
|
|
597
|
-
rect: [0, 0, 4, 2],
|
|
598
|
-
role: "group",
|
|
599
|
-
label: "Root",
|
|
600
|
-
isFocused: false,
|
|
601
|
-
},
|
|
602
|
-
{
|
|
603
|
-
id: "root/button",
|
|
604
|
-
parentId: "root",
|
|
605
|
-
rect: [0, 0, 2, 1],
|
|
606
|
-
role: "button",
|
|
607
|
-
label: "Save",
|
|
608
|
-
hint: "Writes the file",
|
|
609
|
-
isFocused: true,
|
|
610
|
-
},
|
|
611
|
-
{
|
|
612
|
-
id: "root/status",
|
|
613
|
-
parentId: "root",
|
|
614
|
-
rect: [0, 1, 2, 1],
|
|
615
|
-
role: "status",
|
|
616
|
-
label: "Idle",
|
|
617
|
-
liveRegion: "polite",
|
|
618
|
-
isFocused: false,
|
|
619
|
-
},
|
|
620
|
-
{
|
|
621
|
-
id: "root/error",
|
|
622
|
-
parentId: "root",
|
|
623
|
-
rect: [2, 1, 2, 1],
|
|
624
|
-
role: "alert",
|
|
625
|
-
label: "Ready",
|
|
626
|
-
liveRegion: "assertive",
|
|
627
|
-
isFocused: false,
|
|
628
|
-
},
|
|
629
|
-
],
|
|
630
|
-
accessibilityAnnouncements: [
|
|
631
|
-
{ message: "Ready", politeness: "polite" },
|
|
632
|
-
],
|
|
633
|
-
})));
|
|
634
|
-
|
|
635
|
-
const tree = childWithClass(runtime.terminalMount, "webhost-scene__accessibility-tree");
|
|
636
|
-
const announcer = childWithClass(
|
|
637
|
-
runtime.terminalMount,
|
|
638
|
-
"webhost-scene__accessibility-announcer"
|
|
639
|
-
);
|
|
640
|
-
const root = childWithData(tree, "accessibilityId", "root");
|
|
641
|
-
const button = childWithData(root, "accessibilityId", "root/button");
|
|
642
|
-
const status = childWithData(root, "accessibilityId", "root/status");
|
|
643
|
-
|
|
644
|
-
expect(button.getAttribute("role")).toBe("button");
|
|
645
|
-
expect(button.getAttribute("aria-label")).toBe("Save");
|
|
646
|
-
expect(button.getAttribute("aria-description")).toBe("Writes the file");
|
|
647
|
-
expect(button.focused).toBe(true);
|
|
648
|
-
expect(button.lastFocusOptions).toEqual({ preventScroll: true });
|
|
649
|
-
expect(status.getAttribute("role")).toBe("status");
|
|
650
|
-
expect(status.getAttribute("aria-live")).toBe("polite");
|
|
651
|
-
expect(status.style.left).toBe("0px");
|
|
652
|
-
expect(status.style.top).toBe("27px");
|
|
653
|
-
expect(announcer.textContent).toBe("Ready");
|
|
654
|
-
|
|
655
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
656
|
-
version: 2,
|
|
657
|
-
width: 4,
|
|
658
|
-
height: 2,
|
|
659
|
-
styles: [null],
|
|
660
|
-
rows: [[], []],
|
|
661
|
-
accessibilityTree: [
|
|
662
|
-
{
|
|
663
|
-
id: "root/status",
|
|
664
|
-
rect: [0, 1, 2, 1],
|
|
665
|
-
role: "status",
|
|
666
|
-
label: "Saved",
|
|
667
|
-
liveRegion: "polite",
|
|
668
|
-
isFocused: false,
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
id: "root/error",
|
|
672
|
-
rect: [2, 1, 2, 1],
|
|
673
|
-
role: "alert",
|
|
674
|
-
label: "Failed",
|
|
675
|
-
liveRegion: "assertive",
|
|
676
|
-
isFocused: false,
|
|
677
|
-
},
|
|
678
|
-
],
|
|
679
|
-
})));
|
|
680
|
-
|
|
681
|
-
expect(announcer.getAttribute("aria-live")).toBe("assertive");
|
|
682
|
-
expect(announcer.textContent).toBe("Failed\nSaved");
|
|
683
|
-
|
|
684
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
685
|
-
version: 2,
|
|
686
|
-
width: 4,
|
|
687
|
-
height: 2,
|
|
688
|
-
styles: [null],
|
|
689
|
-
rows: [[], []],
|
|
690
|
-
accessibilityAnnouncements: [
|
|
691
|
-
{ message: "Published", politeness: "assertive" },
|
|
692
|
-
{ message: "Queued", politeness: "polite" },
|
|
693
|
-
],
|
|
694
|
-
})));
|
|
695
|
-
|
|
696
|
-
expect(announcer.getAttribute("aria-live")).toBe("assertive");
|
|
697
|
-
expect(announcer.textContent).toBe("Published\nQueued");
|
|
698
|
-
|
|
699
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
700
|
-
version: 2,
|
|
701
|
-
width: 4,
|
|
702
|
-
height: 2,
|
|
703
|
-
styles: [null],
|
|
704
|
-
rows: [[], []],
|
|
705
|
-
accessibilityTree: [
|
|
706
|
-
{
|
|
707
|
-
id: "root/status",
|
|
708
|
-
rect: [0, 1, 2, 1],
|
|
709
|
-
role: "status",
|
|
710
|
-
label: "Saved",
|
|
711
|
-
liveRegion: "polite",
|
|
712
|
-
isFocused: false,
|
|
713
|
-
},
|
|
714
|
-
],
|
|
715
|
-
})));
|
|
716
|
-
|
|
717
|
-
expect(announcer.textContent).toBe("Published\nQueued");
|
|
718
|
-
} finally {
|
|
719
|
-
dom.restore();
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
test("runtime decodes surface images once and reuses the cached image", async () => {
|
|
724
|
-
const decodedBlobs: Blob[] = [];
|
|
725
|
-
const dom = installFakeDOM({
|
|
726
|
-
createImageBitmap: async (blob) => {
|
|
727
|
-
decodedBlobs.push(blob);
|
|
728
|
-
return { imageId: `decoded-${decodedBlobs.length}` };
|
|
729
|
-
},
|
|
730
|
-
});
|
|
731
|
-
try {
|
|
732
|
-
const bridge = new BrowserWASIBridge({
|
|
733
|
-
sceneId: "main",
|
|
734
|
-
columns: 4,
|
|
735
|
-
rows: 2,
|
|
736
|
-
});
|
|
737
|
-
const mount = new FakeElement("div");
|
|
738
|
-
const runtime = new WebHostSceneRuntime({
|
|
739
|
-
mount: mount as unknown as HTMLElement,
|
|
740
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
741
|
-
style: {
|
|
742
|
-
fontSize: 20,
|
|
743
|
-
fontFamily: "Test Mono",
|
|
744
|
-
},
|
|
745
|
-
bridge,
|
|
746
|
-
onInput: () => {},
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
await runtime.mount();
|
|
750
|
-
|
|
751
|
-
const canvas = dom.canvases[0]!;
|
|
752
|
-
const context = canvas.context;
|
|
753
|
-
context.operations = [];
|
|
754
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
755
|
-
version: 1,
|
|
756
|
-
width: 4,
|
|
757
|
-
height: 2,
|
|
758
|
-
styles: [null],
|
|
759
|
-
rows: [[], []],
|
|
760
|
-
images: [
|
|
761
|
-
{
|
|
762
|
-
id: "png:test",
|
|
763
|
-
format: "png",
|
|
764
|
-
bounds: [1, 0, 2, 2],
|
|
765
|
-
visibleBounds: [1, 0, 1, 2],
|
|
766
|
-
scalingMode: "stretch",
|
|
767
|
-
pixelSize: [2, 2],
|
|
768
|
-
dataBase64: "iVBORw==",
|
|
769
|
-
},
|
|
770
|
-
{
|
|
771
|
-
id: "png:test",
|
|
772
|
-
format: "png",
|
|
773
|
-
bounds: [3, 0, 1, 1],
|
|
774
|
-
visibleBounds: [3, 0, 1, 1],
|
|
775
|
-
scalingMode: "stretch",
|
|
776
|
-
pixelSize: [2, 2],
|
|
777
|
-
},
|
|
778
|
-
],
|
|
779
|
-
})));
|
|
780
|
-
await flushPromises();
|
|
781
|
-
|
|
782
|
-
expect(decodedBlobs).toHaveLength(1);
|
|
783
|
-
expect(drawImageOperations(context)).toEqual([
|
|
784
|
-
{
|
|
785
|
-
type: "drawImage",
|
|
786
|
-
imageId: "decoded-1",
|
|
787
|
-
x: 10,
|
|
788
|
-
y: 0,
|
|
789
|
-
width: 20,
|
|
790
|
-
height: 54,
|
|
791
|
-
},
|
|
792
|
-
{
|
|
793
|
-
type: "drawImage",
|
|
794
|
-
imageId: "decoded-1",
|
|
795
|
-
x: 30,
|
|
796
|
-
y: 0,
|
|
797
|
-
width: 10,
|
|
798
|
-
height: 27,
|
|
799
|
-
},
|
|
800
|
-
]);
|
|
801
|
-
expect(context.operations).toContainEqual({
|
|
802
|
-
type: "rect",
|
|
803
|
-
x: 10,
|
|
804
|
-
y: 0,
|
|
805
|
-
width: 10,
|
|
806
|
-
height: 54,
|
|
807
|
-
});
|
|
808
|
-
expect(context.operations).toContainEqual({
|
|
809
|
-
type: "clip",
|
|
810
|
-
path: [["rect", 10, 0, 10, 54]],
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
context.operations = [];
|
|
814
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
815
|
-
version: 1,
|
|
816
|
-
width: 4,
|
|
817
|
-
height: 2,
|
|
818
|
-
styles: [null],
|
|
819
|
-
rows: [[], []],
|
|
820
|
-
images: [
|
|
821
|
-
{
|
|
822
|
-
id: "png:test",
|
|
823
|
-
format: "png",
|
|
824
|
-
bounds: [0, 1, 1, 1],
|
|
825
|
-
visibleBounds: [0, 1, 1, 1],
|
|
826
|
-
scalingMode: "stretch",
|
|
827
|
-
},
|
|
828
|
-
],
|
|
829
|
-
})));
|
|
830
|
-
|
|
831
|
-
expect(decodedBlobs).toHaveLength(1);
|
|
832
|
-
expect(drawImageOperations(context)).toEqual([
|
|
833
|
-
{
|
|
834
|
-
type: "drawImage",
|
|
835
|
-
imageId: "decoded-1",
|
|
836
|
-
x: 0,
|
|
837
|
-
y: 27,
|
|
838
|
-
width: 10,
|
|
839
|
-
height: 27,
|
|
840
|
-
},
|
|
841
|
-
]);
|
|
842
|
-
} finally {
|
|
843
|
-
dom.restore();
|
|
844
|
-
}
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
test("runtime draws box and block elements procedurally instead of as font glyphs", async () => {
|
|
848
|
-
const dom = installFakeDOM();
|
|
849
|
-
try {
|
|
850
|
-
const bridge = new BrowserWASIBridge({
|
|
851
|
-
sceneId: "main",
|
|
852
|
-
columns: 4,
|
|
853
|
-
rows: 2,
|
|
854
|
-
});
|
|
855
|
-
const mount = new FakeElement("div");
|
|
856
|
-
const runtime = new WebHostSceneRuntime({
|
|
857
|
-
mount: mount as unknown as HTMLElement,
|
|
858
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
859
|
-
style: {
|
|
860
|
-
fontSize: 20,
|
|
861
|
-
fontFamily: "Test Mono",
|
|
862
|
-
},
|
|
863
|
-
bridge,
|
|
864
|
-
onInput: () => {},
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
await runtime.mount();
|
|
868
|
-
|
|
869
|
-
const canvas = dom.canvases[0]!;
|
|
870
|
-
const context = canvas.context;
|
|
871
|
-
context.operations = [];
|
|
872
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
873
|
-
version: 1,
|
|
874
|
-
width: 4,
|
|
875
|
-
height: 2,
|
|
876
|
-
styles: [
|
|
877
|
-
null,
|
|
878
|
-
{
|
|
879
|
-
fg: "#EBB33CFF",
|
|
880
|
-
},
|
|
881
|
-
],
|
|
882
|
-
rows: [
|
|
883
|
-
[
|
|
884
|
-
[0, "┌", 1, 1],
|
|
885
|
-
[1, "─", 1, 1],
|
|
886
|
-
[2, "▄", 1, 1],
|
|
887
|
-
[3, "A", 1, 1],
|
|
888
|
-
],
|
|
889
|
-
],
|
|
890
|
-
images: [],
|
|
891
|
-
})));
|
|
892
|
-
|
|
893
|
-
expect(fillTextOperations(context, "┌")).toEqual([]);
|
|
894
|
-
expect(fillTextOperations(context, "─")).toEqual([]);
|
|
895
|
-
expect(fillTextOperations(context, "▄")).toEqual([]);
|
|
896
|
-
expect(fillTextOperations(context, "A")).toHaveLength(1);
|
|
897
|
-
|
|
898
|
-
const boxFills = fillRectOperations(context, "#EBB33CFF");
|
|
899
|
-
expect(boxFills).toContainEqual({
|
|
900
|
-
type: "fillRect",
|
|
901
|
-
x: 4.5,
|
|
902
|
-
y: 13,
|
|
903
|
-
width: 5.5,
|
|
904
|
-
height: 1,
|
|
905
|
-
fillStyle: "#EBB33CFF",
|
|
906
|
-
globalAlpha: 1,
|
|
907
|
-
});
|
|
908
|
-
expect(boxFills).toContainEqual({
|
|
909
|
-
type: "fillRect",
|
|
910
|
-
x: 4.5,
|
|
911
|
-
y: 13,
|
|
912
|
-
width: 1,
|
|
913
|
-
height: 14,
|
|
914
|
-
fillStyle: "#EBB33CFF",
|
|
915
|
-
globalAlpha: 1,
|
|
916
|
-
});
|
|
917
|
-
expect(boxFills).toContainEqual({
|
|
918
|
-
type: "fillRect",
|
|
919
|
-
x: 10,
|
|
920
|
-
y: 13,
|
|
921
|
-
width: 5.5,
|
|
922
|
-
height: 1,
|
|
923
|
-
fillStyle: "#EBB33CFF",
|
|
924
|
-
globalAlpha: 1,
|
|
925
|
-
});
|
|
926
|
-
expect(boxFills).toContainEqual({
|
|
927
|
-
type: "fillRect",
|
|
928
|
-
x: 14.5,
|
|
929
|
-
y: 13,
|
|
930
|
-
width: 5.5,
|
|
931
|
-
height: 1,
|
|
932
|
-
fillStyle: "#EBB33CFF",
|
|
933
|
-
globalAlpha: 1,
|
|
934
|
-
});
|
|
935
|
-
expect(boxFills).toContainEqual({
|
|
936
|
-
type: "fillRect",
|
|
937
|
-
x: 20,
|
|
938
|
-
y: 13.5,
|
|
939
|
-
width: 10,
|
|
940
|
-
height: 13.5,
|
|
941
|
-
fillStyle: "#EBB33CFF",
|
|
942
|
-
globalAlpha: 1,
|
|
943
|
-
});
|
|
944
|
-
} finally {
|
|
945
|
-
dom.restore();
|
|
946
|
-
}
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
test("runtime draws rounded box corners with the cell foreground stroke", async () => {
|
|
950
|
-
const dom = installFakeDOM();
|
|
951
|
-
try {
|
|
952
|
-
const bridge = new BrowserWASIBridge({
|
|
953
|
-
sceneId: "main",
|
|
954
|
-
columns: 4,
|
|
955
|
-
rows: 1,
|
|
956
|
-
});
|
|
957
|
-
const mount = new FakeElement("div");
|
|
958
|
-
const runtime = new WebHostSceneRuntime({
|
|
959
|
-
mount: mount as unknown as HTMLElement,
|
|
960
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
961
|
-
style: {
|
|
962
|
-
fontSize: 20,
|
|
963
|
-
fontFamily: "Test Mono",
|
|
964
|
-
},
|
|
965
|
-
bridge,
|
|
966
|
-
onInput: () => {},
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
await runtime.mount();
|
|
970
|
-
|
|
971
|
-
const canvas = dom.canvases[0]!;
|
|
972
|
-
const context = canvas.context;
|
|
973
|
-
context.strokeStyle = "#000000";
|
|
974
|
-
context.operations = [];
|
|
975
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
976
|
-
version: 1,
|
|
977
|
-
width: 4,
|
|
978
|
-
height: 1,
|
|
979
|
-
styles: [
|
|
980
|
-
null,
|
|
981
|
-
{
|
|
982
|
-
fg: "#EBB33CFF",
|
|
983
|
-
},
|
|
984
|
-
],
|
|
985
|
-
rows: [
|
|
986
|
-
[
|
|
987
|
-
[0, "╭", 1, 1],
|
|
988
|
-
[1, "╮", 1, 1],
|
|
989
|
-
],
|
|
990
|
-
],
|
|
991
|
-
images: [],
|
|
992
|
-
})));
|
|
993
|
-
|
|
994
|
-
expect(fillTextOperations(context, "╭")).toEqual([]);
|
|
995
|
-
expect(fillTextOperations(context, "╮")).toEqual([]);
|
|
996
|
-
const strokes = context.operations.filter((operation) => operation.type === "stroke");
|
|
997
|
-
expect(strokes).toHaveLength(2);
|
|
998
|
-
expect(strokes.every((operation) => operation.strokeStyle === "#EBB33CFF")).toBe(true);
|
|
999
|
-
expect(strokes.every((operation) => operation.lineWidth === 1)).toBe(true);
|
|
1000
|
-
expect(strokes.every((operation) => operation.lineDash instanceof Array)).toBe(true);
|
|
1001
|
-
expect(strokes.every((operation) => (operation.lineDash as unknown[]).length === 0)).toBe(true);
|
|
1002
|
-
expect(strokes.every((operation) => {
|
|
1003
|
-
const path = operation.path as Array<[string, ...number[]]>;
|
|
1004
|
-
return path.some(([command]) => command === "bezierCurveTo");
|
|
1005
|
-
})).toBe(true);
|
|
1006
|
-
} finally {
|
|
1007
|
-
dom.restore();
|
|
1008
|
-
}
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
test("runtime keeps diagnostic stdout visible when output is not a surface frame", async () => {
|
|
1012
|
-
const dom = installFakeDOM();
|
|
1013
|
-
try {
|
|
1014
|
-
const bridge = new BrowserWASIBridge({
|
|
1015
|
-
sceneId: "main",
|
|
1016
|
-
columns: 4,
|
|
1017
|
-
rows: 2,
|
|
1018
|
-
});
|
|
1019
|
-
const mount = new FakeElement("div");
|
|
1020
|
-
const runtime = new WebHostSceneRuntime({
|
|
1021
|
-
mount: mount as unknown as HTMLElement,
|
|
1022
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
1023
|
-
style: {},
|
|
1024
|
-
bridge,
|
|
1025
|
-
onInput: () => {},
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
await runtime.mount();
|
|
1029
|
-
bridge.stdout.write(encoder.encode("legacy output\n"));
|
|
1030
|
-
|
|
1031
|
-
const diagnostic = runtime.terminalMount.children.find(
|
|
1032
|
-
(child) => child.className === "webhost-scene__diagnostic"
|
|
1033
|
-
);
|
|
1034
|
-
expect(diagnostic?.textContent).toBe("legacy output\n");
|
|
1035
|
-
} finally {
|
|
1036
|
-
dom.restore();
|
|
1037
|
-
}
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
test("runtime reports frame diagnostics without rendering them as terminal text", async () => {
|
|
1041
|
-
const dom = installFakeDOM();
|
|
1042
|
-
try {
|
|
1043
|
-
const bridge = new BrowserWASIBridge({
|
|
1044
|
-
sceneId: "main",
|
|
1045
|
-
columns: 4,
|
|
1046
|
-
rows: 2,
|
|
1047
|
-
});
|
|
1048
|
-
const diagnostics: unknown[] = [];
|
|
1049
|
-
const mount = new FakeElement("div");
|
|
1050
|
-
const runtime = new WebHostSceneRuntime({
|
|
1051
|
-
mount: mount as unknown as HTMLElement,
|
|
1052
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
1053
|
-
style: {},
|
|
1054
|
-
bridge,
|
|
1055
|
-
onInput: () => {},
|
|
1056
|
-
onFrameDiagnostic: (diagnostic) => diagnostics.push(diagnostic),
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
await runtime.mount();
|
|
1060
|
-
bridge.stdout.write(encoder.encode(
|
|
1061
|
-
'\u001EframeDiagnostic:{"format":"swift-tui-frame-diagnostics-v1",'
|
|
1062
|
-
+ '"header":["frame","total_ms"],"fields":["7","14.20"]}\n'
|
|
1063
|
-
));
|
|
1064
|
-
|
|
1065
|
-
expect(diagnostics).toEqual([
|
|
1066
|
-
{
|
|
1067
|
-
format: "swift-tui-frame-diagnostics-v1",
|
|
1068
|
-
header: ["frame", "total_ms"],
|
|
1069
|
-
fields: ["7", "14.20"],
|
|
1070
|
-
},
|
|
1071
|
-
]);
|
|
1072
|
-
expect(runtime.terminalMount.children.some(
|
|
1073
|
-
(child) => child.className === "webhost-scene__diagnostic"
|
|
1074
|
-
)).toBe(false);
|
|
1075
|
-
} finally {
|
|
1076
|
-
dom.restore();
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1079
|
-
|
|
1080
|
-
test("runtime maps browser input events to web-surface messages", async () => {
|
|
1081
|
-
const dom = installFakeDOM();
|
|
1082
|
-
try {
|
|
1083
|
-
const inputs: string[] = [];
|
|
1084
|
-
const mount = new FakeElement("div");
|
|
1085
|
-
const runtime = new WebHostSceneRuntime({
|
|
1086
|
-
mount: mount as unknown as HTMLElement,
|
|
1087
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
1088
|
-
style: { fontSize: 20 },
|
|
1089
|
-
onInput: (chunk) => {
|
|
1090
|
-
inputs.push(decoder.decode(chunk));
|
|
1091
|
-
},
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
await runtime.mount();
|
|
1095
|
-
runtime.resize(10, 4);
|
|
1096
|
-
|
|
1097
|
-
runtime.terminalMount.dispatch("keydown", {
|
|
1098
|
-
key: "a",
|
|
1099
|
-
shiftKey: true,
|
|
1100
|
-
altKey: false,
|
|
1101
|
-
ctrlKey: true,
|
|
1102
|
-
metaKey: false,
|
|
1103
|
-
isComposing: false,
|
|
1104
|
-
preventDefault() {},
|
|
1105
|
-
});
|
|
1106
|
-
runtime.terminalMount.dispatch("paste", {
|
|
1107
|
-
clipboardData: {
|
|
1108
|
-
getData: () => "hello world",
|
|
1109
|
-
},
|
|
1110
|
-
preventDefault() {},
|
|
1111
|
-
});
|
|
1112
|
-
runtime.terminalMount.dispatch("pointerdown", pointerEvent({
|
|
1113
|
-
button: 0,
|
|
1114
|
-
buttons: 1,
|
|
1115
|
-
clientX: 25,
|
|
1116
|
-
clientY: 10,
|
|
1117
|
-
pointerId: 7,
|
|
1118
|
-
}));
|
|
1119
|
-
runtime.terminalMount.dispatch("pointermove", pointerEvent({
|
|
1120
|
-
buttons: 1,
|
|
1121
|
-
clientX: 35,
|
|
1122
|
-
clientY: 30,
|
|
1123
|
-
pointerId: 7,
|
|
1124
|
-
}));
|
|
1125
|
-
runtime.terminalMount.dispatch("wheel", {
|
|
1126
|
-
clientX: 35,
|
|
1127
|
-
clientY: 30,
|
|
1128
|
-
deltaX: 0,
|
|
1129
|
-
deltaY: 20,
|
|
1130
|
-
shiftKey: false,
|
|
1131
|
-
altKey: true,
|
|
1132
|
-
ctrlKey: false,
|
|
1133
|
-
preventDefault() {},
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
expect(inputs).toEqual([
|
|
1137
|
-
"\u001Ekey:character:a:5\n",
|
|
1138
|
-
"\u001Epaste:hello%20world\n",
|
|
1139
|
-
"\u001Emouse:down:2.5:0.37037037037037035:primary:0:0:0\n",
|
|
1140
|
-
"\u001Emouse:dragged:3.5:1.1111111111111112:primary:0:0:0\n",
|
|
1141
|
-
"\u001Emouse:scrolled:3.5:1.1111111111111112:none:0:1:2\n",
|
|
1142
|
-
]);
|
|
1143
|
-
} finally {
|
|
1144
|
-
dom.restore();
|
|
1145
|
-
}
|
|
1146
|
-
});
|
|
1147
|
-
|
|
1148
|
-
test("runtime can run as a passive embed without stealing focus or wheel scroll", async () => {
|
|
1149
|
-
const dom = installFakeDOM();
|
|
1150
|
-
try {
|
|
1151
|
-
const inputs: string[] = [];
|
|
1152
|
-
const bridge = new BrowserWASIBridge({
|
|
1153
|
-
sceneId: "main",
|
|
1154
|
-
columns: 4,
|
|
1155
|
-
rows: 2,
|
|
1156
|
-
});
|
|
1157
|
-
const mount = new FakeElement("div");
|
|
1158
|
-
const runtime = new WebHostSceneRuntime({
|
|
1159
|
-
mount: mount as unknown as HTMLElement,
|
|
1160
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
1161
|
-
style: { fontSize: 20 },
|
|
1162
|
-
bridge,
|
|
1163
|
-
onInput: (chunk) => {
|
|
1164
|
-
inputs.push(decoder.decode(chunk));
|
|
1165
|
-
},
|
|
1166
|
-
synchronizeAccessibilityFocus: false,
|
|
1167
|
-
captureWheelInput: false,
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
await runtime.mount();
|
|
1171
|
-
bridge.stdout.write(encoder.encode(surfaceRecord({
|
|
1172
|
-
version: 2,
|
|
1173
|
-
width: 4,
|
|
1174
|
-
height: 2,
|
|
1175
|
-
styles: [null],
|
|
1176
|
-
rows: [[], []],
|
|
1177
|
-
accessibilityTree: [
|
|
1178
|
-
{
|
|
1179
|
-
id: "root/button",
|
|
1180
|
-
rect: [0, 0, 2, 1],
|
|
1181
|
-
role: "button",
|
|
1182
|
-
label: "Save",
|
|
1183
|
-
isFocused: true,
|
|
1184
|
-
},
|
|
1185
|
-
],
|
|
1186
|
-
})));
|
|
1187
|
-
|
|
1188
|
-
const tree = childWithClass(runtime.terminalMount, "webhost-scene__accessibility-tree");
|
|
1189
|
-
const button = childWithData(tree, "accessibilityId", "root/button");
|
|
1190
|
-
let wheelPrevented = false;
|
|
1191
|
-
|
|
1192
|
-
runtime.terminalMount.dispatch("wheel", {
|
|
1193
|
-
clientX: 35,
|
|
1194
|
-
clientY: 30,
|
|
1195
|
-
deltaX: 0,
|
|
1196
|
-
deltaY: 20,
|
|
1197
|
-
shiftKey: false,
|
|
1198
|
-
altKey: false,
|
|
1199
|
-
ctrlKey: false,
|
|
1200
|
-
preventDefault() {
|
|
1201
|
-
wheelPrevented = true;
|
|
1202
|
-
},
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
expect(button.focused).toBe(false);
|
|
1206
|
-
expect(button.lastFocusOptions).toBeUndefined();
|
|
1207
|
-
expect(inputs).toEqual([]);
|
|
1208
|
-
expect(wheelPrevented).toBe(false);
|
|
1209
|
-
} finally {
|
|
1210
|
-
dom.restore();
|
|
1211
|
-
}
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
test("chain mode captures the wheel when a region under the pointer can scroll", async () => {
|
|
1215
|
-
const dom = installFakeDOM();
|
|
1216
|
-
try {
|
|
1217
|
-
// Region covers the whole 4x2 surface; content is taller than the viewport
|
|
1218
|
-
// and scrolled to the top, so a downward wheel has headroom.
|
|
1219
|
-
const result = await wheelScenario({
|
|
1220
|
-
wheelMode: "chain",
|
|
1221
|
-
scrollRegions: [{ id: "list", rect: [0, 0, 4, 2], offset: [0, 0], content: [4, 10] }],
|
|
1222
|
-
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1223
|
-
});
|
|
1224
|
-
expect(result.captured).toBe(true);
|
|
1225
|
-
expect(result.wheelPrevented).toBe(true);
|
|
1226
|
-
} finally {
|
|
1227
|
-
dom.restore();
|
|
1228
|
-
}
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
test("chain mode lets the wheel fall through at the region's scroll edge", async () => {
|
|
1232
|
-
const dom = installFakeDOM();
|
|
1233
|
-
try {
|
|
1234
|
-
// Same region, but scrolled to the bottom (offset.y == maxY == 10 - 2),
|
|
1235
|
-
// so a further downward wheel has no headroom and must chain to the page.
|
|
1236
|
-
const result = await wheelScenario({
|
|
1237
|
-
wheelMode: "chain",
|
|
1238
|
-
scrollRegions: [{ id: "list", rect: [0, 0, 4, 2], offset: [0, 8], content: [4, 10] }],
|
|
1239
|
-
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1240
|
-
});
|
|
1241
|
-
expect(result.captured).toBe(false);
|
|
1242
|
-
expect(result.wheelPrevented).toBe(false);
|
|
1243
|
-
} finally {
|
|
1244
|
-
dom.restore();
|
|
1245
|
-
}
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1248
|
-
test("chain mode captures an upward wheel when scrolled away from the top", async () => {
|
|
1249
|
-
const dom = installFakeDOM();
|
|
1250
|
-
try {
|
|
1251
|
-
// At the bottom edge, downward chains but upward still has headroom.
|
|
1252
|
-
const result = await wheelScenario({
|
|
1253
|
-
wheelMode: "chain",
|
|
1254
|
-
scrollRegions: [{ id: "list", rect: [0, 0, 4, 2], offset: [0, 8], content: [4, 10] }],
|
|
1255
|
-
wheel: { clientX: 5, clientY: 5, deltaY: -20 },
|
|
1256
|
-
});
|
|
1257
|
-
expect(result.captured).toBe(true);
|
|
1258
|
-
expect(result.wheelPrevented).toBe(true);
|
|
1259
|
-
} finally {
|
|
1260
|
-
dom.restore();
|
|
1261
|
-
}
|
|
1262
|
-
});
|
|
1263
|
-
|
|
1264
|
-
test("chain mode falls through when the pointer is outside every scroll region", async () => {
|
|
1265
|
-
const dom = installFakeDOM();
|
|
1266
|
-
try {
|
|
1267
|
-
// Region only covers the right half (cells x>=2); wheel at cell (0,0).
|
|
1268
|
-
const result = await wheelScenario({
|
|
1269
|
-
wheelMode: "chain",
|
|
1270
|
-
scrollRegions: [{ id: "list", rect: [2, 0, 2, 2], offset: [0, 0], content: [2, 10] }],
|
|
1271
|
-
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1272
|
-
});
|
|
1273
|
-
expect(result.captured).toBe(false);
|
|
1274
|
-
expect(result.wheelPrevented).toBe(false);
|
|
1275
|
-
} finally {
|
|
1276
|
-
dom.restore();
|
|
1277
|
-
}
|
|
1278
|
-
});
|
|
1279
|
-
|
|
1280
|
-
test("chain mode falls through when the scene publishes no scroll regions", async () => {
|
|
1281
|
-
const dom = installFakeDOM();
|
|
1282
|
-
try {
|
|
1283
|
-
const result = await wheelScenario({
|
|
1284
|
-
wheelMode: "chain",
|
|
1285
|
-
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1286
|
-
});
|
|
1287
|
-
expect(result.captured).toBe(false);
|
|
1288
|
-
expect(result.wheelPrevented).toBe(false);
|
|
1289
|
-
} finally {
|
|
1290
|
-
dom.restore();
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
test("capture mode always eats the wheel even without scroll regions", async () => {
|
|
1295
|
-
const dom = installFakeDOM();
|
|
1296
|
-
try {
|
|
1297
|
-
const result = await wheelScenario({
|
|
1298
|
-
wheelMode: "capture",
|
|
1299
|
-
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1300
|
-
});
|
|
1301
|
-
expect(result.captured).toBe(true);
|
|
1302
|
-
expect(result.wheelPrevented).toBe(true);
|
|
1303
|
-
} finally {
|
|
1304
|
-
dom.restore();
|
|
1305
|
-
}
|
|
1306
|
-
});
|
|
1307
|
-
|
|
1308
|
-
test("legacy captureWheelInput:true maps to capture mode", async () => {
|
|
1309
|
-
const dom = installFakeDOM();
|
|
1310
|
-
try {
|
|
1311
|
-
const result = await wheelScenario({
|
|
1312
|
-
captureWheelInput: true,
|
|
1313
|
-
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1314
|
-
});
|
|
1315
|
-
expect(result.captured).toBe(true);
|
|
1316
|
-
expect(result.wheelPrevented).toBe(true);
|
|
1317
|
-
} finally {
|
|
1318
|
-
dom.restore();
|
|
1319
|
-
}
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
test("runtime preserves pointer movement within one cell", async () => {
|
|
1323
|
-
const dom = installFakeDOM();
|
|
1324
|
-
try {
|
|
1325
|
-
const inputs: string[] = [];
|
|
1326
|
-
const mount = new FakeElement("div");
|
|
1327
|
-
const runtime = new WebHostSceneRuntime({
|
|
1328
|
-
mount: mount as unknown as HTMLElement,
|
|
1329
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
1330
|
-
style: { fontSize: 20 },
|
|
1331
|
-
onInput: (chunk) => {
|
|
1332
|
-
inputs.push(decoder.decode(chunk));
|
|
1333
|
-
},
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
await runtime.mount();
|
|
1337
|
-
runtime.resize(10, 4);
|
|
1338
|
-
|
|
1339
|
-
runtime.terminalMount.dispatch("pointermove", pointerEvent({
|
|
1340
|
-
buttons: 1,
|
|
1341
|
-
clientX: 21,
|
|
1342
|
-
clientY: 27,
|
|
1343
|
-
pointerId: 7,
|
|
1344
|
-
}));
|
|
1345
|
-
runtime.terminalMount.dispatch("pointermove", pointerEvent({
|
|
1346
|
-
buttons: 1,
|
|
1347
|
-
clientX: 27,
|
|
1348
|
-
clientY: 27,
|
|
1349
|
-
pointerId: 7,
|
|
1350
|
-
}));
|
|
1351
|
-
|
|
1352
|
-
expect(inputs).toEqual([
|
|
1353
|
-
"\u001Emouse:dragged:2.1:1:primary:0:0:0\n",
|
|
1354
|
-
"\u001Emouse:dragged:2.7:1:primary:0:0:0\n",
|
|
1355
|
-
]);
|
|
1356
|
-
} finally {
|
|
1357
|
-
dom.restore();
|
|
1358
|
-
}
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
test("runtime completes captured drags when pointerup lands outside the grid", async () => {
|
|
1362
|
-
const dom = installFakeDOM();
|
|
1363
|
-
try {
|
|
1364
|
-
const inputs: string[] = [];
|
|
1365
|
-
const mount = new FakeElement("div");
|
|
1366
|
-
const runtime = new WebHostSceneRuntime({
|
|
1367
|
-
mount: mount as unknown as HTMLElement,
|
|
1368
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
1369
|
-
style: { fontSize: 20 },
|
|
1370
|
-
onInput: (chunk) => {
|
|
1371
|
-
inputs.push(decoder.decode(chunk));
|
|
1372
|
-
},
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
await runtime.mount();
|
|
1376
|
-
runtime.resize(10, 4);
|
|
1377
|
-
|
|
1378
|
-
runtime.terminalMount.dispatch("pointerdown", pointerEvent({
|
|
1379
|
-
button: 0,
|
|
1380
|
-
buttons: 1,
|
|
1381
|
-
clientX: 25,
|
|
1382
|
-
clientY: 10,
|
|
1383
|
-
pointerId: 7,
|
|
1384
|
-
}));
|
|
1385
|
-
runtime.terminalMount.dispatch("pointermove", pointerEvent({
|
|
1386
|
-
buttons: 1,
|
|
1387
|
-
clientX: 35,
|
|
1388
|
-
clientY: 30,
|
|
1389
|
-
pointerId: 7,
|
|
1390
|
-
}));
|
|
1391
|
-
runtime.terminalMount.dispatch("pointerup", pointerEvent({
|
|
1392
|
-
button: 0,
|
|
1393
|
-
buttons: 0,
|
|
1394
|
-
clientX: 125,
|
|
1395
|
-
clientY: 30,
|
|
1396
|
-
pointerId: 7,
|
|
1397
|
-
}));
|
|
1398
|
-
|
|
1399
|
-
expect(inputs).toEqual([
|
|
1400
|
-
"\u001Emouse:down:2.5:0.37037037037037035:primary:0:0:0\n",
|
|
1401
|
-
"\u001Emouse:dragged:3.5:1.1111111111111112:primary:0:0:0\n",
|
|
1402
|
-
"\u001Emouse:up:12.5:1.1111111111111112:primary:0:0:0\n",
|
|
1403
|
-
]);
|
|
1404
|
-
} finally {
|
|
1405
|
-
dom.restore();
|
|
1406
|
-
}
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
function pointerEvent(
|
|
1410
|
-
overrides: Record<string, unknown>
|
|
1411
|
-
): Record<string, unknown> {
|
|
1412
|
-
return {
|
|
1413
|
-
button: 0,
|
|
1414
|
-
buttons: 0,
|
|
1415
|
-
clientX: 0,
|
|
1416
|
-
clientY: 0,
|
|
1417
|
-
pointerId: 1,
|
|
1418
|
-
shiftKey: false,
|
|
1419
|
-
altKey: false,
|
|
1420
|
-
ctrlKey: false,
|
|
1421
|
-
preventDefault() {},
|
|
1422
|
-
...overrides,
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function fillTextOperations(
|
|
1427
|
-
context: RecordingCanvasContext,
|
|
1428
|
-
text: string
|
|
1429
|
-
): RecordingCanvasOperation[] {
|
|
1430
|
-
return context.operations.filter(
|
|
1431
|
-
(operation) => operation.type === "fillText" && operation.text === text
|
|
1432
|
-
);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function fillRectOperations(
|
|
1436
|
-
context: RecordingCanvasContext,
|
|
1437
|
-
fillStyle: string
|
|
1438
|
-
): RecordingCanvasOperation[] {
|
|
1439
|
-
return context.operations.filter(
|
|
1440
|
-
(operation) => operation.type === "fillRect" && operation.fillStyle === fillStyle
|
|
1441
|
-
);
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
function drawImageOperations(
|
|
1445
|
-
context: RecordingCanvasContext
|
|
1446
|
-
): RecordingCanvasOperation[] {
|
|
1447
|
-
return context.operations.filter((operation) => operation.type === "drawImage");
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
function readCanvasTextLikePixels(
|
|
1451
|
-
canvas: FakeCanvasElement
|
|
1452
|
-
): string {
|
|
1453
|
-
const textSamples = new Map<string, { x: number; y: number; text: string }>();
|
|
1454
|
-
|
|
1455
|
-
for (const operation of canvas.context.operations) {
|
|
1456
|
-
if (operation.type === "clearRect") {
|
|
1457
|
-
const rect = operationRect(operation);
|
|
1458
|
-
if (!rect) {
|
|
1459
|
-
continue;
|
|
1460
|
-
}
|
|
1461
|
-
for (const [key, sample] of textSamples) {
|
|
1462
|
-
if (textSampleInRect(sample, rect)) {
|
|
1463
|
-
textSamples.delete(key);
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
continue;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
if (operation.type !== "fillText" || typeof operation.text !== "string") {
|
|
1470
|
-
continue;
|
|
1471
|
-
}
|
|
1472
|
-
const x = Number(operation.x);
|
|
1473
|
-
const y = Number(operation.y);
|
|
1474
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
1475
|
-
continue;
|
|
1476
|
-
}
|
|
1477
|
-
textSamples.set(`${x}:${y}`, { x, y, text: operation.text });
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
const rows = new Map<number, Array<{ x: number; text: string }>>();
|
|
1481
|
-
for (const sample of textSamples.values()) {
|
|
1482
|
-
const row = rows.get(sample.y) ?? [];
|
|
1483
|
-
row.push({ x: sample.x, text: sample.text });
|
|
1484
|
-
rows.set(sample.y, row);
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
return Array.from(rows.entries())
|
|
1488
|
-
.sort(([lhs], [rhs]) => lhs - rhs)
|
|
1489
|
-
.map(([, row]) =>
|
|
1490
|
-
row
|
|
1491
|
-
.sort((lhs, rhs) => lhs.x - rhs.x)
|
|
1492
|
-
.map((sample) => sample.text)
|
|
1493
|
-
.join("")
|
|
1494
|
-
)
|
|
1495
|
-
.join("\n");
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
function operationRect(
|
|
1499
|
-
operation: RecordingCanvasOperation
|
|
1500
|
-
): { x: number; y: number; width: number; height: number } | undefined {
|
|
1501
|
-
const x = Number(operation.x);
|
|
1502
|
-
const y = Number(operation.y);
|
|
1503
|
-
const width = Number(operation.width);
|
|
1504
|
-
const height = Number(operation.height);
|
|
1505
|
-
if (
|
|
1506
|
-
!Number.isFinite(x)
|
|
1507
|
-
|| !Number.isFinite(y)
|
|
1508
|
-
|| !Number.isFinite(width)
|
|
1509
|
-
|| !Number.isFinite(height)
|
|
1510
|
-
) {
|
|
1511
|
-
return undefined;
|
|
1512
|
-
}
|
|
1513
|
-
return { x, y, width, height };
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
function textSampleInRect(
|
|
1517
|
-
sample: { x: number; y: number },
|
|
1518
|
-
rect: { x: number; y: number; width: number; height: number }
|
|
1519
|
-
): boolean {
|
|
1520
|
-
return sample.x >= rect.x
|
|
1521
|
-
&& sample.x < rect.x + rect.width
|
|
1522
|
-
&& sample.y >= rect.y
|
|
1523
|
-
&& sample.y < rect.y + rect.height;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
function childWithClass(
|
|
1527
|
-
element: FakeElement,
|
|
1528
|
-
className: string
|
|
1529
|
-
): FakeElement {
|
|
1530
|
-
const child = element.children.find((child) => child.className === className);
|
|
1531
|
-
if (!child) {
|
|
1532
|
-
throw new Error(`missing child with class ${className}`);
|
|
1533
|
-
}
|
|
1534
|
-
return child;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function childWithData(
|
|
1538
|
-
element: FakeElement,
|
|
1539
|
-
key: string,
|
|
1540
|
-
value: string
|
|
1541
|
-
): FakeElement {
|
|
1542
|
-
const child = element.children.find((child) => child.dataset[key] === value);
|
|
1543
|
-
if (!child) {
|
|
1544
|
-
throw new Error(`missing child with data-${key} ${value}`);
|
|
1545
|
-
}
|
|
1546
|
-
return child;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
async function flushPromises(): Promise<void> {
|
|
1550
|
-
await Promise.resolve();
|
|
1551
|
-
await Promise.resolve();
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
// Drives a single wheel event over a 4x2 surface (cellWidth 10, cellHeight 27
|
|
1555
|
-
// under the fake DOM) with the given wheel mode and published scroll regions,
|
|
1556
|
-
// and reports whether the wheel was forwarded to the app and/or preventDefault'd.
|
|
1557
|
-
// Assumes a fake DOM is already installed by the caller.
|
|
1558
|
-
async function wheelScenario(options: {
|
|
1559
|
-
wheelMode?: WheelMode;
|
|
1560
|
-
captureWheelInput?: boolean;
|
|
1561
|
-
scrollRegions?: Array<Record<string, unknown>>;
|
|
1562
|
-
wheel: { clientX: number; clientY: number; deltaX?: number; deltaY?: number };
|
|
1563
|
-
}): Promise<{ captured: boolean; wheelPrevented: boolean }> {
|
|
1564
|
-
const inputs: string[] = [];
|
|
1565
|
-
const bridge = new BrowserWASIBridge({ sceneId: "main", columns: 4, rows: 2 });
|
|
1566
|
-
const mount = new FakeElement("div");
|
|
1567
|
-
const runtime = new WebHostSceneRuntime({
|
|
1568
|
-
mount: mount as unknown as HTMLElement,
|
|
1569
|
-
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
1570
|
-
style: { fontSize: 20 },
|
|
1571
|
-
bridge,
|
|
1572
|
-
onInput: (chunk) => {
|
|
1573
|
-
inputs.push(decoder.decode(chunk));
|
|
1574
|
-
},
|
|
1575
|
-
synchronizeAccessibilityFocus: false,
|
|
1576
|
-
wheelMode: options.wheelMode,
|
|
1577
|
-
captureWheelInput: options.captureWheelInput,
|
|
1578
|
-
});
|
|
1579
|
-
|
|
1580
|
-
await runtime.mount();
|
|
1581
|
-
const frame: Record<string, unknown> = {
|
|
1582
|
-
version: 2,
|
|
1583
|
-
width: 4,
|
|
1584
|
-
height: 2,
|
|
1585
|
-
styles: [null],
|
|
1586
|
-
rows: [[], []],
|
|
1587
|
-
};
|
|
1588
|
-
if (options.scrollRegions) {
|
|
1589
|
-
frame.scrollRegions = options.scrollRegions;
|
|
1590
|
-
}
|
|
1591
|
-
bridge.stdout.write(encoder.encode(surfaceRecord(frame)));
|
|
1592
|
-
|
|
1593
|
-
let wheelPrevented = false;
|
|
1594
|
-
runtime.terminalMount.dispatch("wheel", {
|
|
1595
|
-
clientX: options.wheel.clientX,
|
|
1596
|
-
clientY: options.wheel.clientY,
|
|
1597
|
-
deltaX: options.wheel.deltaX ?? 0,
|
|
1598
|
-
deltaY: options.wheel.deltaY ?? 0,
|
|
1599
|
-
shiftKey: false,
|
|
1600
|
-
altKey: false,
|
|
1601
|
-
ctrlKey: false,
|
|
1602
|
-
preventDefault() {
|
|
1603
|
-
wheelPrevented = true;
|
|
1604
|
-
},
|
|
1605
|
-
});
|
|
1606
|
-
|
|
1607
|
-
return { captured: inputs.some((i) => i.includes("scrolled")), wheelPrevented };
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
function surfaceRecord(
|
|
1611
|
-
frame: Record<string, unknown>
|
|
1612
|
-
): string {
|
|
1613
|
-
return `\u001Esurface:${JSON.stringify(frame)}\n`;
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
interface FakeDOMOptions {
|
|
1617
|
-
devicePixelRatio?: number;
|
|
1618
|
-
createImageBitmap?: (blob: Blob) => Promise<unknown>;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
function installFakeDOM(
|
|
1622
|
-
options: FakeDOMOptions = {}
|
|
1623
|
-
): {
|
|
1624
|
-
canvases: FakeCanvasElement[];
|
|
1625
|
-
restore(): void;
|
|
1626
|
-
} {
|
|
1627
|
-
const previousDocument = globalThis.document;
|
|
1628
|
-
const previousWindow = globalThis.window;
|
|
1629
|
-
const previousResizeObserver = globalThis.ResizeObserver;
|
|
1630
|
-
const previousCreateImageBitmap = globalThis.createImageBitmap;
|
|
1631
|
-
const canvases: FakeCanvasElement[] = [];
|
|
1632
|
-
|
|
1633
|
-
globalThis.document = {
|
|
1634
|
-
createElement: (tagName: string) => {
|
|
1635
|
-
if (tagName === "canvas") {
|
|
1636
|
-
const canvas = new FakeCanvasElement();
|
|
1637
|
-
canvases.push(canvas);
|
|
1638
|
-
return canvas;
|
|
1639
|
-
}
|
|
1640
|
-
return new FakeElement(tagName);
|
|
1641
|
-
},
|
|
1642
|
-
} as unknown as Document;
|
|
1643
|
-
globalThis.window = {
|
|
1644
|
-
devicePixelRatio: options.devicePixelRatio ?? 1,
|
|
1645
|
-
} as unknown as Window & typeof globalThis;
|
|
1646
|
-
globalThis.ResizeObserver = FakeResizeObserver as unknown as typeof ResizeObserver;
|
|
1647
|
-
if (options.createImageBitmap) {
|
|
1648
|
-
globalThis.createImageBitmap = options.createImageBitmap as typeof createImageBitmap;
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
return {
|
|
1652
|
-
canvases,
|
|
1653
|
-
restore: () => {
|
|
1654
|
-
globalThis.document = previousDocument;
|
|
1655
|
-
globalThis.window = previousWindow;
|
|
1656
|
-
globalThis.ResizeObserver = previousResizeObserver;
|
|
1657
|
-
globalThis.createImageBitmap = previousCreateImageBitmap;
|
|
1658
|
-
},
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
class FakeResizeObserver {
|
|
1663
|
-
observe(): void {}
|
|
1664
|
-
disconnect(): void {}
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
class FakeStyle {
|
|
1668
|
-
[key: string]: unknown;
|
|
1669
|
-
|
|
1670
|
-
private readonly values = new Map<string, string>();
|
|
1671
|
-
private readonly priorities = new Map<string, string>();
|
|
1672
|
-
|
|
1673
|
-
setProperty(
|
|
1674
|
-
name: string,
|
|
1675
|
-
value: string,
|
|
1676
|
-
priority?: string
|
|
1677
|
-
): void {
|
|
1678
|
-
this.values.set(name, value);
|
|
1679
|
-
this.priorities.set(name, priority ?? "");
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
getPropertyValue(
|
|
1683
|
-
name: string
|
|
1684
|
-
): string {
|
|
1685
|
-
return this.values.get(name) ?? "";
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
getPropertyPriority(
|
|
1689
|
-
name: string
|
|
1690
|
-
): string {
|
|
1691
|
-
return this.priorities.get(name) ?? "";
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
class FakeElement {
|
|
1696
|
-
readonly tagName: string;
|
|
1697
|
-
readonly style = new FakeStyle();
|
|
1698
|
-
readonly dataset: Record<string, string> = {};
|
|
1699
|
-
readonly children: FakeElement[] = [];
|
|
1700
|
-
private readonly eventListeners = new Map<string, Set<(event: Record<string, unknown>) => void>>();
|
|
1701
|
-
private readonly attributes = new Map<string, string>();
|
|
1702
|
-
|
|
1703
|
-
className = "";
|
|
1704
|
-
id = "";
|
|
1705
|
-
hidden = false;
|
|
1706
|
-
focused = false;
|
|
1707
|
-
lastFocusOptions: FocusOptions | undefined;
|
|
1708
|
-
tabIndex = 0;
|
|
1709
|
-
textContent = "";
|
|
1710
|
-
rect = {
|
|
1711
|
-
left: 0,
|
|
1712
|
-
top: 0,
|
|
1713
|
-
width: 100,
|
|
1714
|
-
height: 108,
|
|
1715
|
-
right: 100,
|
|
1716
|
-
bottom: 108,
|
|
1717
|
-
};
|
|
1718
|
-
|
|
1719
|
-
constructor(tagName: string) {
|
|
1720
|
-
this.tagName = tagName.toUpperCase();
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
append(
|
|
1724
|
-
...children: FakeElement[]
|
|
1725
|
-
): void {
|
|
1726
|
-
this.children.push(...children);
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
appendChild(
|
|
1730
|
-
child: FakeElement
|
|
1731
|
-
): FakeElement {
|
|
1732
|
-
this.children.push(child);
|
|
1733
|
-
return child;
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
replaceChildren(
|
|
1737
|
-
...children: FakeElement[]
|
|
1738
|
-
): void {
|
|
1739
|
-
this.children.splice(0, this.children.length, ...children);
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
remove(): void {}
|
|
1743
|
-
focus(options?: FocusOptions): void {
|
|
1744
|
-
this.focused = true;
|
|
1745
|
-
this.lastFocusOptions = options;
|
|
1746
|
-
}
|
|
1747
|
-
setPointerCapture(): void {}
|
|
1748
|
-
releasePointerCapture(): void {}
|
|
1749
|
-
|
|
1750
|
-
setAttribute(
|
|
1751
|
-
name: string,
|
|
1752
|
-
value: string
|
|
1753
|
-
): void {
|
|
1754
|
-
this.attributes.set(name, value);
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
getAttribute(
|
|
1758
|
-
name: string
|
|
1759
|
-
): string | null {
|
|
1760
|
-
return this.attributes.get(name) ?? null;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
getBoundingClientRect(): typeof this.rect {
|
|
1764
|
-
return this.rect;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
addEventListener(
|
|
1768
|
-
type: string,
|
|
1769
|
-
listener: (event: Record<string, unknown>) => void
|
|
1770
|
-
): void {
|
|
1771
|
-
let listeners = this.eventListeners.get(type);
|
|
1772
|
-
if (!listeners) {
|
|
1773
|
-
listeners = new Set();
|
|
1774
|
-
this.eventListeners.set(type, listeners);
|
|
1775
|
-
}
|
|
1776
|
-
listeners.add(listener);
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
removeEventListener(
|
|
1780
|
-
type: string,
|
|
1781
|
-
listener: (event: Record<string, unknown>) => void
|
|
1782
|
-
): void {
|
|
1783
|
-
this.eventListeners.get(type)?.delete(listener);
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
dispatch(
|
|
1787
|
-
type: string,
|
|
1788
|
-
event: Record<string, unknown>
|
|
1789
|
-
): void {
|
|
1790
|
-
for (const listener of this.eventListeners.get(type) ?? []) {
|
|
1791
|
-
listener(event);
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
class FakeCanvasElement extends FakeElement {
|
|
1797
|
-
readonly context = new RecordingCanvasContext();
|
|
1798
|
-
width = 0;
|
|
1799
|
-
height = 0;
|
|
1800
|
-
|
|
1801
|
-
constructor() {
|
|
1802
|
-
super("canvas");
|
|
1803
|
-
this.rect = {
|
|
1804
|
-
left: 0,
|
|
1805
|
-
top: 0,
|
|
1806
|
-
width: 100,
|
|
1807
|
-
height: 108,
|
|
1808
|
-
right: 100,
|
|
1809
|
-
bottom: 108,
|
|
1810
|
-
};
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
getContext(
|
|
1814
|
-
contextId: string
|
|
1815
|
-
): RecordingCanvasContext | undefined {
|
|
1816
|
-
return contextId === "2d" ? this.context : undefined;
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
type RecordingCanvasOperation = Record<string, unknown>;
|
|
1821
|
-
|
|
1822
|
-
class RecordingCanvasContext {
|
|
1823
|
-
operations: RecordingCanvasOperation[] = [];
|
|
1824
|
-
fillStyle = "";
|
|
1825
|
-
strokeStyle = "";
|
|
1826
|
-
font = "";
|
|
1827
|
-
textBaseline = "";
|
|
1828
|
-
globalAlpha = 1;
|
|
1829
|
-
lineWidth = 1;
|
|
1830
|
-
lineCap = "butt";
|
|
1831
|
-
|
|
1832
|
-
private lineDash: number[] = [];
|
|
1833
|
-
private path: Array<[string, ...number[]]> = [];
|
|
1834
|
-
|
|
1835
|
-
measureText(
|
|
1836
|
-
text: string
|
|
1837
|
-
): { width: number } {
|
|
1838
|
-
return { width: Math.max(1, Array.from(text).length) * 10 };
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
setTransform(
|
|
1842
|
-
a: number,
|
|
1843
|
-
b: number,
|
|
1844
|
-
c: number,
|
|
1845
|
-
d: number,
|
|
1846
|
-
e: number,
|
|
1847
|
-
f: number
|
|
1848
|
-
): void {
|
|
1849
|
-
this.operations.push({ type: "setTransform", a, b, c, d, e, f });
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
clearRect(
|
|
1853
|
-
x: number,
|
|
1854
|
-
y: number,
|
|
1855
|
-
width: number,
|
|
1856
|
-
height: number
|
|
1857
|
-
): void {
|
|
1858
|
-
this.operations.push({ type: "clearRect", x, y, width, height });
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
fillRect(
|
|
1862
|
-
x: number,
|
|
1863
|
-
y: number,
|
|
1864
|
-
width: number,
|
|
1865
|
-
height: number
|
|
1866
|
-
): void {
|
|
1867
|
-
this.operations.push({
|
|
1868
|
-
type: "fillRect",
|
|
1869
|
-
x,
|
|
1870
|
-
y,
|
|
1871
|
-
width,
|
|
1872
|
-
height,
|
|
1873
|
-
fillStyle: this.fillStyle,
|
|
1874
|
-
globalAlpha: this.globalAlpha,
|
|
1875
|
-
});
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
fillText(
|
|
1879
|
-
text: string,
|
|
1880
|
-
x: number,
|
|
1881
|
-
y: number
|
|
1882
|
-
): void {
|
|
1883
|
-
this.operations.push({
|
|
1884
|
-
type: "fillText",
|
|
1885
|
-
text,
|
|
1886
|
-
x,
|
|
1887
|
-
y,
|
|
1888
|
-
fillStyle: this.fillStyle,
|
|
1889
|
-
font: this.font,
|
|
1890
|
-
globalAlpha: this.globalAlpha,
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
beginPath(): void {
|
|
1895
|
-
this.path = [];
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
save(): void {
|
|
1899
|
-
this.operations.push({ type: "save" });
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
restore(): void {
|
|
1903
|
-
this.operations.push({ type: "restore" });
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
rect(
|
|
1907
|
-
x: number,
|
|
1908
|
-
y: number,
|
|
1909
|
-
width: number,
|
|
1910
|
-
height: number
|
|
1911
|
-
): void {
|
|
1912
|
-
this.path.push(["rect", x, y, width, height]);
|
|
1913
|
-
this.operations.push({ type: "rect", x, y, width, height });
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
clip(): void {
|
|
1917
|
-
this.operations.push({
|
|
1918
|
-
type: "clip",
|
|
1919
|
-
path: [...this.path],
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
drawImage(
|
|
1924
|
-
image: unknown,
|
|
1925
|
-
x: number,
|
|
1926
|
-
y: number,
|
|
1927
|
-
width: number,
|
|
1928
|
-
height: number
|
|
1929
|
-
): void {
|
|
1930
|
-
this.operations.push({
|
|
1931
|
-
type: "drawImage",
|
|
1932
|
-
imageId: image && typeof image === "object" && "imageId" in image
|
|
1933
|
-
? (image as { imageId: unknown }).imageId
|
|
1934
|
-
: undefined,
|
|
1935
|
-
x,
|
|
1936
|
-
y,
|
|
1937
|
-
width,
|
|
1938
|
-
height,
|
|
1939
|
-
});
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
moveTo(
|
|
1943
|
-
x: number,
|
|
1944
|
-
y: number
|
|
1945
|
-
): void {
|
|
1946
|
-
this.path.push(["moveTo", x, y]);
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
lineTo(
|
|
1950
|
-
x: number,
|
|
1951
|
-
y: number
|
|
1952
|
-
): void {
|
|
1953
|
-
this.path.push(["lineTo", x, y]);
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
bezierCurveTo(
|
|
1957
|
-
control1X: number,
|
|
1958
|
-
control1Y: number,
|
|
1959
|
-
control2X: number,
|
|
1960
|
-
control2Y: number,
|
|
1961
|
-
x: number,
|
|
1962
|
-
y: number
|
|
1963
|
-
): void {
|
|
1964
|
-
this.path.push(["bezierCurveTo", control1X, control1Y, control2X, control2Y, x, y]);
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
stroke(): void {
|
|
1968
|
-
this.operations.push({
|
|
1969
|
-
type: "stroke",
|
|
1970
|
-
strokeStyle: this.strokeStyle,
|
|
1971
|
-
lineWidth: this.lineWidth,
|
|
1972
|
-
lineDash: [...this.lineDash],
|
|
1973
|
-
path: [...this.path],
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
setLineDash(
|
|
1978
|
-
lineDash: number[]
|
|
1979
|
-
): void {
|
|
1980
|
-
this.lineDash = [...lineDash];
|
|
1981
|
-
}
|
|
1982
|
-
}
|