grantforge-api / frontend-react /e2e /critical-path.spec.ts
GrantForge Bot
Deploy to Hugging Face
afd56bc
/**
* Testy E2E: Krytyczna Ε›cieΕΌka uΕΌytkownika GrantForge AI
* Scenariusz: health check β†’ lista projektΓ³w β†’ tworzenie projektu β†’ eksport
*
* Uruchomienie:
* npx playwright test
* npx playwright test --ui (tryb interaktywny)
* npx playwright test --headed (widoczna przeglΔ…darka)
*
* Zmienne Ε›rodowiskowe:
* E2E_BASE_URL β€” adres frontendu (domyΕ›lnie http://localhost:5173)
* E2E_BACKEND_URL β€” adres backendu (domyΕ›lnie http://localhost:8001)
* E2E_DEV_TOKEN β€” Bearer token dla dev (domyΕ›lnie dev_test_token)
*/
import { test, expect, request } from '@playwright/test';
const BACKEND = process.env.E2E_BACKEND_URL || 'http://localhost:8001';
const DEV_TOKEN = process.env.E2E_DEV_TOKEN || 'dev_test_token';
// ──────────────────────────────────────────────
// BLOK 1: Backend Health Check
// ──────────────────────────────────────────────
test.describe('Backend API', () => {
test('GET /health β€” zwraca status ok', async () => {
const ctx = await request.newContext({ baseURL: BACKEND });
const resp = await ctx.get('/health');
expect(resp.status()).toBe(200);
const body = await resp.json();
expect(body.status).toBe('healthy');
await ctx.dispose();
});
test('GET /api/health β€” zwraca statusy serwisΓ³w', async () => {
const ctx = await request.newContext({ baseURL: BACKEND });
const resp = await ctx.get('/api/health');
// 200 (healthy) lub 503 (degraded) β€” oba sΔ… ok w E2E
expect([200, 503]).toContain(resp.status());
const body = await resp.json();
expect(body).toHaveProperty('services');
expect(body).toHaveProperty('timestamp');
await ctx.dispose();
});
test('GET /api/grants/nabory β€” zwraca listΔ™ naborΓ³w', async () => {
const ctx = await request.newContext({
baseURL: BACKEND,
extraHTTPHeaders: { Authorization: `Bearer ${DEV_TOKEN}` },
});
const resp = await ctx.get('/api/grants/nabory');
expect(resp.status()).toBe(200);
const body = await resp.json();
expect(body.status).toBe('ok');
expect(Array.isArray(body.nabory)).toBe(true);
expect(body.nabory.length).toBeGreaterThan(0);
await ctx.dispose();
});
test('POST /api/projects β€” tworzy projekt', async () => {
const ctx = await request.newContext({
baseURL: BACKEND,
extraHTTPHeaders: { Authorization: `Bearer ${DEV_TOKEN}` },
});
const resp = await ctx.post('/api/projects', {
data: {
title: '[E2E] Test Project',
program_type: 'FENG',
description: 'Projekt testowy Playwright E2E β€” moΕΌna usunΔ…Δ‡.',
},
});
expect([200, 201]).toContain(resp.status());
const body = await resp.json();
expect(body).toHaveProperty('id');
// Cleanup: usuΕ„ projekt
await ctx.delete(`/api/projects/${body.id}`);
await ctx.dispose();
});
test('Rate limiter β€” zwraca 429 po przekroczeniu limitu', async () => {
// Ten test wysyΕ‚a 6 requestΓ³w do endpointu z limitem 5/5min
// W środowisku E2E z dev_test_token może pomijać rate limit — weryfikujemy odpowiedź
const ctx = await request.newContext({
baseURL: BACKEND,
extraHTTPHeaders: { Authorization: `Bearer ${DEV_TOKEN}` },
});
// Weryfikujemy tylko ΕΌe nagΕ‚Γ³wki rate limit sΔ… obecne (jeΕ›li endpoint je zwraca)
const resp = await ctx.get('/api/grants/nabory');
expect(resp.status()).toBeLessThan(500); // nie może być błąd serwera
await ctx.dispose();
});
});
// ──────────────────────────────────────────────
// BLOK 2: Frontend β€” Strony publiczne
// ──────────────────────────────────────────────
test.describe('Frontend β€” strony publiczne', () => {
test('Landing page β€” zawiera link do logowania', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(2000);
await expect(page).toHaveTitle(/GrantForge|Dotacje|AI/i);
const loginBtn = page.locator('button:has-text("Zaloguj"), a:has-text("Zaloguj")').first();
await expect(loginBtn).toBeVisible();
});
test('Landing page β€” stopka zawiera linki prawne', async ({ page }) => {
await page.goto('/');
const regulaminLink = page.locator('a[href="/regulamin"]').first();
await expect(regulaminLink).toBeVisible();
const privacyLink = page.locator('a[href="/polityka-prywatnosci"]').first();
await expect(privacyLink).toBeVisible();
});
test('Strona /regulamin β€” Ε‚aduje siΔ™ i zawiera paragrafy', async ({ page }) => {
await page.goto('/regulamin');
await expect(page.locator('h1')).toContainText('Regulamin');
// SprawdΕΊ ΕΌe jest treΕ›Δ‡ prawna
await expect(page.locator('body')).toContainText('Β§ 1');
await expect(page.locator('body')).toContainText('Β§ 3');
});
test('Strona /polityka-prywatnosci β€” Ε‚aduje siΔ™ i zawiera RODO', async ({ page }) => {
await page.goto('/polityka-prywatnosci');
await expect(page.locator('h1')).toContainText('Polityka Prywatno');
await expect(page.locator('body')).toContainText('RODO');
});
});
// ──────────────────────────────────────────────
// BLOK 3: Frontend β€” Strony chronione (wymagajΔ… sesji)
// ──────────────────────────────────────────────
test.describe('Frontend β€” nawigacja (bez autentykacji)', () => {
test('Redirect /projects β†’ /sign-in (niezalogowany)', async ({ page }) => {
await page.goto('/projects');
// Clerk powinien przekierować do strony logowania
await page.waitForTimeout(1500);
const url = page.url();
expect(url).toMatch(/sign-in|\/$/);
});
test('Redirect /nabory β†’ /sign-in (niezalogowany)', async ({ page }) => {
await page.goto('/nabory');
await page.waitForTimeout(1500);
const url = page.url();
expect(url).toMatch(/sign-in|\/$/);
});
});