@@ -585,6 +585,7 @@ async function visibleMidiStudioControlOwnership(page, activeTabId) {
585585 addSongButton : { canonical : "music.songs[] / tools.midi-studio-v2.activeSongId" , kind : "canonical-action" , owner : "Song Setup" , wired : "wired" } ,
586586 clearStatusButton : { canonical : "diagnostic status log" , kind : "action" , owner : "Diagnostics" , wired : "wired" } ,
587587 closeInstrumentPanelButton : { canonical : "accordion view state" , kind : "view-state" , owner : "Instruments" , wired : "wired" } ,
588+ duplicateInstrumentRowButton : { canonical : "music.songs[].studioArrangement.lanes / previewLaneSettings" , kind : "canonical-action" , owner : "Octave Timeline" , wired : "wired" } ,
588589 futureAutosaveButton : { canonical : "future editing history" , kind : "unwired" , owner : "Song Setup" , wired : "unwired" } ,
589590 futureEnableMidiInputButton : { canonical : "future MIDI input" , kind : "unwired" , owner : "MIDI Import" , wired : "unwired" } ,
590591 futureMidiDeviceSelect : { canonical : "future MIDI input" , kind : "unwired" , owner : "MIDI Import" , wired : "unwired" } ,
@@ -643,6 +644,8 @@ async function visibleMidiStudioControlOwnership(page, activeTabId) {
643644 toolCopyJsonButton : { canonical : "serialized midi-studio-v2 tool state" , kind : "action" , owner : "Diagnostics" , wired : "wired" } ,
644645 toolExportToolStateButton : { canonical : "serialized midi-studio-v2 tool state" , kind : "action" , owner : "Export" , wired : "wired" } ,
645646 toolImportManifestButton : { canonical : "imported game manifest / midi-studio-v2 payload" , kind : "canonical-action" , owner : "Global NAV" , wired : "wired" } ,
647+ timelineAddInstrumentRowButton : { canonical : "music.songs[].studioArrangement.lanes / previewLaneSettings" , kind : "canonical-action" , owner : "Octave Timeline" , wired : "wired" } ,
648+ timelineCloseInstrumentPanelButton : { canonical : "accordion view state" , kind : "view-state" , owner : "Octave Timeline" , wired : "wired" } ,
646649 workspaceCopyManifestButton : { canonical : "workspace manifest proxy" , kind : "unwired" , owner : "Workspace NAV" , wired : "unwired" } ,
647650 workspaceExportManifestButton : { canonical : "workspace manifest proxy" , kind : "unwired" , owner : "Workspace NAV" , wired : "unwired" } ,
648651 workspaceImportManifestButton : { canonical : "workspace manifest proxy" , kind : "unwired" , owner : "Workspace NAV" , wired : "unwired" }
@@ -3040,10 +3043,9 @@ test.describe("MIDI Studio V2", () => {
30403043 await instrumentSelect ( page , "bass" ) . selectOption ( "gm-electric-bass-finger" ) ;
30413044 await setInputValue ( page , "#previewVolumeBassInput" , "0.65" ) ;
30423045 await setInputValue ( page , "#previewPanBassInput" , "-0.4" ) ;
3043- await setCheckboxValue ( page , "#previewMuteBassToggle" , true ) ;
3044- expect ( await page . evaluate ( ( ) => window . __midiStudioV2App . selectedSong ( ) . studioArrangement . previewLaneSettings . muted . bass ) ) . toBe ( true ) ;
3045- await setCheckboxValue ( page , "#previewMuteBassToggle" , false ) ;
3046- await setCheckboxValue ( page , "#previewSoloBassToggle" , true ) ;
3046+ await expect ( editor ) . not . toContainText ( "Mute default" ) ;
3047+ await expect ( editor ) . not . toContainText ( "Solo default" ) ;
3048+ await expect ( editor . locator ( "#previewMuteBassToggle, #previewSoloBassToggle" ) ) . toHaveCount ( 0 ) ;
30473049 await setInputValue ( page , "#previewOctaveLowBassInput" , "2" ) ;
30483050 await setInputValue ( page , "#previewOctaveHighBassInput" , "4" ) ;
30493051 await setInputValue ( page , "#previewTransposeBassInput" , "12" ) ;
@@ -3058,11 +3060,9 @@ test.describe("MIDI Studio V2", () => {
30583060 duration : settings . durations . bass ,
30593061 instrument : settings . instruments . bass ,
30603062 instrumentType : settings . instrumentTypes . bass ,
3061- muted : settings . muted . bass ,
30623063 octaveRange : settings . octaveRanges . bass ,
30633064 pan : settings . pans . bass ,
30643065 selectedInstrumentId : app . instrumentGrid . selectedInstrumentId ,
3065- soloed : settings . soloed . bass ,
30663066 transpose : settings . transposes . bass ,
30673067 velocity : settings . velocities . bass ,
30683068 volume : settings . volumes . bass
@@ -3072,11 +3072,9 @@ test.describe("MIDI Studio V2", () => {
30723072 duration : 1.5 ,
30733073 instrument : "gm-electric-bass-finger" ,
30743074 instrumentType : "Bass" ,
3075- muted : false ,
30763075 octaveRange : { high : 4 , low : 2 } ,
30773076 pan : - 0.4 ,
30783077 selectedInstrumentId : "bass" ,
3079- soloed : true ,
30803078 transpose : 12 ,
30813079 velocity : 96 ,
30823080 volume : 0.65
@@ -3518,10 +3516,8 @@ test.describe("MIDI Studio V2", () => {
35183516 "music.songs[].studioArrangement.previewLaneSettings.durations" ,
35193517 "music.songs[].studioArrangement.previewLaneSettings.instruments" ,
35203518 "music.songs[].studioArrangement.previewLaneSettings.instrumentTypes" ,
3521- "music.songs[].studioArrangement.previewLaneSettings.muted" ,
35223519 "music.songs[].studioArrangement.previewLaneSettings.octaveRanges" ,
35233520 "music.songs[].studioArrangement.previewLaneSettings.pans" ,
3524- "music.songs[].studioArrangement.previewLaneSettings.soloed" ,
35253521 "music.songs[].studioArrangement.previewLaneSettings.transposes" ,
35263522 "music.songs[].studioArrangement.previewLaneSettings.velocities" ,
35273523 "music.songs[].studioArrangement.previewLaneSettings.volumes" ,
@@ -3560,19 +3556,17 @@ test.describe("MIDI Studio V2", () => {
35603556
35613557 await selectMidiStudioTab ( page , "instruments" ) ;
35623558 await setInputValue ( page , "#previewVolumeLeadInput" , "0.8" ) ;
3563- await setCheckboxValue ( page , "#previewMuteLeadToggle" , true ) ;
3564- await setCheckboxValue ( page , "#previewSoloLeadToggle" , true ) ;
35653559 expect ( await page . evaluate ( ( ) => {
35663560 const settings = window . __midiStudioV2App . selectedSong ( ) . studioArrangement . previewLaneSettings ;
35673561 return {
3568- muted : settings . muted . lead ,
3569- soloed : settings . soloed . lead ,
35703562 volume : settings . volumes . lead
35713563 } ;
3572- } ) ) . toEqual ( { muted : true , soloed : true , volume : 0.8 } ) ;
3564+ } ) ) . toEqual ( { volume : 0.8 } ) ;
35733565
35743566 await selectMidiStudioTab ( page , "studio" ) ;
35753567 await waitForCanvasRender ( page ) ;
3568+ await timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-timeline-quick-mute='lead']" ) . click ( ) ;
3569+ await timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-timeline-quick-solo='lead']" ) . click ( ) ;
35763570 await expect ( timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-timeline-quick-mute='lead']" ) ) . toHaveAttribute ( "aria-pressed" , "true" ) ;
35773571 await expect ( timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-timeline-quick-solo='lead']" ) ) . toHaveAttribute ( "aria-pressed" , "true" ) ;
35783572 await timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-timeline-quick-mute='lead']" ) . click ( ) ;
@@ -3601,6 +3595,142 @@ test.describe("MIDI Studio V2", () => {
36013595 }
36023596 } ) ;
36033597
3598+ test ( "keeps PR066 timeline Instruments compact and duplicates selected instruments" , async ( { page } ) => {
3599+ await page . setViewportSize ( { width : 1600 , height : 900 } ) ;
3600+ const server = await openMidiStudioForImport ( page ) ;
3601+ try {
3602+ await page . locator ( "#toolImportManifestInput" ) . setInputFiles ( uatManifestPath ) ;
3603+
3604+ await selectMidiStudioTab ( page , "instruments" ) ;
3605+ await selectInstrumentRow ( page , "lead" ) ;
3606+ await page . locator ( "#previewDisplayNameLeadInput" ) . fill ( "Lead Copy Source" ) ;
3607+ await instrumentTypeSelect ( page , "lead" ) . selectOption ( "Bass" ) ;
3608+ await instrumentSelect ( page , "lead" ) . selectOption ( "gm-electric-bass-finger" ) ;
3609+ await setInputValue ( page , "#previewVolumeLeadInput" , "0.6" ) ;
3610+ await setInputValue ( page , "#previewPanLeadInput" , "-0.3" ) ;
3611+ await expect ( page . locator ( "#selectedInstrumentEditor" ) ) . not . toContainText ( "Mute default" ) ;
3612+ await expect ( page . locator ( "#selectedInstrumentEditor" ) ) . not . toContainText ( "Solo default" ) ;
3613+ await expect ( page . locator ( "#selectedInstrumentEditor [id^='previewMute'], #selectedInstrumentEditor [id^='previewSolo']" ) ) . toHaveCount ( 0 ) ;
3614+
3615+ const beforeDuplicate = await page . evaluate ( ( ) => {
3616+ const app = window . __midiStudioV2App ;
3617+ const song = app . selectedSong ( ) ;
3618+ const settings = song . studioArrangement . previewLaneSettings ;
3619+ return {
3620+ leadLane : song . studioArrangement . lanes . lead ,
3621+ settings : {
3622+ displayName : settings . displayNames . lead ,
3623+ instrument : settings . instruments . lead ,
3624+ instrumentType : settings . instrumentTypes . lead ,
3625+ pan : settings . pans . lead ,
3626+ volume : settings . volumes . lead
3627+ }
3628+ } ;
3629+ } ) ;
3630+ expect ( beforeDuplicate . settings ) . toEqual ( {
3631+ displayName : "Lead Copy Source" ,
3632+ instrument : "gm-electric-bass-finger" ,
3633+ instrumentType : "Bass" ,
3634+ pan : - 0.3 ,
3635+ volume : 0.6
3636+ } ) ;
3637+
3638+ await selectMidiStudioTab ( page , "studio" ) ;
3639+ await waitForCanvasRender ( page ) ;
3640+ await expect ( page . locator ( ".midi-studio-v2__timeline-instrument-accordion" ) ) . toBeVisible ( ) ;
3641+ const timelineInstrumentLayout = await page . locator ( ".midi-studio-v2__timeline-instrument-accordion" ) . evaluate ( ( section ) => {
3642+ const content = section . querySelector ( "#timelineInstrumentQuickContent" ) ;
3643+ const rows = Array . from ( section . querySelectorAll ( ".midi-studio-v2__quick-instrument-row" ) ) ;
3644+ const header = section . querySelector ( ".midi-studio-v2__timeline-instrument-accordion-header" ) ;
3645+ const duplicate = header . querySelector ( "#duplicateInstrumentRowButton" ) ;
3646+ const add = header . querySelector ( "#timelineAddInstrumentRowButton" ) ;
3647+ const close = header . querySelector ( "#timelineCloseInstrumentPanelButton" ) ;
3648+ const duplicateRect = duplicate . getBoundingClientRect ( ) ;
3649+ const addRect = add . getBoundingClientRect ( ) ;
3650+ const closeRect = close . getBoundingClientRect ( ) ;
3651+ const rowRects = rows . map ( ( row ) => row . getBoundingClientRect ( ) ) ;
3652+ return {
3653+ addAfterDuplicate : addRect . left > duplicateRect . right ,
3654+ closeAfterAdd : closeRect . left > addRect . right ,
3655+ contentMarginBottom : getComputedStyle ( content ) . marginBottom ,
3656+ contentMarginTop : getComputedStyle ( content ) . marginTop ,
3657+ duplicateText : duplicate . textContent . trim ( ) ,
3658+ rowHeights : rowRects . map ( ( rect ) => rect . height ) ,
3659+ sectionMarginBottom : getComputedStyle ( section ) . marginBottom ,
3660+ sectionMarginTop : getComputedStyle ( section ) . marginTop
3661+ } ;
3662+ } ) ;
3663+ expect ( timelineInstrumentLayout . sectionMarginTop ) . toBe ( "10px" ) ;
3664+ expect ( timelineInstrumentLayout . sectionMarginBottom ) . toBe ( "10px" ) ;
3665+ expect ( timelineInstrumentLayout . contentMarginTop ) . toBe ( "10px" ) ;
3666+ expect ( timelineInstrumentLayout . contentMarginBottom ) . toBe ( "10px" ) ;
3667+ expect ( timelineInstrumentLayout . duplicateText ) . toBe ( "" ) ;
3668+ expect ( timelineInstrumentLayout . addAfterDuplicate ) . toBe ( true ) ;
3669+ expect ( timelineInstrumentLayout . closeAfterAdd ) . toBe ( true ) ;
3670+ expect ( timelineInstrumentLayout . rowHeights . every ( ( height ) => height <= 28 ) ) . toBe ( true ) ;
3671+ await expect ( page . locator ( "#duplicateInstrumentRowButton" ) ) . toHaveAttribute ( "aria-label" , "Duplicate selected instrument" ) ;
3672+ await expect ( page . locator ( "#duplicateInstrumentRowButton" ) ) . toHaveAttribute ( "title" , "Duplicate selected instrument" ) ;
3673+
3674+ await timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-timeline-quick-mute='lead']" ) . click ( ) ;
3675+ await timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-timeline-quick-solo='lead']" ) . click ( ) ;
3676+ await timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-toggle-instrument-visibility='lead']" ) . click ( ) ;
3677+ await page . locator ( "#duplicateInstrumentRowButton" ) . click ( ) ;
3678+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K D u p l i c a t e d i n s t r u m e n t r o w L e a d a s L e a d C o p y ; p l a y b a c k d a t a u p d a t e d \. / ) ;
3679+
3680+ const duplicateState = await page . evaluate ( ( leadLaneSource ) => {
3681+ const app = window . __midiStudioV2App ;
3682+ const song = app . selectedSong ( ) ;
3683+ const settings = song . studioArrangement . previewLaneSettings ;
3684+ const selected = app . instrumentGrid . selectedInstrumentId ;
3685+ return {
3686+ copiedLaneData : song . studioArrangement . lanes [ selected ] === leadLaneSource ,
3687+ duplicateDisplayName : settings . displayNames [ selected ] ,
3688+ duplicateInstrument : settings . instruments [ selected ] ,
3689+ duplicateInstrumentType : settings . instrumentTypes [ selected ] ,
3690+ duplicatePan : settings . pans [ selected ] ,
3691+ duplicateVisible : settings . visible [ selected ] ,
3692+ duplicateMuted : settings . muted [ selected ] ,
3693+ duplicateSoloed : settings . soloed [ selected ] ,
3694+ duplicateVolume : settings . volumes [ selected ] ,
3695+ hasUniqueId : selected !== "lead" && Object . hasOwn ( song . studioArrangement . lanes , selected ) ,
3696+ selected
3697+ } ;
3698+ } , beforeDuplicate . leadLane ) ;
3699+ expect ( duplicateState ) . toEqual ( {
3700+ copiedLaneData : true ,
3701+ duplicateDisplayName : "Lead Copy Source Copy" ,
3702+ duplicateInstrument : "gm-electric-bass-finger" ,
3703+ duplicateInstrumentType : "Bass" ,
3704+ duplicatePan : - 0.3 ,
3705+ duplicateVisible : false ,
3706+ duplicateMuted : true ,
3707+ duplicateSoloed : true ,
3708+ duplicateVolume : 0.6 ,
3709+ hasUniqueId : true ,
3710+ selected : "lead-copy"
3711+ } ) ;
3712+ await expect ( timelineQuickInstrumentRow ( page , "lead-copy" ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
3713+
3714+ await selectMidiStudioTab ( page , "instruments" ) ;
3715+ await expect ( instrumentRow ( page , "lead-copy" ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
3716+ await expect ( page . locator ( "#selectedInstrumentEditor" ) ) . not . toContainText ( "Mute default" ) ;
3717+ await expect ( page . locator ( "#selectedInstrumentEditor" ) ) . not . toContainText ( "Solo default" ) ;
3718+
3719+ await selectMidiStudioTab ( page , "studio" ) ;
3720+ await timelineQuickInstrumentRow ( page , "lead-copy" ) . locator ( "[data-timeline-quick-mute='lead-copy']" ) . click ( ) ;
3721+ await timelineQuickInstrumentRow ( page , "lead-copy" ) . locator ( "[data-toggle-instrument-visibility='lead-copy']" ) . click ( ) ;
3722+ await page . locator ( "#playButton" ) . click ( ) ;
3723+ await expect ( page . locator ( "#playButton" ) ) . toBeDisabled ( ) ;
3724+ await expect ( page . locator ( "#stopButton" ) ) . toBeEnabled ( ) ;
3725+ await page . locator ( "#stopButton" ) . click ( ) ;
3726+ await expect ( page . locator ( "#stopButton" ) ) . toBeDisabled ( ) ;
3727+ await expect ( page . locator ( "#playButton" ) ) . toBeEnabled ( ) ;
3728+ } finally {
3729+ await workspaceV2CoverageReporter . stop ( page ) ;
3730+ await server . close ( ) ;
3731+ }
3732+ } ) ;
3733+
36043734 test ( "derives primary song, instrument, grid, playback, and diagnostics views from the canonical selected song" , async ( { page } ) => {
36053735 const server = await openMidiStudioForImport ( page ) ;
36063736 try {
0 commit comments