react-x11 2.36.0 → 2.37.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/package.json +1 -1
- package/src/appearance.js +48 -15
- package/src/appearancehooks.js +1 -1
- package/src/testing/harness.js +148 -9
- package/src/testing/index.d.ts +46 -1
- package/src/testing/index.js +1 -0
- package/src/types/appearance.d.ts +2 -2
package/package.json
CHANGED
package/src/appearance.js
CHANGED
|
@@ -135,12 +135,22 @@ const SAME = (a, b) =>
|
|
|
135
135
|
* and a re-render per announcement would be churn with nothing behind it.
|
|
136
136
|
*/
|
|
137
137
|
function publish(values, source) {
|
|
138
|
+
// **A pin is a pin.** A rung can answer long after it was asked — the
|
|
139
|
+
// ladder a test started before releasing its pin, a watcher's next line, a
|
|
140
|
+
// portal signal — and its answer lands in whatever test is running by
|
|
141
|
+
// then. Taken, that turns the next test's pixels into this machine's
|
|
142
|
+
// desktop, which is the one thing the pin exists to prevent.
|
|
143
|
+
if (owner === 'test' && source !== 'test') return;
|
|
138
144
|
const next = Object.freeze({ ...NOTHING, ...values, source });
|
|
139
145
|
if (SAME(next, snapshot)) return;
|
|
140
146
|
snapshot = next;
|
|
141
|
-
// What came *off* the disk does
|
|
142
|
-
// value must never reach a developer's
|
|
143
|
-
|
|
147
|
+
// Only what a rung answered is remembered. What came *off* the disk does
|
|
148
|
+
// not go back onto it, a pinned test value must never reach a developer's
|
|
149
|
+
// real cache file — and nor must the defaults a released pin falls back
|
|
150
|
+
// to (`source: null`), which every `cleanup()` publishes: written, they
|
|
151
|
+
// replaced the developer's remembered desktop after every test, and their
|
|
152
|
+
// next app drew its first frame in the wrong colours.
|
|
153
|
+
if (source && source !== 'cache' && source !== 'test') save(next);
|
|
144
154
|
for (const fn of [...watchers]) {
|
|
145
155
|
try {
|
|
146
156
|
fn(snapshot);
|
|
@@ -162,27 +172,42 @@ function publish(values, source) {
|
|
|
162
172
|
* Pass `null` to release the pin and let the ladder run again.
|
|
163
173
|
*/
|
|
164
174
|
export function setAppearanceForTests(values) {
|
|
165
|
-
probe = null;
|
|
166
|
-
// Never touch the developer's own remembered answer either, in either
|
|
167
|
-
// direction: `publish` will not write a pinned value, and marking the cache
|
|
168
|
-
// as already checked keeps `load()` from reading one back over it.
|
|
169
|
-
cacheChecked = true;
|
|
170
175
|
if (values === null) {
|
|
171
|
-
cacheChecked = false;
|
|
172
176
|
// Releasing undoes a *pin*, and only a pin. A real rung's answer is not
|
|
173
177
|
// this function's to throw away: `cleanup()` releases after every test,
|
|
174
178
|
// and a suite that resolved the appearance for real would otherwise find
|
|
175
|
-
// it wiped by the harness that was meant to leave it alone
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
// it wiped by the harness that was meant to leave it alone — or, with
|
|
180
|
+
// the cache marked unread, replaced on the next read by what the disk
|
|
181
|
+
// says, which is a verified answer turned back into a remembered one.
|
|
182
|
+
if (owner !== 'test') return;
|
|
183
|
+
owner = null;
|
|
184
|
+
probe = null;
|
|
185
|
+
// Unpinned, the remembered answer is this machine's to read again —
|
|
186
|
+
// intact, because neither the pin nor this release is ever written
|
|
187
|
+
// (`publish`).
|
|
188
|
+
cacheChecked = false;
|
|
189
|
+
publish({}, null);
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
192
|
+
probe = null;
|
|
193
|
+
// Never touch the developer's own remembered answer either, in either
|
|
194
|
+
// direction: `publish` will not write a pinned value, and marking the cache
|
|
195
|
+
// as already checked keeps `load()` from reading one back over it.
|
|
196
|
+
cacheChecked = true;
|
|
182
197
|
owner = 'test';
|
|
183
198
|
publish(values, 'test');
|
|
184
199
|
}
|
|
185
200
|
|
|
201
|
+
/**
|
|
202
|
+
* What is pinned right now, or null where nothing is. `react-x11/test`'s
|
|
203
|
+
* `setAppearance` merges over it, so changing one value keeps the rest of the
|
|
204
|
+
* pin — and asks the store rather than keeping a copy, which a direct
|
|
205
|
+
* `setAppearanceForTests` call would leave stale.
|
|
206
|
+
*/
|
|
207
|
+
export function pinnedAppearanceForTests() {
|
|
208
|
+
return owner === 'test' ? snapshot : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
186
211
|
// --------------------------------------------------------------------------
|
|
187
212
|
// The last known answer
|
|
188
213
|
// --------------------------------------------------------------------------
|
|
@@ -929,6 +954,11 @@ async function runWatcher() {
|
|
|
929
954
|
answered = true;
|
|
930
955
|
publish(values, 'macos');
|
|
931
956
|
done(true);
|
|
957
|
+
// Refused, because a test pinned the store while this was starting
|
|
958
|
+
// (`publish`): nothing will hear this watcher again, and while it
|
|
959
|
+
// lives the rung starts no other — so the ladder that runs once the
|
|
960
|
+
// pin is released would find the Mac silent.
|
|
961
|
+
if (owner === 'test') proc.kill();
|
|
932
962
|
}
|
|
933
963
|
};
|
|
934
964
|
proc.stdout.setEncoding?.('utf8');
|
|
@@ -1020,7 +1050,10 @@ async function runLadder(app) {
|
|
|
1020
1050
|
answered = false;
|
|
1021
1051
|
}
|
|
1022
1052
|
if (answered) {
|
|
1023
|
-
|
|
1053
|
+
// A pin set while this rung was being asked keeps the store: the
|
|
1054
|
+
// rung's answer was refused (`publish`), and owning it would hand the
|
|
1055
|
+
// rung's live updates a store they must not write to.
|
|
1056
|
+
if (owner !== 'test') owner = name;
|
|
1024
1057
|
return snapshot;
|
|
1025
1058
|
}
|
|
1026
1059
|
}
|
package/src/appearancehooks.js
CHANGED
|
@@ -73,7 +73,7 @@ export function useAppearanceWhen(enabled) {
|
|
|
73
73
|
* | `selection` | the fill under a selected menu or list row — a darker cut of the accent on macOS — or **null** |
|
|
74
74
|
* | `contrast` | `'normal'` or `'high'` |
|
|
75
75
|
* | `reducedMotion` | `true` when the user asked for less animation |
|
|
76
|
-
* | `source` | `'portal'`, `'xsettings'`, `'macos'`, `'cache'`, or null |
|
|
76
|
+
* | `source` | `'portal'`, `'xsettings'`, `'macos'`, `'windows'`, `'cache'`, `'test'` (pinned by `react-x11/test`), or null |
|
|
77
77
|
*
|
|
78
78
|
* **`'no-preference'` means use your own default**, not "use light". And
|
|
79
79
|
* `accent` is null far more often than not, so fall back to your own brand
|
package/src/testing/harness.js
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { createRoot } from '../Reconciler.js';
|
|
12
12
|
import { setAnimationClock } from '../nodes/animation.js';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
pinnedAppearanceForTests,
|
|
15
|
+
setAppearanceForTests,
|
|
16
|
+
} from '../appearance.js';
|
|
14
17
|
|
|
15
18
|
// A test process must not register with the desktop's live AT-SPI registry —
|
|
16
19
|
// a suite on a developer desktop would otherwise parade hundreds of phantom
|
|
@@ -140,10 +143,26 @@ export async function renderX11(element, options = {}) {
|
|
|
140
143
|
wrap = element?.type !== 'window',
|
|
141
144
|
title = 'react-x11 test',
|
|
142
145
|
colorScheme = 'light',
|
|
146
|
+
appearance = null,
|
|
143
147
|
a11y = false,
|
|
144
148
|
...rootOptions
|
|
145
149
|
} = options;
|
|
146
150
|
|
|
151
|
+
// Checked before anything is created, so a mistake costs no server
|
|
152
|
+
const pin = appearancePin(colorScheme, appearance);
|
|
153
|
+
if (setBeforeMount) {
|
|
154
|
+
const values = setBeforeMount;
|
|
155
|
+
setBeforeMount = null;
|
|
156
|
+
throw new Error(
|
|
157
|
+
'react-x11/test: setAppearance() was called before renderX11(), which ' +
|
|
158
|
+
'pins the appearance as it mounts — the values it set would be ' +
|
|
159
|
+
'replaced without a word. Pass them to the render instead:\n\n' +
|
|
160
|
+
` await renderX11(<App />, { appearance: ${JSON.stringify(values)} });\n\n` +
|
|
161
|
+
'setAppearance() is for changing them once the tree is mounted. ' +
|
|
162
|
+
'See docs/testing.md#the-desktops-appearance.',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
147
166
|
let server = null;
|
|
148
167
|
let app = options.app ?? null;
|
|
149
168
|
if (!app) {
|
|
@@ -161,14 +180,9 @@ export async function renderX11(element, options = {}) {
|
|
|
161
180
|
// on a light desktop, red on a dark one, with nothing in the test saying
|
|
162
181
|
// so. After the app exists, so it wins over `createMockApp`'s own pin.
|
|
163
182
|
// `colorScheme: 'dark'` renders the other palette; `'system'` releases the
|
|
164
|
-
// pin, for a test that is *about* what the desktop reports
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
? null
|
|
168
|
-
: colorScheme === 'dark'
|
|
169
|
-
? { colorScheme: 'dark' }
|
|
170
|
-
: {},
|
|
171
|
-
);
|
|
183
|
+
// pin, for a test that is *about* what the desktop reports; `appearance`
|
|
184
|
+
// pins the rest of what `useSystemAppearance()` reports.
|
|
185
|
+
setAppearanceForTests(pin);
|
|
172
186
|
|
|
173
187
|
const root = await createRoot({ app, ...rootOptions });
|
|
174
188
|
|
|
@@ -374,6 +388,131 @@ export async function cleanup() {
|
|
|
374
388
|
// change, every mounted root repaints on one, and a repaint scheduled onto
|
|
375
389
|
// a connection that is about to close lands after it has.
|
|
376
390
|
setAppearanceForTests(null);
|
|
391
|
+
setBeforeMount = null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// --- the desktop's appearance ----------------------------------------------
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* What a test may pin, and what each value may be — the fields
|
|
398
|
+
* `useSystemAppearance()` reports, less `source`, which reads `'test'` for as
|
|
399
|
+
* long as anything is pinned.
|
|
400
|
+
*
|
|
401
|
+
* Checked rather than passed through, because the store takes anything: a
|
|
402
|
+
* `reduceMotion: true` would pin nothing, render the component the way it
|
|
403
|
+
* always does, and pass a test that was meant to prove the opposite.
|
|
404
|
+
*/
|
|
405
|
+
const APPEARANCE = {
|
|
406
|
+
colorScheme: [
|
|
407
|
+
(v) => v === 'light' || v === 'dark' || v === 'no-preference',
|
|
408
|
+
"'light', 'dark' or 'no-preference'",
|
|
409
|
+
],
|
|
410
|
+
accent: [colourOrNull, "a CSS colour such as '#ed5b00', or null"],
|
|
411
|
+
accentText: [colourOrNull, "a CSS colour such as '#ffffff', or null"],
|
|
412
|
+
selection: [colourOrNull, "a CSS colour such as '#c96003', or null"],
|
|
413
|
+
palette: [
|
|
414
|
+
(v) => v === null || (typeof v === 'object' && !Array.isArray(v)),
|
|
415
|
+
'an object of palette tokens, or null',
|
|
416
|
+
],
|
|
417
|
+
contrast: [(v) => v === 'normal' || v === 'high', "'normal' or 'high'"],
|
|
418
|
+
reducedMotion: [(v) => typeof v === 'boolean', 'true or false'],
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
function colourOrNull(value) {
|
|
422
|
+
return value === null || (typeof value === 'string' && value !== '');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function checkAppearance(values, where) {
|
|
426
|
+
if (!values || typeof values !== 'object' || Array.isArray(values)) {
|
|
427
|
+
throw new TypeError(
|
|
428
|
+
`react-x11/test: ${where} takes an object of appearance values, such ` +
|
|
429
|
+
`as { reducedMotion: true } — got ${String(values)}.`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
for (const [key, value] of Object.entries(values)) {
|
|
433
|
+
if (key === 'source') {
|
|
434
|
+
throw new TypeError(
|
|
435
|
+
`react-x11/test: ${where} cannot set \`source\` — a pinned ` +
|
|
436
|
+
"appearance reports 'test', which is how a component can tell " +
|
|
437
|
+
'one from a desktop that answered.',
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const rule = APPEARANCE[key];
|
|
441
|
+
if (!rule) {
|
|
442
|
+
throw new TypeError(
|
|
443
|
+
`react-x11/test: ${where} has no appearance value \`${key}\`. ` +
|
|
444
|
+
`The values are ${Object.keys(APPEARANCE).join(', ')} — the ` +
|
|
445
|
+
'fields useSystemAppearance() reports.',
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (!rule[0](value)) {
|
|
449
|
+
throw new TypeError(
|
|
450
|
+
`react-x11/test: ${where}: \`${key}\` is ${rule[1]}, not ` +
|
|
451
|
+
`${JSON.stringify(value) ?? String(value)}.`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return values;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** `renderX11`'s two appearance options → the pin, or null to release. */
|
|
459
|
+
function appearancePin(colorScheme, appearance) {
|
|
460
|
+
if (colorScheme === 'system') {
|
|
461
|
+
if (appearance) {
|
|
462
|
+
throw new TypeError(
|
|
463
|
+
"react-x11/test: renderX11 was given colorScheme: 'system', which " +
|
|
464
|
+
'follows the real desktop, and `appearance`, which pins it. Pass ' +
|
|
465
|
+
'one: pin `colorScheme` inside `appearance`, or drop `appearance` ' +
|
|
466
|
+
'to follow the desktop.',
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
if (colorScheme !== 'light' && colorScheme !== 'dark') {
|
|
472
|
+
throw new TypeError(
|
|
473
|
+
"react-x11/test: renderX11's colorScheme is 'light' (the default), " +
|
|
474
|
+
`'dark' or 'system', not ${JSON.stringify(colorScheme)}.`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
// Light is pinned as "the desktop said nothing", which resolves to the
|
|
479
|
+
// light palette and is what the mock app pins too
|
|
480
|
+
...(colorScheme === 'dark' ? { colorScheme: 'dark' } : {}),
|
|
481
|
+
...(appearance ? checkAppearance(appearance, 'renderX11 appearance') : {}),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Values set with nothing mounted, which the next `renderX11` would lose. */
|
|
486
|
+
let setBeforeMount = null;
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Change the pinned appearance under a mounted tree, and flush what it
|
|
490
|
+
* changed — the re-render of everything reading `useSystemAppearance()`, the
|
|
491
|
+
* palette's repaint, and the frame — the way `userEvent` flushes an input.
|
|
492
|
+
*
|
|
493
|
+
* Merged over what is pinned: `setAppearance({ reducedMotion: true })` under
|
|
494
|
+
* a `colorScheme: 'dark'` render stays dark. Where nothing is pinned
|
|
495
|
+
* (`colorScheme: 'system'`), it pins these values over the defaults rather
|
|
496
|
+
* than over this machine's answer, so the result is the test's alone.
|
|
497
|
+
*
|
|
498
|
+
* ```js
|
|
499
|
+
* await renderX11(<Spinner />, { appearance: { reducedMotion: true } });
|
|
500
|
+
* await setAppearance({ reducedMotion: false });
|
|
501
|
+
* ```
|
|
502
|
+
*
|
|
503
|
+
* The appearance is the desktop's, so it is one for the process: every
|
|
504
|
+
* mounted root sees the change, as every window would. `cleanup()` releases
|
|
505
|
+
* it, and a pinned value is never written to the cache the next run of a
|
|
506
|
+
* real app reads.
|
|
507
|
+
*/
|
|
508
|
+
export async function setAppearance(values) {
|
|
509
|
+
checkAppearance(values, 'setAppearance()');
|
|
510
|
+
// `source` comes along and does not matter: the pin overwrites it
|
|
511
|
+
const next = { ...pinnedAppearanceForTests(), ...values };
|
|
512
|
+
if (mounted.size === 0) setBeforeMount = { ...setBeforeMount, ...values };
|
|
513
|
+
await act(() => {
|
|
514
|
+
setAppearanceForTests(next);
|
|
515
|
+
});
|
|
377
516
|
}
|
|
378
517
|
|
|
379
518
|
// --- the animation clock ---------------------------------------------------
|
package/src/testing/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ReactNode } from 'react';
|
|
7
7
|
import type { DrawnNode, NtkApp, NtkWindow } from '../types/nodes.js';
|
|
8
|
+
import type { SystemAppearance } from '../types/appearance.js';
|
|
8
9
|
import type { ModifierName } from '../keysyms.js';
|
|
9
10
|
|
|
10
11
|
export * from '../keysyms.js';
|
|
@@ -29,6 +30,14 @@ export interface TestServer {
|
|
|
29
30
|
/** ntk's 2d context. Pixel helpers take it; `renderX11` hands it back. */
|
|
30
31
|
export type TestContext2D = unknown;
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The desktop's appearance as a test pins it: any of the values
|
|
35
|
+
* `useSystemAppearance()` reports, except `source`, which reads `'test'` for
|
|
36
|
+
* as long as anything is pinned. A key outside these throws, so a typo cannot
|
|
37
|
+
* pin nothing and pass.
|
|
38
|
+
*/
|
|
39
|
+
export type PinnedAppearance = Partial<Omit<SystemAppearance, 'source'>>;
|
|
40
|
+
|
|
32
41
|
export interface RenderX11Options {
|
|
33
42
|
/** Window size (default 640×480). */
|
|
34
43
|
width?: number;
|
|
@@ -59,6 +68,21 @@ export interface RenderX11Options {
|
|
|
59
68
|
title?: string;
|
|
60
69
|
/** Render into a connection you already have. */
|
|
61
70
|
app?: NtkApp;
|
|
71
|
+
/**
|
|
72
|
+
* The desktop's colour scheme, **pinned** so a pixel assertion means the
|
|
73
|
+
* same on every machine. `'light'` (the default) and `'dark'` pick the
|
|
74
|
+
* built-in palette; `'system'` releases the pin, for a test that is about
|
|
75
|
+
* what the real desktop reports.
|
|
76
|
+
*/
|
|
77
|
+
colorScheme?: 'light' | 'dark' | 'system';
|
|
78
|
+
/**
|
|
79
|
+
* The rest of the desktop's appearance, pinned from the first render:
|
|
80
|
+
* reduced motion, contrast, the accent. Merged over `colorScheme`, so
|
|
81
|
+
* `appearance.colorScheme` wins where both name one; it cannot be combined
|
|
82
|
+
* with `colorScheme: 'system'`. To change it while mounted, use
|
|
83
|
+
* `setAppearance()`.
|
|
84
|
+
*/
|
|
85
|
+
appearance?: PinnedAppearance;
|
|
62
86
|
/**
|
|
63
87
|
* Install the assistive-technology spy before the mount and hand it back
|
|
64
88
|
* as `at`: an in-process log of everything a screen reader would have
|
|
@@ -288,9 +312,30 @@ export function renderX11(
|
|
|
288
312
|
*/
|
|
289
313
|
export function act(fn?: () => unknown): Promise<void>;
|
|
290
314
|
|
|
291
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Unmount everything, restore the animation clock, release the pinned
|
|
317
|
+
* appearance, close every server.
|
|
318
|
+
*/
|
|
292
319
|
export function cleanup(): Promise<void>;
|
|
293
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Change the pinned appearance under a mounted tree and flush what it moved:
|
|
323
|
+
* every component reading `useSystemAppearance()` re-renders, the palette
|
|
324
|
+
* repaints, and the frame lands — the way `userEvent` flushes an input.
|
|
325
|
+
*
|
|
326
|
+
* Merged over what is pinned, so `setAppearance({ reducedMotion: true })`
|
|
327
|
+
* under a `colorScheme: 'dark'` render stays dark. The appearance is the
|
|
328
|
+
* desktop's and so one per process: every mounted root sees the change.
|
|
329
|
+
* Calling it before `renderX11()` is an error at that render, which pins as
|
|
330
|
+
* it mounts — pass `appearance` to the render instead.
|
|
331
|
+
*
|
|
332
|
+
* ```ts
|
|
333
|
+
* await renderX11(<Spinner />, { appearance: { reducedMotion: true } });
|
|
334
|
+
* await setAppearance({ reducedMotion: false });
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
export function setAppearance(values: PinnedAppearance): Promise<void>;
|
|
338
|
+
|
|
294
339
|
/** Drain in-flight requests on a connection. */
|
|
295
340
|
export function settle(app: NtkApp, roundTrips?: number): Promise<void>;
|
|
296
341
|
|
package/src/testing/index.js
CHANGED
|
@@ -20,10 +20,10 @@ export type ColorSchemePreference = 'system' | 'light' | 'dark';
|
|
|
20
20
|
*
|
|
21
21
|
* `'cache'` is the answer this machine gave last time, read off disk before
|
|
22
22
|
* the first render so it does not start from the defaults. A real rung
|
|
23
|
-
* replaces it a moment later.
|
|
23
|
+
* replaces it a moment later. `'test'` is a value `react-x11/test` pinned.
|
|
24
24
|
*/
|
|
25
25
|
export type AppearanceSource =
|
|
26
|
-
'portal' | 'xsettings' | 'macos' | 'cache' | null;
|
|
26
|
+
'portal' | 'xsettings' | 'macos' | 'windows' | 'cache' | 'test' | null;
|
|
27
27
|
|
|
28
28
|
export interface SystemAppearance {
|
|
29
29
|
readonly colorScheme: ColorScheme;
|