Convert 20 files from fake assertions (console.log with ✓/✗) to real
expect() assertions. This completes the conversion started in the
previous session — zero console.log calls remain in the E2E suite.
Files converted (by batch):
Batch 1: 16-forms-validation (38→0), 13-workflows (18→0), 14-edge-cases (8→0)
Batch 2: 15-routes-coverage (8→0), 20-network-errors (5→0), 04-tracks (4→0),
32-deep-pages (4→0), 19-responsive (3→0), 11-accessibility-ethics (3→0)
Batch 3: 25-profile (2→0), 12-api (2→0), 29-chat-functional (2→0),
30-marketplace-checkout (1→0), 22-performance (1→0),
31-auth-sessions (1→0), 26-smoke (1→0), 02-navigation (1→0)
Batch 4: 24-cross-browser (0 fakes, 12 info→0), 34-workflows-empty (0→0),
33-visual-bugs (0→0)
Total: 139 fake assertions → real expect(), 159 informational logs removed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
553 lines
22 KiB
TypeScript
553 lines
22 KiB
TypeScript
import { test, expect } from '@chromatic-com/playwright';
|
|
import { loginViaAPI,
|
|
CONFIG,
|
|
navigateTo,
|
|
assertNotBroken,
|
|
assertNoDebugText,
|
|
collectNetworkErrors,
|
|
playFirstTrack,
|
|
SELECTORS,
|
|
} from './helpers';
|
|
|
|
// =============================================================================
|
|
// EDGE CASES — Formulaires vides
|
|
// =============================================================================
|
|
|
|
test.describe('EDGE CASES — Formulaires vides', () => {
|
|
test('01. Submit empty login form shows validation errors', async ({ page }) => {
|
|
await navigateTo(page, '/login');
|
|
|
|
// Click submit without filling anything
|
|
const submitBtn = page.getByRole('button', { name: /sign in|se connecter/i });
|
|
await submitBtn.click();
|
|
|
|
// Should stay on login page
|
|
await expect(page).toHaveURL(/login/);
|
|
|
|
// Should show validation error(s) or HTML5 validation prevents submission
|
|
const body = await page.textContent('body') || '';
|
|
const hasValidation = /required|obligatoire|email|invalid|invalide/i.test(body);
|
|
const emailInput = page.locator('input[type="email"]').first();
|
|
const validationMessage = await emailInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
expect(hasValidation || validationMessage.length > 0).toBeTruthy();
|
|
});
|
|
|
|
test('02. Submit empty register form shows validation errors', async ({ page }) => {
|
|
await navigateTo(page, '/register');
|
|
|
|
// Click submit without filling anything
|
|
const submitBtn = page.getByRole('button', { name: /s'inscrire|create account/i });
|
|
await submitBtn.click();
|
|
|
|
// Should stay on register page
|
|
await expect(page).toHaveURL(/register/);
|
|
|
|
// Check for validation errors
|
|
const body = await page.textContent('body') || '';
|
|
const hasValidation = /required|obligatoire|invalid|invalide|trop court|too short/i.test(body);
|
|
const usernameInput = page.locator('#register-username');
|
|
const validationMessage = await usernameInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
expect(hasValidation || validationMessage.length > 0).toBeTruthy();
|
|
});
|
|
|
|
test('03. Submit empty search does not crash', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
test.skip(!searchVisible, 'Search input not visible on this page');
|
|
|
|
// Clear the input and press Enter
|
|
await searchInput.first().fill('');
|
|
await searchInput.first().press('Enter');
|
|
await page.waitForTimeout(1_000);
|
|
|
|
await assertNotBroken(page);
|
|
});
|
|
|
|
test('04. Login with only email filled shows password error', async ({ page }) => {
|
|
await navigateTo(page, '/login');
|
|
|
|
await page.getByLabel(/^email$/i).or(page.locator('input[type="email"]')).first().fill('test@example.com');
|
|
await page.getByRole('button', { name: /sign in|se connecter/i }).click();
|
|
|
|
// Should stay on login
|
|
await expect(page).toHaveURL(/login/);
|
|
|
|
// Password field should show validation
|
|
const passwordInput = page.locator('input[type="password"]').first();
|
|
const validationMessage = await passwordInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = validationMessage.length > 0 || /required|password|mot de passe/i.test(body);
|
|
expect(hasError).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// EDGE CASES — Caracteres speciaux et injection
|
|
// =============================================================================
|
|
|
|
test.describe('EDGE CASES — Caracteres speciaux', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('05. XSS attempt in search does not execute @critical', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
test.skip(!searchVisible, 'Search input not visible on this page');
|
|
|
|
const xssPayload = '<script>alert("xss")</script>';
|
|
await searchInput.first().fill(xssPayload);
|
|
await page.waitForTimeout(1_500);
|
|
|
|
// Verify no alert dialog appeared
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError/i);
|
|
|
|
// The script tag should be sanitized — not rendered as HTML
|
|
const scriptElements = await page.locator('script:has-text("xss")').count();
|
|
expect(scriptElements).toBe(0);
|
|
});
|
|
|
|
test('06. SQL injection attempt in search does not crash @critical', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
test.skip(!searchVisible, 'Search input not visible on this page');
|
|
|
|
const sqlPayload = "'; DROP TABLE users; --";
|
|
await searchInput.first().fill(sqlPayload);
|
|
await page.waitForTimeout(1_500);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|syntax error|SQL/i);
|
|
});
|
|
|
|
test('07. Very long string in search does not crash', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
test.skip(!searchVisible, 'Search input not visible on this page');
|
|
|
|
const longString = 'a'.repeat(600);
|
|
await searchInput.first().fill(longString);
|
|
await page.waitForTimeout(1_500);
|
|
|
|
await assertNotBroken(page);
|
|
});
|
|
|
|
test('08. Emoji search works without crash', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
test.skip(!searchVisible, 'Search input not visible on this page');
|
|
|
|
await searchInput.first().fill('music vibes');
|
|
await page.waitForTimeout(1_500);
|
|
|
|
await assertNotBroken(page);
|
|
});
|
|
|
|
test('09. Unicode and special characters in search', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
test.skip(!searchVisible, 'Search input not visible on this page');
|
|
|
|
const specialChars = 'cafe\u0301 mu\u0308sik \u00e9l\u00e8ve \u00f1';
|
|
await searchInput.first().fill(specialChars);
|
|
await page.waitForTimeout(1_500);
|
|
|
|
await assertNotBroken(page);
|
|
});
|
|
|
|
test('10. HTML entities in login email field', async ({ page }) => {
|
|
// This test needs to be on the login page, so clear the authenticated state
|
|
// (beforeEach logged in via API — we need to undo that for this test)
|
|
await page.evaluate(() => localStorage.removeItem('auth-storage'));
|
|
await page.context().clearCookies();
|
|
|
|
await navigateTo(page, '/login');
|
|
|
|
// Wait for the login form to be fully visible before interacting
|
|
await page.getByTestId('login-form').waitFor({ state: 'visible', timeout: 15_000 });
|
|
|
|
const emailInput = page.locator('input[type="email"]');
|
|
await emailInput.first().waitFor({ state: 'visible', timeout: 10_000 });
|
|
await emailInput.first().fill('test&<b>bold</b>@example.com');
|
|
await page.getByLabel(/^password$|^mot de passe$/i).or(page.locator('input[type="password"]')).first().fill('Password123!');
|
|
await page.getByRole('button', { name: /sign in|se connecter/i }).click();
|
|
|
|
// Should show error (invalid email format), not crash
|
|
await page.waitForTimeout(1_500);
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError/i);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// EDGE CASES — Erreurs reseau
|
|
// =============================================================================
|
|
|
|
test.describe('EDGE CASES — Erreurs reseau', () => {
|
|
test('11. Simulated 500 error on API shows error message, no crash @critical', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
|
|
// Intercept a common API route and return 500
|
|
await page.route('**/api/v1/tracks**', (route) => {
|
|
route.fulfill({
|
|
status: 500,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ error: { code: 'INTERNAL_ERROR', message: 'Simulated server error' } }),
|
|
});
|
|
});
|
|
|
|
await navigateTo(page, '/library');
|
|
await page.waitForTimeout(2_000);
|
|
|
|
// Page should not crash — should show an error state or empty state
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
expect(body.length).toBeGreaterThan(50);
|
|
});
|
|
|
|
test('12. Simulated network timeout shows loading or error state', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
|
|
// Intercept API and simulate a timeout (abort after delay)
|
|
await page.route('**/api/v1/search**', async (route) => {
|
|
// Delay then abort to simulate timeout
|
|
await new Promise((resolve) => setTimeout(resolve, 5_000));
|
|
route.abort('timedout');
|
|
});
|
|
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
test.skip(!searchVisible, 'Search input not visible on this page');
|
|
|
|
await searchInput.first().fill('timeout test');
|
|
|
|
// Wait a moment - should show loading indicator or remain stable
|
|
await page.waitForTimeout(2_000);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
});
|
|
|
|
test('13. API returning malformed JSON does not crash page', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
|
|
await page.route('**/api/v1/tracks**', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: '{ invalid json !!!',
|
|
});
|
|
});
|
|
|
|
await navigateTo(page, '/library');
|
|
await page.waitForTimeout(2_000);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Unexpected token/i);
|
|
expect(body.length).toBeGreaterThan(50);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// EDGE CASES — Ressources inexistantes
|
|
// =============================================================================
|
|
|
|
test.describe('EDGE CASES — Ressources inexistantes', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('14. /tracks/nonexistent-id shows 404 or error page @critical', async ({ page }) => {
|
|
await navigateTo(page, '/tracks/nonexistent-id-99999');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
// Should show a 404 page, error message, or redirect — not crash
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const handled = /not found|introuvable|404|error|does not exist|n'existe pas/i.test(body) ||
|
|
page.url().includes('/404') ||
|
|
page.url().includes('/dashboard');
|
|
expect(handled).toBeTruthy();
|
|
});
|
|
|
|
test('15. /playlists/nonexistent-id shows 404 or error page', async ({ page }) => {
|
|
await navigateTo(page, '/playlists/nonexistent-id-99999');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const handled = /not found|introuvable|404|error/i.test(body) ||
|
|
page.url().includes('/404');
|
|
expect(handled).toBeTruthy();
|
|
});
|
|
|
|
test('16. /u/nonexistent-user shows 404 or error page', async ({ page }) => {
|
|
await navigateTo(page, '/u/this-user-does-not-exist-at-all');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const handled = /not found|introuvable|404|error|n'existe pas/i.test(body) ||
|
|
page.url().includes('/404');
|
|
expect(handled).toBeTruthy();
|
|
});
|
|
|
|
test('17. Completely unknown route shows 404 page', async ({ page }) => {
|
|
await navigateTo(page, '/this-route-definitely-does-not-exist');
|
|
// Wait a bit for redirects to settle
|
|
await page.waitForTimeout(2_000);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
// Should show 404 page or redirect, not blank or crash
|
|
expect(body).not.toMatch(/crash|TypeError/i);
|
|
// Body should have some content (at least a heading or navigation)
|
|
expect(body.trim().length).toBeGreaterThan(10);
|
|
|
|
const is404 = /404|not found|introuvable|page not found/i.test(body) ||
|
|
page.url().includes('/404');
|
|
expect(is404).toBeTruthy();
|
|
});
|
|
|
|
test('18. /marketplace/products/nonexistent-id handles gracefully', async ({ page }) => {
|
|
await navigateTo(page, '/marketplace/products/nonexistent-product-id');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// EDGE CASES — Double actions
|
|
// =============================================================================
|
|
|
|
test.describe('EDGE CASES — Double actions', () => {
|
|
test('19. Double-click on login submit does not cause duplicate requests @critical', async ({ page }) => {
|
|
await navigateTo(page, '/login');
|
|
|
|
await page.getByLabel(/^email$/i).or(page.locator('input[type="email"]')).first().fill(CONFIG.users.listener.email);
|
|
await page.getByLabel(/^password$|^mot de passe$/i).or(page.locator('input[type="password"]')).first().fill(CONFIG.users.listener.password);
|
|
|
|
// Track API calls
|
|
const loginRequests: string[] = [];
|
|
page.on('request', (req) => {
|
|
if (req.url().includes('/auth/login') && req.method() === 'POST') {
|
|
loginRequests.push(req.url());
|
|
}
|
|
});
|
|
|
|
const submitBtn = page.getByRole('button', { name: /sign in|se connecter/i });
|
|
// Double-click rapidly
|
|
await submitBtn.dblclick();
|
|
|
|
// Wait for response
|
|
await page.waitForTimeout(3_000);
|
|
|
|
// Should have sent at most 2 requests (double-click), ideally 1 if debounced
|
|
expect(loginRequests.length).toBeLessThanOrEqual(2);
|
|
// The page should not crash regardless
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError/i);
|
|
});
|
|
|
|
test('20. Rapid page navigation does not crash the app', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
|
|
// Click through pages rapidly without waiting
|
|
const pages = ['/dashboard', '/library', '/discover', '/search', '/playlists', '/profile', '/settings'];
|
|
|
|
for (const route of pages) {
|
|
page.goto(route, { waitUntil: 'commit' }).catch(() => {});
|
|
// Minimal delay to trigger navigation
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
// Wait for final page to settle
|
|
await page.waitForLoadState('domcontentloaded').catch(() => {});
|
|
await page.waitForTimeout(3_000);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
// During rapid navigation, body may be minimal — just ensure no crash
|
|
expect(body.trim().length).toBeGreaterThan(10);
|
|
});
|
|
|
|
test('21. Double-click on like button toggles correctly', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/discover');
|
|
|
|
// Find a like button
|
|
const likeBtn = page.getByRole('button', { name: /ajouter aux favoris|add to favorites/i }).first();
|
|
const likeBtnVisible = await likeBtn.isVisible().catch(() => false);
|
|
test.skip(!likeBtnVisible, 'No like button visible on discover page');
|
|
|
|
// Double-click to toggle like twice
|
|
await likeBtn.dblclick();
|
|
await page.waitForTimeout(1_000);
|
|
|
|
// Should not crash — state may or may not have changed
|
|
await assertNotBroken(page);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// EDGE CASES — Etat du navigateur
|
|
// =============================================================================
|
|
|
|
test.describe('EDGE CASES — Etat du navigateur', () => {
|
|
test('22. Clearing localStorage forces re-login', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await page.waitForTimeout(2_000);
|
|
// Clear auth storage
|
|
await page.evaluate(() => {
|
|
localStorage.removeItem('auth-storage');
|
|
localStorage.clear();
|
|
});
|
|
|
|
// Navigate to a protected page
|
|
await navigateTo(page, '/dashboard');
|
|
await page.waitForTimeout(2_000);
|
|
|
|
// Should redirect to login or show unauthenticated state
|
|
const url = page.url();
|
|
const isLoggedOut = url.includes('/login') || url.includes('/register');
|
|
expect(isLoggedOut).toBeTruthy();
|
|
});
|
|
|
|
test('23. Accessing app with expired/invalid token shows login', async ({ page }) => {
|
|
// Set an invalid auth state
|
|
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate(() => {
|
|
localStorage.setItem('auth-storage', JSON.stringify({
|
|
state: { isAuthenticated: true, isLoading: false, error: null },
|
|
version: 1,
|
|
}));
|
|
});
|
|
|
|
// Try to access protected page with fake auth
|
|
await navigateTo(page, '/dashboard');
|
|
await page.waitForTimeout(3_000);
|
|
|
|
// The API should reject the invalid session and redirect to login
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError/i);
|
|
|
|
const url = page.url();
|
|
const handledInvalidToken = url.includes('/login') || url.includes('/register') ||
|
|
/unauthorized|session|expired|sign in/i.test(body);
|
|
expect(handledInvalidToken).toBeTruthy();
|
|
});
|
|
|
|
test('24. Page loads correctly with JavaScript-disabled cookies notice', async ({ page }) => {
|
|
// Verify the page loads and doesn't depend on cookies being pre-set
|
|
await page.context().clearCookies();
|
|
await navigateTo(page, '/login');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError/i);
|
|
expect(body.length).toBeGreaterThan(50);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// EDGE CASES — Concurrent interactions
|
|
// =============================================================================
|
|
|
|
test.describe('EDGE CASES — Interactions concurrentes', () => {
|
|
test('25. Multiple search queries in quick succession', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
test.skip(!searchVisible, 'Search input not visible on this page');
|
|
|
|
// Type multiple queries rapidly to test debounce handling
|
|
const queries = ['rock', 'jazz', 'electronic', 'hip hop', 'classical'];
|
|
for (const query of queries) {
|
|
await searchInput.first().fill(query);
|
|
await page.waitForTimeout(100); // Very short delay between queries
|
|
}
|
|
|
|
// Wait for the final debounced search to resolve
|
|
await page.waitForTimeout(2_000);
|
|
|
|
await assertNotBroken(page);
|
|
});
|
|
|
|
test('26. Opening search while player is active does not break either', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
|
|
// Start playing a track
|
|
await navigateTo(page, '/discover');
|
|
|
|
// Play first track — hover on card then click play button
|
|
const trackCard = page.locator('[role="article"]').first();
|
|
if (await trackCard.isVisible().catch(() => false)) {
|
|
await trackCard.hover();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
const playBtn = page.getByRole('button', { name: /^lire |^play /i }).first()
|
|
.or(page.locator('[aria-label*="Lire"]').first())
|
|
.or(page.locator('[aria-label*="Play"]').first());
|
|
if (await playBtn.isVisible().catch(() => false)) {
|
|
await playBtn.click();
|
|
await page.waitForTimeout(CONFIG.timeouts.animation);
|
|
}
|
|
|
|
// Navigate to search while track might be playing
|
|
await navigateTo(page, '/search');
|
|
await assertNotBroken(page);
|
|
|
|
// Search should work
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
if (searchVisible) {
|
|
await searchInput.first().fill('test');
|
|
await page.waitForTimeout(1_500);
|
|
await assertNotBroken(page);
|
|
}
|
|
});
|
|
});
|