New tests/e2e/ suite covering: - Auth, navigation, player, tracks, playlists - Search, discover, social, marketplace, chat - Accessibility, API, workflows, edge cases - Routes coverage, forms validation, modals - Empty states, responsive, network errors - Error boundary, performance, visual regression - Cross-browser, profile, smoke, upload - Storybook, deep pages, visual bugs - Includes fixtures, helpers, global setup/teardown - Playwright config and coverage map Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
217 lines
8 KiB
TypeScript
217 lines
8 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { loginViaAPI, CONFIG, navigateTo } from './helpers';
|
|
|
|
/**
|
|
* UPLOAD - Track upload flow tests
|
|
* Selectors based on UploadModal.tsx, UploadModalDropzone.tsx, UploadModalMetadataForm.tsx
|
|
*/
|
|
|
|
// Create a minimal valid MP3 buffer for testing
|
|
function createTestMP3Buffer(): Buffer {
|
|
return Buffer.from(
|
|
'4944330300000000000a544954320000000500000054657374fffb90440000000000000000000000000000000000000000',
|
|
'hex',
|
|
);
|
|
}
|
|
|
|
test.describe('UPLOAD - Track upload flow @critical', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Login as creator (has upload permissions)
|
|
await loginViaAPI(page, CONFIG.users.creator.email, CONFIG.users.creator.password);
|
|
});
|
|
|
|
test('should complete full upload flow: file, metadata, publish, visible in library @critical', async ({ page }) => {
|
|
await navigateTo(page, '/library');
|
|
|
|
// Find and click upload button
|
|
const uploadBtn = page.getByRole('button', { name: /upload|uploader/i }).first();
|
|
const uploadVisible = await uploadBtn.isVisible({ timeout: 10_000 }).catch(() => false);
|
|
if (!uploadVisible) {
|
|
console.log(' Upload button not found — skipping');
|
|
return;
|
|
}
|
|
await uploadBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Wait for upload modal/dialog
|
|
const dialog = page.locator('[role="dialog"]').first();
|
|
const dialogVisible = await dialog.isVisible({ timeout: 5000 }).catch(() => false);
|
|
if (!dialogVisible) {
|
|
console.log(' Upload dialog did not appear — skipping');
|
|
return;
|
|
}
|
|
|
|
// Set file via the hidden input inside the dropzone
|
|
const fileInput = dialog.locator('input[type="file"]').first();
|
|
const fileInputExists = await fileInput.count();
|
|
if (fileInputExists === 0) {
|
|
console.log(' File input not found in upload dialog — skipping');
|
|
return;
|
|
}
|
|
const uniqueTitle = `E2E Upload ${Date.now()}`;
|
|
|
|
await fileInput.setInputFiles({
|
|
name: 'test-track.mp3',
|
|
mimeType: 'audio/mpeg',
|
|
buffer: createTestMP3Buffer(),
|
|
});
|
|
|
|
// Wait for file to be processed (dropzone disappears, metadata form appears)
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Fill metadata
|
|
const titleInput = dialog.locator('#title').or(dialog.locator('input[name="title"]'));
|
|
if (!await titleInput.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
console.log(' Title input not visible after file upload — skipping');
|
|
return;
|
|
}
|
|
await titleInput.fill(uniqueTitle);
|
|
|
|
const artistInput = dialog.locator('#artist').or(dialog.locator('input[name="artist"]'));
|
|
if (await artistInput.isVisible().catch(() => false)) {
|
|
await artistInput.fill('E2E Test Artist');
|
|
}
|
|
|
|
const genreInput = dialog.locator('#genre').or(dialog.locator('input[name="genre"]'));
|
|
if (await genreInput.isVisible().catch(() => false)) {
|
|
await genreInput.fill('Electronic');
|
|
}
|
|
|
|
// Submit the form
|
|
const submitBtn = dialog.locator('button[type="submit"]')
|
|
.or(dialog.locator('button[form="upload-track-form"]'))
|
|
.or(dialog.getByRole('button', { name: /uploader/i }));
|
|
if (!await submitBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
console.log(' Submit button not visible — skipping');
|
|
return;
|
|
}
|
|
await submitBtn.click();
|
|
|
|
// Wait for upload completion (success message or dialog closes)
|
|
const success = dialog.locator('text=/upload|success|succ/i').first();
|
|
const dialogClosed = page.waitForSelector('[role="dialog"]', { state: 'hidden', timeout: 60_000 }).catch(() => null);
|
|
|
|
await Promise.race([
|
|
success.waitFor({ state: 'visible', timeout: 60_000 }).catch(() => {}),
|
|
dialogClosed,
|
|
]);
|
|
|
|
// Verify track appears in library after reload
|
|
await navigateTo(page, '/library');
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Search for the uploaded track
|
|
const trackInLibrary = page.locator(`text=${uniqueTitle}`).first();
|
|
const isVisible = await trackInLibrary.isVisible({ timeout: 10_000 }).catch(() => false);
|
|
if (isVisible) {
|
|
console.log(' Track visible in library');
|
|
} else {
|
|
console.warn(' Track not yet visible (may still be processing)');
|
|
}
|
|
});
|
|
|
|
test('should show error for invalid file format', async ({ page }) => {
|
|
await navigateTo(page, '/library');
|
|
|
|
const uploadBtn = page.getByRole('button', { name: /upload|uploader/i }).first();
|
|
const uploadVisible = await uploadBtn.isVisible({ timeout: 10_000 }).catch(() => false);
|
|
if (!uploadVisible) {
|
|
console.log(' Upload button not found — skipping');
|
|
return;
|
|
}
|
|
await uploadBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
const dialog = page.locator('[role="dialog"]').first();
|
|
const dialogVisible = await dialog.isVisible({ timeout: 5000 }).catch(() => false);
|
|
if (!dialogVisible) {
|
|
console.log(' Upload dialog did not appear — skipping');
|
|
return;
|
|
}
|
|
|
|
const fileInput = dialog.locator('input[type="file"]').first();
|
|
if (await fileInput.count() === 0) {
|
|
console.log(' File input not found — skipping');
|
|
return;
|
|
}
|
|
|
|
// Try uploading a text file
|
|
await fileInput.setInputFiles({
|
|
name: 'invalid.txt',
|
|
mimeType: 'text/plain',
|
|
buffer: Buffer.from('This is not an audio file'),
|
|
});
|
|
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Either: file is rejected (dropzone still visible), or error message appears
|
|
const errorMsg = dialog.locator('text=/format|invalid|non supporté|rejected/i').first();
|
|
const dropzoneStillVisible = dialog.locator('text=/glissez|drag|drop/i').first();
|
|
|
|
const hasError = await errorMsg.isVisible({ timeout: 3000 }).catch(() => false);
|
|
const dropzoneBack = await dropzoneStillVisible.isVisible({ timeout: 3000 }).catch(() => false);
|
|
|
|
expect(hasError || dropzoneBack).toBeTruthy();
|
|
});
|
|
|
|
test('should show validation error when submitting without file', async ({ page }) => {
|
|
await navigateTo(page, '/library');
|
|
|
|
const uploadBtn = page.getByRole('button', { name: /upload|uploader/i }).first();
|
|
const uploadVisible = await uploadBtn.isVisible({ timeout: 10_000 }).catch(() => false);
|
|
if (!uploadVisible) {
|
|
console.log(' Upload button not found — skipping');
|
|
return;
|
|
}
|
|
await uploadBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
const dialog = page.locator('[role="dialog"]').first();
|
|
const dialogVisible = await dialog.isVisible({ timeout: 5000 }).catch(() => false);
|
|
if (!dialogVisible) {
|
|
console.log(' Upload dialog did not appear — skipping');
|
|
return;
|
|
}
|
|
|
|
// The submit button should be disabled when no file is selected
|
|
const submitBtn = dialog.locator('button[type="submit"]')
|
|
.or(dialog.locator('button[form="upload-track-form"]'))
|
|
.or(dialog.getByRole('button', { name: /uploader/i }));
|
|
|
|
if (await submitBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
const isDisabled = await submitBtn.isDisabled();
|
|
expect(isDisabled).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test('should close modal with Escape or close button', async ({ page }) => {
|
|
await navigateTo(page, '/library');
|
|
|
|
const uploadBtn = page.getByRole('button', { name: /upload|uploader/i }).first();
|
|
const uploadVisible = await uploadBtn.isVisible({ timeout: 10_000 }).catch(() => false);
|
|
if (!uploadVisible) {
|
|
console.log(' Upload button not found — skipping');
|
|
return;
|
|
}
|
|
await uploadBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
const dialog = page.locator('[role="dialog"]').first();
|
|
const dialogVisible = await dialog.isVisible({ timeout: 5000 }).catch(() => false);
|
|
if (!dialogVisible) {
|
|
console.log(' Upload dialog did not appear — skipping');
|
|
return;
|
|
}
|
|
|
|
// Close via button
|
|
const closeBtn = dialog.getByRole('button', { name: /close|cancel|fermer|annuler/i }).first();
|
|
if (await closeBtn.isVisible().catch(() => false)) {
|
|
await closeBtn.click();
|
|
await expect(dialog).not.toBeVisible({ timeout: 3000 });
|
|
} else {
|
|
// Close via Escape
|
|
await page.keyboard.press('Escape');
|
|
await expect(dialog).not.toBeVisible({ timeout: 3000 });
|
|
}
|
|
});
|
|
});
|