test(e2e): audit 2.10 — flows critiques Auth, Upload, Purchase, Chat
- purchase.spec.ts: add to cart, checkout, success - chat.spec.ts: load UI, send message (when WebSocket available) - README: document critical flows and prerequisites
This commit is contained in:
parent
67271c7b34
commit
b9875c5e92
4 changed files with 252 additions and 6 deletions
|
|
@ -671,7 +671,7 @@ veza/
|
|||
| 2.7 | ~~Découper `config.go` (1 461 LOC)~~ | **M** | **✅ Fait** — séparé par domaine (env_helpers, db_init, redis_init, rabbitmq, rate_limit, cors, services_init, middlewares_init) |
|
||||
| 2.8 | ~~Gitignorer les fichiers `.out`, test results, `.turbo/`~~ | **S** | **✅ Fait** — .gitignore mis à jour |
|
||||
| 2.9 | ~~Aligner versions Tokio dans `veza-common`~~ | **S** | **✅ Fait** — 1.0 → 1.35 (veza-common, stream-server/tools) |
|
||||
| 2.10 | Ajouter des tests d'intégration E2E pour les flows critiques | **L** | Auth, Upload, Purchase, Chat |
|
||||
| 2.10 | ~~Ajouter des tests d'intégration E2E pour les flows critiques~~ | **L** | **✅ Fait** — Auth, Upload (existants), Purchase, Chat (purchase.spec.ts, chat.spec.ts) |
|
||||
|
||||
### Phase 3 — AMÉLIORATION & PRÉPARATION SCALE (6-12 semaines)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,19 @@
|
|||
|
||||
Ce document liste les parcours critiques couverts par les tests E2E Playwright et les fichiers associés.
|
||||
|
||||
## Parcours critiques
|
||||
## Parcours critiques (Audit 2.10)
|
||||
|
||||
| Parcours | Fichier(s) | Description |
|
||||
|----------|------------|-------------|
|
||||
| **Auth** | `tests/auth.spec.ts` | Login, register, logout, route guards, token refresh. Optionnel : 2FA (compte test dédié). |
|
||||
| **Upload** | `tests/upload.spec.ts` | Upload fichier, upload par chunks. |
|
||||
| **Purchase** | `tests/purchase.spec.ts` | Marketplace → Add to cart → Checkout → Success. État panier vide. |
|
||||
| **Chat** | `tests/chat.spec.ts` | Load /chat, UI (Channels, input), état connecté/déconnecté. Envoi message (skip si WebSocket indisponible). |
|
||||
| **Smoke** | `tests/smoke.spec.ts` | Login → Upload → Création playlist → Ajout track. |
|
||||
| **Playlists** | `tests/playlists.spec.ts` | Création, liste, modification, ajout/suppression de tracks, suppression playlist, recherche. |
|
||||
| **Search** | `tests/search.spec.ts` | Navigation vers `/search`, saisie requête, vérification des résultats (tracks/playlists) ou état vide. |
|
||||
| **Play** | `tests/play.spec.ts` | Après login : search → clic sur un track → page track ou player visible (ou état vide si pas de résultats). |
|
||||
| **Profile** | `tests/profile.spec.ts` | Affichage profil, informations compte. |
|
||||
| **Upload** | `tests/upload.spec.ts` | Upload fichier, upload par chunks. |
|
||||
| **Post-deploy smoke** | `tests/smoke-post-deploy.spec.ts` | Health checks (homepage, login, API) against deployed URL. |
|
||||
|
||||
## Post-deploy smoke tests
|
||||
|
|
@ -33,9 +35,10 @@ In CI (cd.yml), the smoke job runs after deploy when `STAGING_URL` (secret or va
|
|||
|
||||
## Prérequis
|
||||
|
||||
- Frontend servi (ex. `npm run dev`) sur l'URL configurée dans `TEST_CONFIG.FRONTEND_URL`.
|
||||
- Backend API disponible pour auth, search, playlists, upload.
|
||||
- Compte de test valide (voir `e2e/utils/test-helpers.ts` : `TEST_USERS.default`).
|
||||
- **Frontend** : servi (ex. `npm run dev`) sur l'URL configurée dans `TEST_CONFIG.FRONTEND_URL` (défaut : http://localhost:5173).
|
||||
- **Backend API** : disponible pour auth, search, playlists, upload, marketplace (défaut : http://localhost:8080/api/v1).
|
||||
- **Chat server** (optionnel) : pour les tests Chat complets (envoi de message). Sans chat server, les tests Chat font du smoke (load UI, état déconnecté).
|
||||
- **Compte de test** : voir `e2e/utils/test-helpers.ts` : `TEST_USERS.default` (ou `TEST_EMAIL`, `TEST_PASSWORD`).
|
||||
|
||||
## Lancer les E2E
|
||||
|
||||
|
|
@ -52,6 +55,12 @@ Pour un fichier précis :
|
|||
npx playwright test e2e/tests/auth.spec.ts
|
||||
```
|
||||
|
||||
Tous les flows critiques (Auth, Upload, Purchase, Chat) :
|
||||
|
||||
```bash
|
||||
npx playwright test e2e/tests/auth.spec.ts e2e/tests/upload.spec.ts e2e/tests/purchase.spec.ts e2e/tests/chat.spec.ts
|
||||
```
|
||||
|
||||
**Machine à ressources limitées** : lancer **un seul spec** à la fois et **un seul projet** (chromium) pour éviter saturation CPU/RAM. Les specs auth, smoke, playlists, search nécessitent que le **Backend API** soit démarré (sinon les appels API échouent en 500). En CI, la suite complète tourne dans le cloud.
|
||||
|
||||
```bash
|
||||
|
|
|
|||
137
apps/web/e2e/tests/chat.spec.ts
Normal file
137
apps/web/e2e/tests/chat.spec.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_CONFIG, loginAsUser, setupErrorCapture } from '../utils/test-helpers';
|
||||
|
||||
/**
|
||||
* Chat E2E Test Suite (Audit 2.10)
|
||||
*
|
||||
* Couvre le flow critique du chat :
|
||||
* - Load chat page et UI (sidebar, channels, input)
|
||||
* - État déconnecté quand WebSocket indisponible
|
||||
* - Envoi de message quand WebSocket connecté (skip si non connecté)
|
||||
*/
|
||||
|
||||
test.describe('Chat Flow', () => {
|
||||
let consoleErrors: string[] = [];
|
||||
let networkErrors: Array<{ url: string; status: number; method: string }> = [];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const errorCapture = setupErrorCapture(page);
|
||||
consoleErrors = errorCapture.consoleErrors;
|
||||
networkErrors = errorCapture.networkErrors;
|
||||
});
|
||||
|
||||
test('should load chat page and show UI', async ({ page }) => {
|
||||
console.log('🧪 [CHAT] Running: Load chat page and show UI');
|
||||
|
||||
await loginAsUser(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/chat`);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const channelsHeader = page.locator('h3:has-text("Channels")');
|
||||
const loadingState = page.locator('text=/ESTABLISHING UPLINK|Establishing/i');
|
||||
const errorState = page.locator('text=/Connection Terminated|Access Restricted/i');
|
||||
const messageInput = page.locator('input[placeholder*="Broadcast"], input[aria-label*="message" i]');
|
||||
|
||||
const hasChannels = await channelsHeader.isVisible({ timeout: 5000 }).catch(() => false);
|
||||
const hasLoading = await loadingState.isVisible({ timeout: 2000 }).catch(() => false);
|
||||
const hasError = await errorState.isVisible({ timeout: 2000 }).catch(() => false);
|
||||
const hasInput = await messageInput.isVisible({ timeout: 5000 }).catch(() => false);
|
||||
|
||||
expect(hasChannels || hasLoading || hasError).toBeTruthy();
|
||||
|
||||
if (hasChannels) {
|
||||
expect(hasInput).toBeTruthy();
|
||||
console.log('✅ [CHAT] Chat UI loaded with Channels and input');
|
||||
} else if (hasLoading) {
|
||||
console.log('✅ [CHAT] Chat page showing loading state');
|
||||
} else if (hasError) {
|
||||
console.log('✅ [CHAT] Chat page showing error state (expected when chat server unavailable)');
|
||||
}
|
||||
});
|
||||
|
||||
test('should show disconnected state when WebSocket unavailable', async ({ page }) => {
|
||||
console.log('🧪 [CHAT] Running: Disconnected state indicator');
|
||||
|
||||
await loginAsUser(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/chat`);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const channelsHeader = page.locator('h3:has-text("Channels")');
|
||||
const statusDot = page.locator('.rounded-full.w-2.h-2, div.w-2.h-2.rounded-full');
|
||||
|
||||
const hasChannels = await channelsHeader.isVisible({ timeout: 10000 }).catch(() => false);
|
||||
test.skip(!hasChannels, 'Chat UI not loaded - may be loading or error state');
|
||||
|
||||
const hasStatusDot = await statusDot.first().isVisible({ timeout: 3000 }).catch(() => false);
|
||||
expect(hasStatusDot).toBeTruthy();
|
||||
|
||||
const dotClasses = await statusDot.first().getAttribute('class').catch(() => '');
|
||||
const isDisconnected = dotClasses?.includes('bg-destructive') ?? false;
|
||||
const isConnected = dotClasses?.includes('bg-success') ?? false;
|
||||
|
||||
expect(isDisconnected || isConnected).toBeTruthy();
|
||||
console.log(`✅ [CHAT] Status indicator visible (connected: ${isConnected}, disconnected: ${isDisconnected})`);
|
||||
});
|
||||
|
||||
test('should send and display message when WebSocket connected', async ({ page }) => {
|
||||
console.log('🧪 [CHAT] Running: Send message (when WebSocket connected)');
|
||||
|
||||
await loginAsUser(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/chat`);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const channelsHeader = page.locator('h3:has-text("Channels")');
|
||||
const statusDot = page.locator('div.w-2.h-2.rounded-full, .rounded-full.w-2.h-2').first();
|
||||
const conversationItem = page.locator('[data-testid="conversation-item"], button, [role="button"]').filter({ hasText: /general|default|lobby|channel/i }).first();
|
||||
const messageInput = page.locator('input[placeholder*="Broadcast"], input[aria-label*="message" i]');
|
||||
const sendButton = page.locator('button[type="submit"]').filter({ has: page.locator('svg') });
|
||||
|
||||
const hasChannels = await channelsHeader.isVisible({ timeout: 10000 }).catch(() => false);
|
||||
test.skip(!hasChannels, 'Chat UI not loaded');
|
||||
|
||||
const dotClasses = await statusDot.getAttribute('class').catch(() => '');
|
||||
const isConnected = dotClasses?.includes('bg-success') ?? false;
|
||||
test.skip(!isConnected, 'WebSocket not connected - chat server may be unavailable');
|
||||
|
||||
const hasConversation = await conversationItem.isVisible({ timeout: 3000 }).catch(() => false);
|
||||
test.skip(!hasConversation, 'No conversation/channel available to send message');
|
||||
|
||||
await conversationItem.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const testMessage = `E2E test ${Date.now()}`;
|
||||
await messageInput.fill(testMessage);
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await sendButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const messageVisible = await page.locator(`text="${testMessage}"`).isVisible({ timeout: 5000 }).catch(() => false);
|
||||
expect(messageVisible).toBeTruthy();
|
||||
console.log('✅ [CHAT] Message sent and displayed');
|
||||
});
|
||||
|
||||
test.afterEach(async ({}, testInfo) => {
|
||||
console.log('\n📊 [CHAT] === Final Verifications ===');
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log(`🔴 [CHAT] Console errors (${consoleErrors.length}):`);
|
||||
consoleErrors.forEach((e) => console.log(` - ${e}`));
|
||||
}
|
||||
if (networkErrors.length > 0) {
|
||||
console.log(`🔴 [CHAT] Network errors (${networkErrors.length}):`);
|
||||
networkErrors.forEach((e) => console.log(` - ${e.method} ${e.url}: ${e.status}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
100
apps/web/e2e/tests/purchase.spec.ts
Normal file
100
apps/web/e2e/tests/purchase.spec.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
TEST_CONFIG,
|
||||
loginAsUser,
|
||||
setupErrorCapture,
|
||||
waitForToast,
|
||||
} from '../utils/test-helpers';
|
||||
|
||||
/**
|
||||
* Purchase E2E Test Suite (Audit 2.10)
|
||||
*
|
||||
* Couvre le flow critique d'achat :
|
||||
* - Marketplace → Add to cart → Checkout → Success
|
||||
* - Cart empty state
|
||||
*/
|
||||
|
||||
test.describe('Purchase Flow', () => {
|
||||
let consoleErrors: string[] = [];
|
||||
let networkErrors: Array<{ url: string; status: number; method: string }> = [];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const errorCapture = setupErrorCapture(page);
|
||||
consoleErrors = errorCapture.consoleErrors;
|
||||
networkErrors = errorCapture.networkErrors;
|
||||
});
|
||||
|
||||
test('should add product to cart and checkout successfully', async ({ page }) => {
|
||||
console.log('🧪 [PURCHASE] Running: Add to cart and checkout');
|
||||
|
||||
await loginAsUser(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/marketplace`);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
||||
|
||||
const productCard = page.locator('article[aria-label^="Product:"]').first();
|
||||
await expect(productCard).toBeVisible({ timeout: 10000 });
|
||||
|
||||
await productCard.hover();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const addToCartButton = page.locator('button:has-text("Add to Cart")').first();
|
||||
await expect(addToCartButton).toBeVisible({ timeout: 5000 });
|
||||
await addToCartButton.click();
|
||||
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const cartButton = page.locator('button:has-text("Cart")').first();
|
||||
await expect(cartButton).toBeVisible({ timeout: 5000 });
|
||||
await cartButton.click();
|
||||
|
||||
const cartDialog = page.locator('[role="dialog"]').filter({ hasText: 'Shopping Cart' });
|
||||
await expect(cartDialog).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const checkoutButton = cartDialog.locator('button:has-text("Checkout")');
|
||||
await expect(checkoutButton).toBeVisible({ timeout: 3000 });
|
||||
await checkoutButton.click();
|
||||
|
||||
const successToast = await waitForToast(page, 'success', 15000);
|
||||
expect(successToast.toLowerCase()).toMatch(/order|success|placed/);
|
||||
|
||||
console.log('✅ [PURCHASE] Checkout successful');
|
||||
});
|
||||
|
||||
test('should show cart empty message when no items', async ({ page }) => {
|
||||
console.log('🧪 [PURCHASE] Running: Cart empty state');
|
||||
|
||||
await loginAsUser(page);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/marketplace`);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
||||
|
||||
const cartButton = page.locator('button:has-text("Cart")').first();
|
||||
await expect(cartButton).toBeVisible({ timeout: 5000 });
|
||||
await cartButton.click();
|
||||
|
||||
const cartDialog = page.locator('[role="dialog"]').filter({ hasText: 'Shopping Cart' });
|
||||
await expect(cartDialog).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const emptyMessage = cartDialog.locator('text=/cart is empty|your cart is empty/i');
|
||||
await expect(emptyMessage).toBeVisible({ timeout: 5000 });
|
||||
|
||||
console.log('✅ [PURCHASE] Cart empty message displayed');
|
||||
});
|
||||
|
||||
test.afterEach(async ({}, testInfo) => {
|
||||
console.log('\n📊 [PURCHASE] === Final Verifications ===');
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log(`🔴 [PURCHASE] Console errors (${consoleErrors.length}):`);
|
||||
consoleErrors.forEach((e) => console.log(` - ${e}`));
|
||||
}
|
||||
if (networkErrors.length > 0) {
|
||||
console.log(`🔴 [PURCHASE] Network errors (${networkErrors.length}):`);
|
||||
networkErrors.forEach((e) => console.log(` - ${e.method} ${e.url}: ${e.status}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue