/// 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 { 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), } ); }