返回 DeepSeek-Reasonix
updater-scheduling.test.tsx
根目录 / desktop / frontend / src / __tests__ / updater-scheduling.test.tsx
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import React, { act, useEffect } from "react";
4 import { createRoot } from "react-dom/client";
5 import { UpdateBanner } from "../components/UpdateBanner";
6 import { LocaleProvider } from "../lib/i18n";
7 import {
8 __resetUpdaterCheckScheduleForTests,
9 UPDATE_CHECK_INTERVAL_MS,
10 UPDATE_CHECK_STORAGE_KEY,
11 UpdaterProvider,
12 useUpdater,
13 } from "../lib/useUpdater";
14 import { installDesktopHostStub } from "./desktopHostStub";
15
16 const dom = new JSDOM("<!doctype html><div id='root'></div>", { url: "http://localhost", pretendToBeVisual: true });
17 Object.assign(globalThis, {
18 window: dom.window,
19 document: dom.window.document,
20 Node: dom.window.Node,
21 Element: dom.window.Element,
22 HTMLElement: dom.window.HTMLElement,
23 Event: dom.window.Event,
24 MouseEvent: dom.window.MouseEvent,
25 IS_REACT_ACT_ENVIRONMENT: true,
26 });
27
28 let now = 1_800_000_000_000;
29 const originalDateNow = Date.now;
30 Date.now = () => now;
31
32 let checkCalls = 0;
33 const desktopStub = installDesktopHostStub({
34 CheckUpdate: async () => {
35 checkCalls += 1;
36 return {
37 available: false,
38 current: "v1.2.3",
39 latest: "v1.2.3",
40 notes: "",
41 channel: "stable",
42 canSelfUpdate: true,
43 manualOnly: false,
44 installMode: "portable",
45 requiresElevation: false,
46 downloaded: false,
47 downloadUrl: "https://example.invalid/download",
48 assetSize: 0,
49 };
50 },
51 });
52
53 function ManualCheck() {
54 const updater = useUpdater();
55 return <button id="manual-check" onClick={() => void updater.check()}>Check</button>;
56 }
57
58 function AutomaticCheck() {
59 const updater = useUpdater();
60 useEffect(() => { void updater.refresh(); }, [updater.refresh]);
61 return null;
62 }
63
64 async function flush() {
65 await new Promise((resolve) => setTimeout(resolve, 0));
66 }
67
68 const root = createRoot(document.getElementById("root")!);
69 try {
70 __resetUpdaterCheckScheduleForTests();
71 await act(async () => {
72 root.render(<LocaleProvider><UpdaterProvider><UpdateBanner enabled /><ManualCheck /></UpdaterProvider></LocaleProvider>);
73 await flush();
74 });
75 assert.equal(checkCalls, 1, "first enabled mount performs one automatic check");
76
77 now += UPDATE_CHECK_INTERVAL_MS - 1;
78 await act(async () => {
79 window.dispatchEvent(new Event("focus"));
80 document.dispatchEvent(new Event("visibilitychange"));
81 await flush();
82 });
83 assert.equal(checkCalls, 1, "focus and visibility events stay throttled before six hours");
84
85 now += 1;
86 await act(async () => {
87 window.dispatchEvent(new Event("focus"));
88 document.dispatchEvent(new Event("visibilitychange"));
89 await flush();
90 });
91 assert.equal(checkCalls, 2, "simultaneous due events produce one check after six hours");
92
93 await act(async () => root.render(<LocaleProvider><UpdaterProvider><AutomaticCheck /><ManualCheck /></UpdaterProvider></LocaleProvider>));
94 await act(async () => { await flush(); });
95 assert.equal(checkCalls, 2, "stored attempt survives provider remounts");
96
97 await act(async () => {
98 (document.getElementById("manual-check") as HTMLButtonElement).click();
99 await flush();
100 });
101 assert.equal(checkCalls, 3, "manual checks bypass the automatic throttle");
102 assert.equal(window.localStorage.getItem(UPDATE_CHECK_STORAGE_KEY), String(now), "manual check resets the automatic schedule");
103
104 now += UPDATE_CHECK_INTERVAL_MS - 1;
105 await act(async () => {
106 window.dispatchEvent(new Event("focus"));
107 await flush();
108 });
109 assert.equal(checkCalls, 3, "manual check suppresses automatic checks for the next six hours");
110
111 await act(async () => root.render(<LocaleProvider><UpdaterProvider><UpdateBanner enabled={false} /></UpdaterProvider></LocaleProvider>));
112 now += UPDATE_CHECK_INTERVAL_MS;
113 await act(async () => {
114 window.dispatchEvent(new Event("focus"));
115 document.dispatchEvent(new Event("visibilitychange"));
116 await flush();
117 });
118 assert.equal(checkCalls, 3, "disabled automatic checks install no startup, focus, or visibility work");
119
120 __resetUpdaterCheckScheduleForTests();
121 window.localStorage.setItem(UPDATE_CHECK_STORAGE_KEY, String(now + UPDATE_CHECK_INTERVAL_MS));
122 await act(async () => root.render(<LocaleProvider><UpdaterProvider><AutomaticCheck /></UpdaterProvider></LocaleProvider>));
123 await act(async () => { await flush(); });
124 assert.equal(checkCalls, 4, "a future timestamp is discarded and rebuilt by an immediate check");
125
126 const localStorageDescriptor = Object.getOwnPropertyDescriptor(window, "localStorage");
127 Object.defineProperty(window, "localStorage", {
128 configurable: true,
129 get() { throw new Error("storage denied"); },
130 });
131 __resetUpdaterCheckScheduleForTests();
132 await act(async () => root.render(<LocaleProvider><UpdaterProvider><UpdateBanner enabled /></UpdaterProvider></LocaleProvider>));
133 await act(async () => { await flush(); });
134 assert.equal(checkCalls, 5, "storage denial still allows the first process-local check");
135 await act(async () => {
136 window.dispatchEvent(new Event("focus"));
137 document.dispatchEvent(new Event("visibilitychange"));
138 await flush();
139 });
140 assert.equal(checkCalls, 5, "process-local fallback throttles duplicate checks when storage is unavailable");
141 if (localStorageDescriptor) Object.defineProperty(window, "localStorage", localStorageDescriptor);
142
143 console.log("PASS updater six-hour scheduling, persistence, manual bypass, disable, and storage recovery");
144 } finally {
145 await act(async () => root.unmount());
146 desktopStub.uninstall();
147 Date.now = originalDateNow;
148 dom.window.close();
149 }
150
150 lines Plain Text