mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): fix array-form keyframe writes, diamond click-deselect, and nested video sync
- fs.watch's async 'error' event had no listener, crashing the preview
server on EMFILE (exhausted OS watch handles)
- moveKeyframeInScript/resizeKeyframedTweenInScript/removeAllKeyframesFromScript
required object-form keyframes: {"0%": {...}}, silently no-opping on
array-form keyframes: [{...}, {...}]
- a keyframe diamond click's auto-synthesized native click event bubbled
to the ancestor clip's onClick, which toggles selection off when the
clip is already selected (the state every diamond click happens in)
- the clip's trim-resize handles (z-index 4) visually and functionally
covered any keyframe diamond within their 14px edge strip
- synthesizeFlatTweenKeyframes didn't recognize a collapsed
duration:0 + immediateRender static hold (what remove-all-keyframes
produces) as non-animated, so it kept showing a phantom diamond after
Delete All Keyframes
- resolveMediaStartSeconds's fast path for elements with their own
data-start discarded the host composition's inherited start offset,
so a video nested inside a sub-composition played from the root
timeline's time instead of holding until its parent scene began
Fixes #1838
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { shouldWatchProjectFile } from "./fileWatcher.js";
|
||||
const mockWatcher = new EventEmitter() as EventEmitter & { close: () => void };
|
||||
mockWatcher.close = vi.fn();
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
watch: vi.fn(() => mockWatcher),
|
||||
}));
|
||||
|
||||
const { shouldWatchProjectFile, createProjectWatcher } = await import("./fileWatcher.js");
|
||||
|
||||
describe("shouldWatchProjectFile", () => {
|
||||
it("watches files that can affect the project signature", () => {
|
||||
@@ -17,3 +25,15 @@ describe("shouldWatchProjectFile", () => {
|
||||
expect(shouldWatchProjectFile(".hyperframes/cache.json")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createProjectWatcher", () => {
|
||||
// Regression: fs.watch can fail asynchronously (e.g. EMFILE from exhausted
|
||||
// OS watch handles) via an 'error' event, not a thrown exception. An
|
||||
// EventEmitter 'error' with no listener crashes the whole process — this
|
||||
// must degrade gracefully instead, per the sibling synchronous-failure path.
|
||||
it("does not crash the process when the underlying watcher emits 'error'", () => {
|
||||
createProjectWatcher("/fake/project/dir");
|
||||
expect(() => mockWatcher.emit("error", new Error("EMFILE"))).not.toThrow();
|
||||
expect(mockWatcher.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,15 @@ export function createProjectWatcher(projectDir: string): ProjectWatcher {
|
||||
}
|
||||
}, DEBOUNCE_MS);
|
||||
});
|
||||
// fs.watch can fail asynchronously too (e.g. EMFILE from exhausted OS watch
|
||||
// handles) — that surfaces as an 'error' event, not a thrown exception. An
|
||||
// EventEmitter 'error' with no listener crashes the whole process, so this
|
||||
// listener is required for the same "degrade gracefully" the catch below
|
||||
// already promises for the synchronous failure mode.
|
||||
watcher.on("error", () => {
|
||||
watcher?.close();
|
||||
watcher = null;
|
||||
});
|
||||
} catch {
|
||||
// fs.watch may fail on some platforms — degrade gracefully (no auto-refresh)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user