@pylonsync/react 0.22.50 → 0.22.58
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/dist/AppUpdateBar.d.ts +21 -0
- package/dist/appUpdate.d.ts +166 -0
- package/dist/index.d.ts +8 -0
- package/dist/offlineWorker.d.ts +4 -0
- package/dist/queryFilter.d.ts +13 -0
- package/dist/useAppUpdate.d.ts +34 -0
- package/dist/useSyncProgress.d.ts +21 -0
- package/package.json +4 -4
- package/src/AppUpdateBar.tsx +54 -0
- package/src/Link.tsx +21 -3
- package/src/appUpdate.test.ts +461 -0
- package/src/appUpdate.ts +602 -0
- package/src/hooks.ts +2 -68
- package/src/index.ts +10 -0
- package/src/offlineWorker.test.ts +23 -0
- package/src/offlineWorker.ts +8 -0
- package/src/query-filter-cases.json +23 -0
- package/src/queryFilter.test.ts +45 -0
- package/src/queryFilter.ts +138 -0
- package/src/useAppUpdate.test.tsx +98 -0
- package/src/useAppUpdate.ts +49 -0
- package/src/useSession.ts +4 -2
- package/src/useSyncProgress.test.tsx +37 -0
- package/src/useSyncProgress.ts +46 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
// The app update state machine: it learns of a new build from
|
|
2
|
+
// `/_pylon/version` and from a waiting service worker, waits for pending
|
|
3
|
+
// writes before it reloads, reloads once, and never reloads in a loop.
|
|
4
|
+
|
|
5
|
+
import { expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
createAppUpdate,
|
|
8
|
+
RELOAD_GUARD_MS,
|
|
9
|
+
VERSION_URL,
|
|
10
|
+
WORKER_WAIT_MS,
|
|
11
|
+
type AppUpdateEnv,
|
|
12
|
+
type UpdateRegistrationLike,
|
|
13
|
+
type WorkerLike,
|
|
14
|
+
} from "./appUpdate";
|
|
15
|
+
|
|
16
|
+
const BUILD = { version: "v1", builtAt: "2026-10-22T10:00:00.000Z" };
|
|
17
|
+
|
|
18
|
+
class FakeWorker implements WorkerLike {
|
|
19
|
+
state: string;
|
|
20
|
+
messages: unknown[] = [];
|
|
21
|
+
private listeners: Array<() => void> = [];
|
|
22
|
+
constructor(
|
|
23
|
+
public version: string | null,
|
|
24
|
+
state = "installed",
|
|
25
|
+
) {
|
|
26
|
+
this.state = state;
|
|
27
|
+
}
|
|
28
|
+
postMessage(message: unknown) {
|
|
29
|
+
this.messages.push(message);
|
|
30
|
+
}
|
|
31
|
+
addEventListener(_type: "statechange", listener: () => void) {
|
|
32
|
+
this.listeners.push(listener);
|
|
33
|
+
}
|
|
34
|
+
setState(state: string) {
|
|
35
|
+
this.state = state;
|
|
36
|
+
for (const l of this.listeners) l();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class FakeRegistration implements UpdateRegistrationLike {
|
|
41
|
+
waiting: FakeWorker | null = null;
|
|
42
|
+
installing: FakeWorker | null = null;
|
|
43
|
+
updates = 0;
|
|
44
|
+
onUpdate: (() => void) | null = null;
|
|
45
|
+
private found: Array<() => void> = [];
|
|
46
|
+
async update() {
|
|
47
|
+
this.updates++;
|
|
48
|
+
this.onUpdate?.();
|
|
49
|
+
}
|
|
50
|
+
addEventListener(_type: "updatefound", listener: () => void) {
|
|
51
|
+
this.found.push(listener);
|
|
52
|
+
}
|
|
53
|
+
removeEventListener(_type: "updatefound", listener: () => void) {
|
|
54
|
+
this.found = this.found.filter((l) => l !== listener);
|
|
55
|
+
}
|
|
56
|
+
/** A new worker starts installing. */
|
|
57
|
+
startInstall(version: string) {
|
|
58
|
+
this.installing = new FakeWorker(version, "installing");
|
|
59
|
+
for (const l of this.found) l();
|
|
60
|
+
return this.installing;
|
|
61
|
+
}
|
|
62
|
+
/** The installing worker finishes and waits. */
|
|
63
|
+
finishInstall() {
|
|
64
|
+
const w = this.installing!;
|
|
65
|
+
this.installing = null;
|
|
66
|
+
this.waiting = w;
|
|
67
|
+
w.setState("installed");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class MemoryStorage {
|
|
72
|
+
map = new Map<string, string>();
|
|
73
|
+
getItem(k: string) {
|
|
74
|
+
return this.map.get(k) ?? null;
|
|
75
|
+
}
|
|
76
|
+
setItem(k: string, v: string) {
|
|
77
|
+
this.map.set(k, v);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function makeEnv(opts: {
|
|
82
|
+
latest?: { version: string; builtAt?: string; install?: string } | "offline";
|
|
83
|
+
registration?: FakeRegistration | null;
|
|
84
|
+
controller?: boolean;
|
|
85
|
+
pending?: number;
|
|
86
|
+
iosStandalone?: boolean;
|
|
87
|
+
session?: MemoryStorage;
|
|
88
|
+
local?: MemoryStorage;
|
|
89
|
+
}) {
|
|
90
|
+
let now = 1_000_000;
|
|
91
|
+
let latest = opts.latest ?? { version: "v1" };
|
|
92
|
+
let pending = opts.pending ?? 0;
|
|
93
|
+
const outboxListeners = new Set<() => void>();
|
|
94
|
+
const docListeners = new Set<() => void>();
|
|
95
|
+
const controllerListeners = new Set<() => void>();
|
|
96
|
+
const timers: Array<{ fn: () => void; at: number; id: number }> = [];
|
|
97
|
+
const intervals: Array<{ fn: () => void; id: number }> = [];
|
|
98
|
+
let nextId = 1;
|
|
99
|
+
const fetches: string[] = [];
|
|
100
|
+
const warnings: string[] = [];
|
|
101
|
+
let reloads = 0;
|
|
102
|
+
const doc = {
|
|
103
|
+
visibilityState: "visible",
|
|
104
|
+
addEventListener: (_t: string, l: () => void) => docListeners.add(l),
|
|
105
|
+
removeEventListener: (_t: string, l: () => void) => docListeners.delete(l),
|
|
106
|
+
};
|
|
107
|
+
const container = {
|
|
108
|
+
controller: opts.controller === false ? null : {},
|
|
109
|
+
getRegistration: async () => opts.registration ?? undefined,
|
|
110
|
+
addEventListener: (_t: string, l: () => void) => controllerListeners.add(l),
|
|
111
|
+
removeEventListener: (_t: string, l: () => void) => controllerListeners.delete(l),
|
|
112
|
+
};
|
|
113
|
+
const env: AppUpdateEnv = {
|
|
114
|
+
serviceWorker: opts.registration === null ? null : container,
|
|
115
|
+
fetch: async (url) => {
|
|
116
|
+
fetches.push(url);
|
|
117
|
+
if (latest === "offline") throw new TypeError("Failed to fetch");
|
|
118
|
+
const body = latest;
|
|
119
|
+
return { ok: true, json: async () => body };
|
|
120
|
+
},
|
|
121
|
+
document: doc,
|
|
122
|
+
setInterval: (fn) => {
|
|
123
|
+
const id = nextId++;
|
|
124
|
+
intervals.push({ fn, id });
|
|
125
|
+
return id;
|
|
126
|
+
},
|
|
127
|
+
clearInterval: (h) => {
|
|
128
|
+
const i = intervals.findIndex((x) => x.id === h);
|
|
129
|
+
if (i >= 0) intervals.splice(i, 1);
|
|
130
|
+
},
|
|
131
|
+
setTimeout: (fn, ms) => {
|
|
132
|
+
const id = nextId++;
|
|
133
|
+
timers.push({ fn, at: now + ms, id });
|
|
134
|
+
return id;
|
|
135
|
+
},
|
|
136
|
+
clearTimeout: (h) => {
|
|
137
|
+
const i = timers.findIndex((x) => x.id === h);
|
|
138
|
+
if (i >= 0) timers.splice(i, 1);
|
|
139
|
+
},
|
|
140
|
+
now: () => now,
|
|
141
|
+
reload: () => {
|
|
142
|
+
reloads++;
|
|
143
|
+
},
|
|
144
|
+
sessionStorage: opts.session ?? new MemoryStorage(),
|
|
145
|
+
localStorage: opts.local ?? new MemoryStorage(),
|
|
146
|
+
iosStandalone: opts.iosStandalone ?? false,
|
|
147
|
+
pendingWrites: () =>
|
|
148
|
+
opts.pending === undefined
|
|
149
|
+
? null
|
|
150
|
+
: {
|
|
151
|
+
count: () => pending,
|
|
152
|
+
subscribe: (l) => {
|
|
153
|
+
outboxListeners.add(l);
|
|
154
|
+
return () => outboxListeners.delete(l);
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
askVersion: async (worker) => (worker as FakeWorker).version,
|
|
158
|
+
warn: (m) => warnings.push(m),
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
env,
|
|
162
|
+
fetches,
|
|
163
|
+
warnings,
|
|
164
|
+
intervals,
|
|
165
|
+
reloads: () => reloads,
|
|
166
|
+
setLatest: (l: typeof latest) => {
|
|
167
|
+
latest = l;
|
|
168
|
+
},
|
|
169
|
+
setPending: (n: number) => {
|
|
170
|
+
pending = n;
|
|
171
|
+
for (const l of [...outboxListeners]) l();
|
|
172
|
+
},
|
|
173
|
+
outboxListeners,
|
|
174
|
+
setVisibility: (v: string) => {
|
|
175
|
+
doc.visibilityState = v;
|
|
176
|
+
for (const l of [...docListeners]) l();
|
|
177
|
+
},
|
|
178
|
+
fireControllerChange: () => {
|
|
179
|
+
for (const l of [...controllerListeners]) l();
|
|
180
|
+
},
|
|
181
|
+
advance: (ms: number) => {
|
|
182
|
+
now += ms;
|
|
183
|
+
for (const t of timers.filter((t) => t.at <= now)) {
|
|
184
|
+
timers.splice(timers.indexOf(t), 1);
|
|
185
|
+
t.fn();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
192
|
+
async function settle() {
|
|
193
|
+
for (let i = 0; i < 10; i++) await tick();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test("the same build on the server: no update", async () => {
|
|
197
|
+
const f = makeEnv({ latest: { version: "v1" }, registration: new FakeRegistration() });
|
|
198
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
199
|
+
const off = u.subscribe(() => {});
|
|
200
|
+
await u.check();
|
|
201
|
+
const s = u.getSnapshot();
|
|
202
|
+
expect(s.version).toBe("v1");
|
|
203
|
+
expect(s.builtAt).toBe(BUILD.builtAt);
|
|
204
|
+
expect(s.latestVersion).toBe("v1");
|
|
205
|
+
expect(s.updateReady).toBe(false);
|
|
206
|
+
expect(f.fetches[0]).toBe(VERSION_URL);
|
|
207
|
+
off();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("a new build on the server is ready, and the browser fetches its worker", async () => {
|
|
211
|
+
const reg = new FakeRegistration();
|
|
212
|
+
const f = makeEnv({
|
|
213
|
+
latest: { version: "v2", builtAt: "2026-10-22T11:00:00.000Z" },
|
|
214
|
+
registration: reg,
|
|
215
|
+
});
|
|
216
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
217
|
+
await u.check();
|
|
218
|
+
expect(u.getSnapshot().updateReady).toBe(true);
|
|
219
|
+
expect(u.getSnapshot().latestVersion).toBe("v2");
|
|
220
|
+
expect(u.getSnapshot().latestBuiltAt).toBe("2026-10-22T11:00:00.000Z");
|
|
221
|
+
expect(reg.updates).toBe(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("a waiting worker with a new version is ready; one with the same version is not", async () => {
|
|
225
|
+
const reg = new FakeRegistration();
|
|
226
|
+
reg.waiting = new FakeWorker("v1");
|
|
227
|
+
const f = makeEnv({ latest: "offline", registration: reg });
|
|
228
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
229
|
+
await u.check();
|
|
230
|
+
expect(u.getSnapshot().updateReady).toBe(false);
|
|
231
|
+
|
|
232
|
+
reg.waiting = new FakeWorker("v2");
|
|
233
|
+
await u.check();
|
|
234
|
+
expect(u.getSnapshot().updateReady).toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("a worker that finishes installing makes the update ready without a new check", async () => {
|
|
238
|
+
const reg = new FakeRegistration();
|
|
239
|
+
const f = makeEnv({ latest: "offline", registration: reg });
|
|
240
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
241
|
+
let notified = 0;
|
|
242
|
+
u.subscribe(() => notified++);
|
|
243
|
+
await settle();
|
|
244
|
+
expect(u.getSnapshot().updateReady).toBe(false);
|
|
245
|
+
reg.startInstall("v2");
|
|
246
|
+
reg.finishInstall();
|
|
247
|
+
await settle();
|
|
248
|
+
expect(u.getSnapshot().updateReady).toBe(true);
|
|
249
|
+
expect(notified).toBeGreaterThan(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("checks when the page becomes visible and on the interval, only while visible", async () => {
|
|
253
|
+
const f = makeEnv({ latest: { version: "v1" }, registration: new FakeRegistration() });
|
|
254
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
255
|
+
const off = u.subscribe(() => {});
|
|
256
|
+
await settle();
|
|
257
|
+
expect(f.fetches).toHaveLength(1);
|
|
258
|
+
|
|
259
|
+
f.setVisibility("hidden");
|
|
260
|
+
f.intervals[0].fn();
|
|
261
|
+
await settle();
|
|
262
|
+
expect(f.fetches).toHaveLength(1);
|
|
263
|
+
|
|
264
|
+
f.setLatest({ version: "v2" });
|
|
265
|
+
f.setVisibility("visible");
|
|
266
|
+
await settle();
|
|
267
|
+
expect(f.fetches).toHaveLength(2);
|
|
268
|
+
expect(u.getSnapshot().updateReady).toBe(true);
|
|
269
|
+
|
|
270
|
+
f.intervals[0].fn();
|
|
271
|
+
await settle();
|
|
272
|
+
expect(f.fetches).toHaveLength(3);
|
|
273
|
+
|
|
274
|
+
// The last subscriber leaving stops the checks.
|
|
275
|
+
off();
|
|
276
|
+
expect(f.intervals).toHaveLength(0);
|
|
277
|
+
f.setVisibility("visible");
|
|
278
|
+
await settle();
|
|
279
|
+
expect(f.fetches).toHaveLength(3);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("apply waits for pending writes, activates the waiting worker, and reloads once", async () => {
|
|
283
|
+
const reg = new FakeRegistration();
|
|
284
|
+
reg.waiting = new FakeWorker("v2");
|
|
285
|
+
const f = makeEnv({ latest: { version: "v2" }, registration: reg, pending: 2 });
|
|
286
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
287
|
+
await u.check();
|
|
288
|
+
|
|
289
|
+
const done = u.apply();
|
|
290
|
+
await settle();
|
|
291
|
+
expect(u.getSnapshot().applying).toBe(true);
|
|
292
|
+
expect(u.getSnapshot().waitingForSync).toBe(true);
|
|
293
|
+
expect(reg.waiting!.messages).toEqual([]);
|
|
294
|
+
|
|
295
|
+
f.setPending(1);
|
|
296
|
+
await settle();
|
|
297
|
+
expect(reg.waiting!.messages).toEqual([]);
|
|
298
|
+
|
|
299
|
+
f.setPending(0);
|
|
300
|
+
await settle();
|
|
301
|
+
expect(u.getSnapshot().waitingForSync).toBe(false);
|
|
302
|
+
expect(reg.waiting!.messages).toEqual([{ type: "pylon:skip-waiting" }]);
|
|
303
|
+
expect(f.reloads()).toBe(0);
|
|
304
|
+
|
|
305
|
+
f.fireControllerChange();
|
|
306
|
+
await done;
|
|
307
|
+
expect(f.reloads()).toBe(1);
|
|
308
|
+
// A second controllerchange (or a second apply) does not reload again.
|
|
309
|
+
f.fireControllerChange();
|
|
310
|
+
await settle();
|
|
311
|
+
expect(f.reloads()).toBe(1);
|
|
312
|
+
expect(u.getSnapshot().applying).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("apply with a new build the worker has not installed yet waits for the install", async () => {
|
|
316
|
+
const reg = new FakeRegistration();
|
|
317
|
+
reg.onUpdate = () => reg.startInstall("v2");
|
|
318
|
+
const f = makeEnv({ latest: { version: "v2" }, registration: reg });
|
|
319
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
320
|
+
await u.check();
|
|
321
|
+
// check() already asked the browser to fetch the new worker.
|
|
322
|
+
expect(reg.installing).not.toBeNull();
|
|
323
|
+
|
|
324
|
+
const done = u.apply();
|
|
325
|
+
await settle();
|
|
326
|
+
expect(f.reloads()).toBe(0);
|
|
327
|
+
reg.finishInstall();
|
|
328
|
+
await settle();
|
|
329
|
+
expect(reg.waiting!.messages).toContainEqual({ type: "pylon:skip-waiting" });
|
|
330
|
+
f.fireControllerChange();
|
|
331
|
+
await done;
|
|
332
|
+
expect(f.reloads()).toBe(1);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("apply reloads anyway when the worker never takes over", async () => {
|
|
336
|
+
const reg = new FakeRegistration();
|
|
337
|
+
reg.waiting = new FakeWorker("v2");
|
|
338
|
+
const f = makeEnv({ latest: { version: "v2" }, registration: reg });
|
|
339
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
340
|
+
const done = u.apply();
|
|
341
|
+
await settle();
|
|
342
|
+
expect(f.reloads()).toBe(0);
|
|
343
|
+
f.advance(WORKER_WAIT_MS);
|
|
344
|
+
await done;
|
|
345
|
+
expect(f.reloads()).toBe(1);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("without service workers, apply reloads after the writes are sent", async () => {
|
|
349
|
+
const f = makeEnv({ latest: { version: "v2" }, registration: null, pending: 1 });
|
|
350
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
351
|
+
await u.check();
|
|
352
|
+
expect(u.getSnapshot().updateReady).toBe(true);
|
|
353
|
+
const done = u.apply();
|
|
354
|
+
await settle();
|
|
355
|
+
expect(f.reloads()).toBe(0);
|
|
356
|
+
f.setPending(0);
|
|
357
|
+
await done;
|
|
358
|
+
expect(f.reloads()).toBe(1);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("a reload that lands on the same build does not reload again", async () => {
|
|
362
|
+
const session = new MemoryStorage();
|
|
363
|
+
// First page: apply reloads.
|
|
364
|
+
const first = makeEnv({ latest: { version: "v2" }, registration: null, session });
|
|
365
|
+
const u1 = createAppUpdate({ build: BUILD, production: true, env: first.env });
|
|
366
|
+
await u1.apply();
|
|
367
|
+
expect(first.reloads()).toBe(1);
|
|
368
|
+
|
|
369
|
+
// The reloaded page still runs v1 (a stale CDN, say), and the app calls
|
|
370
|
+
// apply() on its own whenever updateReady is true.
|
|
371
|
+
const second = makeEnv({ latest: { version: "v2" }, registration: null, session });
|
|
372
|
+
const u2 = createAppUpdate({ build: BUILD, production: true, env: second.env });
|
|
373
|
+
await u2.check();
|
|
374
|
+
expect(u2.getSnapshot().updateReady).toBe(true);
|
|
375
|
+
await u2.apply();
|
|
376
|
+
expect(second.reloads()).toBe(0);
|
|
377
|
+
expect(second.warnings[0]).toContain("Not reloading again");
|
|
378
|
+
|
|
379
|
+
// After the guard window, apply works again.
|
|
380
|
+
second.advance(RELOAD_GUARD_MS);
|
|
381
|
+
const u3 = createAppUpdate({ build: BUILD, production: true, env: second.env });
|
|
382
|
+
await u3.apply();
|
|
383
|
+
expect(second.reloads()).toBe(1);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("a reload that reached the new build may apply the next one at once", async () => {
|
|
387
|
+
const session = new MemoryStorage();
|
|
388
|
+
const first = makeEnv({ latest: { version: "v2" }, registration: null, session });
|
|
389
|
+
await createAppUpdate({ build: BUILD, production: true, env: first.env }).apply();
|
|
390
|
+
const second = makeEnv({ latest: { version: "v3" }, registration: null, session });
|
|
391
|
+
const u = createAppUpdate({
|
|
392
|
+
build: { version: "v2", builtAt: null },
|
|
393
|
+
production: true,
|
|
394
|
+
env: second.env,
|
|
395
|
+
});
|
|
396
|
+
await u.apply();
|
|
397
|
+
expect(second.reloads()).toBe(1);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("dev, SSR and apps without offline are inert", async () => {
|
|
401
|
+
const f = makeEnv({ latest: { version: "v2" }, registration: new FakeRegistration() });
|
|
402
|
+
for (const u of [
|
|
403
|
+
createAppUpdate({ build: BUILD, production: false, env: f.env }),
|
|
404
|
+
createAppUpdate({ build: null, production: true, env: f.env }),
|
|
405
|
+
createAppUpdate({ build: BUILD, production: true, env: null }),
|
|
406
|
+
]) {
|
|
407
|
+
const off = u.subscribe(() => {});
|
|
408
|
+
await u.check();
|
|
409
|
+
await u.apply();
|
|
410
|
+
expect(u.getSnapshot().updateReady).toBe(false);
|
|
411
|
+
off();
|
|
412
|
+
}
|
|
413
|
+
expect(f.fetches).toHaveLength(0);
|
|
414
|
+
expect(f.reloads()).toBe(0);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("an iOS home-screen app recommends adding it again when the install identity changed", async () => {
|
|
418
|
+
const local = new MemoryStorage();
|
|
419
|
+
const f = makeEnv({
|
|
420
|
+
latest: { version: "v1", install: "i1" },
|
|
421
|
+
registration: new FakeRegistration(),
|
|
422
|
+
iosStandalone: true,
|
|
423
|
+
local,
|
|
424
|
+
});
|
|
425
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
426
|
+
await u.check();
|
|
427
|
+
// The first check records what the app was added with.
|
|
428
|
+
expect(u.getSnapshot().reinstallRecommended).toBe(false);
|
|
429
|
+
|
|
430
|
+
f.setLatest({ version: "v2", install: "i1" });
|
|
431
|
+
await u.check();
|
|
432
|
+
expect(u.getSnapshot().reinstallRecommended).toBe(false);
|
|
433
|
+
|
|
434
|
+
f.setLatest({ version: "v3", install: "i2" });
|
|
435
|
+
await u.check();
|
|
436
|
+
expect(u.getSnapshot().reinstallRecommended).toBe(true);
|
|
437
|
+
|
|
438
|
+
// In a browser tab the same change means nothing.
|
|
439
|
+
const tab = makeEnv({
|
|
440
|
+
latest: { version: "v3", install: "i2" },
|
|
441
|
+
registration: new FakeRegistration(),
|
|
442
|
+
local,
|
|
443
|
+
});
|
|
444
|
+
const inTab = createAppUpdate({ build: BUILD, production: true, env: tab.env });
|
|
445
|
+
await inTab.check();
|
|
446
|
+
expect(inTab.getSnapshot().reinstallRecommended).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("the first check does not record a newer build's install identity", async () => {
|
|
450
|
+
const local = new MemoryStorage();
|
|
451
|
+
const f = makeEnv({
|
|
452
|
+
latest: { version: "v2", install: "i2" },
|
|
453
|
+
registration: new FakeRegistration(),
|
|
454
|
+
iosStandalone: true,
|
|
455
|
+
local,
|
|
456
|
+
});
|
|
457
|
+
const u = createAppUpdate({ build: BUILD, production: true, env: f.env });
|
|
458
|
+
await u.check();
|
|
459
|
+
expect(local.map.size).toBe(0);
|
|
460
|
+
expect(u.getSnapshot().reinstallRecommended).toBe(false);
|
|
461
|
+
});
|