this repo has no description
1import { beforeEach, describe, expect, it } from "vitest";
2import { fireEvent, render, screen, waitFor } from "@testing-library/svelte";
3import Login from "../routes/Login.svelte";
4import {
5 clearMocks,
6 jsonResponse,
7 mockData,
8 mockEndpoint,
9 setupFetchMock,
10} from "./mocks.ts";
11import { _testSetState, type SavedAccount } from "../lib/auth.svelte.ts";
12import {
13 unsafeAsAccessToken,
14 unsafeAsDid,
15 unsafeAsHandle,
16 unsafeAsRefreshToken,
17} from "../lib/types/branded.ts";
18import { getToasts } from "../lib/toast.svelte.ts";
19
20describe("Login", () => {
21 beforeEach(() => {
22 clearMocks();
23 setupFetchMock();
24 mockEndpoint(
25 "/oauth/par",
26 () => jsonResponse({ request_uri: "urn:mock:request" }),
27 );
28 });
29
30 describe("initial render with no saved accounts", () => {
31 beforeEach(() => {
32 _testSetState({
33 session: null,
34 loading: false,
35 error: null,
36 savedAccounts: [],
37 });
38 });
39
40 it("renders login page with title and OAuth button", async () => {
41 render(Login);
42 await waitFor(() => {
43 expect(screen.getByRole("heading", { name: /sign in/i }))
44 .toBeInTheDocument();
45 expect(screen.getByRole("button", { name: /sign in/i }))
46 .toBeInTheDocument();
47 });
48 });
49
50 it("shows create account link", async () => {
51 render(Login);
52 await waitFor(() => {
53 expect(screen.getByText(/don't have an account/i)).toBeInTheDocument();
54 expect(screen.getByRole("link", { name: /create/i })).toHaveAttribute(
55 "href",
56 "/app/register",
57 );
58 });
59 });
60
61 it("shows forgot password and lost passkey links", async () => {
62 render(Login);
63 await waitFor(() => {
64 expect(screen.getByRole("link", { name: /forgot password/i }))
65 .toHaveAttribute("href", "/app/reset-password");
66 expect(screen.getByRole("link", { name: /lost passkey/i }))
67 .toHaveAttribute("href", "/app/request-passkey-recovery");
68 });
69 });
70 });
71
72 describe("with saved accounts", () => {
73 const savedAccounts: SavedAccount[] = [
74 {
75 did: unsafeAsDid("did:web:test.tranquil.dev:u:alice"),
76 handle: unsafeAsHandle("alice.test.tranquil.dev"),
77 accessJwt: unsafeAsAccessToken("mock-jwt-alice"),
78 refreshJwt: unsafeAsRefreshToken("mock-refresh-alice"),
79 },
80 {
81 did: unsafeAsDid("did:web:test.tranquil.dev:u:bob"),
82 handle: unsafeAsHandle("bob.test.tranquil.dev"),
83 accessJwt: unsafeAsAccessToken("mock-jwt-bob"),
84 refreshJwt: unsafeAsRefreshToken("mock-refresh-bob"),
85 },
86 ];
87
88 beforeEach(() => {
89 _testSetState({
90 session: null,
91 loading: false,
92 error: null,
93 savedAccounts,
94 });
95 mockEndpoint(
96 "com.atproto.server.getSession",
97 () =>
98 jsonResponse(
99 mockData.session({
100 handle: unsafeAsHandle("alice.test.tranquil.dev"),
101 }),
102 ),
103 );
104 });
105
106 it("displays saved accounts list", async () => {
107 render(Login);
108 await waitFor(() => {
109 expect(screen.getByText(/@alice\.test\.tranquil\.dev/))
110 .toBeInTheDocument();
111 expect(screen.getByText(/@bob\.test\.tranquil\.dev/))
112 .toBeInTheDocument();
113 });
114 });
115
116 it("shows sign in to another account option", async () => {
117 render(Login);
118 await waitFor(() => {
119 expect(screen.getByText(/sign in to another/i)).toBeInTheDocument();
120 });
121 });
122
123 it("can click on saved account to switch", async () => {
124 render(Login);
125 await waitFor(() => {
126 expect(screen.getByText(/@alice\.test\.tranquil\.dev/))
127 .toBeInTheDocument();
128 });
129 const aliceAccount = screen.getByText(/@alice\.test\.tranquil\.dev/)
130 .closest("[role='button']");
131 if (aliceAccount) {
132 await fireEvent.click(aliceAccount);
133 }
134 await waitFor(() => {
135 expect(globalThis.location.pathname).toBe("/app/dashboard");
136 });
137 });
138
139 it("can remove saved account with forget button", async () => {
140 render(Login);
141 await waitFor(() => {
142 expect(screen.getByText(/@alice\.test\.tranquil\.dev/))
143 .toBeInTheDocument();
144 const forgetButtons = screen.getAllByTitle(/remove/i);
145 expect(forgetButtons.length).toBe(2);
146 });
147 });
148 });
149
150 describe("error handling", () => {
151 it("displays error message as toast when auth state has error", async () => {
152 _testSetState({
153 session: null,
154 loading: false,
155 error: "OAuth login failed",
156 savedAccounts: [],
157 });
158 render(Login);
159 await waitFor(() => {
160 const toasts = getToasts();
161 const errorToast = toasts.find(
162 (t) => t.type === "error" && t.message.includes("OAuth login failed"),
163 );
164 expect(errorToast).toBeDefined();
165 });
166 });
167 });
168
169 describe("verification flow", () => {
170 beforeEach(() => {
171 _testSetState({
172 session: null,
173 loading: false,
174 error: null,
175 savedAccounts: [],
176 });
177 });
178
179 it("shows verification form when pending verification exists", () => {
180 render(Login);
181 });
182 });
183
184 describe("loading state", () => {
185 it("shows loading state while auth is initializing", () => {
186 _testSetState({
187 session: null,
188 loading: true,
189 error: null,
190 savedAccounts: [],
191 });
192 render(Login);
193 });
194 });
195});