| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- /*
- Functional test: generate real webm/wav media in the browser, import
- them into Cine Studio, then exercise playback, editing (split/undo/
- drag), project serialization round-trip and the WebM export pipeline.
- */
- "use strict";
- const { ok, fail, run } = require("../lib/harness");
- run("FUNCTIONAL", async (page, browser) => {
- /* ---- 1. generate + import real media ---- */
- await page.evaluate(async () => {
- function makeVideo(name, seconds, hue) {
- return new Promise((resolve) => {
- const cv = document.createElement("canvas");
- cv.width = 320; cv.height = 180;
- const ctx = cv.getContext("2d");
- const stream = cv.captureStream(15);
- const rec = new MediaRecorder(stream, { mimeType: "video/webm" });
- const chunks = [];
- rec.ondataavailable = e => e.data.size && chunks.push(e.data);
- rec.onstop = () => resolve(new Blob(chunks, { type: "video/webm" }));
- const t0 = performance.now();
- (function draw() {
- const t = (performance.now() - t0) / 1000;
- ctx.fillStyle = `hsl(${hue}, 60%, 40%)`;
- ctx.fillRect(0, 0, 320, 180);
- ctx.fillStyle = "#fff";
- ctx.fillRect((t * 60) % 320, 60, 40, 60);
- if (t < seconds) { requestAnimationFrame(draw); } else { rec.stop(); }
- })();
- rec.start(200);
- });
- }
- function makeWav(seconds) {
- const sr = 22050, n = sr * seconds;
- const buf = new ArrayBuffer(44 + n * 2);
- const dv = new DataView(buf);
- function ws(o, s) { for (let i = 0; i < s.length; i++) dv.setUint8(o + i, s.charCodeAt(i)); }
- ws(0, "RIFF"); dv.setUint32(4, 36 + n * 2, true); ws(8, "WAVEfmt ");
- dv.setUint32(16, 16, true); dv.setUint16(20, 1, true); dv.setUint16(22, 1, true);
- dv.setUint32(24, sr, true); dv.setUint32(28, sr * 2, true);
- dv.setUint16(32, 2, true); dv.setUint16(34, 16, true);
- ws(36, "data"); dv.setUint32(40, n * 2, true);
- for (let i = 0; i < n; i++) {
- dv.setInt16(44 + i * 2, Math.sin(2 * Math.PI * 330 * i / sr) * 12000, true);
- }
- return new Blob([buf], { type: "audio/wav" });
- }
- const v1 = await makeVideo("ClipA.webm", 3, 200);
- const v2 = await makeVideo("ClipB.webm", 2.5, 30);
- window.__testMedia = {
- v1: CS.media.register({ name: "ClipA.webm", blobUrl: URL.createObjectURL(v1), type: "video" }),
- v2: CS.media.register({ name: "ClipB.webm", blobUrl: URL.createObjectURL(v2), type: "video" }),
- a1: CS.media.register({ name: "Tone.wav", blobUrl: URL.createObjectURL(makeWav(4)), type: "audio" })
- };
- });
- await page.waitForFunction(() => {
- const m = window.__testMedia;
- return m.v1.probed && m.v2.probed && m.a1.probed;
- }, { timeout: 30000 });
- const probe = await page.evaluate(() => {
- const m = window.__testMedia;
- return {
- d1: m.v1.duration, d2: m.v2.duration, da: m.a1.duration,
- thumbs: m.v1.thumbs.length, peaks: (m.a1.peaks || []).length,
- offline: m.v1.offline || m.v2.offline || m.a1.offline,
- binItems: document.querySelectorAll("#bin-grid .bin-item").length
- };
- });
- if (probe.offline) fail("media marked offline");
- if (!(probe.d1 > 2 && probe.d1 < 4.5)) fail("ClipA duration wrong: " + probe.d1);
- if (!(probe.da > 3.5 && probe.da < 4.5)) fail("wav duration wrong: " + probe.da);
- if (probe.thumbs < 1) fail("no video thumbnails");
- if (probe.peaks < 100) fail("no audio peaks");
- if (probe.binItems !== 3) fail("bin should show 3 items");
- ok(`probe: video ${probe.d1.toFixed(2)}s + ${probe.d2.toFixed(2)}s, wav ${probe.da.toFixed(2)}s, ${probe.thumbs} thumbs, ${probe.peaks} peaks`);
- /* ---- 2. build a timeline ---- */
- await page.evaluate(() => {
- const m = window.__testMedia;
- const c1 = CS.addClipToTimeline(m.v1, "V1", 0);
- CS.addClipToTimeline(m.v2, "V1", CS.clipDuration(c1));
- CS.addClipToTimeline(m.a1, "A1", 0);
- CS.commit("Test Build");
- });
- const clipCount = await page.evaluate(() => CS.project.clips.length);
- if (clipCount !== 3) fail("expected 3 clips, got " + clipCount);
- ok("3 clips placed on timeline");
- /* ---- 3. playback ---- */
- await page.click("#btn-play");
- await page.waitForTimeout(1600);
- const playState = await page.evaluate(() => {
- const cv = document.getElementById("preview-canvas");
- const ctx = cv.getContext("2d");
- const px = ctx.getImageData(Math.floor(cv.width / 2), Math.floor(cv.height / 2), 1, 1).data;
- return { playhead: CS.state.playhead, playing: CS.state.playing, px: Array.from(px) };
- });
- if (!playState.playing) fail("player is not playing");
- if (playState.playhead < 1.0) fail("playhead did not advance: " + playState.playhead);
- if (playState.px[0] + playState.px[1] + playState.px[2] < 20) fail("preview canvas looks black during playback");
- ok(`playback: playhead=${playState.playhead.toFixed(2)}s, center pixel rgb(${playState.px.slice(0, 3)})`);
- await page.click("#btn-play"); // pause
- /* ---- 4. split / undo / redo ---- */
- const split = await page.evaluate(() => {
- CS.player.seek(1.2);
- CS.selectClip(CS.project.clips[0].id);
- const before = CS.project.clips.length;
- CS.splitAtPlayhead();
- const after = CS.project.clips.length;
- CS.undo();
- const undone = CS.project.clips.length;
- CS.redo();
- const redone = CS.project.clips.length;
- return { before, after, undone, redone };
- });
- if (split.after !== split.before + 1) fail("split did not add a clip");
- if (split.undone !== split.before) fail("undo did not restore");
- if (split.redone !== split.before + 1) fail("redo did not reapply");
- ok(`split/undo/redo: ${split.before} -> ${split.after} -> ${split.undone} -> ${split.redone}`);
- /* ---- 5. drag a clip with the mouse ---- */
- const dragged = await page.evaluate(() => {
- window.__dragTarget = CS.project.clips.find(c => c.trackId === "A1");
- return { id: window.__dragTarget.id, start: window.__dragTarget.start };
- });
- const box = await page.locator(`.tl-clip[data-clip-id="${dragged.id}"]`).boundingBox();
- if (!box) fail("audio clip element not found");
- await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
- await page.mouse.down();
- await page.mouse.move(box.x + box.width / 2 + 120, box.y + box.height / 2, { steps: 8 });
- await page.mouse.up();
- const newStart = await page.evaluate(() => CS.getClip(window.__dragTarget.id).start);
- if (Math.abs(newStart - dragged.start) < 0.5) fail("mouse drag did not move the clip: " + newStart);
- ok(`mouse drag moved audio clip ${dragged.start.toFixed(2)}s -> ${newStart.toFixed(2)}s`);
- /* ---- 6. project serialization round trip ---- */
- const roundTrip = await page.evaluate(() => {
- const json = CS.fileio.serializeProject();
- const beforeClips = CS.project.clips.length;
- const beforeTracks = CS.project.tracks.length;
- CS.fileio.loadProject(JSON.parse(json), "", "roundtrip.cine");
- return {
- beforeClips, beforeTracks,
- afterClips: CS.project.clips.length,
- afterTracks: CS.project.tracks.length,
- name: CS.project.name,
- parses: !!json.length
- };
- });
- if (roundTrip.afterClips !== roundTrip.beforeClips) fail("clips lost in round trip");
- if (roundTrip.afterTracks !== roundTrip.beforeTracks) fail("tracks lost in round trip");
- ok(`project round trip: ${roundTrip.afterClips} clips, ${roundTrip.afterTracks} tracks preserved`);
- /* ---- 7. export to WebM (fresh short timeline) ---- */
- await page.evaluate(() => {
- const m = window.__testMedia;
- CS.newProject({ name: "ExportTest", width: 640, height: 360, fps: 30 });
- CS.player.applyProjectSize();
- // media pool was cleared by newProject: re-register the blobs
- const v = CS.media.register({ name: m.v1.name, blobUrl: m.v1.blobUrl, type: "video" });
- const a = CS.media.register({ name: m.a1.name, blobUrl: m.a1.blobUrl, type: "audio" });
- window.__exportSeed = { v, a };
- });
- await page.waitForFunction(() => window.__exportSeed.v.probed && window.__exportSeed.a.probed, { timeout: 20000 });
- await page.evaluate(() => {
- const s = window.__exportSeed;
- const c = CS.addClipToTimeline(s.v, "V1", 0);
- c.out = Math.min(c.out, 2.5);
- const ca = CS.addClipToTimeline(s.a, "A1", 0);
- ca.out = Math.min(ca.out, 2.5);
- CS.commit("Export Seed");
- window.__exportSize = -1;
- CS.fileio.downloadBlob = function (blob) { window.__exportSize = blob.size; };
- CS.exporter.start({ base: "test_export", format: "webm", destDir: "", toDevice: true });
- });
- await page.waitForFunction(() => window.__exportSize >= 0, { timeout: 30000 });
- const exportSize = await page.evaluate(() => window.__exportSize);
- if (exportSize < 5000) fail("export produced a suspiciously small file: " + exportSize);
- ok(`export produced WebM of ${exportSize} bytes`);
- });
|