@remit/web-client 0.0.212 → 0.0.214
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.214",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -206,6 +206,39 @@ describe("Drawer's ring is what a reader can reach", () => {
|
|
|
206
206
|
|
|
207
207
|
assert.equal(document.activeElement, named("Cancel"));
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The pane renders that dialog without moving focus into it, so focus sits
|
|
212
|
+
* on the drawer control that opened it — now behind the dialog's backdrop.
|
|
213
|
+
* Tab has to enter the dialog's ring rather than walk the controls the
|
|
214
|
+
* backdrop covers.
|
|
215
|
+
*/
|
|
216
|
+
it("pulls focus behind a nested dialog's backdrop into its ring", () => {
|
|
217
|
+
const nestedDialog = createElement(
|
|
218
|
+
"div",
|
|
219
|
+
{
|
|
220
|
+
key: "nested",
|
|
221
|
+
role: "dialog",
|
|
222
|
+
"aria-modal": "true",
|
|
223
|
+
"aria-label": "Reclassify sender",
|
|
224
|
+
},
|
|
225
|
+
button("automated"),
|
|
226
|
+
button("Cancel"),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
open(createElement(Fragment, null, button("trigger"), nestedDialog));
|
|
230
|
+
named("trigger").focus();
|
|
231
|
+
|
|
232
|
+
press("Tab");
|
|
233
|
+
|
|
234
|
+
assert.equal(document.activeElement, named("automated"));
|
|
235
|
+
|
|
236
|
+
named("trigger").focus();
|
|
237
|
+
|
|
238
|
+
press("Tab", true);
|
|
239
|
+
|
|
240
|
+
assert.equal(document.activeElement, named("Cancel"));
|
|
241
|
+
});
|
|
209
242
|
});
|
|
210
243
|
|
|
211
244
|
describe("Drawer gives focus back on the way out", () => {
|
|
@@ -73,25 +73,29 @@ export const Drawer = ({
|
|
|
73
73
|
onClose();
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
76
|
+
// Trap Tab inside the drawer's ring: walking off either end cycles
|
|
77
|
+
// round, and focus that is in the drawer but outside the ring — a
|
|
78
|
+
// control behind a nested dialog's backdrop — is pulled in, so Tab
|
|
79
|
+
// never lands where a pointer cannot reach (#747). The trap only
|
|
80
|
+
// applies while the drawer already holds focus; an error banner or
|
|
81
|
+
// the fatal-error overlay above it keeps its own Tab (#970).
|
|
81
82
|
if (event.key !== "Tab" || !drawerRef.current) return;
|
|
83
|
+
const active = document.activeElement;
|
|
84
|
+
if (!(active instanceof HTMLElement)) return;
|
|
85
|
+
if (!drawerRef.current.contains(active)) return;
|
|
82
86
|
const ring = tabRing(drawerRef.current);
|
|
83
87
|
if (ring.length === 0) return;
|
|
84
88
|
const first = ring[0];
|
|
85
89
|
const last = ring[ring.length - 1];
|
|
86
|
-
const
|
|
90
|
+
const inside = ring.includes(active);
|
|
87
91
|
if (event.shiftKey) {
|
|
88
|
-
if (active === first) {
|
|
92
|
+
if (!inside || active === first) {
|
|
89
93
|
event.preventDefault();
|
|
90
94
|
last.focus();
|
|
91
95
|
}
|
|
92
96
|
return;
|
|
93
97
|
}
|
|
94
|
-
if (active === last) {
|
|
98
|
+
if (!inside || active === last) {
|
|
95
99
|
event.preventDefault();
|
|
96
100
|
first.focus();
|
|
97
101
|
}
|
|
@@ -88,17 +88,23 @@ const updateKey = systemOperationsGetSystemUpdateQueryKey();
|
|
|
88
88
|
const BUDGET_MS =
|
|
89
89
|
(APPLY_BUDGET_SECONDS + NEVER_CAME_BACK_MARGIN_SECONDS) * 1000;
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Poll `ready` across flushes until it holds. A mocked answer still travels
|
|
93
|
+
* through the fetch seam's own promise chain — the request body, the responder,
|
|
94
|
+
* the response stream — so the render an assert reads is an unknown number of
|
|
95
|
+
* turns out, and a fixed count of them is a race under load. Waiting on the
|
|
96
|
+
* condition itself is what makes these tests deterministic.
|
|
97
|
+
*/
|
|
98
|
+
async function waitFor(
|
|
99
|
+
dom: DomHarness,
|
|
100
|
+
ready: () => boolean,
|
|
101
|
+
what: string,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
93
104
|
await dom.flush();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
state.fetchStatus === "idle" &&
|
|
98
|
-
(state.data !== undefined || state.error != null);
|
|
99
|
-
if (done) {
|
|
100
|
-
// The cache has settled; give React the turns to commit the render it
|
|
101
|
-
// scheduled off that settle before the assert reads the DOM.
|
|
105
|
+
if (ready()) {
|
|
106
|
+
// The state is there; give React the turns to commit the render it
|
|
107
|
+
// scheduled off it before the assert reads the DOM.
|
|
102
108
|
await dom.flush();
|
|
103
109
|
await dom.wait(1);
|
|
104
110
|
await dom.flush();
|
|
@@ -106,6 +112,29 @@ async function settle(dom: DomHarness): Promise<void> {
|
|
|
106
112
|
}
|
|
107
113
|
await dom.wait(1);
|
|
108
114
|
}
|
|
115
|
+
throw new Error(`timed out waiting for ${what}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** How many times the poll has answered, either way — it never goes backwards. */
|
|
119
|
+
function answers(dom: DomHarness): number {
|
|
120
|
+
const state = dom.queryClient.getQueryState(updateKey);
|
|
121
|
+
if (!state) return 0;
|
|
122
|
+
return state.dataUpdateCount + state.errorUpdateCount;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function settle(dom: DomHarness): Promise<void> {
|
|
126
|
+
await waitFor(
|
|
127
|
+
dom,
|
|
128
|
+
() => {
|
|
129
|
+
const state = dom.queryClient.getQueryState(updateKey);
|
|
130
|
+
return (
|
|
131
|
+
state !== undefined &&
|
|
132
|
+
state.fetchStatus === "idle" &&
|
|
133
|
+
(state.data !== undefined || state.error != null)
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
"the update query to answer",
|
|
137
|
+
);
|
|
109
138
|
}
|
|
110
139
|
|
|
111
140
|
/**
|
|
@@ -152,18 +181,55 @@ async function startUpdate(
|
|
|
152
181
|
await act(async () => {
|
|
153
182
|
api().install("0.9.4");
|
|
154
183
|
await dom.flush();
|
|
155
|
-
|
|
184
|
+
});
|
|
185
|
+
await waitFor(
|
|
186
|
+
dom,
|
|
187
|
+
() => {
|
|
188
|
+
const surface = api().surface;
|
|
189
|
+
return surface.status === "ready" && surface.overlay.kind === "applying";
|
|
190
|
+
},
|
|
191
|
+
"the install to be accepted",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Re-poll and wait for the answer, whichever way it lands. */
|
|
196
|
+
async function retryConnection(
|
|
197
|
+
dom: DomHarness,
|
|
198
|
+
api: () => SelfUpdateApi,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const before = answers(dom);
|
|
201
|
+
await act(async () => {
|
|
202
|
+
api().onRetryConnection();
|
|
156
203
|
await dom.flush();
|
|
157
204
|
});
|
|
205
|
+
await waitFor(
|
|
206
|
+
dom,
|
|
207
|
+
() =>
|
|
208
|
+
dom.queryClient.getQueryState(updateKey)?.fetchStatus === "idle" &&
|
|
209
|
+
answers(dom) > before,
|
|
210
|
+
"the re-poll to answer",
|
|
211
|
+
);
|
|
158
212
|
}
|
|
159
213
|
|
|
160
214
|
async function press(dom: DomHarness, api: () => SelfUpdateApi): Promise<void> {
|
|
215
|
+
const before = answers(dom);
|
|
161
216
|
await act(async () => {
|
|
162
217
|
api().onCheck();
|
|
163
218
|
await dom.flush();
|
|
164
|
-
await dom.wait(1);
|
|
165
|
-
await dom.flush();
|
|
166
219
|
});
|
|
220
|
+
// The press fires a raw SDK call: on success it refetches, so the poll
|
|
221
|
+
// answers again; on failure the reason lands on the pane.
|
|
222
|
+
await waitFor(
|
|
223
|
+
dom,
|
|
224
|
+
() => {
|
|
225
|
+
const surface = api().surface;
|
|
226
|
+
return (
|
|
227
|
+
answers(dom) > before ||
|
|
228
|
+
(surface.status === "ready" && surface.section.status === "checkFailed")
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
"the pressed check to be requested",
|
|
232
|
+
);
|
|
167
233
|
}
|
|
168
234
|
|
|
169
235
|
async function renderSurface(
|
|
@@ -298,12 +364,7 @@ describe("SelfUpdateOverlay — the blocking screen", () => {
|
|
|
298
364
|
await startUpdate(dom, api);
|
|
299
365
|
|
|
300
366
|
failing = true;
|
|
301
|
-
await
|
|
302
|
-
api().onRetryConnection();
|
|
303
|
-
await dom.flush();
|
|
304
|
-
await dom.wait(1);
|
|
305
|
-
await dom.flush();
|
|
306
|
-
});
|
|
367
|
+
await retryConnection(dom, api);
|
|
307
368
|
|
|
308
369
|
const surface = api().surface;
|
|
309
370
|
assert.equal(
|
|
@@ -329,12 +390,7 @@ describe("SelfUpdateOverlay — the blocking screen", () => {
|
|
|
329
390
|
const realNow = Date.now;
|
|
330
391
|
Date.now = () => realNow() + BUDGET_MS + 60_000;
|
|
331
392
|
try {
|
|
332
|
-
await
|
|
333
|
-
api().onRetryConnection();
|
|
334
|
-
await dom.flush();
|
|
335
|
-
await dom.wait(1);
|
|
336
|
-
await dom.flush();
|
|
337
|
-
});
|
|
393
|
+
await retryConnection(dom, api);
|
|
338
394
|
assert.doesNotMatch(dom.html(), /Installing Remit 0\.9\.4/);
|
|
339
395
|
assert.match(dom.html(), /has not answered since the restart/);
|
|
340
396
|
assert.match(dom.html(), /remit logs/);
|
|
@@ -360,12 +416,7 @@ describe("SelfUpdateOverlay — the blocking screen", () => {
|
|
|
360
416
|
await startUpdate(dom, api);
|
|
361
417
|
|
|
362
418
|
finished = true;
|
|
363
|
-
await
|
|
364
|
-
api().onRetryConnection();
|
|
365
|
-
await dom.flush();
|
|
366
|
-
await dom.wait(1);
|
|
367
|
-
await dom.flush();
|
|
368
|
-
});
|
|
419
|
+
await retryConnection(dom, api);
|
|
369
420
|
|
|
370
421
|
const surface = api().surface;
|
|
371
422
|
assert.equal(
|
|
@@ -471,10 +522,7 @@ describe("useSystemUpdate — actions", () => {
|
|
|
471
522
|
await settle(dom);
|
|
472
523
|
const afterMount = calls;
|
|
473
524
|
|
|
474
|
-
await
|
|
475
|
-
api().onRetryConnection();
|
|
476
|
-
await dom.flush();
|
|
477
|
-
});
|
|
525
|
+
await retryConnection(dom, api);
|
|
478
526
|
|
|
479
527
|
assert.equal(calls > afterMount, true);
|
|
480
528
|
});
|