feat: publish av-tools 0.1.0
This commit is contained in:
185
tests/browser/ui-workflows.spec.ts
Normal file
185
tests/browser/ui-workflows.spec.ts
Normal 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),
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user