react-x11 1.2.0 → 2.0.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/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// The mock ntk application used by the headless tests: enough of a window,
|
|
2
|
+
// a 2d context and an X connection for the renderer to run with no server.
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { setAppearanceForTests } from '../appearance.js';
|
|
6
|
+
import { setCompositingForTests } from '../compositing.js';
|
|
7
|
+
import { setScreensForTests } from '../screens.js';
|
|
8
|
+
import { loadLayout } from '../yoga.js';
|
|
9
|
+
|
|
10
|
+
// As in harness.js: a mock-app test must not register with a live AT-SPI
|
|
11
|
+
// registry on the machine running it.
|
|
12
|
+
process.env.NO_AT_BRIDGE ??= '1';
|
|
13
|
+
|
|
14
|
+
// The layout engine's WebAssembly loads in createRoot() rather than at import
|
|
15
|
+
// time — that is what keeps top-level await out of the bundle, and so lets an
|
|
16
|
+
// app ship as a single executable (see docs/packaging.md). A mock app never
|
|
17
|
+
// goes through createRoot, so the nodes built here would find no Yoga.Node.
|
|
18
|
+
await loadLayout();
|
|
19
|
+
|
|
20
|
+
export { React };
|
|
21
|
+
|
|
22
|
+
// A minimal stand-in for an ntk application object, so the renderer can be
|
|
23
|
+
// exercised without a running X server.
|
|
24
|
+
export function createMockApp() {
|
|
25
|
+
// enough of an emitter for the connection watch in createRoot
|
|
26
|
+
const listeners = {};
|
|
27
|
+
const app = {
|
|
28
|
+
X: {
|
|
29
|
+
display: { screen: [{ root: 1 }] },
|
|
30
|
+
keycode2keysyms: {},
|
|
31
|
+
InternAtom(onlyIfExists, name, cb) {
|
|
32
|
+
// unique ids, so per-name atom tables (src/dnd.js) work on the mock
|
|
33
|
+
if (!app._atoms.has(name)) app._atoms.set(name, 1000 + app._atoms.size);
|
|
34
|
+
cb(null, app._atoms.get(name));
|
|
35
|
+
},
|
|
36
|
+
ConfigureWindow(id, options) {
|
|
37
|
+
app.configureCalls.push([id, options]);
|
|
38
|
+
},
|
|
39
|
+
// Startup notification broadcasts through this (src/startup.js), and
|
|
40
|
+
// it is the only way to see the message: it goes to the root window,
|
|
41
|
+
// which the mock does not model as a window with properties.
|
|
42
|
+
SendClientMessage(destination, wid, type, format, data, eventMask) {
|
|
43
|
+
app.clientMessages.push({
|
|
44
|
+
destination,
|
|
45
|
+
wid,
|
|
46
|
+
type,
|
|
47
|
+
format,
|
|
48
|
+
data,
|
|
49
|
+
eventMask,
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
on(event, fn) {
|
|
53
|
+
(listeners[event] ??= []).push(fn);
|
|
54
|
+
},
|
|
55
|
+
emit(event, ...args) {
|
|
56
|
+
for (const fn of listeners[event] ?? []) fn(...args);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
configureCalls: [],
|
|
60
|
+
clientMessages: [],
|
|
61
|
+
_atoms: new Map([['WM_DELETE_WINDOW', 999]]),
|
|
62
|
+
closed: 0,
|
|
63
|
+
close() {
|
|
64
|
+
app.closed++;
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
},
|
|
67
|
+
// A clipboard that behaves like ntk's without a server: selections are
|
|
68
|
+
// owned in this process, so a write is visible to the next read. Enough
|
|
69
|
+
// for anything that copies or pastes — the ICCCM machinery itself is
|
|
70
|
+
// tested against the real thing in test/clipboard.test.js.
|
|
71
|
+
clipboard: {
|
|
72
|
+
/** every write, in order: `{ data, selection, time }` */
|
|
73
|
+
writes: [],
|
|
74
|
+
/** selection name -> the target map currently owned */
|
|
75
|
+
owned: new Map(),
|
|
76
|
+
_watchers: new Map(),
|
|
77
|
+
write(data, { selection = 'CLIPBOARD', time } = {}) {
|
|
78
|
+
app.clipboard.writes.push({ data, selection, time });
|
|
79
|
+
const map =
|
|
80
|
+
typeof data === 'string'
|
|
81
|
+
? { UTF8_STRING: data, STRING: data }
|
|
82
|
+
: { ...data };
|
|
83
|
+
app.clipboard.owned.set(selection, map);
|
|
84
|
+
for (const fn of app.clipboard._watchers.get(selection) ?? []) {
|
|
85
|
+
fn({ selection, owner: 1, reason: 'new-owner' });
|
|
86
|
+
}
|
|
87
|
+
return Promise.resolve();
|
|
88
|
+
},
|
|
89
|
+
clear(selection = 'CLIPBOARD') {
|
|
90
|
+
app.clipboard.owned.delete(selection);
|
|
91
|
+
for (const fn of app.clipboard._watchers.get(selection) ?? []) {
|
|
92
|
+
fn({ selection, owner: 0, reason: 'new-owner' });
|
|
93
|
+
}
|
|
94
|
+
return Promise.resolve();
|
|
95
|
+
},
|
|
96
|
+
targets({ selection = 'CLIPBOARD' } = {}) {
|
|
97
|
+
return Promise.resolve([
|
|
98
|
+
...Object.keys(app.clipboard.owned.get(selection) ?? {}),
|
|
99
|
+
]);
|
|
100
|
+
},
|
|
101
|
+
read({ selection = 'CLIPBOARD', target } = {}) {
|
|
102
|
+
const map = app.clipboard.owned.get(selection);
|
|
103
|
+
if (!map) {
|
|
104
|
+
return Promise.reject(
|
|
105
|
+
new Error(
|
|
106
|
+
`clipboard: nothing to paste — ${selection} has no owner`,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (target === undefined) {
|
|
111
|
+
const text = map.UTF8_STRING ?? map.STRING;
|
|
112
|
+
return text === undefined
|
|
113
|
+
? Promise.reject(
|
|
114
|
+
new Error('clipboard: owner refused both text targets'),
|
|
115
|
+
)
|
|
116
|
+
: Promise.resolve(text);
|
|
117
|
+
}
|
|
118
|
+
if (!(target in map)) {
|
|
119
|
+
return Promise.reject(
|
|
120
|
+
new Error(`clipboard: owner cannot convert to ${target}`),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const value = map[target];
|
|
124
|
+
return Promise.resolve(
|
|
125
|
+
typeof value === 'string' ? Buffer.from(value, 'utf8') : value,
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
watch(selection, handler) {
|
|
129
|
+
const list = app.clipboard._watchers.get(selection) ?? [];
|
|
130
|
+
list.push(handler);
|
|
131
|
+
app.clipboard._watchers.set(selection, list);
|
|
132
|
+
return Promise.resolve(() => {
|
|
133
|
+
const at = list.indexOf(handler);
|
|
134
|
+
if (at !== -1) list.splice(at, 1);
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
windows: [],
|
|
139
|
+
// A display that does have a 32-bit TrueColor visual, so `<window
|
|
140
|
+
// transparent>` takes its real path here. Delete it from the app to
|
|
141
|
+
// model the displays that do not (XQuartz), where the renderer falls
|
|
142
|
+
// back to an opaque window.
|
|
143
|
+
findArgbVisual() {
|
|
144
|
+
return { visual: 0x21, depth: 32 };
|
|
145
|
+
},
|
|
146
|
+
// …and one with a compositor running — see the bottom of this function,
|
|
147
|
+
// where that half is seeded.
|
|
148
|
+
createWindow(attributes) {
|
|
149
|
+
const handlers = {};
|
|
150
|
+
const ops = [];
|
|
151
|
+
const ctx = {
|
|
152
|
+
ops,
|
|
153
|
+
fillStyle: null,
|
|
154
|
+
strokeStyle: null,
|
|
155
|
+
lineWidth: 1,
|
|
156
|
+
fillRect(x, y, w, h) {
|
|
157
|
+
ops.push(['fillRect', x, y, w, h, ctx.fillStyle]);
|
|
158
|
+
},
|
|
159
|
+
// A batch is `fillRect` per rectangle — that is what ntk's own
|
|
160
|
+
// `fillRects` promises, and the only difference there is the request
|
|
161
|
+
// count, which a mock with no server does not have. So it records
|
|
162
|
+
// the fills themselves: an assertion on what was drawn holds whether
|
|
163
|
+
// the drawing batched or not, and `[x, y, w, h]` quadruples and one
|
|
164
|
+
// flat list are the same drawing too.
|
|
165
|
+
fillRects(rects) {
|
|
166
|
+
const flat = Array.isArray(rects?.[0]) ? rects.flat() : (rects ?? []);
|
|
167
|
+
for (let i = 0; i + 3 < flat.length; i += 4) {
|
|
168
|
+
if (flat[i + 2] > 0 && flat[i + 3] > 0) {
|
|
169
|
+
ctx.fillRect(flat[i], flat[i + 1], flat[i + 2], flat[i + 3]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
// ntk's context has this beside `fillRect`, so an `onDraw` that
|
|
174
|
+
// outlines a box paints on a real server and used to crash on the
|
|
175
|
+
// mock — a missing method here is an app's headless test failing at
|
|
176
|
+
// something that works
|
|
177
|
+
strokeRect(x, y, w, h) {
|
|
178
|
+
ops.push(['strokeRect', x, y, w, h, ctx.strokeStyle, ctx.lineWidth]);
|
|
179
|
+
},
|
|
180
|
+
beginPath() {},
|
|
181
|
+
clearRect(x, y, w, h) {
|
|
182
|
+
ops.push(['clearRect', x, y, w, h]);
|
|
183
|
+
},
|
|
184
|
+
rect(x, y, w, h) {
|
|
185
|
+
ops.push(['rect', x, y, w, h]);
|
|
186
|
+
},
|
|
187
|
+
roundRect(x, y, w, h, r) {
|
|
188
|
+
ops.push(['roundRect', x, y, w, h, r]);
|
|
189
|
+
},
|
|
190
|
+
fill() {
|
|
191
|
+
ops.push(['fill', ctx.fillStyle]);
|
|
192
|
+
},
|
|
193
|
+
stroke() {
|
|
194
|
+
ops.push(['stroke', ctx.strokeStyle, ctx.lineWidth]);
|
|
195
|
+
},
|
|
196
|
+
clip() {
|
|
197
|
+
ops.push(['clip']);
|
|
198
|
+
},
|
|
199
|
+
save() {
|
|
200
|
+
ops.push(['save']);
|
|
201
|
+
},
|
|
202
|
+
restore() {
|
|
203
|
+
ops.push(['restore']);
|
|
204
|
+
},
|
|
205
|
+
translate(x, y) {
|
|
206
|
+
ops.push(['translate', x, y]);
|
|
207
|
+
},
|
|
208
|
+
setLineDash(segments) {
|
|
209
|
+
ops.push(['setLineDash', segments]);
|
|
210
|
+
},
|
|
211
|
+
moveTo(x, y) {
|
|
212
|
+
ops.push(['moveTo', x, y]);
|
|
213
|
+
},
|
|
214
|
+
lineTo(x, y) {
|
|
215
|
+
ops.push(['lineTo', x, y]);
|
|
216
|
+
},
|
|
217
|
+
closePath() {
|
|
218
|
+
ops.push(['closePath']);
|
|
219
|
+
},
|
|
220
|
+
drawImage(...args) {
|
|
221
|
+
ops.push(['drawImage']);
|
|
222
|
+
},
|
|
223
|
+
// the raw-pixel write, recorded with its destination — where these
|
|
224
|
+
// pixels land is the thing to assert about an `onDraw` that uses it,
|
|
225
|
+
// since putImageData ignores the transform (docs/elements.md,
|
|
226
|
+
// "<canvas>") and an un-offset call is a real bug this mock should
|
|
227
|
+
// not hide by crashing first
|
|
228
|
+
putImageData(data, x, y) {
|
|
229
|
+
ops.push(['putImageData', data?.width, data?.height, x, y]);
|
|
230
|
+
},
|
|
231
|
+
// enough of the canvas surface for SvgView.draw to run headlessly
|
|
232
|
+
scale(sx, sy) {
|
|
233
|
+
ops.push(['scale', sx, sy]);
|
|
234
|
+
},
|
|
235
|
+
transform() {},
|
|
236
|
+
setTransform() {},
|
|
237
|
+
getTransform() {
|
|
238
|
+
return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
239
|
+
},
|
|
240
|
+
arc() {},
|
|
241
|
+
ellipse() {},
|
|
242
|
+
bezierCurveTo() {},
|
|
243
|
+
quadraticCurveTo() {},
|
|
244
|
+
createLinearGradient() {
|
|
245
|
+
return { addColorStop() {} };
|
|
246
|
+
},
|
|
247
|
+
createRadialGradient() {
|
|
248
|
+
return { addColorStop() {} };
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
const wnd = {
|
|
252
|
+
id: app.windows.length + 100,
|
|
253
|
+
X: app.X,
|
|
254
|
+
attributes,
|
|
255
|
+
x: attributes.x,
|
|
256
|
+
y: attributes.y,
|
|
257
|
+
width: attributes.width,
|
|
258
|
+
height: attributes.height,
|
|
259
|
+
title: attributes.title,
|
|
260
|
+
mapped: false,
|
|
261
|
+
destroyed: false,
|
|
262
|
+
parent: null,
|
|
263
|
+
calls: [],
|
|
264
|
+
ctx,
|
|
265
|
+
map() {
|
|
266
|
+
wnd.mapped = true;
|
|
267
|
+
wnd.calls.push(['map']);
|
|
268
|
+
},
|
|
269
|
+
unmap() {
|
|
270
|
+
wnd.mapped = false;
|
|
271
|
+
wnd.calls.push(['unmap']);
|
|
272
|
+
},
|
|
273
|
+
reparentTo(parent, x, y) {
|
|
274
|
+
wnd.parent = parent;
|
|
275
|
+
wnd.calls.push(['reparentTo', parent.id, x, y]);
|
|
276
|
+
},
|
|
277
|
+
destroy() {
|
|
278
|
+
wnd.destroyed = true;
|
|
279
|
+
wnd.calls.push(['destroy']);
|
|
280
|
+
},
|
|
281
|
+
// ntk >= 3.7.0: menus hold a pointer grab while they are up
|
|
282
|
+
grabPointer(options, cb) {
|
|
283
|
+
wnd.grabbed = true;
|
|
284
|
+
wnd.calls.push(['grabPointer']);
|
|
285
|
+
cb?.(null, 0);
|
|
286
|
+
},
|
|
287
|
+
ungrabPointer() {
|
|
288
|
+
wnd.grabbed = false;
|
|
289
|
+
wnd.calls.push(['ungrabPointer']);
|
|
290
|
+
},
|
|
291
|
+
// ntk >= 7.5.0. `xi2: 'auto'` creates a window on core events and
|
|
292
|
+
// calls this the first time it is scrolled, so the call is the
|
|
293
|
+
// observable half of the upgrade — `wnd.xi2Selected` is what a test
|
|
294
|
+
// asserts on. Resolves `true` the way a server with XInput2 answers;
|
|
295
|
+
// `app.hasXI2 = false` is the other kind of display.
|
|
296
|
+
selectXI2(types) {
|
|
297
|
+
wnd.calls.push(['selectXI2', types]);
|
|
298
|
+
if (app.hasXI2 === false) return Promise.resolve(false);
|
|
299
|
+
wnd.xi2Selected = types === undefined || types.length > 0;
|
|
300
|
+
return Promise.resolve(true);
|
|
301
|
+
},
|
|
302
|
+
resize(width, height) {
|
|
303
|
+
wnd.width = width;
|
|
304
|
+
wnd.height = height;
|
|
305
|
+
wnd.calls.push(['resize', width, height]);
|
|
306
|
+
},
|
|
307
|
+
move(x, y) {
|
|
308
|
+
wnd.x = x;
|
|
309
|
+
wnd.y = y;
|
|
310
|
+
wnd.calls.push(['move', x, y]);
|
|
311
|
+
},
|
|
312
|
+
setTitle(title) {
|
|
313
|
+
wnd.title = title;
|
|
314
|
+
wnd.calls.push(['setTitle', title]);
|
|
315
|
+
},
|
|
316
|
+
setSizeHints(hints) {
|
|
317
|
+
wnd.calls.push(['setSizeHints', hints]);
|
|
318
|
+
},
|
|
319
|
+
setClass(instance, className) {
|
|
320
|
+
wnd.calls.push(['setClass', instance, className]);
|
|
321
|
+
},
|
|
322
|
+
setWindowType(type) {
|
|
323
|
+
wnd.calls.push(['setWindowType', type]);
|
|
324
|
+
},
|
|
325
|
+
// ntk >= 4.3: ICCCM WM_TRANSIENT_FOR
|
|
326
|
+
setTransientFor(owner) {
|
|
327
|
+
wnd.transientFor = owner;
|
|
328
|
+
wnd.calls.push(['setTransientFor', owner]);
|
|
329
|
+
},
|
|
330
|
+
setAlwaysOnTop(on) {
|
|
331
|
+
wnd.calls.push(['setAlwaysOnTop', on]);
|
|
332
|
+
},
|
|
333
|
+
// ntk >= 4.1: the general _NET_WM_STATE surface. Resolves to whether
|
|
334
|
+
// the WM advertises the state; the mock says yes.
|
|
335
|
+
setWmState(names, action = 'add') {
|
|
336
|
+
wnd.calls.push(['setWmState', names, action]);
|
|
337
|
+
return Promise.resolve(true);
|
|
338
|
+
},
|
|
339
|
+
getWmStates() {
|
|
340
|
+
return Promise.resolve(wnd.wmStates ?? []);
|
|
341
|
+
},
|
|
342
|
+
setProperty(name, value, options) {
|
|
343
|
+
wnd.calls.push(['setProperty', name, value, options]);
|
|
344
|
+
return Promise.resolve(wnd);
|
|
345
|
+
},
|
|
346
|
+
deleteProperty(name) {
|
|
347
|
+
wnd.calls.push(['deleteProperty', name]);
|
|
348
|
+
return Promise.resolve(wnd);
|
|
349
|
+
},
|
|
350
|
+
setActions() {
|
|
351
|
+
wnd.calls.push(['setActions']);
|
|
352
|
+
},
|
|
353
|
+
setCursor(name) {
|
|
354
|
+
wnd.cursor = name;
|
|
355
|
+
wnd.calls.push(['setCursor', name]);
|
|
356
|
+
},
|
|
357
|
+
getContext() {
|
|
358
|
+
return ctx;
|
|
359
|
+
},
|
|
360
|
+
// ntk >= 4.3 (sidorares/ntk#139): the scroll-blit fast path. Records
|
|
361
|
+
// and accepts; a test wanting the fallback deletes the method or
|
|
362
|
+
// replaces it with one returning false.
|
|
363
|
+
scrollRegion(rect, dx, dy) {
|
|
364
|
+
wnd.calls.push(['scrollRegion', rect, dx, dy]);
|
|
365
|
+
return true;
|
|
366
|
+
},
|
|
367
|
+
// ntk >= 5.2 (sidorares/ntk#148): the frame-clock gate a discrete
|
|
368
|
+
// input's paint goes through (issue #141). No server here to fall
|
|
369
|
+
// behind, so
|
|
370
|
+
// it is open unless a test closes it — set `wnd.frameInFlight` to a
|
|
371
|
+
// function of your own, or flip `wnd._frameInFlight`, to model a
|
|
372
|
+
// connection that has not acknowledged the last frame.
|
|
373
|
+
frameInFlight() {
|
|
374
|
+
return Boolean(wnd._frameInFlight);
|
|
375
|
+
},
|
|
376
|
+
on(name, fn) {
|
|
377
|
+
(handlers[name] ??= []).push(fn);
|
|
378
|
+
},
|
|
379
|
+
emit(name, ev) {
|
|
380
|
+
for (const fn of handlers[name] ?? []) fn(ev);
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
app.windows.push(wnd);
|
|
384
|
+
return wnd;
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
// The mock display composites. Seeded here rather than probed, because
|
|
388
|
+
// there is no X connection to ask — and seeding it means `createRoot`
|
|
389
|
+
// finds a session already open and leaves it alone. Tests that want the
|
|
390
|
+
// other half of the matrix — ARGB visuals with nothing blending them,
|
|
391
|
+
// where a transparent corner would render black — call
|
|
392
|
+
// `setCompositingForTests(app, false)` before rendering.
|
|
393
|
+
setCompositingForTests(app, true);
|
|
394
|
+
// And the desktop's appearance, for the same reason and a sharper one: the
|
|
395
|
+
// built-in palette follows the system, so an unpinned test renders in
|
|
396
|
+
// whatever colours the developer's own desktop happens to be in. Pinned to
|
|
397
|
+
// "no desktop said anything", which is the truth here and resolves to the
|
|
398
|
+
// light palette. `setAppearanceForTests({ colorScheme: 'dark' })` is the
|
|
399
|
+
// other half of the matrix.
|
|
400
|
+
setAppearanceForTests({});
|
|
401
|
+
// And a monitor, for the third reason of the same kind: a `<window>` with
|
|
402
|
+
// no size is sized from its content and capped at the screen, and a mock
|
|
403
|
+
// with no screen to cap against would let a headless test grow a window to
|
|
404
|
+
// a size no desktop would ever have given it. One 1280x800 output, which
|
|
405
|
+
// is a modest laptop — big enough not to be in the way of a test that is
|
|
406
|
+
// about something else, small enough that a test about the cap can reach
|
|
407
|
+
// it. `setScreensForTests(app, {})` takes it away again.
|
|
408
|
+
setScreensForTests(app, {
|
|
409
|
+
monitors: [{ x: 0, y: 0, width: 1280, height: 800 }],
|
|
410
|
+
});
|
|
411
|
+
return app;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Move the pointer inside a window, in window coordinates.
|
|
416
|
+
*
|
|
417
|
+
* `time` is the X server timestamp in milliseconds. Anything that reads the
|
|
418
|
+
* pointer's *speed* rather than its position needs it — the attention
|
|
419
|
+
* tracker estimates a trajectory from consecutive samples — and a test that
|
|
420
|
+
* leaves it out gets the wall clock, which makes the velocity depend on how
|
|
421
|
+
* fast the machine ran the loop.
|
|
422
|
+
*/
|
|
423
|
+
export function moveMouse(wnd, x, y, { time } = {}) {
|
|
424
|
+
wnd.emit('mousemove', time === undefined ? { x, y } : { x, y, time });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Press and release button 1 at a point; pass {release:false} to hold. */
|
|
428
|
+
export function pressButton(wnd, x, y, { press = true, release = true } = {}) {
|
|
429
|
+
if (press) wnd.emit('mousedown', { x, y, keycode: 1 });
|
|
430
|
+
if (release) wnd.emit('mouseup', { x, y, keycode: 1 });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* A scroll at a point, in **notches** — ntk's `wheel` event, which is what
|
|
435
|
+
* the renderer reads whether the connection had XI2 or only the wheel
|
|
436
|
+
* buttons behind it.
|
|
437
|
+
*
|
|
438
|
+
* A notch is one click of a wheel, and a fraction of one is what a touchpad
|
|
439
|
+
* measures, so `{ deltaY: 0.25, smooth: true }` is a quarter of a notch from
|
|
440
|
+
* a device that can do that — the case the emulated buttons cannot express
|
|
441
|
+
* and a test of smooth scrolling is about. `buttons` takes the X modifier
|
|
442
|
+
* mask, for the Shift that turns a vertical wheel sideways.
|
|
443
|
+
*/
|
|
444
|
+
export function spinWheel(
|
|
445
|
+
wnd,
|
|
446
|
+
x,
|
|
447
|
+
y,
|
|
448
|
+
{ deltaX = 0, deltaY = 1, smooth = false, buttons = 0 } = {},
|
|
449
|
+
) {
|
|
450
|
+
wnd.emit('wheel', {
|
|
451
|
+
name: 'wheel',
|
|
452
|
+
x,
|
|
453
|
+
y,
|
|
454
|
+
rootx: x,
|
|
455
|
+
rooty: y,
|
|
456
|
+
buttons,
|
|
457
|
+
deltaX,
|
|
458
|
+
deltaY,
|
|
459
|
+
deltaMode: 'line',
|
|
460
|
+
smooth,
|
|
461
|
+
source: smooth ? 'valuator' : 'button',
|
|
462
|
+
});
|
|
463
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Pixel assertions: what the X server actually put on the surface, read
|
|
2
|
+
// back with GetImage.
|
|
3
|
+
//
|
|
4
|
+
// This is the assertion that cannot lie. A snapshot of a node tree proves
|
|
5
|
+
// the tree; a pixel proves the tree, the layout, the clip, the paint order,
|
|
6
|
+
// the colour parsing and the wire encoding all at once — and it is the only
|
|
7
|
+
// way to catch the class of bug where everything is committed correctly and
|
|
8
|
+
// nothing reaches the screen.
|
|
9
|
+
//
|
|
10
|
+
// Two details the readback demands:
|
|
11
|
+
//
|
|
12
|
+
// - **Straight RGBA.** `getImageData` normalizes the server's native byte
|
|
13
|
+
// order and premultiplication away, so `[i], [i+1], [i+2]` is red, green,
|
|
14
|
+
// blue — the canvas contract. This used to be a raw BGRA readback the
|
|
15
|
+
// caller had to un-swizzle, and reading it the old way now swaps red and
|
|
16
|
+
// blue, which looks like a colour bug in the code under test.
|
|
17
|
+
// - **Antialiasing means tolerance.** A glyph edge or a rounded corner is
|
|
18
|
+
// never exactly the fill colour, so every comparison here takes one.
|
|
19
|
+
|
|
20
|
+
const DEFAULT_TOLERANCE = 8;
|
|
21
|
+
|
|
22
|
+
/** `#rgb`, `#rrggbb` or `[r,g,b]` → `[r, g, b]`. */
|
|
23
|
+
export function toRgb(colour) {
|
|
24
|
+
if (Array.isArray(colour)) return colour.slice(0, 3);
|
|
25
|
+
const hex = String(colour).replace('#', '');
|
|
26
|
+
const full =
|
|
27
|
+
hex.length === 3
|
|
28
|
+
? hex
|
|
29
|
+
.split('')
|
|
30
|
+
.map((c) => c + c)
|
|
31
|
+
.join('')
|
|
32
|
+
: hex;
|
|
33
|
+
return [
|
|
34
|
+
parseInt(full.slice(0, 2), 16),
|
|
35
|
+
parseInt(full.slice(2, 4), 16),
|
|
36
|
+
parseInt(full.slice(4, 6), 16),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readImage(ctx, x, y, width, height) {
|
|
41
|
+
return new Promise((resolve, reject) =>
|
|
42
|
+
ctx.getImageData(x, y, width, height, (err, data) =>
|
|
43
|
+
err ? reject(err) : resolve(data),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `[r, g, b]` at a window coordinate. */
|
|
49
|
+
export async function pixelAt(ctx, x, y) {
|
|
50
|
+
const image = await readImage(ctx, x, y, 1, 1);
|
|
51
|
+
// ntk's getImageData speaks straight RGBA, like canvas — the server's own
|
|
52
|
+
// byte order is its business, not ours
|
|
53
|
+
return [image.data[0], image.data[1], image.data[2]];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether two colours are the same within a tolerance. */
|
|
57
|
+
export function isNear(rgb, want, tolerance = DEFAULT_TOLERANCE) {
|
|
58
|
+
const target = toRgb(want);
|
|
59
|
+
return rgb.every((c, i) => Math.abs(c - target[i]) <= tolerance);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hex = (rgb) =>
|
|
63
|
+
'#' + rgb.map((c) => c.toString(16).padStart(2, '0')).join('');
|
|
64
|
+
|
|
65
|
+
/** Assert the pixel at `x,y`. Throws with both colours in hex on a miss. */
|
|
66
|
+
export async function expectPixel(ctx, x, y, want, options = {}) {
|
|
67
|
+
const { tolerance = DEFAULT_TOLERANCE, message = null } = options;
|
|
68
|
+
const got = await pixelAt(ctx, x, y);
|
|
69
|
+
if (isNear(got, want, tolerance)) return got;
|
|
70
|
+
throw new Error(
|
|
71
|
+
`${message ? message + ': ' : ''}expected ${hex(toRgb(want))} at ${x},${y}` +
|
|
72
|
+
` (±${tolerance}), got ${hex(got)}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Poll until the pixel arrives, or fail. For anything that finishes on its
|
|
78
|
+
* own schedule — a transition, an image decode, an async rich-content
|
|
79
|
+
* reflow — where the alternative is a sleep long enough to be flaky anyway.
|
|
80
|
+
*/
|
|
81
|
+
export async function waitForPixel(ctx, x, y, want, options = {}) {
|
|
82
|
+
const {
|
|
83
|
+
timeout = 2000,
|
|
84
|
+
interval = 20,
|
|
85
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
86
|
+
message = null,
|
|
87
|
+
} = options;
|
|
88
|
+
const deadline = Date.now() + timeout;
|
|
89
|
+
let got;
|
|
90
|
+
for (;;) {
|
|
91
|
+
got = await pixelAt(ctx, x, y);
|
|
92
|
+
if (isNear(got, want, tolerance)) return got;
|
|
93
|
+
if (Date.now() >= deadline) break;
|
|
94
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
95
|
+
}
|
|
96
|
+
throw new Error(
|
|
97
|
+
`${message ? message + ': ' : ''}timed out after ${timeout}ms waiting for ` +
|
|
98
|
+
`${hex(toRgb(want))} at ${x},${y} (±${tolerance}); last read ${hex(got)}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** How many pixels in a region are near a colour — "did it draw anything?" */
|
|
103
|
+
export async function countPixels(
|
|
104
|
+
ctx,
|
|
105
|
+
region,
|
|
106
|
+
want,
|
|
107
|
+
tolerance = DEFAULT_TOLERANCE,
|
|
108
|
+
) {
|
|
109
|
+
const { x = 0, y = 0, width, height } = region;
|
|
110
|
+
const image = await readImage(ctx, x, y, width, height);
|
|
111
|
+
const target = toRgb(want);
|
|
112
|
+
let n = 0;
|
|
113
|
+
for (let i = 0; i < width * height; i++) {
|
|
114
|
+
const rgb = [
|
|
115
|
+
image.data[i * 4],
|
|
116
|
+
image.data[i * 4 + 1],
|
|
117
|
+
image.data[i * 4 + 2],
|
|
118
|
+
];
|
|
119
|
+
if (rgb.every((c, k) => Math.abs(c - target[k]) <= tolerance)) n++;
|
|
120
|
+
}
|
|
121
|
+
return n;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Write a PNG of the window (or a region of it). Not an assertion — the
|
|
126
|
+
* thing to reach for when a pixel assertion fails and the message is not
|
|
127
|
+
* enough, and how a PR gets a screenshot rendered by its own code.
|
|
128
|
+
*/
|
|
129
|
+
export async function toPNG(ctx, file, region = {}) {
|
|
130
|
+
const { PNG } = await import('pngjs');
|
|
131
|
+
const { writeFileSync } = await import('node:fs');
|
|
132
|
+
const { x = 0, y = 0, width, height } = region;
|
|
133
|
+
if (!width || !height) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
'react-x11/test: toPNG needs { width, height } — the context does not ' +
|
|
136
|
+
'know how big the window is.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const image = await readImage(ctx, x, y, width, height);
|
|
140
|
+
const png = new PNG({ width, height });
|
|
141
|
+
for (let i = 0; i < width * height; i++) {
|
|
142
|
+
// both sides are straight RGBA now; only the alpha is ours to decide,
|
|
143
|
+
// and a screenshot wants an opaque one
|
|
144
|
+
png.data[i * 4 + 0] = image.data[i * 4 + 0];
|
|
145
|
+
png.data[i * 4 + 1] = image.data[i * 4 + 1];
|
|
146
|
+
png.data[i * 4 + 2] = image.data[i * 4 + 2];
|
|
147
|
+
png.data[i * 4 + 3] = 255;
|
|
148
|
+
}
|
|
149
|
+
const buffer = PNG.sync.write(png);
|
|
150
|
+
if (file) writeFileSync(file, buffer);
|
|
151
|
+
return buffer;
|
|
152
|
+
}
|