@remit/ui 0.0.21 → 0.0.23
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/components/self-update-confirm-dialog.tsx +86 -0
- package/src/components/self-update-dot.tsx +32 -0
- package/src/components/self-update-progress-overlay.stories.tsx +80 -0
- package/src/components/self-update-progress-overlay.tsx +203 -0
- package/src/components/self-update-section.tsx +377 -0
- package/src/components/self-update.render.test.ts +294 -0
- package/src/components/self-update.stories.tsx +212 -0
- package/src/components/self-update.ts +127 -0
- package/src/components/swipeable-row.tsx +1 -1
- package/src/index.ts +32 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CheckCircle2,
|
|
3
|
+
CloudOff,
|
|
4
|
+
Download,
|
|
5
|
+
ExternalLink,
|
|
6
|
+
Loader2,
|
|
7
|
+
RotateCcw,
|
|
8
|
+
TriangleAlert,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { type ReactNode, useState } from "react";
|
|
11
|
+
import { cn } from "../lib/cn.js";
|
|
12
|
+
import { Badge } from "./badge.js";
|
|
13
|
+
import { Banner } from "./banner.js";
|
|
14
|
+
import { Button, ButtonLink } from "./button.js";
|
|
15
|
+
import {
|
|
16
|
+
formatRelativeCheck,
|
|
17
|
+
formatReleaseDate,
|
|
18
|
+
type SelfUpdateState,
|
|
19
|
+
} from "./self-update.js";
|
|
20
|
+
|
|
21
|
+
export interface SelfUpdateSectionProps {
|
|
22
|
+
state: SelfUpdateState;
|
|
23
|
+
onCheck: () => void;
|
|
24
|
+
/** Opens consent before anything is replaced. */
|
|
25
|
+
onInstall: () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Clears a finished result from the pane. Required: it is the only exit
|
|
28
|
+
* from `succeeded` and `rolledBack`, and without it the pane sticks on a
|
|
29
|
+
* red failure row for good.
|
|
30
|
+
*/
|
|
31
|
+
onDismissResult: () => void;
|
|
32
|
+
/** Fixed "now" so stories and tests read the same relative times. */
|
|
33
|
+
now?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function SectionRow({
|
|
37
|
+
children,
|
|
38
|
+
tone,
|
|
39
|
+
}: {
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
tone?: "danger";
|
|
42
|
+
}) {
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
className={cn(
|
|
46
|
+
"rounded-sm border bg-surface px-row-inset py-3",
|
|
47
|
+
tone === "danger" ? "border-danger/50" : "border-line",
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Updates, in Settings › Advanced.
|
|
57
|
+
*
|
|
58
|
+
* A mail client is read first and administered second, so an available update
|
|
59
|
+
* is stated here and nowhere else — no modal, no interruption, no repeat
|
|
60
|
+
* asking. Applying one is consequential (the server goes away and comes back),
|
|
61
|
+
* so it is never one click from this pane.
|
|
62
|
+
*/
|
|
63
|
+
export function SelfUpdateSection({
|
|
64
|
+
state,
|
|
65
|
+
onCheck,
|
|
66
|
+
onInstall,
|
|
67
|
+
onDismissResult,
|
|
68
|
+
now = Date.now(),
|
|
69
|
+
}: SelfUpdateSectionProps) {
|
|
70
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
71
|
+
|
|
72
|
+
const checking = state.status === "checking";
|
|
73
|
+
const installable = state.status === "available";
|
|
74
|
+
|
|
75
|
+
const handleCheck = () => {
|
|
76
|
+
if (checking) {
|
|
77
|
+
setNotice("Already checking. The result appears here in a moment.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setNotice(null);
|
|
81
|
+
onCheck();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleInstall = () => {
|
|
85
|
+
if (!installable && state.status !== "rolledBack") {
|
|
86
|
+
setNotice(
|
|
87
|
+
"There is no update to install. Check for updates first — if one is found it appears here.",
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setNotice(null);
|
|
92
|
+
onInstall();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const body = ((): ReactNode => {
|
|
96
|
+
switch (state.status) {
|
|
97
|
+
case "upToDate":
|
|
98
|
+
return (
|
|
99
|
+
<SectionRow>
|
|
100
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
101
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
102
|
+
<CheckCircle2
|
|
103
|
+
className="size-4 shrink-0 text-positive"
|
|
104
|
+
aria-hidden
|
|
105
|
+
/>
|
|
106
|
+
<p className="text-sm text-fg">
|
|
107
|
+
Remit {state.version} is the latest version.
|
|
108
|
+
<span className="text-fg-subtle">
|
|
109
|
+
{" "}
|
|
110
|
+
Checked {formatRelativeCheck(state.checkedAt, now)}.
|
|
111
|
+
</span>
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
<Button
|
|
115
|
+
variant="secondary"
|
|
116
|
+
size="sm"
|
|
117
|
+
className="shrink-0"
|
|
118
|
+
onClick={handleCheck}
|
|
119
|
+
>
|
|
120
|
+
Check again
|
|
121
|
+
</Button>
|
|
122
|
+
</div>
|
|
123
|
+
</SectionRow>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
case "checking":
|
|
127
|
+
return (
|
|
128
|
+
<SectionRow>
|
|
129
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
130
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
131
|
+
<Loader2
|
|
132
|
+
className="size-4 shrink-0 animate-spin text-fg-subtle"
|
|
133
|
+
aria-hidden
|
|
134
|
+
/>
|
|
135
|
+
<p className="text-sm text-fg-muted">
|
|
136
|
+
Looking for a newer version. You are on {state.version}.
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
<Button
|
|
140
|
+
variant="secondary"
|
|
141
|
+
size="sm"
|
|
142
|
+
className="shrink-0"
|
|
143
|
+
onClick={handleCheck}
|
|
144
|
+
>
|
|
145
|
+
Check again
|
|
146
|
+
</Button>
|
|
147
|
+
</div>
|
|
148
|
+
</SectionRow>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
case "checkFailed":
|
|
152
|
+
return (
|
|
153
|
+
<SectionRow>
|
|
154
|
+
<div className="space-y-2">
|
|
155
|
+
<div className="flex items-start gap-2">
|
|
156
|
+
<CloudOff
|
|
157
|
+
className="mt-0.5 size-4 shrink-0 text-fg-subtle"
|
|
158
|
+
aria-hidden
|
|
159
|
+
/>
|
|
160
|
+
<div className="min-w-0 space-y-1">
|
|
161
|
+
<p className="text-sm text-fg">
|
|
162
|
+
Could not reach the update source.
|
|
163
|
+
</p>
|
|
164
|
+
<p className="text-xs text-fg-muted">{state.reason}</p>
|
|
165
|
+
<p className="text-xs text-fg-subtle">
|
|
166
|
+
You are still on {state.version} and it keeps working.
|
|
167
|
+
{state.lastCheckedAt !== undefined && (
|
|
168
|
+
<>
|
|
169
|
+
{" "}
|
|
170
|
+
Last successful check{" "}
|
|
171
|
+
{formatRelativeCheck(state.lastCheckedAt, now)}.
|
|
172
|
+
</>
|
|
173
|
+
)}
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div className="flex justify-end">
|
|
178
|
+
<Button variant="secondary" size="sm" onClick={handleCheck}>
|
|
179
|
+
Try again
|
|
180
|
+
</Button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</SectionRow>
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
case "available":
|
|
187
|
+
return (
|
|
188
|
+
<SectionRow>
|
|
189
|
+
<div className="space-y-3">
|
|
190
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
191
|
+
<span className="text-sm font-semibold text-fg">
|
|
192
|
+
Remit {state.release.version}
|
|
193
|
+
</span>
|
|
194
|
+
<Badge tone="accent">update available</Badge>
|
|
195
|
+
<span className="text-2xs text-fg-subtle">
|
|
196
|
+
released {formatReleaseDate(state.release.releasedAt)} · you
|
|
197
|
+
are on {state.version}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
<p className="text-sm text-fg-muted">{state.release.summary}</p>
|
|
201
|
+
<p className="text-xs text-fg-subtle">
|
|
202
|
+
Installing restarts Remit, so mail stops loading for about a
|
|
203
|
+
minute. If the new version does not come back up, the one you
|
|
204
|
+
are running now is restored on its own.
|
|
205
|
+
</p>
|
|
206
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
207
|
+
<Button
|
|
208
|
+
size="sm"
|
|
209
|
+
icon={<Download className="size-3.5" />}
|
|
210
|
+
onClick={handleInstall}
|
|
211
|
+
>
|
|
212
|
+
Install {state.release.version}
|
|
213
|
+
</Button>
|
|
214
|
+
<ButtonLink
|
|
215
|
+
variant="secondary"
|
|
216
|
+
size="sm"
|
|
217
|
+
external
|
|
218
|
+
href={state.release.releaseNotesUrl}
|
|
219
|
+
icon={<ExternalLink className="size-3.5" />}
|
|
220
|
+
>
|
|
221
|
+
Release notes
|
|
222
|
+
</ButtonLink>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</SectionRow>
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
case "applying":
|
|
229
|
+
return (
|
|
230
|
+
<SectionRow>
|
|
231
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
232
|
+
<Loader2
|
|
233
|
+
className="size-4 shrink-0 animate-spin text-accent-2"
|
|
234
|
+
aria-hidden
|
|
235
|
+
/>
|
|
236
|
+
<p className="text-sm text-fg-muted">
|
|
237
|
+
Installing Remit {state.target}. The restart screen has the
|
|
238
|
+
details.
|
|
239
|
+
</p>
|
|
240
|
+
</div>
|
|
241
|
+
</SectionRow>
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
case "succeeded":
|
|
245
|
+
return (
|
|
246
|
+
<Banner tone="success" onDismiss={onDismissResult}>
|
|
247
|
+
<div className="space-y-1">
|
|
248
|
+
<p className="font-semibold">Updated to Remit {state.version}.</p>
|
|
249
|
+
<p>
|
|
250
|
+
You were on {state.previousVersion}.{" "}
|
|
251
|
+
<a
|
|
252
|
+
href={state.releaseNotesUrl}
|
|
253
|
+
target="_blank"
|
|
254
|
+
rel="noopener noreferrer"
|
|
255
|
+
className="underline"
|
|
256
|
+
>
|
|
257
|
+
See what changed
|
|
258
|
+
</a>
|
|
259
|
+
.
|
|
260
|
+
</p>
|
|
261
|
+
</div>
|
|
262
|
+
</Banner>
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
case "rolledBack":
|
|
266
|
+
return (
|
|
267
|
+
<SectionRow tone="danger">
|
|
268
|
+
<div className="space-y-3">
|
|
269
|
+
<div className="flex items-start gap-2">
|
|
270
|
+
<TriangleAlert
|
|
271
|
+
className="mt-0.5 size-4 shrink-0 text-danger"
|
|
272
|
+
aria-hidden
|
|
273
|
+
/>
|
|
274
|
+
<div className="min-w-0 space-y-1">
|
|
275
|
+
<p className="text-sm font-semibold text-fg">
|
|
276
|
+
Remit {state.attemptedVersion} did not start. Remit reports
|
|
277
|
+
that it put {state.version} back.
|
|
278
|
+
</p>
|
|
279
|
+
<p className="text-sm text-fg-muted">
|
|
280
|
+
You are running {state.version} again. A failed update can
|
|
281
|
+
still have changed things on the way — the log below is the
|
|
282
|
+
only account of what it got as far as doing.
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
<div className="space-y-1">
|
|
287
|
+
<p className="text-xs text-fg-subtle">
|
|
288
|
+
What Remit reported as the failure
|
|
289
|
+
</p>
|
|
290
|
+
<code className="block rounded-xs bg-danger-soft px-2 py-1 text-2xs text-danger">
|
|
291
|
+
{state.reason}
|
|
292
|
+
</code>
|
|
293
|
+
</div>
|
|
294
|
+
<div className="space-y-1">
|
|
295
|
+
<p className="text-xs text-fg-subtle">
|
|
296
|
+
Read the full log before trying again:
|
|
297
|
+
</p>
|
|
298
|
+
<code className="block rounded-xs bg-surface-sunken px-2 py-1 text-2xs text-fg-muted">
|
|
299
|
+
{state.logsCommand}
|
|
300
|
+
</code>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
303
|
+
<Button
|
|
304
|
+
variant="secondary"
|
|
305
|
+
size="sm"
|
|
306
|
+
icon={<RotateCcw className="size-3.5" />}
|
|
307
|
+
onClick={handleInstall}
|
|
308
|
+
>
|
|
309
|
+
Try {state.attemptedVersion} again
|
|
310
|
+
</Button>
|
|
311
|
+
<Button variant="ghost" size="sm" onClick={onDismissResult}>
|
|
312
|
+
Stay on {state.version}
|
|
313
|
+
</Button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</SectionRow>
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
case "unreachable":
|
|
320
|
+
return (
|
|
321
|
+
<SectionRow tone="danger">
|
|
322
|
+
<div className="space-y-2">
|
|
323
|
+
<div className="flex items-start gap-2">
|
|
324
|
+
<TriangleAlert
|
|
325
|
+
className="mt-0.5 size-4 shrink-0 text-danger"
|
|
326
|
+
aria-hidden
|
|
327
|
+
/>
|
|
328
|
+
<div className="min-w-0 space-y-1">
|
|
329
|
+
<p className="text-sm font-semibold text-fg">
|
|
330
|
+
Installing {state.attemptedVersion} left the server
|
|
331
|
+
unreachable.
|
|
332
|
+
</p>
|
|
333
|
+
<p className="text-sm text-fg-muted">
|
|
334
|
+
Remit has answered again since, but nothing here can say
|
|
335
|
+
what happened during the silence.
|
|
336
|
+
</p>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
<code className="block rounded-xs bg-surface-sunken px-2 py-1 text-2xs text-fg-muted">
|
|
340
|
+
{state.logsCommand}
|
|
341
|
+
</code>
|
|
342
|
+
<div className="flex justify-end">
|
|
343
|
+
<Button variant="ghost" size="sm" onClick={onDismissResult}>
|
|
344
|
+
Dismiss
|
|
345
|
+
</Button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</SectionRow>
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
default: {
|
|
352
|
+
const exhaustive: never = state;
|
|
353
|
+
return exhaustive;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
})();
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<section className="space-y-3">
|
|
360
|
+
<header className="space-y-1">
|
|
361
|
+
<h2 className="text-sm font-semibold text-fg">Updates</h2>
|
|
362
|
+
<p className="text-xs text-fg-muted">
|
|
363
|
+
This Remit runs on your own server, so it updates when you say so.
|
|
364
|
+
Your mail lives at your provider and is never touched by an update.
|
|
365
|
+
</p>
|
|
366
|
+
</header>
|
|
367
|
+
|
|
368
|
+
{body}
|
|
369
|
+
|
|
370
|
+
{notice && (
|
|
371
|
+
<p role="status" className="text-xs text-fg-muted">
|
|
372
|
+
{notice}
|
|
373
|
+
</p>
|
|
374
|
+
)}
|
|
375
|
+
</section>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
import {
|
|
6
|
+
demoLogsCommand,
|
|
7
|
+
demoRelease,
|
|
8
|
+
demoRunId,
|
|
9
|
+
type SelfUpdateState,
|
|
10
|
+
updateWaitNote,
|
|
11
|
+
} from "./self-update.js";
|
|
12
|
+
import { SelfUpdateConfirmDialog } from "./self-update-confirm-dialog.js";
|
|
13
|
+
import { UpdateAvailableDot } from "./self-update-dot.js";
|
|
14
|
+
import {
|
|
15
|
+
SelfUpdateProgressOverlay,
|
|
16
|
+
SelfUpdateUnreachableScreen,
|
|
17
|
+
} from "./self-update-progress-overlay.js";
|
|
18
|
+
import { SelfUpdateSection } from "./self-update-section.js";
|
|
19
|
+
|
|
20
|
+
const noop = () => {};
|
|
21
|
+
const NOW = Date.parse("2026-07-20T12:00:00.000Z");
|
|
22
|
+
const CURRENT = "0.9.3";
|
|
23
|
+
|
|
24
|
+
/** SSR splits interpolations with comment markers; sentences read across them. */
|
|
25
|
+
const render = (element: Parameters<typeof renderToString>[0]) =>
|
|
26
|
+
renderToString(element).replaceAll("<!-- -->", "");
|
|
27
|
+
|
|
28
|
+
const section = (state: SelfUpdateState) =>
|
|
29
|
+
render(
|
|
30
|
+
createElement(SelfUpdateSection, {
|
|
31
|
+
state,
|
|
32
|
+
now: NOW,
|
|
33
|
+
onCheck: noop,
|
|
34
|
+
onInstall: noop,
|
|
35
|
+
onDismissResult: noop,
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
describe("SelfUpdateSection", () => {
|
|
40
|
+
it("states being up to date without raising an alert", () => {
|
|
41
|
+
const html = section({
|
|
42
|
+
status: "upToDate",
|
|
43
|
+
version: CURRENT,
|
|
44
|
+
checkedAt: NOW - 60_000,
|
|
45
|
+
});
|
|
46
|
+
assert.match(html, /0\.9\.3 is the latest version/);
|
|
47
|
+
assert.doesNotMatch(html, /role="alert"/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("offers an available update without shouting about it", () => {
|
|
51
|
+
const html = section({
|
|
52
|
+
status: "available",
|
|
53
|
+
version: CURRENT,
|
|
54
|
+
release: demoRelease,
|
|
55
|
+
});
|
|
56
|
+
assert.match(html, /Install 0\.9\.4/);
|
|
57
|
+
assert.match(html, /Release notes/);
|
|
58
|
+
assert.doesNotMatch(html, /role="alert"/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("names the automatic rollback before the user commits", () => {
|
|
62
|
+
const html = section({
|
|
63
|
+
status: "available",
|
|
64
|
+
version: CURRENT,
|
|
65
|
+
release: demoRelease,
|
|
66
|
+
});
|
|
67
|
+
assert.match(html, /restored on its own/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("keeps a failed check separate from a failed update", () => {
|
|
71
|
+
const html = section({
|
|
72
|
+
status: "checkFailed",
|
|
73
|
+
version: CURRENT,
|
|
74
|
+
reason: "No route to github.com",
|
|
75
|
+
lastCheckedAt: NOW - 3_600_000,
|
|
76
|
+
});
|
|
77
|
+
assert.match(html, /Could not reach the update source/);
|
|
78
|
+
assert.match(html, /still on 0\.9\.3 and it keeps working/);
|
|
79
|
+
assert.match(html, /Try again/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("says which version is running after a rollback, and where the log is", () => {
|
|
83
|
+
const html = section({
|
|
84
|
+
status: "rolledBack",
|
|
85
|
+
runId: demoRunId,
|
|
86
|
+
version: CURRENT,
|
|
87
|
+
attemptedVersion: "0.9.4",
|
|
88
|
+
reason: "migration 0042 failed",
|
|
89
|
+
logsCommand: demoLogsCommand,
|
|
90
|
+
});
|
|
91
|
+
assert.match(html, /running 0\.9\.3 again/);
|
|
92
|
+
assert.match(html, /migration 0042 failed/);
|
|
93
|
+
assert.match(html, /remit logs --since 10m/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("attributes the rollback to the server rather than asserting it", () => {
|
|
97
|
+
const html = section({
|
|
98
|
+
status: "rolledBack",
|
|
99
|
+
runId: demoRunId,
|
|
100
|
+
version: CURRENT,
|
|
101
|
+
attemptedVersion: "0.9.4",
|
|
102
|
+
reason: "migration 0042 failed",
|
|
103
|
+
logsCommand: demoLogsCommand,
|
|
104
|
+
});
|
|
105
|
+
assert.match(html, /Remit reports that it put 0\.9\.3 back/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("never claims data survived a failed update", () => {
|
|
109
|
+
const html = section({
|
|
110
|
+
status: "rolledBack",
|
|
111
|
+
runId: demoRunId,
|
|
112
|
+
version: CURRENT,
|
|
113
|
+
attemptedVersion: "0.9.4",
|
|
114
|
+
reason: "migration 0042_add_thread_index failed",
|
|
115
|
+
logsCommand: demoLogsCommand,
|
|
116
|
+
});
|
|
117
|
+
// The server reported a rollback and a reason. It reported nothing about
|
|
118
|
+
// data, and a half-applied migration is exactly where that matters.
|
|
119
|
+
assert.doesNotMatch(html, /Nothing was lost/);
|
|
120
|
+
assert.doesNotMatch(html, /everything works/);
|
|
121
|
+
assert.match(html, /can still have changed things/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("renders every status rather than going blank", () => {
|
|
125
|
+
assert.match(
|
|
126
|
+
section({
|
|
127
|
+
status: "applying",
|
|
128
|
+
runId: demoRunId,
|
|
129
|
+
version: CURRENT,
|
|
130
|
+
target: "0.9.4",
|
|
131
|
+
phase: "restarting",
|
|
132
|
+
elapsedSeconds: 20,
|
|
133
|
+
}),
|
|
134
|
+
/Installing Remit 0\.9\.4/,
|
|
135
|
+
);
|
|
136
|
+
assert.match(
|
|
137
|
+
section({
|
|
138
|
+
status: "unreachable",
|
|
139
|
+
runId: demoRunId,
|
|
140
|
+
previousVersion: CURRENT,
|
|
141
|
+
attemptedVersion: "0.9.4",
|
|
142
|
+
elapsedSeconds: 420,
|
|
143
|
+
logsCommand: demoLogsCommand,
|
|
144
|
+
}),
|
|
145
|
+
/left the server unreachable/,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("offers a way forward from every failure", () => {
|
|
150
|
+
const rolledBack = section({
|
|
151
|
+
status: "rolledBack",
|
|
152
|
+
runId: demoRunId,
|
|
153
|
+
version: CURRENT,
|
|
154
|
+
attemptedVersion: "0.9.4",
|
|
155
|
+
reason: "boom",
|
|
156
|
+
logsCommand: demoLogsCommand,
|
|
157
|
+
});
|
|
158
|
+
assert.match(rolledBack, /Try 0\.9\.4 again/);
|
|
159
|
+
assert.match(rolledBack, /Stay on 0\.9\.3/);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("keeps the check control pressable while a check is running", () => {
|
|
163
|
+
const html = section({ status: "checking", version: CURRENT });
|
|
164
|
+
assert.match(html, /Check again/);
|
|
165
|
+
assert.doesNotMatch(html, /disabled=/);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("SelfUpdateConfirmDialog", () => {
|
|
170
|
+
const html = render(
|
|
171
|
+
createElement(SelfUpdateConfirmDialog, {
|
|
172
|
+
open: true,
|
|
173
|
+
currentVersion: CURRENT,
|
|
174
|
+
release: demoRelease,
|
|
175
|
+
onClose: noop,
|
|
176
|
+
onConfirm: noop,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
it("reflects the consequence in the order the user will feel it", () => {
|
|
181
|
+
assert.match(html, /lose its connection/);
|
|
182
|
+
assert.match(html, /stays at your provider/);
|
|
183
|
+
assert.match(html, /restored automatically/);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("always offers the way back", () => {
|
|
187
|
+
assert.match(html, /Not now/);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("renders nothing when closed", () => {
|
|
191
|
+
assert.equal(
|
|
192
|
+
renderToString(
|
|
193
|
+
createElement(SelfUpdateConfirmDialog, {
|
|
194
|
+
open: false,
|
|
195
|
+
currentVersion: CURRENT,
|
|
196
|
+
release: demoRelease,
|
|
197
|
+
onClose: noop,
|
|
198
|
+
onConfirm: noop,
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
"",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("SelfUpdateProgressOverlay", () => {
|
|
207
|
+
it("tells the user the disconnection is expected", () => {
|
|
208
|
+
const html = render(
|
|
209
|
+
createElement(SelfUpdateProgressOverlay, {
|
|
210
|
+
target: "0.9.4",
|
|
211
|
+
phase: "restarting",
|
|
212
|
+
elapsedSeconds: 20,
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
assert.match(html, /no server to talk to/);
|
|
216
|
+
assert.match(html, /Waiting for Remit to answer again/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("stops promising a minute once a minute has passed", () => {
|
|
220
|
+
assert.match(updateWaitNote(30), /about a minute/);
|
|
221
|
+
assert.doesNotMatch(updateWaitNote(200), /about a minute/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("never describes the server from a lost connection", () => {
|
|
225
|
+
// The unreachable screen refuses to claim the rollback ran; the timeout
|
|
226
|
+
// copy sits on the same dead connection and may not claim it either.
|
|
227
|
+
for (const seconds of [30, 120, 240, 600]) {
|
|
228
|
+
assert.doesNotMatch(updateWaitNote(seconds), /old version|way back|kept/);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("blocks the window rather than its nearest positioned ancestor", () => {
|
|
233
|
+
const html = render(
|
|
234
|
+
createElement(SelfUpdateProgressOverlay, {
|
|
235
|
+
target: "0.9.4",
|
|
236
|
+
phase: "restarting",
|
|
237
|
+
elapsedSeconds: 20,
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
assert.match(html, /fixed inset-0/);
|
|
241
|
+
assert.doesNotMatch(html, /absolute inset-0/);
|
|
242
|
+
assert.match(html, /aria-modal="true"/);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("scopes the live region to the line that changes", () => {
|
|
246
|
+
const html = render(
|
|
247
|
+
createElement(SelfUpdateProgressOverlay, {
|
|
248
|
+
target: "0.9.4",
|
|
249
|
+
phase: "restarting",
|
|
250
|
+
elapsedSeconds: 20,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
assert.equal(html.match(/aria-live/g)?.length, 1);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("SelfUpdateUnreachableScreen", () => {
|
|
258
|
+
const html = render(
|
|
259
|
+
createElement(SelfUpdateUnreachableScreen, {
|
|
260
|
+
attemptedVersion: "0.9.4",
|
|
261
|
+
previousVersion: CURRENT,
|
|
262
|
+
elapsedSeconds: 420,
|
|
263
|
+
logsCommand: demoLogsCommand,
|
|
264
|
+
onRetryConnection: noop,
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
it("raises an alert and never claims the rollback succeeded", () => {
|
|
269
|
+
assert.match(html, /role="alertdialog"/);
|
|
270
|
+
assert.match(html, /aria-modal="true"/);
|
|
271
|
+
assert.match(html, /cannot confirm that from here/);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("points at the machine that can answer", () => {
|
|
275
|
+
assert.match(html, /remit logs --since 10m/);
|
|
276
|
+
assert.match(html, /Try connecting again/);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("UpdateAvailableDot", () => {
|
|
281
|
+
it("carries a label for assistive tech when shown", () => {
|
|
282
|
+
const html = renderToString(
|
|
283
|
+
createElement(UpdateAvailableDot, { show: true }, null),
|
|
284
|
+
);
|
|
285
|
+
assert.match(html, /Update available/);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("adds nothing when there is no update", () => {
|
|
289
|
+
const html = renderToString(
|
|
290
|
+
createElement(UpdateAvailableDot, { show: false }, null),
|
|
291
|
+
);
|
|
292
|
+
assert.doesNotMatch(html, /Update available/);
|
|
293
|
+
});
|
|
294
|
+
});
|