@@ -161,8 +161,18 @@ const stub = (name: string) => ({
161161 default : ( ) => < div data-testid = { name } />
162162} ) ;
163163jest . mock ( "../../properties/LanguageModelSelect" , ( ) => stub ( "lang-model" ) ) ;
164- jest . mock ( "../../properties/ImageModelSelect" , ( ) => stub ( "image-model" ) ) ;
165- jest . mock ( "../../properties/VideoModelSelect" , ( ) => stub ( "video-model" ) ) ;
164+ jest . mock ( "../../properties/ImageModelSelect" , ( ) => ( {
165+ __esModule : true ,
166+ default : ( ) => (
167+ < div role = "combobox" aria-label = "Still model" data-testid = "image-model" />
168+ )
169+ } ) ) ;
170+ jest . mock ( "../../properties/VideoModelSelect" , ( ) => ( {
171+ __esModule : true ,
172+ default : ( ) => (
173+ < div role = "combobox" aria-label = "Clip model" data-testid = "video-model" />
174+ )
175+ } ) ) ;
166176// The card's own behaviour has its own suite (ShotCard.test.tsx); this stub
167177// keeps the contract the board drives — the shot id hook the keyboard
168178// navigation focuses, selection on click, and the drag callbacks.
@@ -261,6 +271,10 @@ beforeEach(() => {
261271 mockGenre = "" ;
262272 mockEntities = [ ] ;
263273 mockPresets = [ ] ;
274+ boardModels = {
275+ imageModel : { id : "fal-ai/flux/schnell" , provider : "fal_ai" } ,
276+ videoModel : { id : "pixverse/720p" , provider : "fal_ai" }
277+ } ;
264278} ) ;
265279
266280const renderBoard = ( onDirect : ( n : number ) => void ) =>
@@ -438,12 +452,117 @@ describe("StoryboardBoard toolbar", () => {
438452 expect (
439453 screen . getByRole ( "button" , { name : "Render stills (1)" } )
440454 ) . toBeEnabled ( ) ;
455+ expect (
456+ screen . getByRole ( "button" , { name : "Render stills (1)" } )
457+ ) . toHaveAttribute ( "aria-current" , "step" ) ;
441458 expect ( screen . getByRole ( "button" , { name : "Render clips" } ) ) . toBeDisabled ( ) ;
442459 expect (
443460 screen . getByRole ( "button" , { name : "Assemble timeline" } )
444461 ) . toBeDisabled ( ) ;
445462 } ) ;
446463
464+ it ( "highlights clip rendering after every shot has a still" , ( ) => {
465+ mockShots = [
466+ {
467+ ...makeShot ( "s1" ) ,
468+ status : "keyframe_ready" ,
469+ keyframe : { type : "image" , asset_id : "still-1" }
470+ }
471+ ] ;
472+ renderBoard ( jest . fn ( ) ) ;
473+
474+ expect (
475+ screen . getByRole ( "button" , { name : "Render clips (1)" } )
476+ ) . toHaveAttribute ( "aria-current" , "step" ) ;
477+ expect (
478+ screen . getByRole ( "button" , { name : "Render stills" } )
479+ ) . not . toHaveAttribute ( "aria-current" ) ;
480+ } ) ;
481+
482+ it ( "treats a URI-backed legacy keyframe as a completed still" , ( ) => {
483+ mockShots = [
484+ {
485+ ...makeShot ( "s1" ) ,
486+ status : "keyframe_ready" ,
487+ keyframe : { type : "image" , uri : "asset://still-1.png" }
488+ }
489+ ] ;
490+ renderBoard ( jest . fn ( ) ) ;
491+
492+ expect (
493+ screen . getByRole ( "button" , { name : "Render clips (1)" } )
494+ ) . toHaveAttribute ( "aria-current" , "step" ) ;
495+ } ) ;
496+
497+ it ( "highlights clip rendering for a direct-render shot" , ( ) => {
498+ mockShots = [ { ...makeShot ( "s1" ) , render_mode : "direct" } ] ;
499+ renderBoard ( jest . fn ( ) ) ;
500+
501+ expect (
502+ screen . getByRole ( "button" , { name : "Render clips (1)" } )
503+ ) . toHaveAttribute ( "aria-current" , "step" ) ;
504+ expect (
505+ screen . getByRole ( "button" , { name : "Render stills" } )
506+ ) . toBeDisabled ( ) ;
507+ } ) ;
508+
509+ it ( "does not advance to clips while another still is rendering" , ( ) => {
510+ mockShots = [
511+ { ...makeShot ( "s1" ) , status : "keyframe_generating" } ,
512+ {
513+ ...makeShot ( "s2" ) ,
514+ status : "keyframe_ready" ,
515+ keyframe : { type : "image" , asset_id : "still-2" }
516+ }
517+ ] ;
518+ renderBoard ( jest . fn ( ) ) ;
519+
520+ expect (
521+ screen . getByRole ( "button" , { name : "Render clips (1)" } )
522+ ) . not . toHaveAttribute ( "aria-current" ) ;
523+ } ) ;
524+
525+ it ( "opens the still model picker and asks for a model before rendering" , ( ) => {
526+ boardModels = { imageModel : null , videoModel : null } ;
527+ mockShots = [ makeShot ( "s1" ) ] ;
528+ renderBoard ( jest . fn ( ) ) ;
529+
530+ expect (
531+ screen . getByRole ( "combobox" , { name : "Still model" } )
532+ ) . toBeInTheDocument ( ) ;
533+ expect ( screen . getByRole ( "alert" ) ) . toHaveTextContent (
534+ "Choose a still model before rendering stills."
535+ ) ;
536+ expect (
537+ screen . getByRole ( "button" , { name : "Render stills (1)" } )
538+ ) . toBeDisabled ( ) ;
539+ } ) ;
540+
541+ it ( "opens the clip model picker and asks for a model before rendering" , ( ) => {
542+ boardModels = {
543+ imageModel : { id : "fal-ai/flux/schnell" , provider : "fal_ai" } ,
544+ videoModel : null
545+ } ;
546+ mockShots = [
547+ {
548+ ...makeShot ( "s1" ) ,
549+ status : "keyframe_ready" ,
550+ keyframe : { type : "image" , asset_id : "still-1" }
551+ }
552+ ] ;
553+ renderBoard ( jest . fn ( ) ) ;
554+
555+ expect (
556+ screen . getByRole ( "combobox" , { name : "Clip model" } )
557+ ) . toBeInTheDocument ( ) ;
558+ expect ( screen . getByRole ( "alert" ) ) . toHaveTextContent (
559+ "Choose a clip model before rendering clips."
560+ ) ;
561+ expect (
562+ screen . getByRole ( "button" , { name : "Render clips (1)" } )
563+ ) . toBeDisabled ( ) ;
564+ } ) ;
565+
447566 it ( "puts each batch's price on its own button" , ( ) => {
448567 // Two shots await a still; only the one already holding a keyframe can be
449568 // animated, so the two buttons quote different batches.
0 commit comments