Skip to content

Commit 95f7d53

Browse files
committed
frontend unit+e2e tests
1 parent 30ee844 commit 95f7d53

File tree

13 files changed

+4168
-239
lines changed

13 files changed

+4168
-239
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
name: Frontend Tests
2+
3+
on:
4+
push:
5+
branches: [main, dev]
6+
paths:
7+
- 'frontend/**'
8+
- '.github/workflows/frontend-tests.yml'
9+
pull_request:
10+
branches: [main, dev]
11+
paths:
12+
- 'frontend/**'
13+
- '.github/workflows/frontend-tests.yml'
14+
workflow_dispatch:
15+
16+
jobs:
17+
unit-tests:
18+
name: Unit Tests
19+
runs-on: ubuntu-latest
20+
defaults:
21+
run:
22+
working-directory: frontend
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Setup Node.js
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: '22'
30+
cache: 'npm'
31+
cache-dependency-path: frontend/package-lock.json
32+
33+
- name: Install dependencies
34+
run: npm ci
35+
36+
- name: Run unit tests
37+
run: npm test
38+
39+
- name: Run tests with coverage
40+
run: npm run test:coverage
41+
42+
- name: Upload coverage report
43+
uses: actions/upload-artifact@v4
44+
if: always()
45+
with:
46+
name: frontend-coverage
47+
path: frontend/coverage/
48+
49+
e2e-tests:
50+
name: E2E Tests
51+
runs-on: ubuntu-latest
52+
needs: unit-tests
53+
steps:
54+
- uses: actions/checkout@v4
55+
56+
- name: Setup Docker Buildx
57+
uses: docker/setup-buildx-action@v3
58+
59+
- name: Install yq
60+
run: |
61+
sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq
62+
sudo chmod +x /usr/local/bin/yq
63+
64+
- name: Setup Kubernetes (k3s)
65+
run: |
66+
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --tls-san host.docker.internal" sh -
67+
mkdir -p $HOME/.kube
68+
sudo k3s kubectl config view --raw > $HOME/.kube/config
69+
sudo chmod 600 $HOME/.kube/config
70+
timeout 90 bash -c 'until sudo k3s kubectl cluster-info; do sleep 5; done'
71+
72+
- name: Create kubeconfig for CI
73+
run: |
74+
cat > backend/kubeconfig.yaml <<EOF
75+
apiVersion: v1
76+
kind: Config
77+
clusters:
78+
- name: ci-cluster
79+
cluster:
80+
server: https://host.docker.internal:6443
81+
insecure-skip-tls-verify: true
82+
users:
83+
- name: ci-user
84+
user:
85+
token: "ci-token"
86+
contexts:
87+
- name: ci
88+
context:
89+
cluster: ci-cluster
90+
user: ci-user
91+
current-context: ci
92+
EOF
93+
94+
- name: Pre-pull base images
95+
run: |
96+
docker pull python:3.12-slim &
97+
docker pull ghcr.io/astral-sh/uv:0.9.18 &
98+
docker pull node:22-alpine &
99+
docker pull confluentinc/cp-kafka:7.5.0 &
100+
docker pull confluentinc/cp-zookeeper:7.5.0 &
101+
docker pull mongo:8.0 &
102+
docker pull redis:7-alpine &
103+
wait
104+
105+
- name: Modify Docker Compose for CI
106+
run: |
107+
cp docker-compose.yaml docker-compose.ci.yaml
108+
yq eval '.services.backend.environment += ["TESTING=true"]' -i docker-compose.ci.yaml
109+
yq eval '.services.backend.environment += ["MONGO_ROOT_USER=root"]' -i docker-compose.ci.yaml
110+
yq eval '.services.backend.environment += ["MONGO_ROOT_PASSWORD=rootpassword"]' -i docker-compose.ci.yaml
111+
yq eval '.services.backend.environment += ["OTEL_SDK_DISABLED=true"]' -i docker-compose.ci.yaml
112+
yq eval '.services.backend.volumes = [.services.backend.volumes[] | select(. != "./backend:/app")]' -i docker-compose.ci.yaml
113+
yq eval '.services."k8s-worker".volumes = [.services."k8s-worker".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml
114+
yq eval '.services."pod-monitor".volumes = [.services."pod-monitor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml
115+
yq eval '.services."result-processor".volumes = [.services."result-processor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml
116+
yq eval 'del(.services.kafka.environment.KAFKA_OPTS)' -i docker-compose.ci.yaml
117+
yq eval 'del(.services.zookeeper.environment.KAFKA_OPTS)' -i docker-compose.ci.yaml
118+
yq eval 'del(.services.zookeeper.environment.ZOOKEEPER_AUTH_PROVIDER_1)' -i docker-compose.ci.yaml
119+
yq eval '.services.kafka.volumes = [.services.kafka.volumes[] | select(. | contains("jaas.conf") | not)]' -i docker-compose.ci.yaml
120+
yq eval '.services.zookeeper.volumes = [.services.zookeeper.volumes[] | select(. | contains("/etc/kafka") | not)]' -i docker-compose.ci.yaml
121+
yq eval '.services.zookeeper.environment.ZOOKEEPER_4LW_COMMANDS_WHITELIST = "ruok,srvr"' -i docker-compose.ci.yaml
122+
yq eval 'del(.services.zookeeper.healthcheck)' -i docker-compose.ci.yaml
123+
yq eval '.services.kafka.depends_on.zookeeper.condition = "service_started"' -i docker-compose.ci.yaml
124+
yq eval 'select(.services."cert-generator".extra_hosts == null).services."cert-generator".extra_hosts = []' -i docker-compose.ci.yaml
125+
yq eval '.services."cert-generator".extra_hosts += ["host.docker.internal:host-gateway"]' -i docker-compose.ci.yaml
126+
yq eval '.services."cert-generator".environment += ["CI=true"]' -i docker-compose.ci.yaml
127+
yq eval '.services."cert-generator".volumes += [env(HOME) + "/.kube/config:/root/.kube/config:ro"]' -i docker-compose.ci.yaml
128+
129+
- name: Build services
130+
uses: docker/bake-action@v6
131+
with:
132+
source: .
133+
files: docker-compose.ci.yaml
134+
load: true
135+
set: |
136+
*.cache-from=type=gha,scope=buildkit-${{ github.repository }}-${{ github.ref_name }}
137+
*.cache-from=type=gha,scope=buildkit-${{ github.repository }}-main
138+
*.cache-to=type=gha,mode=max,scope=buildkit-${{ github.repository }}-${{ github.ref_name }}
139+
env:
140+
BUILDKIT_PROGRESS: plain
141+
142+
- name: Start services
143+
run: |
144+
docker compose -f docker-compose.ci.yaml up -d --remove-orphans
145+
docker compose -f docker-compose.ci.yaml ps
146+
147+
- name: Wait for backend
148+
run: |
149+
curl --retry 60 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:443/api/v1/health/live
150+
151+
- name: Wait for frontend
152+
run: |
153+
curl --retry 30 --retry-delay 5 --retry-all-errors -ksf https://127.0.0.1:5001/ || true
154+
155+
- name: Setup Node.js
156+
uses: actions/setup-node@v4
157+
with:
158+
node-version: '22'
159+
cache: 'npm'
160+
cache-dependency-path: frontend/package-lock.json
161+
162+
- name: Install frontend dependencies
163+
working-directory: frontend
164+
run: npm ci
165+
166+
- name: Install Playwright browsers
167+
working-directory: frontend
168+
run: npx playwright install chromium
169+
170+
- name: Run E2E tests
171+
working-directory: frontend
172+
env:
173+
CI: true
174+
run: npx playwright test --reporter=html
175+
176+
- name: Upload Playwright report
177+
uses: actions/upload-artifact@v4
178+
if: always()
179+
with:
180+
name: playwright-report
181+
path: frontend/playwright-report/
182+
183+
- name: Collect logs on failure
184+
if: failure()
185+
run: |
186+
mkdir -p logs
187+
docker compose -f docker-compose.ci.yaml logs > logs/docker-compose.log
188+
docker compose -f docker-compose.ci.yaml logs frontend > logs/frontend.log
189+
docker compose -f docker-compose.ci.yaml logs backend > logs/backend.log
190+
191+
- name: Upload logs
192+
if: failure()
193+
uses: actions/upload-artifact@v4
194+
with:
195+
name: e2e-test-logs
196+
path: logs/

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
<img src="https://img.shields.io/github/actions/workflow/status/HardMax71/Integr8sCode/docker.yml?branch=main&label=docker&logo=docker&logoColor=white" alt="Docker Scan Status" />
1717
</a>
1818
<a href="https://github.com/HardMax71/Integr8sCode/actions/workflows/tests.yml">
19-
<img src="https://img.shields.io/github/actions/workflow/status/HardMax71/Integr8sCode/tests.yml?branch=main&label=tests&logo=pytest" alt="Tests Status" />
19+
<img src="https://img.shields.io/github/actions/workflow/status/HardMax71/Integr8sCode/tests.yml?branch=main&label=backend%20tests&logo=pytest" alt="Backend Tests Status" />
20+
</a>
21+
<a href="https://github.com/HardMax71/Integr8sCode/actions/workflows/frontend-tests.yml">
22+
<img src="https://img.shields.io/github/actions/workflow/status/HardMax71/Integr8sCode/frontend-tests.yml?branch=main&label=frontend%20tests&logo=vitest&logoColor=white" alt="Frontend Tests Status" />
2023
</a>
2124
<a href="https://codecov.io/gh/HardMax71/Integr8sCode">
2225
<img src="https://img.shields.io/codecov/c/github/HardMax71/Integr8sCode?flag=backend&label=backend%20coverage&logo=codecov" alt="Backend Coverage" />

frontend/e2e/auth.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Authentication', () => {
4+
test.beforeEach(async ({ page }) => {
5+
// Clear any existing auth state
6+
await page.context().clearCookies();
7+
});
8+
9+
test('shows login page with form elements', async ({ page }) => {
10+
await page.goto('/login');
11+
12+
await expect(page.locator('h2')).toContainText('Sign in to your account');
13+
await expect(page.locator('#username')).toBeVisible();
14+
await expect(page.locator('#password')).toBeVisible();
15+
await expect(page.locator('button[type="submit"]')).toBeVisible();
16+
});
17+
18+
test('shows validation when submitting empty form', async ({ page }) => {
19+
await page.goto('/login');
20+
21+
// HTML5 validation should prevent submission
22+
const usernameInput = page.locator('#username');
23+
await expect(usernameInput).toHaveAttribute('required', '');
24+
});
25+
26+
test('shows error with invalid credentials', async ({ page }) => {
27+
await page.goto('/login');
28+
29+
await page.fill('#username', 'invaliduser');
30+
await page.fill('#password', 'wrongpassword');
31+
await page.click('button[type="submit"]');
32+
33+
// Wait for error message to appear
34+
await expect(page.locator('p.text-red-600, p.text-red-400')).toBeVisible({ timeout: 10000 });
35+
});
36+
37+
test('redirects to editor on successful login', async ({ page }) => {
38+
await page.goto('/login');
39+
40+
// Use test credentials (adjust based on your test environment)
41+
await page.fill('#username', 'user');
42+
await page.fill('#password', 'user123');
43+
await page.click('button[type="submit"]');
44+
45+
// Should redirect to editor
46+
await expect(page).toHaveURL(/\/editor/, { timeout: 15000 });
47+
});
48+
49+
test('shows loading state during login', async ({ page }) => {
50+
await page.goto('/login');
51+
52+
await page.fill('#username', 'user');
53+
await page.fill('#password', 'user123');
54+
55+
// Start login but don't wait for it
56+
const submitButton = page.locator('button[type="submit"]');
57+
await submitButton.click();
58+
59+
// Button should show loading text
60+
await expect(submitButton).toContainText(/Logging in|Sign in/);
61+
});
62+
63+
test('redirects unauthenticated users from protected routes', async ({ page }) => {
64+
// Try to access protected route
65+
await page.goto('/editor');
66+
67+
// Should redirect to login
68+
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
69+
});
70+
71+
test('preserves redirect path after login', async ({ page }) => {
72+
// Try to access specific protected route
73+
await page.goto('/settings');
74+
75+
// Should redirect to login
76+
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
77+
78+
// Login
79+
await page.fill('#username', 'user');
80+
await page.fill('#password', 'user123');
81+
await page.click('button[type="submit"]');
82+
83+
// Should redirect back to settings
84+
await expect(page).toHaveURL(/\/settings/, { timeout: 15000 });
85+
});
86+
87+
test('has link to registration page', async ({ page }) => {
88+
await page.goto('/login');
89+
90+
const registerLink = page.locator('a[href="/register"]');
91+
await expect(registerLink).toBeVisible();
92+
await expect(registerLink).toContainText('create a new account');
93+
});
94+
95+
test('can navigate to registration page', async ({ page }) => {
96+
await page.goto('/login');
97+
98+
await page.click('a[href="/register"]');
99+
100+
await expect(page).toHaveURL(/\/register/);
101+
});
102+
});
103+
104+
test.describe('Logout', () => {
105+
test.beforeEach(async ({ page }) => {
106+
// Login first
107+
await page.goto('/login');
108+
await page.fill('#username', 'user');
109+
await page.fill('#password', 'user123');
110+
await page.click('button[type="submit"]');
111+
await expect(page).toHaveURL(/\/editor/, { timeout: 15000 });
112+
});
113+
114+
test('can logout from authenticated state', async ({ page }) => {
115+
// Find and click logout button (adjust selector based on your UI)
116+
const logoutButton = page.locator('button:has-text("Logout"), a:has-text("Logout"), [data-testid="logout"]');
117+
118+
if (await logoutButton.isVisible()) {
119+
await logoutButton.click();
120+
// Should redirect to login
121+
await expect(page).toHaveURL(/\/login/, { timeout: 10000 });
122+
}
123+
});
124+
});

0 commit comments

Comments
 (0)