feat: publish av-tools 0.1.0

This commit is contained in:
2026-07-24 14:58:03 +02:00
commit fcc537b024
254 changed files with 63270 additions and 0 deletions

View File

@@ -0,0 +1,256 @@
import { expect, test } from '@playwright/test';
import {
observeRuntime,
startNestedStaticServer,
type NestedStaticServer,
} from './browser-helpers';
test.describe('static application and Toolbox shell', () => {
test('runs standalone, stays lazy and supports keyboard-operated shell controls', async ({
page,
}) => {
const runtime = observeRuntime(page);
await page.goto('/');
await expect(page).toHaveTitle('Audio & Video Tools');
await expect(
page.getByRole('heading', {
level: 1,
name: 'Audio & Video Tools',
})
).toBeVisible();
await expect(page.locator('[data-toolbox-context]')).toHaveAttribute(
'data-toolbox-context',
'standalone'
);
await expect(page.getByRole('main')).toBeVisible();
await expect(
page.getByRole('tab', { name: 'Quick Convert' })
).toHaveAttribute('aria-selected', 'true');
await expect(
page.getByRole('button', { name: 'Choose files' })
).toBeEnabled();
expect(
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/'))
).toBe(false);
expect(
runtime.requests.some((url) => /\/assets\/worker-[^/]+\.js$/u.test(url))
).toBe(false);
const helpButton = page.getByRole('button', { name: 'Help' });
await helpButton.focus();
await page.keyboard.press('Enter');
const helpDialog = page.getByRole('dialog', {
name: 'Audio & Video Tools',
});
await expect(helpDialog).toBeVisible();
await expect(
helpDialog.getByRole('heading', {
level: 2,
name: 'Audio & Video Tools',
})
).toBeVisible();
await page.keyboard.press('Escape');
await expect(helpDialog).toBeHidden();
const personalizeButton = page.getByRole('button', {
name: 'Personalize',
});
await personalizeButton.focus();
await page.keyboard.press('Enter');
await expect(
page.getByRole('heading', {
level: 2,
name: 'Personalize your toolbox',
})
).toBeVisible();
await page.getByRole('button', { name: 'Dark' }).click();
await expect(page.locator('.toolbox-shell').first()).toHaveAttribute(
'data-toolbox-theme',
'dark'
);
await page.keyboard.press('Escape');
await expect(personalizeButton).toBeFocused();
const networkUrls = runtime.requests.filter((url) => /^https?:/u.test(url));
expect(
networkUrls.every(
(url) => new URL(url).origin === 'http://127.0.0.1:4173'
)
).toBe(true);
expect(runtime.failedRequests).toEqual([]);
expect(runtime.pageErrors).toEqual([]);
expect(runtime.consoleErrors).toEqual([]);
});
test('keeps preview primary and exposes accessible Edit drawers on narrow screens', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
await page.getByRole('tab', { name: 'Edit' }).click();
await expect(page.getByRole('heading', { name: 'Preview' })).toBeVisible();
const mediaDrawer = page.locator('#edit-media-drawer');
const inspectorDrawer = page.locator('#edit-inspector-drawer');
await expect(mediaDrawer).toBeHidden();
await expect(inspectorDrawer).toBeHidden();
const sources = page.getByRole('button', { name: 'Sources' });
await sources.click();
await expect(mediaDrawer).toBeVisible();
await expect(
mediaDrawer.getByRole('button', { name: 'Close media bin' })
).toBeFocused();
await page.keyboard.press('Escape');
await expect(mediaDrawer).toBeHidden();
await expect(sources).toBeFocused();
const inspector = page.getByRole('button', { name: 'Inspector' });
await inspector.click();
await expect(inspectorDrawer).toBeVisible();
const closeInspector = inspectorDrawer.getByRole('button', {
name: 'Close inspector',
});
await expect(closeInspector).toBeFocused();
await closeInspector.click();
await expect(inspectorDrawer).toBeHidden();
await expect(inspector).toBeFocused();
});
});
test.describe('nested static deployment and Toolbox context', () => {
let nestedServer: NestedStaticServer;
test.beforeAll(async () => {
nestedServer = await startNestedStaticServer();
});
test.afterAll(async () => {
await nestedServer.close();
});
test('loads a valid same-origin catalog and all runtime assets below the nested path', async ({
page,
}) => {
const runtime = observeRuntime(page);
await page.goto(`${nestedServer.appUrl}?toolbox=../toolbox.catalog.json`);
await expect(page.locator('[data-toolbox-context]')).toHaveAttribute(
'data-toolbox-context',
'connected'
);
await expect(
page.getByRole('link', { name: 'add·ideas Toolbox' })
).toHaveAttribute('href', `${nestedServer.origin}/deep/nested/`);
await page.getByRole('button', { name: 'Apps' }).click();
const appSwitcher = page.getByRole('navigation', {
name: 'Toolbox applications',
});
await expect(
appSwitcher.getByRole('link', { name: 'Browser Test Tool' })
).toBeVisible();
await expect(
appSwitcher.getByRole('link', { name: 'Audio & Video Tools' })
).toBeVisible();
await page.keyboard.press('Escape');
expect(
runtime.requests.some((url) =>
/\/deep\/nested\/av\/assets\/index-[^/]+\.js$/u.test(url)
)
).toBe(true);
expect(
runtime.requests.some((url) =>
/\/deep\/nested\/av\/assets\/index-[^/]+\.css$/u.test(url)
)
).toBe(true);
expect(
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/'))
).toBe(false);
await page.getByText('Engine & browser').click();
await page.getByLabel('Engine mode').selectOption('force-single-thread');
await page.getByRole('button', { name: 'Initialize engine' }).click();
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible({ timeout: 120_000 });
await expect(page.getByText('0.12.15 / 0.12.10')).toBeVisible();
await expect(
page.getByText('Encoders').locator('..').locator('dd')
).not.toHaveText('0');
await expect(
page.getByText('Filters').locator('..').locator('dd')
).not.toHaveText('0');
const coreScript = runtime.responses.find((response) =>
response.url.endsWith(
'/deep/nested/av/vendor/ffmpeg/0.12.10/st/ffmpeg-core.js'
)
);
const coreWasm = runtime.responses.find((response) =>
response.url.endsWith(
'/deep/nested/av/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm'
)
);
expect(coreScript).toMatchObject({
status: 200,
contentType: 'text/javascript; charset=utf-8',
});
expect(coreWasm).toMatchObject({
status: 200,
contentType: 'application/wasm',
});
expect(
runtime.responses.some((response) =>
/\/deep\/nested\/av\/assets\/worker-[^/]+\.js$/u.test(response.url)
)
).toBe(true);
const networkUrls = runtime.requests.filter((url) => /^https?:/u.test(url));
expect(
networkUrls.every((url) => new URL(url).origin === nestedServer.origin)
).toBe(true);
expect(runtime.failedRequests).toEqual([]);
expect(runtime.pageErrors).toEqual([]);
expect(runtime.consoleErrors).toEqual([]);
});
test('rejects an invalid same-origin catalog and remains usable standalone', async ({
page,
}) => {
const runtime = observeRuntime(page);
await page.goto(`${nestedServer.appUrl}?toolbox=../invalid-catalog.json`);
await expect
.poll(() => runtime.consoleWarnings.length, { timeout: 10_000 })
.toBeGreaterThan(0);
await expect(page.locator('[data-toolbox-context]')).toHaveAttribute(
'data-toolbox-context',
'standalone'
);
await expect(
page.getByRole('button', { name: 'Choose files' })
).toBeEnabled();
expect(
runtime.consoleWarnings.some((warning) =>
warning.includes('Toolbox context unavailable; continuing standalone.')
)
).toBe(true);
expect(
runtime.requests.some(
(url) =>
url === `${nestedServer.origin}/deep/nested/invalid-catalog.json`
)
).toBe(true);
expect(runtime.failedRequests).toEqual([]);
expect(runtime.pageErrors).toEqual([]);
expect(runtime.consoleErrors).toEqual([]);
});
});

View File

@@ -0,0 +1,298 @@
/// <reference types="node" />
import { createServer, type Server } from 'node:http';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
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';
export const REPOSITORY_ROOT = resolve(
dirname(fileURLToPath(import.meta.url)),
'../..'
);
export const DIST_ROOT = join(REPOSITORY_ROOT, 'dist');
export const GENERATED_FIXTURES = join(
REPOSITORY_ROOT,
'tests/fixtures/generated'
);
export const PRIMARY_FIXTURE = join(GENERATED_FIXTURES, 'pattern-av.mp4');
const NESTED_PREFIX = '/deep/nested/av/';
const SECURITY_HEADERS = {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin',
};
function contentType(filePath: string): string {
const types: Readonly<Record<string, string>> = {
'.css': 'text/css; charset=utf-8',
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.wasm': 'application/wasm',
};
return types[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
}
function readApplicationManifest(): Record<string, unknown> {
return JSON.parse(
readFileSync(join(DIST_ROOT, 'toolbox-app.json'), 'utf8')
) as Record<string, unknown>;
}
function writeJson(
response: import('node:http').ServerResponse,
status: number,
value: unknown
): void {
response.writeHead(status, {
...SECURITY_HEADERS,
'Cache-Control': 'no-store',
'Content-Type': 'application/json; charset=utf-8',
});
response.end(JSON.stringify(value));
}
function serveFile(
response: import('node:http').ServerResponse,
filePath: string
): void {
try {
const stats = statSync(filePath);
if (!stats.isFile()) {
response.writeHead(404, SECURITY_HEADERS);
response.end('Not found');
return;
}
response.writeHead(200, {
...SECURITY_HEADERS,
'Cache-Control': 'no-store',
'Content-Length': String(stats.size),
'Content-Type': contentType(filePath),
});
response.end(readFileSync(filePath));
} catch {
response.writeHead(404, SECURITY_HEADERS);
response.end('Not found');
}
}
function closeServer(server: Server): Promise<void> {
return new Promise((resolveClose, rejectClose) => {
server.close((error) => {
if (error) rejectClose(error);
else resolveClose();
});
});
}
export interface NestedStaticServer {
origin: string;
appUrl: string;
close: () => Promise<void>;
}
/**
* Serves the built artifact exactly as it will be assembled below a portal
* path. This catches root-absolute URLs that a root-only preview cannot.
*/
export async function startNestedStaticServer(): Promise<NestedStaticServer> {
const applicationManifest = readApplicationManifest();
const otherManifest = {
...applicationManifest,
id: 'de.add-ideas.browser-test-tool',
name: 'Browser Test Tool',
description: 'A same-origin catalog peer used by browser validation.',
entry: './',
actions: [],
};
const catalog = {
$schema:
'https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-catalog.v1.schema.json',
schemaVersion: 1,
id: 'de.add-ideas.browser-test-toolbox',
name: 'Browser Test Toolbox',
home: './',
theme: { mode: 'system', brand: 'Browser test' },
apps: [
{ manifest: './av/toolbox-app.json', enabled: true },
{ manifest: './other/toolbox-app.json', enabled: true },
],
};
const distWithSeparator = `${resolve(DIST_ROOT)}${sep}`;
const server = createServer((request, response) => {
const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
const { pathname } = requestUrl;
if (pathname === '/deep/nested/toolbox.catalog.json') {
writeJson(response, 200, catalog);
return;
}
if (pathname === '/deep/nested/invalid-catalog.json') {
writeJson(response, 200, {
schemaVersion: 999,
id: 'invalid',
});
return;
}
if (pathname === '/deep/nested/other/toolbox-app.json') {
writeJson(response, 200, otherManifest);
return;
}
if (pathname === '/deep/nested/favicon.svg') {
serveFile(response, join(DIST_ROOT, 'favicon.svg'));
return;
}
if (!pathname.startsWith(NESTED_PREFIX)) {
response.writeHead(404, SECURITY_HEADERS);
response.end('Not found');
return;
}
const relativePath = decodeURIComponent(
pathname.slice(NESTED_PREFIX.length)
);
const requestedPath = resolve(
DIST_ROOT,
relativePath === '' ? 'index.html' : relativePath
);
if (
requestedPath !== resolve(DIST_ROOT) &&
!requestedPath.startsWith(distWithSeparator)
) {
response.writeHead(400, SECURITY_HEADERS);
response.end('Invalid path');
return;
}
serveFile(response, requestedPath);
});
await new Promise<void>((resolveListen, rejectListen) => {
server.once('error', rejectListen);
server.listen(0, '127.0.0.1', () => {
server.off('error', rejectListen);
resolveListen();
});
});
const address = server.address();
if (!address || typeof address === 'string') {
await closeServer(server);
throw new Error('Nested test server did not expose a TCP port');
}
const origin = `http://127.0.0.1:${address.port}`;
return {
origin,
appUrl: `${origin}${NESTED_PREFIX}`,
close: () => closeServer(server),
};
}
export interface RuntimeObservation {
requests: string[];
responses: Array<{
url: string;
status: number;
contentType: string | undefined;
}>;
failedRequests: string[];
pageErrors: string[];
consoleErrors: string[];
consoleWarnings: string[];
}
export function observeRuntime(page: Page): RuntimeObservation {
const observation: RuntimeObservation = {
requests: [],
responses: [],
failedRequests: [],
pageErrors: [],
consoleErrors: [],
consoleWarnings: [],
};
page.on('request', (request: Request) => {
observation.requests.push(request.url());
});
page.on('response', (response: Response) => {
observation.responses.push({
url: response.url(),
status: response.status(),
contentType: response.headers()['content-type'],
});
});
page.on('requestfailed', (request: Request) => {
observation.failedRequests.push(
`${request.url()}: ${request.failure()?.errorText ?? 'unknown failure'}`
);
});
page.on('pageerror', (error: Error) => {
observation.pageErrors.push(error.stack ?? error.message);
});
page.on('console', (message: ConsoleMessage) => {
if (message.type() === 'error') {
observation.consoleErrors.push(message.text());
}
if (message.type() === 'warning') {
observation.consoleWarnings.push(message.text());
}
});
return observation;
}
export interface TemporaryMediaFixture {
filePath: string;
dispose: () => void;
}
export function createCancellationFixture(): TemporaryMediaFixture {
const directory = mkdtempSync(join(tmpdir(), 'av-tools-browser-'));
const filePath = join(directory, 'long-cancellation-source.mp4');
try {
execFileSync(
'ffmpeg',
[
'-hide_banner',
'-loglevel',
'error',
'-y',
'-stream_loop',
'14',
'-i',
PRIMARY_FIXTURE,
'-t',
'30',
'-vf',
'scale=1280:720',
'-r',
'30',
'-c:v',
'libx264',
'-preset',
'ultrafast',
'-crf',
'32',
'-c:a',
'aac',
'-b:a',
'64k',
filePath,
],
{ stdio: 'pipe' }
);
} catch (error) {
rmSync(directory, { recursive: true, force: true });
throw error;
}
return {
filePath,
dispose: () => rmSync(directory, { recursive: true, force: true }),
};
}

View File

@@ -0,0 +1,359 @@
/// <reference types="node" />
import { execFileSync } from 'node:child_process';
import { basename, join } from 'node:path';
import { expect, test } from '@playwright/test';
import {
createCancellationFixture,
GENERATED_FIXTURES,
observeRuntime,
PRIMARY_FIXTURE,
type TemporaryMediaFixture,
} from './browser-helpers';
function mediaInput(page: import('@playwright/test').Page) {
return page.locator('input[type="file"][accept*="audio"]');
}
async function selectSingleThread(
page: import('@playwright/test').Page
): Promise<void> {
await page.getByText('Engine & browser').click();
await page.getByLabel('Engine mode').selectOption('force-single-thread');
}
function assertLocalOnly(runtime: ReturnType<typeof observeRuntime>): void {
const networkUrls = runtime.requests.filter((url) => /^https?:/u.test(url));
expect(
networkUrls.every((url) => new URL(url).origin === 'http://127.0.0.1:4173')
).toBe(true);
const materialFailures = runtime.failedRequests.filter(
(failure) =>
!(failure.startsWith('blob:') && failure.endsWith('net::ERR_ABORTED'))
);
// Chromium aborts the superseded local preview blob when React switches from
// the source object URL to the generated result object URL.
expect(materialFailures).toEqual([]);
expect(runtime.pageErrors).toEqual([]);
expect(runtime.consoleErrors).toEqual([]);
}
test.describe.serial('real pinned FFmpeg browser runtime', () => {
let cancellationFixture: TemporaryMediaFixture;
test.beforeAll(() => {
cancellationFixture = createCancellationFixture();
});
test.afterAll(() => {
cancellationFixture.dispose();
});
test('loads and queries the multithread core under cross-origin isolation', async ({
page,
}) => {
const runtime = observeRuntime(page);
await page.goto('/');
const environment = await page.evaluate(() => ({
isolated: globalThis.crossOriginIsolated,
sharedArrayBuffer: typeof globalThis.SharedArrayBuffer,
}));
test.skip(
!environment.isolated || environment.sharedArrayBuffer !== 'function',
'Chromium did not expose SharedArrayBuffer in this server context'
);
await page.getByText('Engine & browser').click();
await page.getByRole('button', { name: 'Initialize engine' }).click();
await expect(
page.locator('.engine-pill').filter({ hasText: 'Multithreaded' })
).toBeVisible({ timeout: 120_000 });
await expect(page.getByText('0.12.15 / 0.12.10')).toBeVisible();
await expect(page.getByText('Isolation').locator('..')).toContainText(
'Enabled'
);
const expectedAssets = [
'/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.js',
'/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.wasm',
'/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.worker.js',
];
for (const suffix of expectedAssets) {
expect(
runtime.responses.some(
(response) => response.status === 200 && response.url.endsWith(suffix)
)
).toBe(true);
}
const wasm = runtime.responses.find((response) =>
response.url.endsWith('/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.wasm')
);
expect(wasm?.contentType).toContain('application/wasm');
expect(
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/0.12.10/st/'))
).toBe(false);
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
{ timeout: 120_000 }
);
await page.getByRole('button', { name: 'Convert', exact: true }).click();
const result = page.locator('.result-card');
await expect(result).toContainText('pattern-av-convert.mp4', {
timeout: 120_000,
});
await expect(result).toContainText(/created · verified|completed locally/u);
const outputBytes = await page
.locator('.quick-preview video')
.evaluate(async (element) => {
const response = await fetch((element as HTMLVideoElement).src);
return (await response.arrayBuffer()).byteLength;
});
expect(outputBytes).toBeGreaterThan(1_000);
assertLocalOnly(runtime);
});
test('falls back to the single-thread core when the preferred multithread core cannot load', async ({
page,
}) => {
const runtime = observeRuntime(page);
await page.route('**/vendor/ffmpeg/0.12.10/mt/**', (route) =>
route.abort('failed')
);
await page.goto('/');
const environment = await page.evaluate(() => ({
isolated: globalThis.crossOriginIsolated,
sharedArrayBuffer: typeof globalThis.SharedArrayBuffer,
}));
test.skip(
!environment.isolated || environment.sharedArrayBuffer !== 'function',
'Chromium did not expose SharedArrayBuffer in this server context'
);
await page.getByText('Engine & browser').click();
await page.getByLabel('Engine mode').selectOption('prefer-multithread');
await page.getByRole('button', { name: 'Initialize engine' }).click();
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible({ timeout: 120_000 });
await expect(
page.getByText(
'The multithread core failed to load. The app recovered with the single-thread compatibility core.'
)
).toBeVisible();
expect(
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/0.12.10/mt/'))
).toBe(true);
expect(
runtime.responses.some(
(response) =>
response.status === 200 &&
response.url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm')
)
).toBe(true);
expect(runtime.pageErrors).toEqual([]);
expect(runtime.consoleErrors).toEqual([]);
});
test('probes H.264/AAC media and produces a verified H.264/AAC result with the single-thread core', async ({
page,
}) => {
await page.addInitScript(() => {
Reflect.deleteProperty(globalThis, 'showSaveFilePicker');
});
const runtime = observeRuntime(page);
await page.goto('/');
await selectSingleThread(page);
expect(
runtime.requests.some((url) => url.includes('/vendor/ffmpeg/'))
).toBe(false);
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
{ timeout: 120_000 }
);
await expect(page.locator('.source-summary')).toContainText(
'pattern-av.mp4'
);
await expect(page.locator('.source-summary')).toContainText('0:02.00');
await expect(
page.getByRole('checkbox', { name: /#0 · h264/u })
).toBeChecked();
await expect(
page.getByRole('checkbox', { name: /#1 · aac/u })
).toBeChecked();
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible();
await page.getByRole('button', { name: 'Convert', exact: true }).click();
const result = page.locator('.result-card');
await expect(result).toBeVisible({ timeout: 120_000 });
await expect(result).toContainText('pattern-av-convert.mp4');
await expect(result).toContainText(/created · verified|completed locally/u);
const resultVideo = page.locator('.quick-preview video');
await expect(resultVideo).toHaveAttribute('src', /^blob:/u);
const outputBytes = await resultVideo.evaluate(async (element) => {
const video = element as HTMLVideoElement;
const response = await fetch(video.src);
return new Uint8Array(await response.arrayBuffer()).length;
});
expect(outputBytes).toBeGreaterThan(1_000);
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Save result' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('pattern-av-convert.mp4');
const downloadedPath = await download.path();
expect(downloadedPath).not.toBeNull();
const probeJson = execFileSync(
'ffprobe',
[
'-v',
'error',
'-show_entries',
'stream=codec_name,codec_type',
'-of',
'json',
downloadedPath as string,
],
{ encoding: 'utf8' }
);
const nativeProbe = JSON.parse(probeJson) as {
streams?: Array<{ codec_name?: string; codec_type?: string }>;
};
expect(nativeProbe.streams).toEqual(
expect.arrayContaining([
expect.objectContaining({
codec_name: 'h264',
codec_type: 'video',
}),
expect.objectContaining({
codec_name: 'aac',
codec_type: 'audio',
}),
])
);
expect(
runtime.requests.some((url) =>
url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm')
)
).toBe(true);
assertLocalOnly(runtime);
});
test('refreshes retained tile-filter state before a fourth contact sheet', async ({
page,
}) => {
test.setTimeout(120_000);
const runtime = observeRuntime(page);
await page.goto('/');
await selectSingleThread(page);
await page.getByRole('tab', { name: 'Edit' }).click();
await mediaInput(page)
.first()
.setInputFiles(join(GENERATED_FIXTURES, 'compatible-a.mp4'));
await expect(
page
.locator('.media-card')
.filter({ hasText: 'compatible-a.mp4' })
.locator('.phase--ready')
).toBeVisible({ timeout: 120_000 });
const advanced = page.locator('.advanced-operations');
await advanced
.getByRole('tab', { name: 'Thumbnails and contact sheets' })
.click();
await advanced
.getByRole('spinbutton', { name: 'Frames', exact: true })
.fill('4');
await advanced
.getByRole('spinbutton', { name: 'Rows', exact: true })
.fill('2');
await advanced
.getByRole('spinbutton', { name: 'Columns', exact: true })
.fill('2');
await advanced
.getByRole('spinbutton', { name: /^Cell width/u })
.fill('128');
const results = page.locator('.result-card');
const create = advanced.getByRole('button', {
name: 'Generate contact sheet',
});
for (let index = 1; index <= 4; index += 1) {
await create.click();
await expect(results).toHaveCount(index, { timeout: 120_000 });
await expect(page.getByRole('alert')).toHaveCount(0);
}
await expect(
results.filter({ hasText: 'compatible-a-contact-sheet' })
).toHaveCount(4);
expect(
runtime.requests.filter((url) =>
url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm')
)
).toHaveLength(2);
assertLocalOnly(runtime);
});
test('cancels an active transcode, recreates the engine and completes another job', async ({
page,
}) => {
const runtime = observeRuntime(page);
await page.goto('/');
await selectSingleThread(page);
await mediaInput(page).setInputFiles(cancellationFixture.filePath);
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
{ timeout: 120_000 }
);
await page.getByRole('button', { name: 'Convert', exact: true }).click();
const cancelButton = page.getByRole('button', {
name: 'Cancel & recover',
});
await expect(cancelButton).toBeVisible({ timeout: 30_000 });
await cancelButton.click();
await expect(page.getByRole('alert')).toContainText(
'The operation was cancelled and the engine was recreated.',
{ timeout: 120_000 }
);
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible({ timeout: 120_000 });
await page
.getByRole('button', {
name: `Remove ${basename(cancellationFixture.filePath)}`,
})
.click();
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
{ timeout: 120_000 }
);
await page.getByRole('button', { name: 'Convert', exact: true }).click();
await expect(page.locator('.result-card')).toContainText(
'pattern-av-convert.mp4',
{ timeout: 120_000 }
);
await expect(page.getByRole('alert')).toHaveCount(0);
assertLocalOnly(runtime);
});
});

View File

@@ -0,0 +1,143 @@
/// <reference types="node" />
import { join } from 'node:path';
import { expect, test, type Page } from '@playwright/test';
import {
GENERATED_FIXTURES,
observeRuntime,
PRIMARY_FIXTURE,
type RuntimeObservation,
} from './browser-helpers';
function mediaInput(page: Page) {
return page.locator('input[type="file"][accept*="audio"]').first();
}
async function initializeSingleThread(page: Page): Promise<void> {
await page.getByText('Engine & browser').click();
await page.getByLabel('Engine mode').selectOption('force-single-thread');
await page.getByRole('button', { name: 'Initialize engine' }).click();
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible({ timeout: 120_000 });
}
function assertLocalOnly(runtime: RuntimeObservation): void {
const networkUrls = runtime.requests.filter((url) => /^https?:/u.test(url));
expect(
networkUrls.every((url) => new URL(url).origin === 'http://127.0.0.1:4173')
).toBe(true);
const materialFailures = runtime.failedRequests.filter(
(failure) =>
!(failure.startsWith('blob:') && failure.endsWith('net::ERR_ABORTED'))
);
expect(materialFailures).toEqual([]);
expect(runtime.pageErrors).toEqual([]);
expect(runtime.consoleErrors).toEqual([]);
}
test.describe
.serial('generated edge-case fixtures in pinned ffmpeg.wasm', () => {
test('probes attached cover art without treating it as the playback video stream', async ({
page,
}) => {
test.setTimeout(180_000);
const runtime = observeRuntime(page);
await page.goto('/');
await initializeSingleThread(page);
await mediaInput(page).setInputFiles(
join(GENERATED_FIXTURES, 'attached-cover.mp3')
);
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
{ timeout: 60_000 }
);
await expect(page.locator('.source-summary')).toContainText(
'attached-cover.mp3'
);
await expect(page.locator('.source-summary')).toContainText('0:01.08');
await expect(page.locator('.quick-preview audio')).toHaveAttribute(
'src',
/^blob:/u
);
await expect(page.locator('.quick-preview video')).toHaveCount(0);
await expect(
page.getByRole('checkbox', { name: /#0 · mp3/u })
).toBeChecked();
await expect(
page.getByRole('checkbox', { name: /#1 · png/u })
).toBeChecked();
await page.getByRole('tab', { name: 'Edit' }).click();
const inspectorStreams = page.locator('.inspector-streams');
await expect(
inspectorStreams.locator('li').filter({ hasText: '#0 · mp3' })
).toContainText('audio');
await expect(
inspectorStreams.locator('li').filter({ hasText: '#1 · png' })
).toContainText('video');
const advanced = page.locator('.advanced-operations');
await advanced.getByRole('tab', { name: 'Metadata policies' }).click();
await expect(advanced.getByLabel('Cover art')).toHaveValue('keep');
await advanced
.getByText('Other stream dispositions', { exact: true })
.nth(1)
.click();
await expect(
advanced.getByRole('checkbox', {
name: 'Attached pic stream #1',
})
).toBeChecked();
assertLocalOnly(runtime);
});
test('bounds a truncated-file probe failure, cleans up, and probes a valid file next', async ({
page,
}) => {
test.setTimeout(180_000);
const runtime = observeRuntime(page);
await page.goto('/');
await initializeSingleThread(page);
const probeStartedAt = Date.now();
await mediaInput(page).setInputFiles(
join(GENERATED_FIXTURES, 'truncated.mp4')
);
const malformedCard = page
.locator('.media-card')
.filter({ hasText: 'truncated.mp4' });
await expect(malformedCard.locator('.phase--error')).toContainText(
'ffprobe did not find any media streams.',
{ timeout: 30_000 }
);
expect(Date.now() - probeStartedAt).toBeLessThan(30_000);
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible();
await mediaInput(page).setInputFiles(PRIMARY_FIXTURE);
const validCard = page
.locator('.media-card')
.filter({ hasText: 'pattern-av.mp4' });
await expect(validCard.locator('.phase--ready')).toContainText('1 audio', {
timeout: 60_000,
});
await validCard.click();
await expect(
page.getByRole('checkbox', { name: /#0 · h264/u })
).toBeChecked();
await expect(
page.getByRole('checkbox', { name: /#1 · aac/u })
).toBeChecked();
await expect(page.getByRole('alert')).toHaveCount(0);
assertLocalOnly(runtime);
});
});

View File

@@ -0,0 +1,996 @@
/// <reference types="node" />
import { execFileSync } from 'node:child_process';
import { readFileSync, statSync } from 'node:fs';
import { basename, join } from 'node:path';
import {
expect,
test,
type Download,
type Locator,
type Page,
} from '@playwright/test';
import {
createCancellationFixture,
GENERATED_FIXTURES,
observeRuntime,
type RuntimeObservation,
type TemporaryMediaFixture,
} from './browser-helpers';
const fixture = (name: string): string => join(GENERATED_FIXTURES, name);
function mediaInput(page: Page): Locator {
return page.locator('input[type="file"][accept*="audio"]').first();
}
async function importMedia(
page: Page,
names: readonly string[]
): Promise<void> {
await mediaInput(page).setInputFiles(names.map((name) => fixture(name)));
for (const name of names) {
const card = page.locator('.media-card').filter({ hasText: name });
await expect(card.locator('.phase--ready')).toBeVisible({
timeout: 120_000,
});
}
}
async function initializeSingleThread(page: Page): Promise<void> {
await page.getByText('Engine & browser').click();
await page.getByLabel('Engine mode').selectOption('force-single-thread');
await page.getByRole('button', { name: 'Initialize engine' }).click();
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible({ timeout: 120_000 });
}
const RESULT_SNAPSHOT_ATTRIBUTE = 'data-playwright-existing-result';
async function markExistingResults(page: Page): Promise<void> {
await page.locator('.result-card').evaluateAll((cards) => {
for (const card of cards) {
card.setAttribute('data-playwright-existing-result', '');
}
});
}
function newResultCards(page: Page): Locator {
return page.locator(`.result-card:not([${RESULT_SNAPSHOT_ATTRIBUTE}])`);
}
function accumulatedOutputName(fileName: string): RegExp {
const dot = fileName.lastIndexOf('.');
const stem = dot > 0 ? fileName.slice(0, dot) : fileName;
const extension = dot > 0 ? fileName.slice(dot) : '';
const escape = (value: string): string =>
value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
return new RegExp(`^${escape(stem)}(?:-\\d+)?${escape(extension)}$`, 'u');
}
async function runForResults(
page: Page,
action: () => Promise<void>,
expectedFileNames: readonly string[],
options: { verifyMedia?: boolean } = {}
): Promise<readonly Locator[]> {
await markExistingResults(page);
await action();
const cards = expectedFileNames.map((_, index) =>
newResultCards(page).nth(index)
);
await expect
.poll(
async () => {
if ((await cards[0]?.locator('strong').count()) === 1) {
return 'result';
}
if ((await page.getByRole('alert').count()) > 0) {
const alert = (await page.getByRole('alert').allTextContents()).join(
' | '
);
const diagnostics =
(await page.locator('.diagnostics pre').textContent()) ?? '';
throw new Error(
`${alert}\n\nFFmpeg diagnostics:\n${diagnostics
.split('\n')
.slice(-160)
.join('\n')}`
);
}
return 'waiting';
},
{ timeout: 120_000 }
)
.toBe('result');
for (const [index, card] of cards.entries()) {
await expect(card.locator('strong')).toHaveText(
accumulatedOutputName(expectedFileNames[index] as string),
{ timeout: 120_000 }
);
await expect(card).toContainText(
/created · (?:verified|verification warning)|completed locally/u,
{ timeout: 120_000 }
);
if (options.verifyMedia) {
await expect(card).toContainText(/verified stream/u, {
timeout: 120_000,
});
}
}
await expect(page.getByRole('alert')).toHaveCount(0);
return cards;
}
async function downloadResult(
page: Page,
card: Locator
): Promise<{ download: Download; filePath: string }> {
const downloadPromise = page.waitForEvent('download');
await card.locator('button.button--primary').click();
const download = await downloadPromise;
const filePath = await download.path();
expect(filePath).not.toBeNull();
expect(statSync(filePath as string).size).toBeGreaterThan(0);
return { download, filePath: filePath as string };
}
function probe(filePath: string): {
streams?: Array<{
codec_name?: string;
codec_type?: string;
width?: number;
height?: number;
avg_frame_rate?: string;
tags?: Record<string, string>;
disposition?: Record<string, number>;
}>;
format?: { tags?: Record<string, string> };
chapters?: Array<{ tags?: Record<string, string> }>;
} {
return JSON.parse(
execFileSync(
'ffprobe',
[
'-v',
'error',
'-show_streams',
'-show_format',
'-show_chapters',
'-of',
'json',
filePath,
],
{ encoding: 'utf8' }
)
) as ReturnType<typeof probe>;
}
function assertLocalOnly(runtime: RuntimeObservation): void {
const networkUrls = runtime.requests.filter((url) => /^https?:/u.test(url));
expect(
networkUrls.every((url) => new URL(url).origin === 'http://127.0.0.1:4173')
).toBe(true);
const materialFailures = runtime.failedRequests.filter(
(failure) =>
!(failure.startsWith('blob:') && failure.endsWith('net::ERR_ABORTED'))
);
expect(materialFailures).toEqual([]);
expect(runtime.pageErrors).toEqual([]);
expect(runtime.consoleErrors).toEqual([]);
}
test.describe.serial('real ffmpeg.wasm operation matrix', () => {
let cancellationFixture: TemporaryMediaFixture;
test.beforeAll(() => {
cancellationFixture = createCancellationFixture();
});
test.afterAll(() => {
cancellationFixture.dispose();
});
test('executes conversion, structural, analysis, image, metadata, chapter and subtitle workflows in one local engine session', async ({
page,
}) => {
test.setTimeout(300_000);
page.setDefaultTimeout(15_000);
await page.addInitScript(() => {
Reflect.deleteProperty(globalThis, 'showSaveFilePicker');
});
const runtime = observeRuntime(page);
await page.goto('/');
await initializeSingleThread(page);
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 expect(
page
.locator('.media-card')
.filter({ hasText: sourceName })
.locator('.phase--ready')
).toBeVisible({ timeout: 120_000 });
await page.getByRole('button', { name: 'Convert', exact: true }).click();
const enqueueButton = page.getByRole('button', {
name: 'Add to queue',
});
await expect(enqueueButton).toBeVisible();
await enqueueButton.click();
await expect(page.getByText('1 waiting', { exact: true })).toBeVisible();
const firstCancelButton = page.getByRole('button', {
name: 'Cancel & recover',
});
const firstCancelElement = await firstCancelButton.elementHandle();
await firstCancelButton.click();
await expect
.poll(
() =>
firstCancelElement?.evaluate((element) => element.isConnected) ??
false,
{ timeout: 120_000 }
)
.toBe(false);
await expect(page.getByText('1 waiting', { exact: true })).toBeHidden({
timeout: 120_000,
});
const secondCancelButton = page.getByRole('button', {
name: 'Cancel & recover',
});
await expect(secondCancelButton).toBeVisible({
timeout: 120_000,
});
await secondCancelButton.click();
await expect(secondCancelButton).toBeHidden({ timeout: 120_000 });
await expect(page.getByRole('alert')).toContainText(
'The operation was cancelled and the engine was recreated.',
{ timeout: 120_000 }
);
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible({ timeout: 120_000 });
await page.getByRole('button', { name: 'New', exact: true }).click();
});
await test.step('audio conversion and WebM remux/re-encode', async () => {
await importMedia(page, ['tone.flac']);
await page.getByLabel('Export preset').selectOption('audio-mp3');
const [mp3Card] = await runForResults(
page,
() =>
page.getByRole('button', { name: 'Convert', exact: true }).click(),
['tone-convert.mp3'],
{ verifyMedia: true }
);
const mp3 = await downloadResult(page, mp3Card as Locator);
expect(probe(mp3.filePath).streams).toEqual(
expect.arrayContaining([
expect.objectContaining({
codec_name: 'mp3',
codec_type: 'audio',
}),
])
);
await page.getByLabel('Export preset').selectOption('audio-ogg-vorbis');
const [oggCard] = await runForResults(
page,
() =>
page.getByRole('button', { name: 'Convert', exact: true }).click(),
['tone-convert.ogg'],
{ verifyMedia: true }
);
expect(
probe((await downloadResult(page, oggCard as Locator)).filePath).streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({
codec_name: 'vorbis',
codec_type: 'audio',
}),
])
);
await page.getByLabel('Export preset').selectOption('audio-flac');
const [flacCard] = await runForResults(
page,
() =>
page.getByRole('button', { name: 'Convert', exact: true }).click(),
['tone-convert.flac'],
{ verifyMedia: true }
);
expect(
probe((await downloadResult(page, flacCard as Locator)).filePath)
.streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({
codec_name: 'flac',
codec_type: 'audio',
}),
])
);
await page.getByLabel('Export preset').selectOption('audio-wav-pcm');
const [wavCard] = await runForResults(
page,
() =>
page.getByRole('button', { name: 'Convert', exact: true }).click(),
['tone-convert.wav'],
{ verifyMedia: true }
);
expect(
probe((await downloadResult(page, wavCard as Locator)).filePath).streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({
codec_name: 'pcm_s16le',
codec_type: 'audio',
}),
])
);
await importMedia(page, ['pattern-av.mp4']);
await page
.locator('.media-card')
.filter({ hasText: 'pattern-av.mp4' })
.click();
await page.getByLabel('Export preset').selectOption('audio-mp3');
await page.getByRole('checkbox', { name: /#0 · h264/u }).uncheck();
const [extractedAudioCard] = await runForResults(
page,
() =>
page.getByRole('button', { name: 'Convert', exact: true }).click(),
['pattern-av-convert.mp3'],
{ verifyMedia: true }
);
const extractedAudio = probe(
(await downloadResult(page, extractedAudioCard as Locator)).filePath
);
expect(
extractedAudio.streams?.map((stream) => stream.codec_type)
).toEqual(['audio']);
expect(extractedAudio.streams?.[0]?.codec_name).toBe('mp3');
await page
.getByLabel('Export preset')
.selectOption('mp4-h264-compatibility');
await page.getByRole('checkbox', { name: /#0 · h264/u }).check();
await page.getByRole('checkbox', { name: /#1 · aac/u }).uncheck();
const [videoOnlyCard] = await runForResults(
page,
() =>
page.getByRole('button', { name: 'Convert', exact: true }).click(),
['pattern-av-convert.mp4'],
{ verifyMedia: true }
);
const videoOnly = probe(
(await downloadResult(page, videoOnlyCard as Locator)).filePath
);
expect(videoOnly.streams?.map((stream) => stream.codec_type)).toEqual([
'video',
]);
expect(videoOnly.streams?.[0]?.codec_name).toBe('h264');
await importMedia(page, ['pattern.webm']);
await page
.locator('.media-card')
.filter({ hasText: 'pattern.webm' })
.click();
await page.getByRole('button', { name: 'Fast remux' }).click();
await page.getByLabel('Target container').selectOption('matroska');
const [remuxCard] = await runForResults(
page,
() => page.getByRole('button', { name: 'Remux', exact: true }).click(),
['pattern-remux.mkv'],
{ verifyMedia: true }
);
const remux = await downloadResult(page, remuxCard as Locator);
expect(probe(remux.filePath).streams).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'vp8', codec_type: 'video' }),
expect.objectContaining({
codec_name: 'vorbis',
codec_type: 'audio',
}),
])
);
await page.getByRole('button', { name: 'Re-encode' }).click();
await page.getByLabel('Export preset').selectOption('webm-vp8-vorbis');
const [webmCard] = await runForResults(
page,
() =>
page.getByRole('button', { name: 'Convert', exact: true }).click(),
['pattern-convert.webm'],
{ verifyMedia: true }
);
const webm = await downloadResult(page, webmCard as Locator);
expect(probe(webm.filePath).streams).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'vp8', codec_type: 'video' }),
expect.objectContaining({
codec_name: 'vorbis',
codec_type: 'audio',
}),
])
);
await importMedia(page, ['metadata.mp4']);
await page
.locator('.media-card')
.filter({ hasText: 'metadata.mp4' })
.click();
await page.getByRole('button', { name: 'Fast remux' }).click();
await page.getByLabel('Target container').selectOption('mp4');
await page.getByRole('checkbox', { name: 'Remove metadata' }).check();
const [metadataRemovalCard] = await runForResults(
page,
() => page.getByRole('button', { name: 'Remux', exact: true }).click(),
['metadata-remux.mp4'],
{ verifyMedia: true }
);
const metadataRemovalProbe = probe(
(await downloadResult(page, metadataRemovalCard as Locator)).filePath
);
expect(metadataRemovalProbe.format?.tags?.title).toBeUndefined();
expect(metadataRemovalProbe.format?.tags?.artist).toBeUndefined();
expect(metadataRemovalProbe.format?.tags?.comment).toBeUndefined();
});
await test.step('fast and accurate trim plus fast and normalized concat', async () => {
await page.getByRole('button', { name: 'New', exact: true }).click();
await page.getByRole('tab', { name: 'Edit' }).click();
await importMedia(page, ['compatible-a.mp4', 'compatible-b.mp4']);
const selectedTimelineClip = page
.locator('.timeline-clip')
.filter({ hasText: 'compatible-a.mp4' });
await selectedTimelineClip.locator('.timeline-clip__main').click();
await selectedTimelineClip.getByLabel('In').fill('0.1');
await selectedTimelineClip.getByLabel('Out').fill('0.8');
const structural = page.locator('.structural-operations');
const [fastTrimCard] = await runForResults(
page,
() =>
structural.getByRole('button', { name: 'Export fast trim' }).click(),
['compatible-a-trim.mp4'],
{ verifyMedia: true }
);
expect(
probe((await downloadResult(page, fastTrimCard as Locator)).filePath)
.streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }),
])
);
await structural.getByLabel('Accurate re-encode').check();
await structural
.getByLabel('Accurate trim preset')
.selectOption('mp4-h264-compatibility');
const [accurateTrimCard] = await runForResults(
page,
() =>
structural.getByRole('button', { name: 'Export exact trim' }).click(),
['compatible-a-trim.mp4'],
{ verifyMedia: true }
);
expect(
probe(
(await downloadResult(page, accurateTrimCard as Locator)).filePath
).streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }),
expect.objectContaining({ codec_name: 'aac', codec_type: 'audio' }),
])
);
await expect(
structural.getByRole('heading', {
name: 'Fast compatibility passed',
})
).toBeVisible();
const [fastConcatCard] = await runForResults(
page,
() =>
structural
.getByRole('button', { name: 'Export fast concat' })
.click(),
['compatible-a-concat.mp4'],
{ verifyMedia: true }
);
expect(
probe((await downloadResult(page, fastConcatCard as Locator)).filePath)
.streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }),
expect.objectContaining({ codec_name: 'aac', codec_type: 'audio' }),
])
);
await structural.getByLabel('Normalized re-encode').check();
await structural
.getByLabel('Normalized concat preset')
.selectOption('mp4-h264-compatibility');
await structural.getByLabel('Insert silence').check();
await structural.getByLabel('Reject subtitle input').check();
const [normalizedConcatCard] = await runForResults(
page,
() =>
structural
.getByRole('button', { name: 'Export normalized concat' })
.click(),
['compatible-a-concat.mp4'],
{ verifyMedia: true }
);
expect(
probe(
(await downloadResult(page, normalizedConcatCard as Locator)).filePath
).streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }),
expect.objectContaining({ codec_name: 'aac', codec_type: 'audio' }),
])
);
});
const advanced = page.locator('.advanced-operations');
await test.step('fast/accurate split and waveform/loudness analysis', async () => {
await advanced.getByLabel('Marker time (seconds)').fill('0.5');
await advanced.getByRole('button', { name: 'Add marker' }).click();
await runForResults(
page,
() =>
advanced.getByRole('button', { name: 'Queue 2 segments' }).click(),
['compatible-a-split-001.mp4', 'compatible-a-split-002.mp4'],
{ verifyMedia: true }
);
await advanced.getByLabel('Accurate re-encode').check();
await advanced
.getByLabel('Accurate split preset')
.selectOption('mp4-h264-compatibility');
await runForResults(
page,
() =>
advanced.getByRole('button', { name: 'Queue 2 segments' }).click(),
['compatible-a-split-001.mp4', 'compatible-a-split-002.mp4'],
{ verifyMedia: true }
);
await advanced
.getByRole('tab', { name: 'Waveform and loudness' })
.click();
const [waveformCard] = await runForResults(
page,
() =>
advanced
.getByRole('button', { name: 'Generate static waveform' })
.click(),
['compatible-a-waveform-static.png']
);
expect(
statSync((await downloadResult(page, waveformCard as Locator)).filePath)
.size
).toBeGreaterThan(1_000);
await advanced
.getByRole('button', { name: 'Analyze peak waveform' })
.click();
await expect(
page.getByRole('region', {
name: 'Waveform editor for compatible-a.mp4',
})
).toBeVisible({ timeout: 120_000 });
await advanced.getByRole('button', { name: 'Measure EBU R128' }).click();
await expect(page.locator('.project-notice')).toContainText('Measured', {
timeout: 120_000,
});
await expect(advanced.getByText('Measured loudness')).toBeVisible();
await advanced
.getByLabel('Normalization preset')
.selectOption('audio-mp3');
const [normalizedAudioCard] = await runForResults(
page,
() =>
advanced
.getByRole('button', { name: 'Apply measured normalization' })
.click(),
['compatible-a-normalize.mp3'],
{ verifyMedia: true }
);
expect(
probe(
(await downloadResult(page, normalizedAudioCard as Locator)).filePath
).streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'mp3', codec_type: 'audio' }),
])
);
const [peakAudioCard] = await runForResults(
page,
() =>
advanced
.getByRole('button', { name: 'Apply peak normalization' })
.click(),
['compatible-a-peak-normalize.mp3'],
{ verifyMedia: true }
);
expect(
probe((await downloadResult(page, peakAudioCard as Locator)).filePath)
.streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'mp3', codec_type: 'audio' }),
])
);
});
await test.step('thumbnail series and contact sheet', async () => {
await advanced
.getByRole('tab', { name: 'Thumbnails and contact sheets' })
.click();
await advanced.getByLabel('Number of frames').fill('3');
await runForResults(
page,
() =>
advanced
.getByRole('button', { name: 'Generate thumbnail series' })
.click(),
[
'compatible-a-thumbnail-001-250ms.jpg',
'compatible-a-thumbnail-002-500ms.jpg',
'compatible-a-thumbnail-003-750ms.jpg',
]
);
await advanced
.getByRole('spinbutton', { name: 'Frames', exact: true })
.fill('4');
await advanced
.getByRole('spinbutton', { name: 'Rows', exact: true })
.fill('2');
await advanced
.getByRole('spinbutton', { name: 'Columns', exact: true })
.fill('2');
await advanced
.getByRole('spinbutton', { name: /^Cell width/u })
.fill('128');
const [sheetCard] = await runForResults(
page,
() =>
advanced
.getByRole('button', { name: 'Generate contact sheet' })
.click(),
['compatible-a-contact-sheet.jpg']
);
expect(
statSync((await downloadResult(page, sheetCard as Locator)).filePath)
.size
).toBeGreaterThan(500);
});
await test.step('single frame and a transformed timeline with audio/video fades', async () => {
const inspector = page.locator('.inspector');
await inspector.getByRole('tab', { name: 'Frames' }).click();
const inspectorBody = inspector.locator('.inspector__body');
await inspectorBody
.getByRole('spinbutton', { name: 'Time (seconds)', exact: true })
.fill('0.2');
await inspectorBody
.getByRole('spinbutton', { name: 'Width', exact: true })
.fill('96');
await inspectorBody
.getByRole('combobox', { name: 'Format', exact: true })
.selectOption('png');
const [singleFrameCard] = await runForResults(
page,
() =>
inspectorBody.getByRole('button', { name: 'Extract frame' }).click(),
['compatible-a-thumbnail-200ms.png']
);
expect(
statSync(
(await downloadResult(page, singleFrameCard as Locator)).filePath
).size
).toBeGreaterThan(100);
await inspector.getByRole('tab', { name: 'Transform' }).click();
const cropFields = inspector.locator('.crop-editor__numeric');
await cropFields
.getByRole('spinbutton', { name: 'X', exact: true })
.fill('4');
await cropFields
.getByRole('spinbutton', { name: 'Y', exact: true })
.fill('2');
await cropFields
.getByRole('spinbutton', { name: 'Width', exact: true })
.fill('120');
await cropFields
.getByRole('spinbutton', { name: 'Height', exact: true })
.fill('68');
const resizeFields = inspector.locator('.inspector-section > .mini-grid');
await resizeFields
.getByRole('spinbutton', { name: 'Width', exact: true })
.fill('96');
await resizeFields
.getByRole('spinbutton', { name: 'Height', exact: true })
.fill('54');
await resizeFields
.getByRole('combobox', { name: 'Rotate', exact: true })
.selectOption('90');
await resizeFields
.getByRole('combobox', { name: 'Scale algorithm', exact: true })
.selectOption('bicubic');
await resizeFields
.getByRole('textbox', { name: 'Padding colour', exact: true })
.fill('#112233');
await page
.getByRole('spinbutton', { name: 'Output frame rate', exact: true })
.fill('12');
const videoFades = inspector
.locator('fieldset.subpanel')
.filter({ hasText: 'Video fades' });
await videoFades
.getByRole('spinbutton', { name: 'Fade in from black (s)' })
.fill('0.1');
await videoFades
.getByRole('spinbutton', { name: 'Fade out to black (s)' })
.fill('0.1');
await inspector.getByRole('tab', { name: 'Audio' }).click();
await expect(
inspectorBody.getByRole('checkbox', { name: 'Include audio' })
).toBeChecked();
await inspectorBody
.getByRole('spinbutton', { name: 'Fade in (s)', exact: true })
.fill('0.1');
await inspectorBody
.getByRole('spinbutton', { name: 'Fade out (s)', exact: true })
.fill('0.1');
const [timelineCard] = await runForResults(
page,
() => page.getByRole('button', { name: 'Render timeline' }).click(),
['output.mp4'],
{ verifyMedia: true }
);
const timelineProbe = probe(
(await downloadResult(page, timelineCard as Locator)).filePath
);
const timelineVideo = timelineProbe.streams?.find(
(stream) => stream.codec_type === 'video'
);
expect(timelineVideo).toMatchObject({
codec_name: 'h264',
width: 96,
height: 54,
avg_frame_rate: '12/1',
});
expect(
timelineProbe.streams?.some(
(stream) =>
stream.codec_type === 'audio' && stream.codec_name === 'aac'
)
).toBe(true);
});
await test.step('metadata and chapters survive round-trip probing', async () => {
await advanced.getByRole('tab', { name: 'Metadata policies' }).click();
const metadataPanel = advanced.getByRole('tabpanel');
await metadataPanel
.getByRole('textbox', { name: 'Title', exact: true })
.fill('Browser Matrix');
await metadataPanel
.getByRole('textbox', { name: 'Artist', exact: true })
.fill('av-tools Playwright');
const [metadataCard] = await runForResults(
page,
() =>
advanced
.getByRole('button', { name: 'Apply metadata policies' })
.click(),
['compatible-a-metadata.mp4'],
{ verifyMedia: true }
);
const metadataProbe = probe(
(await downloadResult(page, metadataCard as Locator)).filePath
);
expect(metadataProbe.format?.tags).toMatchObject({
title: 'Browser Matrix',
artist: 'av-tools Playwright',
});
await advanced.getByRole('tab', { name: 'Chapter execution' }).click();
await advanced
.getByLabel('Create output chapters from')
.selectOption('fixed');
await advanced.getByLabel('Chapter interval').fill('0.5');
const [chapterCard] = await runForResults(
page,
() =>
advanced
.getByRole('button', { name: 'Replace output chapters' })
.click(),
['compatible-a-chapters.mp4'],
{ verifyMedia: true }
);
const chapterProbe = probe(
(await downloadResult(page, chapterCard as Locator)).filePath
);
expect(chapterProbe.chapters).toHaveLength(2);
expect(
chapterProbe.chapters?.map((chapter) => chapter.tags?.title)
).toEqual(['Chapter 1', 'Chapter 2']);
});
await test.step('subtitle convert, mux, burn-in and extraction', async () => {
await advanced.getByRole('tab', { name: 'Subtitle operations' }).click();
const subtitlePanel = advanced.getByRole('tabpanel');
const subtitleOperation = subtitlePanel
.locator('label.advanced-field')
.filter({ hasText: /^Operation/u })
.locator('select');
const outputTextFormat = subtitlePanel
.locator('label.advanced-field')
.filter({ hasText: /^Output text format/u })
.locator('select');
await subtitleOperation.selectOption('convert');
const localSubtitleInput = subtitlePanel.locator(
'label.advanced-file-field input[type="file"]'
);
await markExistingResults(page);
await localSubtitleInput.setInputFiles({
name: 'invalid-subtitle.srt',
mimeType: 'application/x-subrip',
buffer: Buffer.from('this is not a timed subtitle\n'),
});
await subtitlePanel
.getByRole('button', { name: 'Convert subtitle' })
.click();
await expect(
advanced.locator('.advanced-operations__status')
).toContainText(/FFmpeg (?:exited|produced)/u, {
timeout: 120_000,
});
await expect(newResultCards(page)).toHaveCount(0);
await expect(
page
.locator('.engine-pill')
.filter({ hasText: 'Single-threaded compatibility mode' })
).toBeVisible({ timeout: 120_000 });
await localSubtitleInput.setInputFiles(fixture('subtitle.srt'));
await outputTextFormat.selectOption('vtt');
await subtitlePanel.getByLabel('Time offset').fill('0.1');
const [convertedCard] = await runForResults(
page,
() =>
subtitlePanel
.getByRole('button', { name: 'Convert subtitle' })
.click(),
['subtitle-subtitle.vtt']
);
const converted = await downloadResult(page, convertedCard as Locator);
expect(readFileSync(converted.filePath, 'utf8')).toContain('WEBVTT');
await subtitleOperation.selectOption('soft-mux');
await subtitlePanel
.locator('label.advanced-field')
.filter({ hasText: /^Target container/u })
.locator('select')
.selectOption('matroska');
await subtitlePanel
.getByRole('textbox', { name: 'Language', exact: true })
.fill('eng');
await subtitlePanel
.getByRole('textbox', { name: 'Track title', exact: true })
.fill('Browser subtitle');
await subtitlePanel
.getByRole('checkbox', { name: 'Default track' })
.check();
const [muxedCard] = await runForResults(
page,
() =>
subtitlePanel
.getByRole('button', { name: 'Mux soft subtitle' })
.click(),
['compatible-a-subtitled.mkv'],
{ verifyMedia: true }
);
const muxed = await downloadResult(page, muxedCard as Locator);
expect(probe(muxed.filePath).streams).toEqual(
expect.arrayContaining([
expect.objectContaining({
codec_name: 'subrip',
codec_type: 'subtitle',
tags: expect.objectContaining({
language: 'eng',
title: 'Browser subtitle',
}),
disposition: expect.objectContaining({ default: 1 }),
}),
])
);
await subtitleOperation.selectOption('burn-in');
await subtitlePanel
.locator('label.advanced-field')
.filter({ hasText: /^Video export preset/u })
.locator('select')
.selectOption('mp4-h264-compatibility');
const [burnedCard] = await runForResults(
page,
() =>
subtitlePanel
.getByRole('button', { name: 'Burn subtitle into video' })
.click(),
['compatible-a-subtitle-burn.mp4'],
{ verifyMedia: true }
);
expect(
probe((await downloadResult(page, burnedCard as Locator)).filePath)
.streams
).toEqual(
expect.arrayContaining([
expect.objectContaining({ codec_name: 'h264', codec_type: 'video' }),
])
);
await page.getByRole('button', { name: 'New', exact: true }).click();
await mediaInput(page).setInputFiles({
name: 'subtitled-source.mkv',
mimeType: 'video/x-matroska',
buffer: readFileSync(muxed.filePath),
});
await expect(
page
.locator('.media-card')
.filter({ hasText: 'subtitled-source.mkv' })
.locator('.phase--ready')
).toBeVisible({ timeout: 120_000 });
await subtitleOperation.selectOption('extract');
await outputTextFormat.selectOption('srt');
const [extractedCard] = await runForResults(
page,
() =>
subtitlePanel
.getByRole('button', { name: 'Extract subtitle' })
.click(),
['subtitled-source-subtitle-eng.srt']
);
const extracted = await downloadResult(page, extractedCard as Locator);
expect(readFileSync(extracted.filePath, 'utf8')).toContain(
'Generated subtitle'
);
});
expect(
runtime.requests.some((url) =>
url.endsWith('/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm')
)
).toBe(true);
assertLocalOnly(runtime);
});
});

View File

@@ -0,0 +1,242 @@
/// <reference types="node" />
import { expect, test, type Browser, type Page } from '@playwright/test';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { PRIMARY_FIXTURE } from './browser-helpers';
interface BenchmarkResult {
readonly mode: 'single-thread' | 'multithread';
readonly source: 'small' | 'medium';
readonly sourceDescription: string;
readonly cacheCondition: string;
readonly initMs: number;
readonly probeMs: number;
readonly conversionMs: number;
readonly outputReadMs: number;
readonly cleanupMs: number;
readonly exportEndToEndMs: number;
readonly outputBytes: number;
readonly hardwareConcurrency: number;
readonly crossOriginIsolated: boolean;
readonly inputBytes: number;
readonly usedJsHeapBytes?: number;
}
test.describe('opt-in pinned-core performance record', () => {
test.skip(
process.env.AV_BENCHMARK !== '1',
'Run with AV_BENCHMARK=1 through npm run benchmark:browser.'
);
test('records comparable fresh-context ST and MT end-to-end timings', async ({
browser,
}, testInfo) => {
test.setTimeout(600_000);
const medium = createMediumFixture();
const results: BenchmarkResult[] = [];
try {
results.push(
await runMode(browser, 'single-thread', PRIMARY_FIXTURE, 'small')
);
results.push(
await runMode(browser, 'multithread', PRIMARY_FIXTURE, 'small')
);
results.push(
await runMode(browser, 'single-thread', medium.filePath, 'medium')
);
results.push(
await runMode(browser, 'multithread', medium.filePath, 'medium')
);
await testInfo.attach('av-tools-browser-benchmark.json', {
body: Buffer.from(`${JSON.stringify(results, null, 2)}\n`),
contentType: 'application/json',
});
process.stdout.write(`AV_BENCHMARK_RESULT ${JSON.stringify(results)}\n`);
} finally {
medium.dispose();
}
});
});
async function runMode(
browser: Browser,
mode: BenchmarkResult['mode'],
fixturePath: string,
source: BenchmarkResult['source']
): Promise<BenchmarkResult> {
const context = await browser.newContext();
const page = await context.newPage();
try {
page.on('dialog', (dialog) => void dialog.accept());
await page.addInitScript(() => {
Reflect.deleteProperty(globalThis, 'showSaveFilePicker');
});
await page.goto('http://127.0.0.1:4173/');
const environment = await page.evaluate(() => ({
hardwareConcurrency: navigator.hardwareConcurrency,
crossOriginIsolated: globalThis.crossOriginIsolated,
}));
if (mode === 'multithread') {
expect(environment.crossOriginIsolated).toBe(true);
}
await page.getByText('Engine & browser').click();
await page
.getByLabel('Engine mode')
.selectOption(
mode === 'single-thread' ? 'force-single-thread' : 'prefer-multithread'
);
const initStarted = await monotonicNow(page);
await page.getByRole('button', { name: 'Initialize engine' }).click();
await expect(
page.locator('.engine-pill').filter({
hasText:
mode === 'single-thread'
? 'Single-threaded compatibility mode'
: 'Multithreaded',
})
).toBeVisible({ timeout: 120_000 });
const initMs = (await monotonicNow(page)) - initStarted;
const probeStarted = await monotonicNow(page);
await mediaInput(page).setInputFiles(fixturePath);
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
{ timeout: 120_000 }
);
const probeMs = (await monotonicNow(page)) - probeStarted;
const exportStarted = await monotonicNow(page);
await page.getByRole('button', { name: 'Convert', exact: true }).click();
const result = page.locator('.result-card').last();
await expect(result).toContainText(
/created · verified|completed locally/u,
{
timeout: 180_000,
}
);
const exportEndToEndMs = (await monotonicNow(page)) - exportStarted;
const reportDownload = page.waitForEvent('download');
await page
.getByRole('button', { name: 'Export validation report' })
.click();
const reportPath = await (await reportDownload).path();
expect(reportPath).not.toBeNull();
const report = JSON.parse(
readFileSync(reportPath as string, 'utf8')
) as PerformanceReport;
const outputBytes = await page
.locator('.quick-preview video')
.evaluate(async (element) => {
const response = await fetch((element as HTMLVideoElement).src);
return (await response.arrayBuffer()).byteLength;
});
const usedJsHeapBytes = await page.evaluate(() => {
const memory = (
performance as Performance & {
memory?: { usedJSHeapSize?: number };
}
).memory;
return memory?.usedJSHeapSize;
});
const benchmarkSample: BenchmarkResult = {
mode,
source,
sourceDescription:
source === 'small'
? '2 s, 160×90 H.264/AAC synthetic pattern'
: '6 s, 640×360 H.264/AAC synthetic loop',
cacheCondition:
'fresh browser context; browser HTTP cache may be warm after the first run',
initMs: rounded(initMs),
probeMs: rounded(probeMs),
conversionMs: rounded(report.execution.conversionMilliseconds),
outputReadMs: rounded(report.execution.outputReadMilliseconds),
cleanupMs: rounded(report.execution.cleanupMilliseconds),
exportEndToEndMs: rounded(exportEndToEndMs),
outputBytes,
hardwareConcurrency: environment.hardwareConcurrency,
crossOriginIsolated: environment.crossOriginIsolated,
inputBytes: statSync(fixturePath).size,
...(usedJsHeapBytes === undefined ? {} : { usedJsHeapBytes }),
};
process.stdout.write(
`AV_BENCHMARK_SAMPLE ${JSON.stringify(benchmarkSample)}\n`
);
return benchmarkSample;
} finally {
await context.close();
}
}
interface PerformanceReport {
readonly execution: {
readonly conversionMilliseconds: number;
readonly outputReadMilliseconds: number;
readonly cleanupMilliseconds: number;
};
}
function createMediumFixture(): {
readonly filePath: string;
readonly dispose: () => void;
} {
const directory = mkdtempSync(join(tmpdir(), 'av-tools-benchmark-'));
const filePath = join(directory, 'medium-pattern.mp4');
try {
execFileSync(
'ffmpeg',
[
'-hide_banner',
'-loglevel',
'error',
'-y',
'-stream_loop',
'2',
'-i',
PRIMARY_FIXTURE,
'-t',
'6',
'-vf',
'scale=640:360',
'-r',
'24',
'-c:v',
'libx264',
'-preset',
'ultrafast',
'-crf',
'30',
'-c:a',
'aac',
'-b:a',
'64k',
filePath,
],
{ stdio: 'pipe' }
);
} catch (error) {
rmSync(directory, { recursive: true, force: true });
throw error;
}
return {
filePath,
dispose: () => rmSync(directory, { recursive: true, force: true }),
};
}
function mediaInput(page: Page) {
return page.locator('input[type="file"][accept*="audio"]').first();
}
async function monotonicNow(page: Page): Promise<number> {
return page.evaluate(() => performance.now());
}
function rounded(value: number): number {
return Math.round(value * 10) / 10;
}

View File

@@ -0,0 +1,185 @@
/// <reference types="node" />
import { readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { expect, test } from '@playwright/test';
import { GENERATED_FIXTURES, PRIMARY_FIXTURE } from './browser-helpers';
test('covers drop, editor keyboard/drag controls, cache, and project reattachment', async ({
page,
}) => {
test.setTimeout(240_000);
await page.addInitScript(() => {
Object.defineProperty(HTMLMediaElement.prototype, 'canPlayType', {
configurable: true,
value: () => '',
});
Reflect.deleteProperty(globalThis, 'showSaveFilePicker');
});
await page.goto('/');
await page.getByText('Engine & browser').click();
await page.getByLabel('Engine mode').selectOption('force-single-thread');
await dropFixture(page, PRIMARY_FIXTURE, 'video/mp4');
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
'Ready',
{ timeout: 120_000 }
);
await expect(page.locator('.quick-preview video')).toHaveAttribute(
'src',
/^blob:/u
);
await page.getByRole('button', { name: 'Create proxy' }).click();
await expect(
page.locator('.result-card').filter({ hasText: 'pattern-av-preview.mp4' })
).toContainText(/created · verified|completed locally/u, {
timeout: 120_000,
});
await page.getByText('Derived cache').click();
const clearCache = page.getByRole('button', { name: 'Clear cache' });
await expect(clearCache).toBeEnabled({ timeout: 10_000 });
await clearCache.click();
await expect(page.getByRole('status')).toContainText(
'Derived cache cleared.'
);
await page.getByRole('tab', { name: 'Edit' }).click();
const compatibleB = join(GENERATED_FIXTURES, 'compatible-b.mp4');
await mediaInput(page).setInputFiles(compatibleB);
await expect(page.locator('.media-card .phase--ready')).toHaveCount(2, {
timeout: 120_000,
});
const clips = page.locator('.timeline-clip');
await expect(clips).toHaveCount(2);
const secondName = await clips.nth(1).locator('strong').textContent();
await clips.nth(1).dragTo(clips.nth(0));
await expect(clips.nth(0).locator('strong')).toHaveText(
secondName ?? 'compatible-b.mp4'
);
await clips
.filter({ hasText: 'compatible-b.mp4' })
.locator('.timeline-clip__main')
.click();
const inspector = page.locator('.inspector');
await inspector.getByRole('tab', { name: 'Transform' }).click();
const crop = inspector.locator('.crop-editor__numeric');
await crop.getByRole('spinbutton', { name: 'Width' }).fill('100');
await crop.getByRole('spinbutton', { name: 'Height' }).fill('60');
const cropMove = inspector.getByRole('button', {
name: 'Move crop rectangle',
});
await cropMove.focus();
await page.keyboard.press('Shift+ArrowRight');
await expect(crop.getByRole('spinbutton', { name: 'X' })).toHaveValue('10');
const resizeRight = inspector.getByRole('button', {
name: 'Resize crop from right',
});
const handleBox = await resizeRight.boundingBox();
expect(handleBox).not.toBeNull();
const widthBefore = Number(
await crop.getByRole('spinbutton', { name: 'Width' }).inputValue()
);
const handleCenterX = (handleBox?.x ?? 0) + (handleBox?.width ?? 0) / 2;
const handleCenterY = (handleBox?.y ?? 0) + (handleBox?.height ?? 0) / 2;
await page.mouse.move(handleCenterX, handleCenterY);
await page.mouse.down();
await page.mouse.move(handleCenterX + 16, handleCenterY);
await page.mouse.up();
await expect
.poll(async () =>
Number(await crop.getByRole('spinbutton', { name: 'Width' }).inputValue())
)
.not.toBe(widthBefore);
await crop.getByRole('spinbutton', { name: 'X' }).fill('999');
await expect(inspector.locator('.crop-editor__errors')).toHaveAttribute(
'role',
'alert'
);
await inspector.getByRole('button', { name: 'Reset crop' }).click();
const advanced = page.locator('.advanced-operations');
await advanced.getByRole('tab', { name: 'Waveform and loudness' }).click();
await advanced.getByRole('button', { name: 'Analyze peak waveform' }).click();
const inHandle = page.getByRole('slider', { name: 'In trim handle' });
await expect(inHandle).toBeVisible({ timeout: 120_000 });
const inBefore = Number(await inHandle.getAttribute('aria-valuenow'));
await inHandle.focus();
await page.keyboard.press('ArrowRight');
await expect
.poll(async () => Number(await inHandle.getAttribute('aria-valuenow')))
.toBeGreaterThan(inBefore);
const projectDownload = page.waitForEvent('download');
await page.getByRole('button', { name: 'Save project' }).click();
const projectPath = await (await projectDownload).path();
expect(projectPath).not.toBeNull();
await page.getByRole('button', { name: 'New', exact: true }).click();
await page
.locator('.project-actions input[type="file"]')
.setInputFiles(projectPath as string);
await expect(page.locator('.timeline-clip')).toHaveCount(2);
await expect(page.locator('.media-card')).toHaveCount(0);
page.on('dialog', (dialog) => void dialog.accept());
await mediaInput(page).setInputFiles([PRIMARY_FIXTURE, compatibleB]);
await expect(page.locator('.media-card .phase--ready')).toHaveCount(2, {
timeout: 120_000,
});
await expect(page.locator('.timeline-clip')).toHaveCount(2);
});
function mediaInput(page: import('@playwright/test').Page) {
return page.locator('input[type="file"][accept*="audio"]').first();
}
async function dropFixture(
page: import('@playwright/test').Page,
filePath: string,
mimeType: string
): Promise<void> {
const bytes = readFileSync(filePath).toString('base64');
const stats = statSync(filePath);
await page
.locator('.drop-zone')
.first()
.evaluate(
(element, fixture) => {
const binary = atob(fixture.base64);
const data = Uint8Array.from(binary, (character) =>
character.charCodeAt(0)
);
const transfer = new DataTransfer();
transfer.items.add(
new File([data], fixture.name, {
type: fixture.mimeType,
lastModified: fixture.lastModified,
})
);
element.dispatchEvent(
new DragEvent('dragenter', {
bubbles: true,
cancelable: true,
dataTransfer: transfer,
})
);
element.dispatchEvent(
new DragEvent('drop', {
bubbles: true,
cancelable: true,
dataTransfer: transfer,
})
);
},
{
base64: bytes,
name: filePath.split('/').at(-1) ?? 'fixture.mp4',
mimeType,
lastModified: Math.trunc(stats.mtimeMs),
}
);
}