aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/pages
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/pages')
-rw-r--r--src/client/pages/DeckCardsPage.test.tsx33
-rw-r--r--src/client/pages/DeckCardsPage.tsx17
-rw-r--r--src/client/pages/DeckDetailPage.test.tsx27
-rw-r--r--src/client/pages/DeckDetailPage.tsx4
-rw-r--r--src/client/pages/HomePage.test.tsx32
-rw-r--r--src/client/pages/HomePage.tsx11
-rw-r--r--src/client/pages/NoteTypesPage.test.tsx25
-rw-r--r--src/client/pages/NoteTypesPage.tsx14
-rw-r--r--src/client/pages/StudyPage.test.tsx24
-rw-r--r--src/client/pages/StudyPage.tsx4
10 files changed, 116 insertions, 75 deletions
diff --git a/src/client/pages/DeckCardsPage.test.tsx b/src/client/pages/DeckCardsPage.test.tsx
index d70da83..01a2420 100644
--- a/src/client/pages/DeckCardsPage.test.tsx
+++ b/src/client/pages/DeckCardsPage.test.tsx
@@ -1,20 +1,15 @@
/**
* @vitest-environment jsdom
*/
+import { QueryClient } from "@tanstack/query-core";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createStore, Provider } from "jotai";
+import { queryClientAtom } from "jotai-tanstack-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Route, Router } from "wouter";
import { memoryLocation } from "wouter/memory-location";
-import {
- authLoadingAtom,
- type Card,
- cardsByDeckAtomFamily,
- type Deck,
- deckByIdAtomFamily,
-} from "../atoms";
-import { clearAtomFamilyCaches } from "../atoms/utils";
+import { authLoadingAtom, type Card, type Deck } from "../atoms";
import { DeckCardsPage } from "./DeckCardsPage";
const mockDeckGet = vi.fn();
@@ -63,6 +58,14 @@ vi.mock("../api/client", () => ({
},
}));
+// Mock queryClient module so pages use our test queryClient
+let testQueryClient: QueryClient;
+vi.mock("../queryClient", () => ({
+ get queryClient() {
+ return testQueryClient;
+ },
+}));
+
import { ApiClientError, apiClient } from "../api/client";
const mockDeck = {
@@ -184,17 +187,18 @@ function renderWithProviders({
const { hook } = memoryLocation({ path, static: true });
const store = createStore();
store.set(authLoadingAtom, false);
+ store.set(queryClientAtom, testQueryClient);
// Extract deckId from path
const deckIdMatch = path.match(/\/decks\/([^/]+)/);
const deckId = deckIdMatch?.[1] ?? "deck-1";
- // Hydrate atoms if initial data provided
+ // Seed query cache if initial data provided
if (initialDeck !== undefined) {
- store.set(deckByIdAtomFamily(deckId), initialDeck);
+ testQueryClient.setQueryData(["decks", deckId], initialDeck);
}
if (initialCards !== undefined) {
- store.set(cardsByDeckAtomFamily(deckId), initialCards);
+ testQueryClient.setQueryData(["decks", deckId, "cards"], initialCards);
}
return render(
@@ -211,6 +215,11 @@ function renderWithProviders({
describe("DeckCardsPage", () => {
beforeEach(() => {
vi.clearAllMocks();
+ testQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: { staleTime: Number.POSITIVE_INFINITY, retry: false },
+ },
+ });
vi.mocked(apiClient.getTokens).mockReturnValue({
accessToken: "access-token",
refreshToken: "refresh-token",
@@ -238,7 +247,7 @@ describe("DeckCardsPage", () => {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
- clearAtomFamilyCaches();
+ testQueryClient.clear();
});
it("renders back link and deck name", () => {
diff --git a/src/client/pages/DeckCardsPage.tsx b/src/client/pages/DeckCardsPage.tsx
index f7fd3b7..73a83ae 100644
--- a/src/client/pages/DeckCardsPage.tsx
+++ b/src/client/pages/DeckCardsPage.tsx
@@ -11,7 +11,7 @@ import {
faXmark,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useAtomValue, useSetAtom } from "jotai";
+import { useAtomValue } from "jotai";
import {
Suspense,
useCallback,
@@ -19,7 +19,6 @@ import {
useMemo,
useRef,
useState,
- useTransition,
} from "react";
import { useDebouncedCallback } from "use-debounce";
import { Link, useParams } from "wouter";
@@ -32,6 +31,7 @@ import { EditNoteModal } from "../components/EditNoteModal";
import { ErrorBoundary } from "../components/ErrorBoundary";
import { ImportNotesModal } from "../components/ImportNotesModal";
import { LoadingSpinner } from "../components/LoadingSpinner";
+import { queryClient } from "../queryClient";
/** Combined type for display: note group */
type CardDisplayItem = { type: "note"; noteId: string; cards: Card[] };
@@ -173,7 +173,7 @@ function NoteGroupCard({
}
function DeckHeader({ deckId }: { deckId: string }) {
- const deck = useAtomValue(deckByIdAtomFamily(deckId));
+ const { data: deck } = useAtomValue(deckByIdAtomFamily(deckId));
return (
<div className="mb-8">
@@ -267,7 +267,7 @@ function CardList({
onDeleteNote: (noteId: string) => void;
onCreateNote: () => void;
}) {
- const cards = useAtomValue(cardsByDeckAtomFamily(deckId));
+ const { data: cards } = useAtomValue(cardsByDeckAtomFamily(deckId));
const [currentPage, setCurrentPage] = useState(0);
// Group cards by note for display, applying search filter
@@ -414,7 +414,7 @@ function CardsContent({
onEditNote: (noteId: string) => void;
onDeleteNote: (noteId: string) => void;
}) {
- const cards = useAtomValue(cardsByDeckAtomFamily(deckId));
+ const { data: cards } = useAtomValue(cardsByDeckAtomFamily(deckId));
const [searchInput, setSearchInput] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const debouncedSetQuery = useDebouncedCallback((value: string) => {
@@ -521,9 +521,6 @@ function CardsContent({
export function DeckCardsPage() {
const { deckId } = useParams<{ deckId: string }>();
- const [, startTransition] = useTransition();
-
- const reloadCards = useSetAtom(cardsByDeckAtomFamily(deckId || ""));
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
@@ -533,9 +530,7 @@ export function DeckCardsPage() {
const [deletingNoteId, setDeletingNoteId] = useState<string | null>(null);
const handleCardMutation = () => {
- startTransition(() => {
- reloadCards();
- });
+ queryClient.invalidateQueries({ queryKey: ["decks", deckId, "cards"] });
};
if (!deckId) {
diff --git a/src/client/pages/DeckDetailPage.test.tsx b/src/client/pages/DeckDetailPage.test.tsx
index 903edb7..41f42fd 100644
--- a/src/client/pages/DeckDetailPage.test.tsx
+++ b/src/client/pages/DeckDetailPage.test.tsx
@@ -1,19 +1,14 @@
/**
* @vitest-environment jsdom
*/
+import { QueryClient } from "@tanstack/query-core";
import { cleanup, render, screen } from "@testing-library/react";
import { createStore, Provider } from "jotai";
+import { queryClientAtom } from "jotai-tanstack-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Route, Router } from "wouter";
import { memoryLocation } from "wouter/memory-location";
-import {
- authLoadingAtom,
- type Card,
- cardsByDeckAtomFamily,
- type Deck,
- deckByIdAtomFamily,
-} from "../atoms";
-import { clearAtomFamilyCaches } from "../atoms/utils";
+import { authLoadingAtom, type Card, type Deck } from "../atoms";
import { DeckDetailPage } from "./DeckDetailPage";
const mockDeckGet = vi.fn();
@@ -58,6 +53,8 @@ vi.mock("../api/client", () => ({
import { apiClient } from "../api/client";
+let testQueryClient: QueryClient;
+
const mockDeck = {
id: "deck-1",
name: "Japanese Vocabulary",
@@ -127,17 +124,18 @@ function renderWithProviders({
const { hook } = memoryLocation({ path, static: true });
const store = createStore();
store.set(authLoadingAtom, false);
+ store.set(queryClientAtom, testQueryClient);
// Extract deckId from path
const deckIdMatch = path.match(/\/decks\/([^/]+)/);
const deckId = deckIdMatch?.[1] ?? "deck-1";
- // Hydrate atoms if initial data provided
+ // Seed query cache if initial data provided
if (initialDeck !== undefined) {
- store.set(deckByIdAtomFamily(deckId), initialDeck);
+ testQueryClient.setQueryData(["decks", deckId], initialDeck);
}
if (initialCards !== undefined) {
- store.set(cardsByDeckAtomFamily(deckId), initialCards);
+ testQueryClient.setQueryData(["decks", deckId, "cards"], initialCards);
}
return render(
@@ -154,6 +152,11 @@ function renderWithProviders({
describe("DeckDetailPage", () => {
beforeEach(() => {
vi.clearAllMocks();
+ testQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: { staleTime: Number.POSITIVE_INFINITY, retry: false },
+ },
+ });
vi.mocked(apiClient.getTokens).mockReturnValue({
accessToken: "access-token",
refreshToken: "refresh-token",
@@ -180,7 +183,7 @@ describe("DeckDetailPage", () => {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
- clearAtomFamilyCaches();
+ testQueryClient.clear();
});
it("renders back link and deck name", () => {
diff --git a/src/client/pages/DeckDetailPage.tsx b/src/client/pages/DeckDetailPage.tsx
index d39f063..be4dc90 100644
--- a/src/client/pages/DeckDetailPage.tsx
+++ b/src/client/pages/DeckDetailPage.tsx
@@ -12,7 +12,7 @@ import { ErrorBoundary } from "../components/ErrorBoundary";
import { LoadingSpinner } from "../components/LoadingSpinner";
function DeckHeader({ deckId }: { deckId: string }) {
- const deck = useAtomValue(deckByIdAtomFamily(deckId));
+ const { data: deck } = useAtomValue(deckByIdAtomFamily(deckId));
return (
<div className="mb-8">
@@ -25,7 +25,7 @@ function DeckHeader({ deckId }: { deckId: string }) {
}
function DeckStats({ deckId }: { deckId: string }) {
- const cards = useAtomValue(cardsByDeckAtomFamily(deckId));
+ const { data: cards } = useAtomValue(cardsByDeckAtomFamily(deckId));
// Count cards due today
const now = new Date();
diff --git a/src/client/pages/HomePage.test.tsx b/src/client/pages/HomePage.test.tsx
index 8946fcf..8b17506 100644
--- a/src/client/pages/HomePage.test.tsx
+++ b/src/client/pages/HomePage.test.tsx
@@ -2,15 +2,16 @@
* @vitest-environment jsdom
*/
import "fake-indexeddb/auto";
+import { QueryClient } from "@tanstack/query-core";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createStore, Provider } from "jotai";
+import { queryClientAtom } from "jotai-tanstack-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Router } from "wouter";
import { memoryLocation } from "wouter/memory-location";
import { apiClient } from "../api/client";
-import { authLoadingAtom, type Deck, decksAtom } from "../atoms";
-import { clearAtomFamilyCaches } from "../atoms/utils";
+import { authLoadingAtom, type Deck } from "../atoms";
import { HomePage } from "./HomePage";
const mockDeckPut = vi.fn();
@@ -51,6 +52,14 @@ vi.mock("../api/client", () => ({
},
}));
+// Mock queryClient module so pages use our test queryClient
+let testQueryClient: QueryClient;
+vi.mock("../queryClient", () => ({
+ get queryClient() {
+ return testQueryClient;
+ },
+}));
+
// Mock fetch globally for Edit/Delete modals
const mockFetch = vi.fn();
global.fetch = mockFetch;
@@ -109,10 +118,11 @@ function renderWithProviders({
const { hook } = memoryLocation({ path });
const store = createStore();
store.set(authLoadingAtom, false);
+ store.set(queryClientAtom, testQueryClient);
- // If initialDecks provided, hydrate the atom to skip Suspense
+ // If initialDecks provided, seed query cache to skip Suspense
if (initialDecks !== undefined) {
- store.set(decksAtom, initialDecks);
+ testQueryClient.setQueryData(["decks"], initialDecks);
}
return render(
@@ -127,7 +137,11 @@ function renderWithProviders({
describe("HomePage", () => {
beforeEach(() => {
vi.clearAllMocks();
- clearAtomFamilyCaches();
+ testQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: { staleTime: Number.POSITIVE_INFINITY, retry: false },
+ },
+ });
vi.mocked(apiClient.getTokens).mockReturnValue({
accessToken: "access-token",
refreshToken: "refresh-token",
@@ -152,7 +166,7 @@ describe("HomePage", () => {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
- clearAtomFamilyCaches();
+ testQueryClient.clear();
});
it("renders page title and logout button", () => {
@@ -260,6 +274,12 @@ describe("HomePage", () => {
});
it("passes auth header when fetching decks", async () => {
+ testQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: { staleTime: 0, retry: false },
+ },
+ });
+
vi.mocked(apiClient.rpc.api.decks.$get).mockResolvedValue(
mockResponse({
ok: true,
diff --git a/src/client/pages/HomePage.tsx b/src/client/pages/HomePage.tsx
index ad6ece4..d04ca08 100644
--- a/src/client/pages/HomePage.tsx
+++ b/src/client/pages/HomePage.tsx
@@ -7,7 +7,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useAtomValue, useSetAtom } from "jotai";
-import { Suspense, useState, useTransition } from "react";
+import { Suspense, useState } from "react";
import { Link } from "wouter";
import { type Deck, decksAtom, logoutAtom } from "../atoms";
import { CreateDeckModal } from "../components/CreateDeckModal";
@@ -17,6 +17,7 @@ import { ErrorBoundary } from "../components/ErrorBoundary";
import { LoadingSpinner } from "../components/LoadingSpinner";
import { SyncButton } from "../components/SyncButton";
import { SyncStatusIndicator } from "../components/SyncStatusIndicator";
+import { queryClient } from "../queryClient";
function DeckList({
onEditDeck,
@@ -25,7 +26,7 @@ function DeckList({
onEditDeck: (deck: Deck) => void;
onDeleteDeck: (deck: Deck) => void;
}) {
- const decks = useAtomValue(decksAtom);
+ const { data: decks } = useAtomValue(decksAtom);
if (decks.length === 0) {
return (
@@ -113,17 +114,13 @@ function DeckList({
export function HomePage() {
const logout = useSetAtom(logoutAtom);
- const reloadDecks = useSetAtom(decksAtom);
- const [, startTransition] = useTransition();
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingDeck, setEditingDeck] = useState<Deck | null>(null);
const [deletingDeck, setDeletingDeck] = useState<Deck | null>(null);
const handleDeckMutation = () => {
- startTransition(() => {
- reloadDecks();
- });
+ queryClient.invalidateQueries({ queryKey: ["decks"] });
};
return (
diff --git a/src/client/pages/NoteTypesPage.test.tsx b/src/client/pages/NoteTypesPage.test.tsx
index 8bacd0f..ac68e35 100644
--- a/src/client/pages/NoteTypesPage.test.tsx
+++ b/src/client/pages/NoteTypesPage.test.tsx
@@ -2,14 +2,15 @@
* @vitest-environment jsdom
*/
import "fake-indexeddb/auto";
+import { QueryClient } from "@tanstack/query-core";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createStore, Provider } from "jotai";
+import { queryClientAtom } from "jotai-tanstack-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Router } from "wouter";
import { memoryLocation } from "wouter/memory-location";
-import { authLoadingAtom, type NoteType, noteTypesAtom } from "../atoms";
-import { clearAtomFamilyCaches } from "../atoms/utils";
+import { authLoadingAtom, type NoteType } from "../atoms";
import { NoteTypesPage } from "./NoteTypesPage";
interface RenderOptions {
@@ -59,6 +60,14 @@ vi.mock("../api/client", () => ({
},
}));
+// Mock queryClient module so pages use our test queryClient
+let testQueryClient: QueryClient;
+vi.mock("../queryClient", () => ({
+ get queryClient() {
+ return testQueryClient;
+ },
+}));
+
import { ApiClientError, apiClient } from "../api/client";
const mockNoteTypes = [
@@ -89,10 +98,11 @@ function renderWithProviders({
const { hook } = memoryLocation({ path });
const store = createStore();
store.set(authLoadingAtom, false);
+ store.set(queryClientAtom, testQueryClient);
- // Hydrate atom if initial data provided
+ // Seed query cache if initial data provided
if (initialNoteTypes !== undefined) {
- store.set(noteTypesAtom, initialNoteTypes);
+ testQueryClient.setQueryData(["noteTypes"], initialNoteTypes);
}
return render(
@@ -107,6 +117,11 @@ function renderWithProviders({
describe("NoteTypesPage", () => {
beforeEach(() => {
vi.clearAllMocks();
+ testQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: { staleTime: Number.POSITIVE_INFINITY, retry: false },
+ },
+ });
vi.mocked(apiClient.getTokens).mockReturnValue({
accessToken: "access-token",
refreshToken: "refresh-token",
@@ -138,7 +153,7 @@ describe("NoteTypesPage", () => {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
- clearAtomFamilyCaches();
+ testQueryClient.clear();
});
it("renders page title and back button", () => {
diff --git a/src/client/pages/NoteTypesPage.tsx b/src/client/pages/NoteTypesPage.tsx
index 8e742a7..d838e5b 100644
--- a/src/client/pages/NoteTypesPage.tsx
+++ b/src/client/pages/NoteTypesPage.tsx
@@ -7,8 +7,8 @@ import {
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useAtomValue, useSetAtom } from "jotai";
-import { Suspense, useState, useTransition } from "react";
+import { useAtomValue } from "jotai";
+import { Suspense, useState } from "react";
import { Link } from "wouter";
import { type NoteType, noteTypesAtom } from "../atoms";
import { CreateNoteTypeModal } from "../components/CreateNoteTypeModal";
@@ -16,6 +16,7 @@ import { DeleteNoteTypeModal } from "../components/DeleteNoteTypeModal";
import { ErrorBoundary } from "../components/ErrorBoundary";
import { LoadingSpinner } from "../components/LoadingSpinner";
import { NoteTypeEditor } from "../components/NoteTypeEditor";
+import { queryClient } from "../queryClient";
function NoteTypeList({
onEditNoteType,
@@ -24,7 +25,7 @@ function NoteTypeList({
onEditNoteType: (id: string) => void;
onDeleteNoteType: (noteType: NoteType) => void;
}) {
- const noteTypes = useAtomValue(noteTypesAtom);
+ const { data: noteTypes } = useAtomValue(noteTypesAtom);
if (noteTypes.length === 0) {
return (
@@ -114,9 +115,6 @@ function NoteTypeList({
}
export function NoteTypesPage() {
- const reloadNoteTypes = useSetAtom(noteTypesAtom);
- const [, startTransition] = useTransition();
-
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [editingNoteTypeId, setEditingNoteTypeId] = useState<string | null>(
null,
@@ -126,9 +124,7 @@ export function NoteTypesPage() {
);
const handleNoteTypeMutation = () => {
- startTransition(() => {
- reloadNoteTypes();
- });
+ queryClient.invalidateQueries({ queryKey: ["noteTypes"] });
};
return (
diff --git a/src/client/pages/StudyPage.test.tsx b/src/client/pages/StudyPage.test.tsx
index a366f35..dfadea9 100644
--- a/src/client/pages/StudyPage.test.tsx
+++ b/src/client/pages/StudyPage.test.tsx
@@ -1,19 +1,15 @@
/**
* @vitest-environment jsdom
*/
+import { QueryClient } from "@tanstack/query-core";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createStore, Provider } from "jotai";
+import { queryClientAtom } from "jotai-tanstack-query";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Route, Router } from "wouter";
import { memoryLocation } from "wouter/memory-location";
-import {
- authLoadingAtom,
- type StudyCard,
- type StudyData,
- studyDataAtomFamily,
-} from "../atoms";
-import { clearAtomFamilyCaches } from "../atoms/utils";
+import { authLoadingAtom, type StudyCard, type StudyData } from "../atoms";
import { StudyPage } from "./StudyPage";
interface RenderOptions {
@@ -72,6 +68,8 @@ vi.mock("../api/client", () => ({
import { ApiClientError, apiClient } from "../api/client";
+let testQueryClient: QueryClient;
+
const mockDeck = {
id: "deck-1",
name: "Japanese Vocabulary",
@@ -121,14 +119,15 @@ function renderWithProviders({
const { hook } = memoryLocation({ path, static: true });
const store = createStore();
store.set(authLoadingAtom, false);
+ store.set(queryClientAtom, testQueryClient);
// Extract deckId from path
const deckIdMatch = path.match(/\/decks\/([^/]+)/);
const deckId = deckIdMatch?.[1] ?? "deck-1";
- // Hydrate atom if initial data provided
+ // Seed query cache if initial data provided
if (initialStudyData !== undefined) {
- store.set(studyDataAtomFamily(deckId), initialStudyData);
+ testQueryClient.setQueryData(["decks", deckId, "study"], initialStudyData);
}
return render(
@@ -145,6 +144,11 @@ function renderWithProviders({
describe("StudyPage", () => {
beforeEach(() => {
vi.clearAllMocks();
+ testQueryClient = new QueryClient({
+ defaultOptions: {
+ queries: { staleTime: Number.POSITIVE_INFINITY, retry: false },
+ },
+ });
vi.mocked(apiClient.getTokens).mockReturnValue({
accessToken: "access-token",
refreshToken: "refresh-token",
@@ -161,7 +165,7 @@ describe("StudyPage", () => {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
- clearAtomFamilyCaches();
+ testQueryClient.clear();
});
describe("Loading and Initial State", () => {
diff --git a/src/client/pages/StudyPage.tsx b/src/client/pages/StudyPage.tsx
index cec11d3..dd82b27 100644
--- a/src/client/pages/StudyPage.tsx
+++ b/src/client/pages/StudyPage.tsx
@@ -37,7 +37,9 @@ const RatingStyles: Record<Rating, string> = {
};
function StudySession({ deckId }: { deckId: string }) {
- const { deck, cards } = useAtomValue(studyDataAtomFamily(deckId));
+ const {
+ data: { deck, cards },
+ } = useAtomValue(studyDataAtomFamily(deckId));
// Session state (kept as useState - transient UI state)
const [currentIndex, setCurrentIndex] = useState(0);