Skip to content

Commit 0abf10e

Browse files
ci: add e2e tests to workflow
Add database services startup, migration, and Playwright installation to CI pipeline. Configure Playwright for api and web test projects with web server setup. Add invalidateUserDashboardCache method in RedisService. Enhance getDashboard in GroupsService to fetch user groups. Add dashboard.spec.ts e2e test file and test-results directory.
1 parent 73b5fea commit 0abf10e

6 files changed

Lines changed: 108 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,28 @@ jobs:
5959
- name: Build
6060
run: pnpm build
6161

62+
- name: Start Database Services
63+
run: docker compose up -d
64+
65+
- name: Migrate Database
66+
env:
67+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
68+
JWT_SECRET: supersecret
69+
STRIPE_SECRET_KEY: test_key
70+
run: pnpm --filter backend prisma:migrate
71+
72+
- name: Install Playwright Browsers
73+
run: pnpm exec playwright install --with-deps
74+
75+
- name: Run E2E Tests
76+
env:
77+
CI: true
78+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
79+
JWT_SECRET: supersecret
80+
STRIPE_SECRET_KEY: test_key
81+
NEXT_PUBLIC_APP_URL: http://localhost:3000
82+
run: pnpm run e2e
83+
6284
- name: Upload coverage
6385
if: always()
6486
uses: actions/upload-artifact@v4

apps/backend/src/groups/groups.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export class GroupsService {
251251
}
252252

253253
async getDashboard(userId: string): Promise<GroupDashboardDto> {
254+
const groups = await this.prisma.group.findMany({
254255
where: {
255256
members: {
256257
some: {

apps/backend/src/redis/redis.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ export class RedisService {
6161
}
6262
}
6363

64+
async invalidateUserDashboardCache(userId: string): Promise<void> {
65+
try {
66+
await this.redis.del(`user:${userId}:dashboard`);
67+
} catch (error) {
68+
this.logger.warn(`Redis invalidate skipped: ${error instanceof Error ? error.message : 'unknown error'}`);
69+
}
70+
}
71+
6472
private async safeGet(key: string): Promise<string | null> {
6573
try {
6674
return await this.redis.get(key);

playwright.config.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,42 @@
1-
import { defineConfig } from '@playwright/test';
1+
import { defineConfig, devices } from '@playwright/test';
22

33
export default defineConfig({
44
testDir: './tests/e2e',
55
timeout: 120_000,
66
use: {
7-
baseURL: process.env.E2E_API_BASE_URL ?? 'http://localhost:3001/api/v1',
7+
trace: 'on-first-retry',
88
},
9+
projects: [
10+
{
11+
name: 'api',
12+
testMatch: /fairshare\.spec\.ts/,
13+
use: {
14+
baseURL: process.env.E2E_API_BASE_URL ?? 'http://localhost:3001/api/v1',
15+
},
16+
},
17+
{
18+
name: 'web',
19+
testMatch: /dashboard\.spec\.ts/,
20+
use: {
21+
...devices['Desktop Chrome'],
22+
baseURL: process.env.WEB_URL ?? 'http://localhost:3000',
23+
},
24+
},
25+
],
26+
webServer: process.env.CI
27+
? [
28+
{
29+
command: 'pnpm --filter backend start',
30+
port: 3001,
31+
timeout: 180_000,
32+
reuseExistingServer: !process.env.CI,
33+
},
34+
{
35+
command: 'pnpm --filter web start',
36+
port: 3000,
37+
timeout: 180_000,
38+
reuseExistingServer: !process.env.CI,
39+
},
40+
]
41+
: undefined,
942
});

test-results/.last-run.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": "passed",
3+
"failedTests": []
4+
}

tests/e2e/dashboard.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const randomEmail = () => `dashboard_ui_${Date.now()}_${Math.random().toString(36).slice(2)}@example.com`;
4+
5+
test.describe('Dashboard UI', () => {
6+
test('User can log in and view dashboard', async ({ page, request }) => {
7+
const email = randomEmail();
8+
const password = 'PasswordUI123!';
9+
10+
// Seed user via API to avoid UI registration flakiness in testing
11+
const apiBaseUrl = process.env.E2E_API_BASE_URL ?? 'http://localhost:3001/api/v1';
12+
const registerResp = await request.post(`${apiBaseUrl}/auth/register`, {
13+
data: { name: 'UI User', email, password },
14+
});
15+
expect(registerResp.ok()).toBeTruthy();
16+
17+
await test.step('Log into web app', async () => {
18+
await page.goto('/login');
19+
await page.fill('input[type="email"]', email);
20+
await page.fill('input[type="password"]', password);
21+
await page.click('button[type="submit"]');
22+
23+
// Should redirect to dashboard
24+
await expect(page).toHaveURL(/\/dashboard/);
25+
});
26+
27+
await test.step('Verify Dashboard Elements', async () => {
28+
// Check for navigation or header
29+
await expect(page.locator('h1', { hasText: 'Dashboard' }).first()).toBeVisible();
30+
31+
// Removed flaky networkidle wait
32+
33+
34+
// The user doesn't have any groups yet, so check for empty state
35+
await expect(page.getByText('group', { exact: false }).first()).toBeVisible();
36+
});
37+
});
38+
});

0 commit comments

Comments
 (0)