feat: Add support for editing video

Add an Edit
This commit is contained in:
Claude Worker
2026-07-18 16:43:15 +00:00
parent 94dba16ff6
commit 8c0776f97d
7 changed files with 834 additions and 21 deletions

View File

@@ -0,0 +1,72 @@
/**
* Smoke test for the "Edit & download" custom-video feature (Task #68).
*
* The editor lets a user cut parts out of a video and save the result as a
* custom offline video that can be added to playlists like any other. This
* spec runs against the static frontend (no backend), so it exercises the
* pieces that don't need the yt-dlp/ffmpeg server round-trip:
* • video-edit.js loads and exposes window.VideoEdit with correct maths.
* • The now-playing "Edit & download" button (#editBtn) is present.
* • The editor modal opens, accepts a cut, and reports the right final
* length via the shared keep-segment maths.
*/
const { test, expect } = require('@playwright/test');
test.describe('Video editor — Edit & download', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForSelector('.app', { state: 'attached' });
});
test('exposes window.VideoEdit with correct cut/keep maths', async ({ page }) => {
const ok = await page.evaluate(() => typeof window.VideoEdit === 'object' && window.VideoEdit !== null);
expect(ok).toBe(true);
const keep = await page.evaluate(() =>
window.VideoEdit.invertCuts([{ start: 10, end: 20 }], 60));
expect(keep).toEqual([{ start: 0, end: 10 }, { start: 20, end: 60 }]);
const param = await page.evaluate(() =>
window.VideoEdit.keepToParam(window.VideoEdit.invertCuts([{ start: 10, end: 20 }], 60)));
expect(param).toBe('0-10,20-60');
const finalLen = await page.evaluate(() =>
window.VideoEdit.keepDuration(window.VideoEdit.invertCuts([{ start: 10, end: 20 }], 60)));
expect(finalLen).toBe(50);
});
test('renders the now-playing Edit & download button', async ({ page }) => {
const btn = page.locator('#editBtn');
await expect(btn).toHaveCount(1);
await expect(btn).toContainText('Edit');
});
test('editor modal opens for a source video and computes the final length', async ({ page }) => {
// Drive openVideoEditor directly with a fake source that has a known
// duration, so the test doesn't depend on network playback. OPFS may be
// absent in the test browser; if so the editor toasts and returns — assert
// whichever path this browser takes so the test is deterministic.
const supported = await page.evaluate(() =>
!!(window.OPFS && window.OPFS.isSupported && window.OPFS.isSupported()));
const opened = await page.evaluate(() => {
window.openVideoEditor({ id: 'testsource1', title: 'Sample', duration: 100, thumbnail: '' });
const modal = document.getElementById('modal');
return modal && !modal.classList.contains('hidden');
});
if (!supported) {
// Without OPFS the editor declines to open — that's the correct guard.
expect(opened).toBe(false);
return;
}
expect(opened).toBe(true);
// Add a cut 0:100:40 → final length should be 100 - 30 = 70s = "1:10".
await page.fill('.video-editor .ve-from', '0:10');
await page.fill('.video-editor .ve-to', '0:40');
await page.click('.video-editor .ve-add');
await expect(page.locator('.video-editor .ve-cut')).toHaveCount(1);
await expect(page.locator('.video-editor .ve-summary')).toContainText('1:10');
});
});