test: add frontend test suite with 54 tests
Build & Deploy / build-and-deploy (push) Successful in 23s
Build & Deploy / build-and-deploy (push) Successful in 23s
- Install vitest, @testing-library/svelte, jsdom - Configure vitest in vite.config.js with browser conditions for Svelte 5 - Pure function tests for api.js (artworkUrl, bundleUrl, setupBundleUrl) - Router tests (push, replace hash manipulation) - GameCard component tests (title, year, genre, cover, badge, click) - Library page tests (loading, empty, game grid, year/genre filters) - GameDetail page tests (loading, error, display, edit mode, badges) - Play page tests (loading, not-ready, unsupported, booting, error)
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/svelte";
|
||||
import GameDetail from "../GameDetail.svelte";
|
||||
|
||||
// Mock api.js
|
||||
vi.mock("../../lib/api.js", () => ({
|
||||
getGame: vi.fn(),
|
||||
deleteGame: vi.fn(),
|
||||
updateGame: vi.fn(),
|
||||
searchIGDB: vi.fn(),
|
||||
scrapeIGDB: vi.fn(),
|
||||
downloadGame: vi.fn(),
|
||||
bundleUrl: vi.fn(),
|
||||
setupBundleUrl: vi.fn(),
|
||||
artworkUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock router
|
||||
vi.mock("../../lib/router.js", () => ({
|
||||
push: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as api from "../../lib/api.js";
|
||||
|
||||
function makeGame(overrides = {}) {
|
||||
return {
|
||||
id: "game-1",
|
||||
title: "Commander Keen",
|
||||
year: 1991,
|
||||
genre: "Platformer",
|
||||
developer: "id Software",
|
||||
publisher: "Apogee",
|
||||
description: "A classic DOS platformer.",
|
||||
platform: "dos",
|
||||
ready: true,
|
||||
has_cover: false,
|
||||
has_setup: false,
|
||||
bundle_file: "commander-keen.zip",
|
||||
bundle_size: 512000,
|
||||
executables: ["keen.exe"],
|
||||
executable: null,
|
||||
videos: [],
|
||||
screenshots: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("GameDetail", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows loading state initially", () => {
|
||||
api.getGame.mockReturnValue(new Promise(() => {}));
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
expect(screen.getByText("Loading...")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows error when game is not found", async () => {
|
||||
api.getGame.mockRejectedValue(new Error("Game not found"));
|
||||
render(GameDetail, { props: { id: "missing" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("Game not found")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders game title", async () => {
|
||||
api.getGame.mockResolvedValue(makeGame());
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("Commander Keen")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders developer and publisher", async () => {
|
||||
api.getGame.mockResolvedValue(
|
||||
makeGame({ developer: "id Software", publisher: "Apogee" })
|
||||
);
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText(/id Software/)).toBeTruthy();
|
||||
expect(screen.getByText(/Apogee/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders year and genre as links", async () => {
|
||||
api.getGame.mockResolvedValue(
|
||||
makeGame({ year: 1991, genre: "Platformer" })
|
||||
);
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("1991").closest("a")).toBeTruthy();
|
||||
expect(screen.getByText("Platformer").closest("a")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows DOS badge for dos platform", async () => {
|
||||
api.getGame.mockResolvedValue(makeGame({ platform: "dos" }));
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("💾 DOS")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Windows badge for windows platform", async () => {
|
||||
api.getGame.mockResolvedValue(makeGame({ platform: "windows" }));
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText(/Windows 3\.1/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Play button for ready DOS games", async () => {
|
||||
api.getGame.mockResolvedValue(
|
||||
makeGame({ ready: true, platform: "dos" })
|
||||
);
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("▶ Play")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows disabled Play button for Windows games", async () => {
|
||||
api.getGame.mockResolvedValue(
|
||||
makeGame({ ready: true, platform: "windows" })
|
||||
);
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("▶ Unplayable")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Setup button when has_setup is true", async () => {
|
||||
api.getGame.mockResolvedValue(
|
||||
makeGame({ has_setup: true })
|
||||
);
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("🛠 Setup")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows Edit, Download, Delete buttons", async () => {
|
||||
api.getGame.mockResolvedValue(makeGame());
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("✏ Edit")).toBeTruthy();
|
||||
expect(screen.getByText("⬇ Download")).toBeTruthy();
|
||||
expect(screen.getByText("✕ Delete")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("enters edit mode when Edit button is clicked", async () => {
|
||||
api.getGame.mockResolvedValue(makeGame());
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("✏ Edit")).toBeTruthy();
|
||||
});
|
||||
screen.getByText("✏ Edit").click();
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("Save")).toBeTruthy();
|
||||
expect(screen.getByText("Cancel")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows cover placeholder when no cover", async () => {
|
||||
api.getGame.mockResolvedValue(makeGame({ has_cover: false }));
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("💾")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows description when provided", async () => {
|
||||
api.getGame.mockResolvedValue(
|
||||
makeGame({ description: "A classic DOS platformer." })
|
||||
);
|
||||
render(GameDetail, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("A classic DOS platformer.")
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/svelte";
|
||||
import Library from "../Library.svelte";
|
||||
|
||||
// Mock the API module that Library imports
|
||||
vi.mock("../../lib/api.js", () => ({
|
||||
listGames: vi.fn(),
|
||||
uploadGame: vi.fn(),
|
||||
searchIGDB: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as api from "../../lib/api.js";
|
||||
|
||||
function makeGame(overrides = {}) {
|
||||
return {
|
||||
id: "g1",
|
||||
title: "Test Game",
|
||||
year: 1995,
|
||||
genre: "Adventure",
|
||||
platform: "dos",
|
||||
has_cover: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Library", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
window.location.hash = "#/";
|
||||
});
|
||||
|
||||
it("shows loading state initially", () => {
|
||||
api.listGames.mockReturnValue(new Promise(() => {})); // never resolves
|
||||
render(Library);
|
||||
expect(screen.getByText("Scanning library...")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows empty state when no games", async () => {
|
||||
api.listGames.mockResolvedValue({ games: [] });
|
||||
render(Library);
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("No games yet")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders game cards when games load", async () => {
|
||||
api.listGames.mockResolvedValue({
|
||||
games: [
|
||||
makeGame({ id: "g1", title: "Game One" }),
|
||||
makeGame({ id: "g2", title: "Game Two" }),
|
||||
],
|
||||
});
|
||||
render(Library);
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("Game One")).toBeTruthy();
|
||||
expect(screen.getByText("Game Two")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows game count", async () => {
|
||||
api.listGames.mockResolvedValue({
|
||||
games: [
|
||||
makeGame({ id: "g1", title: "One" }),
|
||||
makeGame({ id: "g2", title: "Two" }),
|
||||
],
|
||||
});
|
||||
render(Library);
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("2 games")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows singular game count for one game", async () => {
|
||||
api.listGames.mockResolvedValue({
|
||||
games: [makeGame({ id: "g1", title: "Only" })],
|
||||
});
|
||||
render(Library);
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("1 game")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("filters", () => {
|
||||
it("shows filter options derived from loaded games", async () => {
|
||||
api.listGames.mockResolvedValue({
|
||||
games: [
|
||||
makeGame({ id: "g1", genre: "RPG", year: 1997 }),
|
||||
makeGame({ id: "g2", genre: "RPG", year: 1998 }),
|
||||
makeGame({ id: "g3", genre: "Adventure", year: 1995 }),
|
||||
],
|
||||
});
|
||||
render(Library);
|
||||
await vi.waitFor(() => {
|
||||
// Year labels appear both as filter options and in game cards
|
||||
expect(screen.getAllByText("1995").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("1997").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("1998").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("applies genre filter from props", async () => {
|
||||
api.listGames.mockResolvedValue({
|
||||
games: [
|
||||
makeGame({ id: "g1", title: "RPG Game", genre: "RPG" }),
|
||||
makeGame({ id: "g2", title: "Adventure Game", genre: "Adventure" }),
|
||||
],
|
||||
});
|
||||
render(Library, { props: { genreFilter: "RPG" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("RPG Game")).toBeTruthy();
|
||||
expect(screen.queryByText("Adventure Game")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("applies year filter from props", async () => {
|
||||
api.listGames.mockResolvedValue({
|
||||
games: [
|
||||
makeGame({ id: "g1", title: "Old Game", year: 1993 }),
|
||||
makeGame({ id: "g2", title: "New Game", year: 1997 }),
|
||||
],
|
||||
});
|
||||
render(Library, { props: { yearFilter: "1997" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("New Game")).toBeTruthy();
|
||||
expect(screen.queryByText("Old Game")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows filter-empty message when no filter options exist", async () => {
|
||||
api.listGames.mockResolvedValue({
|
||||
games: [
|
||||
makeGame({ id: "g1", genre: null, year: null }),
|
||||
makeGame({ id: "g2", genre: null, year: null }),
|
||||
],
|
||||
});
|
||||
render(Library);
|
||||
await vi.waitFor(() => {
|
||||
const dashes = screen.getAllByText("—");
|
||||
expect(dashes.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/svelte";
|
||||
import Play from "../Play.svelte";
|
||||
|
||||
// Mock the API module that Play imports
|
||||
vi.mock("../../lib/api.js", () => ({
|
||||
getGame: vi.fn(),
|
||||
bundleUrl: vi.fn(),
|
||||
setupBundleUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock router
|
||||
vi.mock("../../lib/router.js", () => ({
|
||||
push: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as api from "../../lib/api.js";
|
||||
|
||||
function makeGame(overrides = {}) {
|
||||
return {
|
||||
id: "game-1",
|
||||
title: "Commander Keen",
|
||||
platform: "dos",
|
||||
ready: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Play", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("shows loading state initially", () => {
|
||||
api.getGame.mockReturnValue(new Promise(() => {}));
|
||||
render(Play, { props: { id: "game-1" } });
|
||||
expect(screen.getByText("Loading game data...")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows not-ready state when game is not processed", async () => {
|
||||
api.getGame.mockResolvedValue(makeGame({ ready: false }));
|
||||
render(Play, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("Not ready")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows unsupported warning for Windows games", async () => {
|
||||
api.getGame.mockResolvedValue(
|
||||
makeGame({ platform: "windows", ready: true })
|
||||
);
|
||||
render(Play, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("Windows game")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error box when game fetch fails", async () => {
|
||||
api.getGame.mockRejectedValue(new Error("Game not found"));
|
||||
render(Play, { props: { id: "missing" } });
|
||||
// The error should appear in the overlay as well
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("⚠️")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows emulator booting state for ready DOS games", async () => {
|
||||
api.getGame.mockResolvedValue(
|
||||
makeGame({ platform: "dos", ready: true })
|
||||
);
|
||||
api.bundleUrl.mockReturnValue("/games/game-1.jsdos");
|
||||
render(Play, { props: { id: "game-1" } });
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByText("Starting emulator...")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user