Skip to content

Commit 0fa2c00

Browse files
authored
Merge pull request #4159 from Green-hats/niu/dashboard-storage-unavailable
fix(dashboard): survive unavailable browser storage
2 parents 3152998 + e5eda31 commit 0fa2c00

2 files changed

Lines changed: 49 additions & 3 deletions

File tree

‎apps/presentation/dashboard/src/views/dashboard-page.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ import {
151151
addSshTunnelStatusSource,
152152
bindConfiguredSshHostAliases,
153153
defaultLocalStatusSourceUrl,
154+
emptyStatusSourceCatalog,
154155
loadStatusSourceCatalog,
155156
localStatusSource,
156157
activeStatusSourceForUrl,
@@ -2983,9 +2984,14 @@ export function DashboardPage() {
29832984
preferredGoalRef.current = search.goalId;
29842985
const [payload, setPayload] = useState<StatusPayload>(exampleStatusPayload);
29852986
const [source, setSource] = useState<DataSource>({ kind: "example", label: "bundled example" });
2986-
const [statusSourceCatalog, setStatusSourceCatalog] = useState(() =>
2987-
loadStatusSourceCatalog(window.localStorage, window.location.href)
2988-
);
2987+
const [statusSourceCatalog, setStatusSourceCatalog] = useState(() => {
2988+
try {
2989+
return loadStatusSourceCatalog(window.localStorage, window.location.href);
2990+
} catch {
2991+
// Browsers may reject access to the storage object itself.
2992+
return emptyStatusSourceCatalog();
2993+
}
2994+
});
29892995
const statusSourceCatalogRef = useRef(statusSourceCatalog);
29902996
statusSourceCatalogRef.current = statusSourceCatalog;
29912997
const [statusUrl, setStatusUrl] = useState(search.statusUrl);

‎examples/status-source-switch-browser-smoke.mjs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,46 @@ async function main() {
7272
const appUrl = `http://127.0.0.1:${port}/${packaged ? "chat/" : ""}`;
7373
await waitForHttp(appUrl);
7474
browser = await launchBrowser(chromium);
75+
for (const storageFailure of ["getter", "methods"]) {
76+
const isolated = await browser.newPage({ viewport: { width: 1512, height: 982 } });
77+
const errors = [];
78+
isolated.on("pageerror", (error) => errors.push(error.message));
79+
await isolated.addInitScript((failure) => {
80+
const deny = () => { throw new DOMException("Storage unavailable", "SecurityError"); };
81+
if (failure === "getter") {
82+
Object.defineProperty(window, "localStorage", { get: deny });
83+
} else {
84+
Storage.prototype.getItem = deny;
85+
Storage.prototype.setItem = deny;
86+
}
87+
}, storageFailure);
88+
await isolated.route(`http://127.0.0.1:${port}/ssh-hosts`, (route) => route.fulfill({
89+
json: { ok: true, schema_version: "ssh_host_catalog_v0", hosts: [] },
90+
}));
91+
await isolated.route(`http://127.0.0.1:${port}/status.json*`, (route) => route.fulfill({
92+
json: statusPayload("local-goal", "Local Goal Only"),
93+
}));
94+
await isolated.route("http://127.0.0.1:8976/status.json*", (route) => route.fulfill({
95+
json: statusPayload("remote-b-goal", "Remote B Goal Only"),
96+
}));
97+
await isolated.goto(appUrl, { waitUntil: "networkidle" });
98+
await isolated.getByText("Local Goal Only", { exact: true }).first().waitFor({ timeout: 10_000 });
99+
const select = isolated.getByRole("combobox", { name: "选择控制面来源" });
100+
if (await selectedSourceLabel(select) !== "本机") throw new Error(`${storageFailure}: missing local source fallback`);
101+
await isolated.getByRole("button", { name: "添加 SSH 隧道来源" }).click();
102+
await isolated.getByRole("tab", { name: "手动 URL" }).click();
103+
await isolated.getByLabel("名称").fill("Session Remote");
104+
await isolated.getByLabel("本地转发 URL").fill("http://127.0.0.1:8976/status.json");
105+
await isolated.getByRole("button", { name: "添加只读来源" }).click();
106+
await isolated.getByText("Remote B Goal Only", { exact: true }).first().waitFor({ timeout: 10_000 });
107+
await isolated.locator(".personal-read-only-source", { hasText: "Session Remote" }).waitFor();
108+
await selectSource(isolated, select, "本机");
109+
await isolated.getByText("Local Goal Only", { exact: true }).first().waitFor({ timeout: 10_000 });
110+
await isolated.reload({ waitUntil: "networkidle" });
111+
await isolated.getByText("Local Goal Only", { exact: true }).first().waitFor({ timeout: 10_000 });
112+
if (errors.length) throw new Error(`${storageFailure}: uncaught page errors: ${errors.join("; ")}`);
113+
await isolated.close();
114+
}
75115
const page = await browser.newPage({ viewport: { width: 1512, height: 982 } });
76116
const state = {
77117
ensureGates: new Map(),

0 commit comments

Comments
 (0)