Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions packages/studio/src/components/nle/useCompositionStack.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,72 @@ describe("useCompositionStack — project scoping", () => {
});
}
});

describe("useCompositionStack — activating a composition by path", () => {
afterEach(() => {
document.body.innerHTML = "";
});

function mountStack(activeCompositionPath: string | null) {
const host = document.createElement("div");
document.body.append(host);
const root = createRoot(host);
const seen: { stack: ReturnType<typeof useCompositionStack>["compositionStack"] } = {
stack: [],
};

function Harness(props: { activeCompositionPath: string | null }) {
seen.stack = useCompositionStack({ projectId: "p", ...props }).compositionStack;
return null;
}

return { root, seen, Harness, activeCompositionPath };
}

// The root stays on the master level; everything else pushes a second level.
for (const path of [
"compositions/scene-a.html",
"parts/part-1.html",
"chapter-2.html",
"a/b/c/deep.html",
]) {
it(`pushes a level for ${path}`, async () => {
const { root, seen, Harness } = mountStack(path);

await act(async () => {
root.render(<Harness activeCompositionPath={path} />);
});

expect(seen.stack).toHaveLength(2);
expect(seen.stack[1]?.id).toBe(path);
expect(seen.stack[1]?.previewUrl).toBe(`/api/projects/p/preview/comp/${path}`);

act(() => root.unmount());
});
}

it("labels a non-compositions/ path without mangling it", async () => {
const { root, seen, Harness } = mountStack("parts/part-1.html");

await act(async () => {
root.render(<Harness activeCompositionPath="parts/part-1.html" />);
});

expect(seen.stack[1]?.label).toBe("parts/part-1");

act(() => root.unmount());
});

it("keeps the master alone for the root composition", async () => {
const { root, seen, Harness } = mountStack("index.html");

await act(async () => {
root.render(<Harness activeCompositionPath="index.html" />);
});

expect(seen.stack).toHaveLength(1);
expect(seen.stack[0]?.id).toBe("master");

act(() => root.unmount());
});
});
10 changes: 8 additions & 2 deletions packages/studio/src/components/nle/useCompositionStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,21 @@ export function useCompositionStack({
if (activeCompositionPath === "index.html") {
usePlayerStore.getState().setElements([]);
updateCompositionStack([master]);
} else if (activeCompositionPath && activeCompositionPath.startsWith("compositions/")) {
} else if (activeCompositionPath) {
// Any composition file that isn't the root, wherever it lives. Gating
// this on a `compositions/` prefix meant a project laying its comps out
// anywhere else (`parts/part-1.html`, generated multi-part builds) hit
// no branch at all: the stack kept the master mounted while the Comps
// panel highlighted the row, so the canvas and timeline stayed on
// index.html and edits landed in the root file.
const label = activeCompositionPath.replace(/^compositions\//, "").replace(/\.html$/, "");
const previewUrl = `/api/projects/${projectId}/preview/comp/${encodePreviewPath(activeCompositionPath)}`;
usePlayerStore.getState().setElements([]);
updateCompositionStack((prev) => {
if (prev[prev.length - 1]?.id === activeCompositionPath) return prev;
return [master, { id: activeCompositionPath, label, previewUrl }];
});
} else if (!activeCompositionPath) {
} else {
usePlayerStore.getState().setElements([]);
updateCompositionStack([master]);
}
Expand Down
Loading