Forráskód Böngészése

test: add Phase 3 smoke tests with Vitest

Set up vitest.config.ts and create smoke tests for genre filter hook,
query key consistency across movie hooks, and realtime cache update logic.
Query key tests intentionally assert "group-movies" to document the bug
in hooks still using "movies" as the key prefix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User 2 hónapja
szülő
commit
3963a13e13

+ 47 - 0
src/__tests__/movies/genre-filter.test.ts

@@ -0,0 +1,47 @@
+import { renderHook, act } from "@testing-library/react";
+import { useGenreFilter } from "@/hooks/use-genre-filter";
+
+describe("useGenreFilter", () => {
+  it("initial state has selectedGenre === null", () => {
+    const { result } = renderHook(() => useGenreFilter());
+    expect(result.current.selectedGenre).toBeNull();
+  });
+
+  it("filterByGenre sets selectedGenre", () => {
+    const { result } = renderHook(() => useGenreFilter());
+
+    act(() => {
+      result.current.filterByGenre("Action");
+    });
+
+    expect(result.current.selectedGenre).toBe("Action");
+  });
+
+  it("filterByGenre toggles back to null when called with same genre", () => {
+    const { result } = renderHook(() => useGenreFilter());
+
+    act(() => {
+      result.current.filterByGenre("Action");
+    });
+    expect(result.current.selectedGenre).toBe("Action");
+
+    act(() => {
+      result.current.filterByGenre("Action");
+    });
+    expect(result.current.selectedGenre).toBeNull();
+  });
+
+  it("clearGenreFilter resets to null", () => {
+    const { result } = renderHook(() => useGenreFilter());
+
+    act(() => {
+      result.current.filterByGenre("Comedy");
+    });
+    expect(result.current.selectedGenre).toBe("Comedy");
+
+    act(() => {
+      result.current.clearGenreFilter();
+    });
+    expect(result.current.selectedGenre).toBeNull();
+  });
+});

+ 31 - 0
src/__tests__/movies/query-keys.test.ts

@@ -0,0 +1,31 @@
+import { readFileSync } from "fs";
+import path from "path";
+
+describe("Query key consistency", () => {
+  const hooksDir = path.resolve(__dirname, "../../hooks");
+
+  it("useGroupMovies uses group-movies key", () => {
+    const src = readFileSync(path.join(hooksDir, "use-group-movies.ts"), "utf-8");
+    expect(src).toContain('"group-movies"');
+  });
+
+  it("useAddMovie invalidates group-movies key", () => {
+    const src = readFileSync(path.join(hooksDir, "use-add-movie.ts"), "utf-8");
+    expect(src).toContain('"group-movies"');
+  });
+
+  it("useDeleteMovie invalidates group-movies key", () => {
+    const src = readFileSync(path.join(hooksDir, "use-delete-movie.ts"), "utf-8");
+    expect(src).toContain('"group-movies"');
+  });
+
+  it("useToggleWatched invalidates group-movies key", () => {
+    const src = readFileSync(path.join(hooksDir, "use-toggle-watched.ts"), "utf-8");
+    expect(src).toContain('"group-movies"');
+  });
+
+  it("useRealtimeMovies uses group-movies key", () => {
+    const src = readFileSync(path.join(hooksDir, "use-realtime-movies.ts"), "utf-8");
+    expect(src).toContain('"group-movies"');
+  });
+});

+ 40 - 0
src/__tests__/movies/realtime-cache.test.ts

@@ -0,0 +1,40 @@
+import { readFileSync } from "fs";
+import path from "path";
+
+describe("Realtime movies cache update logic", () => {
+  const src = readFileSync(
+    path.resolve(__dirname, "../../hooks/use-realtime-movies.ts"),
+    "utf-8",
+  );
+
+  it("handles INSERT events with duplicate guard", () => {
+    expect(src).toContain("INSERT");
+    // Verify duplicate check exists to prevent optimistic update collisions
+    expect(src).toMatch(/old\.some\(.*\.id\s*===\s*newMovie\.id/);
+  });
+
+  it("handles UPDATE events with map replacement", () => {
+    expect(src).toContain("UPDATE");
+    // Verify map-based replacement pattern
+    expect(src).toMatch(/old\.map\(/);
+  });
+
+  it("handles DELETE events with filter removal", () => {
+    expect(src).toContain("DELETE");
+    // Verify filter-based removal pattern
+    expect(src).toMatch(/old\.filter\(/);
+  });
+
+  it("guards against null old data in setQueryData callbacks", () => {
+    // Each callback should handle the case where old cache data is undefined/null
+    const setQueryDataBlocks = src.match(/setQueryData.*?\(.*?old.*?\)/gs);
+    expect(setQueryDataBlocks).not.toBeNull();
+    expect(src).toMatch(/if\s*\(\s*!old\s*\)/);
+  });
+
+  it("uses moviesQueryKey helper for consistent key generation", () => {
+    expect(src).toMatch(/function\s+moviesQueryKey/);
+    // Verify the helper is used in the callback
+    expect(src).toMatch(/const\s+key\s*=\s*moviesQueryKey\(/);
+  });
+});

+ 14 - 0
vitest.config.ts

@@ -0,0 +1,14 @@
+import { defineConfig } from "vitest/config";
+import path from "path";
+
+export default defineConfig({
+  test: {
+    environment: "jsdom",
+    globals: true,
+  },
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, "src"),
+    },
+  },
+});