@@ -417,6 +417,20 @@ async function expectInstrumentClickKeepsScroll(page, locator) {
417417 await expect . poll ( ( ) => instrumentScrollSnapshot ( page ) ) . toEqual ( before ) ;
418418}
419419
420+ async function timelineScrollSnapshot ( page ) {
421+ return page . locator ( "#instrumentGridOutput" ) . evaluate ( ( output ) => {
422+ const firstHeader = output . querySelector ( ".midi-studio-v2__grid-cell--timing-header" ) ;
423+ const firstNoteCell = output . querySelector ( ".midi-studio-v2__octave-note-cell" ) ;
424+ return {
425+ headerLeft : Math . round ( firstHeader ?. getBoundingClientRect ( ) . left || 0 ) ,
426+ noteLeft : Math . round ( firstNoteCell ?. getBoundingClientRect ( ) . left || 0 ) ,
427+ scrollDataset : output . dataset . timelineScrollLeft || "" ,
428+ scrollLeft : Math . round ( output . scrollLeft ) ,
429+ scrollTop : Math . round ( output . scrollTop )
430+ } ;
431+ } ) ;
432+ }
433+
420434function laneHeader ( page , lane ) {
421435 return page . locator ( `.midi-studio-v2__lane-header-cell[data-lane="${ lane } "]` ) ;
422436}
@@ -801,6 +815,82 @@ test.describe("MIDI Studio V2", () => {
801815 }
802816 } ) ;
803817
818+ test ( "fast octave note editing supports drag painting keyboard shortcuts selection and timeline scroll sync" , async ( { page } ) => {
819+ const server = await openMidiStudioForImport ( page ) ;
820+ try {
821+ await page . locator ( "#toolImportManifestInput" ) . setInputFiles ( uatManifestPath ) ;
822+ await selectInstrumentRow ( page , "lead" ) ;
823+ await expect ( octaveCell ( page , "C6" , 2 ) ) . not . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
824+
825+ await octaveCell ( page , "C6" , 2 ) . click ( ) ;
826+ await expect ( octaveCell ( page , "C6" , 2 ) ) . toHaveText ( "C6" ) ;
827+ await expect ( octaveCell ( page , "C6" , 2 ) ) . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
828+ await expect ( octaveCell ( page , "C6" , 2 ) ) . toHaveClass ( / m i d i - s t u d i o - v 2 _ _ g r i d - c e l l - - n o t e - s e l e c t e d / ) ;
829+ await octaveCell ( page , "C6" , 2 ) . click ( ) ;
830+ await expect ( octaveCell ( page , "C6" , 2 ) ) . not . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
831+ await octaveCell ( page , "C6" , 2 ) . click ( ) ;
832+
833+ await octaveCell ( page , "C6" , 3 ) . dragTo ( octaveCell ( page , "C6" , 5 ) ) ;
834+ for ( const stepIndex of [ 3 , 4 , 5 ] ) {
835+ await expect ( octaveCell ( page , "C6" , stepIndex ) ) . toHaveText ( "C6" ) ;
836+ await expect ( octaveCell ( page , "C6" , stepIndex ) ) . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
837+ }
838+ await octaveCell ( page , "C6" , 5 ) . dragTo ( octaveCell ( page , "C6" , 7 ) ) ;
839+ for ( const stepIndex of [ 5 , 6 , 7 ] ) {
840+ await expect ( octaveCell ( page , "C6" , stepIndex ) ) . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
841+ }
842+
843+ await octaveCell ( page , "C5" , 10 ) . click ( ) ;
844+ await octaveCell ( page , "E5" , 10 ) . click ( ) ;
845+ const chordValues = await page . evaluate ( ( ) => window . __midiStudioV2App . lastInstrumentGridResult . timeline
846+ . filter ( ( event ) => event . lane === "lead" && event . stepIndex === 10 )
847+ . map ( ( event ) => event . value )
848+ . sort ( ) ) ;
849+ expect ( chordValues ) . toEqual ( expect . arrayContaining ( [ "C5" , "E5" ] ) ) ;
850+ expect ( chordValues . length ) . toBeGreaterThanOrEqual ( 2 ) ;
851+
852+ await octaveCell ( page , "C6" , 7 ) . click ( ) ;
853+ await page . keyboard . press ( "ArrowRight" ) ;
854+ await expect ( octaveCell ( page , "C6" , 8 ) ) . toHaveClass ( / m i d i - s t u d i o - v 2 _ _ g r i d - c e l l - - n o t e - s e l e c t e d / ) ;
855+ await page . keyboard . press ( "Control+D" ) ;
856+ await expect ( octaveCell ( page , "C6" , 9 ) ) . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
857+ await expect ( octaveCell ( page , "C6" , 9 ) ) . toHaveClass ( / m i d i - s t u d i o - v 2 _ _ g r i d - c e l l - - n o t e - s e l e c t e d / ) ;
858+ await page . keyboard . press ( "Delete" ) ;
859+ await expect ( octaveCell ( page , "C6" , 9 ) ) . not . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
860+ await expect ( octaveCell ( page , "C6" , 9 ) ) . toHaveClass ( / m i d i - s t u d i o - v 2 _ _ g r i d - c e l l - - n o t e - s e l e c t e d / ) ;
861+ await page . keyboard . press ( "ArrowLeft" ) ;
862+ await page . keyboard . press ( "Backspace" ) ;
863+ await expect ( octaveCell ( page , "C6" , 8 ) ) . not . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
864+
865+ const output = page . locator ( "#instrumentGridOutput" ) ;
866+ await output . evaluate ( ( element ) => {
867+ element . scrollLeft = 180 ;
868+ element . scrollTop = 90 ;
869+ element . dispatchEvent ( new Event ( "scroll" ) ) ;
870+ } ) ;
871+ const beforeScroll = await timelineScrollSnapshot ( page ) ;
872+ expect ( beforeScroll . scrollDataset ) . toBe ( String ( beforeScroll . scrollLeft ) ) ;
873+ await octaveCell ( page , "D6" , 11 ) . click ( ) ;
874+ await expect . poll ( ( ) => timelineScrollSnapshot ( page ) ) . toEqual ( expect . objectContaining ( {
875+ scrollLeft : beforeScroll . scrollLeft ,
876+ scrollTop : beforeScroll . scrollTop
877+ } ) ) ;
878+ const afterScroll = await timelineScrollSnapshot ( page ) ;
879+ expect ( afterScroll . headerLeft ) . toBe ( afterScroll . noteLeft ) ;
880+ expect ( afterScroll . scrollDataset ) . toBe ( String ( afterScroll . scrollLeft ) ) ;
881+
882+ await page . keyboard . press ( "Space" ) ;
883+ await expect ( page . locator ( "#stopButton" ) ) . toBeEnabled ( ) ;
884+ await expect ( page . locator ( "#playButton" ) ) . toBeDisabled ( ) ;
885+ await page . keyboard . press ( "Space" ) ;
886+ await expect ( page . locator ( "#stopButton" ) ) . toBeDisabled ( ) ;
887+ await expect ( page . locator ( "#playButton" ) ) . toBeEnabled ( ) ;
888+ } finally {
889+ await workspaceV2CoverageReporter . stop ( page ) ;
890+ await server . close ( ) ;
891+ }
892+ } ) ;
893+
804894 test ( "imports UAT manifest and plays the upbeat multi-instrument studio arrangement" , async ( { page } ) => {
805895 const server = await openMidiStudioForImport ( page ) ;
806896 try {
0 commit comments