Files
learnharness/tests/render-update-banner.test.tsx
T
amosblomqvist e8e00a5237 auto-update wiring: silent download, restart banner, local-feed drill seam
electron-updater in the shell (packaged builds only; dev never updates).
Checks at startup + every 24h, downloads silently, and the renderer shows
a Restart banner once the update is staged (it installs on quit either
way). Manual "Check for Updates..." under the app menu answers via dialog.
Every updater event routes through the shell log and is caught - the
updater can never take the app down (verified live: a feed failure logs
three lines and the app runs on).

LH_UPDATE_FEED overrides the baked GitHub feed with a generic local
server - the pre-release drill: serve latest-mac.yml + zip + blockmap
from localhost and a real installed copy updates itself, no release, no
public repo.

The asar allow-list re-includes electron-updater + its 16-package runtime
tree explicitly (gitignore last-match-wins; asar verified at 2.1 MB).
The banner rides the preload bridge (onUpdateStatus/installUpdate) and
renders nothing in the browser. window.lh typing centralized in
sidebar.tsx's declaration.
2026-10-10 14:11:19 +03:00

83 lines
3.2 KiB
TypeScript

/**
* Update-banner render tests (Wave C, 2026-10-10): the banner is a pure
* function of the last lh:update-status push — hidden without one (the
* browser/dev world: no window.lh at all), visible with the downloaded
* status, Restart wired to the bridge. The subscribe effect only runs
* client-side, so the SSR pass pins the hidden state; the pushed state is
* exercised by mounting with the bridge stubbed (classic-JSX shim as in
* render-sidebar.test.tsx).
*/
import React from "react";
(globalThis as Record<string, unknown>).React = React;
import assert from "node:assert/strict";
import { describe, it, afterEach } from "node:test";
import { renderToStaticMarkup } from "react-dom/server";
import * as ReactDOMClient from "react-dom/client";
import { flushSync } from "react-dom";
import { parseHTML } from "linkedom";
import UpdateBanner from "../components/update-banner.tsx";
// linkedom as the DOM for the interactive pass (first createRoot test in
// the repo — the banner's push→render→click flow needs real effects).
// The window.lh bridge type is declared globally by components/sidebar.tsx.
const dom = parseHTML("<html><body></body></html>");
(globalThis as Record<string, unknown>).window = dom.window;
(globalThis as Record<string, unknown>).document = dom.document;
describe("update banner (Wave C)", () => {
afterEach(() => {
delete (window as { lh?: unknown }).lh;
});
it("renders nothing with no bridge (browser / SSR)", () => {
const html = renderToStaticMarkup(<UpdateBanner />);
assert.equal(html, "");
});
it("renders nothing when the bridge exists but no status has arrived", () => {
const listeners: Array<(s: { status: string; version: string | null }) => void> = [];
(window as { lh?: unknown }).lh = {
onUpdateStatus: (cb: (s: { status: string; version: string | null }) => void) => {
listeners.push(cb);
return () => {};
},
installUpdate: () => {},
};
const html = renderToStaticMarkup(<UpdateBanner />);
assert.equal(html, "");
void listeners;
});
it("shows the version + Restart once the downloaded push arrives, and Restart installs", async () => {
let pushed: ((s: { status: string; version: string | null }) => void) | null = null;
let installed = 0;
(window as { lh?: unknown }).lh = {
onUpdateStatus: (cb: (s: { status: string; version: string | null }) => void) => {
pushed = cb;
return () => {};
},
installUpdate: () => {
installed += 1;
},
};
const host = document.createElement("div");
document.body.appendChild(host);
const root = ReactDOMClient.createRoot(host);
flushSync(() => root.render(<UpdateBanner />));
assert.equal(host.innerHTML, "", "hidden before the push");
await new Promise((r) => setTimeout(r, 0));
flushSync(() => pushed?.({ status: "downloaded", version: "0.3.1" }));
assert.ok(host.innerHTML.includes("0.3.1"), "the version renders");
assert.ok(host.innerHTML.includes("Restart"), "the restart affordance renders");
const button = host.querySelector<HTMLButtonElement>("[data-testid='update-banner'] button");
assert.ok(button, "restart button exists");
button!.click();
assert.equal(installed, 1, "Restart asks the shell to quit-and-install");
root.unmount();
host.remove();
});
});