react-x11 2.45.0 → 2.45.1
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/package.json +1 -1
- package/src/cocoa/glarea.js +18 -2
- package/src/foreignnodes.js +9 -6
- package/src/glnodes.js +12 -2
- package/src/nodes/node.js +19 -0
- package/src/testing/harness.js +25 -7
- package/src/testing/index.d.ts +17 -1
- package/src/testing/queries.js +7 -18
package/package.json
CHANGED
package/src/cocoa/glarea.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// What GlAreaNode needs from us is the child-"window" contract it already
|
|
12
12
|
// speaks (src/glnodes.js): `createWindow({ parent, … })` answering an
|
|
13
13
|
// object with `getContext('opengl', config)`, `setState(rect)`, `map()`,
|
|
14
|
-
// `destroy()`, `requestAnimationFrame`. On X11 that child is a real X
|
|
14
|
+
// `unmap()`, `destroy()`, `requestAnimationFrame`. On X11 that child is a real X
|
|
15
15
|
// window stacked above the parent's drawing; here it is a sublayer of the
|
|
16
16
|
// window's root layer with a high zPosition — the same "GL sits above the
|
|
17
17
|
// 2D" semantics, by the same mechanism the platform gives us. The layer
|
|
@@ -153,6 +153,9 @@ export class CocoaGLArea {
|
|
|
153
153
|
return layer;
|
|
154
154
|
});
|
|
155
155
|
this.rect = null;
|
|
156
|
+
// Ours to keep, because every `setState` writes the layer's whole prop
|
|
157
|
+
// set: a geometry change on a hidden surface must not show it again.
|
|
158
|
+
this._hidden = false;
|
|
156
159
|
this.setState({
|
|
157
160
|
x: options.x ?? 0,
|
|
158
161
|
y: options.y ?? 0,
|
|
@@ -178,7 +181,7 @@ export class CocoaGLArea {
|
|
|
178
181
|
// mirroring the layer is the flip, applied where the compositor is
|
|
179
182
|
// already transforming instead of in anyone's shader.
|
|
180
183
|
transform: { scaleY: -1 },
|
|
181
|
-
hidden:
|
|
184
|
+
hidden: this._hidden,
|
|
182
185
|
});
|
|
183
186
|
this._context?._resized(rect.width, rect.height);
|
|
184
187
|
}
|
|
@@ -200,9 +203,22 @@ export class CocoaGLArea {
|
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
map() {
|
|
206
|
+
this._hidden = false;
|
|
203
207
|
if (!this.destroyed) this._setLayerProps({ hidden: false });
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The other half of `map()`, which this surface went without: a layer is
|
|
212
|
+
* composited by the WindowServer rather than painted by the window, so a
|
|
213
|
+
* subtree going off the screen leaves it on screen over what replaced it.
|
|
214
|
+
* X11 and Wayland unmap the surface's window for this; here the layer's
|
|
215
|
+
* own `hidden` carries it.
|
|
216
|
+
*/
|
|
217
|
+
unmap() {
|
|
218
|
+
this._hidden = true;
|
|
219
|
+
if (!this.destroyed) this._setLayerProps({ hidden: true });
|
|
220
|
+
}
|
|
221
|
+
|
|
206
222
|
/**
|
|
207
223
|
* Everything this layer is handed goes out with Core Animation's implicit
|
|
208
224
|
* actions off. The layer is ours, not a presenter's, so no frame's
|
package/src/foreignnodes.js
CHANGED
|
@@ -488,18 +488,21 @@ export class ForeignNode extends Node {
|
|
|
488
488
|
wnd.setBackgroundPixel(pixel);
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
|
|
492
|
-
|
|
491
|
+
/** The client's window is a window of the display system over this one,
|
|
492
|
+
* so it follows the whole subtree's visibility — see
|
|
493
|
+
* `GlAreaNode._syncSurfaceVisibility`, which answers the same call. */
|
|
494
|
+
_syncSurfaceVisibility() {
|
|
493
495
|
this._syncMapped();
|
|
494
496
|
}
|
|
495
497
|
|
|
496
|
-
/** The container follows
|
|
497
|
-
*
|
|
498
|
-
*
|
|
498
|
+
/** The container follows whether this subtree is on screen — this node's
|
|
499
|
+
* own `hidden` flag, or anything above it that took the subtree away; the
|
|
500
|
+
* client follows the container, because an inferior of an unmapped window
|
|
501
|
+
* is not viewable however mapped it is itself. */
|
|
499
502
|
_syncMapped() {
|
|
500
503
|
const wnd = this.socket?.window;
|
|
501
504
|
if (!wnd) return;
|
|
502
|
-
if (this.
|
|
505
|
+
if (this._hiddenInTree()) wnd.unmap?.();
|
|
503
506
|
else wnd.map?.();
|
|
504
507
|
}
|
|
505
508
|
|
package/src/glnodes.js
CHANGED
|
@@ -324,6 +324,9 @@ export class GlAreaNode extends Node {
|
|
|
324
324
|
// same zPosition. The window's hit test reads the list in that order
|
|
325
325
|
// (`EventManager._surfaceAt`).
|
|
326
326
|
this._joinSurfaces();
|
|
327
|
+
// A surface created inside a subtree that is already hidden — a tab
|
|
328
|
+
// mounted behind the one on screen — must not come up over it.
|
|
329
|
+
this._syncSurfaceVisibility();
|
|
327
330
|
this.gl = wnd.getContext('opengl', config);
|
|
328
331
|
// A buffer freed by the display is a frame that can be drawn again — for
|
|
329
332
|
// a frame that was waiting for one. The Cocoa surface and the CGL
|
|
@@ -725,8 +728,15 @@ export class GlAreaNode extends Node {
|
|
|
725
728
|
this.requestFrame();
|
|
726
729
|
}
|
|
727
730
|
|
|
728
|
-
|
|
729
|
-
|
|
731
|
+
/**
|
|
732
|
+
* The surface is a window of the display system, so it goes when the
|
|
733
|
+
* subtree does — `display: 'none'` anywhere above it, or React's own
|
|
734
|
+
* `hidden` for `<Activity>`/`<Suspense>`, on this node or on an ancestor.
|
|
735
|
+
* `Node._syncSurfaces` calls this for every route; the panes the children
|
|
736
|
+
* are drawn on follow the same answer.
|
|
737
|
+
*/
|
|
738
|
+
_syncSurfaceVisibility() {
|
|
739
|
+
const hidden = this._hiddenInTree();
|
|
730
740
|
if (hidden) this.window?.unmap?.();
|
|
731
741
|
else this.window?.map?.();
|
|
732
742
|
this._overlay?.setHidden(hidden);
|
package/src/nodes/node.js
CHANGED
|
@@ -860,12 +860,31 @@ export class Node {
|
|
|
860
860
|
// window rather than for this subtree — the set is the handful of nodes
|
|
861
861
|
// that declare an `animation`, and each one answers for itself.
|
|
862
862
|
if (this.root?._loopNodes?.size) this.root._refreshLoops();
|
|
863
|
+
// Same funnel, same reason, one layer down: a surface — a `<glarea>`'s GL
|
|
864
|
+
// child, an embedded client's window — is a real window of the display
|
|
865
|
+
// system stacked above everything this window paints, not pixels in its
|
|
866
|
+
// 2d context. Hiding a subtree takes away its layout and its paint, and
|
|
867
|
+
// reaches a surface inside it through neither: it would stay on screen,
|
|
868
|
+
// over whatever is shown in its place, which is what a tab that keeps its
|
|
869
|
+
// screens mounted does every time it switches.
|
|
870
|
+
this._syncSurfaces();
|
|
863
871
|
const events = this._focusManager();
|
|
864
872
|
if (!events) return;
|
|
865
873
|
if (visible) events.subtreeRevealed(this);
|
|
866
874
|
else events.subtreeHidden(this);
|
|
867
875
|
}
|
|
868
876
|
|
|
877
|
+
/**
|
|
878
|
+
* Ask every surface in this subtree — this node included — whether it is
|
|
879
|
+
* still on screen, and map or unmap it to match. Walked rather than kept
|
|
880
|
+
* in a registry because the subtree whose visibility just changed is
|
|
881
|
+
* exactly the scope, and all but two elements answer nothing.
|
|
882
|
+
*/
|
|
883
|
+
_syncSurfaces() {
|
|
884
|
+
this._syncSurfaceVisibility?.();
|
|
885
|
+
for (const child of this.children) child._syncSurfaces?.();
|
|
886
|
+
}
|
|
887
|
+
|
|
869
888
|
/**
|
|
870
889
|
* Focus this node, as clicking it would: the owning window's focus moves
|
|
871
890
|
* here, `onBlur` fires on whatever had it, `onFocus` here. Also pulls the
|
package/src/testing/harness.js
CHANGED
|
@@ -559,32 +559,50 @@ function restoreFrameClock() {
|
|
|
559
559
|
}
|
|
560
560
|
|
|
561
561
|
/**
|
|
562
|
-
* Retry `fn` until it stops throwing, flushing between attempts
|
|
562
|
+
* Retry `fn` until it stops throwing, flushing between attempts, and resolve
|
|
563
|
+
* with what it returned once that is on screen.
|
|
563
564
|
*
|
|
564
565
|
* The tool for anything whose arrival is not a single deterministic step:
|
|
565
566
|
* an async rich-content reflow, an image decode, a timer, or a repaint whose
|
|
566
|
-
* cause travelled through the X connection and back. Each
|
|
567
|
-
*
|
|
568
|
-
*
|
|
567
|
+
* cause travelled through the X connection and back. Each retry is preceded
|
|
568
|
+
* by an `act()`, so waiting also *advances* — this is not a sleep with a
|
|
569
|
+
* condition on it.
|
|
569
570
|
*
|
|
570
571
|
* ```js
|
|
571
572
|
* await waitFor(async () => {
|
|
572
573
|
* assert.notDeepStrictEqual(await pixelAt(ctx, x, y), before);
|
|
573
574
|
* });
|
|
575
|
+
* const row = await waitFor(() => screen.getByText('notes.txt'));
|
|
576
|
+
* await userEvent.click(row);
|
|
574
577
|
* ```
|
|
578
|
+
*
|
|
579
|
+
* **The attempt that passes is followed by an `act()` too.** What it found
|
|
580
|
+
* can have arrived in the sleep before it — an fs read, a timer — and that
|
|
581
|
+
* commits outside act, on React's own scheduler, while the frame that lays
|
|
582
|
+
* it out runs on the window's clock, which need not have ticked since.
|
|
583
|
+
* Without it, a node it returned could have no rect yet, and the click after
|
|
584
|
+
* it would throw `has no laid-out rect yet` (#904). It follows whichever
|
|
585
|
+
* attempt passed, the first included, since the commit can land before
|
|
586
|
+
* `waitFor` is called. And it has to come after the attempt: an `act()`
|
|
587
|
+
* before each one leaves the same gap in its own closing round trip, after
|
|
588
|
+
* the frame it ran.
|
|
575
589
|
*/
|
|
576
590
|
export async function waitFor(fn, { timeout = 2000, interval = 10 } = {}) {
|
|
577
591
|
const deadline = Date.now() + timeout;
|
|
578
592
|
let last;
|
|
579
593
|
for (;;) {
|
|
594
|
+
let found;
|
|
580
595
|
try {
|
|
581
|
-
|
|
596
|
+
found = await fn();
|
|
582
597
|
} catch (err) {
|
|
583
598
|
last = err;
|
|
599
|
+
if (Date.now() >= deadline) throw last;
|
|
600
|
+
await act();
|
|
601
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
602
|
+
continue;
|
|
584
603
|
}
|
|
585
|
-
if (Date.now() >= deadline) throw last;
|
|
586
604
|
await act();
|
|
587
|
-
|
|
605
|
+
return found;
|
|
588
606
|
}
|
|
589
607
|
}
|
|
590
608
|
|
package/src/testing/index.d.ts
CHANGED
|
@@ -174,6 +174,13 @@ export interface A11ySpy {
|
|
|
174
174
|
uninstall(): void;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Testing Library's four families over the retained node tree: `getBy*`
|
|
179
|
+
* (exactly one, or throw), `queryBy*` (one or null), `getAllBy*` (one or
|
|
180
|
+
* more), and `findBy*` — `getBy*` under {@link waitFor}, with its options and
|
|
181
|
+
* defaults, so it retries until the node appears and resolves once a frame
|
|
182
|
+
* has laid it out.
|
|
183
|
+
*/
|
|
177
184
|
export interface Queries {
|
|
178
185
|
getByText(text: string | RegExp, options?: TextMatchOptions): DrawnNode;
|
|
179
186
|
getAllByText(text: string | RegExp, options?: TextMatchOptions): DrawnNode[];
|
|
@@ -339,7 +346,16 @@ export function setAppearance(values: PinnedAppearance): Promise<void>;
|
|
|
339
346
|
/** Drain in-flight requests on a connection. */
|
|
340
347
|
export function settle(app: NtkApp, roundTrips?: number): Promise<void>;
|
|
341
348
|
|
|
342
|
-
/**
|
|
349
|
+
/**
|
|
350
|
+
* Retry until it stops throwing, `act`-ing between attempts — and once more
|
|
351
|
+
* after the attempt that passed, so what it resolves with is on screen: a
|
|
352
|
+
* node it found has been laid out, and a click on it lands.
|
|
353
|
+
*
|
|
354
|
+
* ```ts
|
|
355
|
+
* const row = await waitFor(() => screen.getByText('notes.txt'));
|
|
356
|
+
* await userEvent.click(row);
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
343
359
|
export function waitFor<T>(
|
|
344
360
|
fn: () => T | Promise<T>,
|
|
345
361
|
options?: WaitOptions,
|
package/src/testing/queries.js
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
// getBy* exactly one, or throw
|
|
13
13
|
// queryBy* one or null, never throws — the way to assert absence
|
|
14
14
|
// getAllBy* one or more, or throw
|
|
15
|
-
// findBy* getBy
|
|
15
|
+
// findBy* getBy* under waitFor: retried until it appears or the timeout
|
|
16
|
+
// expires, and laid out by the time it resolves
|
|
16
17
|
|
|
17
18
|
import { queryAllByComponent, componentInventory } from './components.js';
|
|
19
|
+
import { waitFor } from './harness.js';
|
|
18
20
|
import { roleNameOf } from '../a11y.js';
|
|
19
21
|
|
|
20
|
-
const DEFAULT_TIMEOUT = 1000;
|
|
21
|
-
|
|
22
22
|
/** Depth-first, in paint order, including `<popup>` subtrees. */
|
|
23
23
|
function walk(node, visit) {
|
|
24
24
|
if (!node || node.destroyed) return;
|
|
@@ -97,20 +97,6 @@ function one(root, found, what, printTree = inventory) {
|
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
async function retry(fn, { timeout = DEFAULT_TIMEOUT, interval = 20 } = {}) {
|
|
101
|
-
const deadline = Date.now() + timeout;
|
|
102
|
-
let last;
|
|
103
|
-
for (;;) {
|
|
104
|
-
try {
|
|
105
|
-
return fn();
|
|
106
|
-
} catch (err) {
|
|
107
|
-
last = err;
|
|
108
|
-
if (Date.now() >= deadline) throw last;
|
|
109
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
100
|
/**
|
|
115
101
|
* The queries, bound to a subtree. `within(node)` is this with a different
|
|
116
102
|
* root; `screen` is this bound to whatever `renderX11` mounted last.
|
|
@@ -199,9 +185,12 @@ export function within(root) {
|
|
|
199
185
|
if (found.length > 1) return one(root, found, label(args[0]));
|
|
200
186
|
return found[0] ?? null;
|
|
201
187
|
};
|
|
188
|
+
// Through waitFor rather than a loop of its own: what this resolves with
|
|
189
|
+
// has to be laid out, or a click on it throws, and waitFor is what runs
|
|
190
|
+
// the frame before resolving
|
|
202
191
|
q[`find${suffix}`] = (...args) => {
|
|
203
192
|
const options = args.length > 1 ? args[args.length - 1] : undefined;
|
|
204
|
-
return
|
|
193
|
+
return waitFor(() => q[`get${suffix}`](...args), options);
|
|
205
194
|
};
|
|
206
195
|
}
|
|
207
196
|
return q;
|