@remit/web-client 0.0.95 → 0.0.96
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.96",
|
|
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": {
|
|
@@ -26,12 +26,15 @@ import {
|
|
|
26
26
|
|
|
27
27
|
let harness: DomHarness | undefined;
|
|
28
28
|
let http: HttpMock | undefined;
|
|
29
|
+
let restoreFetch: (() => void) | undefined;
|
|
29
30
|
|
|
30
31
|
afterEach(() => {
|
|
31
32
|
harness?.close();
|
|
32
33
|
harness = undefined;
|
|
33
34
|
http?.restore();
|
|
34
35
|
http = undefined;
|
|
36
|
+
restoreFetch?.();
|
|
37
|
+
restoreFetch = undefined;
|
|
35
38
|
});
|
|
36
39
|
|
|
37
40
|
interface MailboxProgress {
|
|
@@ -72,22 +75,83 @@ const accountOf = (path: string): string => path.split("/")[2] ?? "";
|
|
|
72
75
|
const progress = (dom: DomHarness): InitialSyncProgress =>
|
|
73
76
|
JSON.parse(dom.text());
|
|
74
77
|
|
|
75
|
-
const
|
|
78
|
+
const UNKNOWN: InitialSyncProgress = {
|
|
79
|
+
syncing: false,
|
|
80
|
+
resolved: false,
|
|
81
|
+
synced: 0,
|
|
82
|
+
total: 0,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** How long the hook gets to answer a request that is already in hand. */
|
|
86
|
+
const RESOLVE_CEILING_MS = 2000;
|
|
87
|
+
/** How long the hook is watched for an answer, or a request, it must not give. */
|
|
88
|
+
const QUIET_WINDOW_MS = 500;
|
|
89
|
+
const STEP_MS = 10;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Let real time pass until the hook itself says `done`, or the ceiling runs out.
|
|
93
|
+
*
|
|
94
|
+
* Every wait in this file is a question put to the hook rather than a count of
|
|
95
|
+
* turns or a sleep the answer is assumed to fit inside: the poll and the answer
|
|
96
|
+
* deadline run on real timers, so a loaded machine changes how much work lands
|
|
97
|
+
* per turn, and the ceiling is what keeps a hook that never answers a failure
|
|
98
|
+
* instead of a hang.
|
|
99
|
+
*/
|
|
100
|
+
const settleUntil = async (
|
|
101
|
+
dom: DomHarness,
|
|
102
|
+
done: () => boolean,
|
|
103
|
+
ceilingMs: number,
|
|
104
|
+
): Promise<void> => {
|
|
105
|
+
const deadline = Date.now() + ceilingMs;
|
|
106
|
+
for (;;) {
|
|
107
|
+
await dom.flush();
|
|
108
|
+
if (done() || Date.now() >= deadline) return;
|
|
109
|
+
await dom.wait(STEP_MS);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const mount = (
|
|
76
114
|
accountIds: string[],
|
|
77
115
|
answer: (accountId: string) => unknown,
|
|
78
116
|
enabled = true,
|
|
79
|
-
):
|
|
117
|
+
): DomHarness => {
|
|
80
118
|
http = mockFetch((call) => answer(accountOf(call.path)));
|
|
81
119
|
harness = createDomHarness();
|
|
82
120
|
harness.renderApp(createElement(Probe, { accountIds, enabled }));
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
return harness;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const mountResolved = async (
|
|
125
|
+
accountIds: string[],
|
|
126
|
+
answer: (accountId: string) => unknown,
|
|
127
|
+
): Promise<DomHarness> => {
|
|
128
|
+
const dom = mount(accountIds, answer);
|
|
129
|
+
await settleUntil(dom, () => progress(dom).resolved, RESOLVE_CEILING_MS);
|
|
130
|
+
return dom;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** `acc-2` never answers at all: its request is left hanging for good. */
|
|
134
|
+
const mountSilentAccount = (
|
|
135
|
+
answerOfAccountOne: RemitImapAccountSyncStatusResponse,
|
|
136
|
+
): DomHarness => {
|
|
137
|
+
const original = globalThis.fetch;
|
|
138
|
+
restoreFetch = () => {
|
|
139
|
+
globalThis.fetch = original;
|
|
140
|
+
};
|
|
141
|
+
globalThis.fetch = ((input: RequestInfo | URL) => {
|
|
142
|
+
const url = input instanceof Request ? input.url : String(input);
|
|
143
|
+
if (url.includes("acc-2")) return new Promise<Response>(() => {});
|
|
144
|
+
return Promise.resolve(
|
|
145
|
+
new Response(JSON.stringify(answerOfAccountOne), {
|
|
146
|
+
status: 200,
|
|
147
|
+
headers: { "content-type": "application/json" },
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
}) as typeof globalThis.fetch;
|
|
151
|
+
harness = createDomHarness();
|
|
152
|
+
harness.renderApp(
|
|
153
|
+
createElement(Probe, { accountIds: ["acc-1", "acc-2"], enabled: true }),
|
|
154
|
+
);
|
|
91
155
|
return harness;
|
|
92
156
|
};
|
|
93
157
|
|
|
@@ -112,7 +176,7 @@ describe("isSyncingPhase", () => {
|
|
|
112
176
|
|
|
113
177
|
describe("useInitialSyncProgress", () => {
|
|
114
178
|
it("reports syncing with the counts of the accounts still syncing", async () => {
|
|
115
|
-
const dom = await
|
|
179
|
+
const dom = await mountResolved(["acc-1"], (accountId) =>
|
|
116
180
|
status(accountId, "syncing_inbox", [
|
|
117
181
|
{ synced: 40, total: 100 },
|
|
118
182
|
{ synced: 10, total: 60 },
|
|
@@ -127,7 +191,7 @@ describe("useInitialSyncProgress", () => {
|
|
|
127
191
|
});
|
|
128
192
|
|
|
129
193
|
it("leaves out the counts of an account that has finished", async () => {
|
|
130
|
-
const dom = await
|
|
194
|
+
const dom = await mountResolved(["acc-1", "acc-2"], (accountId) =>
|
|
131
195
|
accountId === "acc-1"
|
|
132
196
|
? status(accountId, "syncing_inbox", [{ synced: 40, total: 100 }])
|
|
133
197
|
: status(accountId, "complete", [{ synced: 900, total: 900 }]),
|
|
@@ -141,7 +205,7 @@ describe("useInitialSyncProgress", () => {
|
|
|
141
205
|
});
|
|
142
206
|
|
|
143
207
|
it("resolves to not-syncing once every account is done", async () => {
|
|
144
|
-
const dom = await
|
|
208
|
+
const dom = await mountResolved(["acc-1", "acc-2"], (accountId) =>
|
|
145
209
|
status(accountId, "complete", [{ synced: 900, total: 900 }]),
|
|
146
210
|
);
|
|
147
211
|
assert.deepEqual(progress(dom), {
|
|
@@ -153,7 +217,7 @@ describe("useInitialSyncProgress", () => {
|
|
|
153
217
|
});
|
|
154
218
|
|
|
155
219
|
it("counts an unreachable account as answered rather than holding the list in limbo", async () => {
|
|
156
|
-
const dom = await
|
|
220
|
+
const dom = await mountResolved(["acc-1", "acc-2"], (accountId) =>
|
|
157
221
|
accountId === "acc-1"
|
|
158
222
|
? status(accountId, "syncing_inbox", [{ synced: 5, total: 50 }])
|
|
159
223
|
: httpError(503),
|
|
@@ -167,39 +231,17 @@ describe("useInitialSyncProgress", () => {
|
|
|
167
231
|
it("knows nothing until every account has answered", async () => {
|
|
168
232
|
// One account answers, the other never does — the hook must not report on
|
|
169
233
|
// the half it has.
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
JSON.stringify(
|
|
178
|
-
status("acc-1", "syncing_inbox", [{ synced: 5, total: 50 }]),
|
|
179
|
-
),
|
|
180
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
181
|
-
),
|
|
182
|
-
);
|
|
183
|
-
}) as typeof globalThis.fetch;
|
|
184
|
-
harness = createDomHarness();
|
|
185
|
-
harness.renderApp(
|
|
186
|
-
createElement(Probe, { accountIds: ["acc-1", "acc-2"], enabled: true }),
|
|
187
|
-
);
|
|
188
|
-
await harness.flush();
|
|
189
|
-
await harness.flush();
|
|
190
|
-
assert.deepEqual(progress(harness), {
|
|
191
|
-
syncing: false,
|
|
192
|
-
resolved: false,
|
|
193
|
-
synced: 0,
|
|
194
|
-
total: 0,
|
|
195
|
-
});
|
|
196
|
-
} finally {
|
|
197
|
-
globalThis.fetch = original;
|
|
198
|
-
}
|
|
234
|
+
const dom = mountSilentAccount(
|
|
235
|
+
status("acc-1", "syncing_inbox", [{ synced: 5, total: 50 }]),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
await settleUntil(dom, () => progress(dom).resolved, QUIET_WINDOW_MS);
|
|
239
|
+
|
|
240
|
+
assert.deepEqual(progress(dom), UNKNOWN);
|
|
199
241
|
});
|
|
200
242
|
|
|
201
243
|
it("answers immediately, and never syncing, for no accounts at all", async () => {
|
|
202
|
-
const dom = await
|
|
244
|
+
const dom = await mountResolved([], () => ({}));
|
|
203
245
|
assert.deepEqual(progress(dom), {
|
|
204
246
|
syncing: false,
|
|
205
247
|
resolved: true,
|
|
@@ -211,24 +253,28 @@ describe("useInitialSyncProgress", () => {
|
|
|
211
253
|
|
|
212
254
|
it("stops asking once every account has answered and none is syncing", async () => {
|
|
213
255
|
// The caught-up user's answer cannot change by being asked again.
|
|
214
|
-
const dom = await
|
|
256
|
+
const dom = await mountResolved(["acc-1", "acc-2"], (accountId) =>
|
|
215
257
|
status(accountId, "complete"),
|
|
216
258
|
);
|
|
217
259
|
const asked = http?.calls.length ?? 0;
|
|
218
260
|
assert.equal(asked, 2);
|
|
219
261
|
|
|
220
|
-
await
|
|
262
|
+
await settleUntil(
|
|
263
|
+
dom,
|
|
264
|
+
() => (http?.calls.length ?? 0) > asked,
|
|
265
|
+
POLL_MS + QUIET_WINDOW_MS,
|
|
266
|
+
);
|
|
221
267
|
|
|
222
268
|
assert.equal(http?.calls.length, asked);
|
|
223
269
|
assert.equal(progress(dom).resolved, true);
|
|
224
270
|
});
|
|
225
271
|
|
|
226
272
|
it("keeps asking while an account is still syncing", async () => {
|
|
227
|
-
const dom = await
|
|
273
|
+
const dom = await mountResolved(["acc-1"], (accountId) =>
|
|
228
274
|
status(accountId, "syncing_inbox", [{ synced: 5, total: 50 }]),
|
|
229
275
|
);
|
|
230
276
|
|
|
231
|
-
await dom.
|
|
277
|
+
await settleUntil(dom, () => (http?.calls.length ?? 0) > 1, POLL_MS * 3);
|
|
232
278
|
|
|
233
279
|
assert.ok(
|
|
234
280
|
(http?.calls.length ?? 0) > 1,
|
|
@@ -237,50 +283,31 @@ describe("useInitialSyncProgress", () => {
|
|
|
237
283
|
});
|
|
238
284
|
|
|
239
285
|
it("answers without an account that never responds", async () => {
|
|
240
|
-
const
|
|
241
|
-
try {
|
|
242
|
-
globalThis.fetch = ((input: RequestInfo | URL) => {
|
|
243
|
-
const url = input instanceof Request ? input.url : String(input);
|
|
244
|
-
if (url.includes("acc-2")) return new Promise<Response>(() => {});
|
|
245
|
-
return Promise.resolve(
|
|
246
|
-
new Response(JSON.stringify(status("acc-1", "complete")), {
|
|
247
|
-
status: 200,
|
|
248
|
-
headers: { "content-type": "application/json" },
|
|
249
|
-
}),
|
|
250
|
-
);
|
|
251
|
-
}) as typeof globalThis.fetch;
|
|
252
|
-
harness = createDomHarness();
|
|
253
|
-
harness.renderApp(
|
|
254
|
-
createElement(Probe, { accountIds: ["acc-1", "acc-2"], enabled: true }),
|
|
255
|
-
);
|
|
256
|
-
await harness.flush();
|
|
257
|
-
assert.equal(progress(harness).resolved, false);
|
|
258
|
-
|
|
259
|
-
await harness.wait(ANSWER_DEADLINE_MS + 500);
|
|
260
|
-
|
|
261
|
-
assert.deepEqual(progress(harness), {
|
|
262
|
-
syncing: false,
|
|
263
|
-
resolved: true,
|
|
264
|
-
synced: 0,
|
|
265
|
-
total: 0,
|
|
266
|
-
});
|
|
267
|
-
} finally {
|
|
268
|
-
globalThis.fetch = original;
|
|
269
|
-
}
|
|
270
|
-
});
|
|
286
|
+
const dom = mountSilentAccount(status("acc-1", "complete"));
|
|
271
287
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
288
|
+
await settleUntil(dom, () => progress(dom).resolved, QUIET_WINDOW_MS);
|
|
289
|
+
assert.equal(progress(dom).resolved, false);
|
|
290
|
+
|
|
291
|
+
await settleUntil(
|
|
292
|
+
dom,
|
|
293
|
+
() => progress(dom).resolved,
|
|
294
|
+
ANSWER_DEADLINE_MS * 2,
|
|
277
295
|
);
|
|
296
|
+
|
|
278
297
|
assert.deepEqual(progress(dom), {
|
|
279
298
|
syncing: false,
|
|
280
|
-
resolved:
|
|
299
|
+
resolved: true,
|
|
281
300
|
synced: 0,
|
|
282
301
|
total: 0,
|
|
283
302
|
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("asks nothing and claims nothing while disabled", async () => {
|
|
306
|
+
const dom = mount(["acc-1"], () => status("acc-1", "syncing_inbox"), false);
|
|
307
|
+
|
|
308
|
+
await settleUntil(dom, () => progress(dom).resolved, QUIET_WINDOW_MS);
|
|
309
|
+
|
|
310
|
+
assert.deepEqual(progress(dom), UNKNOWN);
|
|
284
311
|
assert.deepEqual(http?.calls ?? [], []);
|
|
285
312
|
});
|
|
286
313
|
});
|