this repo has no description
1import { describe, it, expect, beforeEach } from 'vitest'
2import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'
3import Login from '../routes/Login.svelte'
4import {
5 setupFetchMock,
6 mockEndpoint,
7 jsonResponse,
8 errorResponse,
9 mockData,
10 clearMocks,
11} from './mocks'
12
13describe('Login', () => {
14 beforeEach(() => {
15 clearMocks()
16 setupFetchMock()
17 window.location.hash = ''
18 })
19
20 describe('initial render', () => {
21 it('renders login form with all elements and correct initial state', () => {
22 render(Login)
23
24 expect(screen.getByRole('heading', { name: /sign in/i })).toBeInTheDocument()
25 expect(screen.getByLabelText(/handle or email/i)).toBeInTheDocument()
26 expect(screen.getByLabelText(/password/i)).toBeInTheDocument()
27 expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument()
28 expect(screen.getByRole('button', { name: /sign in/i })).toBeDisabled()
29 expect(screen.getByText(/don't have an account/i)).toBeInTheDocument()
30 expect(screen.getByRole('link', { name: /create one/i })).toHaveAttribute('href', '#/register')
31 })
32 })
33
34 describe('form validation', () => {
35 it('enables submit button only when both fields are filled', async () => {
36 render(Login)
37
38 const identifierInput = screen.getByLabelText(/handle or email/i)
39 const passwordInput = screen.getByLabelText(/password/i)
40 const submitButton = screen.getByRole('button', { name: /sign in/i })
41
42 await fireEvent.input(identifierInput, { target: { value: 'testuser' } })
43 expect(submitButton).toBeDisabled()
44
45 await fireEvent.input(identifierInput, { target: { value: '' } })
46 await fireEvent.input(passwordInput, { target: { value: 'password123' } })
47 expect(submitButton).toBeDisabled()
48
49 await fireEvent.input(identifierInput, { target: { value: 'testuser' } })
50 expect(submitButton).not.toBeDisabled()
51 })
52 })
53
54 describe('login submission', () => {
55 it('calls createSession with correct credentials', async () => {
56 let capturedBody: Record<string, string> | null = null
57
58 mockEndpoint('com.atproto.server.createSession', (_url, options) => {
59 capturedBody = JSON.parse((options?.body as string) || '{}')
60 return jsonResponse(mockData.session())
61 })
62
63 render(Login)
64
65 await fireEvent.input(screen.getByLabelText(/handle or email/i), { target: { value: 'testuser@example.com' } })
66 await fireEvent.input(screen.getByLabelText(/password/i), { target: { value: 'mypassword' } })
67 await fireEvent.click(screen.getByRole('button', { name: /sign in/i }))
68
69 await waitFor(() => {
70 expect(capturedBody).toEqual({
71 identifier: 'testuser@example.com',
72 password: 'mypassword',
73 })
74 })
75 })
76
77 it('shows styled error message on invalid credentials', async () => {
78 mockEndpoint('com.atproto.server.createSession', () =>
79 errorResponse('AuthenticationRequired', 'Invalid identifier or password', 401)
80 )
81
82 render(Login)
83
84 await fireEvent.input(screen.getByLabelText(/handle or email/i), { target: { value: 'wronguser' } })
85 await fireEvent.input(screen.getByLabelText(/password/i), { target: { value: 'wrongpassword' } })
86 await fireEvent.click(screen.getByRole('button', { name: /sign in/i }))
87
88 await waitFor(() => {
89 const errorDiv = screen.getByText(/invalid identifier or password/i)
90 expect(errorDiv).toBeInTheDocument()
91 expect(errorDiv).toHaveClass('error')
92 })
93 })
94
95 it('navigates to dashboard on successful login', async () => {
96 mockEndpoint('com.atproto.server.createSession', () =>
97 jsonResponse(mockData.session())
98 )
99
100 render(Login)
101
102 await fireEvent.input(screen.getByLabelText(/handle or email/i), { target: { value: 'test' } })
103 await fireEvent.input(screen.getByLabelText(/password/i), { target: { value: 'password' } })
104 await fireEvent.click(screen.getByRole('button', { name: /sign in/i }))
105
106 await waitFor(() => {
107 expect(window.location.hash).toBe('#/dashboard')
108 })
109 })
110 })
111
112 describe('account verification flow', () => {
113 it('shows verification form with all controls when account is not verified', async () => {
114 mockEndpoint('com.atproto.server.createSession', () => ({
115 ok: false,
116 status: 401,
117 json: async () => ({
118 error: 'AccountNotVerified',
119 message: 'Account not verified',
120 did: 'did:web:test.bspds.dev:u:testuser',
121 }),
122 }))
123
124 render(Login)
125
126 await fireEvent.input(screen.getByLabelText(/handle or email/i), { target: { value: 'unverified@test.com' } })
127 await fireEvent.input(screen.getByLabelText(/password/i), { target: { value: 'password' } })
128 await fireEvent.click(screen.getByRole('button', { name: /sign in/i }))
129
130 await waitFor(() => {
131 expect(screen.getByRole('heading', { name: /verify your account/i })).toBeInTheDocument()
132 expect(screen.getByLabelText(/verification code/i)).toBeInTheDocument()
133 expect(screen.getByRole('button', { name: /resend code/i })).toBeInTheDocument()
134 expect(screen.getByRole('button', { name: /back to login/i })).toBeInTheDocument()
135 })
136 })
137
138 it('returns to login form when clicking back', async () => {
139 mockEndpoint('com.atproto.server.createSession', () => ({
140 ok: false,
141 status: 401,
142 json: async () => ({
143 error: 'AccountNotVerified',
144 message: 'Account not verified',
145 did: 'did:web:test.bspds.dev:u:testuser',
146 }),
147 }))
148
149 render(Login)
150
151 await fireEvent.input(screen.getByLabelText(/handle or email/i), { target: { value: 'test' } })
152 await fireEvent.input(screen.getByLabelText(/password/i), { target: { value: 'password' } })
153 await fireEvent.click(screen.getByRole('button', { name: /sign in/i }))
154
155 await waitFor(() => {
156 expect(screen.getByRole('button', { name: /back to login/i })).toBeInTheDocument()
157 })
158
159 await fireEvent.click(screen.getByRole('button', { name: /back to login/i }))
160
161 await waitFor(() => {
162 expect(screen.getByRole('heading', { name: /sign in/i })).toBeInTheDocument()
163 expect(screen.queryByLabelText(/verification code/i)).not.toBeInTheDocument()
164 })
165 })
166 })
167})