@@ -3,6 +3,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33import { tmpdir } from "node:os" ;
44import { join , resolve } from "node:path" ;
55import { fileURLToPath } from "node:url" ;
6+ import { isClipVisibleAt } from "@hyperframes/core" ;
67import { describe , expect , it } from "vitest" ;
78
89const cliEntry = resolve ( fileURLToPath ( import . meta. url ) , ".." , ".." , "cli.ts" ) ;
@@ -102,6 +103,135 @@ describe("timeline edit command", () => {
102103 }
103104 } ) ;
104105
106+ const meeting = ( aStart : string , aDuration : string , bStart : string , fps = "" ) =>
107+ `<div data-composition-id="main"${ fps } data-duration="40"><div id="a" data-hf-id="a" data-start="${ aStart } " data-duration="${ aDuration } " data-track-index="0"></div><div id="b" data-hf-id="b" data-start="${ bStart } " data-duration="2" data-track-index="0"></div></div>` ;
108+ const clip = ( html : string , id : string ) => {
109+ const tag = new RegExp ( `<div[^>]*\\sid="${ id } "[^>]*>` ) . exec ( html ) ?. [ 0 ] ?? "" ;
110+ const attr = ( name : string ) => new RegExp ( `${ name } ="([^"]+)"` ) . exec ( tag ) ?. [ 1 ] ;
111+ return { start : attr ( "data-start" ) , duration : attr ( "data-duration" ) } ;
112+ } ;
113+ const endOf = ( html : string , id : string ) =>
114+ Number ( clip ( html , id ) . start ) + Number ( clip ( html , id ) . duration ) ;
115+ const visibleAt = ( html : string , time : number , ids : string [ ] ) =>
116+ ids . filter ( ( id ) => isClipVisibleAt ( time , Number ( clip ( html , id ) . start ) , endOf ( html , id ) , 40 ) ) ;
117+
118+ const composition = ( fps : string , ...clips : [ string , string , string ] [ ] ) =>
119+ `<div data-composition-id="main"${ fps } data-duration="40">${ clips
120+ . map (
121+ ( [ id , start , duration ] ) =>
122+ `<div id="${ id } " data-hf-id="${ id } " data-start="${ start } " data-duration="${ duration } " data-track-index="0"></div>` ,
123+ )
124+ . join ( "" ) } </div>`;
125+
126+ it . each ( [
127+ [
128+ "move" ,
129+ [ "#b" , "20f" ] ,
130+ composition ( ' data-fps="24"' , [ "a" , "0" , String ( 20 / 24 + 5e-7 ) ] , [ "b" , "2" , "0.25" ] ) ,
131+ ] ,
132+ [ "duplicate" , [ "#c" , "--at" , "1" ] , composition ( "" , [ "a" , "0" , "1.0000005" ] , [ "c" , "3" , "1" ] ) ] ,
133+ ] ) ( "refuses a %s that lands half a microsecond inside another clip" , ( verb , args , html ) => {
134+ const dir = project ( ) ;
135+ try {
136+ writeFileSync ( join ( dir , "index.html" ) , html ) ;
137+ expect ( run ( dir , verb , ...args ) . status ) . not . toBe ( 0 ) ;
138+ expect ( readFileSync ( join ( dir , "index.html" ) , "utf8" ) ) . toBe ( html ) ;
139+ } finally {
140+ rmSync ( dir , { recursive : true , force : true } ) ;
141+ }
142+ } ) ;
143+
144+ it . each ( [
145+ [
146+ "a clip wholly before the insertion point" ,
147+ [ "#x" , "--at" , "5" ] ,
148+ composition ( "" , [ "x" , "0" , "1" ] , [ "t" , "4.9999995" , "0.0000002" ] ) ,
149+ 4.9999995 ,
150+ ] ,
151+ [
152+ "a clip half a microsecond after it" ,
153+ [ "#a" ] ,
154+ composition ( "" , [ "a" , "0" , "1" ] , [ "t" , "1.0000005" , "1" ] ) ,
155+ 1.0000005 + 1 ,
156+ ] ,
157+ ] ) ( "duplicates without snapping %s to the copy" , ( _ , args , html , tStart ) => {
158+ const dir = project ( ) ;
159+ try {
160+ writeFileSync ( join ( dir , "index.html" ) , html ) ;
161+ expect ( run ( dir , "duplicate" , ...args ) . status ) . toBe ( 0 ) ;
162+ expect ( Number ( clip ( readFileSync ( join ( dir , "index.html" ) , "utf8" ) , "t" ) . start ) ) . toBe ( tStart ) ;
163+ } finally {
164+ rmSync ( dir , { recursive : true , force : true } ) ;
165+ }
166+ } ) ;
167+
168+ it ( "trims a clip that only meets the one before it, ending where asked" , ( ) => {
169+ const dir = project ( ) ;
170+ try {
171+ writeFileSync ( join ( dir , "index.html" ) , meeting ( "19.8" , "6.4" , "26.2" ) ) ;
172+ expect ( run ( dir , "trim" , "#b" , "--end" , "29" ) . status ) . toBe ( 0 ) ;
173+ const html = readFileSync ( join ( dir , "index.html" ) , "utf8" ) ;
174+ expect ( clip ( html , "b" ) ) . toEqual ( { start : "26.2" , duration : "2.8000000000000007" } ) ;
175+ expect ( endOf ( html , "b" ) ) . toBe ( 29 ) ;
176+ } finally {
177+ rmSync ( dir , { recursive : true , force : true } ) ;
178+ }
179+ } ) ;
180+
181+ it ( "trims up to the next clip's start without ending past it, where no duration lands exactly" , ( ) => {
182+ const dir = project ( ) ;
183+ try {
184+ writeFileSync ( join ( dir , "index.html" ) , meeting ( "4.74" , "10" , "25.74" ) ) ;
185+ expect ( run ( dir , "trim" , "#a" , "--end" , "25.74" ) . status ) . toBe ( 0 ) ;
186+ const html = readFileSync ( join ( dir , "index.html" ) , "utf8" ) ;
187+ expect ( endOf ( html , "a" ) ) . toBeLessThanOrEqual ( 25.74 ) ;
188+ expect ( visibleAt ( html , 25.74 , [ "a" , "b" ] ) ) . toEqual ( [ "b" ] ) ;
189+ } finally {
190+ rmSync ( dir , { recursive : true , force : true } ) ;
191+ }
192+ } ) ;
193+
194+ it ( "trims to frame 20 at 30 fps so that frame shows the next clip and not this one" , ( ) => {
195+ const dir = project ( ) ;
196+ try {
197+ writeFileSync ( join ( dir , "index.html" ) , meeting ( "0" , "2" , "2" , ' data-fps="30"' ) ) ;
198+ expect ( run ( dir , "trim" , "#a" , "--end" , "20f" ) . status ) . toBe ( 0 ) ;
199+ expect ( run ( dir , "trim" , "#b" , "--start" , "20f" ) . status ) . toBe ( 0 ) ;
200+ const html = readFileSync ( join ( dir , "index.html" ) , "utf8" ) ;
201+ expect ( [ clip ( html , "a" ) . duration , clip ( html , "b" ) . start ] ) . toEqual ( [
202+ "0.6666666666666666" ,
203+ "0.6666666666666666" ,
204+ ] ) ;
205+ expect ( visibleAt ( html , 19 / 30 , [ "a" , "b" ] ) ) . toEqual ( [ "a" ] ) ;
206+ expect ( visibleAt ( html , 20 / 30 , [ "a" , "b" ] ) ) . toEqual ( [ "b" ] ) ;
207+ } finally {
208+ rmSync ( dir , { recursive : true , force : true } ) ;
209+ }
210+ } ) ;
211+
212+ it . each ( [
213+ [ "19.8" , "6.4" , "26.2" ] ,
214+ [ "0" , "0.6666666666666666" , "0.6666666666666666" ] ,
215+ [ "0.1" , "1.1" , "1.2" ] ,
216+ ] ) (
217+ "duplicates a clip at %s lasting %s up against the clip at %s, each boundary exact" ,
218+ ( aStart , aDuration , bStart ) => {
219+ const dir = project ( ) ;
220+ try {
221+ writeFileSync ( join ( dir , "index.html" ) , meeting ( aStart , aDuration , bStart ) ) ;
222+ expect ( run ( dir , "duplicate" , "#a" ) . status ) . toBe ( 0 ) ;
223+ const html = readFileSync ( join ( dir , "index.html" ) , "utf8" ) ;
224+ const ids = [ "a" , "a-copy" , "b" ] ;
225+ expect ( Number ( clip ( html , "a-copy" ) . start ) ) . toBe ( endOf ( html , "a" ) ) ;
226+ expect ( Number ( clip ( html , "b" ) . start ) ) . toBe ( endOf ( html , "a-copy" ) ) ;
227+ expect ( visibleAt ( html , endOf ( html , "a" ) , ids ) ) . toEqual ( [ "a-copy" ] ) ;
228+ expect ( visibleAt ( html , endOf ( html , "a-copy" ) , ids ) ) . toEqual ( [ "b" ] ) ;
229+ } finally {
230+ rmSync ( dir , { recursive : true , force : true } ) ;
231+ }
232+ } ,
233+ ) ;
234+
105235 it ( "refuses an ambiguous reference" , ( ) => {
106236 const dir = project ( ) ;
107237 try {
0 commit comments