334 lines
14 KiB
TypeScript
334 lines
14 KiB
TypeScript
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import { loginViaAPI, CONFIG, navigateTo, navigateToPageWithTracks, assertNoDebugText, collectNetworkErrors } from './helpers';
|
||
|
|
|
||
|
|
test.describe('TRACKS — Affichage et navigation', () => {
|
||
|
|
test.beforeEach(async ({ page }) => {
|
||
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('01. Une page affiche des tracks @critical', async ({ page }) => {
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
// /discover shows genres, not tracks directly. Use /library or navigate through a genre.
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
const trackItems = page.locator('[role="article"]');
|
||
|
|
const count = await trackItems.count();
|
||
|
|
console.log(` Tracks displayed: ${count}`);
|
||
|
|
expect(count).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('02. Les track cards affichent titre + artiste + artwork', async ({ page }) => {
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
// First track card: role="article" aria-label="Track: {title}"
|
||
|
|
const firstTrack = page.locator('[role="article"]').first();
|
||
|
|
|
||
|
|
// Title: h3 element
|
||
|
|
const title = firstTrack.locator('h3');
|
||
|
|
await expect(title).toBeVisible();
|
||
|
|
const titleText = await title.textContent() || '';
|
||
|
|
expect(titleText.trim().length).toBeGreaterThan(0);
|
||
|
|
expect(titleText).not.toContain('undefined');
|
||
|
|
expect(titleText).not.toContain('[object Object]');
|
||
|
|
|
||
|
|
// Artist: p element with text-muted-foreground class
|
||
|
|
const artist = firstTrack.locator('p.text-muted-foreground').first();
|
||
|
|
if (await artist.isVisible().catch(() => false)) {
|
||
|
|
const artistText = await artist.textContent() || '';
|
||
|
|
expect(artistText.trim().length).toBeGreaterThan(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Artwork: img inside .aspect-square container
|
||
|
|
const img = firstTrack.locator('.aspect-square img').first();
|
||
|
|
if (await img.isVisible().catch(() => false)) {
|
||
|
|
const src = await img.getAttribute('src');
|
||
|
|
expect(src).toBeTruthy();
|
||
|
|
expect(src).not.toContain('undefined');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('03. Cliquer sur un track ouvre sa page detail @critical', async ({ page }) => {
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
// TrackCard is a button with aria-label="Piste: {title}"
|
||
|
|
const trackButton = page.getByRole('button', { name: /^piste:/i }).first();
|
||
|
|
const hasTrack = await trackButton.isVisible({ timeout: 5_000 }).catch(() => false);
|
||
|
|
test.skip(!hasTrack, 'No track button available (cards may use different interaction)');
|
||
|
|
|
||
|
|
await trackButton.click();
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
|
||
|
|
// Route is /tracks/:id (NOT /track/:id)
|
||
|
|
expect(page.url()).toMatch(/\/tracks\//);
|
||
|
|
|
||
|
|
await assertNoDebugText(page);
|
||
|
|
|
||
|
|
const body = await page.textContent('body') || '';
|
||
|
|
expect(body.length).toBeGreaterThan(200);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('04. Page detail d\'un track — elements essentiels presents', async ({ page }) => {
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
const trackButton = page.getByRole('button', { name: /^piste:/i }).first();
|
||
|
|
const hasTrack = await trackButton.isVisible({ timeout: 5_000 }).catch(() => false);
|
||
|
|
test.skip(!hasTrack, 'No track button available');
|
||
|
|
|
||
|
|
await trackButton.click();
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
|
||
|
|
// Verify key elements on track detail page
|
||
|
|
const elements = {
|
||
|
|
'Title': page.getByRole('heading').first(),
|
||
|
|
'Play button': page.getByRole('button', { name: /lire|play|lecture/i }).first(),
|
||
|
|
'Artwork': page.locator('img').first(),
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const [name, locator] of Object.entries(elements)) {
|
||
|
|
const visible = await locator.isVisible().catch(() => false);
|
||
|
|
console.log(` ${name}: ${visible ? 'visible' : 'not found'}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('05. Les commentaires se chargent sur la page track', async ({ page }) => {
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
const trackButton = page.getByRole('button', { name: /^piste:/i }).first();
|
||
|
|
const hasTrack = await trackButton.isVisible({ timeout: 5_000 }).catch(() => false);
|
||
|
|
test.skip(!hasTrack, 'No track button available');
|
||
|
|
|
||
|
|
await trackButton.click();
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
|
||
|
|
// Comment input: textarea or input with placeholder containing "comment"
|
||
|
|
const commentInput = page.getByPlaceholder(/commentaire|comment/i).first()
|
||
|
|
.or(page.locator('textarea').first());
|
||
|
|
|
||
|
|
const hasInput = await commentInput.isVisible().catch(() => false);
|
||
|
|
console.log(` Comment input: ${hasInput ? 'visible' : 'not found'}`);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe('TRACKS — Interactions', () => {
|
||
|
|
test.beforeEach(async ({ page }) => {
|
||
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('06. Like un track (toggle)', async ({ page }) => {
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
// Track cards have a LikeButton with aria-label="Ajouter aux favoris" / "Retirer des favoris"
|
||
|
|
// Hover on the first card to reveal the like button
|
||
|
|
const trackCard = page.locator('[role="article"]').first();
|
||
|
|
|
||
|
|
await trackCard.hover();
|
||
|
|
await page.waitForTimeout(300);
|
||
|
|
|
||
|
|
const likeBtn = page.getByRole('button', { name: /ajouter aux favoris|retirer des favoris/i }).first();
|
||
|
|
|
||
|
|
if (await likeBtn.isVisible().catch(() => false)) {
|
||
|
|
// Capture initial aria-pressed state
|
||
|
|
const initialPressed = await likeBtn.getAttribute('aria-pressed');
|
||
|
|
|
||
|
|
await likeBtn.click();
|
||
|
|
await page.waitForTimeout(1_000);
|
||
|
|
|
||
|
|
// After clicking, aria-pressed should toggle
|
||
|
|
// Re-hover since the overlay may have changed
|
||
|
|
await trackCard.hover();
|
||
|
|
await page.waitForTimeout(300);
|
||
|
|
|
||
|
|
const updatedBtn = page.getByRole('button', { name: /ajouter aux favoris|retirer des favoris/i }).first();
|
||
|
|
const newPressed = await updatedBtn.getAttribute('aria-pressed');
|
||
|
|
|
||
|
|
console.log(` Like toggle: initial=${initialPressed}, after=${newPressed}`);
|
||
|
|
if (initialPressed !== null && newPressed !== null) {
|
||
|
|
expect(newPressed).not.toBe(initialPressed);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
console.log(' Like button not visible (may require hover on card overlay)');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('07. Ajouter un commentaire sur un track', async ({ page }) => {
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
// Navigate to track detail page via TrackCard button
|
||
|
|
const trackButton = page.getByRole('button', { name: /^piste:/i }).first();
|
||
|
|
const hasTrack = await trackButton.isVisible({ timeout: 5_000 }).catch(() => false);
|
||
|
|
test.skip(!hasTrack, 'No track button available');
|
||
|
|
|
||
|
|
await trackButton.click();
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
|
||
|
|
const commentInput = page.getByPlaceholder(/commentaire|comment/i).first()
|
||
|
|
.or(page.locator('textarea').first());
|
||
|
|
|
||
|
|
if (await commentInput.isVisible().catch(() => false)) {
|
||
|
|
const testComment = `Test E2E ${Date.now()}`;
|
||
|
|
await commentInput.fill(testComment);
|
||
|
|
|
||
|
|
// Submit
|
||
|
|
const submitBtn = page.getByRole('button', { name: /publier|envoyer|submit|post/i }).first();
|
||
|
|
if (await submitBtn.isVisible().catch(() => false)) {
|
||
|
|
await submitBtn.click();
|
||
|
|
await page.waitForTimeout(2_000);
|
||
|
|
|
||
|
|
const commentExists = await page.getByText(testComment).isVisible().catch(() => false);
|
||
|
|
console.log(` Comment posted and visible: ${commentExists ? 'yes' : 'no'}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('08. Repost un track', async ({ page }) => {
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
const repostBtn = page.getByRole('button', { name: /repost|repartag/i }).first();
|
||
|
|
const visible = await repostBtn.isVisible().catch(() => false);
|
||
|
|
console.log(` Repost button: ${visible ? 'visible' : 'not found'}`);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe('TRACKS — Upload (createur)', () => {
|
||
|
|
test.beforeEach(async ({ page }) => {
|
||
|
|
await loginViaAPI(page, CONFIG.users.creator.email, CONFIG.users.creator.password);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('09. Upload accessible pour un createur via la bibliotheque @critical', async ({ page }) => {
|
||
|
|
// Upload is a modal in /library, NOT a separate /upload page
|
||
|
|
await navigateTo(page, '/library');
|
||
|
|
|
||
|
|
const body = await page.textContent('body') || '';
|
||
|
|
// No 403 or redirect
|
||
|
|
expect(body).not.toMatch(/403|forbidden|acces refuse|access denied/i);
|
||
|
|
|
||
|
|
// Look for upload button/link that opens the upload modal
|
||
|
|
const uploadTrigger = page.getByRole('button', { name: /upload|importer|ajouter/i }).first()
|
||
|
|
.or(page.getByText(/upload|importer|telecharger/i).first());
|
||
|
|
|
||
|
|
const visible = await uploadTrigger.isVisible().catch(() => false);
|
||
|
|
console.log(` Upload trigger in library: ${visible ? 'visible' : 'not found'}`);
|
||
|
|
|
||
|
|
if (visible) {
|
||
|
|
await uploadTrigger.click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
// After clicking, a modal should appear with file input or dropzone
|
||
|
|
const uploadZone = page.locator('input[type="file"]')
|
||
|
|
.or(page.getByText(/glisser|drag|drop|deposer/i).first())
|
||
|
|
.or(page.locator('[class*="dropzone"]').first());
|
||
|
|
|
||
|
|
const uploadVisible = await uploadZone.isVisible().catch(() => false);
|
||
|
|
console.log(` Upload zone in modal: ${uploadVisible ? 'visible' : 'not found'}`);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('10. Formulaire d\'upload — champs de metadonnees presents', async ({ page }) => {
|
||
|
|
await navigateTo(page, '/library');
|
||
|
|
|
||
|
|
// Open upload modal
|
||
|
|
const uploadTrigger = page.getByRole('button', { name: /upload|importer|ajouter/i }).first()
|
||
|
|
.or(page.getByText(/upload|importer/i).first());
|
||
|
|
|
||
|
|
if (await uploadTrigger.isVisible().catch(() => false)) {
|
||
|
|
await uploadTrigger.click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const fields = {
|
||
|
|
'Title': /titre|title/i,
|
||
|
|
'Genre': /genre/i,
|
||
|
|
'Tags': /tags/i,
|
||
|
|
'Description': /description/i,
|
||
|
|
};
|
||
|
|
|
||
|
|
for (const [name, pattern] of Object.entries(fields)) {
|
||
|
|
const field = page.getByLabel(pattern).or(page.locator(`[name*="${name.toLowerCase()}"]`)).first();
|
||
|
|
const visible = await field.isVisible().catch(() => false);
|
||
|
|
console.log(` Field ${name}: ${visible ? 'visible' : 'not found'}`);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
console.log(' Upload trigger not found in library page');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('11. Validation — soumettre sans fichier affiche une erreur', async ({ page }) => {
|
||
|
|
await navigateTo(page, '/library');
|
||
|
|
|
||
|
|
// Open upload modal
|
||
|
|
const uploadTrigger = page.getByRole('button', { name: /upload|importer|ajouter/i }).first()
|
||
|
|
.or(page.getByText(/upload|importer/i).first());
|
||
|
|
|
||
|
|
if (await uploadTrigger.isVisible().catch(() => false)) {
|
||
|
|
await uploadTrigger.click();
|
||
|
|
await page.waitForTimeout(500);
|
||
|
|
|
||
|
|
const submitBtn = page.getByRole('button', { name: /upload|publier|submit|envoyer/i });
|
||
|
|
if (await submitBtn.isVisible().catch(() => false)) {
|
||
|
|
await submitBtn.click();
|
||
|
|
|
||
|
|
const error = page.getByText(/fichier.*requis|file.*required|selectionner|select.*file/i);
|
||
|
|
const hasError = await error.isVisible({ timeout: 3_000 }).catch(() => false);
|
||
|
|
console.log(` Validation without file: ${hasError ? 'error shown' : 'no error'}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe('TRACKS — Waveform et visualisation', () => {
|
||
|
|
test('12. La waveform s\'affiche dans le player bar', async ({ page }) => {
|
||
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
||
|
|
test.skip(page.url().includes('/login'), 'Login failed — skipping');
|
||
|
|
|
||
|
|
const hasTracks = await navigateToPageWithTracks(page);
|
||
|
|
test.skip(!hasTracks, 'No tracks available in test environment');
|
||
|
|
|
||
|
|
// Play a track to activate the player bar
|
||
|
|
const trackCard = page.locator('[role="article"]').first();
|
||
|
|
|
||
|
|
await trackCard.hover();
|
||
|
|
await page.waitForTimeout(300);
|
||
|
|
|
||
|
|
const playBtn = page.getByRole('button', { name: /^Lire /i }).first();
|
||
|
|
if (await playBtn.isVisible().catch(() => false)) {
|
||
|
|
await playBtn.click();
|
||
|
|
await page.waitForTimeout(1_000);
|
||
|
|
}
|
||
|
|
|
||
|
|
// The PlayerBarProgress contains waveform bars (divs), not canvas/svg
|
||
|
|
// It is a role="slider" with aria-label="Progression"
|
||
|
|
const progressBar = page.locator('[role="slider"][aria-label="Progression"]');
|
||
|
|
const visible = await progressBar.isVisible().catch(() => false);
|
||
|
|
console.log(` Waveform progress bar visible: ${visible ? 'yes' : 'no'}`);
|
||
|
|
|
||
|
|
if (visible) {
|
||
|
|
const box = await progressBar.boundingBox();
|
||
|
|
expect(box).not.toBeNull();
|
||
|
|
expect(box!.width).toBeGreaterThan(100);
|
||
|
|
|
||
|
|
// The waveform bars are div elements inside the progress bar
|
||
|
|
const waveformBars = progressBar.locator('div.rounded-sm');
|
||
|
|
const barCount = await waveformBars.count();
|
||
|
|
console.log(` Waveform bars count: ${barCount}`);
|
||
|
|
// PlayerBarProgress generates 48 waveform bars
|
||
|
|
if (barCount > 0) {
|
||
|
|
expect(barCount).toBeGreaterThanOrEqual(10);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|