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>
This commit is contained in:
+138
-41
@@ -12,56 +12,153 @@ import { test, expect } from '@playwright/test';
|
||||
test.describe('User Journey E2E', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.fixme('should register a new user', async ({ page }) => {
|
||||
// TODO: Navigate to /web or /login
|
||||
// Switch to register tab (text=Inscription)
|
||||
// Fill #registerUsername with unique username
|
||||
// Fill #registerPassword and #registerPasswordConfirm
|
||||
// Click #registerSubmit
|
||||
// Wait for API response via waitForResponse(r => r.url().includes('/api/auth/register'))
|
||||
// Verify #authSuccess becomes visible or contains success message
|
||||
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);
|
||||
});
|
||||
|
||||
test.fixme('should login with registered credentials', async ({ page }) => {
|
||||
// TODO: Navigate to /login
|
||||
// Fill #loginUsername and #loginPassword
|
||||
// Click #loginSubmit
|
||||
// Wait for response via waitForResponse(r => r.url().includes('/api/auth/login'))
|
||||
// Verify redirect to /web or home page
|
||||
// Verify auth token is stored (check localStorage or cookie)
|
||||
// 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();
|
||||
});
|
||||
|
||||
test.fixme('should browse homepage without errors', async ({ page }) => {
|
||||
// TODO: Navigate to /web
|
||||
// Wait for page to load via waitForSelector for main content area
|
||||
// Verify no console errors
|
||||
// Verify page title or main heading is visible
|
||||
// Note: content may be empty in test env — just verify no crash
|
||||
// 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);
|
||||
});
|
||||
|
||||
test.fixme('should search for anime', async ({ page }) => {
|
||||
// TODO: Click on anime search tab (if tabs exist)
|
||||
// Fill #animeSearchInput with "Naruto"
|
||||
// Submit the search form (trigger HTMX request)
|
||||
// Wait for response via waitForResponse(r => r.url().includes('/api/anime/search'))
|
||||
// Verify search results appear or "no results" message shown
|
||||
// Verify results container has expected selectors
|
||||
// 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();
|
||||
});
|
||||
|
||||
test.fixme('should update settings', async ({ page }) => {
|
||||
// TODO: Click on settings tab or navigate to settings section
|
||||
// Wait for settings panel to load via waitForResponse(r => r.url().includes('/api/settings'))
|
||||
// Verify #default_lang dropdown exists
|
||||
// Change language setting (select different option)
|
||||
// Submit/save settings form
|
||||
// Wait for response via waitForResponse(r => r.url().includes('/api/settings') && r.request().method() === 'PATCH')
|
||||
// Verify success toast notification appears
|
||||
// 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 });
|
||||
});
|
||||
|
||||
test.fixme('should logout successfully', async ({ page }) => {
|
||||
// TODO: Click logout button
|
||||
// Wait for response via waitForResponse(r => r.url().includes('/api/auth/logout'))
|
||||
// Verify redirect to login page or auth state is cleared
|
||||
// Verify protected content is no longer accessible
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user