import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, test } from '@playwright/test'; test('an artist directory uploads each album as a separate batch with a shared portrait', async ({ page }, info) => { const root = info.outputPath('творожный бизнес'); const albums = ['(2026) творог грехов - творожный бизнес', '(2025) молчанка - творожный бизнес']; for (const album of albums) { await mkdir(join(root, album), { recursive: true }); await writeFile(join(root, album, 'song.flac'), Buffer.alloc(128)); } const portrait = Buffer.from('portrait'); await writeFile(join(root, 'artist.jpg'), portrait); await writeFile(join(root, albums[0]!, 'cover.jpg'), Buffer.from('album cover')); type Batch = { artist_image_base64?: string; cover_base64?: string; files: { client_id: string; rel_path: string }[] }; const batches: Batch[] = []; await page.exposeFunction('recordArtistBatch', (batch: Batch) => { batches.push(batch); }); await page.addInitScript(() => localStorage.setItem('pipu.locale', 'ru')); await page.goto('/login'); await page.getByLabel('Логин', { exact: true }).fill('mobile-admin'); await page.getByRole('textbox', { name: 'Пароль', exact: true }).fill('mobile-admin-password'); await page.getByRole('button', { name: 'Войти', exact: true }).click(); await expect(page.getByRole('button', { name: 'Меню профиля' })).toBeVisible(); await page.evaluate(() => { const original = window.fetch; window.fetch = async (input, init) => { const req = new Request(input, init); const path = new URL(req.url).pathname; const json = (data: unknown) => new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } }); if (path === '/api/v1/uploads/precheck') { const body = await req.json(); return json({ files: body.files.map((f: { client_id: string }) => ({ client_id: f.client_id, result: 'accepted' })) }); } if (path === '/api/v1/uploads' && req.method === 'POST') { const body = await req.json(); await (window as unknown as { recordArtistBatch: (body: unknown) => Promise }).recordArtistBatch(body); return json({ batch_id: crypto.randomUUID(), files: body.files.map((f: { client_id: string }) => ({ client_id: f.client_id, upload_file_id: f.client_id, tus_url: `/api/v1/uploads/files/${f.client_id}` })) }); } return original(input, init); }; history.pushState({}, '', '/uploads/new'); window.dispatchEvent(new PopStateEvent('popstate')); }); await page.route('**/api/v1/uploads/files/*', async (route) => { await route.fulfill({ status: route.request().method() === 'HEAD' ? 200 : 204, headers: { 'Upload-Length': '128', 'Upload-Offset': route.request().method() === 'HEAD' ? '0' : '128', 'Tus-Resumable': '1.0.0' } }); }); await page.locator('input[webkitdirectory]').setInputFiles(root); const send = page.getByRole('button', { name: /^Отправить/ }); await expect(send).toBeEnabled(); await expect(page.getByText(/Пропущен/)).toHaveCount(0); await send.click(); await expect(page.getByText(/Готово: 2 файла/)).toBeVisible(); expect(batches).toHaveLength(2); expect(batches.map((batch) => batch.files[0]!.rel_path).sort()).toEqual(albums.map((album) => `творожный бизнес/${album}/song.flac`).sort()); for (const batch of batches) { expect(batch.files).toHaveLength(1); expect(batch.artist_image_base64).toBe(portrait.toString('base64')); } expect(batches.find((batch) => batch.files[0]!.rel_path.includes(albums[0]!))!.cover_base64).toBe(Buffer.from('album cover').toString('base64')); expect(batches.find((batch) => batch.files[0]!.rel_path.includes(albums[1]!))!.cover_base64).toBeUndefined(); });