@flighthq/application 0.1.0
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/dist/application.d.ts +24 -0
- package/dist/application.d.ts.map +1 -0
- package/dist/application.js +371 -0
- package/dist/application.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/window.d.ts +63 -0
- package/dist/window.d.ts.map +1 -0
- package/dist/window.js +661 -0
- package/dist/window.js.map +1 -0
- package/package.json +38 -0
- package/src/application.test.ts +743 -0
- package/src/window.test.ts +1185 -0
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import { cancelSignal, connectSignal, emitSignal } from '@flighthq/signals';
|
|
2
|
+
import type { Matrix, RenderState, WindowBackend } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
attachWindowClose,
|
|
6
|
+
attachWindowDropFile,
|
|
7
|
+
attachWindowFocus,
|
|
8
|
+
attachWindowFullscreen,
|
|
9
|
+
attachWindowMove,
|
|
10
|
+
attachWindowOrientation,
|
|
11
|
+
attachWindowRenderContext,
|
|
12
|
+
attachWindowRenderState,
|
|
13
|
+
attachWindowResize,
|
|
14
|
+
attachWindowVisibility,
|
|
15
|
+
centerWindow,
|
|
16
|
+
closeWindow,
|
|
17
|
+
computeWindowDeviceTransform,
|
|
18
|
+
createApplicationWindow,
|
|
19
|
+
createWebWindowBackend,
|
|
20
|
+
detachWindowClose,
|
|
21
|
+
detachWindowDropFile,
|
|
22
|
+
detachWindowFocus,
|
|
23
|
+
detachWindowFullscreen,
|
|
24
|
+
detachWindowMove,
|
|
25
|
+
detachWindowOrientation,
|
|
26
|
+
detachWindowRenderContext,
|
|
27
|
+
detachWindowRenderState,
|
|
28
|
+
detachWindowResize,
|
|
29
|
+
detachWindowVisibility,
|
|
30
|
+
disposeApplicationWindow,
|
|
31
|
+
exitApplicationFullscreen,
|
|
32
|
+
exitApplicationPointerLock,
|
|
33
|
+
flashWindowFrame,
|
|
34
|
+
focusWindow,
|
|
35
|
+
getWindowBackend,
|
|
36
|
+
getWindowBounds,
|
|
37
|
+
getWindowDisplay,
|
|
38
|
+
hideWindow,
|
|
39
|
+
lockApplicationPointer,
|
|
40
|
+
maximizeWindow,
|
|
41
|
+
minimizeWindow,
|
|
42
|
+
openWindow,
|
|
43
|
+
prepareElementForInput,
|
|
44
|
+
requestApplicationFullscreen,
|
|
45
|
+
requestWindowAttention,
|
|
46
|
+
requestWindowClose,
|
|
47
|
+
restoreWindow,
|
|
48
|
+
setWindowAlwaysOnTop,
|
|
49
|
+
setWindowBackend,
|
|
50
|
+
setWindowContentProtection,
|
|
51
|
+
setWindowFullscreen,
|
|
52
|
+
setWindowHasShadow,
|
|
53
|
+
setWindowIcon,
|
|
54
|
+
setWindowMaximumSize,
|
|
55
|
+
setWindowMenuBarVisible,
|
|
56
|
+
setWindowMinimumSize,
|
|
57
|
+
setWindowOpacity,
|
|
58
|
+
setWindowParent,
|
|
59
|
+
setWindowPosition,
|
|
60
|
+
setWindowProgress,
|
|
61
|
+
setWindowResizable,
|
|
62
|
+
setWindowSize,
|
|
63
|
+
setWindowSkipTaskbar,
|
|
64
|
+
setWindowTitle,
|
|
65
|
+
showWindow,
|
|
66
|
+
} from './window';
|
|
67
|
+
|
|
68
|
+
function makeRenderState(): RenderState {
|
|
69
|
+
return { renderTransform2D: { a: 0, b: 0, c: 0, d: 0, tx: 0, ty: 0 } } as unknown as RenderState;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function recordingWindowBackend(): WindowBackend & { calls: string[] } {
|
|
73
|
+
const calls: string[] = [];
|
|
74
|
+
return {
|
|
75
|
+
calls,
|
|
76
|
+
open(_win, options) {
|
|
77
|
+
calls.push(`open:${options.title ?? ''}`);
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
close() {
|
|
81
|
+
calls.push('close');
|
|
82
|
+
},
|
|
83
|
+
setTitle(_win, title) {
|
|
84
|
+
calls.push(`setTitle:${title}`);
|
|
85
|
+
},
|
|
86
|
+
setPosition(_win, x, y) {
|
|
87
|
+
calls.push(`setPosition:${x},${y}`);
|
|
88
|
+
},
|
|
89
|
+
setSize(_win, width, height) {
|
|
90
|
+
calls.push(`setSize:${width},${height}`);
|
|
91
|
+
},
|
|
92
|
+
getBounds(_win, out) {
|
|
93
|
+
out.x = 1;
|
|
94
|
+
out.y = 2;
|
|
95
|
+
out.width = 3;
|
|
96
|
+
out.height = 4;
|
|
97
|
+
return out;
|
|
98
|
+
},
|
|
99
|
+
minimize() {
|
|
100
|
+
calls.push('minimize');
|
|
101
|
+
},
|
|
102
|
+
maximize() {
|
|
103
|
+
calls.push('maximize');
|
|
104
|
+
},
|
|
105
|
+
restore() {
|
|
106
|
+
calls.push('restore');
|
|
107
|
+
},
|
|
108
|
+
focus() {
|
|
109
|
+
calls.push('focus');
|
|
110
|
+
},
|
|
111
|
+
show() {
|
|
112
|
+
calls.push('show');
|
|
113
|
+
},
|
|
114
|
+
hide() {
|
|
115
|
+
calls.push('hide');
|
|
116
|
+
},
|
|
117
|
+
center() {
|
|
118
|
+
calls.push('center');
|
|
119
|
+
},
|
|
120
|
+
setResizable(_win, resizable) {
|
|
121
|
+
calls.push(`setResizable:${resizable}`);
|
|
122
|
+
},
|
|
123
|
+
setAlwaysOnTop(_win, alwaysOnTop) {
|
|
124
|
+
calls.push(`setAlwaysOnTop:${alwaysOnTop}`);
|
|
125
|
+
},
|
|
126
|
+
setMinimumSize(_win, width, height) {
|
|
127
|
+
calls.push(`setMinimumSize:${width},${height}`);
|
|
128
|
+
},
|
|
129
|
+
setMaximumSize(_win, width, height) {
|
|
130
|
+
calls.push(`setMaximumSize:${width},${height}`);
|
|
131
|
+
},
|
|
132
|
+
setFullscreen(_win, fullscreen) {
|
|
133
|
+
calls.push(`setFullscreen:${fullscreen}`);
|
|
134
|
+
},
|
|
135
|
+
setIcon(_win, icon) {
|
|
136
|
+
calls.push(`setIcon:${icon}`);
|
|
137
|
+
},
|
|
138
|
+
setOpacity(_win, opacity) {
|
|
139
|
+
calls.push(`setOpacity:${opacity}`);
|
|
140
|
+
},
|
|
141
|
+
setSkipTaskbar(_win, skip) {
|
|
142
|
+
calls.push(`setSkipTaskbar:${skip}`);
|
|
143
|
+
},
|
|
144
|
+
setMenuBarVisible(_win, visible) {
|
|
145
|
+
calls.push(`setMenuBarVisible:${visible}`);
|
|
146
|
+
},
|
|
147
|
+
setParent(_win, parent) {
|
|
148
|
+
calls.push(`setParent:${parent === null ? 'null' : 'win'}`);
|
|
149
|
+
},
|
|
150
|
+
setProgress(_win, progress) {
|
|
151
|
+
calls.push(`setProgress:${progress}`);
|
|
152
|
+
},
|
|
153
|
+
requestAttention(_win, attention) {
|
|
154
|
+
calls.push(`requestAttention:${attention}`);
|
|
155
|
+
},
|
|
156
|
+
setContentProtection(_win, enabled) {
|
|
157
|
+
calls.push(`setContentProtection:${enabled}`);
|
|
158
|
+
},
|
|
159
|
+
flashWindowFrame() {
|
|
160
|
+
calls.push('flashWindowFrame');
|
|
161
|
+
},
|
|
162
|
+
setHasShadow(_win, hasShadow) {
|
|
163
|
+
calls.push(`setHasShadow:${hasShadow}`);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
afterEach(() => setWindowBackend(null));
|
|
169
|
+
|
|
170
|
+
describe('attachWindowClose', () => {
|
|
171
|
+
it('emits onClose on pagehide', () => {
|
|
172
|
+
const win = createApplicationWindow();
|
|
173
|
+
let closed = false;
|
|
174
|
+
connectSignal(win.onClose, () => {
|
|
175
|
+
closed = true;
|
|
176
|
+
});
|
|
177
|
+
attachWindowClose(win);
|
|
178
|
+
window.dispatchEvent(new Event('pagehide'));
|
|
179
|
+
expect(closed).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('emits onCloseRequest on beforeunload', () => {
|
|
183
|
+
const win = createApplicationWindow();
|
|
184
|
+
let requested = false;
|
|
185
|
+
connectSignal(win.onCloseRequest, () => {
|
|
186
|
+
requested = true;
|
|
187
|
+
});
|
|
188
|
+
attachWindowClose(win);
|
|
189
|
+
window.dispatchEvent(new Event('beforeunload'));
|
|
190
|
+
expect(requested).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('attachWindowDropFile', () => {
|
|
195
|
+
it('emits onDropFile with file name', () => {
|
|
196
|
+
const element = document.createElement('div');
|
|
197
|
+
const win = createApplicationWindow();
|
|
198
|
+
let received: string | null = null;
|
|
199
|
+
connectSignal(win.onDropFile, (path) => {
|
|
200
|
+
received = path;
|
|
201
|
+
});
|
|
202
|
+
attachWindowDropFile(win, element);
|
|
203
|
+
|
|
204
|
+
const event = new Event('drop') as Event & { dataTransfer: { files: { name: string }[] } };
|
|
205
|
+
Object.defineProperty(event, 'dataTransfer', {
|
|
206
|
+
value: { files: [{ name: 'test.png' }] },
|
|
207
|
+
});
|
|
208
|
+
element.dispatchEvent(event);
|
|
209
|
+
|
|
210
|
+
expect(received).toBe('test.png');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('attachWindowFocus', () => {
|
|
215
|
+
it('emits onFocusIn on focus', () => {
|
|
216
|
+
const element = document.createElement('div');
|
|
217
|
+
const win = createApplicationWindow();
|
|
218
|
+
let called = false;
|
|
219
|
+
connectSignal(win.onFocusIn, () => {
|
|
220
|
+
called = true;
|
|
221
|
+
});
|
|
222
|
+
attachWindowFocus(win, element);
|
|
223
|
+
element.dispatchEvent(new Event('focus'));
|
|
224
|
+
expect(called).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('emits onFocusOut on blur', () => {
|
|
228
|
+
const element = document.createElement('div');
|
|
229
|
+
const win = createApplicationWindow();
|
|
230
|
+
let called = false;
|
|
231
|
+
connectSignal(win.onFocusOut, () => {
|
|
232
|
+
called = true;
|
|
233
|
+
});
|
|
234
|
+
attachWindowFocus(win, element);
|
|
235
|
+
element.dispatchEvent(new Event('blur'));
|
|
236
|
+
expect(called).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('attachWindowFullscreen', () => {
|
|
241
|
+
it('emits onFullscreenChanged', () => {
|
|
242
|
+
const win = createApplicationWindow();
|
|
243
|
+
let called = false;
|
|
244
|
+
connectSignal(win.onFullscreenChanged, () => {
|
|
245
|
+
called = true;
|
|
246
|
+
});
|
|
247
|
+
attachWindowFullscreen(win);
|
|
248
|
+
document.dispatchEvent(new Event('fullscreenchange'));
|
|
249
|
+
expect(called).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('attachWindowMove', () => {
|
|
254
|
+
it('emits onMove and updates position when screenX/screenY changes', () => {
|
|
255
|
+
const win = createApplicationWindow();
|
|
256
|
+
win.x = 0;
|
|
257
|
+
win.y = 0;
|
|
258
|
+
let moved = false;
|
|
259
|
+
connectSignal(win.onMove, () => {
|
|
260
|
+
moved = true;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
vi.stubGlobal('screenX', 100);
|
|
264
|
+
vi.stubGlobal('screenY', 200);
|
|
265
|
+
attachWindowMove(win);
|
|
266
|
+
window.dispatchEvent(new Event('resize'));
|
|
267
|
+
|
|
268
|
+
expect(moved).toBe(true);
|
|
269
|
+
expect(win.x).toBe(100);
|
|
270
|
+
expect(win.y).toBe(200);
|
|
271
|
+
vi.unstubAllGlobals();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('does not emit onMove when position has not changed', () => {
|
|
275
|
+
const win = createApplicationWindow();
|
|
276
|
+
win.x = 50;
|
|
277
|
+
win.y = 50;
|
|
278
|
+
let moved = false;
|
|
279
|
+
connectSignal(win.onMove, () => {
|
|
280
|
+
moved = true;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
vi.stubGlobal('screenX', 50);
|
|
284
|
+
vi.stubGlobal('screenY', 50);
|
|
285
|
+
attachWindowMove(win);
|
|
286
|
+
window.dispatchEvent(new Event('resize'));
|
|
287
|
+
|
|
288
|
+
expect(moved).toBe(false);
|
|
289
|
+
vi.unstubAllGlobals();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('attachWindowOrientation', () => {
|
|
294
|
+
it('emits onOrientationChanged on change', () => {
|
|
295
|
+
const win = createApplicationWindow();
|
|
296
|
+
let called = false;
|
|
297
|
+
connectSignal(win.onOrientationChanged, () => {
|
|
298
|
+
called = true;
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
let capturedHandler: (() => void) | null = null;
|
|
302
|
+
Object.defineProperty(screen, 'orientation', {
|
|
303
|
+
value: {
|
|
304
|
+
addEventListener: (_: string, fn: () => void) => {
|
|
305
|
+
capturedHandler = fn;
|
|
306
|
+
},
|
|
307
|
+
removeEventListener: vi.fn(),
|
|
308
|
+
},
|
|
309
|
+
configurable: true,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
attachWindowOrientation(win);
|
|
313
|
+
capturedHandler!();
|
|
314
|
+
expect(called).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('attachWindowRenderContext', () => {
|
|
319
|
+
it('emits onRenderContextLost on webglcontextlost', () => {
|
|
320
|
+
const canvas = document.createElement('canvas');
|
|
321
|
+
const win = createApplicationWindow();
|
|
322
|
+
let called = false;
|
|
323
|
+
connectSignal(win.onRenderContextLost, () => {
|
|
324
|
+
called = true;
|
|
325
|
+
});
|
|
326
|
+
attachWindowRenderContext(win, canvas);
|
|
327
|
+
canvas.dispatchEvent(new Event('webglcontextlost'));
|
|
328
|
+
expect(called).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('emits onRenderContextRestored on webglcontextrestored', () => {
|
|
332
|
+
const canvas = document.createElement('canvas');
|
|
333
|
+
const win = createApplicationWindow();
|
|
334
|
+
let called = false;
|
|
335
|
+
connectSignal(win.onRenderContextRestored, () => {
|
|
336
|
+
called = true;
|
|
337
|
+
});
|
|
338
|
+
attachWindowRenderContext(win, canvas);
|
|
339
|
+
canvas.dispatchEvent(new Event('webglcontextrestored'));
|
|
340
|
+
expect(called).toBe(true);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('attachWindowRenderState', () => {
|
|
345
|
+
it('sizes the canvas backing store and writes the device transform from the window', () => {
|
|
346
|
+
const win = createApplicationWindow();
|
|
347
|
+
win.width = 800;
|
|
348
|
+
win.height = 600;
|
|
349
|
+
win.devicePixelRatio = 2;
|
|
350
|
+
const canvas = document.createElement('canvas');
|
|
351
|
+
const state = makeRenderState();
|
|
352
|
+
attachWindowRenderState(win, state, canvas);
|
|
353
|
+
expect(canvas.width).toBe(1600);
|
|
354
|
+
expect(canvas.height).toBe(1200);
|
|
355
|
+
expect(state.renderTransform2D?.a).toBe(2);
|
|
356
|
+
expect(state.renderTransform2D?.d).toBe(2);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('reapplies the backing size and transform on window resize', () => {
|
|
360
|
+
const win = createApplicationWindow();
|
|
361
|
+
win.width = 800;
|
|
362
|
+
win.height = 600;
|
|
363
|
+
win.devicePixelRatio = 1;
|
|
364
|
+
const canvas = document.createElement('canvas');
|
|
365
|
+
const state = makeRenderState();
|
|
366
|
+
attachWindowRenderState(win, state, canvas);
|
|
367
|
+
win.width = 400;
|
|
368
|
+
win.devicePixelRatio = 2;
|
|
369
|
+
emitSignal(win.onResize);
|
|
370
|
+
expect(canvas.width).toBe(800);
|
|
371
|
+
expect(state.renderTransform2D?.a).toBe(2);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('attachWindowResize', () => {
|
|
376
|
+
let resizeCallback: ResizeObserverCallback;
|
|
377
|
+
let disconnectFn: ReturnType<typeof vi.fn>;
|
|
378
|
+
|
|
379
|
+
beforeEach(() => {
|
|
380
|
+
disconnectFn = vi.fn();
|
|
381
|
+
vi.stubGlobal(
|
|
382
|
+
'ResizeObserver',
|
|
383
|
+
class {
|
|
384
|
+
constructor(cb: ResizeObserverCallback) {
|
|
385
|
+
resizeCallback = cb;
|
|
386
|
+
}
|
|
387
|
+
observe() {}
|
|
388
|
+
disconnect = disconnectFn;
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
afterEach(() => {
|
|
394
|
+
vi.unstubAllGlobals();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('emits onResize and updates dimensions', () => {
|
|
398
|
+
vi.stubGlobal('devicePixelRatio', 2);
|
|
399
|
+
const win = createApplicationWindow();
|
|
400
|
+
let called = false;
|
|
401
|
+
connectSignal(win.onResize, () => {
|
|
402
|
+
called = true;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
attachWindowResize(win, document.createElement('div'));
|
|
406
|
+
resizeCallback([{ contentRect: { width: 1280, height: 720 } } as ResizeObserverEntry], {} as ResizeObserver);
|
|
407
|
+
|
|
408
|
+
expect(called).toBe(true);
|
|
409
|
+
expect(win.width).toBe(1280);
|
|
410
|
+
expect(win.height).toBe(720);
|
|
411
|
+
expect(win.devicePixelRatio).toBe(2);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('replaces a previous observer when called again', () => {
|
|
415
|
+
const win = createApplicationWindow();
|
|
416
|
+
attachWindowResize(win, document.createElement('div'));
|
|
417
|
+
attachWindowResize(win, document.createElement('div'));
|
|
418
|
+
expect(disconnectFn).toHaveBeenCalledTimes(1);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('attachWindowVisibility', () => {
|
|
423
|
+
it('emits onDeactivate when page is hidden', () => {
|
|
424
|
+
const win = createApplicationWindow();
|
|
425
|
+
let called = false;
|
|
426
|
+
connectSignal(win.onDeactivate, () => {
|
|
427
|
+
called = true;
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
attachWindowVisibility(win);
|
|
431
|
+
Object.defineProperty(document, 'hidden', { value: true, configurable: true });
|
|
432
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
433
|
+
Object.defineProperty(document, 'hidden', { value: false, configurable: true });
|
|
434
|
+
|
|
435
|
+
expect(called).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('emits onActivate when page becomes visible', () => {
|
|
439
|
+
const win = createApplicationWindow();
|
|
440
|
+
let called = false;
|
|
441
|
+
connectSignal(win.onActivate, () => {
|
|
442
|
+
called = true;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
attachWindowVisibility(win);
|
|
446
|
+
Object.defineProperty(document, 'hidden', { value: false, configurable: true });
|
|
447
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
448
|
+
|
|
449
|
+
expect(called).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe('centerWindow', () => {
|
|
454
|
+
it('delegates to the backend', () => {
|
|
455
|
+
const backend = recordingWindowBackend();
|
|
456
|
+
setWindowBackend(backend);
|
|
457
|
+
centerWindow(createApplicationWindow());
|
|
458
|
+
expect(backend.calls).toContain('center');
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('closeWindow', () => {
|
|
463
|
+
it('closes and emits onClose when not vetoed', () => {
|
|
464
|
+
const backend = recordingWindowBackend();
|
|
465
|
+
setWindowBackend(backend);
|
|
466
|
+
const win = createApplicationWindow();
|
|
467
|
+
let closed = false;
|
|
468
|
+
connectSignal(win.onClose, () => {
|
|
469
|
+
closed = true;
|
|
470
|
+
});
|
|
471
|
+
expect(closeWindow(win)).toBe(true);
|
|
472
|
+
expect(backend.calls).toContain('close');
|
|
473
|
+
expect(closed).toBe(true);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('aborts and returns false when a listener vetoes', () => {
|
|
477
|
+
const backend = recordingWindowBackend();
|
|
478
|
+
setWindowBackend(backend);
|
|
479
|
+
const win = createApplicationWindow();
|
|
480
|
+
connectSignal(win.onCloseRequest, () => cancelSignal(win.onCloseRequest));
|
|
481
|
+
expect(closeWindow(win)).toBe(false);
|
|
482
|
+
expect(backend.calls).not.toContain('close');
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe('computeWindowDeviceTransform', () => {
|
|
487
|
+
it('writes a uniform devicePixelRatio scale into out and returns it', () => {
|
|
488
|
+
const win = createApplicationWindow();
|
|
489
|
+
win.devicePixelRatio = 3;
|
|
490
|
+
const out = { a: 0, b: 0, c: 0, d: 0, tx: 9, ty: 9 } as unknown as Matrix;
|
|
491
|
+
const result = computeWindowDeviceTransform(win, out);
|
|
492
|
+
expect(result).toBe(out);
|
|
493
|
+
expect(out.a).toBe(3);
|
|
494
|
+
expect(out.d).toBe(3);
|
|
495
|
+
expect(out.b).toBe(0);
|
|
496
|
+
expect(out.c).toBe(0);
|
|
497
|
+
expect(out.tx).toBe(0);
|
|
498
|
+
expect(out.ty).toBe(0);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('overwrites every field when out already carries stale values (read-before-write)', () => {
|
|
502
|
+
const win = createApplicationWindow();
|
|
503
|
+
win.devicePixelRatio = 2;
|
|
504
|
+
// out's only input is win (a different object), so out cannot alias an input here; this asserts
|
|
505
|
+
// the read-before-write guarantee by handing the function a fully-populated out it must clobber.
|
|
506
|
+
const out = { a: 99, b: 99, c: 99, d: 99, tx: 99, ty: 99 } as unknown as Matrix;
|
|
507
|
+
const result = computeWindowDeviceTransform(win, out);
|
|
508
|
+
expect(result).toBe(out);
|
|
509
|
+
expect(out.a).toBe(2);
|
|
510
|
+
expect(out.b).toBe(0);
|
|
511
|
+
expect(out.c).toBe(0);
|
|
512
|
+
expect(out.d).toBe(2);
|
|
513
|
+
expect(out.tx).toBe(0);
|
|
514
|
+
expect(out.ty).toBe(0);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe('createApplicationWindow', () => {
|
|
519
|
+
it('returns all signals with no side effects', () => {
|
|
520
|
+
const win = createApplicationWindow();
|
|
521
|
+
expect(win.onActivate).toBeDefined();
|
|
522
|
+
expect(win.onClose).toBeDefined();
|
|
523
|
+
expect(win.onDeactivate).toBeDefined();
|
|
524
|
+
expect(win.onDropFile).toBeDefined();
|
|
525
|
+
expect(win.onFocusIn).toBeDefined();
|
|
526
|
+
expect(win.onFocusOut).toBeDefined();
|
|
527
|
+
expect(win.onFullscreenChanged).toBeDefined();
|
|
528
|
+
expect(win.onMaximize).toBeDefined();
|
|
529
|
+
expect(win.onMinimize).toBeDefined();
|
|
530
|
+
expect(win.onMove).toBeDefined();
|
|
531
|
+
expect(win.onOrientationChanged).toBeDefined();
|
|
532
|
+
expect(win.onRenderContextLost).toBeDefined();
|
|
533
|
+
expect(win.onRenderContextRestored).toBeDefined();
|
|
534
|
+
expect(win.onResize).toBeDefined();
|
|
535
|
+
expect(win.onRestore).toBeDefined();
|
|
536
|
+
|
|
537
|
+
let called = false;
|
|
538
|
+
connectSignal(win.onFullscreenChanged, () => {
|
|
539
|
+
called = true;
|
|
540
|
+
});
|
|
541
|
+
document.dispatchEvent(new Event('fullscreenchange'));
|
|
542
|
+
expect(called).toBe(false);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('initializes dimensions and devicePixelRatio to defaults', () => {
|
|
546
|
+
const win = createApplicationWindow();
|
|
547
|
+
expect(win.width).toBe(0);
|
|
548
|
+
expect(win.height).toBe(0);
|
|
549
|
+
expect(win.devicePixelRatio).toBe(1);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
describe('createWebWindowBackend', () => {
|
|
554
|
+
it('sets document.title and reports bounds without throwing', () => {
|
|
555
|
+
const backend = createWebWindowBackend();
|
|
556
|
+
const win = createApplicationWindow();
|
|
557
|
+
backend.setTitle(win, 'Hello');
|
|
558
|
+
expect(document.title).toBe('Hello');
|
|
559
|
+
const bounds = backend.getBounds(win, { x: 0, y: 0, width: 0, height: 0 });
|
|
560
|
+
expect(typeof bounds.width).toBe('number');
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe('detachWindowClose', () => {
|
|
565
|
+
it('stops emitting onClose after detach', () => {
|
|
566
|
+
const win = createApplicationWindow();
|
|
567
|
+
let closed = false;
|
|
568
|
+
connectSignal(win.onClose, () => {
|
|
569
|
+
closed = true;
|
|
570
|
+
});
|
|
571
|
+
attachWindowClose(win);
|
|
572
|
+
detachWindowClose(win);
|
|
573
|
+
window.dispatchEvent(new Event('pagehide'));
|
|
574
|
+
expect(closed).toBe(false);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
describe('detachWindowDropFile', () => {
|
|
579
|
+
it('removes listeners', () => {
|
|
580
|
+
const element = document.createElement('div');
|
|
581
|
+
const win = createApplicationWindow();
|
|
582
|
+
let called = false;
|
|
583
|
+
connectSignal(win.onDropFile, () => {
|
|
584
|
+
called = true;
|
|
585
|
+
});
|
|
586
|
+
attachWindowDropFile(win, element);
|
|
587
|
+
detachWindowDropFile(win);
|
|
588
|
+
|
|
589
|
+
const event = new Event('drop');
|
|
590
|
+
Object.defineProperty(event, 'dataTransfer', { value: { files: [] } });
|
|
591
|
+
element.dispatchEvent(event);
|
|
592
|
+
|
|
593
|
+
expect(called).toBe(false);
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
describe('detachWindowFocus', () => {
|
|
598
|
+
it('removes both listeners', () => {
|
|
599
|
+
const element = document.createElement('div');
|
|
600
|
+
const win = createApplicationWindow();
|
|
601
|
+
let called = false;
|
|
602
|
+
connectSignal(win.onFocusIn, () => {
|
|
603
|
+
called = true;
|
|
604
|
+
});
|
|
605
|
+
attachWindowFocus(win, element);
|
|
606
|
+
detachWindowFocus(win);
|
|
607
|
+
element.dispatchEvent(new Event('focus'));
|
|
608
|
+
expect(called).toBe(false);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe('detachWindowFullscreen', () => {
|
|
613
|
+
it('removes the listener', () => {
|
|
614
|
+
const win = createApplicationWindow();
|
|
615
|
+
let called = false;
|
|
616
|
+
connectSignal(win.onFullscreenChanged, () => {
|
|
617
|
+
called = true;
|
|
618
|
+
});
|
|
619
|
+
attachWindowFullscreen(win);
|
|
620
|
+
detachWindowFullscreen(win);
|
|
621
|
+
document.dispatchEvent(new Event('fullscreenchange'));
|
|
622
|
+
expect(called).toBe(false);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe('detachWindowMove', () => {
|
|
627
|
+
it('removes the listener so onMove no longer fires', () => {
|
|
628
|
+
const win = createApplicationWindow();
|
|
629
|
+
win.x = 0;
|
|
630
|
+
let moved = false;
|
|
631
|
+
connectSignal(win.onMove, () => {
|
|
632
|
+
moved = true;
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
vi.stubGlobal('screenX', 100);
|
|
636
|
+
vi.stubGlobal('screenY', 100);
|
|
637
|
+
attachWindowMove(win);
|
|
638
|
+
detachWindowMove(win);
|
|
639
|
+
window.dispatchEvent(new Event('resize'));
|
|
640
|
+
|
|
641
|
+
expect(moved).toBe(false);
|
|
642
|
+
vi.unstubAllGlobals();
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe('detachWindowOrientation', () => {
|
|
647
|
+
it('removes the listener', () => {
|
|
648
|
+
const removeListener = vi.fn();
|
|
649
|
+
Object.defineProperty(screen, 'orientation', {
|
|
650
|
+
value: { addEventListener: vi.fn(), removeEventListener: removeListener },
|
|
651
|
+
configurable: true,
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const win = createApplicationWindow();
|
|
655
|
+
attachWindowOrientation(win);
|
|
656
|
+
detachWindowOrientation(win);
|
|
657
|
+
expect(removeListener).toHaveBeenCalled();
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
describe('detachWindowRenderContext', () => {
|
|
662
|
+
it('removes render context listeners', () => {
|
|
663
|
+
const canvas = document.createElement('canvas');
|
|
664
|
+
const win = createApplicationWindow();
|
|
665
|
+
let called = false;
|
|
666
|
+
connectSignal(win.onRenderContextLost, () => {
|
|
667
|
+
called = true;
|
|
668
|
+
});
|
|
669
|
+
attachWindowRenderContext(win, canvas);
|
|
670
|
+
detachWindowRenderContext(win);
|
|
671
|
+
canvas.dispatchEvent(new Event('webglcontextlost'));
|
|
672
|
+
expect(called).toBe(false);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
describe('detachWindowRenderState', () => {
|
|
677
|
+
it('stops reacting to window resize', () => {
|
|
678
|
+
const win = createApplicationWindow();
|
|
679
|
+
win.width = 800;
|
|
680
|
+
win.height = 600;
|
|
681
|
+
win.devicePixelRatio = 1;
|
|
682
|
+
const canvas = document.createElement('canvas');
|
|
683
|
+
const state = makeRenderState();
|
|
684
|
+
attachWindowRenderState(win, state, canvas);
|
|
685
|
+
detachWindowRenderState(win);
|
|
686
|
+
win.width = 400;
|
|
687
|
+
emitSignal(win.onResize);
|
|
688
|
+
expect(canvas.width).toBe(800);
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
describe('detachWindowResize', () => {
|
|
693
|
+
it('disconnects the observer', () => {
|
|
694
|
+
const disconnectFn = vi.fn();
|
|
695
|
+
vi.stubGlobal(
|
|
696
|
+
'ResizeObserver',
|
|
697
|
+
class {
|
|
698
|
+
constructor() {}
|
|
699
|
+
observe() {}
|
|
700
|
+
disconnect = disconnectFn;
|
|
701
|
+
},
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const win = createApplicationWindow();
|
|
705
|
+
attachWindowResize(win, document.createElement('div'));
|
|
706
|
+
detachWindowResize(win);
|
|
707
|
+
|
|
708
|
+
expect(disconnectFn).toHaveBeenCalled();
|
|
709
|
+
vi.unstubAllGlobals();
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe('detachWindowVisibility', () => {
|
|
714
|
+
it('removes the listener', () => {
|
|
715
|
+
const win = createApplicationWindow();
|
|
716
|
+
let called = false;
|
|
717
|
+
connectSignal(win.onDeactivate, () => {
|
|
718
|
+
called = true;
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
attachWindowVisibility(win);
|
|
722
|
+
detachWindowVisibility(win);
|
|
723
|
+
Object.defineProperty(document, 'hidden', { value: true, configurable: true });
|
|
724
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
725
|
+
Object.defineProperty(document, 'hidden', { value: false, configurable: true });
|
|
726
|
+
|
|
727
|
+
expect(called).toBe(false);
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
describe('disposeApplicationWindow', () => {
|
|
732
|
+
it('runs all teardown so attached observers stop firing', () => {
|
|
733
|
+
const disconnectFn = vi.fn();
|
|
734
|
+
vi.stubGlobal(
|
|
735
|
+
'ResizeObserver',
|
|
736
|
+
class {
|
|
737
|
+
constructor() {}
|
|
738
|
+
observe() {}
|
|
739
|
+
disconnect = disconnectFn;
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
const win = createApplicationWindow();
|
|
744
|
+
attachWindowResize(win, document.createElement('div'));
|
|
745
|
+
attachWindowFullscreen(win);
|
|
746
|
+
|
|
747
|
+
disposeApplicationWindow(win);
|
|
748
|
+
|
|
749
|
+
expect(disconnectFn).toHaveBeenCalled();
|
|
750
|
+
let called = false;
|
|
751
|
+
connectSignal(win.onFullscreenChanged, () => {
|
|
752
|
+
called = true;
|
|
753
|
+
});
|
|
754
|
+
document.dispatchEvent(new Event('fullscreenchange'));
|
|
755
|
+
expect(called).toBe(false);
|
|
756
|
+
|
|
757
|
+
vi.unstubAllGlobals();
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe('exitApplicationFullscreen', () => {
|
|
762
|
+
it('calls document.exitFullscreen', async () => {
|
|
763
|
+
const mock = vi.fn().mockResolvedValue(undefined);
|
|
764
|
+
Object.defineProperty(document, 'exitFullscreen', { value: mock, configurable: true });
|
|
765
|
+
await exitApplicationFullscreen();
|
|
766
|
+
expect(mock).toHaveBeenCalled();
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
describe('exitApplicationPointerLock', () => {
|
|
771
|
+
it('calls document.exitPointerLock when available', async () => {
|
|
772
|
+
const mock = vi.fn();
|
|
773
|
+
Object.defineProperty(document, 'exitPointerLock', { value: mock, configurable: true });
|
|
774
|
+
await exitApplicationPointerLock();
|
|
775
|
+
expect(mock).toHaveBeenCalled();
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
describe('flashWindowFrame', () => {
|
|
780
|
+
it('delegates to the backend', () => {
|
|
781
|
+
const backend = recordingWindowBackend();
|
|
782
|
+
setWindowBackend(backend);
|
|
783
|
+
const win = createApplicationWindow();
|
|
784
|
+
flashWindowFrame(win);
|
|
785
|
+
expect(backend.calls).toContain('flashWindowFrame');
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
describe('focusWindow', () => {
|
|
790
|
+
it('marks focused and delegates to the backend', () => {
|
|
791
|
+
const backend = recordingWindowBackend();
|
|
792
|
+
setWindowBackend(backend);
|
|
793
|
+
const win = createApplicationWindow();
|
|
794
|
+
focusWindow(win);
|
|
795
|
+
expect(win.focused).toBe(true);
|
|
796
|
+
expect(backend.calls).toContain('focus');
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
describe('getWindowBackend', () => {
|
|
801
|
+
it('falls back to a web backend', () => {
|
|
802
|
+
expect(getWindowBackend()).not.toBeNull();
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('returns the registered backend', () => {
|
|
806
|
+
const backend = recordingWindowBackend();
|
|
807
|
+
setWindowBackend(backend);
|
|
808
|
+
expect(getWindowBackend()).toBe(backend);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
describe('getWindowBounds', () => {
|
|
813
|
+
it('fills the out bounds from the backend', () => {
|
|
814
|
+
setWindowBackend(recordingWindowBackend());
|
|
815
|
+
const out = { x: 0, y: 0, width: 0, height: 0 };
|
|
816
|
+
expect(getWindowBounds(createApplicationWindow(), out)).toBe(out);
|
|
817
|
+
expect(out.width).toBe(3);
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
describe('getWindowDisplay', () => {
|
|
822
|
+
it('returns -1 on web (no multi-monitor API)', () => {
|
|
823
|
+
const win = createApplicationWindow();
|
|
824
|
+
expect(getWindowDisplay(win)).toBe(-1);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
describe('hideWindow', () => {
|
|
829
|
+
it('marks not visible and delegates to the backend', () => {
|
|
830
|
+
const backend = recordingWindowBackend();
|
|
831
|
+
setWindowBackend(backend);
|
|
832
|
+
const win = createApplicationWindow();
|
|
833
|
+
hideWindow(win);
|
|
834
|
+
expect(win.visible).toBe(false);
|
|
835
|
+
expect(backend.calls).toContain('hide');
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
describe('lockApplicationPointer', () => {
|
|
840
|
+
it('calls requestPointerLock on the element', async () => {
|
|
841
|
+
const element = document.createElement('div');
|
|
842
|
+
const mock = vi.fn().mockResolvedValue(undefined);
|
|
843
|
+
element.requestPointerLock = mock;
|
|
844
|
+
await lockApplicationPointer(element);
|
|
845
|
+
expect(mock).toHaveBeenCalled();
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('resolves without throwing when requestPointerLock is unavailable', async () => {
|
|
849
|
+
const element = document.createElement('div');
|
|
850
|
+
// Remove requestPointerLock to simulate an older browser.
|
|
851
|
+
Object.defineProperty(element, 'requestPointerLock', { value: undefined, configurable: true });
|
|
852
|
+
await expect(lockApplicationPointer(element)).resolves.toBeUndefined();
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
describe('maximizeWindow', () => {
|
|
857
|
+
it('sets maximized and emits onMaximize once', () => {
|
|
858
|
+
const backend = recordingWindowBackend();
|
|
859
|
+
setWindowBackend(backend);
|
|
860
|
+
const win = createApplicationWindow();
|
|
861
|
+
let count = 0;
|
|
862
|
+
connectSignal(win.onMaximize, () => count++);
|
|
863
|
+
maximizeWindow(win);
|
|
864
|
+
maximizeWindow(win);
|
|
865
|
+
expect(win.maximized).toBe(true);
|
|
866
|
+
expect(count).toBe(1);
|
|
867
|
+
expect(backend.calls).toContain('maximize');
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
describe('minimizeWindow', () => {
|
|
872
|
+
it('sets minimized and emits onMinimize', () => {
|
|
873
|
+
const backend = recordingWindowBackend();
|
|
874
|
+
setWindowBackend(backend);
|
|
875
|
+
const win = createApplicationWindow();
|
|
876
|
+
let called = false;
|
|
877
|
+
connectSignal(win.onMinimize, () => {
|
|
878
|
+
called = true;
|
|
879
|
+
});
|
|
880
|
+
minimizeWindow(win);
|
|
881
|
+
expect(win.minimized).toBe(true);
|
|
882
|
+
expect(called).toBe(true);
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
describe('openWindow', () => {
|
|
887
|
+
it('applies options to the entity and delegates to the backend', () => {
|
|
888
|
+
const backend = recordingWindowBackend();
|
|
889
|
+
setWindowBackend(backend);
|
|
890
|
+
const win = createApplicationWindow();
|
|
891
|
+
expect(openWindow(win, { title: 'Game', width: 640, height: 480, alwaysOnTop: true })).toBe(true);
|
|
892
|
+
expect(win.title).toBe('Game');
|
|
893
|
+
expect(win.width).toBe(640);
|
|
894
|
+
expect(win.alwaysOnTop).toBe(true);
|
|
895
|
+
expect(backend.calls).toContain('open:Game');
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it('centers the window after open when center option is true', () => {
|
|
899
|
+
const backend = recordingWindowBackend();
|
|
900
|
+
setWindowBackend(backend);
|
|
901
|
+
const win = createApplicationWindow();
|
|
902
|
+
openWindow(win, { title: 'Centered', center: true });
|
|
903
|
+
expect(backend.calls).toContain('center');
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it('does not center when center option is not set', () => {
|
|
907
|
+
const backend = recordingWindowBackend();
|
|
908
|
+
setWindowBackend(backend);
|
|
909
|
+
const win = createApplicationWindow();
|
|
910
|
+
openWindow(win, { title: 'Normal' });
|
|
911
|
+
expect(backend.calls).not.toContain('center');
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
describe('prepareElementForInput', () => {
|
|
916
|
+
it('sets touch-action and user-select', () => {
|
|
917
|
+
const element = document.createElement('div');
|
|
918
|
+
prepareElementForInput(element);
|
|
919
|
+
expect(element.style.touchAction).toBe('none');
|
|
920
|
+
expect(element.style.userSelect).toBe('none');
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it('sets transform on canvas elements', () => {
|
|
924
|
+
const canvas = document.createElement('canvas');
|
|
925
|
+
prepareElementForInput(canvas);
|
|
926
|
+
expect(canvas.style.transform).toBe('translateZ(0)');
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it('does not set transform on non-canvas elements', () => {
|
|
930
|
+
const div = document.createElement('div');
|
|
931
|
+
prepareElementForInput(div);
|
|
932
|
+
expect(div.style.transform).toBe('');
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
describe('requestApplicationFullscreen', () => {
|
|
937
|
+
it('calls requestFullscreen on the element', async () => {
|
|
938
|
+
const element = document.createElement('div');
|
|
939
|
+
const mock = vi.fn().mockResolvedValue(undefined);
|
|
940
|
+
element.requestFullscreen = mock;
|
|
941
|
+
await requestApplicationFullscreen(element);
|
|
942
|
+
expect(mock).toHaveBeenCalled();
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
describe('requestWindowAttention', () => {
|
|
947
|
+
it('delegates to the backend', () => {
|
|
948
|
+
const backend = recordingWindowBackend();
|
|
949
|
+
setWindowBackend(backend);
|
|
950
|
+
requestWindowAttention(createApplicationWindow(), true);
|
|
951
|
+
expect(backend.calls).toContain('requestAttention:true');
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
describe('requestWindowClose', () => {
|
|
956
|
+
it('returns true when not vetoed', () => {
|
|
957
|
+
setWindowBackend(recordingWindowBackend());
|
|
958
|
+
expect(requestWindowClose(createApplicationWindow())).toBe(true);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('returns false when a listener vetoes', () => {
|
|
962
|
+
const win = createApplicationWindow();
|
|
963
|
+
connectSignal(win.onCloseRequest, () => cancelSignal(win.onCloseRequest));
|
|
964
|
+
expect(requestWindowClose(win)).toBe(false);
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
describe('restoreWindow', () => {
|
|
969
|
+
it('clears minimized/maximized and emits onRestore', () => {
|
|
970
|
+
const backend = recordingWindowBackend();
|
|
971
|
+
setWindowBackend(backend);
|
|
972
|
+
const win = createApplicationWindow();
|
|
973
|
+
maximizeWindow(win);
|
|
974
|
+
let restored = false;
|
|
975
|
+
connectSignal(win.onRestore, () => {
|
|
976
|
+
restored = true;
|
|
977
|
+
});
|
|
978
|
+
restoreWindow(win);
|
|
979
|
+
expect(win.maximized).toBe(false);
|
|
980
|
+
expect(restored).toBe(true);
|
|
981
|
+
expect(backend.calls).toContain('restore');
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
describe('setWindowAlwaysOnTop', () => {
|
|
986
|
+
it('sets state and delegates', () => {
|
|
987
|
+
const backend = recordingWindowBackend();
|
|
988
|
+
setWindowBackend(backend);
|
|
989
|
+
const win = createApplicationWindow();
|
|
990
|
+
setWindowAlwaysOnTop(win, true);
|
|
991
|
+
expect(win.alwaysOnTop).toBe(true);
|
|
992
|
+
expect(backend.calls).toContain('setAlwaysOnTop:true');
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
describe('setWindowBackend', () => {
|
|
997
|
+
it('clears back to the web fallback when passed null', () => {
|
|
998
|
+
setWindowBackend(recordingWindowBackend());
|
|
999
|
+
setWindowBackend(null);
|
|
1000
|
+
expect(getWindowBackend()).not.toBeNull();
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
describe('setWindowContentProtection', () => {
|
|
1005
|
+
it('delegates to the backend', () => {
|
|
1006
|
+
const backend = recordingWindowBackend();
|
|
1007
|
+
setWindowBackend(backend);
|
|
1008
|
+
const win = createApplicationWindow();
|
|
1009
|
+
setWindowContentProtection(win, true);
|
|
1010
|
+
expect(backend.calls).toContain('setContentProtection:true');
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
describe('setWindowFullscreen', () => {
|
|
1015
|
+
it('sets state and emits onFullscreenChanged once', () => {
|
|
1016
|
+
const backend = recordingWindowBackend();
|
|
1017
|
+
setWindowBackend(backend);
|
|
1018
|
+
const win = createApplicationWindow();
|
|
1019
|
+
let count = 0;
|
|
1020
|
+
connectSignal(win.onFullscreenChanged, () => count++);
|
|
1021
|
+
setWindowFullscreen(win, true);
|
|
1022
|
+
setWindowFullscreen(win, true);
|
|
1023
|
+
expect(win.fullscreen).toBe(true);
|
|
1024
|
+
expect(count).toBe(1);
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
describe('setWindowHasShadow', () => {
|
|
1029
|
+
it('delegates to the backend', () => {
|
|
1030
|
+
const backend = recordingWindowBackend();
|
|
1031
|
+
setWindowBackend(backend);
|
|
1032
|
+
const win = createApplicationWindow();
|
|
1033
|
+
setWindowHasShadow(win, false);
|
|
1034
|
+
expect(backend.calls).toContain('setHasShadow:false');
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
describe('setWindowIcon', () => {
|
|
1039
|
+
it('sets the icon and delegates', () => {
|
|
1040
|
+
const backend = recordingWindowBackend();
|
|
1041
|
+
setWindowBackend(backend);
|
|
1042
|
+
const win = createApplicationWindow();
|
|
1043
|
+
setWindowIcon(win, 'icon.png');
|
|
1044
|
+
expect(win.icon).toBe('icon.png');
|
|
1045
|
+
expect(backend.calls).toContain('setIcon:icon.png');
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
describe('setWindowMaximumSize', () => {
|
|
1050
|
+
it('sets constraints and delegates', () => {
|
|
1051
|
+
const backend = recordingWindowBackend();
|
|
1052
|
+
setWindowBackend(backend);
|
|
1053
|
+
const win = createApplicationWindow();
|
|
1054
|
+
setWindowMaximumSize(win, 1920, 1080);
|
|
1055
|
+
expect(win.maxWidth).toBe(1920);
|
|
1056
|
+
expect(win.maxHeight).toBe(1080);
|
|
1057
|
+
expect(backend.calls).toContain('setMaximumSize:1920,1080');
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
describe('setWindowMenuBarVisible', () => {
|
|
1062
|
+
it('delegates to the backend', () => {
|
|
1063
|
+
const backend = recordingWindowBackend();
|
|
1064
|
+
setWindowBackend(backend);
|
|
1065
|
+
setWindowMenuBarVisible(createApplicationWindow(), false);
|
|
1066
|
+
expect(backend.calls).toContain('setMenuBarVisible:false');
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
describe('setWindowMinimumSize', () => {
|
|
1071
|
+
it('sets constraints and delegates', () => {
|
|
1072
|
+
const backend = recordingWindowBackend();
|
|
1073
|
+
setWindowBackend(backend);
|
|
1074
|
+
const win = createApplicationWindow();
|
|
1075
|
+
setWindowMinimumSize(win, 320, 240);
|
|
1076
|
+
expect(win.minWidth).toBe(320);
|
|
1077
|
+
expect(win.minHeight).toBe(240);
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
describe('setWindowOpacity', () => {
|
|
1082
|
+
it('sets opacity and delegates', () => {
|
|
1083
|
+
const backend = recordingWindowBackend();
|
|
1084
|
+
setWindowBackend(backend);
|
|
1085
|
+
const win = createApplicationWindow();
|
|
1086
|
+
setWindowOpacity(win, 0.5);
|
|
1087
|
+
expect(win.opacity).toBe(0.5);
|
|
1088
|
+
expect(backend.calls).toContain('setOpacity:0.5');
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
describe('setWindowParent', () => {
|
|
1093
|
+
it('delegates to the backend with null', () => {
|
|
1094
|
+
const backend = recordingWindowBackend();
|
|
1095
|
+
setWindowBackend(backend);
|
|
1096
|
+
setWindowParent(createApplicationWindow(), null);
|
|
1097
|
+
expect(backend.calls).toContain('setParent:null');
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
describe('setWindowPosition', () => {
|
|
1102
|
+
it('sets position and emits onMove', () => {
|
|
1103
|
+
const backend = recordingWindowBackend();
|
|
1104
|
+
setWindowBackend(backend);
|
|
1105
|
+
const win = createApplicationWindow();
|
|
1106
|
+
let moved = false;
|
|
1107
|
+
connectSignal(win.onMove, () => {
|
|
1108
|
+
moved = true;
|
|
1109
|
+
});
|
|
1110
|
+
setWindowPosition(win, 100, 50);
|
|
1111
|
+
expect(win.x).toBe(100);
|
|
1112
|
+
expect(win.y).toBe(50);
|
|
1113
|
+
expect(moved).toBe(true);
|
|
1114
|
+
});
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
describe('setWindowProgress', () => {
|
|
1118
|
+
it('delegates to the backend', () => {
|
|
1119
|
+
const backend = recordingWindowBackend();
|
|
1120
|
+
setWindowBackend(backend);
|
|
1121
|
+
setWindowProgress(createApplicationWindow(), 0.25);
|
|
1122
|
+
expect(backend.calls).toContain('setProgress:0.25');
|
|
1123
|
+
});
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
describe('setWindowResizable', () => {
|
|
1127
|
+
it('sets state and delegates', () => {
|
|
1128
|
+
const backend = recordingWindowBackend();
|
|
1129
|
+
setWindowBackend(backend);
|
|
1130
|
+
const win = createApplicationWindow();
|
|
1131
|
+
setWindowResizable(win, false);
|
|
1132
|
+
expect(win.resizable).toBe(false);
|
|
1133
|
+
expect(backend.calls).toContain('setResizable:false');
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
describe('setWindowSize', () => {
|
|
1138
|
+
it('sets size and emits onResize', () => {
|
|
1139
|
+
const backend = recordingWindowBackend();
|
|
1140
|
+
setWindowBackend(backend);
|
|
1141
|
+
const win = createApplicationWindow();
|
|
1142
|
+
let resized = false;
|
|
1143
|
+
connectSignal(win.onResize, () => {
|
|
1144
|
+
resized = true;
|
|
1145
|
+
});
|
|
1146
|
+
setWindowSize(win, 800, 600);
|
|
1147
|
+
expect(win.width).toBe(800);
|
|
1148
|
+
expect(win.height).toBe(600);
|
|
1149
|
+
expect(resized).toBe(true);
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
describe('setWindowSkipTaskbar', () => {
|
|
1154
|
+
it('sets state and delegates', () => {
|
|
1155
|
+
const backend = recordingWindowBackend();
|
|
1156
|
+
setWindowBackend(backend);
|
|
1157
|
+
const win = createApplicationWindow();
|
|
1158
|
+
setWindowSkipTaskbar(win, true);
|
|
1159
|
+
expect(win.skipTaskbar).toBe(true);
|
|
1160
|
+
expect(backend.calls).toContain('setSkipTaskbar:true');
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
describe('setWindowTitle', () => {
|
|
1165
|
+
it('sets the title and delegates', () => {
|
|
1166
|
+
const backend = recordingWindowBackend();
|
|
1167
|
+
setWindowBackend(backend);
|
|
1168
|
+
const win = createApplicationWindow();
|
|
1169
|
+
setWindowTitle(win, 'My App');
|
|
1170
|
+
expect(win.title).toBe('My App');
|
|
1171
|
+
expect(backend.calls).toContain('setTitle:My App');
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
describe('showWindow', () => {
|
|
1176
|
+
it('marks visible and delegates to the backend', () => {
|
|
1177
|
+
const backend = recordingWindowBackend();
|
|
1178
|
+
setWindowBackend(backend);
|
|
1179
|
+
const win = createApplicationWindow();
|
|
1180
|
+
hideWindow(win);
|
|
1181
|
+
showWindow(win);
|
|
1182
|
+
expect(win.visible).toBe(true);
|
|
1183
|
+
expect(backend.calls).toContain('show');
|
|
1184
|
+
});
|
|
1185
|
+
});
|