/** * @file Pets.test.tsx * @description The mascot's custom pets: `PetSprite` plays the spritesheet * row of Sagi's mood and steps through its frames (a still image bobs instead), * and `PetMenu` hides the mascot, sizes it, picks Sagi / a featured pet * (downloading it on the first pick) / an own pet, deletes an own pet (falling * back to Sagi when it was the one worn), and uploads an image. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); const own = { id: "p-aaaaaaaaaaaa", displayName: "My cat", description: "", kind: "image", source: "upload", }; return { ...actual, api: { pets: { spriteUrl: (id: string) => `/api/pets/${id}/sprite`, list: vi.fn(async () => ({ pets: [own], featured: [ { id: "goku", displayName: "Goku", by: "hackminit", installed: false, page: "" }, ], sheet: { width: 1536, height: 1872, cellWidth: 192, cellHeight: 208 }, })), installFeatured: vi.fn(async () => ({ pet: { id: "cp-goku", displayName: "Goku", description: "", kind: "codex", source: "featured", featuredId: "goku", }, })), upload: vi.fn(async () => ({ pet: { id: "p-bbbbbbbbbbbb", displayName: "Dog", description: "", kind: "image", source: "upload", }, })), importBundle: vi.fn(), remove: vi.fn(async () => ({ ok: true })), }, }, }; }); import { api } from "../../../lib/api"; import { PetSprite } from "../PetSprite"; import { PetMenu } from "../PetMenu"; import { sagiPrefs } from "../prefs"; describe("PetSprite", () => { beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); it("plays the row of the mood and steps through its frames", () => { const { rerender } = render( {}} /> ); const sprite = screen.getByTestId("pet-sprite"); // 104 px tall = half a 208 px cell: row 7 (running) starts 7 × 104 px down. expect(sprite.dataset.row).toBe("7"); expect(sprite.style.backgroundPosition).toBe("0px -728px"); act(() => vi.advanceTimersByTime(130)); expect(sprite.style.backgroundPosition).toBe("-96px -728px"); rerender( {}} /> ); expect(screen.getByTestId("pet-sprite").dataset.row).toBe("5"); }); it("holds the first frame under reduced motion", () => { render( {}} /> ); act(() => vi.advanceTimersByTime(1000)); expect(screen.getByTestId("pet-sprite").style.backgroundPosition).toBe("0px 0px"); }); it("shows an uploaded image still, and reports one that will not load", () => { const broken = vi.fn(); const { container } = render( ); const img = container.querySelector("img") as HTMLImageElement; expect(img.getAttribute("src")).toBe("/api/pets/p-aaaaaaaaaaaa/sprite"); fireEvent.error(img); expect(broken).toHaveBeenCalled(); }); }); describe("PetMenu", () => { beforeEach(() => { localStorage.clear(); vi.clearAllMocks(); }); const renderMenu = () => render( {}} onSettings={() => {}} />); it("hides the mascot and sizes it", () => { renderMenu(); fireEvent.change(screen.getByTestId("pet-size"), { target: { value: "250" } }); expect(sagiPrefs.getSize()).toBe(250); fireEvent.click(screen.getByText("Hide pet")); expect(sagiPrefs.getEnabled()).toBe(false); }); it("downloads a featured pet on its first pick, then wears it", async () => { renderMenu(); fireEvent.click(screen.getByTestId("pet-choose")); fireEvent.click(await screen.findByTestId("pet-featured-goku")); await waitFor(() => expect(sagiPrefs.getPet()).toEqual({ id: "cp-goku", kind: "codex" })); expect(api.pets.installFeatured).toHaveBeenCalledWith("goku"); }); it("deleting the pet being worn puts Sagi back", async () => { sagiPrefs.setPet({ id: "p-aaaaaaaaaaaa", kind: "image" }); renderMenu(); fireEvent.click(screen.getByTestId("pet-choose")); fireEvent.click(await screen.findByTestId("pet-delete-p-aaaaaaaaaaaa")); await waitFor(() => expect(sagiPrefs.getPet()).toEqual({ id: "sagi" })); expect(api.pets.remove).toHaveBeenCalledWith("p-aaaaaaaaaaaa"); }); it("uploads an image and wears it", async () => { renderMenu(); const file = new File([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], "Dog.png", { type: "image/png", }); fireEvent.change(screen.getByTestId("pet-image-input"), { target: { files: [file] } }); await waitFor(() => expect(sagiPrefs.getPet()).toEqual({ id: "p-bbbbbbbbbbbb", kind: "image" }) ); expect(api.pets.upload).toHaveBeenCalledWith(file); }); });