mirror of
https://github.com/block/buzz.git
synced 2026-11-08 10:21:46 +00:00
fix(desktop): retire lifecycle continuations when controls are canceled
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -261,3 +261,125 @@ test("terminal receiver failure is a scope-owned notification, not pre-shell lay
|
||||
dom.window.close();
|
||||
}
|
||||
});
|
||||
|
||||
for (const interruption of ["cancel", "account", "community", "unmount"]) {
|
||||
test(`mounted ${interruption} retires a pending Stop and never starts on late confirmation`, async () => {
|
||||
const dom = new JSDOM("<div id='root'></div>", {
|
||||
url: "https://desktop.test",
|
||||
});
|
||||
Object.assign(globalThis, {
|
||||
window: dom.window,
|
||||
document: dom.window.document,
|
||||
localStorage: dom.window.localStorage,
|
||||
IS_REACT_ACT_ENVIRONMENT: true,
|
||||
});
|
||||
const { createRoot } = await import("react-dom/client");
|
||||
const query = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
|
||||
});
|
||||
query.setQueryData(
|
||||
["relay-agents"],
|
||||
[{ pubkey: "agent", name: "Agent", ownerPubkey: "owner" }],
|
||||
);
|
||||
const original = {
|
||||
fetch: relayClient.fetchEvents,
|
||||
publish: relayClient.publishEvent,
|
||||
};
|
||||
const prepared = [];
|
||||
let confirmStop;
|
||||
window.__TAURI_INTERNALS__ = {
|
||||
invoke: async (command, args) => {
|
||||
if (command === "observe_desktop_placement") return;
|
||||
if (command === "read_desktop_placement")
|
||||
return ["source", "selection"];
|
||||
if (command.startsWith("prepare_desktop_")) {
|
||||
const request = {
|
||||
id: `request-${prepared.length}`,
|
||||
kind: command.endsWith("_stop") ? 50180 : 50182,
|
||||
...args,
|
||||
};
|
||||
prepared.push(request);
|
||||
return request;
|
||||
}
|
||||
if (command === "read_desktop_lifecycle_results") return "running";
|
||||
if (command === "read_desktop_stop_results")
|
||||
return new Promise((resolve) => {
|
||||
confirmStop = resolve;
|
||||
});
|
||||
throw Error(command);
|
||||
},
|
||||
};
|
||||
relayClient.fetchEvents = async () => [];
|
||||
relayClient.publishEvent = async (_event, _timeout, _failure, check) =>
|
||||
check();
|
||||
const root = createRoot(document.getElementById("root"));
|
||||
const scope = { owner: "owner", community: "wss://one.example" };
|
||||
const render = (nextScope) =>
|
||||
root.render(
|
||||
React.createElement(
|
||||
QueryClientProvider,
|
||||
{ client: query },
|
||||
React.createElement(DesktopLifecycleControl, {
|
||||
scope: nextScope,
|
||||
desktops: [
|
||||
{ id: "source", name: "Source" },
|
||||
{ id: "target", name: "Target" },
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
const click = async (text) =>
|
||||
React.act(async () =>
|
||||
[...document.querySelectorAll("button")]
|
||||
.find((b) => b.textContent === text)
|
||||
.click(),
|
||||
);
|
||||
const select = async (label, value) =>
|
||||
React.act(async () => {
|
||||
const element = document.querySelector(`select[aria-label="${label}"]`);
|
||||
element.value = value;
|
||||
element.dispatchEvent(
|
||||
new dom.window.Event("change", { bubbles: true }),
|
||||
);
|
||||
});
|
||||
try {
|
||||
await React.act(async () => render(scope));
|
||||
await select("Agent to place", "agent");
|
||||
await select("Destination Desktop", "target");
|
||||
await click("Move to destination");
|
||||
assert.equal(typeof confirmStop, "function");
|
||||
if (interruption === "cancel") await click("Cancel waiting");
|
||||
else if (interruption === "unmount")
|
||||
await React.act(async () => root.unmount());
|
||||
else
|
||||
await React.act(async () =>
|
||||
render({
|
||||
...scope,
|
||||
[interruption === "account" ? "owner" : "community"]: "changed",
|
||||
}),
|
||||
);
|
||||
await React.act(async () => confirmStop("stopped"));
|
||||
assert.equal(prepared.filter((r) => r.action === "start").length, 0);
|
||||
assert.doesNotMatch(
|
||||
document.body.textContent,
|
||||
/Retry same request|Source Stop confirmed/,
|
||||
);
|
||||
if (interruption === "cancel")
|
||||
assert.match(
|
||||
document.body.textContent,
|
||||
/Dispatched operations may still finish/,
|
||||
);
|
||||
if (interruption === "account" || interruption === "community")
|
||||
assert.equal(
|
||||
document.querySelector('select[aria-label="Agent to place"]').value,
|
||||
"",
|
||||
);
|
||||
} finally {
|
||||
await React.act(async () => root.unmount());
|
||||
query.clear();
|
||||
relayClient.fetchEvents = original.fetch;
|
||||
relayClient.publishEvent = original.publish;
|
||||
dom.window.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,11 +82,16 @@ export function DesktopLifecycleControl({
|
||||
const generation = useRef(0);
|
||||
useEffect(() => {
|
||||
active.current = true;
|
||||
setAgent("");
|
||||
setDestination("");
|
||||
setRequest(null);
|
||||
setStatus("");
|
||||
setBusy(false);
|
||||
return () => {
|
||||
active.current = false;
|
||||
generation.current++;
|
||||
};
|
||||
}, []);
|
||||
}, [scope.owner, scope.community]);
|
||||
const run = async (action: "start" | "restart" | "move" | "retry") => {
|
||||
const token = ++generation.current;
|
||||
const valid = () => active.current && generation.current === token;
|
||||
@@ -215,6 +220,22 @@ export function DesktopLifecycleControl({
|
||||
>
|
||||
Move to destination
|
||||
</Button>
|
||||
{busy && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
generation.current++;
|
||||
setBusy(false);
|
||||
setRequest(null);
|
||||
setStatus(
|
||||
"Stopped waiting. Dispatched operations may still finish; no later launch will be requested by this control.",
|
||||
);
|
||||
}}
|
||||
>
|
||||
Cancel waiting
|
||||
</Button>
|
||||
)}
|
||||
{request && (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user