@@ -298,6 +298,50 @@ async function openMidiStudioForImport(page, audioOptions = {}) {
298298 return server ;
299299}
300300
301+ async function openMidiStudioFromWorkspace ( page , manifestPayload , audioOptions = { } ) {
302+ const server = await startRepoServer ( ) ;
303+ const hostContextId = "midi-studio-v2-workspace-context" ;
304+ await installMockAudio ( page , audioOptions ) ;
305+ await page . addInitScript ( ( { hostContextId : contextId , manifest } ) => {
306+ const toolId = "midi-studio-v2" ;
307+ const toolSettings = manifest . tools ?. [ toolId ] || { } ;
308+ const payload = {
309+ schema : "html-js-gaming.midi-studio-v2" ,
310+ toolId,
311+ version : manifest . music ?. version || 1 ,
312+ runtimePreference : manifest . music ?. runtimePreference || "rendered" ,
313+ activeSongId : toolSettings . activeSongId || manifest . music ?. activeSongId || manifest . music ?. songs ?. [ 0 ] ?. id || "" ,
314+ directorMode : toolSettings . directorMode || manifest . music ?. directorMode || { } ,
315+ songs : manifest . music ?. songs || [ ]
316+ } ;
317+ window . sessionStorage . setItem ( contextId , JSON . stringify ( {
318+ ...manifest ,
319+ tools : {
320+ ...( manifest . tools || { } ) ,
321+ [ toolId ] : payload
322+ }
323+ } ) ) ;
324+ window . sessionStorage . setItem ( `workspace.tools.${ toolId } ` , JSON . stringify ( {
325+ schema : { name : "workspace-tool-state" , version : 1 } ,
326+ workspace : {
327+ hostContextId : contextId ,
328+ source : "workspace-manager-v2" ,
329+ toolId
330+ } ,
331+ data : payload ,
332+ dirty : {
333+ isDirty : false ,
334+ reason : "clean" ,
335+ changedAt : "" ,
336+ changedKeys : [ ]
337+ }
338+ } ) ) ;
339+ } , { hostContextId, manifest : manifestPayload } ) ;
340+ await workspaceV2CoverageReporter . start ( page ) ;
341+ await page . goto ( `${ server . baseUrl } /tools/midi-studio-v2/index.html?launch=workspace&fromTool=workspace-manager-v2&hostContextId=${ hostContextId } &workspaceMode=uat` , { waitUntil : "domcontentloaded" } ) ;
342+ return server ;
343+ }
344+
301345async function selectMidiStudioTab ( page , tabId ) {
302346 const tab = page . locator ( `[data-midi-studio-tab="${ tabId } "]` ) ;
303347 if ( await tab . getAttribute ( "aria-selected" ) === "true" ) {
@@ -1028,6 +1072,52 @@ test.describe("MIDI Studio V2", () => {
10281072 }
10291073 } ) ;
10301074
1075+ test ( "separates Workspace launch save ownership from Tool Mode standalone save" , async ( { page } ) => {
1076+ const manifest = JSON . parse ( await fs . readFile ( uatManifestPath , "utf8" ) ) ;
1077+ const server = await openMidiStudioFromWorkspace ( page , manifest ) ;
1078+ try {
1079+ await expect ( page . locator ( "#launchModeIndicator" ) ) . toHaveText ( "Workspace Mode" ) ;
1080+ await expect ( page . locator ( "body" ) ) . toHaveAttribute ( "data-midi-studio-launch-mode" , "workspace" ) ;
1081+ await expect ( page . locator ( '[data-launch-mode-nav="workspace"]' ) ) . toBeVisible ( ) ;
1082+ await expect ( page . locator ( "#returnToWorkspaceButton" ) ) . toBeVisible ( ) ;
1083+ await expect ( page . locator ( "#saveProjectButton" ) ) . toBeHidden ( ) ;
1084+ await expect ( page . locator ( "#toolImportManifestButton" ) ) . toBeHidden ( ) ;
1085+ await expect ( page . locator ( "#renderedExportSaveButton" ) ) . toBeHidden ( ) ;
1086+ await expect ( page . locator ( "#workspaceImportManifestButton" ) ) . toBeHidden ( ) ;
1087+ await expect ( page . locator ( "#workspaceCopyManifestButton" ) ) . toBeHidden ( ) ;
1088+ await expect ( page . locator ( "#workspaceExportManifestButton" ) ) . toBeHidden ( ) ;
1089+
1090+ await selectInstrumentRow ( page , "lead" ) ;
1091+ await octaveCell ( page , "C6" , 2 ) . click ( ) ;
1092+ await expect ( octaveCell ( page , "C6" , 2 ) ) . toHaveAttribute ( "data-note-lanes" , / l e a d / ) ;
1093+ const workspaceState = await page . evaluate ( ( ) => {
1094+ const session = JSON . parse ( window . sessionStorage . getItem ( "workspace.tools.midi-studio-v2" ) ) ;
1095+ const context = JSON . parse ( window . sessionStorage . getItem ( "midi-studio-v2-workspace-context" ) ) ;
1096+ const sessionSong = session . data . songs . find ( ( song ) => song . id === session . data . activeSongId ) ;
1097+ const contextSong = context . tools [ "midi-studio-v2" ] . songs . find ( ( song ) => song . id === context . tools [ "midi-studio-v2" ] . activeSongId ) ;
1098+ return {
1099+ contextLead : contextSong . studioArrangement . lanes . lead ,
1100+ dirty : session . dirty ,
1101+ sessionLead : sessionSong . studioArrangement . lanes . lead
1102+ } ;
1103+ } ) ;
1104+ expect ( workspaceState . sessionLead ) . toContain ( "C6" ) ;
1105+ expect ( workspaceState . contextLead ) . toContain ( "C6" ) ;
1106+ expect ( workspaceState . dirty ) . toMatchObject ( {
1107+ isDirty : true ,
1108+ reason : "midi-studio-note-grid-edited"
1109+ } ) ;
1110+ expect ( workspaceState . dirty . changedKeys ) . toEqual ( expect . arrayContaining ( [ "data.songs.studioArrangement" ] ) ) ;
1111+
1112+ await page . locator ( "#returnToWorkspaceButton" ) . click ( ) ;
1113+ await expect ( page ) . toHaveURL ( / w o r k s p a c e - m a n a g e r - v 2 \/ i n d e x \. h t m l .* h o s t C o n t e x t I d = m i d i - s t u d i o - v 2 - w o r k s p a c e - c o n t e x t / ) ;
1114+ await expect ( page ) . toHaveURL ( / w o r k s p a c e = u a t / ) ;
1115+ } finally {
1116+ await workspaceV2CoverageReporter . stop ( page ) ;
1117+ await server . close ( ) ;
1118+ }
1119+ } ) ;
1120+
10311121 test ( "octave timeline freezes compact headers and note labels while active cells stay textless" , async ( { page } ) => {
10321122 const server = await openMidiStudioForImport ( page ) ;
10331123 try {
@@ -1780,6 +1870,8 @@ test.describe("MIDI Studio V2", () => {
17801870 const server = await openMidiStudio ( page ) ;
17811871 try {
17821872 await expect ( page . locator ( "body" ) ) . toHaveAttribute ( "data-tool-id" , "midi-studio-v2" ) ;
1873+ await expect ( page . locator ( "body" ) ) . toHaveAttribute ( "data-midi-studio-launch-mode" , "tool" ) ;
1874+ await expect ( page . locator ( "#launchModeIndicator" ) ) . toHaveText ( "Tool Mode" ) ;
17831875 await expect ( page . locator ( "[data-midi-studio-header]" ) ) . toContainText ( "MIDI Studio V2" ) ;
17841876 await expect ( page . locator ( "#songList [data-song-id]" ) ) . toHaveCount ( 3 ) ;
17851877 await expect ( page . locator ( '[data-song-id="theme-main"]' ) ) . toHaveAttribute ( "aria-pressed" , "true" ) ;
@@ -1796,11 +1888,13 @@ test.describe("MIDI Studio V2", () => {
17961888 await expect ( page . locator ( "#exportMp3Button" ) ) . toHaveCount ( 0 ) ;
17971889 await expect ( page . locator ( "#exportOggButton" ) ) . toHaveCount ( 0 ) ;
17981890 await expect ( page . locator ( ".midi-studio-v2__tool-menu #toolImportManifestButton" ) ) . toBeVisible ( ) ;
1891+ await expect ( page . locator ( ".midi-studio-v2__tool-menu #saveProjectButton" ) ) . toBeVisible ( ) ;
17991892 await expect ( page . locator ( ".midi-studio-v2__tool-menu #loadExampleAndPlayButton" ) ) . toHaveCount ( 0 ) ;
18001893 await expect ( page . locator ( ".midi-studio-v2__tool-menu #stopAllAudioButton" ) ) . toBeVisible ( ) ;
1801- await expect ( page . locator ( '.midi-studio-v2__tool-menu label[for="renderedExportTargetTypeSelect"]' ) ) . toContainText ( "Type" ) ;
1894+ await expect ( page . locator ( '.midi-studio-v2__tool-menu label[for="renderedExportTargetTypeSelect"]' ) ) . toContainText ( "Output Type" ) ;
18021895 await expect ( page . locator ( ".midi-studio-v2__tool-menu #renderedExportTargetTypeSelect" ) ) . toBeVisible ( ) ;
18031896 await expect ( page . locator ( ".midi-studio-v2__tool-menu #renderedExportSaveButton" ) ) . toBeVisible ( ) ;
1897+ await expect ( page . locator ( ".midi-studio-v2__tool-menu #renderedExportSaveButton" ) ) . toHaveText ( "Save Output" ) ;
18041898 await expect ( page . locator ( "#midiSourceDetails" ) ) . toContainText ( "No MIDI source inspected." ) ;
18051899 await expect ( page . locator ( "#audioDiagnosticsContent" ) ) . toBeHidden ( ) ;
18061900 await expect ( page . locator ( "#playbackState" ) ) . toContainText ( "Audible preview ready: Main Theme." ) ;
@@ -1925,7 +2019,7 @@ test.describe("MIDI Studio V2", () => {
19252019 }
19262020 } ) ;
19272021
1928- test ( "exports through Type dropdown and Save without claiming files were written " , async ( { page } ) => {
2022+ test ( "exports output through Type dropdown and Save Output without claiming project save " , async ( { page } ) => {
19292023 const server = await openMidiStudio ( page ) ;
19302024 try {
19312025 await expect ( page . locator ( "#exportWavButton" ) ) . toHaveCount ( 0 ) ;
@@ -1934,6 +2028,7 @@ test.describe("MIDI Studio V2", () => {
19342028 await expect ( page . locator ( "#renderedExportTargetTypeSelect" ) ) . toBeVisible ( ) ;
19352029 await expect ( page . locator ( "#renderedExportTargetTypeSelect option" ) ) . toContainText ( [ "WAV" , "MP3" , "OGG" ] ) ;
19362030 await expect ( page . locator ( "#renderedExportSaveButton" ) ) . toBeVisible ( ) ;
2031+ await expect ( page . locator ( "#renderedExportSaveButton" ) ) . toHaveText ( "Save Output" ) ;
19372032 const exportControlsFit = await page . locator ( ".midi-studio-v2__tool-menu" ) . evaluate ( ( menu ) => {
19382033 const label = menu . querySelector ( 'label[for="renderedExportTargetTypeSelect"]' ) . getBoundingClientRect ( ) ;
19392034 const typeSelect = menu . querySelector ( "#renderedExportTargetTypeSelect" ) . getBoundingClientRect ( ) ;
@@ -1955,6 +2050,7 @@ test.describe("MIDI Studio V2", () => {
19552050 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / W A R N E x p o r t r e n d e r i n g n o t i m p l e m e n t e d f o r W A V \. P l a n n e d t a r g e t : a s s e t s \/ m u s i c \/ r e n d e r e d \/ t h e m e - m a i n \. w a v \. / ) ;
19562051 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / W A R N E x p o r t r e n d e r i n g n o t i m p l e m e n t e d f o r M P 3 \. P l a n n e d t a r g e t : a s s e t s \/ m u s i c \/ r e n d e r e d \/ t h e m e - m a i n \. m p 3 \. / ) ;
19572052 await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / W A R N E x p o r t r e n d e r i n g n o t i m p l e m e n t e d f o r O G G \. P l a n n e d t a r g e t : a s s e t s \/ m u s i c \/ r e n d e r e d \/ t h e m e - m a i n \. o g g \. / ) ;
2053+ await expect ( page . locator ( "#statusLog" ) ) . not . toHaveValue ( / S a v e P r o j e c t c o m p l e t e d / ) ;
19582054 await page . locator ( '[data-song-id="source-only"]' ) . click ( ) ;
19592055 await page . locator ( "#renderedExportTargetTypeSelect" ) . selectOption ( "wav" ) ;
19602056 await page . locator ( "#renderedExportSaveButton" ) . click ( ) ;
0 commit comments