198 lines
6.9 KiB
TypeScript
198 lines
6.9 KiB
TypeScript
/// <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 page
|
|
.getByRole('button', { name: 'Inspect details for pattern-av.mp4' })
|
|
.click();
|
|
await expect(page.locator('.source-summary .phase--ready')).toHaveText(
|
|
'Details 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 page
|
|
.getByRole('button', { name: 'Inspect details for compatible-b.mp4' })
|
|
.click();
|
|
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 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,
|
|
});
|
|
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),
|
|
}
|
|
);
|
|
}
|