@@ -287,6 +287,27 @@ async function audioDiagnosticsRows(page) {
287287 ] ) ) ) ;
288288}
289289
290+ function spreadsheetCell ( page , lane , stepIndex ) {
291+ return page . locator ( `.midi-studio-v2__spreadsheet-note-cell[data-lane="${ lane } "][data-step-index="${ stepIndex } "]` ) ;
292+ }
293+
294+ async function setSpreadsheetRowToggle ( page , laneLabel , kind , checked ) {
295+ const toggle = page . locator ( `.midi-studio-v2__lane-header-cell [aria-label="${ kind } ${ laneLabel } "]` ) ;
296+ await expect ( toggle ) . toHaveCount ( 1 ) ;
297+ await toggle . evaluate (
298+ ( input , nextChecked ) => {
299+ input . checked = nextChecked ;
300+ input . dispatchEvent ( new Event ( "change" , { bubbles : true } ) ) ;
301+ } ,
302+ checked
303+ ) ;
304+ if ( checked ) {
305+ await expect ( toggle ) . toBeChecked ( ) ;
306+ } else {
307+ await expect ( toggle ) . not . toBeChecked ( ) ;
308+ }
309+ }
310+
290311test . describe ( "MIDI Studio V2" , ( ) => {
291312 test . afterAll ( async ( ) => {
292313 await workspaceV2CoverageReporter . writeReport ( ) ;
@@ -372,6 +393,13 @@ test.describe("MIDI Studio V2", () => {
372393 await expect ( page . locator ( "#previewInstrumentPadSelect" ) ) . toHaveValue ( "ambient-pad" ) ;
373394 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K L o a d e d e x p l i c i t d e m o t e s t s o n g d a t a \. D e m o p a t h s a r e d e c l a r e d f o r U A T o n l y ; t h e y a r e n o t h i d d e n f a l l b a c k a s s e t s \. / ) ;
374395 await expect ( page . locator ( "#instrumentGridSectionSelect" ) ) . toContainText ( "intro" ) ;
396+ await expect ( page . locator ( ".midi-studio-v2__grid-cell" ) . first ( ) ) . toHaveText ( "Instrument/Lane" ) ;
397+ await expect ( page . locator ( ".midi-studio-v2__spreadsheet-note-cell[data-lane]" ) ) . not . toHaveCount ( 0 ) ;
398+ await expect ( spreadsheetCell ( page , "chords" , 0 ) ) . toHaveText ( "Am" ) ;
399+ await expect ( spreadsheetCell ( page , "bass" , 0 ) ) . toHaveText ( "A2" ) ;
400+ await expect ( spreadsheetCell ( page , "pad" , 0 ) ) . toHaveText ( "Am" ) ;
401+ await expect ( spreadsheetCell ( page , "lead" , 0 ) ) . toHaveText ( "A4" ) ;
402+ await expect ( spreadsheetCell ( page , "drums" , 0 ) ) . toHaveText ( "kick" ) ;
375403 await page . locator ( "#playSectionButton" ) . click ( ) ;
376404 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 \. / ) ;
377405 expect ( await page . evaluate ( ( ) => window . __midiStudioPreviewSynthEvents . some ( ( event ) => event . action === "oscillator-start" ) ) ) . toBe ( true ) ;
@@ -422,7 +450,8 @@ test.describe("MIDI Studio V2", () => {
422450 await expect ( page . locator ( "#instrumentGridTransportState" ) ) . toContainText ( "Playing section Preview Synth timing preview: intro" ) ;
423451 expect ( await page . evaluate ( ( ) => window . __midiStudioPreviewSynthEvents . some ( ( event ) => event . action === "resume" ) ) ) . toBe ( true ) ;
424452 expect ( await page . evaluate ( ( ) => window . __midiStudioPreviewSynthEvents . some ( ( event ) => event . action === "oscillator-start" ) ) ) . toBe ( true ) ;
425- await expect ( page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) ) . not . toHaveAttribute ( "data-step-index" , "0" ) ;
453+ await expect ( page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell--playhead-active" ) ) . not . toHaveAttribute ( "data-step-index" , "0" ) ;
454+ await expect ( spreadsheetCell ( page , "bass" , 0 ) ) . toHaveText ( "A2" ) ;
426455 const diagnostics = await audioDiagnosticsRows ( page ) ;
427456 expect ( Number ( diagnostics [ "Playable note count" ] ) ) . toBeGreaterThan ( 0 ) ;
428457 expect ( diagnostics ) . toMatchObject ( {
@@ -739,6 +768,33 @@ Am F`);
739768 }
740769 } ) ;
741770
771+ test ( "edits spreadsheet note cells into playable timeline data" , async ( { page } ) => {
772+ const server = await openMidiStudio ( page ) ;
773+ try {
774+ await fillInstrumentGrid ( page ) ;
775+ await page . locator ( "#normalizeInstrumentGridButton" ) . click ( ) ;
776+ const beforeDiagnostics = await audioDiagnosticsRows ( page ) ;
777+ const beforeCount = Number ( beforeDiagnostics [ "Playable note count" ] ) ;
778+ await spreadsheetCell ( page , "lead" , 3 ) . fill ( "B4" ) ;
779+ await expect ( page . locator ( "#instrumentGridLeadInput" ) ) . toHaveValue ( "E4 G4 A4 B4 | C5 B4 G4 -" ) ;
780+ await page . locator ( "#normalizeInstrumentGridButton" ) . click ( ) ;
781+ await expect ( spreadsheetCell ( page , "lead" , 3 ) ) . toHaveText ( "B4" ) ;
782+ const editedModel = await page . evaluate ( ( ) => window . __midiStudioV2App . lastInstrumentGridResult ) ;
783+ expect ( editedModel . timeline . some ( ( event ) => event . lane === "lead" && event . value === "B4" && event . bar === 1 ) ) . toBe ( true ) ;
784+ expect ( Number ( ( await audioDiagnosticsRows ( page ) ) [ "Playable note count" ] ) ) . toBeGreaterThan ( beforeCount ) ;
785+ await page . locator ( "#playSectionButton" ) . click ( ) ;
786+ 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 \. / ) ;
787+
788+ await spreadsheetCell ( page , "lead" , 1 ) . fill ( "BAD" ) ;
789+ await page . locator ( "#normalizeInstrumentGridButton" ) . click ( ) ;
790+ await expect ( page . locator ( "#instrumentGridSummary" ) ) . toContainText ( 'Invalid note token "BAD"' ) ;
791+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / F A I L I n s t r u m e n t g r i d r e j e c t e d : I n v a l i d n o t e t o k e n " B A D " i n l e a d a t b a r 1 , b e a t 2 \. / ) ;
792+ } finally {
793+ await workspaceV2CoverageReporter . stop ( page ) ;
794+ await server . close ( ) ;
795+ }
796+ } ) ;
797+
742798 test ( "keeps beat bar alignment consistent across grid lanes" , async ( { page } ) => {
743799 const server = await openMidiStudio ( page ) ;
744800 try {
@@ -748,9 +804,19 @@ Am F`);
748804 } ) ;
749805 await page . locator ( "#normalizeInstrumentGridButton" ) . click ( ) ;
750806 expect ( await page . locator ( ".midi-studio-v2__instrument-grid" ) . evaluate ( ( grid ) => getComputedStyle ( grid ) . gridTemplateColumns . split ( " " ) . length ) ) . toBe ( 9 ) ;
751- expect ( await page . locator ( ".midi-studio-v2__grid-cell--section" ) . evaluateAll ( ( cells ) => cells . map ( ( cell ) => cell . style . gridColumn ) ) ) . toEqual ( [ "span 4" , "span 4" ] ) ;
752- expect ( await page . locator ( ".midi-studio-v2__grid-cell--beat" ) . evaluateAll ( ( cells ) => cells . map ( ( cell ) => cell . textContent ) ) ) . toEqual ( [ "B1.1" , "B1.2" , "B1.3" , "B1.4" , "B2.1" , "B2.2" , "B2.3" , "B2.4" ] ) ;
753- expect ( await page . locator ( ".midi-studio-v2__instrument-grid" ) . evaluate ( ( grid ) => Array . from ( grid . children ) . filter ( ( cell ) => cell . textContent === "Am" ) . length ) ) . toBe ( 3 ) ;
807+ await expect ( page . locator ( ".midi-studio-v2__grid-cell" ) . first ( ) ) . toHaveText ( "Instrument/Lane" ) ;
808+ expect ( await page . locator ( ".midi-studio-v2__grid-cell--timing-header" ) . evaluateAll ( ( cells ) => cells . map ( ( cell ) => cell . textContent ) ) ) . toEqual ( [
809+ "Section introBar 1Beat 1" ,
810+ "Section introBar 1Beat 2" ,
811+ "Section introBar 1Beat 3" ,
812+ "Section introBar 1Beat 4" ,
813+ "Section loopBar 2Beat 1" ,
814+ "Section loopBar 2Beat 2" ,
815+ "Section loopBar 2Beat 3" ,
816+ "Section loopBar 2Beat 4"
817+ ] ) ;
818+ await expect ( spreadsheetCell ( page , "chords" , 0 ) ) . toHaveText ( "Am" ) ;
819+ await expect ( spreadsheetCell ( page , "bass" , 0 ) ) . toHaveText ( "A2" ) ;
754820 } finally {
755821 await workspaceV2CoverageReporter . stop ( page ) ;
756822 await server . close ( ) ;
@@ -813,11 +879,11 @@ Am F`);
813879 await expect ( page . locator ( "#instrumentGridSnapIndicator" ) ) . toContainText ( "Snap: 1 bar / 2 beats / 1/2" ) ;
814880 await page . locator ( "#normalizeInstrumentGridButton" ) . click ( ) ;
815881 expect ( await page . locator ( ".midi-studio-v2__instrument-grid" ) . evaluate ( ( grid ) => getComputedStyle ( grid ) . gridTemplateColumns . split ( " " ) . length ) ) . toBe ( 5 ) ;
816- expect ( await page . locator ( ".midi-studio-v2__grid-cell--beat " ) . evaluateAll ( ( cells ) => cells . map ( ( cell ) => cell . textContent ) ) ) . toEqual ( [
817- "B1.1. 1" ,
818- "B1.1. 2" ,
819- "B1.2. 1" ,
820- "B1.2. 2"
882+ expect ( await page . locator ( ".midi-studio-v2__grid-cell--timing-header " ) . evaluateAll ( ( cells ) => cells . map ( ( cell ) => cell . textContent ) ) ) . toEqual ( [
883+ "Section introBar 1Beat 1Subdivision 1" ,
884+ "Section introBar 1Beat 1Subdivision 2" ,
885+ "Section introBar 1Beat 2Subdivision 1" ,
886+ "Section introBar 1Beat 2Subdivision 2"
821887 ] ) ;
822888 await page . locator ( "#instrumentGridSubdivisionInput" ) . selectOption ( "16" ) ;
823889 await expect ( page . locator ( "#instrumentGridSnapIndicator" ) ) . toContainText ( "Snap: 1 bar / 2 beats / 1/16" ) ;
@@ -843,15 +909,15 @@ Am F`);
843909 await expect ( page . locator ( "#instrumentGridOutput" ) ) . toContainText ( "Bar 5" ) ;
844910 await expect ( page . locator ( ".midi-studio-v2__grid-cell--bar" ) ) . toHaveCount ( 5 ) ;
845911 await expect ( page . locator ( ".midi-studio-v2__grid-cell--ruler" ) . first ( ) ) . toContainText ( "1" ) ;
846- await expect ( page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) ) . toHaveAttribute ( "data-section" , "intro" ) ;
912+ await expect ( page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell-- playhead-active" ) ) . toHaveAttribute ( "data-section" , "intro" ) ;
847913 await expect ( page . locator ( "#instrumentGridSectionSelect" ) ) . toContainText ( "bridge" ) ;
848914 await page . locator ( "#instrumentGridLoopStartSelect" ) . selectOption ( "loop" ) ;
849915 await page . locator ( "#instrumentGridLoopEndSelect" ) . selectOption ( "boss" ) ;
850916 expect ( await page . locator ( ".midi-studio-v2__grid-cell--loop-region" ) . count ( ) ) . toBeGreaterThan ( 0 ) ;
851917 await page . locator ( '[data-section-preset="boss"]' ) . click ( ) ;
852918 await expect ( page . locator ( "#instrumentGridSectionSelect" ) ) . toHaveValue ( "boss" ) ;
853919 await page . locator ( "#jumpToSectionButton" ) . click ( ) ;
854- await expect ( page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) ) . toHaveAttribute ( "data-section" , "boss" ) ;
920+ await expect ( page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell-- playhead-active" ) ) . toHaveAttribute ( "data-section" , "boss" ) ;
855921 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K T i m i n g p l a y h e a d j u m p e d t o s e c t i o n b o s s \. / ) ;
856922 } finally {
857923 await workspaceV2CoverageReporter . stop ( page ) ;
@@ -877,12 +943,12 @@ Am F`);
877943 await expect ( page . locator ( '[data-lane="bass"][data-source="generated"]' ) ) . toHaveCount ( 4 ) ;
878944 await expect ( page . locator ( '[data-lane="bass"][data-source="manual"]' ) ) . toHaveCount ( 1 ) ;
879945 await page . locator ( "#instrumentGridSectionSelect" ) . selectOption ( "intro" ) ;
880- const beforeStep = await page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) . getAttribute ( "data-step-index" ) ;
946+ const beforeStep = await page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell-- playhead-active" ) . getAttribute ( "data-step-index" ) ;
881947 await page . locator ( "#playSectionButton" ) . click ( ) ;
882948 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 \. / ) ;
883- await expect ( page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) ) . not . toHaveAttribute ( "data-step-index" , beforeStep || "" ) ;
884- await expect ( page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) ) . toHaveAttribute ( "data-beat" , / 1 | 2 / ) ;
885- await expect ( page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) ) . toHaveAttribute ( "data-subdivision-step" , / 1 | 2 / ) ;
949+ await expect ( page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell-- playhead-active" ) ) . not . toHaveAttribute ( "data-step-index" , beforeStep || "" ) ;
950+ await expect ( page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell-- playhead-active" ) ) . toHaveAttribute ( "data-beat" , / 1 | 2 / ) ;
951+ await expect ( page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell-- playhead-active" ) ) . toHaveAttribute ( "data-subdivision-step" , / 1 | 2 / ) ;
886952 expect ( await page . evaluate ( ( ) => window . __midiStudioPreviewSynthEvents . some ( ( event ) => event . action === "oscillator-start" ) ) ) . toBe ( true ) ;
887953 await page . locator ( "#stopTimingPreviewButton" ) . click ( ) ;
888954 await expect ( page . locator ( "#instrumentGridTransportState" ) ) . toContainText ( "Preview Synth timing preview stopped." ) ;
@@ -905,11 +971,11 @@ Am F`);
905971 pad : ""
906972 } ) ;
907973 await page . locator ( "#normalizeInstrumentGridButton" ) . click ( ) ;
908- const beforeStep = await page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) . getAttribute ( "data-step-index" ) ;
974+ const beforeStep = await page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell-- playhead-active" ) . getAttribute ( "data-step-index" ) ;
909975 await page . locator ( "#playSectionButton" ) . click ( ) ;
910976 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / F A I L N o p l a y a b l e P r e v i e w S y n t h n o t e s f o u n d f o r s e c t i o n i n t r o \. G e n e r a t e o r e n t e r c h o r d s , b a s s , p a d , l e a d , o r d r u m c e l l s b e f o r e p l a y i n g \. / ) ;
911977 await expect ( page . locator ( "#audioDiagnostics" ) ) . toContainText ( "No playable Preview Synth notes found for section intro." ) ;
912- await expect ( page . locator ( ".midi-studio-v2__grid-cell--playhead-active" ) ) . toHaveAttribute ( "data-step-index" , beforeStep || "0" ) ;
978+ await expect ( page . locator ( ".midi-studio-v2__grid-cell--timing-header.midi-studio-v2__grid-cell-- playhead-active" ) ) . toHaveAttribute ( "data-step-index" , beforeStep || "0" ) ;
913979 expect ( await page . evaluate ( ( ) => window . __midiStudioPreviewSynthEvents . some ( ( event ) => event . action === "oscillator-start" ) ) ) . toBe ( false ) ;
914980 } finally {
915981 await workspaceV2CoverageReporter . stop ( page ) ;
@@ -946,16 +1012,16 @@ Am F`);
9461012 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K P r e v i e w i n s t r u m e n t s e l e c t e d f o r L e a d : R e t r o P u l s e L e a d \. / ) ;
9471013 await page . locator ( "#normalizeInstrumentGridButton" ) . click ( ) ;
9481014
949- await page . locator ( "#previewMuteBassToggle" ) . check ( ) ;
1015+ await setSpreadsheetRowToggle ( page , "Bass" , "mute" , true ) ;
9501016 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / W A R N L a n e m u t e d : B a s s \. / ) ;
9511017 await page . locator ( "#playSectionButton" ) . click ( ) ;
9521018 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 \. / ) ;
9531019 await expect ( page . locator ( '.midi-studio-v2__grid-cell--lane-active[data-lane="bass"]' ) ) . toHaveCount ( 0 ) ;
9541020 expect ( ( await audioDiagnosticsRows ( page ) ) [ "Muted lanes" ] ) . toBe ( "bass" ) ;
9551021 await page . locator ( "#stopTimingPreviewButton" ) . click ( ) ;
9561022
957- await page . locator ( "#previewMuteBassToggle" ) . uncheck ( ) ;
958- await page . locator ( "#previewSoloLeadToggle" ) . check ( ) ;
1023+ await setSpreadsheetRowToggle ( page , "Bass" , "mute" , false ) ;
1024+ await setSpreadsheetRowToggle ( page , "Lead" , "solo" , true ) ;
9591025 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K L a n e s o l o e d : L e a d \. / ) ;
9601026 await page . locator ( "#playSectionButton" ) . click ( ) ;
9611027 await expect ( page . locator ( ".midi-studio-v2__grid-cell--lane-active" ) ) . not . toHaveCount ( 0 ) ;
@@ -967,7 +1033,7 @@ Am F`);
9671033 expect ( soloDiagnostics [ "Soloed lanes" ] ) . toBe ( "lead" ) ;
9681034 await page . locator ( "#stopTimingPreviewButton" ) . click ( ) ;
9691035
970- await page . locator ( "#previewSoloLeadToggle" ) . uncheck ( ) ;
1036+ await setSpreadsheetRowToggle ( page , "Lead" , "solo" , false ) ;
9711037 await page . locator ( "#previewInstrumentLeadSelect" ) . selectOption ( "" ) ;
9721038 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / W A R N M i s s i n g p r e v i e w i n s t r u m e n t s e l e c t i o n f o r L e a d \. C h o o s e a P r e v i e w S y n t h i n s t r u m e n t b e f o r e p l a y b a c k \. / ) ;
9731039 await page . locator ( "#playSectionButton" ) . click ( ) ;
0 commit comments