@@ -4043,6 +4043,132 @@ test.describe("MIDI Studio V2", () => {
40434043 }
40444044 } ) ;
40454045
4046+ test ( "color-codes PR069 octave timeline sections and matching section buttons" , async ( { page } ) => {
4047+ await page . setViewportSize ( { width : 1600 , height : 900 } ) ;
4048+ const server = await openMidiStudio ( page ) ;
4049+ try {
4050+ await fillGuidedSongSheet ( page , {
4051+ loopSections : "loop, bridge" ,
4052+ sections : "intro: C F\nloop: G C\nbridge: Dm G"
4053+ } ) ;
4054+ await page . locator ( "#parseSongSheetButton" ) . click ( ) ;
4055+ await selectMidiStudioTab ( page , "studio" ) ;
4056+ await waitForCanvasRender ( page ) ;
4057+
4058+ const sectionState = await canvasTimelineState ( page ) ;
4059+ expect ( sectionState . sections . map ( ( section ) => section . label ) ) . toEqual ( [ "intro" , "loop" , "bridge" ] ) ;
4060+ expect ( new Set ( sectionState . sections . map ( ( section ) => section . color ) ) . size ) . toBe ( 3 ) ;
4061+
4062+ const canvasSectionPixels = await page . evaluate ( ( ) => {
4063+ const canvas = document . querySelector ( "[data-octave-timeline-canvas='true']" ) ;
4064+ const state = window . __midiStudioV2App . instrumentGrid . timelineCanvasState ( ) ;
4065+ const ratio = canvas . width / canvas . clientWidth ;
4066+ const context = canvas . getContext ( "2d" ) ;
4067+ const sampleBody = ( sectionLabel ) => {
4068+ const section = state . sections . find ( ( entry ) => entry . label === sectionLabel ) ;
4069+ const x = Math . round ( ( state . axisWidth + section . startStep * state . cellSize + state . cellSize / 2 ) * ratio ) ;
4070+ const y = Math . round ( ( state . headerHeight + 4 ) * ratio ) ;
4071+ return Array . from ( context . getImageData ( x , y , 1 , 1 ) . data ) . slice ( 0 , 3 ) ;
4072+ } ;
4073+ return {
4074+ bridge : sampleBody ( "bridge" ) ,
4075+ intro : sampleBody ( "intro" ) ,
4076+ loop : sampleBody ( "loop" )
4077+ } ;
4078+ } ) ;
4079+ expect ( canvasSectionPixels . intro ) . not . toEqual ( canvasSectionPixels . loop ) ;
4080+ expect ( canvasSectionPixels . loop ) . not . toEqual ( canvasSectionPixels . bridge ) ;
4081+
4082+ const buttonState = await page . locator ( ".midi-studio-v2__section-preset" ) . evaluateAll ( ( buttons ) => Object . fromEntries ( buttons . map ( ( button ) => [
4083+ button . dataset . sectionPreset ,
4084+ {
4085+ color : button . dataset . sectionColor || "" ,
4086+ disabled : button . disabled ,
4087+ loop : button . dataset . sectionLoop ,
4088+ selected : button . dataset . sectionSelected ,
4089+ title : button . title ,
4090+ unwired : button . classList . contains ( "midi-studio-v2__unwired-control" )
4091+ }
4092+ ] ) ) ) ;
4093+ const colorsBySection = Object . fromEntries ( sectionState . sections . map ( ( section ) => [ section . label , section . color ] ) ) ;
4094+ expect ( buttonState . intro ) . toEqual ( expect . objectContaining ( {
4095+ color : colorsBySection . intro ,
4096+ disabled : false ,
4097+ unwired : false
4098+ } ) ) ;
4099+ expect ( buttonState . loop ) . toEqual ( expect . objectContaining ( {
4100+ color : colorsBySection . loop ,
4101+ disabled : false ,
4102+ unwired : false
4103+ } ) ) ;
4104+ expect ( buttonState . bridge ) . toEqual ( expect . objectContaining ( {
4105+ color : colorsBySection . bridge ,
4106+ disabled : false ,
4107+ unwired : false
4108+ } ) ) ;
4109+ expect ( buttonState . boss ) . toEqual ( expect . objectContaining ( {
4110+ disabled : true ,
4111+ title : "Boss section not available for this song" ,
4112+ unwired : true
4113+ } ) ) ;
4114+ await expect ( page . locator ( "[data-section-preset='boss']" ) ) . toHaveClass ( / m i d i - s t u d i o - v 2 _ _ u n w i r e d - c o n t r o l / ) ;
4115+
4116+ await page . locator ( "[data-section-preset='intro']" ) . click ( ) ;
4117+ await expect ( page . locator ( "#instrumentGridSectionSelect" ) ) . toHaveValue ( "intro" ) ;
4118+ await expect ( page . locator ( "[data-section-preset='intro']" ) ) . toHaveAttribute ( "data-section-selected" , "true" ) ;
4119+ await expect ( page . locator ( "#instrumentGridOutput" ) ) . toHaveAttribute ( "data-playhead-section" , "intro" ) ;
4120+ expect ( ( await canvasTimelineState ( page ) ) . selectedSection ) . toEqual ( expect . objectContaining ( {
4121+ color : colorsBySection . intro ,
4122+ label : "intro"
4123+ } ) ) ;
4124+
4125+ await page . locator ( "#playSectionButton" ) . click ( ) ;
4126+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K P r e v i e w S y n t h s t a r t e d f o r s e c t i o n i n t r o w i t h \d + p l a y a b l e e v e n t s \. / ) ;
4127+ await expect ( page . locator ( "[data-section-preset='intro']" ) ) . toHaveClass ( / i s - s e l e c t e d - s e c t i o n / ) ;
4128+ await page . locator ( "#stopTimingPreviewButton" ) . click ( ) ;
4129+
4130+ await page . locator ( "#instrumentGridLoopStartSelect" ) . selectOption ( "loop" ) ;
4131+ await page . locator ( "#instrumentGridLoopEndSelect" ) . selectOption ( "bridge" ) ;
4132+ await expect ( page . locator ( "[data-section-preset='loop']" ) ) . toHaveAttribute ( "data-section-loop" , "true" ) ;
4133+ await expect ( page . locator ( "[data-section-preset='bridge']" ) ) . toHaveAttribute ( "data-section-loop" , "true" ) ;
4134+ await page . locator ( "#playLoopButton" ) . click ( ) ;
4135+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K P r e v i e w S y n t h s t a r t e d f o r l o o p l o o p t o b r i d g e w i t h \d + p l a y a b l e e v e n t s \. / ) ;
4136+ const loopState = await canvasTimelineState ( page ) ;
4137+ expect ( loopState . loopBounds ) . toEqual ( expect . objectContaining ( {
4138+ endLabel : "bridge" ,
4139+ startLabel : "loop"
4140+ } ) ) ;
4141+ await page . locator ( "#stopTimingPreviewButton" ) . click ( ) ;
4142+
4143+ const scrollEvidence = await page . locator ( "#instrumentGridOutput" ) . evaluate ( ( output ) => {
4144+ output . style . width = "340px" ;
4145+ output . style . maxWidth = "340px" ;
4146+ output . scrollLeft = 220 ;
4147+ output . scrollTop = 180 ;
4148+ output . dispatchEvent ( new Event ( "scroll" ) ) ;
4149+ return {
4150+ scrollLeft : Math . round ( output . scrollLeft ) ,
4151+ scrollTop : Math . round ( output . scrollTop )
4152+ } ;
4153+ } ) ;
4154+ await expect ( octaveTimelineCanvas ( page ) ) . toHaveAttribute ( "data-frozen-header" , "true" ) ;
4155+ const scrolledState = await canvasTimelineState ( page ) ;
4156+ expect ( scrolledState . frozenHeaderVisible ) . toBe ( true ) ;
4157+ expect ( Math . round ( scrolledState . frozenHeaderScrollLeft ) ) . toBe ( scrollEvidence . scrollLeft ) ;
4158+ expect ( Math . round ( scrolledState . frozenHeaderScrollTop ) ) . toBe ( scrollEvidence . scrollTop ) ;
4159+
4160+ await page . locator ( "#playButton" ) . click ( ) ;
4161+ await expect ( page . locator ( "#playButton" ) ) . toBeDisabled ( ) ;
4162+ await expect ( page . locator ( "#stopButton" ) ) . toBeEnabled ( ) ;
4163+ await page . locator ( "#stopButton" ) . click ( ) ;
4164+ await expect ( page . locator ( "#stopButton" ) ) . toBeDisabled ( ) ;
4165+ await expect ( page . locator ( "#playButton" ) ) . toBeEnabled ( ) ;
4166+ } finally {
4167+ await workspaceV2CoverageReporter . stop ( page ) ;
4168+ await server . close ( ) ;
4169+ }
4170+ } ) ;
4171+
40464172 test ( "derives primary song, instrument, grid, playback, and diagnostics views from the canonical selected song" , async ( { page } ) => {
40474173 const server = await openMidiStudioForImport ( page ) ;
40484174 try {
0 commit comments