@@ -621,6 +621,8 @@ async function visibleMidiStudioControlOwnership(page, activeTabId) {
621621 instrumentSetField : { canonical : "music.songs[].instrumentSet" , kind : "readonly" , owner : "MIDI Import" , wired : "wired" } ,
622622 jumpToSectionButton : { canonical : "timing preview section state" , kind : "workflow-state" , owner : "Octave Timeline" , wired : "wired" } ,
623623 loopToggle : { canonical : "playback loop state" , kind : "workflow-state" , owner : "Octave Timeline" , wired : "wired" } ,
624+ moveInstrumentDownButton : { canonical : "music.songs[].studioArrangement.lanes order" , kind : "canonical-action" , owner : "Octave Timeline" , wired : "wired" } ,
625+ moveInstrumentUpButton : { canonical : "music.songs[].studioArrangement.lanes order" , kind : "canonical-action" , owner : "Octave Timeline" , wired : "wired" } ,
624626 normalizeInstrumentGridButton : { canonical : "music.songs[].studioArrangement" , kind : "canonical-action" , owner : "Auto-Create Parts" , wired : "wired" } ,
625627 parseSongSheetButton : { canonical : "music.songs[].studioArrangement" , kind : "canonical-action" , owner : "Song Setup" , wired : "wired" } ,
626628 playButton : { canonical : "playback from selected canonical song model" , kind : "action" , owner : "Global NAV" , wired : "wired" } ,
@@ -3675,7 +3677,7 @@ test.describe("MIDI Studio V2", () => {
36753677 await timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-timeline-quick-solo='lead']" ) . click ( ) ;
36763678 await timelineQuickInstrumentRow ( page , "lead" ) . locator ( "[data-toggle-instrument-visibility='lead']" ) . click ( ) ;
36773679 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 \. / ) ;
3680+ 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 1 ; p l a y b a c k d a t a u p d a t e d \. / ) ;
36793681
36803682 const duplicateState = await page . evaluate ( ( leadLaneSource ) => {
36813683 const app = window . __midiStudioV2App ;
@@ -3698,7 +3700,7 @@ test.describe("MIDI Studio V2", () => {
36983700 } , beforeDuplicate . leadLane ) ;
36993701 expect ( duplicateState ) . toEqual ( {
37003702 copiedLaneData : true ,
3701- duplicateDisplayName : "Lead Copy Source Copy " ,
3703+ duplicateDisplayName : "Lead 1 " ,
37023704 duplicateInstrument : "gm-electric-bass-finger" ,
37033705 duplicateInstrumentType : "Bass" ,
37043706 duplicatePan : - 0.3 ,
@@ -3707,18 +3709,18 @@ test.describe("MIDI Studio V2", () => {
37073709 duplicateSoloed : true ,
37083710 duplicateVolume : 0.6 ,
37093711 hasUniqueId : true ,
3710- selected : "lead-copy "
3712+ selected : "lead-1 "
37113713 } ) ;
3712- await expect ( timelineQuickInstrumentRow ( page , "lead-copy " ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
3714+ await expect ( timelineQuickInstrumentRow ( page , "lead-1 " ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
37133715
37143716 await selectMidiStudioTab ( page , "instruments" ) ;
3715- await expect ( instrumentRow ( page , "lead-copy " ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
3717+ await expect ( instrumentRow ( page , "lead-1 " ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
37163718 await expect ( page . locator ( "#selectedInstrumentEditor" ) ) . not . toContainText ( "Mute default" ) ;
37173719 await expect ( page . locator ( "#selectedInstrumentEditor" ) ) . not . toContainText ( "Solo default" ) ;
37183720
37193721 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 timelineQuickInstrumentRow ( page , "lead-1 " ) . locator ( "[data-timeline-quick-mute='lead-1 ']" ) . click ( ) ;
3723+ await timelineQuickInstrumentRow ( page , "lead-1 " ) . locator ( "[data-toggle-instrument-visibility='lead-1 ']" ) . click ( ) ;
37223724 await page . locator ( "#playButton" ) . click ( ) ;
37233725 await expect ( page . locator ( "#playButton" ) ) . toBeDisabled ( ) ;
37243726 await expect ( page . locator ( "#stopButton" ) ) . toBeEnabled ( ) ;
@@ -3731,6 +3733,158 @@ test.describe("MIDI Studio V2", () => {
37313733 }
37323734 } ) ;
37333735
3736+ test ( "manages PR067 instrument duplication ordering and guarded deletion" , async ( { page } ) => {
3737+ await page . setViewportSize ( { width : 1600 , height : 900 } ) ;
3738+ const server = await openMidiStudioForImport ( page ) ;
3739+ try {
3740+ await page . locator ( "#toolImportManifestInput" ) . setInputFiles ( uatManifestPath ) ;
3741+
3742+ await selectMidiStudioTab ( page , "instruments" ) ;
3743+ await selectInstrumentRow ( page , "lead" ) ;
3744+ await instrumentTypeSelect ( page , "lead" ) . selectOption ( "Bass" ) ;
3745+ await instrumentSelect ( page , "lead" ) . selectOption ( "gm-electric-bass-finger" ) ;
3746+ await setInputValue ( page , "#previewVolumeLeadInput" , "0.62" ) ;
3747+ await setInputValue ( page , "#previewPanLeadInput" , "-0.25" ) ;
3748+ await setInputValue ( page , "#previewTransposeLeadInput" , "7" ) ;
3749+ await setInputValue ( page , "#previewVelocityLeadInput" , "91" ) ;
3750+ await setInputValue ( page , "#previewDurationLeadInput" , "1.4" ) ;
3751+ await page . evaluate ( ( ) => {
3752+ const app = window . __midiStudioV2App ;
3753+ const state = app . instrumentGrid . previewLaneState . lead ;
3754+ state . effects = { ...state . effects , reverb : "future-room" } ;
3755+ state . advanced = { ...state . advanced , midiChannel : 3 } ;
3756+ app . syncSelectedArrangementFromGridInput ( app . instrumentGrid . readInput ( ) ) ;
3757+ } ) ;
3758+ const leadState = await page . evaluate ( ( ) => {
3759+ const app = window . __midiStudioV2App ;
3760+ const song = app . selectedSong ( ) ;
3761+ const settings = song . studioArrangement . previewLaneSettings ;
3762+ return {
3763+ advanced : settings . advanced . lead ,
3764+ effects : settings . effects . lead ,
3765+ laneText : song . studioArrangement . lanes . lead ,
3766+ settings : {
3767+ duration : settings . durations . lead ,
3768+ instrument : settings . instruments . lead ,
3769+ instrumentType : settings . instrumentTypes . lead ,
3770+ pan : settings . pans . lead ,
3771+ transpose : settings . transposes . lead ,
3772+ velocity : settings . velocities . lead ,
3773+ volume : settings . volumes . lead
3774+ }
3775+ } ;
3776+ } ) ;
3777+
3778+ await selectMidiStudioTab ( page , "studio" ) ;
3779+ await waitForCanvasRender ( page ) ;
3780+ await page . locator ( "#duplicateInstrumentRowButton" ) . click ( ) ;
3781+ 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 1 ; p l a y b a c k d a t a u p d a t e d \. / ) ;
3782+ await expect ( timelineQuickInstrumentRow ( page , "lead-1" ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
3783+ await expect ( timelineQuickInstrumentRow ( page , "lead-1" ) ) . toHaveAttribute ( "data-duplicate-confirmation" , "true" ) ;
3784+
3785+ const duplicateState = await page . evaluate ( ( expectedLeadState ) => {
3786+ const app = window . __midiStudioV2App ;
3787+ const song = app . selectedSong ( ) ;
3788+ const settings = song . studioArrangement . previewLaneSettings ;
3789+ const selected = app . instrumentGrid . selectedInstrumentId ;
3790+ return {
3791+ copiedAdvanced : settings . advanced [ selected ] ,
3792+ copiedEffects : settings . effects [ selected ] ,
3793+ copiedLaneText : song . studioArrangement . lanes [ selected ] === expectedLeadState . laneText ,
3794+ copiedSettings : {
3795+ duration : settings . durations [ selected ] ,
3796+ instrument : settings . instruments [ selected ] ,
3797+ instrumentType : settings . instrumentTypes [ selected ] ,
3798+ pan : settings . pans [ selected ] ,
3799+ transpose : settings . transposes [ selected ] ,
3800+ velocity : settings . velocities [ selected ] ,
3801+ volume : settings . volumes [ selected ]
3802+ } ,
3803+ displayName : settings . displayNames [ selected ] ,
3804+ hasUniqueId : selected === "lead-1" && Object . hasOwn ( song . studioArrangement . lanes , selected ) ,
3805+ order : Object . keys ( song . studioArrangement . lanes ) ,
3806+ selected
3807+ } ;
3808+ } , leadState ) ;
3809+ expect ( duplicateState ) . toEqual ( {
3810+ copiedAdvanced : leadState . advanced ,
3811+ copiedEffects : leadState . effects ,
3812+ copiedLaneText : true ,
3813+ copiedSettings : leadState . settings ,
3814+ displayName : "Lead 1" ,
3815+ hasUniqueId : true ,
3816+ order : expect . arrayContaining ( [ "lead" , "lead-1" ] ) ,
3817+ selected : "lead-1"
3818+ } ) ;
3819+ const duplicateOrder = duplicateState . order ;
3820+ const duplicateIndex = duplicateOrder . indexOf ( "lead-1" ) ;
3821+ expect ( duplicateIndex ) . toBeGreaterThan ( 0 ) ;
3822+ expect ( duplicateOrder [ duplicateIndex - 1 ] ) . toBe ( "lead" ) ;
3823+
3824+ await selectMidiStudioTab ( page , "instruments" ) ;
3825+ await expect ( instrumentRow ( page , "lead-1" ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
3826+ await selectMidiStudioTab ( page , "studio" ) ;
3827+ await page . locator ( "#moveInstrumentUpButton" ) . click ( ) ;
3828+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K M o v e d i n s t r u m e n t r o w L e a d 1 u p ; c a n o n i c a l o r d e r u p d a t e d \. / ) ;
3829+ const orderAfterMoveUp = await page . evaluate ( ( ) => Object . keys ( window . __midiStudioV2App . selectedSong ( ) . studioArrangement . lanes ) ) ;
3830+ expect ( orderAfterMoveUp . indexOf ( "lead-1" ) ) . toBe ( duplicateIndex - 1 ) ;
3831+ await page . locator ( "#moveInstrumentDownButton" ) . click ( ) ;
3832+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K M o v e d i n s t r u m e n t r o w L e a d 1 d o w n ; c a n o n i c a l o r d e r u p d a t e d \. / ) ;
3833+ const orderAfterMoveDown = await page . evaluate ( ( ) => Object . keys ( window . __midiStudioV2App . selectedSong ( ) . studioArrangement . lanes ) ) ;
3834+ expect ( orderAfterMoveDown ) . toEqual ( duplicateOrder ) ;
3835+ await expect ( timelineQuickInstrumentRow ( page , "lead-1" ) ) . toHaveClass ( / i s - s e l e c t e d / ) ;
3836+ const expectedSelectionAfterDelete = orderAfterMoveDown [ orderAfterMoveDown . indexOf ( "lead-1" ) + 1 ]
3837+ || orderAfterMoveDown [ orderAfterMoveDown . indexOf ( "lead-1" ) - 1 ]
3838+ || "" ;
3839+
3840+ await page . locator ( "#playButton" ) . click ( ) ;
3841+ await expect ( page . locator ( "#playButton" ) ) . toBeDisabled ( ) ;
3842+ await expect ( page . locator ( "#stopButton" ) ) . toBeEnabled ( ) ;
3843+ await page . locator ( "#stopButton" ) . click ( ) ;
3844+ await expect ( page . locator ( "#stopButton" ) ) . toBeDisabled ( ) ;
3845+ await expect ( page . locator ( "#playButton" ) ) . toBeEnabled ( ) ;
3846+
3847+ await selectMidiStudioTab ( page , "instruments" ) ;
3848+ await instrumentRow ( page , "lead-1" ) . locator ( "[data-delete-instrument-row='lead-1']" ) . click ( ) ;
3849+ await expect ( page . locator ( "[data-delete-confirmation-lane='lead-1']" ) ) . toBeVisible ( ) ;
3850+ expect ( await page . evaluate ( ( ) => Object . hasOwn ( window . __midiStudioV2App . selectedSong ( ) . studioArrangement . lanes , "lead-1" ) ) ) . toBe ( true ) ;
3851+ await page . locator ( "[data-confirm-delete-instrument-row='lead-1']" ) . click ( ) ;
3852+ await expect ( instrumentRow ( page , "lead-1" ) ) . toHaveCount ( 0 ) ;
3853+ const afterDelete = await page . evaluate ( ( ) => {
3854+ const app = window . __midiStudioV2App ;
3855+ return {
3856+ hasDeletedLane : Object . hasOwn ( app . selectedSong ( ) . studioArrangement . lanes , "lead-1" ) ,
3857+ selected : app . instrumentGrid . selectedInstrumentId
3858+ } ;
3859+ } ) ;
3860+ expect ( afterDelete ) . toEqual ( { hasDeletedLane : false , selected : expectedSelectionAfterDelete } ) ;
3861+
3862+ await page . evaluate ( ( ) => {
3863+ const app = window . __midiStudioV2App ;
3864+ const song = app . selectedSong ( ) ;
3865+ const settings = song . studioArrangement . previewLaneSettings ;
3866+ song . studioArrangement . lanes = { lead : song . studioArrangement . lanes . lead } ;
3867+ Object . keys ( settings ) . forEach ( ( key ) => {
3868+ if ( settings [ key ] && typeof settings [ key ] === "object" && ! Array . isArray ( settings [ key ] ) ) {
3869+ settings [ key ] = { lead : settings [ key ] . lead } ;
3870+ }
3871+ } ) ;
3872+ song . studioArrangement . previewInstruments = { lead : settings . instruments . lead } ;
3873+ app . applySelectedSongArrangement ( "final instrument delete guard" ) ;
3874+ } ) ;
3875+ await selectMidiStudioTab ( page , "instruments" ) ;
3876+ await expect ( instrumentRow ( page , "lead" ) ) . toHaveCount ( 1 ) ;
3877+ await instrumentRow ( page , "lead" ) . locator ( "[data-delete-instrument-row='lead']" ) . click ( ) ;
3878+ await expect ( page . locator ( "[data-delete-blocked-lane='lead']" ) ) . toBeVisible ( ) ;
3879+ await expect ( page . locator ( "[data-delete-blocked-lane='lead']" ) ) . toContainText ( "Final instrument cannot be deleted" ) ;
3880+ expect ( await page . evaluate ( ( ) => Object . keys ( window . __midiStudioV2App . selectedSong ( ) . studioArrangement . lanes ) ) ) . toEqual ( [ "lead" ] ) ;
3881+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / W A R N F i n a l i n s t r u m e n t c a n n o t b e d e l e t e d : L e a d \. / ) ;
3882+ } finally {
3883+ await workspaceV2CoverageReporter . stop ( page ) ;
3884+ await server . close ( ) ;
3885+ }
3886+ } ) ;
3887+
37343888 test ( "derives primary song, instrument, grid, playback, and diagnostics views from the canonical selected song" , async ( { page } ) => {
37353889 const server = await openMidiStudioForImport ( page ) ;
37363890 try {
0 commit comments