@hyperframes/studio 0.8.188 → 0.8.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-DwWIKKZG.js → hyperframes-player-0HXJPmYC.js} +1 -1
- package/dist/assets/{index-CESvLzz0.css → index-B3vk3hfU.css} +1 -1
- package/dist/assets/{index-M8lEpS0Q.js → index-BYw97F07.js} +127 -127
- package/dist/assets/{index-Cjwz7_13.js → index-DsSfhjUS.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +3864 -3820
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/editor/overlayFrameLoop.test.ts +42 -0
- package/src/components/editor/overlayFrameLoop.ts +11 -3
- package/src/components/renders/useRenderQueue.ts +0 -8
- package/src/player/components/PlayerControls.test.tsx +67 -2
- package/src/player/components/PlayerControls.tsx +5 -3
- package/src/player/components/PreviewSeekBar.tsx +47 -0
- package/src/components/renders/useRenderQueueMotionBlur.test.tsx +0 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.190",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"dockview-react": "8.3.1",
|
|
60
60
|
"gsap": "^3.13.0",
|
|
61
61
|
"mediabunny": "^1.45.3",
|
|
62
|
-
"@hyperframes/
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
62
|
+
"@hyperframes/core": "0.8.190",
|
|
63
|
+
"@hyperframes/parsers": "0.8.190",
|
|
64
|
+
"@hyperframes/sdk": "0.8.190",
|
|
65
|
+
"@hyperframes/player": "0.8.190",
|
|
66
|
+
"@hyperframes/shader-transitions": "0.8.190",
|
|
67
|
+
"@hyperframes/studio-server": "0.8.190"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.190"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -121,6 +121,48 @@ describe("overlay frame loop", () => {
|
|
|
121
121
|
expect(runs).toBeLessThanOrEqual(Math.ceil(1000 / IDLE_POLL_MS) + 1);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
it("polls less often the longer nothing happens, and fast again after a wake", () => {
|
|
125
|
+
let runs = 0;
|
|
126
|
+
subscribeOverlayFrame(() => {
|
|
127
|
+
runs += 1;
|
|
128
|
+
});
|
|
129
|
+
framesOver(10_000);
|
|
130
|
+
runs = 0;
|
|
131
|
+
// Backed off: a 4 s wait between polls, where a steady quarter-second poll would run 48 times.
|
|
132
|
+
framesOver(12_000);
|
|
133
|
+
expect(runs).toBeLessThanOrEqual(4);
|
|
134
|
+
|
|
135
|
+
window.dispatchEvent(new Event("pointermove"));
|
|
136
|
+
framesOver(500);
|
|
137
|
+
runs = 0;
|
|
138
|
+
framesOver(IDLE_POLL_MS * 2);
|
|
139
|
+
expect(runs).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("does not poll while the page is hidden, and wakes when it shows", () => {
|
|
143
|
+
let hidden = false;
|
|
144
|
+
Object.defineProperty(document, "hidden", { configurable: true, get: () => hidden });
|
|
145
|
+
try {
|
|
146
|
+
let runs = 0;
|
|
147
|
+
subscribeOverlayFrame(() => {
|
|
148
|
+
runs += 1;
|
|
149
|
+
});
|
|
150
|
+
framesOver(500);
|
|
151
|
+
hidden = true;
|
|
152
|
+
runs = 0;
|
|
153
|
+
framesOver(8_000);
|
|
154
|
+
// At most the one poll armed before the page hid; none is armed after it.
|
|
155
|
+
expect(runs).toBeLessThanOrEqual(1);
|
|
156
|
+
|
|
157
|
+
hidden = false;
|
|
158
|
+
window.dispatchEvent(new Event("visibilitychange"));
|
|
159
|
+
framesOver(200);
|
|
160
|
+
expect(runs).toBeGreaterThan(5);
|
|
161
|
+
} finally {
|
|
162
|
+
delete (document as { hidden?: boolean }).hidden;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
124
166
|
it("wakes to full rate on a pointer event", () => {
|
|
125
167
|
let runs = 0;
|
|
126
168
|
subscribeOverlayFrame(() => {
|
|
@@ -30,11 +30,14 @@ import { usePlayerStore } from "../../player/store/playerStore";
|
|
|
30
30
|
const AWAKE_MS = 400;
|
|
31
31
|
/** How often a parked loop runs one frame anyway, in case a wake was missed. */
|
|
32
32
|
export const IDLE_POLL_MS = 250;
|
|
33
|
+
/** The longest a parked loop waits between polls; each idle poll doubles the wait up to this. */
|
|
34
|
+
export const MAX_IDLE_POLL_MS = 4000;
|
|
33
35
|
|
|
34
36
|
const subscribers = new Set<() => void>();
|
|
35
37
|
|
|
36
38
|
let frameId: number | null = null;
|
|
37
39
|
let idleTimerId: ReturnType<typeof setTimeout> | null = null;
|
|
40
|
+
let idlePollMs = IDLE_POLL_MS;
|
|
38
41
|
let awakeUntil = 0;
|
|
39
42
|
let listenersAttached = false;
|
|
40
43
|
let stopBootWake: (() => void) | null = null;
|
|
@@ -137,10 +140,13 @@ function schedule(): void {
|
|
|
137
140
|
frameId = requestAnimationFrame(runFrame);
|
|
138
141
|
return;
|
|
139
142
|
}
|
|
143
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
144
|
+
const wait = idlePollMs;
|
|
145
|
+
idlePollMs = Math.min(idlePollMs * 2, MAX_IDLE_POLL_MS);
|
|
140
146
|
idleTimerId = setTimeout(() => {
|
|
141
147
|
idleTimerId = null;
|
|
142
148
|
frameId = requestAnimationFrame(runFrame);
|
|
143
|
-
},
|
|
149
|
+
}, wait);
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
/** Longest idle slice, so input that arrives during one waits at most this long for its frame. */
|
|
@@ -167,6 +173,7 @@ export function runWhenInputIdle(step: (timeLeft: () => number) => boolean): voi
|
|
|
167
173
|
/** Something moved, or might have. Run frames at full rate for a moment. */
|
|
168
174
|
export function requestOverlayFrames(): void {
|
|
169
175
|
awakeUntil = performance.now() + AWAKE_MS;
|
|
176
|
+
idlePollMs = IDLE_POLL_MS;
|
|
170
177
|
if (idleTimerId != null) {
|
|
171
178
|
clearTimeout(idleTimerId);
|
|
172
179
|
idleTimerId = null;
|
|
@@ -199,8 +206,8 @@ function detachListeners(): void {
|
|
|
199
206
|
|
|
200
207
|
/**
|
|
201
208
|
* Run `update` on the shared loop. The subscriber must be a pure poll: it is
|
|
202
|
-
* called on an ordinary animation frame while the editor is active
|
|
203
|
-
*
|
|
209
|
+
* called on an ordinary animation frame while the editor is active, then
|
|
210
|
+
* less often the longer it stays idle, down to once every four seconds.
|
|
204
211
|
*/
|
|
205
212
|
export function subscribeOverlayFrame(update: () => void): () => void {
|
|
206
213
|
subscribers.add(update);
|
|
@@ -230,6 +237,7 @@ export function resetOverlayFrameLoopForTests(): void {
|
|
|
230
237
|
frameId = null;
|
|
231
238
|
idleTimerId = null;
|
|
232
239
|
awakeUntil = 0;
|
|
240
|
+
idlePollMs = IDLE_POLL_MS;
|
|
233
241
|
lastPreviewFrame = null;
|
|
234
242
|
lastPreviewPlaying = null;
|
|
235
243
|
detachListeners();
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { htmlInCanvasForm } from "@hyperframes/core/html-in-canvas";
|
|
2
1
|
import { buildProjectApiPath } from "../../utils/projectRouting";
|
|
3
2
|
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
4
3
|
import type { CanvasResolution } from "@hyperframes/parsers";
|
|
@@ -11,11 +10,6 @@ import { ffmpegInstallMessage, useFfmpegStatus } from "./useFfmpegStatus";
|
|
|
11
10
|
import { requestStudioFeedback, type FeedbackContext } from "../feedback/feedbackTrigger";
|
|
12
11
|
import { studioApiFetch } from "../../utils/studioApiFetch";
|
|
13
12
|
|
|
14
|
-
// WYSIWYG: the preview iframe runs in this browser, so a render blurs only if this one can.
|
|
15
|
-
function canPreviewMotionBlur(): boolean {
|
|
16
|
-
return typeof document !== "undefined" && htmlInCanvasForm() !== null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
13
|
export interface RenderJob {
|
|
20
14
|
id: string;
|
|
21
15
|
status: "rendering" | "complete" | "failed" | "cancelled";
|
|
@@ -244,12 +238,10 @@ export function useRenderQueue(
|
|
|
244
238
|
gpu?: boolean;
|
|
245
239
|
telemetryDistinctId?: string;
|
|
246
240
|
telemetryOptOut?: boolean;
|
|
247
|
-
previewMotionBlur: boolean;
|
|
248
241
|
} = {
|
|
249
242
|
fps,
|
|
250
243
|
quality,
|
|
251
244
|
format,
|
|
252
|
-
previewMotionBlur: canPreviewMotionBlur(),
|
|
253
245
|
};
|
|
254
246
|
// The id is MINTED by getAnonymousId(), so calling it unconditionally
|
|
255
247
|
// created a telemetry identity for a profile that had opted out — and
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
7
7
|
import { PlayerControls } from "./PlayerControls";
|
|
8
8
|
|
|
@@ -10,22 +10,87 @@ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
|
10
10
|
|
|
11
11
|
let host: HTMLDivElement;
|
|
12
12
|
let root: Root;
|
|
13
|
+
const onSeek = vi.fn();
|
|
14
|
+
|
|
15
|
+
function seekBar(): HTMLInputElement {
|
|
16
|
+
const input = host.querySelector('input[aria-label="Seek preview"]');
|
|
17
|
+
if (!(input instanceof HTMLInputElement)) throw new Error("Preview seek bar is missing");
|
|
18
|
+
return input;
|
|
19
|
+
}
|
|
13
20
|
|
|
14
21
|
beforeEach(async () => {
|
|
22
|
+
onSeek.mockClear();
|
|
15
23
|
usePlayerStore.setState({
|
|
16
24
|
isPlaying: true,
|
|
17
25
|
currentTime: 2,
|
|
18
26
|
duration: 30,
|
|
19
27
|
timeDisplayMode: "time",
|
|
28
|
+
timelineReady: true,
|
|
20
29
|
});
|
|
21
30
|
host = document.createElement("div");
|
|
22
31
|
document.body.append(host);
|
|
23
32
|
root = createRoot(host);
|
|
24
33
|
await act(async () =>
|
|
25
|
-
root.render(React.createElement(PlayerControls, { onTogglePlay: () => {}, onSeek
|
|
34
|
+
root.render(React.createElement(PlayerControls, { onTogglePlay: () => {}, onSeek })),
|
|
26
35
|
);
|
|
27
36
|
});
|
|
28
37
|
|
|
38
|
+
describe("PlayerControls preview seeking", () => {
|
|
39
|
+
it("shows the current position and follows the playback clock", () => {
|
|
40
|
+
const input = seekBar();
|
|
41
|
+
expect(input.type).toBe("range");
|
|
42
|
+
expect(input.valueAsNumber).toBe(2);
|
|
43
|
+
expect(input.max).toBe("30");
|
|
44
|
+
expect(input.disabled).toBe(false);
|
|
45
|
+
|
|
46
|
+
liveTime.notify(12.5);
|
|
47
|
+
expect(input.valueAsNumber).toBe(12.5);
|
|
48
|
+
expect(input.getAttribute("aria-valuetext")).toBe("00:12 of 00:30");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("seeks continuously and reaches both ends of the preview", () => {
|
|
52
|
+
const input = seekBar();
|
|
53
|
+
const setValue = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
|
|
54
|
+
if (!setValue) throw new Error("Native range value setter is missing");
|
|
55
|
+
for (const time of [8.25, 15.5, 30, 0]) {
|
|
56
|
+
act(() => {
|
|
57
|
+
setValue.call(input, String(time));
|
|
58
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
59
|
+
});
|
|
60
|
+
expect(onSeek).toHaveBeenLastCalledWith(time);
|
|
61
|
+
}
|
|
62
|
+
expect(onSeek).toHaveBeenCalledTimes(4);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("updates its duration and resets its position for a new composition", async () => {
|
|
66
|
+
await act(async () => usePlayerStore.setState({ duration: 5, currentTime: 0 }));
|
|
67
|
+
expect(seekBar().max).toBe("5");
|
|
68
|
+
expect(seekBar().valueAsNumber).toBe(0);
|
|
69
|
+
expect(seekBar().getAttribute("aria-valuetext")).toBe("00:00 of 00:05");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("disables seeking until the preview is ready or has a duration", async () => {
|
|
73
|
+
await act(async () => usePlayerStore.setState({ timelineReady: false }));
|
|
74
|
+
expect(seekBar().disabled).toBe(true);
|
|
75
|
+
await act(async () => usePlayerStore.setState({ timelineReady: true, duration: 0 }));
|
|
76
|
+
expect(seekBar().disabled).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("respects the disabled prop in fullscreen", async () => {
|
|
80
|
+
await act(async () =>
|
|
81
|
+
root.render(
|
|
82
|
+
React.createElement(PlayerControls, {
|
|
83
|
+
onTogglePlay: () => {},
|
|
84
|
+
onSeek,
|
|
85
|
+
disabled: true,
|
|
86
|
+
isFullscreen: true,
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
expect(seekBar().disabled).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
29
94
|
afterEach(async () => {
|
|
30
95
|
await act(async () => root.unmount());
|
|
31
96
|
host.remove();
|
|
@@ -10,6 +10,7 @@ import { ShortcutsButton } from "./ShortcutsPanel";
|
|
|
10
10
|
import type { ShortcutSection } from "./studioShortcuts";
|
|
11
11
|
import { SpeedMenu } from "./SpeedMenu";
|
|
12
12
|
import { VolumeControl } from "./VolumeControl";
|
|
13
|
+
import { PreviewSeekBar } from "./PreviewSeekBar";
|
|
13
14
|
|
|
14
15
|
/* ── Icon sub-components ─────────────────────────────────────────── */
|
|
15
16
|
|
|
@@ -221,9 +222,10 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
221
222
|
});
|
|
222
223
|
|
|
223
224
|
return (
|
|
224
|
-
<div>
|
|
225
|
+
<div className="relative z-20">
|
|
226
|
+
<PreviewSeekBar duration={duration} disabled={controlsDisabled} onSeek={onSeek} />
|
|
225
227
|
<div
|
|
226
|
-
className="grid h-
|
|
228
|
+
className="grid h-7 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center px-3"
|
|
227
229
|
aria-disabled={disabled || undefined}
|
|
228
230
|
style={{
|
|
229
231
|
paddingBottom: "env(safe-area-inset-bottom)",
|
|
@@ -257,7 +259,7 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
257
259
|
onTogglePlay();
|
|
258
260
|
}}
|
|
259
261
|
disabled={controlsDisabled}
|
|
260
|
-
className="flex h-
|
|
262
|
+
className="flex h-7 w-8 items-center justify-center justify-self-center rounded-md text-neutral-100 transition-colors hover:text-white disabled:pointer-events-none disabled:opacity-30"
|
|
261
263
|
>
|
|
262
264
|
<PlayPauseMorphIcon playing={isPlaying} />
|
|
263
265
|
</button>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { memo, useEffect, useRef } from "react";
|
|
2
|
+
import { formatTime } from "../lib/time";
|
|
3
|
+
import { liveTime, usePlayerStore } from "../store/playerStore";
|
|
4
|
+
|
|
5
|
+
export const PreviewSeekBar = memo(function PreviewSeekBar({
|
|
6
|
+
duration,
|
|
7
|
+
disabled,
|
|
8
|
+
onSeek,
|
|
9
|
+
}: {
|
|
10
|
+
duration: number;
|
|
11
|
+
disabled: boolean;
|
|
12
|
+
onSeek: (time: number) => void;
|
|
13
|
+
}) {
|
|
14
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const input = inputRef.current;
|
|
18
|
+
if (!input) return;
|
|
19
|
+
const updateTime = (time: number) => {
|
|
20
|
+
input.value = String(time);
|
|
21
|
+
const text = `${formatTime(time)} of ${formatTime(duration)}`;
|
|
22
|
+
if (input.getAttribute("aria-valuetext") !== text) input.setAttribute("aria-valuetext", text);
|
|
23
|
+
};
|
|
24
|
+
updateTime(usePlayerStore.getState().currentTime);
|
|
25
|
+
const unsubscribe = liveTime.subscribe(updateTime);
|
|
26
|
+
return () => {
|
|
27
|
+
unsubscribe();
|
|
28
|
+
};
|
|
29
|
+
}, [duration]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex h-3 items-center px-3">
|
|
33
|
+
<input
|
|
34
|
+
ref={inputRef}
|
|
35
|
+
type="range"
|
|
36
|
+
min="0"
|
|
37
|
+
max={duration}
|
|
38
|
+
step="any"
|
|
39
|
+
defaultValue={0}
|
|
40
|
+
disabled={disabled || duration <= 0}
|
|
41
|
+
aria-label="Seek preview"
|
|
42
|
+
onChange={(event) => onSeek(event.currentTarget.valueAsNumber)}
|
|
43
|
+
className="h-6 w-full cursor-pointer accent-neutral-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-ink disabled:cursor-default disabled:opacity-30"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
|
|
3
|
-
// WYSIWYG: a render only blurs what the Studio preview could show, and only the browser knows
|
|
4
|
-
// whether it has html-in-canvas. The request body is how it tells the server.
|
|
5
|
-
|
|
6
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
-
import { startRenderAndReadBody, type MountedQueue } from "./renderQueueTestHarness";
|
|
8
|
-
|
|
9
|
-
vi.mock("../../telemetry/events", () => ({ trackStudioRenderStart: vi.fn() }));
|
|
10
|
-
|
|
11
|
-
const { useRenderQueue } = await import("./useRenderQueue");
|
|
12
|
-
|
|
13
|
-
let queue: MountedQueue | null = null;
|
|
14
|
-
|
|
15
|
-
async function startRenderBody(): Promise<Record<string, unknown>> {
|
|
16
|
-
const started = await startRenderAndReadBody(useRenderQueue, {
|
|
17
|
-
opts: { fps: 30, quality: "standard", format: "mp4" },
|
|
18
|
-
});
|
|
19
|
-
queue = started.queue;
|
|
20
|
-
return started.body;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function stubHtmlInCanvas(members: string[]): void {
|
|
24
|
-
vi.stubGlobal(
|
|
25
|
-
"CanvasRenderingContext2D",
|
|
26
|
-
class {
|
|
27
|
-
drawElementImage(): void {}
|
|
28
|
-
},
|
|
29
|
-
);
|
|
30
|
-
const proto = HTMLCanvasElement.prototype as unknown as Record<string, unknown>;
|
|
31
|
-
for (const name of ["requestPaint", ...members]) proto[name] = () => {};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
queue?.unmount();
|
|
36
|
-
queue = null;
|
|
37
|
-
document.body.innerHTML = "";
|
|
38
|
-
for (const name of ["requestPaint", "layoutSubtree", "updateElementGeometry"]) {
|
|
39
|
-
delete (HTMLCanvasElement.prototype as unknown as Record<string, unknown>)[name];
|
|
40
|
-
}
|
|
41
|
-
vi.unstubAllGlobals();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("render request previewMotionBlur", () => {
|
|
45
|
-
it("is false in a browser without html-in-canvas", async () => {
|
|
46
|
-
expect((await startRenderBody())["previewMotionBlur"]).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it.each([
|
|
50
|
-
["the layoutsubtree form", "layoutSubtree"],
|
|
51
|
-
["the content=drawable form", "updateElementGeometry"],
|
|
52
|
-
])("is true in a browser with html-in-canvas in %s", async (_name, member) => {
|
|
53
|
-
stubHtmlInCanvas([member]);
|
|
54
|
-
expect((await startRenderBody())["previewMotionBlur"]).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("is false when html-in-canvas has neither known form", async () => {
|
|
58
|
-
stubHtmlInCanvas([]);
|
|
59
|
-
expect((await startRenderBody())["previewMotionBlur"]).toBe(false);
|
|
60
|
-
});
|
|
61
|
-
});
|