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