feat: make media inspection progressive

This commit is contained in:
2026-07-27 18:25:47 +02:00
parent 6c68f9bfbd
commit f2c0fc12d2
32 changed files with 2169 additions and 462 deletions

View File

@@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test';
import {
observeRuntime,
PRIMARY_FIXTURE,
startNestedStaticServer,
type NestedStaticServer,
} from './browser-helpers';
@@ -119,6 +120,38 @@ test.describe('static application and Toolbox shell', () => {
await expect(inspectorDrawer).toBeHidden();
await expect(inspector).toBeFocused();
});
test('shows native preview metadata without loading the FFmpeg runtime', async ({
page,
}) => {
const runtime = observeRuntime(page);
await page.goto('/');
await page
.locator('input[type="file"][accept*="audio"]')
.first()
.setInputFiles(PRIMARY_FIXTURE);
await expect(page.locator('.quick-preview video')).toBeVisible();
const card = page
.locator('.media-card')
.filter({ hasText: 'pattern-av.mp4' });
await expect(card.locator('.phase--idle')).toContainText(
'Ready to play · Details on demand'
);
await expect(card).toContainText(/\d+:\d{2}\.\d{2}/u);
expect(
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/'))
).toBe(false);
await page.getByRole('tab', { name: 'Edit' }).click();
await expect(
page.getByRole('button', { name: 'Inspect & render timeline' })
).toBeEnabled();
expect(
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/'))
).toBe(false);
});
});
test.describe('nested static deployment and Toolbox context', () => {

View File

@@ -7,6 +7,7 @@ import { tmpdir } from 'node:os';
import { dirname, extname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { ConsoleMessage, Page, Request, Response } from '@playwright/test';
import { expect } from '@playwright/test';
export const REPOSITORY_ROOT = resolve(
dirname(fileURLToPath(import.meta.url)),
@@ -19,6 +20,20 @@ export const GENERATED_FIXTURES = join(
);
export const PRIMARY_FIXTURE = join(GENERATED_FIXTURES, 'pattern-av.mp4');
export async function inspectMediaDetails(
page: Page,
fileName: string
): Promise<void> {
const card = page.locator('.media-card').filter({ hasText: fileName });
const inspect = page.getByRole('button', {
name: `Inspect details for ${fileName}`,
});
if ((await inspect.count()) > 0) await inspect.click();
await expect(card.locator('.phase--ready')).toBeVisible({
timeout: 120_000,
});
}
const NESTED_PREFIX = '/deep/nested/av/';
const SECURITY_HEADERS = {
'Cross-Origin-Embedder-Policy': 'require-corp',

View File

@@ -6,6 +6,7 @@ import { expect, test } from '@playwright/test';
import {
createCancellationFixture,
GENERATED_FIXTURES,
inspectMediaDetails,
observeRuntime,
PRIMARY_FIXTURE,
type TemporaryMediaFixture,
@@ -99,8 +100,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
).toBe(false);
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
await inspectMediaDetails(page, 'pattern-av.mp4');
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
'Details ready',
{ timeout: 120_000 }
);
await page.getByRole('button', { name: 'Convert', exact: true }).click();
@@ -184,8 +186,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
).toBe(false);
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
await inspectMediaDetails(page, 'pattern-av.mp4');
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
'Details ready',
{ timeout: 120_000 }
);
await expect(page.locator('.source-summary')).toContainText(
@@ -275,6 +278,7 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
await mediaInput(page)
.first()
.setInputFiles(join(GENERATED_FIXTURES, 'compatible-a.mp4'));
await inspectMediaDetails(page, 'compatible-a.mp4');
await expect(
page
.locator('.media-card')
@@ -329,8 +333,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
await page.goto('/');
await selectSingleThread(page);
await mediaInput(page).setInputFiles(cancellationFixture.filePath);
await inspectMediaDetails(page, basename(cancellationFixture.filePath));
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
'Details ready',
{ timeout: 120_000 }
);
@@ -357,8 +362,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
})
.click();
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
await inspectMediaDetails(page, 'pattern-av.mp4');
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
'Details ready',
{ timeout: 120_000 }
);
await page.getByRole('button', { name: 'Convert', exact: true }).click();
@@ -388,10 +394,15 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
const queuedCard = page.locator('.media-card').filter({
hasText: 'pattern-av.mp4',
});
await page
.getByRole('button', {
name: `Inspect details for ${activeName}`,
})
.click();
await expect(activeCard.locator('.phase--probing')).toBeVisible({
timeout: 30_000,
});
await expect(queuedCard.locator('.phase--queued')).toBeVisible();
await expect(queuedCard.locator('.phase--idle')).toBeVisible();
await page.getByRole('button', { name: 'Remove pattern-av.mp4' }).click();
await page.getByRole('button', { name: `Remove ${activeName}` }).click();
@@ -401,8 +412,9 @@ test.describe.serial('real pinned FFmpeg browser runtime', () => {
).toBeEnabled({ timeout: 120_000 });
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
await inspectMediaDetails(page, 'pattern-av.mp4');
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
'Details ready',
{ timeout: 120_000 }
);
await expect(page.getByRole('alert')).toHaveCount(0);

View File

@@ -4,6 +4,7 @@ import { join } from 'node:path';
import { expect, test, type Page } from '@playwright/test';
import {
GENERATED_FIXTURES,
inspectMediaDetails,
observeRuntime,
PRIMARY_FIXTURE,
type RuntimeObservation,
@@ -51,8 +52,9 @@ test.describe
await mediaInput(page).setInputFiles(
join(GENERATED_FIXTURES, 'attached-cover.mp3')
);
await inspectMediaDetails(page, 'attached-cover.mp3');
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
'Details ready',
{ timeout: 60_000 }
);
await expect(page.locator('.source-summary')).toContainText(
@@ -108,13 +110,23 @@ test.describe
await mediaInput(page).setInputFiles(
join(GENERATED_FIXTURES, 'truncated.mp4')
);
await page
.getByRole('button', {
name: 'Inspect details for truncated.mp4',
})
.click();
const malformedCard = page
.locator('.media-card')
.filter({ hasText: 'truncated.mp4' });
await expect(malformedCard.locator('.phase--error')).toContainText(
'ffprobe did not find any media streams.',
'File ready · Details unavailable',
{ timeout: 30_000 }
);
await expect(
page.getByRole('button', {
name: 'Retry detailed inspection for truncated.mp4',
})
).toHaveAttribute('title', 'ffprobe did not find any media streams.');
expect(Date.now() - probeStartedAt).toBeLessThan(30_000);
await expect(
page
@@ -123,6 +135,7 @@ test.describe
).toBeVisible();
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
await inspectMediaDetails(page, 'pattern-av.mp4');
const validCard = page
.locator('.media-card')
.filter({ hasText: 'pattern-av.mp4' });

View File

@@ -6,7 +6,7 @@ import { expect, test } from '@playwright/test';
const mediaPath = process.env.AV_TOOLS_LARGE_MEDIA;
test('plays a recognized large local video while inspection continues', async ({
test('plays a recognized large local video before detailed inspection', async ({
page,
}) => {
test.skip(
@@ -27,7 +27,15 @@ test('plays a recognized large local video while inspection continues', async ({
await expect(preview).toBeVisible({ timeout: 15_000 });
const previewVisibleMilliseconds = Date.now() - startedAt;
expect(previewVisibleMilliseconds).toBeLessThan(15_000);
await expect(card.locator('.phase--probing')).toBeVisible();
await expect(card.locator('.phase--idle')).toContainText('Details on demand');
await expect(card).toContainText(/43:42\.\d{2}/u, { timeout: 15_000 });
expect(
await page.evaluate(() =>
performance
.getEntriesByType('resource')
.some((entry) => entry.name.includes('/vendor/ffmpeg/'))
)
).toBe(false);
const elementIdentity = await preview.evaluate((element) => {
element.dataset.evidenceIdentity = crypto.randomUUID();
@@ -50,9 +58,11 @@ test('plays a recognized large local video while inspection continues', async ({
)
.toBeGreaterThan(0);
await expect(card.locator('.phase--ready')).toBeVisible({
timeout: 330_000,
});
await page
.getByRole('button', { name: `Inspect details for ${name}` })
.click();
await expect(card.locator('.phase--probing')).toBeVisible();
await expect(card.locator('.phase--ready')).toBeVisible({ timeout: 120_000 });
await expect(preview).toHaveAttribute(
'data-evidence-identity',
elementIdentity

View File

@@ -31,6 +31,9 @@ async function importMedia(
await mediaInput(page).setInputFiles(names.map((name) => fixture(name)));
for (const name of names) {
const card = page.locator('.media-card').filter({ hasText: name });
await page
.getByRole('button', { name: `Inspect details for ${name}` })
.click();
await expect(card.locator('.phase--ready')).toBeVisible({
timeout: 120_000,
});
@@ -210,6 +213,11 @@ test.describe.serial('real ffmpeg.wasm operation matrix', () => {
await test.step('queues a second browser job, cancels the active job and runs the waiting job after recovery', async () => {
const sourceName = basename(cancellationFixture.filePath);
await mediaInput(page).setInputFiles(cancellationFixture.filePath);
await page
.getByRole('button', {
name: `Inspect details for ${sourceName}`,
})
.click();
await expect(
page
.locator('.media-card')
@@ -1055,6 +1063,11 @@ test.describe.serial('real ffmpeg.wasm operation matrix', () => {
mimeType: 'video/x-matroska',
buffer: readFileSync(muxed.filePath),
});
await page
.getByRole('button', {
name: 'Inspect details for subtitled-source.mkv',
})
.click();
await expect(
page
.locator('.media-card')

View File

@@ -103,8 +103,9 @@ async function runMode(
const probeStarted = await monotonicNow(page);
await mediaInput(page).setInputFiles(fixturePath);
await page.getByRole('button', { name: /^Inspect details for /u }).click();
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
'Details ready',
{ timeout: 120_000 }
);
const probeMs = (await monotonicNow(page)) - probeStarted;

View File

@@ -21,8 +21,11 @@ test('covers drop, editor keyboard/drag controls, cache, and project reattachmen
await page.getByLabel('Engine mode').selectOption('force-single-thread');
await dropFixture(page, PRIMARY_FIXTURE, 'video/mp4');
await page
.getByRole('button', { name: 'Inspect details for pattern-av.mp4' })
.click();
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
'Details ready',
{ timeout: 120_000 }
);
await expect(page.locator('.quick-preview video')).toHaveAttribute(
@@ -47,6 +50,9 @@ test('covers drop, editor keyboard/drag controls, cache, and project reattachmen
await page.getByRole('tab', { name: 'Edit' }).click();
const compatibleB = join(GENERATED_FIXTURES, 'compatible-b.mp4');
await mediaInput(page).setInputFiles(compatibleB);
await page
.getByRole('button', { name: 'Inspect details for compatible-b.mp4' })
.click();
await expect(page.locator('.media-card .phase--ready')).toHaveCount(2, {
timeout: 120_000,
});
@@ -127,6 +133,12 @@ test('covers drop, editor keyboard/drag controls, cache, and project reattachmen
page.on('dialog', (dialog) => void dialog.accept());
await mediaInput(page).setInputFiles([PRIMARY_FIXTURE, compatibleB]);
await page
.getByRole('button', { name: 'Inspect details for pattern-av.mp4' })
.click();
await page
.getByRole('button', { name: 'Inspect details for compatible-b.mp4' })
.click();
await expect(page.locator('.media-card .phase--ready')).toHaveCount(2, {
timeout: 120_000,
});