fork
Configure Feed
Select the types of activity you want to include in your feed.
learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
fork
Configure Feed
Select the types of activity you want to include in your feed.
1import { api } from "$lib/api";
2import { toast } from "$lib/toast";
3import { cleanup, fireEvent, render, screen, waitFor, within } from "@solidjs/testing-library";
4import { JSX } from "solid-js";
5import { afterEach, describe, expect, it, vi } from "vitest";
6import DeckView from "../DeckView";
7
8const { mockNavigate } = vi.hoisted(() => ({ mockNavigate: vi.fn() }));
9
10vi.mock(
11 "$lib/api",
12 () => ({
13 api: {
14 getDeck: vi.fn(),
15 getDeckCards: vi.fn(),
16 forkDeck: vi.fn(),
17 getComments: vi.fn(),
18 addComment: vi.fn(),
19 getDueCards: vi.fn(),
20 submitReview: vi.fn(),
21 },
22 }),
23);
24
25vi.mock("$lib/toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
26
27vi.mock(
28 "@solidjs/router",
29 () => ({
30 useParams: () => ({ id: "123" }),
31 useNavigate: () => mockNavigate,
32 A: (props: { href: string; children: JSX.Element }) => <a href={props.href}>{props.children}</a>,
33 }),
34);
35
36describe("DeckView", () => {
37 afterEach(() => {
38 cleanup();
39 vi.clearAllMocks();
40 });
41
42 const mockDeck = {
43 id: "123",
44 title: "Test Deck",
45 description: "A test deck",
46 tags: ["test"],
47 visibility: { type: "Public" },
48 owner_did: "did:test",
49 };
50
51 const mockCards = [{ id: "c1", front: "Front 1", back: "Back 1" }, { id: "c2", front: "Front 2", back: "Back 2" }];
52
53 it("renders deck details and cards", async () => {
54 vi.mocked(api.getDeck).mockResolvedValue(
55 { ok: true, json: () => Promise.resolve(mockDeck) } as unknown as Response,
56 );
57 vi.mocked(api.getDeckCards).mockResolvedValue(
58 { ok: true, json: () => Promise.resolve(mockCards) } as unknown as Response,
59 );
60 vi.mocked(api.getDueCards).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
61 vi.mocked(api.getComments).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
62
63 render(() => <DeckView />);
64
65 await waitFor(() => expect(screen.getByText("Test Deck")).toBeInTheDocument());
66 expect(screen.getByText("A test deck")).toBeInTheDocument();
67 expect(screen.getByText("#test")).toBeInTheDocument();
68 expect(screen.getByText("Front 1")).toBeInTheDocument();
69 });
70
71 it("handles deck fork flow successfully", async () => {
72 vi.mocked(api.getDeck).mockResolvedValue(
73 { ok: true, json: () => Promise.resolve(mockDeck) } as unknown as Response,
74 );
75 vi.mocked(api.getDeckCards).mockResolvedValue(
76 { ok: true, json: () => Promise.resolve(mockCards) } as unknown as Response,
77 );
78 vi.mocked(api.getDueCards).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
79 vi.mocked(api.forkDeck).mockResolvedValue(
80 { ok: true, json: () => Promise.resolve({ id: "456" }) } as unknown as Response,
81 );
82 vi.mocked(api.getComments).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
83
84 render(() => <DeckView />);
85
86 await waitFor(() => expect(screen.getByText("Test Deck")).toBeInTheDocument());
87
88 const forkButton = screen.getByRole("button", { name: /Fork Deck/i });
89 fireEvent.click(forkButton);
90
91 const dialog = await screen.findByRole("dialog");
92 expect(within(dialog).getByText(/Are you sure you want to fork/)).toBeInTheDocument();
93
94 const confirmButton = within(dialog).getByRole("button", { name: /Fork Deck/i });
95 fireEvent.click(confirmButton);
96
97 await waitFor(() => {
98 expect(api.forkDeck).toHaveBeenCalledWith("123");
99 expect(toast.success).toHaveBeenCalledWith("Deck forked successfully!");
100 expect(mockNavigate).toHaveBeenCalledWith("/decks/456");
101 });
102 });
103
104 it("handles deck fork failure", async () => {
105 vi.mocked(api.getDeck).mockResolvedValue(
106 { ok: true, json: () => Promise.resolve(mockDeck) } as unknown as Response,
107 );
108 vi.mocked(api.getDeckCards).mockResolvedValue(
109 { ok: true, json: () => Promise.resolve(mockCards) } as unknown as Response,
110 );
111 vi.mocked(api.getDueCards).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
112 vi.mocked(api.forkDeck).mockResolvedValue({ ok: false } as unknown as Response);
113 vi.mocked(api.getComments).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
114
115 render(() => <DeckView />);
116
117 await waitFor(() => expect(screen.getByText("Test Deck")).toBeInTheDocument());
118
119 const forkButton = screen.getByRole("button", { name: /Fork Deck/i });
120 fireEvent.click(forkButton);
121
122 const dialog = await screen.findByRole("dialog");
123 const confirmButton = within(dialog).getByRole("button", { name: /Fork Deck/i });
124 fireEvent.click(confirmButton);
125
126 await waitFor(() => {
127 expect(api.forkDeck).toHaveBeenCalledWith("123");
128 expect(toast.error).toHaveBeenCalledWith("Failed to fork deck.");
129 expect(mockNavigate).not.toHaveBeenCalled();
130 });
131 });
132
133 it("renders not found state when deck returns error", async () => {
134 vi.mocked(api.getDeck).mockResolvedValue({ ok: false } as unknown as Response);
135 vi.mocked(api.getDueCards).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
136 render(() => <DeckView />);
137 await waitFor(() => expect(screen.getByText(/Deck not found/i)).toBeInTheDocument());
138 });
139 it("renders study button with due cards count", async () => {
140 vi.mocked(api.getDeck).mockResolvedValue(
141 { ok: true, json: () => Promise.resolve(mockDeck) } as unknown as Response,
142 );
143 vi.mocked(api.getDeckCards).mockResolvedValue(
144 { ok: true, json: () => Promise.resolve(mockCards) } as unknown as Response,
145 );
146 vi.mocked(api.getDueCards).mockResolvedValue(
147 {
148 ok: true,
149 json: () => Promise.resolve([{ review_id: "r1", card_id: "c1", deck_id: "123", front: "F", back: "B" }]),
150 } as unknown as Response,
151 );
152 vi.mocked(api.getComments).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
153
154 render(() => <DeckView />);
155
156 await waitFor(() => expect(screen.getByText("Test Deck")).toBeInTheDocument());
157
158 const studyButton = await screen.findByRole("button", { name: /Study Deck \(1 due\)/i });
159 expect(studyButton).toBeInTheDocument();
160 expect(studyButton).not.toBeDisabled();
161 });
162
163 it("enters study mode when study button is clicked", async () => {
164 vi.mocked(api.getDeck).mockResolvedValue(
165 { ok: true, json: () => Promise.resolve(mockDeck) } as unknown as Response,
166 );
167 vi.mocked(api.getDeckCards).mockResolvedValue(
168 { ok: true, json: () => Promise.resolve(mockCards) } as unknown as Response,
169 );
170 vi.mocked(api.getDueCards).mockResolvedValue(
171 {
172 ok: true,
173 json: () =>
174 Promise.resolve([{
175 review_id: "r1",
176 card_id: "c1",
177 deck_id: "123",
178 front: "Study Front",
179 back: "Study Back",
180 deck_title: "Test Deck",
181 hints: [],
182 }]),
183 } as unknown as Response,
184 );
185 vi.mocked(api.getComments).mockResolvedValue({ ok: true, json: () => Promise.resolve([]) } as unknown as Response);
186
187 render(() => <DeckView />);
188
189 await waitFor(() => expect(screen.getByText("Test Deck")).toBeInTheDocument());
190
191 const studyButton = await screen.findByRole("button", { name: /Study Deck \(1 due\)/i });
192 fireEvent.click(studyButton);
193
194 await waitFor(() => expect(screen.getByText("Card 1 of 1")).toBeInTheDocument());
195 expect(screen.getByText("Study Front")).toBeInTheDocument();
196 });
197});