mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
B7: the group bus strip — droppable, and deliberately minimal per the casual-user design constraints (groups doc §5): a volume slider, a level bar that moves with the sound, and the words "Too loud" when it clips. No dB numbers, no peak-hold readout, no routing row. Transport (core): groupInput() now routes each group through input -> [FX chain or dry passthrough] -> output -> master, with one AnalyserNode per group tapped off `output` (post-FX, so the meter reads what the bus actually outputs) — fftSize 256, level not spectrum. groupLevel(groupId) returns RMS-ish level 0..1 + a clipped flag off a reused per-group buffer (no per-frame allocation), or null when the group is idle/unknown. The runtime posts group-levels messages only while playing, piggybacking the existing message channel rather than adding a new poll loop. Studio: groupLevels.ts is a plain pub-sub store (mirrors liveTime.ts's shape) fed by useTimelinePlayer's message handler via parseGroupLevelsMessage; useGroupLevel throttles re-renders to ~33ms. TimelineGroupBusStrip renders in the group row's own `∿` lane area (STRIP_H, already sized in B2's row-height pipeline) — drag writes live via onSetAudioGroupAttributeLive, release commits one undo entry via onSetAudioGroupAttributeQuiet (packages/studio/src/hooks/ timelineAudioGroupVolume.ts, extracted from timelineTrackVisibility.ts to stay under the 600-line cap; mirrors FxParamRow's live/commit split). "Too loud" holds for ~2s after the last clipped block, tracked in the component, not the transport. volumeByGroup mirrors labelByGroup in useTimelineTrackDerivations.ts so the strip's slider round-trips the group's own data-volume. Fixed two pre-existing group-routing tests in webAudioTransport.test.ts that hardcoded gain-node creation order/count — B7 inserts an extra `output` gain node between the group's input and master (for the meter to tap), which shifted node indices the tests asserted on directly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
3.6 KiB
TypeScript
111 lines
3.6 KiB
TypeScript
// @vitest-environment happy-dom
|
|
|
|
import React, { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { EditorShell } from "./EditorShell";
|
|
|
|
const hookMocks = vi.hoisted(() => ({
|
|
useTimelineSelectionPreviewSync: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../hooks/useTimelineSelectionPreviewSync", () => hookMocks);
|
|
vi.mock("../contexts/StudioContext", () => ({
|
|
useStudioPlaybackContext: () => ({
|
|
captionEditMode: false,
|
|
refreshKey: 0,
|
|
refreshPreviewDocumentVersion: vi.fn(),
|
|
timelineElements: [],
|
|
}),
|
|
useStudioShellContext: () => ({
|
|
projectId: "project-1",
|
|
activeCompPath: "index.html",
|
|
setActiveCompPath: vi.fn(),
|
|
handlePreviewIframeRef: vi.fn(),
|
|
showToast: vi.fn(),
|
|
}),
|
|
}));
|
|
vi.mock("../contexts/DomEditContext", () => ({
|
|
useDomEditActionsContext: () => ({
|
|
handleTimelineElementSelect: vi.fn(),
|
|
buildDomSelectionForTimelineElement: vi.fn(),
|
|
applyDomSelection: vi.fn(),
|
|
applyMarqueeSelection: vi.fn(),
|
|
}),
|
|
useDomEditSelectionContext: () => ({
|
|
domEditSelection: null,
|
|
domEditGroupSelections: [],
|
|
}),
|
|
}));
|
|
vi.mock("./nle/NLEContext", () => ({
|
|
NLEProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
useNLEContext: () => ({
|
|
compositionStack: [],
|
|
updateCompositionStack: vi.fn(),
|
|
containerRef: { current: null },
|
|
}),
|
|
}));
|
|
vi.mock("./nle/useTimelineEditCallbacks", () => ({
|
|
useTimelineEditCallbacks: () => ({}),
|
|
}));
|
|
vi.mock("./nle/PreviewPane", () => ({ PreviewPane: () => null }));
|
|
vi.mock("./nle/PreviewOverlays", () => ({ PreviewOverlays: () => null }));
|
|
vi.mock("./nle/TimelinePane", () => ({ TimelinePane: () => null }));
|
|
vi.mock("../captions/components/CaptionTimeline", () => ({ CaptionTimeline: () => null }));
|
|
vi.mock("./StudioFeedbackBar", () => ({ StudioFeedbackBar: () => null }));
|
|
|
|
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = "";
|
|
hookMocks.useTimelineSelectionPreviewSync.mockClear();
|
|
});
|
|
|
|
describe("EditorShell timeline selection sync", () => {
|
|
it("keeps the timeline store mirrored into the preview selection", () => {
|
|
const host = document.createElement("div");
|
|
document.body.append(host);
|
|
const root = createRoot(host);
|
|
|
|
act(() => {
|
|
root.render(
|
|
<EditorShell
|
|
left={null}
|
|
right={null}
|
|
timelineToolbar={null}
|
|
renderClipContent={() => null}
|
|
handleTimelineElementDelete={vi.fn()}
|
|
handleTimelineAssetDrop={vi.fn()}
|
|
handleTimelineFileDrop={vi.fn()}
|
|
handleTimelineElementMove={vi.fn()}
|
|
handleTimelineElementsMove={vi.fn()}
|
|
handleTimelineElementResize={vi.fn()}
|
|
handleTimelineGroupResize={vi.fn()}
|
|
handleToggleTrackHidden={vi.fn()}
|
|
setAudioGroupAttribute={{ setLive: vi.fn(), setQuiet: vi.fn() }}
|
|
handleBlockedTimelineEdit={vi.fn()}
|
|
handleTimelineElementSplit={vi.fn()}
|
|
handleRazorSplit={vi.fn()}
|
|
handleRazorSplitAll={vi.fn()}
|
|
setCompIdToSrc={vi.fn()}
|
|
setCompositionLoading={vi.fn()}
|
|
shouldShowMotionPath={false}
|
|
shouldShowSelectedDomBounds={false}
|
|
/>,
|
|
);
|
|
});
|
|
|
|
expect(hookMocks.useTimelineSelectionPreviewSync).toHaveBeenCalledOnce();
|
|
expect(hookMocks.useTimelineSelectionPreviewSync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
activeCompPath: "index.html",
|
|
timelineElements: [],
|
|
domEditSelection: null,
|
|
domEditGroupSelections: [],
|
|
}),
|
|
);
|
|
|
|
act(() => root.unmount());
|
|
});
|
|
});
|