mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
* fix(studio): drain pending edits before reload * fix(studio): address drain review feedback (#2989) - prioritize conflicts and clear recovered DOM queue errors - cover delayed blur effects and missing drain branches - document stacked consumers and extend write-token retention * test(studio): satisfy drain audit gate (#2989) - share the editor-save hook harness across drain regressions - extract settled failure inspection from the drain loop
151 lines
4.3 KiB
TypeScript
151 lines
4.3 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
addStudioPendingEditFlushListener,
|
|
flushStudioPendingEdits,
|
|
trackStudioPendingEdit,
|
|
} from "./studioPendingEdits";
|
|
import { StudioFileConflictError } from "./studioSaveDiagnostics";
|
|
|
|
describe("studio pending edit flush", () => {
|
|
it("waits for mounted panels to persist pending local edits", async () => {
|
|
const persist = vi.fn(async () => undefined);
|
|
const remove = addStudioPendingEditFlushListener(persist);
|
|
|
|
try {
|
|
await expect(flushStudioPendingEdits()).resolves.toEqual({ status: "clean" });
|
|
expect(persist).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
remove();
|
|
}
|
|
});
|
|
|
|
it("commits the focused debounced field before draining pending work", async () => {
|
|
const input = document.createElement("textarea");
|
|
document.body.append(input);
|
|
const persist = vi.fn(async () => undefined);
|
|
input.addEventListener("blur", () => {
|
|
trackStudioPendingEdit(persist());
|
|
});
|
|
input.focus();
|
|
|
|
await expect(flushStudioPendingEdits()).resolves.toEqual({ status: "clean" });
|
|
|
|
expect(document.activeElement).not.toBe(input);
|
|
expect(persist).toHaveBeenCalledOnce();
|
|
input.remove();
|
|
});
|
|
|
|
it("waits for a post-blur effect to register its pending edit listener", async () => {
|
|
const input = document.createElement("textarea");
|
|
document.body.append(input);
|
|
const persist = vi.fn(async () => undefined);
|
|
let removeListener: (() => void) | undefined;
|
|
let registrationDone: Promise<void> | undefined;
|
|
input.addEventListener("blur", () => {
|
|
registrationDone = new Promise<void>((resolve) => {
|
|
setTimeout(() => {
|
|
removeListener = addStudioPendingEditFlushListener(persist);
|
|
resolve();
|
|
}, 0);
|
|
});
|
|
});
|
|
input.focus();
|
|
|
|
try {
|
|
await expect(flushStudioPendingEdits()).resolves.toEqual({ status: "clean" });
|
|
|
|
expect(persist).toHaveBeenCalledOnce();
|
|
} finally {
|
|
await registrationDone;
|
|
removeListener?.();
|
|
input.remove();
|
|
}
|
|
});
|
|
|
|
it("preserves a pending edit failure instead of reporting a clean drain", async () => {
|
|
const failure = new Error("field save failed");
|
|
const remove = addStudioPendingEditFlushListener(async () => {
|
|
throw failure;
|
|
});
|
|
|
|
try {
|
|
await expect(flushStudioPendingEdits()).resolves.toEqual({
|
|
status: "failed",
|
|
error: failure,
|
|
});
|
|
} finally {
|
|
remove();
|
|
}
|
|
});
|
|
|
|
it("keeps the full typed conflict payload for the external-change decision", async () => {
|
|
const conflict = new StudioFileConflictError({
|
|
filePath: "index.html",
|
|
currentVersion: "v2",
|
|
currentContent: "external",
|
|
attemptedContent: "studio",
|
|
});
|
|
const remove = addStudioPendingEditFlushListener(async () => {
|
|
throw conflict;
|
|
});
|
|
|
|
try {
|
|
await expect(flushStudioPendingEdits()).resolves.toEqual({
|
|
status: "conflict",
|
|
error: conflict,
|
|
});
|
|
} finally {
|
|
remove();
|
|
}
|
|
});
|
|
|
|
it("prioritizes a conflict when pending edits fail with mixed errors", async () => {
|
|
const failure = new Error("field save failed");
|
|
const conflict = new StudioFileConflictError({
|
|
filePath: "index.html",
|
|
currentVersion: "v2",
|
|
currentContent: "external",
|
|
attemptedContent: "studio",
|
|
});
|
|
const removeFailure = addStudioPendingEditFlushListener(async () => {
|
|
throw failure;
|
|
});
|
|
const removeConflict = addStudioPendingEditFlushListener(async () => {
|
|
throw conflict;
|
|
});
|
|
|
|
try {
|
|
await expect(flushStudioPendingEdits()).resolves.toEqual({
|
|
status: "conflict",
|
|
error: conflict,
|
|
});
|
|
} finally {
|
|
removeFailure();
|
|
removeConflict();
|
|
}
|
|
});
|
|
|
|
it("waits for edits already started by unmounted panels", async () => {
|
|
const steps: string[] = [];
|
|
let resolvePersist!: () => void;
|
|
trackStudioPendingEdit(
|
|
new Promise<void>((resolve) => {
|
|
resolvePersist = resolve;
|
|
}).then(() => {
|
|
steps.push("persisted");
|
|
}),
|
|
);
|
|
|
|
const flushed = flushStudioPendingEdits().then(() => {
|
|
steps.push("flushed");
|
|
});
|
|
await Promise.resolve();
|
|
expect(steps).toEqual([]);
|
|
|
|
resolvePersist();
|
|
await flushed;
|
|
expect(steps).toEqual(["persisted", "flushed"]);
|
|
});
|
|
});
|