| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- /*
- Feature test for the second iteration: effects, titles, transitions,
- elements, filters, list view - verified end to end with real
- in-browser generated media and pixel sampling.
- */
- "use strict";
- const { ok, fail, run } = require("../lib/harness");
- run("FEATURE", async (page, browser) => {
- // traffic lights must be gone
- const hasTL = await page.evaluate(() => !!document.getElementById("traffic-lights"));
- if (hasTL) fail("traffic lights still present");
- ok("macOS traffic lights removed");
- /* ---- media: one real video clip ---- */
- await page.evaluate(async () => {
- function makeVideo(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() {
- ctx.fillStyle = `hsl(${hue}, 65%, 45%)`;
- ctx.fillRect(0, 0, 320, 180);
- if ((performance.now() - t0) / 1000 < seconds) { requestAnimationFrame(draw); } else { rec.stop(); }
- })();
- rec.start(200);
- });
- }
- const blob = await makeVideo(3, 210);
- window.__m = CS.media.register({ name: "Base.webm", blobUrl: URL.createObjectURL(blob), type: "video" });
- });
- await page.waitForFunction(() => window.__m.probed, { timeout: 30000 });
- const samplePx = () => page.evaluate(() => {
- const cv = document.getElementById("preview-canvas");
- const d = cv.getContext("2d").getImageData(Math.floor(cv.width / 2), Math.floor(cv.height / 2), 1, 1).data;
- return [d[0], d[1], d[2]];
- });
- /* ---- effects ---- */
- await page.evaluate(() => {
- const c = CS.addClipToTimeline(window.__m, "V1", 0);
- window.__clip = c;
- CS.commit("seed");
- CS.selectClip(c.id);
- CS.player.seek(1.0);
- });
- await page.waitForTimeout(700);
- const base = await samplePx();
- if (base[0] + base[1] + base[2] < 20) fail("baseline frame is black");
- await page.evaluate(() => CS.effects.applyToClip(window.__clip, "bw"));
- await page.waitForTimeout(250);
- const bw = await samplePx();
- if (Math.abs(bw[0] - bw[1]) > 6 || Math.abs(bw[1] - bw[2]) > 6) fail("B&W effect not applied: " + bw);
- ok(`B&W effect desaturates (rgb ${bw})`);
- await page.evaluate(() => {
- CS.effects.removeFromClip(window.__clip, "bw");
- CS.effects.applyToClip(window.__clip, "invert");
- });
- await page.waitForTimeout(250);
- const inv = await samplePx();
- if (Math.abs(inv[0] - base[0]) < 30 && Math.abs(inv[2] - base[2]) < 30) fail("invert changed nothing: " + inv + " vs " + base);
- ok(`invert flips colors (rgb ${base} -> ${inv})`);
- await page.evaluate(() => {
- CS.effects.removeFromClip(window.__clip, "invert");
- CS.effects.applyToClip(window.__clip, "fadein"); // 1s default
- CS.player.seek(0.06);
- });
- await page.waitForTimeout(500);
- const faded = await samplePx();
- if (faded[0] + faded[1] + faded[2] > base[0] + base[1] + base[2] * 0.5) fail("fade-in start not dark: " + faded);
- await page.evaluate(() => CS.player.seek(2.0));
- await page.waitForTimeout(400);
- const unfaded = await samplePx();
- if (unfaded[0] + unfaded[1] + unfaded[2] < 20) fail("frame after fade window is dark");
- ok(`fade-in ramps alpha (start rgb ${faded}, later rgb ${unfaded})`);
- await page.evaluate(() => {
- CS.effects.applyToClip(window.__clip, "pixelate");
- CS.effects.applyToClip(window.__clip, "vignette");
- CS.effects.applyToClip(window.__clip, "grain");
- CS.player.seek(1.5);
- });
- await page.waitForTimeout(300);
- const fxCount = await page.evaluate(() => window.__clip.props.effects.length);
- if (fxCount !== 4) fail("expected 4 effects on clip, got " + fxCount);
- const fxBadge = await page.evaluate(() => !!document.querySelector(".tl-clip .clip-fx"));
- if (!fxBadge) fail("fx badge missing on timeline clip");
- ok("pixelate + vignette + grain stack renders, fx badge shown");
- /* ---- undo restores effect stack ---- */
- const undoCount = await page.evaluate(() => { CS.undo(); return window.__clip ? CS.getClip(window.__clip.id).props.effects.length : -1; });
- if (undoCount !== 3) fail("undo did not pop effect stack: " + undoCount);
- await page.evaluate(() => CS.redo());
- ok("undo/redo covers effects");
- /* ---- titles ---- */
- await page.evaluate(() => {
- CS.player.seek(1.5);
- CS.titles.insertPreset("title");
- const t = CS.selectedClip();
- t.props.text.content = "BIG";
- t.props.text.size = 400;
- CS.titles.invalidate(t);
- window.__title = t;
- CS.player.render();
- });
- await page.waitForTimeout(300);
- const titleInfo = await page.evaluate(() => ({
- kind: window.__title.kind,
- track: window.__title.trackId,
- tracks: CS.project.tracks.filter(t => t.kind === "video").length
- }));
- if (titleInfo.kind !== "title") fail("title clip not created");
- if (titleInfo.track === "V1") fail("title landed on the busy V1 track");
- const titlePx = await samplePx();
- if (titlePx[0] < 180 || titlePx[1] < 180 || titlePx[2] < 180) fail("title text not visible at center: " + titlePx);
- ok(`title clip renders on ${titleInfo.track} (center rgb ${titlePx})`);
- /* ---- elements (color board) ---- */
- await page.evaluate(() => {
- CS.deleteSelectedClip(); // remove title again
- CS.player.seek(10);
- CS.titles.insertElement("red");
- CS.player.render();
- });
- await page.waitForTimeout(200);
- const redPx = await samplePx();
- if (!(redPx[0] > 130 && redPx[1] < 90)) fail("color element not red: " + redPx);
- await page.evaluate(() => { CS.deleteSelectedClip(); });
- ok(`color element renders (rgb ${redPx})`);
- /* ---- transitions ---- */
- await page.evaluate(() => {
- // second clip right after the first, dissolve into it
- const c2 = CS.addClipToTimeline(window.__m, "V1", CS.clipEnd(window.__clip));
- window.__clip2 = c2;
- CS.selectClip(c2.id);
- CS.transitions.applyToSelected("dissolve");
- window.__clip2 = CS.getClip(c2.id);
- });
- const trInfo = await page.evaluate(() => ({
- type: window.__clip2.props.transition && window.__clip2.props.transition.type,
- frozen: Object.keys(CS.transitions.frozenTargets(window.__clip2.start + 0.3)).length,
- marker: !!document.querySelector(".tl-clip .clip-tr")
- }));
- if (trInfo.type !== "dissolve") fail("transition not stored");
- if (trInfo.frozen !== 1) fail("frozen predecessor not detected");
- if (!trInfo.marker) fail("transition marker missing on clip");
- await page.evaluate(() => CS.player.seek(window.__clip2.start + 0.3));
- await page.waitForTimeout(600);
- const trPx = await samplePx();
- if (trPx[0] + trPx[1] + trPx[2] < 20) fail("transition window renders black: " + trPx);
- ok(`dissolve transition renders mid-window (rgb ${trPx})`);
- /* ---- filters panel + galleries render ---- */
- const galleries = await page.evaluate(() => {
- CS.panels.show("fx");
- const fx = document.querySelectorAll("#fx-grid .fx-card").length;
- CS.panels.show("titles");
- const ti = document.querySelectorAll("#titles-grid .fx-card").length;
- CS.panels.show("transitions");
- const tr = document.querySelectorAll("#transitions-grid .fx-card").length;
- CS.panels.show("elements");
- const el = document.querySelectorAll("#elements-grid .fx-card").length;
- CS.panels.show("filters");
- const fi = document.querySelectorAll("#filters-grid .fx-card").length;
- CS.panels.show("media");
- return { fx, ti, tr, el, fi };
- });
- if (galleries.fx < 10) fail("effects gallery incomplete: " + galleries.fx);
- if (galleries.ti < 4 || galleries.tr < 3 || galleries.el < 8 || galleries.fi < 5) {
- fail("gallery counts wrong: " + JSON.stringify(galleries));
- }
- ok(`galleries render (${galleries.fx} effects, ${galleries.ti} titles, ${galleries.tr} transitions, ${galleries.el} elements, ${galleries.fi} filters)`);
- /* ---- filter look via inspector.applyPreset ---- */
- await page.evaluate(() => {
- CS.selectClip(window.__clip.id);
- CS.inspector.applyPreset(CS.getClip(window.__clip.id), "mono");
- });
- const preset = await page.evaluate(() => CS.getClip(window.__clip.id).props.preset);
- if (preset !== "mono") fail("filter preset not applied");
- ok("filter look applies color preset");
- /* ---- list view toggle ---- */
- await page.click("#btn-bin-view");
- const listMode = await page.evaluate(() => document.getElementById("bin-grid").classList.contains("list-mode"));
- if (!listMode) fail("list view did not activate");
- await page.click("#btn-bin-view");
- ok("media bin list view toggles");
- /* ---- serialization round trip with generated clips ---- */
- const rt = await page.evaluate(() => {
- CS.player.seek(1.5);
- CS.titles.insertPreset("lowerthird");
- const before = CS.project.clips.length;
- const beforeTitles = CS.project.clips.filter(c => c.kind === "title").length;
- const json = CS.fileio.serializeProject();
- CS.fileio.loadProject(JSON.parse(json), "", "rt.cine");
- return {
- before, beforeTitles,
- after: CS.project.clips.length,
- afterTitles: CS.project.clips.filter(c => c.kind === "title").length,
- transition: CS.project.clips.some(c => c.props.transition && c.props.transition.type === "dissolve"),
- effects: CS.project.clips.some(c => (c.props.effects || []).length > 0)
- };
- });
- if (rt.after !== rt.before) fail("clips lost in round trip: " + JSON.stringify(rt));
- if (rt.afterTitles !== rt.beforeTitles) fail("title clips lost in round trip");
- if (!rt.transition) fail("transition lost in round trip");
- if (!rt.effects) fail("effects lost in round trip");
- ok("round trip preserves titles, transitions and effect stacks");
- });
|