fix(uploads): serve private previews through API and show batch cover status
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Canceled after 4m13s
ci / module boundaries / boundaries (push) Successful in 8s
ci / api / api (push) Successful in 56s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Successful in 1m7s

This commit is contained in:
mrleo1nid committed 2026-10-29 11:34:55 +03:00
1 parent 37e33c656b
commit 7d62531c02
15 files changed
+533 -410

No files matched your search

+37 -7
View File
@@ -1,5 +1,15 @@
import { expect, test, type Page } from '@playwright/test';
function previewWav() {
const dataSize = 8000 * 2 * 2;
const wav = Buffer.alloc(44 + dataSize);
wav.write('RIFF', 0); wav.writeUInt32LE(36 + dataSize, 4); wav.write('WAVEfmt ', 8);
wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22);
wav.writeUInt32LE(8000, 24); wav.writeUInt32LE(16000, 28); wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34);
wav.write('data', 36); wav.writeUInt32LE(dataSize, 40);
return wav;
}
async function setup(page: Page, scenario: 'queue' | 'template' | 'mine') {
await page.addInitScript(() => localStorage.setItem('pipu.locale', 'ru'));
await page.goto('/login');
@@ -11,7 +21,7 @@ async function setup(page: Page, scenario: 'queue' | 'template' | 'mine') {
const modulePath = '/src/test/mobileFixtures.ts';
const { mobileFixtures } = await import(modulePath);
const source = structuredClone(mobileFixtures['/api/v1/admin/sources'].items[0]);
const file = structuredClone(mobileFixtures['/api/v1/admin/uploads'].items[0]);
const file = { ...structuredClone(mobileFixtures['/api/v1/admin/uploads'].items[0]), batch_has_cover: true };
const files = [
{ ...file, id: 'visible', original_name: 'visible.flac', target_source_name: source.name, target_rel_path: 'Artist/Album/01 - Song.flac', target_storage_path: 'nas:/music/Artist/Album/01 - Song.flac', stream_url: `/api/v1/admin/uploads/files/${file.id}/stream?exp=9999999999&sig=test` },
{ ...file, id: 'approved', original_name: 'approved.flac', status: 'approved' },
@@ -75,6 +85,7 @@ async function setup(page: Page, scenario: 'queue' | 'template' | 'mine') {
test('queue shows the storage and path, and recalculates after fixing placement', async ({ page }, info) => {
await setup(page, 'queue');
await expect(page.getByText('Есть обложка', { exact: true })).toBeVisible();
await expect(page.getByText('Artist/Album/01 - Song.flac', { exact: true })).toBeHidden();
await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click();
await expect(page.getByText('Artist/Album/01 - Song.flac', { exact: true })).toBeVisible();
@@ -170,12 +181,7 @@ test('queue shows the current full destination after saving a changed source tem
});
test('queue preview loads duration before play and plays signed audio', async ({ page }) => {
const dataSize = 8000 * 2 * 2;
const wav = Buffer.alloc(44 + dataSize);
wav.write('RIFF', 0); wav.writeUInt32LE(36 + dataSize, 4); wav.write('WAVEfmt ', 8);
wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22);
wav.writeUInt32LE(8000, 24); wav.writeUInt32LE(16000, 28); wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34);
wav.write('data', 36); wav.writeUInt32LE(dataSize, 40);
const wav = previewWav();
await page.route('**/api/v1/admin/uploads/files/*/stream?*', (route) => route.fulfill({ contentType: 'audio/wav', body: wav }));
await setup(page, 'queue');
await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click();
@@ -186,3 +192,27 @@ test('queue preview loads duration before play and plays signed audio', async ({
await expect.poll(() => audio.evaluate((el: HTMLAudioElement) => el.currentTime)).toBeGreaterThan(0);
expect(await audio.evaluate((el: HTMLAudioElement) => el.error)).toBeNull();
});
test('queue preview explains HTTP failures and recovers on retry', async ({ page }) => {
let available = false;
// The E2E bridge mocks window.fetch; media diagnostics must reach the same
// intercepted HTTP route as the native audio element.
await page.addInitScript(() => Object.assign(window, { previewNativeFetch: window.fetch.bind(window) }));
await page.route('**/api/v1/admin/uploads/files/*/stream?*', (route) => available
? route.fulfill({ contentType: 'audio/wav', body: previewWav() })
: route.fulfill({ status: 403, contentType: 'text/plain', body: 'Forbidden' }));
await setup(page, 'queue');
await page.evaluate(() => {
const original = window.fetch;
const native = (window as unknown as { previewNativeFetch: typeof fetch }).previewNativeFetch;
window.fetch = (input, init) => new URL(new Request(input, init).url).pathname.endsWith('/stream')
? native(input, init) : original(input, init);
});
await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click();
const preview = page.locator('audio').first().locator('..');
await expect(preview.getByRole('alert')).toHaveText('Не удалось загрузить аудио: HTTP 403.');
available = true;
await preview.getByRole('button', { name: 'Повторить', exact: true }).click();
await expect.poll(() => preview.locator('audio').evaluate((el: HTMLAudioElement) => el.duration)).toBe(2);
await expect(preview.getByRole('alert')).toHaveCount(0);
});