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
9 changes: 8 additions & 1 deletion app/lib/overlays.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("toNorm", () => {
});

describe("createOverlay", () => {
it("creates speech overlay with defaults", () => {
it("creates speech overlay with defaults and tailAnchor", () => {
const o = createOverlay("speech", 0.2, 0.3);
expect(o.type).toBe("speech");
expect(o.x).toBe(0.2);
Expand All @@ -43,9 +43,16 @@ describe("createOverlay", () => {
expect(o.height).toBe(0.12);
expect(o.text).toBe("");
expect(o.speaker).toBe("");
expect(o.tailAnchor).toEqual({ x: 0.5, y: 1.2 });
expect(o.id).toMatch(/^overlay-/);
});

it("tailAnchor survives JSON roundtrip", () => {
const o = createOverlay("speech");
const json = JSON.parse(JSON.stringify(o));
expect(json.tailAnchor).toEqual({ x: 0.5, y: 1.2 });
});

it("creates sfx overlay with smaller dimensions", () => {
const o = createOverlay("sfx");
expect(o.type).toBe("sfx");
Expand Down
3 changes: 2 additions & 1 deletion app/lib/overlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface Overlay {
height: number;
text: string;
speaker?: string;
tailAnchor?: { x: number; y: number };
}

export function toPixel(norm: number, containerSize: number): number {
Expand All @@ -33,6 +34,6 @@ export function createOverlay(type: OverlayType, x = 0.1, y = 0.1): Overlay {
width: type === "sfx" ? 0.15 : 0.25,
height: type === "sfx" ? 0.08 : 0.12,
text: "",
...(type === "speech" ? { speaker: "" } : {}),
...(type === "speech" ? { speaker: "", tailAnchor: { x: 0.5, y: 1.2 } } : {}),
};
}
99 changes: 97 additions & 2 deletions app/web/components/LetteringEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ describe("LetteringEditor", () => {

fireEvent.click(screen.getByTestId("overlay-test-overlay-2"));

expect(screen.getByText("Narration")).toBeInTheDocument();
expect(screen.queryByTestId("inspector-empty")).not.toBeInTheDocument();
expect(screen.getByTestId("delete-overlay")).toBeInTheDocument();
});

it("deselects overlay when clicking background", () => {
Expand All @@ -156,7 +156,7 @@ describe("LetteringEditor", () => {
simulateImageLoad();

fireEvent.click(screen.getByTestId("overlay-test-overlay-3"));
expect(screen.getByText("Speech")).toBeInTheDocument();
expect(screen.getByTestId("delete-overlay")).toBeInTheDocument();

fireEvent.click(screen.getByTestId("editor-surface"));
expect(screen.getByTestId("inspector-empty")).toBeInTheDocument();
Expand Down Expand Up @@ -204,6 +204,101 @@ describe("LetteringEditor", () => {
expect(el.style.height).toBe("100px");
});

it("adds overlay via toolbar button", () => {
render(
<LetteringEditor storyName="story" cut={makeCut()} onSave={vi.fn()} onClose={vi.fn()} />,
);
simulateImageLoad();

expect(screen.getByTestId("overlay-count")).toHaveTextContent("0 overlays");
fireEvent.click(screen.getByTestId("add-speech"));
expect(screen.getByTestId("overlay-count")).toHaveTextContent("1 overlays");
});

it("edits overlay text via inspector", () => {
render(
<LetteringEditor storyName="story" cut={makeCut()} onSave={vi.fn()} onClose={vi.fn()} />,
);
simulateImageLoad();

fireEvent.click(screen.getByTestId("add-narration"));
const overlayEl = document.querySelector("[data-testid^='overlay-overlay-']")!;
fireEvent.click(overlayEl);

const textInput = screen.getByTestId("inspector-text");
fireEvent.change(textInput, { target: { value: "The dawn broke." } });

expect(textInput).toHaveValue("The dawn broke.");
});

it("deletes overlay with double-click confirmation", () => {
render(
<LetteringEditor storyName="story" cut={makeCut()} onSave={vi.fn()} onClose={vi.fn()} />,
);
simulateImageLoad();

fireEvent.click(screen.getByTestId("add-sfx"));
expect(screen.getByTestId("overlay-count")).toHaveTextContent("1 overlays");

const overlayEl = document.querySelector("[data-testid^='overlay-overlay-']")!;
fireEvent.click(overlayEl);

const deleteBtn = screen.getByTestId("delete-overlay");
expect(deleteBtn).toHaveTextContent("Delete");
fireEvent.click(deleteBtn);
expect(deleteBtn).toHaveTextContent("Click again to delete");
fireEvent.click(deleteBtn);

expect(screen.getByTestId("overlay-count")).toHaveTextContent("0 overlays");
});

it("saves overlays via onSave callback", () => {
const onSave = vi.fn();
render(
<LetteringEditor storyName="story" cut={makeCut()} onSave={onSave} onClose={vi.fn()} />,
);
simulateImageLoad();

fireEvent.click(screen.getByTestId("add-speech"));
fireEvent.click(screen.getByText("Save"));

expect(onSave).toHaveBeenCalledWith(
expect.arrayContaining([expect.objectContaining({ type: "speech" })]),
);
});

it("shows tail anchor controls for speech overlay without tailAnchor field", () => {
const overlay: Overlay = {
id: "test-no-tail",
type: "speech",
x: 0.1,
y: 0.1,
width: 0.25,
height: 0.12,
text: "Hello",
speaker: "Mira",
};

render(
<LetteringEditor
storyName="story"
cut={makeCut({ overlays: [overlay] })}
onSave={vi.fn()}
onClose={vi.fn()}
/>,
);

simulateImageLoad();
fireEvent.click(screen.getByTestId("overlay-test-no-tail"));

const tailX = screen.getByTestId("inspector-tail-x") as HTMLInputElement;
const tailY = screen.getByTestId("inspector-tail-y") as HTMLInputElement;
expect(tailX).toBeInTheDocument();
expect(tailY).toBeInTheDocument();
expect(parseFloat(tailX.value)).toBe(0.5);
expect(parseFloat(tailY.value)).toBe(1.2);
});

it("calls onClose when Close button is clicked", () => {
const onClose = vi.fn();
render(
Expand Down
Loading
Loading