@@ -1135,6 +1135,14 @@ test.describe("Workspace Manager V2 bootstrap", () => {
11351135 await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . not . toHaveValue ( / T e x t t o S p e e c h V 2 d e f a u l t q u e u e | L o a d e d 3 s c h e m a - c o m p l e t e / ) ;
11361136 const emptySummary = JSON . parse ( await page . locator ( "#text2speech-V2SpeechSummary" ) . textContent ( ) ) ;
11371137 expect ( emptySummary ) . toEqual ( [ ] ) ;
1138+ await page . locator ( "#text2speech-V2AddItemButton" ) . click ( ) ;
1139+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / F A I L T e x t t o S p e e c h V 2 A d d b l o c k e d : N a m e i s r e q u i r e d b e f o r e c r e a t i n g a n a m e d s p e e c h i t e m \. / ) ;
1140+ await expect ( page . locator ( "#text2speech-V2QueueTiles [data-speech-item-id]" ) ) . toHaveCount ( 0 ) ;
1141+ await page . locator ( "#text2speech-V2SpeechItemName" ) . fill ( "Draft empty state line" ) ;
1142+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . not . toHaveValue ( / F A I L T e x t t o S p e e c h V 2 n a m e u p d a t e f a i l e d : n o n a m e d s p e e c h i t e m i s s e l e c t e d \. / ) ;
1143+ await page . locator ( "#text2speech-V2AddItemButton" ) . click ( ) ;
1144+ await expect ( page . locator ( "#text2speech-V2QueueTiles [data-speech-item-id]" ) ) . toHaveCount ( 1 ) ;
1145+ await expect ( page . locator ( '[data-speech-item-id="draft-empty-state-line"] .text2speech-V2__queue-tile-name' ) ) . toHaveText ( "Draft empty state line" ) ;
11381146 expect ( pageErrors ) . toEqual ( [ ] ) ;
11391147 } finally {
11401148 await coverageReporter . stop ( page ) ;
@@ -1431,7 +1439,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14311439 } ) ;
14321440 expect ( schemaRequiredFields . required ) . toEqual ( REQUIRED_TEXT2SPEECH_OPTION_FIELDS ) ;
14331441 expect ( schemaRequiredFields . rootType ) . toBe ( "array" ) ;
1434- expect ( schemaRequiredFields . rootMinItems ) . toBe ( 1 ) ;
1442+ expect ( schemaRequiredFields . rootMinItems ) . toBeUndefined ( ) ;
14351443 expect ( schemaRequiredFields . rootItemsRef ) . toBe ( "#/$defs/speechQueueItem" ) ;
14361444 expect ( schemaRequiredFields . assetManagerSchemaType ) . toBe ( "object" ) ;
14371445 expect ( schemaRequiredFields . paletteManagerSchemaType ) . toBe ( "object" ) ;
@@ -1468,6 +1476,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14681476 length : TEXT_TO_SPEECH_SAMPLE_ITEM_IDS . length ,
14691477 validation : { ok : true }
14701478 } ) ;
1479+ expect ( await page . evaluate ( ( ) => window [ "__text2speech-V2App" ] . validatePayload ( [ ] , "empty Text to Speech V2 root array" ) ) ) . toEqual ( { ok : true } ) ;
14711480 const validationSourceCleanup = await page . evaluate ( async ( ) => {
14721481 const source = await fetch ( "/tools/text2speech-V2/js/TextToSpeechToolApp.js" , { cache : "no-store" } ) . then ( ( response ) => response . text ( ) ) ;
14731482 return {
@@ -1768,6 +1777,16 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17681777 } ) ;
17691778
17701779 test ( "deletes the last named sentence into a safe empty runtime state" , async ( { page } ) => {
1780+ await page . addInitScript ( ( ) => {
1781+ Object . defineProperty ( navigator , "clipboard" , {
1782+ configurable : true ,
1783+ value : {
1784+ writeText : async ( text ) => {
1785+ window . __text2speechV2CopiedJson = text ;
1786+ }
1787+ }
1788+ } ) ;
1789+ } ) ;
17711790 const server = await openTextToSpeechTool ( page , TEXT_TO_SPEECH_SAMPLE_QUERY ) ;
17721791 const pageErrors = [ ] ;
17731792
@@ -1788,9 +1807,11 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17881807 await expect ( page . locator ( "#text2speech-V2ResumeButton" ) ) . toBeDisabled ( ) ;
17891808 await expect ( page . locator ( "#text2speech-V2StopButton" ) ) . toBeDisabled ( ) ;
17901809 expect ( JSON . parse ( await page . locator ( "#text2speech-V2SpeechSummary" ) . textContent ( ) ) ) . toEqual ( [ ] ) ;
1791- await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / F A I L T e x t t o S p e e c h V 2 e m p t y s t a t e : a d d a n a m e d s p e e c h i t e m b e f o r e p l a y b a c k , e x p o r t , c o p y , o r w o r k s p a c e s a v e \. / ) ;
1810+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / O K T e x t t o S p e e c h V 2 e m p t y s t a t e : 0 n a m e d s p e e c h i t e m s \. A d d a N a m e a n d c l i c k A d d t o c r e a t e a n e w i t e m \. / ) ;
1811+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . not . toHaveValue ( / m u s t c o n t a i n a t l e a s t 1 i t e m | b e f o r e p l a y b a c k , e x p o r t , c o p y , o r w o r k s p a c e s a v e | n a m e u p d a t e f a i l e d / ) ;
17921812 await page . locator ( "#text2speech-V2CopyJsonButton" ) . click ( ) ;
1793- await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / F A I L C o p y J S O N b l o c k e d : T e x t t o S p e e c h V 2 p a y l o a d f r o m T e x t t o S p e e c h V 2 c u r r e n t U I p a y l o a d f a i l e d t o o l s \/ s c h e m a s \/ t o o l s \/ t e x t 2 s p e e c h - V 2 \. s c h e m a \. j s o n v a l i d a t i o n : r o o t : m u s t c o n t a i n a t l e a s t 1 i t e m / ) ;
1813+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / O K C o p i e d T e x t t o S p e e c h V 2 J S O N r o o t a r r a y t o c l i p b o a r d \( 0 i t e m s \) \. / ) ;
1814+ expect ( await page . evaluate ( ( ) => JSON . parse ( window . __text2speechV2CopiedJson ) ) ) . toEqual ( [ ] ) ;
17941815 expect ( pageErrors ) . toEqual ( [ ] ) ;
17951816 } finally {
17961817 await coverageReporter . stop ( page ) ;
@@ -2112,7 +2133,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
21122133 } ;
21132134 } ) ;
21142135 expect ( schemaContract . rootType ) . toBe ( "array" ) ;
2115- expect ( schemaContract . rootMinItems ) . toBe ( 1 ) ;
2136+ expect ( schemaContract . rootMinItems ) . toBeUndefined ( ) ;
21162137 expect ( schemaContract . rootItemsRef ) . toBe ( "#/$defs/speechQueueItem" ) ;
21172138 expect ( schemaContract . itemAdditionalProperties ) . toBe ( false ) ;
21182139 expect ( schemaContract . required ) . toEqual ( REQUIRED_TEXT2SPEECH_OPTION_FIELDS ) ;
@@ -2160,10 +2181,15 @@ test.describe("Workspace Manager V2 bootstrap", () => {
21602181 } ;
21612182 const oldRootObjectResult = await app . contextService . validateGeneratedManifest ( oldRootObjectContext ) ;
21622183 cases . push ( { label : "old-root-object" , ok : oldRootObjectResult . ok , message : oldRootObjectResult . message } ) ;
2184+ const emptyArrayContext = structuredClone ( context ) ;
2185+ emptyArrayContext . tools [ "text2speech-V2" ] = [ ] ;
2186+ const emptyArrayResult = await app . contextService . validateGeneratedManifest ( emptyArrayContext ) ;
2187+ cases . push ( { label : "empty-array" , ok : emptyArrayResult . ok , message : emptyArrayResult . message } ) ;
21632188 await validate ( "valid-base" , ( ) => { } ) ;
21642189 return cases ;
21652190 } , TEXT_TO_SPEECH_SAMPLE_PRESET_PATH ) ;
21662191 expect ( validationMessages . find ( ( entry ) => entry . label === "valid-base" ) ) . toMatchObject ( { ok : true } ) ;
2192+ expect ( validationMessages . find ( ( entry ) => entry . label === "empty-array" ) ) . toMatchObject ( { ok : true } ) ;
21672193 expect ( validationMessages . find ( ( entry ) => entry . label === "old-root-object" ) . message ) . toMatch ( / r o o t \. t o o l s \. t e x t 2 s p e e c h - V 2 : e x p e c t e d a r r a y / ) ;
21682194 expect ( validationMessages . find ( ( entry ) => entry . label === "removed-fields" ) . message ) . toMatch ( / r o o t \. t o o l s \. t e x t 2 s p e e c h - V 2 \[ 0 \] \. a u t o S p e a k i s n o t a l l o w e d / ) ;
21692195 expect ( validationMessages . find ( ( entry ) => entry . label === "removed-fields" ) . message ) . toMatch ( / r o o t \. t o o l s \. t e x t 2 s p e e c h - V 2 \[ 0 \] \. q u e u e M o d e i s n o t a l l o w e d / ) ;
@@ -3690,6 +3716,119 @@ test.describe("Workspace Manager V2 bootstrap", () => {
36903716 }
36913717 } ) ;
36923718
3719+ test ( "saves empty Text to Speech V2 arrays through workspace return and manifest write-back" , async ( { page } ) => {
3720+ const server = await openWorkspaceManagerV2 ( page ) ;
3721+ const pageErrors = [ ] ;
3722+
3723+ page . on ( "pageerror" , ( error ) => {
3724+ pageErrors . push ( error . message ) ;
3725+ } ) ;
3726+
3727+ try {
3728+ await installMockSpeechSynthesis ( page ) ;
3729+ await selectMockRepo ( page ) ;
3730+ await page . locator ( "#activeGameSelect" ) . selectOption ( "Asteroids" ) ;
3731+ await expectWorkspaceReturnRehydrated ( page ) ;
3732+ const seededWorkspace = await page . evaluate ( async ( samplePresetPath ) => {
3733+ const app = window . __workspaceManagerV2App ;
3734+ const samplePayload = await fetch ( samplePresetPath , { cache : "no-store" } ) . then ( ( response ) => response . json ( ) ) ;
3735+ const payload = [ structuredClone ( samplePayload [ 0 ] ) ] ;
3736+ const context = structuredClone ( app . activeContext ) ;
3737+ context . tools [ "text2speech-V2" ] = payload ;
3738+ const validation = await app . contextService . validateGeneratedManifest ( context ) ;
3739+ const hostContextId = app . contextService . writePersistedContext ( app . activeHostContextId , context ) ;
3740+ const metrics = app . contextSummaryMetrics ( context ) ;
3741+ app . applyContextResult ( {
3742+ assetCount : metrics . assetCount ,
3743+ context,
3744+ game : app . activeGame ,
3745+ hostContextId,
3746+ paletteSwatches : metrics . paletteSwatches
3747+ } , { requiresRepoHandle : app . activeToolStateRequiresRepoHandle } ) ;
3748+ return { hostContextId, validation } ;
3749+ } , TEXT_TO_SPEECH_SAMPLE_PRESET_PATH ) ;
3750+ expect ( seededWorkspace . validation ) . toEqual ( { ok : true } ) ;
3751+ expect ( await page . evaluate ( async ( ) => {
3752+ const app = window . __workspaceManagerV2App ;
3753+ const context = structuredClone ( app . activeContext ) ;
3754+ context . tools [ "text2speech-V2" ] = [ ] ;
3755+ return await app . contextService . validateGeneratedManifest ( context ) ;
3756+ } ) ) . toEqual ( { ok : true } ) ;
3757+
3758+ await page . locator ( '[data-workspace-tool-id="text2speech-V2"]' ) . click ( ) ;
3759+ await expect ( page ) . toHaveURL ( / t e x t 2 s p e e c h - V 2 \/ i n d e x \. h t m l .* l a u n c h = w o r k s p a c e / ) ;
3760+ await expect ( page . locator ( "#text2speech-V2QueueTiles [data-speech-item-id]" ) ) . toHaveCount ( 1 ) ;
3761+ await page . locator ( "#text2speech-V2DeleteItemButton" ) . click ( ) ;
3762+ await expect ( page . locator ( "#text2speech-V2QueueTiles [data-speech-item-id]" ) ) . toHaveCount ( 0 ) ;
3763+ expect ( JSON . parse ( await page . locator ( "#text2speech-V2SpeechSummary" ) . textContent ( ) ) ) . toEqual ( [ ] ) ;
3764+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / O K T e x t t o S p e e c h V 2 e m p t y s t a t e : 0 n a m e d s p e e c h i t e m s \. A d d a N a m e a n d c l i c k A d d t o c r e a t e a n e w i t e m \. / ) ;
3765+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / O K T e x t t o S p e e c h V 2 d i r t y s t a t e : t r u e ; r e a s o n = s p e e c h - i t e m - d e l e t e d ; c h a n g e d K e y s = q u e u e ; q u e u e = 0 \. / ) ;
3766+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . not . toHaveValue ( / s c h e m a r e q u i r e s a t l e a s t o n e n a m e d s p e e c h i t e m | n a m e u p d a t e f a i l e d / ) ;
3767+
3768+ await page . locator ( "#returnToWorkspaceButton" ) . click ( ) ;
3769+ await expect ( page ) . toHaveURL ( new RegExp ( `workspace-manager-v2/index\\.html\\?hostContextId=${ seededWorkspace . hostContextId } ` ) ) ;
3770+ await expectWorkspaceReturnedFromTool ( page , { dirty : true } ) ;
3771+ const returnedState = await page . evaluate ( ( ) => {
3772+ const session = JSON . parse ( sessionStorage . getItem ( "workspace.tools.text2speech-V2" ) ) ;
3773+ const outputContext = JSON . parse ( document . querySelector ( "#workspaceContextOutput" ) . value ) ;
3774+ return {
3775+ activePayload : window . __workspaceManagerV2App . activeContext . tools [ "text2speech-V2" ] ,
3776+ outputPayload : outputContext . tools [ "text2speech-V2" ] ,
3777+ sessionData : session . data ,
3778+ sessionDirty : session . dirty
3779+ } ;
3780+ } ) ;
3781+ expect ( returnedState . activePayload ) . toEqual ( [ ] ) ;
3782+ expect ( returnedState . outputPayload ) . toEqual ( [ ] ) ;
3783+ expect ( returnedState . sessionData ) . toEqual ( [ ] ) ;
3784+ expect ( returnedState . sessionDirty ) . toMatchObject ( {
3785+ isDirty : true ,
3786+ reason : "speech-item-deleted" ,
3787+ changedKeys : [ "queue" ]
3788+ } ) ;
3789+
3790+ await page . locator ( "#saveWorkspaceButton" ) . click ( ) ;
3791+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K S a v e d a n d m a r k e d c l e a n : w o r k s p a c e \. t o o l s \. t e x t 2 s p e e c h - V 2 \. / ) ;
3792+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / I N F O S a v e d T e x t t o S p e e c h V 2 p a y l o a d c o u n t : 0 \. / ) ;
3793+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / I N F O S a v e d t o o l S t a t e i t e m s : 4 \( a s s e t - m a n a g e r - v 2 a s s e t s = 1 4 ; p a l e t t e - m a n a g e r - v 2 s w a t c h e s = 1 0 ; t e x t 2 s p e e c h - V 2 q u e u e = 0 ; v e c t o r - m a p - e d i t o r v e c t o r s = 5 \) \. / ) ;
3794+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K S a v e v a l i d a t i o n r e s u l t : g a m e m a n i f e s t v a l i d ; r o o t g a m e \. w o r k s p a c e t o o l S t a t e v a l i d ; s a v e d c o n t e x t m a t c h e d r e - r e a d f i l e \. / ) ;
3795+ const savedState = await page . evaluate ( ( hostContextId ) => {
3796+ const writes = JSON . parse ( sessionStorage . getItem ( "workspace.repo.manifestWrites" ) || "[]" ) ;
3797+ return {
3798+ activePayload : window . __workspaceManagerV2App . activeContext . tools [ "text2speech-V2" ] ,
3799+ manifest : JSON . parse ( writes . at ( - 1 ) . contents ) ,
3800+ savedContext : JSON . parse ( sessionStorage . getItem ( hostContextId ) ) ,
3801+ session : JSON . parse ( sessionStorage . getItem ( "workspace.tools.text2speech-V2" ) ) ,
3802+ writePath : writes . at ( - 1 ) . path
3803+ } ;
3804+ } , seededWorkspace . hostContextId ) ;
3805+ expect ( savedState . writePath ) . toBe ( "HTML-JavaScript-Gaming/games/Asteroids/game.manifest.json" ) ;
3806+ expect ( savedState . activePayload ) . toEqual ( [ ] ) ;
3807+ expect ( savedState . savedContext . tools [ "text2speech-V2" ] ) . toEqual ( [ ] ) ;
3808+ expect ( savedState . session . data ) . toEqual ( [ ] ) ;
3809+ expect ( savedState . session . dirty ) . toEqual ( {
3810+ isDirty : false ,
3811+ reason : null ,
3812+ changedAt : null ,
3813+ changedKeys : [ ]
3814+ } ) ;
3815+ expect ( savedState . manifest . game . workspace . tools [ "text2speech-V2" ] ) . toEqual ( [ ] ) ;
3816+
3817+ await page . locator ( '[data-workspace-tool-id="text2speech-V2"]' ) . click ( ) ;
3818+ await expect ( page ) . toHaveURL ( / t e x t 2 s p e e c h - V 2 \/ i n d e x \. h t m l .* l a u n c h = w o r k s p a c e / ) ;
3819+ await expect ( page . locator ( "#text2speech-V2QueueTiles [data-speech-item-id]" ) ) . toHaveCount ( 0 ) ;
3820+ await expect ( page . locator ( "#text2speech-V2SpeakButton" ) ) . toBeDisabled ( ) ;
3821+ expect ( JSON . parse ( await page . locator ( "#text2speech-V2SpeechSummary" ) . textContent ( ) ) ) . toEqual ( [ ] ) ;
3822+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / O K T e x t t o S p e e c h V 2 s c h e m a v a l i d a t i o n r e s u l t : t o o l s \/ s c h e m a s \/ t o o l s \/ t e x t 2 s p e e c h - V 2 \. s c h e m a \. j s o n v a l i d ; q u e u e = 0 \. / ) ;
3823+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . toHaveValue ( / O K L o a d e d 0 s c h e m a - c o m p l e t e T e x t t o S p e e c h V 2 q u e u e i t e m s \. / ) ;
3824+ await expect ( page . locator ( "#text2speech-V2StatusLog" ) ) . not . toHaveValue ( / q u e u e s e l e c t i o n f a i l e d | s c h e m a r e q u i r e s a t l e a s t o n e n a m e d s p e e c h i t e m / ) ;
3825+ expect ( pageErrors ) . toEqual ( [ ] ) ;
3826+ } finally {
3827+ await coverageReporter . stop ( page ) ;
3828+ await server . close ( ) ;
3829+ }
3830+ } ) ;
3831+
36933832 test ( "keeps Preview Generator V2 repo writer after Asset Manager V2 deletes the preview asset entry" , async ( { page } ) => {
36943833 const server = await openWorkspaceManagerV2 ( page ) ;
36953834 const pageErrors = [ ] ;
0 commit comments