Skip to content

Commit cc37ad7

Browse files
authored
test: added basic e2e tests (#46)
* test: added basic e2e tests * tests: all e2e tests now passing * ci: run e2e tests in ci * chore: comment out testing hooks in pre-commit config * tests: improved e2e tests * ci: run gptme-server for e2e tests * ci: use ANTHROPIC_API_KEY secret when running e2e tests * ci: use latest master of gptme-server for now * tests: fixes to e2e test
1 parent 243d078 commit cc37ad7

10 files changed

+1278
-129
lines changed

.github/workflows/ci.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on:
44
push:
55
branches: [ master ]
66
pull_request:
7-
branches: [ master ]
7+
branches: [ master, 'dev/*' ]
88

99
jobs:
1010
build:
@@ -36,3 +36,29 @@ jobs:
3636

3737
- name: Build
3838
run: npm run build
39+
40+
- name: Start gptme-server in background
41+
env:
42+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
43+
run: |
44+
# pipx install gptme[server]
45+
pipx install "gptme[server] @ git+https://github.com/gptme/gptme.git"
46+
gptme-server --cors-origin="http://localhost:5701" & # run in background
47+
sleep 3 # sleep so we get initial logs
48+
49+
- name: Install Playwright browsers
50+
run: npx playwright install --with-deps chromium
51+
52+
- name: Check that server is up
53+
run: curl --retry 2 --retry-delay 5 --retry-connrefused -sSfL http://localhost:5700/api
54+
55+
- name: Run e2e tests
56+
run: npm run test:e2e
57+
58+
- name: Upload test results
59+
if: always()
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: playwright-report
63+
path: playwright-report/
64+
retention-days: 30

.pre-commit-config.yaml

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,28 @@ repos:
55
- id: check-yaml
66
- id: end-of-file-fixer
77
- id: trailing-whitespace
8+
89
- repo: local
910
hooks:
1011
- id: lint
1112
name: lint
1213
stages: [commit]
1314
types: [javascript, jsx, ts, tsx]
14-
entry: npm run lint
15-
language: system
16-
pass_filenames: false
17-
always_run: true
18-
- id: format
19-
name: format
20-
stages: [commit]
21-
types: [javascript, jsx, ts, tsx]
22-
entry: npm run format
15+
entry: npm run lint:fix
2316
language: system
2417
pass_filenames: false
2518
always_run: true
19+
20+
# handled by lint:fix
21+
#- id: format
22+
# name: format
23+
# stages: [commit]
24+
# types: [javascript, jsx, ts, tsx]
25+
# entry: npm run format
26+
# language: system
27+
# pass_filenames: false
28+
# always_run: true
29+
2630
- id: typecheck
2731
name: typecheck
2832
stages: [commit]
@@ -31,11 +35,22 @@ repos:
3135
language: system
3236
pass_filenames: false
3337
always_run: true
38+
39+
# Uncomment the following lines to enable testing hooks
40+
# Don't commit them uncommented since they take too long to run every time
41+
3442
#- id: test
3543
# name: test
36-
# stages: [commit]
3744
# types: [javascript, jsx, ts, tsx]
3845
# entry: npm test
3946
# language: system
4047
# pass_filenames: false
4148
# always_run: true
49+
50+
#- id: test-e2e
51+
# name: test-e2e
52+
# types: [javascript, jsx, ts, tsx]
53+
# entry: npm run test:e2e
54+
# language: system
55+
# pass_filenames: false
56+
# always_run: true

e2e/conversation.spec.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Connecting', () => {
4+
test('should connect and list conversations', async ({ page }) => {
5+
// Go to the app
6+
await page.goto('/');
7+
8+
// Should show demo conversations initially
9+
await expect(page.getByText('Introduction to gptme')).toBeVisible();
10+
11+
// Should show connection status
12+
const connectionButton = page.getByRole('button', { name: /Connect/i });
13+
await expect(connectionButton).toBeVisible();
14+
15+
// Click the demo conversation
16+
await page.getByText('Introduction to gptme').click();
17+
18+
// Should show the conversation content
19+
await expect(page.getByText(/Hello! I'm gptme, your AI programming assistant/)).toBeVisible();
20+
await page.goto('/');
21+
22+
// Should show demo conversations immediately
23+
await expect(page.getByText('Introduction to gptme')).toBeVisible();
24+
25+
// Wait for successful connection
26+
await expect(page.getByRole('button', { name: /Connect/i })).toHaveClass(/text-green-600/, {
27+
timeout: 10000,
28+
});
29+
30+
// Wait for success toast to confirm API connection
31+
await expect(page.getByText('Connected to gptme server')).toBeVisible();
32+
33+
// Wait for conversations to load
34+
// Should show both demo conversations and connected conversations
35+
await expect(page.getByText('Introduction to gptme')).toBeVisible();
36+
37+
// Wait for loading state to finish
38+
await expect(page.getByText('Loading conversations...')).toBeHidden();
39+
40+
// Get the conversation list
41+
const conversationList = page.getByTestId('conversation-list');
42+
43+
// Get all conversation titles
44+
const conversationTitles = await conversationList
45+
.locator('[data-testid="conversation-title"]')
46+
.allTextContents();
47+
48+
// Should have both demo and API conversations
49+
const demoConversations = conversationTitles.filter((title) => title.includes('Introduction'));
50+
const apiConversations = conversationTitles.filter((title) => /^\d+$/.test(title));
51+
52+
expect(demoConversations.length).toBeGreaterThan(0);
53+
54+
if (apiConversations.length > 0) {
55+
// Check for historical timestamps if we have API conversations
56+
const timestamps = await conversationList
57+
.getByRole('button')
58+
.locator('time')
59+
.allTextContents();
60+
expect(timestamps.length).toBeGreaterThan(1);
61+
62+
// There should be some timestamps that aren't "just now"
63+
const nonJustNowTimestamps = timestamps.filter((t) => t !== 'just now');
64+
expect(nonJustNowTimestamps.length).toBeGreaterThan(0);
65+
} else {
66+
// This happens when e2e tests are run in CI with a fresh gptme-server
67+
console.log('No API conversations found, skipping timestamp check');
68+
}
69+
});
70+
71+
test('should handle connection errors gracefully', async ({ page }) => {
72+
// Start with server unavailable
73+
await page.goto('/');
74+
75+
// Should still show demo conversations
76+
await expect(page.getByText('Introduction to gptme')).toBeVisible();
77+
78+
// Click connect button and try to connect to non-existent server
79+
const connectionButton = page.getByRole('button', { name: /Connect/i });
80+
await connectionButton.click();
81+
82+
// Fill in invalid server URL and try to connect
83+
await page.getByLabel('Server URL').fill('http://localhost:1');
84+
await page.getByRole('button', { name: /^(Connect|Reconnect)$/ }).click();
85+
86+
// Wait for error toast to appear
87+
await expect(page.getByText('Could not connect to gptme instance')).toBeVisible({
88+
timeout: 10000,
89+
});
90+
91+
// Close the connection dialog by clicking outside
92+
await page.keyboard.press('Escape');
93+
94+
// Verify connection button is in disconnected state
95+
await expect(connectionButton).toBeVisible();
96+
await expect(connectionButton).not.toHaveClass(/text-green-600/);
97+
98+
// Should show demo conversations
99+
await expect(page.getByText('Introduction to gptme')).toBeVisible();
100+
101+
// Should not show any API conversations
102+
const conversationList = page.getByTestId('conversation-list');
103+
const conversationTitles = await conversationList
104+
.locator('[data-testid="conversation-title"]')
105+
.allTextContents();
106+
107+
const apiConversations = conversationTitles.filter((title) => /^\d+$/.test(title));
108+
expect(apiConversations.length).toBe(0);
109+
});
110+
});
111+
112+
test.describe('Conversation Flow', () => {
113+
test('should be able to create a new conversation and send a message', async ({ page }) => {
114+
await page.goto('/');
115+
116+
// Click the "New Conversation" button to start a new conversation
117+
await page.locator('[data-testid="new-conversation-button"]').click();
118+
119+
// Wait for the new conversation page to load
120+
await expect(page).toHaveURL(/\?conversation=\d+$/);
121+
122+
const message = 'Hello. We are testing, just say exactly "Hello world" without anything else.';
123+
124+
// Type a message
125+
await page.getByRole('textbox').fill(message);
126+
await page.keyboard.press('Enter');
127+
128+
// Should show the message in the conversation
129+
// Look specifically for the user's message in a user message container
130+
await expect(
131+
page.locator('.role-user', {
132+
hasText: message,
133+
})
134+
).toBeVisible();
135+
136+
// Should show the AI's response
137+
await expect(
138+
page.locator('.role-assistant', {
139+
hasText: 'Hello world',
140+
})
141+
).toBeVisible();
142+
});
143+
});

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint';
66
import prettier from 'eslint-plugin-prettier';
77

88
export default tseslint.config(
9-
{ ignores: ['dist'] },
9+
{ ignores: ['dist', 'playwright.config.ts'] },
1010
{
1111
extends: [js.configs.recommended, ...tseslint.configs.recommended],
1212
files: ['**/*.{ts,tsx}'],

jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ export default {
1414
},
1515
],
1616
},
17+
testPathIgnorePatterns: ['/node_modules/', '/e2e/'],
1718
extensionsToTreatAsEsm: ['.ts', '.tsx'],
1819
};

0 commit comments

Comments
 (0)