Skip to content

Commit 9dd9a98

Browse files
feat: add tests (#843)
1 parent 95a2af2 commit 9dd9a98

35 files changed

+1068
-196
lines changed

.github/workflows/playwright.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Playwright Tests
2+
on:
3+
push:
4+
branches: [main, master]
5+
pull_request:
6+
branches: [main, master]
7+
8+
jobs:
9+
test:
10+
timeout-minutes: 30
11+
runs-on: ubuntu-latest
12+
env:
13+
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
14+
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
15+
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 1
21+
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: lts/*
25+
26+
- name: Install pnpm
27+
uses: pnpm/action-setup@v2
28+
with:
29+
version: latest
30+
run_install: false
31+
32+
- name: Get pnpm store directory
33+
id: pnpm-cache
34+
shell: bash
35+
run: |
36+
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
37+
38+
- uses: actions/cache@v3
39+
with:
40+
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
41+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
42+
restore-keys: |
43+
${{ runner.os }}-pnpm-store-
44+
45+
- uses: actions/setup-node@v4
46+
with:
47+
node-version: lts/*
48+
cache: "pnpm"
49+
50+
- name: Install dependencies
51+
run: pnpm install --frozen-lockfile
52+
53+
- name: Cache Playwright browsers
54+
uses: actions/cache@v3
55+
id: playwright-cache
56+
with:
57+
path: ~/.cache/ms-playwright
58+
key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }}
59+
60+
- name: Install Playwright Browsers
61+
if: steps.playwright-cache.outputs.cache-hit != 'true'
62+
run: pnpm exec playwright install --with-deps chromium
63+
64+
- name: Run Playwright tests
65+
run: pnpm test
66+
67+
- uses: actions/upload-artifact@v4
68+
if: always() && !cancelled()
69+
with:
70+
name: playwright-report
71+
path: playwright-report/
72+
retention-days: 7

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,10 @@ yarn-error.log*
3636
.vercel
3737
.vscode
3838
.env*.local
39+
40+
# Playwright
41+
/test-results/
42+
/playwright-report/
43+
/blob-report/
44+
/playwright/.cache/
45+
/playwright/.auth/

app/(auth)/login/page.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import Link from 'next/link';
44
import { useRouter } from 'next/navigation';
55
import { useActionState, useEffect, useState } from 'react';
6-
import { toast } from 'sonner';
6+
import { toast } from '@/components/toast';
77

88
import { AuthForm } from '@/components/auth-form';
99
import { SubmitButton } from '@/components/submit-button';
@@ -25,9 +25,15 @@ export default function Page() {
2525

2626
useEffect(() => {
2727
if (state.status === 'failed') {
28-
toast.error('Invalid credentials!');
28+
toast({
29+
type: 'error',
30+
description: 'Invalid credentials!',
31+
});
2932
} else if (state.status === 'invalid_data') {
30-
toast.error('Failed validating your submission!');
33+
toast({
34+
type: 'error',
35+
description: 'Failed validating your submission!',
36+
});
3137
} else if (state.status === 'success') {
3238
setIsSuccessful(true);
3339
router.refresh();

app/(auth)/register/page.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import Link from 'next/link';
44
import { useRouter } from 'next/navigation';
55
import { useActionState, useEffect, useState } from 'react';
6-
import { toast } from 'sonner';
76

87
import { AuthForm } from '@/components/auth-form';
98
import { SubmitButton } from '@/components/submit-button';
109

1110
import { register, type RegisterActionState } from '../actions';
11+
import { toast } from '@/components/toast';
1212

1313
export default function Page() {
1414
const router = useRouter();
@@ -25,13 +25,17 @@ export default function Page() {
2525

2626
useEffect(() => {
2727
if (state.status === 'user_exists') {
28-
toast.error('Account already exists');
28+
toast({ type: 'error', description: 'Account already exists!' });
2929
} else if (state.status === 'failed') {
30-
toast.error('Failed to create account');
30+
toast({ type: 'error', description: 'Failed to create account!' });
3131
} else if (state.status === 'invalid_data') {
32-
toast.error('Failed validating your submission!');
32+
toast({
33+
type: 'error',
34+
description: 'Failed validating your submission!',
35+
});
3336
} else if (state.status === 'success') {
34-
toast.success('Account created successfully');
37+
toast({ type: 'success', description: 'Account created successfully!' });
38+
3539
setIsSuccessful(true);
3640
router.refresh();
3741
}

app/(chat)/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
updateChatVisiblityById,
1010
} from '@/lib/db/queries';
1111
import { VisibilityType } from '@/components/visibility-selector';
12-
import { myProvider } from '@/lib/ai/models';
12+
import { myProvider } from '@/lib/ai/providers';
1313

1414
export async function saveChatModelAsCookie(model: string) {
1515
const cookieStore = await cookies();

app/(chat)/api/chat/route.ts

Lines changed: 102 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import {
44
smoothStream,
55
streamText,
66
} from 'ai';
7-
87
import { auth } from '@/app/(auth)/auth';
9-
import { myProvider } from '@/lib/ai/models';
108
import { systemPrompt } from '@/lib/ai/prompts';
119
import {
1210
deleteChatById,
@@ -19,113 +17,124 @@ import {
1917
getMostRecentUserMessage,
2018
sanitizeResponseMessages,
2119
} from '@/lib/utils';
22-
2320
import { generateTitleFromUserMessage } from '../../actions';
2421
import { createDocument } from '@/lib/ai/tools/create-document';
2522
import { updateDocument } from '@/lib/ai/tools/update-document';
2623
import { requestSuggestions } from '@/lib/ai/tools/request-suggestions';
2724
import { getWeather } from '@/lib/ai/tools/get-weather';
25+
import { isProductionEnvironment } from '@/lib/constants';
26+
import { NextResponse } from 'next/server';
27+
import { myProvider } from '@/lib/ai/providers';
2828

2929
export const maxDuration = 60;
3030

3131
export async function POST(request: Request) {
32-
const {
33-
id,
34-
messages,
35-
selectedChatModel,
36-
}: { id: string; messages: Array<Message>; selectedChatModel: string } =
37-
await request.json();
38-
39-
const session = await auth();
40-
41-
if (!session || !session.user || !session.user.id) {
42-
return new Response('Unauthorized', { status: 401 });
43-
}
44-
45-
const userMessage = getMostRecentUserMessage(messages);
32+
try {
33+
const {
34+
id,
35+
messages,
36+
selectedChatModel,
37+
}: {
38+
id: string;
39+
messages: Array<Message>;
40+
selectedChatModel: string;
41+
} = await request.json();
42+
43+
const session = await auth();
44+
45+
if (!session || !session.user || !session.user.id) {
46+
return new Response('Unauthorized', { status: 401 });
47+
}
4648

47-
if (!userMessage) {
48-
return new Response('No user message found', { status: 400 });
49-
}
49+
const userMessage = getMostRecentUserMessage(messages);
5050

51-
const chat = await getChatById({ id });
51+
if (!userMessage) {
52+
return new Response('No user message found', { status: 400 });
53+
}
5254

53-
if (!chat) {
54-
const title = await generateTitleFromUserMessage({ message: userMessage });
55-
await saveChat({ id, userId: session.user.id, title });
56-
}
55+
const chat = await getChatById({ id });
5756

58-
await saveMessages({
59-
messages: [{ ...userMessage, createdAt: new Date(), chatId: id }],
60-
});
61-
62-
return createDataStreamResponse({
63-
execute: (dataStream) => {
64-
const result = streamText({
65-
model: myProvider.languageModel(selectedChatModel),
66-
system: systemPrompt({ selectedChatModel }),
67-
messages,
68-
maxSteps: 5,
69-
experimental_activeTools:
70-
selectedChatModel === 'chat-model-reasoning'
71-
? []
72-
: [
73-
'getWeather',
74-
'createDocument',
75-
'updateDocument',
76-
'requestSuggestions',
77-
],
78-
experimental_transform: smoothStream({ chunking: 'word' }),
79-
experimental_generateMessageId: generateUUID,
80-
tools: {
81-
getWeather,
82-
createDocument: createDocument({ session, dataStream }),
83-
updateDocument: updateDocument({ session, dataStream }),
84-
requestSuggestions: requestSuggestions({
85-
session,
86-
dataStream,
87-
}),
88-
},
89-
onFinish: async ({ response, reasoning }) => {
90-
if (session.user?.id) {
91-
try {
92-
const sanitizedResponseMessages = sanitizeResponseMessages({
93-
messages: response.messages,
94-
reasoning,
95-
});
96-
97-
await saveMessages({
98-
messages: sanitizedResponseMessages.map((message) => {
99-
return {
100-
id: message.id,
101-
chatId: id,
102-
role: message.role,
103-
content: message.content,
104-
createdAt: new Date(),
105-
};
106-
}),
107-
});
108-
} catch (error) {
109-
console.error('Failed to save chat');
110-
}
111-
}
112-
},
113-
experimental_telemetry: {
114-
isEnabled: true,
115-
functionId: 'stream-text',
116-
},
57+
if (!chat) {
58+
const title = await generateTitleFromUserMessage({
59+
message: userMessage,
11760
});
61+
await saveChat({ id, userId: session.user.id, title });
62+
}
11863

119-
result.consumeStream();
64+
await saveMessages({
65+
messages: [{ ...userMessage, createdAt: new Date(), chatId: id }],
66+
});
12067

121-
result.mergeIntoDataStream(dataStream, {
122-
sendReasoning: true,
123-
});
124-
},
125-
onError: () => {
126-
return 'Oops, an error occured!';
127-
},
128-
});
68+
return createDataStreamResponse({
69+
execute: (dataStream) => {
70+
const result = streamText({
71+
model: myProvider.languageModel(selectedChatModel),
72+
system: systemPrompt({ selectedChatModel }),
73+
messages,
74+
maxSteps: 5,
75+
experimental_activeTools:
76+
selectedChatModel === 'chat-model-reasoning'
77+
? []
78+
: [
79+
'getWeather',
80+
'createDocument',
81+
'updateDocument',
82+
'requestSuggestions',
83+
],
84+
experimental_transform: smoothStream({ chunking: 'word' }),
85+
experimental_generateMessageId: generateUUID,
86+
tools: {
87+
getWeather,
88+
createDocument: createDocument({ session, dataStream }),
89+
updateDocument: updateDocument({ session, dataStream }),
90+
requestSuggestions: requestSuggestions({
91+
session,
92+
dataStream,
93+
}),
94+
},
95+
onFinish: async ({ response, reasoning }) => {
96+
if (session.user?.id) {
97+
try {
98+
const sanitizedResponseMessages = sanitizeResponseMessages({
99+
messages: response.messages,
100+
reasoning,
101+
});
102+
103+
await saveMessages({
104+
messages: sanitizedResponseMessages.map((message) => {
105+
return {
106+
id: message.id,
107+
chatId: id,
108+
role: message.role,
109+
content: message.content,
110+
createdAt: new Date(),
111+
};
112+
}),
113+
});
114+
} catch (error) {
115+
console.error('Failed to save chat');
116+
}
117+
}
118+
},
119+
experimental_telemetry: {
120+
isEnabled: isProductionEnvironment,
121+
functionId: 'stream-text',
122+
},
123+
});
124+
125+
result.consumeStream();
126+
127+
result.mergeIntoDataStream(dataStream, {
128+
sendReasoning: true,
129+
});
130+
},
131+
onError: () => {
132+
return 'Oops, an error occured!';
133+
},
134+
});
135+
} catch (error) {
136+
return NextResponse.json({ error }, { status: 400 });
137+
}
129138
}
130139

131140
export async function DELETE(request: Request) {

artifacts/code/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from 'zod';
22
import { streamObject } from 'ai';
3-
import { myProvider } from '@/lib/ai/models';
3+
import { myProvider } from '@/lib/ai/providers';
44
import { codePrompt, updateDocumentPrompt } from '@/lib/ai/prompts';
55
import { createDocumentHandler } from '@/lib/artifacts/server';
66

artifacts/image/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { myProvider } from '@/lib/ai/models';
1+
import { myProvider } from '@/lib/ai/providers';
22
import { createDocumentHandler } from '@/lib/artifacts/server';
33
import { experimental_generateImage } from 'ai';
44

0 commit comments

Comments
 (0)