fix: update E2E tests for multi-theme migration (settings, sources, generation)

- settings.spec.ts: test batch_size instead of removed theme field
- settings-export.spec.ts: test batch_size export/import, open collapsed details section
- sources.spec.ts: convert to API-based test since sources now belong to themes
- generation-live.spec.ts: already updated (no changes needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent 6f3e6883c9
commit 885af2d147

@ -2,11 +2,11 @@
* E2E test: Settings export/import flow. * E2E test: Settings export/import flow.
* *
* Validates: * Validates:
* 1. Set theme to "Export Test" and save * 1. Set batch_size to 10 and save
* 2. Export settings (triggers download) * 2. Export settings (triggers download)
* 3. Change theme to "Changed" and save * 3. Change batch_size to 15 and save
* 4. Import the previously exported file * 4. Import the previously exported file
* 5. Assert theme shows "Export Test" again * 5. Assert batch_size shows 10 again
*/ */
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
@ -27,9 +27,9 @@ test.describe('Settings export/import', () => {
page.locator('h1', { hasText: 'Parametres de generation' }), page.locator('h1', { hasText: 'Parametres de generation' }),
).toBeVisible({ timeout: 10_000 }); ).toBeVisible({ timeout: 10_000 });
// Step 3: Set theme to "Export Test" // Step 3: Set batch_size to 10
const themeInput = page.locator('#theme'); const batchSizeInput = page.locator('#batchSize');
await themeInput.fill('Export Test'); await batchSizeInput.fill('10');
// Save // Save
await page await page
@ -41,7 +41,10 @@ test.describe('Settings export/import', () => {
page.getByText('Parametres enregistres avec succes'), page.getByText('Parametres enregistres avec succes'),
).toBeVisible({ timeout: 5_000 }); ).toBeVisible({ timeout: 5_000 });
// Step 4: Click the export button and capture the download // Step 4: Open the Import/Export section (it's inside a <details> element)
await page.locator('summary', { hasText: /import/i }).click();
// Click the export button and capture the download
const downloadPromise = page.waitForEvent('download'); const downloadPromise = page.waitForEvent('download');
await page.locator('button', { hasText: 'Exporter' }).first().click(); await page.locator('button', { hasText: 'Exporter' }).first().click();
@ -55,13 +58,13 @@ test.describe('Settings export/import', () => {
); );
await download.saveAs(downloadPath); await download.saveAs(downloadPath);
// Verify the downloaded file contains our theme // Verify the downloaded file contains our batch_size
const fileContent = fs.readFileSync(downloadPath, 'utf-8'); const fileContent = fs.readFileSync(downloadPath, 'utf-8');
const parsed = JSON.parse(fileContent); const parsed = JSON.parse(fileContent);
expect(parsed.theme).toBe('Export Test'); expect(parsed.batch_size).toBe(10);
// Step 5: Change theme to "Changed" and save // Step 5: Change batch_size to 15 and save
await themeInput.fill('Changed'); await batchSizeInput.fill('15');
await page await page
.locator('button', { hasText: 'Enregistrer les parametres' }) .locator('button', { hasText: 'Enregistrer les parametres' })
.click(); .click();
@ -71,7 +74,7 @@ test.describe('Settings export/import', () => {
).toBeVisible({ timeout: 5_000 }); ).toBeVisible({ timeout: 5_000 });
// Verify it changed // Verify it changed
await expect(themeInput).toHaveValue('Changed'); await expect(batchSizeInput).toHaveValue('15');
// Step 6: Import the previously downloaded file // Step 6: Import the previously downloaded file
// The import button triggers a hidden file input. We need to set the file // The import button triggers a hidden file input. We need to set the file
@ -84,8 +87,8 @@ test.describe('Settings export/import', () => {
page.getByText("Configuration importee avec succes"), page.getByText("Configuration importee avec succes"),
).toBeVisible({ timeout: 5_000 }); ).toBeVisible({ timeout: 5_000 });
// Step 7: Assert the theme input shows "Export Test" again // Step 7: Assert the batch_size input shows 10 again
await expect(themeInput).toHaveValue('Export Test'); await expect(batchSizeInput).toHaveValue('10');
// Clean up temp file // Clean up temp file
try { try {

@ -3,17 +3,17 @@
* *
* Validates that a user can: * Validates that a user can:
* 1. Navigate to /settings * 1. Navigate to /settings
* 2. Change the theme input * 2. Change the batch_size input
* 3. Save * 3. Save
* 4. Reload the page * 4. Reload the page
* 5. Assert the theme value persisted * 5. Assert the batch_size value persisted
*/ */
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { loginAsUser } from '../helpers/auth'; import { loginAsUser } from '../helpers/auth';
test.describe('Settings', () => { test.describe('Settings', () => {
test('should save and persist theme setting across page reloads', async ({ test('should save and persist batch_size setting across page reloads', async ({
page, page,
}) => { }) => {
// Step 1: Login as regular user via cookie injection // Step 1: Login as regular user via cookie injection
@ -27,15 +27,15 @@ test.describe('Settings', () => {
page.locator('h1', { hasText: 'Parametres de generation' }), page.locator('h1', { hasText: 'Parametres de generation' }),
).toBeVisible({ timeout: 10_000 }); ).toBeVisible({ timeout: 10_000 });
// Step 3: Change the theme input to "Cybersecurite" // Step 3: Change the batch_size input to 10
const themeInput = page.locator('#theme'); const batchSizeInput = page.locator('#batchSize');
await expect(themeInput).toBeVisible(); await expect(batchSizeInput).toBeVisible();
// Clear existing value and type new one // Clear existing value and type new one
await themeInput.fill('Cybersecurite'); await batchSizeInput.fill('10');
// Verify the input has the new value // Verify the input has the new value
await expect(themeInput).toHaveValue('Cybersecurite'); await expect(batchSizeInput).toHaveValue('10');
// Step 4: Click the save button // Step 4: Click the save button
const saveButton = page.locator('button', { const saveButton = page.locator('button', {
@ -56,7 +56,7 @@ test.describe('Settings', () => {
page.locator('h1', { hasText: 'Parametres de generation' }), page.locator('h1', { hasText: 'Parametres de generation' }),
).toBeVisible({ timeout: 10_000 }); ).toBeVisible({ timeout: 10_000 });
// Step 6: Assert the theme input value is "Cybersecurite" // Step 6: Assert the batch_size input value is 10
await expect(page.locator('#theme')).toHaveValue('Cybersecurite'); await expect(page.locator('#batchSize')).toHaveValue('10');
}); });
}); });

@ -1,107 +1,130 @@
/** /**
* E2E test: Sources CRUD. * E2E test: Sources CRUD via API.
* *
* Validates: * Validates:
* 1. Adding a single source * 1. Creating a theme (sources now belong to themes)
* 2. Bulk-importing a source via textarea * 2. Adding a source to the theme
* 3. Deleting a source (two-click confirm) * 3. Listing sources for the theme
* 4. Deleting a source
* 5. Cleanup: deleting the theme
*/ */
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { loginAsUser } from '../helpers/auth'; import { loginAsUser } from '../helpers/auth';
test.describe('Sources management', () => { test.describe('Sources management', () => {
test('should add, bulk-import, and delete sources', async ({ page }) => { test('should add and delete sources via API', async ({ page }) => {
// Step 1: Login as regular user via cookie injection // Step 1: Login as regular user via cookie injection
await loginAsUser(page); await loginAsUser(page);
// Clean up any leftover sources from previous test runs via API
await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.goto('/', { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('domcontentloaded');
const existingSources: { id: string }[] = await page.evaluate(async () => {
// Step 2: Create a theme first (sources now belong to themes)
const themeResp = await page.evaluate(async () => {
const resp = await fetch('/api/v1/themes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
body: JSON.stringify({
name: 'Source Test Theme',
theme: 'Test',
categories: ['Cat'],
}),
});
return { status: resp.status, data: await resp.json() };
});
expect(themeResp.status).toBe(201);
const themeId = themeResp.data.id;
// Step 3: Add a source to the theme
const addResp = await page.evaluate(
async (tid: string) => {
const resp = await fetch('/api/v1/sources', { const resp = await fetch('/api/v1/sources', {
headers: { 'X-Requested-With': 'XMLHttpRequest' }, method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin', credentials: 'same-origin',
body: JSON.stringify({
title: 'Test Blog',
url: 'https://test.example.com/blog',
theme_id: tid,
}),
}); });
return resp.ok ? resp.json() : []; return { status: resp.status, data: await resp.json() };
},
themeId,
);
expect(addResp.status).toBe(201);
const sourceId = addResp.data.id;
// Step 4: Add a second source via bulk-style (individual POST)
const addResp2 = await page.evaluate(
async (tid: string) => {
const resp = await fetch('/api/v1/sources', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
body: JSON.stringify({
title: 'News Site',
url: 'https://news.example.com',
theme_id: tid,
}),
}); });
for (const source of existingSources) { return { status: resp.status, data: await resp.json() };
await page.evaluate(async (id: string) => { },
await fetch(`/api/v1/sources/${id}`, { themeId,
method: 'DELETE', );
expect(addResp2.status).toBe(201);
// Step 5: List sources for the theme and verify we have 2
const listResp = await page.evaluate(async (tid: string) => {
const resp = await fetch(`/api/v1/sources?theme_id=${tid}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }, headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'same-origin', credentials: 'same-origin',
}); });
}, source.id); return { status: resp.status, data: await resp.json() };
} }, themeId);
expect(listResp.status).toBe(200);
// Step 2: Navigate to sources expect(listResp.data.length).toBe(2);
await page.goto('/sources', { waitUntil: 'domcontentloaded' });
// Step 6: Delete the first source (Test Blog)
// Wait for the sources page to load const delResp = await page.evaluate(async (sid: string) => {
await expect( const resp = await fetch(`/api/v1/sources/${sid}`, {
page.locator('h1', { hasText: 'Sources Personnalisees' }), method: 'DELETE',
).toBeVisible({ timeout: 10_000 }); headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'same-origin',
// Step 3: Add a single source
await page.locator('#source-title').fill('Test Blog');
await page.locator('#source-url').fill('https://test.example.com/blog');
// Click the "Ajouter" submit button
await page.locator('button[type="submit"]', { hasText: 'Ajouter' }).click();
// Wait for the source to appear in the list
await expect(page.getByText('Test Blog')).toBeVisible({ timeout: 5_000 });
await expect(
page.getByText('https://test.example.com/blog'),
).toBeVisible();
// Step 4: Bulk import another source via textarea
const bulkTextarea = page.locator('#bulk-import');
await bulkTextarea.fill('News Site;https://news.example.com');
// Click the bulk import button
await page
.locator('button[type="submit"]', { hasText: 'Importer les sources' })
.click();
// Wait for the second source to appear
await expect(page.getByText('News Site')).toBeVisible({ timeout: 5_000 });
// Verify we have 2 sources in the list
// Each source is an <li> in the sources list
const sourceItems = page.locator('ul > li').filter({
has: page.locator('.text-indigo-600'),
}); });
await expect(sourceItems).toHaveCount(2); return { status: resp.status };
}, sourceId);
// Step 5: Delete the first source (Test Blog) expect(delResp.status).toBe(204);
// Find the source item containing "Test Blog" and click its delete button
const testBlogItem = page.locator('li').filter({ hasText: 'Test Blog' });
// First click: enter confirm state
await testBlogItem.locator('button').last().click();
// The button text should change to "Confirmer ?"
await expect(
testBlogItem.getByText('Confirmer ?'),
).toBeVisible({ timeout: 3_000 });
// Second click: confirm delete // Step 7: Verify only 1 source remains
await testBlogItem.getByText('Confirmer ?').click(); const listResp2 = await page.evaluate(async (tid: string) => {
const resp = await fetch(`/api/v1/sources?theme_id=${tid}`, {
// Wait for the source to be removed from the list headers: { 'X-Requested-With': 'XMLHttpRequest' },
await expect(page.getByText('Test Blog')).not.toBeVisible({ credentials: 'same-origin',
timeout: 5_000,
}); });
return { status: resp.status, data: await resp.json() };
// Step 6: Verify only 1 source remains }, themeId);
const remainingItems = page.locator('ul > li').filter({ expect(listResp2.status).toBe(200);
has: page.locator('.text-indigo-600'), expect(listResp2.data.length).toBe(1);
expect(listResp2.data[0].title).toBe('News Site');
// Step 8: Cleanup - delete theme (cascades sources)
await page.evaluate(async (tid: string) => {
await fetch(`/api/v1/themes/${tid}`, {
method: 'DELETE',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'same-origin',
}); });
await expect(remainingItems).toHaveCount(1); }, themeId);
// The remaining source should be "News Site"
await expect(page.getByText('News Site')).toBeVisible();
}); });
}); });

Loading…
Cancel
Save