Files
hyperframes/packages/studio/src/hooks/externalFileChangeOwnership.test.ts
T
Miguel Ángel dbb8222c8d fix(studio): catch up the preview after missed file changes (#5126)
* fix(studio): recover previews after missed file changes

* fix(studio): keep queued reloads with the current owner

* fix(studio): keep a queued outside change when switching compositions

* test(cli): the project watcher closes its parent folder's watch too

* fix(studio): keep the project-folder change out of the shared signature check

* fix(studio): keep the edited file's draft and reload every session after a reconnect

A reconnect or replaced folder is a project-wide change: every open SDK session reloads, a failed save is recorded against the file the draft belongs to, and two queued changes to different files merge into one project-wide reload.

* fix(studio): refresh the preview cache when the .hyperframes folder is replaced

* fix(cli): report the files of a folder moved into the project

Replacing a populated folder such as .hyperframes/ now reaches the signature cache through its files, so the shared signature rule stays as on main and Studio's own writes inside .hyperframes/ still invalidate nothing.

* refactor(studio): name the merged file-change payload and share the test sender

* fix(studio): keep a failed draft's recovery copy until that file itself saves

* style(studio): rewrap the merged payload line after the rename

* fix(studio): deliver a held change's full reload when its block clears

A clean save, Use external and Keep Studio now all deliver the merged scope a blocked change owes, and a moved-in .hyperframes folder reports its manifests so a dropped one still clears the cached signature.

* fix(studio): keep restored drafts out of the reload scope and clear the lint gates

* fix(studio): retain held reload scope before React renders
2026-10-07 07:32:16 +00:00

78 lines
2.9 KiB
TypeScript

import { readFileSync } from "node:fs";
import { describe, expect, it, vi } from "vitest";
import { sseFileChangeChannel } from "./useExternalFileChangeCoordinator";
describe("external file-change subscription ownership", () => {
it("has one subscriber instead of independent Preview and SDK listeners", () => {
const preview = readFileSync(new URL("./usePreviewPersistence.ts", import.meta.url), "utf8");
const sdk = readFileSync(new URL("./useSdkSession.ts", import.meta.url), "utf8");
const coordinator = readFileSync(
new URL("./useExternalFileChangeCoordinator.ts", import.meta.url),
"utf8",
);
expect(preview).not.toContain('hot.on("hf:file-change"');
expect(sdk).not.toContain('hot.on("hf:file-change"');
expect(coordinator.match(/hot\.on\("hf:file-change"/g)).toHaveLength(1);
});
it("selects the SSE channel for production and hands it the shared handler", () => {
const coordinator = readFileSync(
new URL("./useExternalFileChangeCoordinator.ts", import.meta.url),
"utf8",
);
expect(coordinator.match(/sseFileChangeChannel\(handler, catchUp\)/g)).toHaveLength(1);
});
it("asks to catch up on every reconnect, since changes made while disconnected are never resent", () => {
const listeners = new Map<string, (event: unknown) => void>();
class FakeEventSource {
addEventListener(type: string, listener: (event: unknown) => void) {
listeners.set(type, listener);
}
close = vi.fn();
}
vi.stubGlobal("EventSource", FakeEventSource);
try {
const onReconnect = vi.fn();
sseFileChangeChannel(vi.fn(), onReconnect);
listeners.get("open")?.(new Event("open"));
expect(onReconnect).not.toHaveBeenCalled();
listeners.get("error")?.(new Event("error"));
listeners.get("open")?.(new Event("open"));
listeners.get("open")?.(new Event("open"));
expect(onReconnect).toHaveBeenCalledTimes(2);
} finally {
vi.unstubAllGlobals();
}
});
it("delivers the raw file-change event from EventSource to the handler and closes on cleanup", () => {
const listeners = new Map<string, (event: unknown) => void>();
const close = vi.fn();
const opened: string[] = [];
class FakeEventSource {
constructor(url: string) {
opened.push(url);
}
addEventListener(type: string, listener: (event: unknown) => void) {
listeners.set(type, listener);
}
close = close;
}
vi.stubGlobal("EventSource", FakeEventSource);
try {
const onDelivery = vi.fn();
const stop = sseFileChangeChannel(onDelivery, vi.fn());
const event = new MessageEvent("file-change", { data: JSON.stringify({ path: "a.html" }) });
listeners.get("file-change")?.(event);
expect(opened).toEqual(["/api/events"]);
expect(onDelivery).toHaveBeenCalledWith(event);
stop();
expect(close).toHaveBeenCalledTimes(1);
} finally {
vi.unstubAllGlobals();
}
});
});