Files
ohm_streaming/tests/e2e/user_journey.spec.ts
root 29c051be69
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled
test: implement E2E user journey tests with Playwright
- Implement registration flow with API response verification
- Implement login flow with token storage validation
- Implement homepage browsing with JS error detection
- Implement anime search with HTMX debounce handling
- Implement settings update with PATCH request verification
- Implement logout flow with redirect and token cleanup
- Convert all .fixme() tests to executable test() functions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-31 16:19:46 +00:00

165 lines
6.2 KiB
TypeScript

import { test, expect } from '@playwright/test';
/**
* User Journey E2E Tests
*
* Simulates a complete user flow: register → login → browse → search → settings → logout.
* All tests are serial because they share browser state (auth token, navigation).
*
* FORBIDDEN: Do NOT use page.waitForTimeout() — use waitForResponse() or waitForSelector()
*/
test.describe('User Journey E2E', () => {
test.describe.configure({ mode: 'serial' });
const testData = {
username: `e2e_user_${Date.now()}`,
password: 'TestPass123!',
};
// Register a new user account via the UI form
test('should register a new user', async ({ page }) => {
await page.goto('/login');
// Switch to the register tab
await page.click('text=Inscription');
// Fill out the registration form
await page.fill('#registerUsername', testData.username);
await page.fill('#registerPassword', testData.password);
await page.fill('#registerPasswordConfirm', testData.password);
// Submit and wait for the API response
const [response] = await Promise.all([
page.waitForResponse((resp) => resp.url().includes('/api/auth/register')),
page.click('#registerSubmit'),
]);
// Registration should succeed (201 or 200)
expect(response.status()).toBeLessThan(400);
// Verify the success message appears
await page.locator('#authSuccess').waitFor({ state: 'visible', timeout: 10000 });
const successText = await page.locator('#authSuccess').textContent();
expect(successText).toMatch(/réussie|inscription/i);
});
// Login with the credentials registered in the previous test
test('should login with registered credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('#loginUsername', testData.username);
await page.fill('#loginPassword', testData.password);
// Submit and wait for the login API response
const [response] = await Promise.all([
page.waitForResponse((resp) => resp.url().includes('/api/auth/login')),
page.click('#loginSubmit'),
]);
expect(response.status()).toBeLessThan(400);
// Verify success message
await page.locator('#authSuccess').waitFor({ state: 'visible', timeout: 10000 });
const successText = await page.locator('#authSuccess').textContent();
expect(successText).toMatch(/réussie/i);
// Wait for redirect to /web
await page.waitForURL('**/web**', { timeout: 10000 });
// Verify the auth token is stored in localStorage
const token = await page.evaluate(() => localStorage.getItem('auth_token'));
expect(token).toBeTruthy();
});
// Browse the homepage — verify layout loads without JS errors
test('should browse homepage without errors', async ({ page }) => {
// Collect JS page errors
const errors: string[] = [];
page.on('pageerror', (err) => errors.push(err.message));
// Ensure we are on /web (carried over from login)
if (!page.url().includes('/web')) {
await page.goto('/web');
}
// Wait for main content area to be visible
await page.locator('#main-content').waitFor({ state: 'visible', timeout: 10000 });
// Verify the header heading
await expect(page.locator('header h1')).toContainText('Ohm Stream');
// Verify at least one navigation tab is visible
await expect(page.locator('.tab').first()).toBeVisible();
// Verify the user info panel (logged-in state indicator)
await expect(page.locator('#userInfo')).toBeVisible();
// No JavaScript errors should have been thrown
expect(errors).toHaveLength(0);
});
// Search for an anime via the Anime tab — results may be empty but the UI must respond
test('should search for anime', async ({ page }) => {
// Navigate to the Anime tab
await page.click('.tab:has-text("Anime")');
await page.locator('#tab-anime').waitFor({ state: 'visible', timeout: 5000 });
// Fill the search input — HTMX debounce triggers the request automatically
await page.fill('#animeSearchInput', 'Naruto');
// Wait for either results, an empty-state message, or the loading spinner to disappear
await Promise.race([
page.locator('#animeSearchResults .sr-card').first().waitFor({ timeout: 15000 }),
page.locator('#animeSearchResults .sr-empty').first().waitFor({ timeout: 15000 }),
page.locator('#search-loading').waitFor({ state: 'detached', timeout: 15000 }),
]);
// The search results container must be present regardless of result count
await expect(page.locator('#animeSearchResults')).toBeVisible();
});
// Change a setting (language) and verify the PATCH response and toast notification
test('should update settings', async ({ page }) => {
// Open the settings tab
await page.click('.tab:has-text("Paramètres")');
// Settings panel is loaded dynamically via HTMX — wait for the form
await page.locator('#default_lang').waitFor({ state: 'visible', timeout: 15000 });
// Change the default language
await page.selectOption('#default_lang', 'vf');
// Submit the settings form and capture the PATCH response
const [response] = await Promise.all([
page.waitForResponse(
(resp) => resp.url().includes('/api/settings') && resp.request().method() === 'PATCH'
),
page.locator('form[hx-patch="/api/settings"] button[type="submit"]').click(),
]);
expect(response.status()).toBe(200);
// Verify a toast notification appears confirming the save
await page.locator('.toast').first().waitFor({ state: 'visible', timeout: 5000 });
});
// Logout — verify the API call succeeds, redirect happens, and token is cleared
test('should logout successfully', async ({ page }) => {
// Click the logout button and wait for the API response
const [response] = await Promise.all([
page.waitForResponse((resp) => resp.url().includes('/api/auth/logout')),
page.locator('#userInfo button:has-text("Déconnexion")').click(),
]);
expect(response.status()).toBeLessThan(400);
// Should be redirected back to the login page
await page.waitForURL('**/login**', { timeout: 10000 });
// The auth token must be cleared from localStorage
const token = await page.evaluate(() => localStorage.getItem('auth_token'));
expect(token).toBeNull();
});
});