import { expect, test } from '@playwright/test'; test('locks the comment and retries one failed upload while another is running', async ({ page }, info) => { 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, status = 200) => new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json' } }); if (path === '/api/v1/uploads/quota') return json({ limit_bytes: 1e9, used_bytes: 0, per_user_limit_bytes: null, user_used_bytes: 0, max_file_bytes: 1e9 }); 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(); document.documentElement.dataset.uploadComment = body.comment; document.documentElement.dataset.uploadReservations = String(Number(document.documentElement.dataset.uploadReservations ?? 0) + 1); return json({ batch_id: 'test-batch', files: body.files.map((f: { client_id: string; name: string }) => ({ client_id: f.client_id, upload_file_id: f.name, tus_url: `/api/v1/uploads/files/${f.name}` })) }, 201); } return original(input, init); }; history.pushState({}, '', '/uploads/new'); window.dispatchEvent(new PopStateEvent('popstate')); }); let retry = false; let failures = 0; let resumedOffset: string | undefined; let releaseSecond!: () => void; const secondPending = new Promise((resolve) => { releaseSecond = resolve; }); await page.route('**/api/v1/uploads/files/*', async (route) => { const req = route.request(); const first = req.url().endsWith('/first.flac'); if (req.method() === 'HEAD') { await route.fulfill({ status: 200, headers: { 'Upload-Length': '1024', 'Upload-Offset': first && failures > 0 ? '256' : '0', 'Tus-Resumable': '1.0.0' } }); return; } if (first && !retry) { failures++; await route.fulfill({ status: 507, contentType: 'application/problem+json', body: JSON.stringify({ status: 507, code: 'uploads.storage_full', detail: 'upload storage is full' }) }); return; } if (first) resumedOffset = req.headers()['upload-offset']; else await secondPending; await route.fulfill({ status: 204, headers: { 'Upload-Offset': '1024', 'Tus-Resumable': '1.0.0' } }); }); const inputs = page.locator('input[type=file]'); await inputs.first().setInputFiles(['first.flac', 'second.flac'].map((name) => ({ name, mimeType: 'audio/flac', buffer: Buffer.alloc(1024) }))); const comment = page.getByRole('textbox', { name: 'Комментарий для администратора' }); await comment.fill('Lossless album'); await expect(comment).toBeEnabled(); const send = page.getByRole('button', { name: /^Отправить/ }); await expect(send).toBeEnabled(); await send.click(); await expect(comment).toBeDisabled(); await expect(page.getByRole('button', { name: 'Очистить список' })).toBeDisabled(); await expect(page.getByText(/На сервере закончилось место для загрузок/)).toBeVisible(); const retryButton = page.getByRole('button', { name: 'Повторить загрузку first.flac', exact: true }); await expect(retryButton).toBeEnabled(); await page.setViewportSize({ width: 390, height: 844 }); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1); await page.screenshot({ path: info.outputPath('upload-retry.png'), fullPage: true }); expect(failures).toBe(1); retry = true; await retryButton.click(); await expect.poll(() => resumedOffset).toBe('256'); await expect(retryButton).toHaveCount(0); await expect(comment).toBeDisabled(); await expect(page.locator('html')).toHaveAttribute('data-upload-reservations', '1'); await expect(page.locator('html')).toHaveAttribute('data-upload-comment', 'Lossless album'); releaseSecond(); await expect(page.getByText(/Готово: 2 файла/)).toBeVisible(); await page.getByRole('button', { name: 'Загрузить ещё', exact: true }).click(); await inputs.first().setInputFiles({ name: 'third.flac', mimeType: 'audio/flac', buffer: Buffer.alloc(1024) }); await expect(comment).toBeEnabled(); });