fix(studio): a reload refused because the page is missing mid-edit warns instead of erroring (#6491)

This commit is contained in:
Miguel Ángel
2026-10-26 23:53:48 +00:00
committed by GitHub
parent dcec31b468
commit 097cc094b7
5 changed files with 53 additions and 3 deletions
@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { refusedPreviewMessage, refusedPreviewStatus } from "./studioPreviewMark";
describe("refusedPreviewStatus", () => {
it.each([404, 410, 500])("reads %i back out of the refused page's message", (status) => {
expect(refusedPreviewStatus(refusedPreviewMessage(status))).toBe(status);
});
it.each([
"Composition timeline not found after 8s",
"it took too long to load",
"x The preview page could not be loaded (HTTP 404).",
])("finds no status in %j", (cause) => {
expect(refusedPreviewStatus(cause)).toBeNull();
});
});
+7
View File
@@ -3,6 +3,13 @@ export const STUDIO_PREVIEW_CAPTURE_MODE = "capture";
export const STUDIO_PREVIEW_UPCOMING_ATTR = "data-hf-upcoming";
export const STUDIO_PREVIEW_LAZY_ATTR = "data-hf-preview-lazy";
export const STUDIO_PREVIEW_ERRORS = "__hfPreviewErrors";
export const refusedPreviewMessage = (status: number) =>
`The preview page could not be loaded (HTTP ${status}).`;
export function refusedPreviewStatus(message: string): number | null {
const status = /^The preview page could not be loaded \(HTTP (\d{3})\)\.$/.exec(message)?.[1];
return status ? Number(status) : null;
}
export const PREVIEW_RASTER_ATTR = "data-hf-preview-raster";
export const RUNTIME_STAMPED_ATTR = "data-hf-autostamped";
export const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
@@ -1,6 +1,9 @@
import { STUDIO_PREVIEW_ERRORS } from "@hyperframes/core/studio-preview-mark";
import {
refusedPreviewMessage,
STUDIO_PREVIEW_ERRORS,
} from "@hyperframes/core/studio-preview-mark";
export function previewErrorPage(status: number): string {
const message = `The preview page could not be loaded (HTTP ${status}).`;
const message = refusedPreviewMessage(status);
return `<!doctype html><meta charset="utf-8"><script>window.${STUDIO_PREVIEW_ERRORS}=[${JSON.stringify(message)}]</script><p>${message}</p>`;
}
@@ -8,6 +8,7 @@ import {
isStudioManualEditGestureLiveIn,
studioManualEditSavesIn,
} from "../../components/editor/manualEditsDom";
import { refusedPreviewStatus } from "@hyperframes/core/studio-preview-mark";
import { logReload } from "../../utils/reloadDebug";
import {
useTimelineSyncCallbacks,
@@ -131,7 +132,9 @@ export function useShadowPreviewReload({
setPreviewReloading(false);
const message = `The preview did not reload (${cause}). It was still waiting for ${waitingFor}. The previous preview is still showing.`;
logReload("shadow-failed", { cause, waitingFor });
console.error(`[studio] ${message}`);
const refused = refusedPreviewStatus(cause);
const pageMissingNow = refused !== null && refused < 500;
(pageMissingNow ? console.warn : console.error)(`[studio] ${message}`);
onReloadFailedRef.current?.(message);
},
[stopPendingShadow, isRefreshingRef, pendingSeekRef],
@@ -5,6 +5,7 @@ import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SHADOW_READY_BUDGETS, SHADOW_READY_TIMEOUT_MS } from "./useShadowPreviewReload";
import { refusedPreviewMessage } from "@hyperframes/core/studio-preview-mark";
import { usePlayerStore } from "../store/playerStore";
import { isPreviewChanging } from "../previewReloading";
import { NLEProvider, useNLEContext, type NLEContextValue } from "../../components/nle/NLEContext";
@@ -639,6 +640,26 @@ describe("shadow reload readiness and failure", () => {
unmount(root);
});
// An agent's edit moves the page away for a few saves: each reload is refused until it is back.
it.each([
[404, "warn"],
[500, "error"],
] as const)("reports a reload the server refused with %i as a console %s", (status, level) => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const onPreviewReloadFailed = vi.fn();
const { getApi, live, gen, root } = beginReload({ onPreviewReloadFailed });
act(() => getApi().onShadowError(gen, refusedPreviewMessage(status)));
expect(getApi().iframeRef.current).toBe(live);
expect(onPreviewReloadFailed).toHaveBeenCalledOnce();
const said = (spy: typeof warn) =>
spy.mock.calls.some(([text]) => String(text).includes(`(HTTP ${status})`));
expect([said(warn), said(error)]).toEqual([level === "warn", level === "error"]);
unmount(root);
});
it("runs no load side effects for a superseded shadow", () => {
const { getApi, gen, root } = beginReload();
usePlayerStore.getState().setCurrentTime(7);