Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ htmlcov/
.dmypy.json
dmypy.json

# Test coverage reports
apps/web/coverage/

# Node.js / npm / yarn / pnpm
node_modules/
npm-debug.log*
Expand Down
20 changes: 10 additions & 10 deletions apps/web/__tests__/integration/auth-flow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ describe('Authentication Flow Integration', () => {
// Fill out registration form
await userEvent.type(screen.getByLabelText(/display name/i), 'New User');
await userEvent.type(screen.getByLabelText(/email/i), 'newuser@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'SecurePass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'SecurePass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'SecurePass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'SecurePass123!');

// Submit form
fireEvent.click(screen.getByRole('button', { name: /create account/i }));
Expand All @@ -101,14 +101,14 @@ describe('Authentication Flow Integration', () => {
'credentials',
expect.objectContaining({
email: 'newuser@example.com',
password: 'SecurePass123',
password: 'SecurePass123!',
})
);
});

// Verify redirect
await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/dashboard');
expect(mockRouter.push).toHaveBeenCalledWith('/courses');
});
});

Expand Down Expand Up @@ -138,7 +138,7 @@ describe('Authentication Flow Integration', () => {

// Verify redirect
await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/dashboard');
expect(mockRouter.push).toHaveBeenCalledWith('/courses');
});
});
});
Expand All @@ -147,7 +147,7 @@ describe('Authentication Flow Integration', () => {
it('handles login failure gracefully', async () => {
(signIn as jest.Mock).mockResolvedValue({
ok: false,
error: 'Invalid email or password',
error: 'CredentialsSignin',
});

render(<LoginPage />);
Expand Down Expand Up @@ -177,8 +177,8 @@ describe('Authentication Flow Integration', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'duplicate@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'SecurePass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'SecurePass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'SecurePass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'SecurePass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -196,8 +196,8 @@ describe('Authentication Flow Integration', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'SecurePass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'SecurePass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'SecurePass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'SecurePass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand Down
24 changes: 18 additions & 6 deletions apps/web/__tests__/integration/study-flow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@ import userEvent from '@testing-library/user-event';
import { useSession } from 'next-auth/react';
import { useParams } from 'next/navigation';

// scrollIntoView not implemented in jsdom
// scrollIntoView and matchMedia not implemented in jsdom
beforeAll(() => {
window.HTMLElement.prototype.scrollIntoView = jest.fn();
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
})),
});
});

// ── Module mocks ────────────────────────────────────────────────────────────
Expand All @@ -18,6 +27,7 @@ jest.mock('next-auth/react', () => ({
jest.mock('next/navigation', () => ({
useParams: jest.fn(),
useRouter: jest.fn(() => ({ push: jest.fn() })),
useSearchParams: jest.fn(() => ({ get: jest.fn().mockReturnValue(null) })),
}));

jest.mock('@/lib/services/courses', () => ({
Expand All @@ -36,6 +46,7 @@ jest.mock('@/lib/services/documents', () => ({

jest.mock('@/lib/api/documents', () => ({
getDocumentPreviewView: jest.fn(),
getDocumentListRows: jest.fn(),
}));

jest.mock('@/lib/services/chat', () => ({
Expand All @@ -47,7 +58,7 @@ jest.mock('@/lib/services/chat', () => ({
import { getCourse, getCourseLessons } from '@/lib/services/courses';
import { getCourseProgress, markLessonComplete } from '@/lib/services/progress';
import { getDocuments } from '@/lib/services/documents';
import { getDocumentPreviewView } from '@/lib/api/documents';
import { getDocumentPreviewView, getDocumentListRows } from '@/lib/api/documents';
import { sendChatMessage } from '@/lib/services/chat';

// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand Down Expand Up @@ -79,6 +90,7 @@ function setupMocks() {
(getCourseLessons as jest.Mock).mockResolvedValue(LESSONS);
(getCourseProgress as jest.Mock).mockResolvedValue(PROGRESS);
(getDocuments as jest.Mock).mockResolvedValue([]);
(getDocumentListRows as jest.Mock).mockResolvedValue([]);
(getDocumentPreviewView as jest.Mock).mockResolvedValue({
id: 'doc-1',
filename: 'guide.pdf',
Expand Down Expand Up @@ -191,7 +203,7 @@ describe('Study Flow Integration', () => {
filename: 'bitcoin-guide.pdf',
extractedTextPreview: null,
pageCount: 5,
sections: [{ title: 'What is Bitcoin?' }],
sections: ['What is Bitcoin?'],
sampleChunks: [
{ text: 'Bitcoin is a decentralized digital currency.', section: 'What is Bitcoin?' },
{ text: 'Transactions are verified by network nodes.', section: 'What is Bitcoin?' },
Expand All @@ -218,7 +230,7 @@ describe('Study Flow Integration', () => {
filename: 'guide.pdf',
extractedTextPreview: null,
pageCount: 2,
sections: [{ title: 'Overview' }, { title: 'Key Concepts' }],
sections: ['Overview', 'Key Concepts'],
sampleChunks: [{ text: 'Some chunk text.' }],
});

Expand Down Expand Up @@ -313,7 +325,7 @@ describe('Study Flow Integration', () => {

await waitFor(() => {
expect(screen.getByText(/proof of work is the consensus mechanism/i)).toBeInTheDocument();
expect(screen.getByText('Relevance: 88%')).toBeInTheDocument();
expect(screen.getByText(/88%/)).toBeInTheDocument();
});
});
});
Expand Down Expand Up @@ -383,7 +395,7 @@ describe('Study Flow Integration', () => {
fireEvent.click(screen.getByRole('button', { name: /mark as complete/i }));

await waitFor(() => {
expect(screen.getByText(/new badge earned/i)).toBeInTheDocument();
expect(screen.getByText(/badge earned/i)).toBeInTheDocument();
expect(screen.getByText('First Steps')).toBeInTheDocument();
});
});
Expand Down
6 changes: 3 additions & 3 deletions apps/web/__tests__/unit/OutputPane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('OutputPane', () => {

it('shows the empty-state prompt when no messages', () => {
render(<OutputPane {...defaultProps} />);
expect(screen.getByText(/ask me anything/i)).toBeInTheDocument();
expect(screen.getByText(/type a topic/i)).toBeInTheDocument();
});

it('shows lesson-specific placeholder when a lesson is selected', () => {
Expand All @@ -57,7 +57,7 @@ describe('OutputPane', () => {
/>
);
const textarea = screen.getByRole('textbox', { name: /message input/i });
expect(textarea).toHaveAttribute('placeholder', 'Ask about "How Mining Works"…');
expect(textarea).toHaveAttribute('placeholder', 'Ask about "How Mining Works" or pick an action above…');
});
});

Expand Down Expand Up @@ -178,7 +178,7 @@ describe('OutputPane', () => {
fireEvent.click(screen.getByRole('button', { name: /send message/i }));

await waitFor(() => {
expect(screen.getByText('Relevance: 92%')).toBeInTheDocument();
expect(screen.getByText(/92%/)).toBeInTheDocument();
});
});

Expand Down
8 changes: 4 additions & 4 deletions apps/web/__tests__/unit/login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe('LoginPage', () => {
email: 'test@example.com',
password: 'Password123',
redirect: false,
callbackUrl: '/dashboard',
callbackUrl: '/courses',
});
});
});
Expand All @@ -137,7 +137,7 @@ describe('LoginPage', () => {
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));

await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/dashboard');
expect(mockRouter.push).toHaveBeenCalledWith('/courses');
expect(mockRouter.refresh).toHaveBeenCalled();
});
});
Expand Down Expand Up @@ -167,7 +167,7 @@ describe('LoginPage', () => {
});

it('displays error message on failed login', async () => {
(signIn as jest.Mock).mockResolvedValue({ ok: false, error: 'Invalid credentials' });
(signIn as jest.Mock).mockResolvedValue({ ok: false, error: 'CredentialsSignin' });

render(<LoginPage />);

Expand All @@ -176,7 +176,7 @@ describe('LoginPage', () => {
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));

await waitFor(() => {
expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
expect(screen.getByText(/invalid email or password/i)).toBeInTheDocument();
});
});

Expand Down
52 changes: 26 additions & 26 deletions apps/web/__tests__/unit/signup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('SignupPage', () => {
it('renders signup form with all elements', () => {
render(<SignupPage />);

expect(screen.getByRole('heading', { name: /create your account/i })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: /create account/i })).toBeInTheDocument();
expect(screen.getByLabelText(/display name/i)).toBeInTheDocument();
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument();
Expand All @@ -51,16 +51,16 @@ describe('SignupPage', () => {
it('shows password requirements hint', () => {
render(<SignupPage />);

expect(screen.getByText(/min 8 characters/i)).toBeInTheDocument();
expect(screen.getByText(/min 12/i)).toBeInTheDocument();
});
});

describe('Validation', () => {
it('shows error when email is empty', async () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -73,8 +73,8 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'invalid-email');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -93,7 +93,7 @@ describe('SignupPage', () => {
fireEvent.click(screen.getByRole('button', { name: /create account/i }));

await waitFor(() => {
expect(screen.getByText(/at least 8 characters/i)).toBeInTheDocument();
expect(screen.getByText(/at least 12 characters/i)).toBeInTheDocument();
});
});

Expand Down Expand Up @@ -143,7 +143,7 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'DifferentPass456');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));
Expand All @@ -157,7 +157,7 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -179,8 +179,8 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -205,8 +205,8 @@ describe('SignupPage', () => {

await userEvent.type(screen.getByLabelText(/display name/i), 'Test User');
await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -218,7 +218,7 @@ describe('SignupPage', () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'test@example.com',
password: 'ValidPass123',
password: 'ValidPass123!',
display_name: 'Test User',
}),
})
Expand All @@ -239,8 +239,8 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -249,7 +249,7 @@ describe('SignupPage', () => {
'credentials',
expect.objectContaining({
email: 'test@example.com',
password: 'ValidPass123',
password: 'ValidPass123!',
})
);
});
Expand All @@ -268,13 +268,13 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/dashboard');
expect(mockRouter.push).toHaveBeenCalledWith('/courses');
});
});

Expand All @@ -288,8 +288,8 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'existing@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -308,8 +308,8 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand All @@ -326,8 +326,8 @@ describe('SignupPage', () => {
render(<SignupPage />);

await userEvent.type(screen.getByLabelText(/email/i), 'test@example.com');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123');
await userEvent.type(screen.getByLabelText(/^password$/i), 'ValidPass123!');
await userEvent.type(screen.getByLabelText(/confirm password/i), 'ValidPass123!');

fireEvent.click(screen.getByRole('button', { name: /create account/i }));

Expand Down
Loading