mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
* 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
78 lines
2.9 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|