11import { expect , test } from "@playwright/test" ;
2- import { readFile } from "node:fs/promises" ;
2+ import { readFile , writeFile } from "node:fs/promises" ;
33import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs" ;
44import { workspaceV2CoverageReporter as coverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs" ;
55
@@ -1142,10 +1142,16 @@ test.describe("Workspace Manager V2 bootstrap", () => {
11421142 await expect ( page . locator ( "[data-tool-starter-header]" ) ) . toContainText ( tool . description ) ;
11431143 await expect ( page . locator ( ".tool-starter__tool__menu" ) ) . toBeVisible ( ) ;
11441144 await expect ( page . locator ( ".tool-starter__workspace__menu" ) ) . toBeHidden ( ) ;
1145- await page . locator ( "#sourceInput" ) . fill ( `${ tool . name } launch coverage` ) ;
1146- await page . locator ( "#toolExportButton" ) . click ( ) ;
1147- await expect ( page . locator ( "#inspectorOutput" ) ) . toContainText ( `"toolId": "${ tool . id } "` ) ;
1148- await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( new RegExp ( `Processed source value: ${ tool . name } launch coverage` ) ) ;
1145+ if ( tool . id === "world-vector-studio-v2" ) {
1146+ await page . locator ( "#sourceInput" ) . fill ( `${ tool . name } launch coverage` ) ;
1147+ await page . locator ( "#toolExportButton" ) . click ( ) ;
1148+ await expect ( page . locator ( "#inspectorOutput" ) ) . toContainText ( `"toolId": "${ tool . id } "` ) ;
1149+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( new RegExp ( `Processed source value: ${ tool . name } launch coverage` ) ) ;
1150+ } else {
1151+ await expect ( page . locator ( '[data-launch-mode-nav="tool"] button' ) ) . toHaveText ( [ "Import" , "Copy JSON" , "Export" ] ) ;
1152+ await expect ( page . locator ( "#objectVectorStudioV2LoadStatus" ) ) . toContainText ( "Schema-only loading is idle" ) ;
1153+ await expect ( page . locator ( "#objectVectorStudioV2ObjectTiles" ) ) . toContainText ( "No objects loaded" ) ;
1154+ }
11491155 }
11501156 expect ( pageErrors ) . toEqual ( [ ] ) ;
11511157 } finally {
@@ -1154,6 +1160,138 @@ test.describe("Workspace Manager V2 bootstrap", () => {
11541160 }
11551161 } ) ;
11561162
1163+ test ( "shows Object Vector Studio V2 layout shell and schema-only palette gate" , async ( { page } , testInfo ) => {
1164+ const server = await startRepoServer ( ) ;
1165+ const pageErrors = [ ] ;
1166+
1167+ page . on ( "pageerror" , ( error ) => {
1168+ pageErrors . push ( error . message ) ;
1169+ } ) ;
1170+
1171+ await coverageReporter . start ( page ) ;
1172+ try {
1173+ await page . goto ( `${ server . baseUrl } /tools/object-vector-studio-v2/index.html` , { waitUntil : "networkidle" } ) ;
1174+ await expect ( page . locator ( "body.tools-platform-tool-page[data-tool-id='object-vector-studio-v2']" ) ) . toBeVisible ( ) ;
1175+ await expect ( page . locator ( "[data-tool-starter-header]" ) ) . toContainText ( "Object Vector Studio V2" ) ;
1176+ await expect ( page . locator ( '[data-launch-mode-nav="tool"]' ) ) . toBeVisible ( ) ;
1177+ await expect ( page . locator ( '[data-launch-mode-nav="tool"] button' ) ) . toHaveText ( [ "Import" , "Copy JSON" , "Export" ] ) ;
1178+ await expect ( page . locator ( '[data-launch-mode-nav="workspace"]' ) ) . toBeHidden ( ) ;
1179+ await expect ( page . locator ( "#objectVectorStudioV2CopyJsonButton" ) ) . toBeDisabled ( ) ;
1180+ await expect ( page . locator ( "#objectVectorStudioV2ExportJsonButton" ) ) . toBeDisabled ( ) ;
1181+
1182+ await expect ( page . locator ( ".tool-starter__panel--left > .accordion-v2 > .accordion-v2__header > span:first-child" ) ) . toHaveText ( [ "Object" , "Shape/Tools" , "Objects" ] ) ;
1183+ await expect ( page . locator ( ".tool-starter__panel--right > .accordion-v2 > .accordion-v2__header > span:first-child" ) ) . toHaveText ( [ "Palette" , "Object Details" , "JSON Details" , "Status Log" ] ) ;
1184+ await expect ( page . locator ( "#objectVectorStudioV2PaletteGate" ) ) . toHaveValue ( "Required before render" ) ;
1185+ await expect ( page . locator ( "#objectVectorStudioV2ObjectTiles" ) ) . toContainText ( "No objects loaded" ) ;
1186+ await expect ( page . locator ( ".object-vector-studio-v2__tool-toggle" ) ) . toHaveText ( [ "+ Select" , "Sh Shape" , "P Path" , "M Move" , "R Rotate" , "G Group" ] ) ;
1187+
1188+ await page . locator ( '[data-shape-tool="path"]' ) . click ( ) ;
1189+ await expect ( page . locator ( '[data-shape-tool="path"]' ) ) . toHaveAttribute ( "aria-pressed" , "true" ) ;
1190+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / I N F O S h a p e \/ T o o l s s h e l l t o g g l e s e l e c t e d : p a t h / ) ;
1191+
1192+ const invalidPayloadPath = testInfo . outputPath ( "object-vector-invalid.json" ) ;
1193+ await writeFile ( invalidPayloadPath , JSON . stringify ( { objects : [ ] } , null , 2 ) , "utf8" ) ;
1194+ await page . locator ( "#objectVectorStudioV2ImportJsonInput" ) . setInputFiles ( invalidPayloadPath ) ;
1195+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / F A I L O b j e c t V e c t o r S t u d i o V 2 s c h e m a v a l i d a t i o n f a i l e d f r o m i m p o r t : o b j e c t - v e c t o r - i n v a l i d \. j s o n : r o o t \. p a l e t t e i s r e q u i r e d \. / ) ;
1196+ await expect ( page . locator ( "#objectVectorStudioV2ObjectTiles" ) ) . toContainText ( "No objects loaded" ) ;
1197+
1198+ const validPayload = {
1199+ palette : {
1200+ id : "arcade-primary" ,
1201+ swatches : [
1202+ { id : "white" , value : "#ffffff" } ,
1203+ { id : "cyan" , value : "#6fd3ff" }
1204+ ]
1205+ } ,
1206+ objects : Array . from ( { length : 18 } , ( _ , index ) => ( {
1207+ id : `object-${ index + 1 } ` ,
1208+ name : index === 0 ? "Asteroids Ship" : `Object ${ index + 1 } ` ,
1209+ type : index === 1 ? "enemy" : "ship"
1210+ } ) )
1211+ } ;
1212+ const validPayloadPath = testInfo . outputPath ( "object-vector-valid.json" ) ;
1213+ await writeFile ( validPayloadPath , JSON . stringify ( validPayload , null , 2 ) , "utf8" ) ;
1214+ await page . locator ( "#objectVectorStudioV2ImportJsonInput" ) . setInputFiles ( validPayloadPath ) ;
1215+ await expect ( page . locator ( "#objectVectorStudioV2PaletteGate" ) ) . toHaveValue ( "Palette loaded" ) ;
1216+ await expect ( page . locator ( "#objectVectorStudioV2PaletteSummary" ) ) . toContainText ( "Palette arcade-primary: 2 swatches." ) ;
1217+ await expect ( page . locator ( "#objectVectorStudioV2ObjectTiles .object-vector-studio-v2__object-tile" ) ) . toHaveCount ( 18 ) ;
1218+ await expect ( page . locator ( "#objectVectorStudioV2ObjectDetails" ) ) . toContainText ( "Asteroids Ship" ) ;
1219+ await expect ( page . locator ( "#objectVectorStudioV2SelectedItemVisibility" ) ) . toContainText ( "Selected item visible: Asteroids Ship" ) ;
1220+ await expect ( page . locator ( "#objectVectorStudioV2JsonDetails" ) ) . toContainText ( '"palette"' ) ;
1221+ await expect ( page . locator ( "#objectVectorStudioV2CopyJsonButton" ) ) . toBeEnabled ( ) ;
1222+ await expect ( page . locator ( "#objectVectorStudioV2ExportJsonButton" ) ) . toBeEnabled ( ) ;
1223+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / O K L o a d e d O b j e c t V e c t o r S t u d i o V 2 s c h e m a p a y l o a d f r o m i m p o r t : o b j e c t - v e c t o r - v a l i d \. j s o n : 1 8 o b j e c t s , 2 p a l e t t e s w a t c h e s \. / ) ;
1224+
1225+ await page . locator ( '[data-object-id="object-2"]' ) . click ( ) ;
1226+ await expect ( page . locator ( '[data-object-id="object-2"]' ) ) . toHaveAttribute ( "aria-pressed" , "true" ) ;
1227+ await expect ( page . locator ( "#objectVectorStudioV2ObjectDetails" ) ) . toContainText ( "Object 2" ) ;
1228+
1229+ const leftOpenHeights = await page . locator ( ".tool-starter__panel--left .accordion-v2.is-open" ) . evaluateAll ( ( sections ) => (
1230+ sections . map ( ( section ) => Math . round ( section . getBoundingClientRect ( ) . height ) )
1231+ ) ) ;
1232+ expect ( Math . max ( ...leftOpenHeights ) - Math . min ( ...leftOpenHeights ) ) . toBeLessThanOrEqual ( 18 ) ;
1233+
1234+ const tileScrollState = await page . locator ( "#objectVectorStudioV2ObjectTiles" ) . evaluate ( ( element ) => ( {
1235+ clientHeight : Math . round ( element . clientHeight ) ,
1236+ scrollHeight : Math . round ( element . scrollHeight )
1237+ } ) ) ;
1238+ expect ( tileScrollState . scrollHeight ) . toBeGreaterThan ( tileScrollState . clientHeight ) ;
1239+
1240+ await page . getByRole ( "button" , { name : "Shape/Tools" } ) . click ( ) ;
1241+ await expect ( page . locator ( "#objectVectorStudioV2ShapeToolsContent" ) ) . toBeHidden ( ) ;
1242+ const collapsedLayout = await page . locator ( ".tool-starter__panel--left .accordion-v2" ) . evaluateAll ( ( sections ) => (
1243+ sections . map ( ( section ) => ( {
1244+ isOpen : section . classList . contains ( "is-open" ) ,
1245+ height : Math . round ( section . getBoundingClientRect ( ) . height )
1246+ } ) )
1247+ ) ) ;
1248+ const collapsedHeight = collapsedLayout . find ( ( entry ) => ! entry . isOpen ) ?. height || 0 ;
1249+ expect ( collapsedHeight ) . toBeLessThan ( 64 ) ;
1250+ const remainingHeights = collapsedLayout . filter ( ( entry ) => entry . isOpen ) . map ( ( entry ) => entry . height ) ;
1251+ expect ( Math . max ( ...remainingHeights ) - Math . min ( ...remainingHeights ) ) . toBeLessThanOrEqual ( 18 ) ;
1252+
1253+ const summary = page . locator ( "[data-tool-starter-summary]" ) ;
1254+ await summary . click ( ) ;
1255+ await expect ( page . locator ( ".is-collapsible" ) ) . not . toHaveAttribute ( "open" , "" ) ;
1256+ const fullscreenActive = await page . locator ( "body" ) . evaluate ( ( body ) => body . classList . contains ( "tools-platform-fullscreen-active" ) ) ;
1257+ if ( ! fullscreenActive ) {
1258+ await page . evaluate ( ( ) => {
1259+ window . __objectVectorStudioV2App . shell . applyFullscreenState ( true ) ;
1260+ window . __objectVectorStudioV2App . shell . updateSummary ( ) ;
1261+ } ) ;
1262+ }
1263+ await expect ( page . locator ( "body" ) ) . toHaveClass ( / t o o l s - p l a t f o r m - f u l l s c r e e n - a c t i v e / ) ;
1264+ const fullscreenLayout = await page . evaluate ( ( ) => {
1265+ const left = document . querySelector ( ".tool-starter__panel--left" ) . getBoundingClientRect ( ) ;
1266+ const center = document . querySelector ( ".tool-starter__panel--center" ) . getBoundingClientRect ( ) ;
1267+ const right = document . querySelector ( ".tool-starter__panel--right" ) . getBoundingClientRect ( ) ;
1268+ return {
1269+ centerHeight : Math . round ( center . height ) ,
1270+ centerWidth : Math . round ( center . width ) ,
1271+ leftBeforeCenter : left . right <= center . left ,
1272+ rightAfterCenter : right . left >= center . right
1273+ } ;
1274+ } ) ;
1275+ expect ( fullscreenLayout . leftBeforeCenter ) . toBe ( true ) ;
1276+ expect ( fullscreenLayout . rightAfterCenter ) . toBe ( true ) ;
1277+ expect ( fullscreenLayout . centerWidth ) . toBeGreaterThan ( 300 ) ;
1278+ expect ( fullscreenLayout . centerHeight ) . toBeGreaterThan ( 300 ) ;
1279+
1280+ await page . goto ( `${ server . baseUrl } /tools/object-vector-studio-v2/index.html?launch=workspace&fromTool=workspace-manager-v2&hostContextId=object-vector-v2-shell&workspaceMode=uat` , { waitUntil : "networkidle" } ) ;
1281+ await expect ( page . locator ( '[data-launch-mode-nav="tool"]' ) ) . toBeHidden ( ) ;
1282+ await expect ( page . locator ( '[data-launch-mode-nav="workspace"] button' ) ) . toHaveText ( [ "Return to Workspace" ] ) ;
1283+ await expect ( page . locator ( "#statusLog" ) ) . toHaveValue ( / F A I L S c h e m a - o n l y w o r k s p a c e l o a d i n g b l o c k e d : w o r k s p a c e \. t o o l s \. o b j e c t - v e c t o r - s t u d i o - v 2 i s m i s s i n g / ) ;
1284+ await page . locator ( "#returnToWorkspaceButton" ) . click ( ) ;
1285+ 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 = o b j e c t - v e c t o r - v 2 - s h e l l / ) ;
1286+ await expect ( page ) . toHaveURL ( / w o r k s p a c e = u a t / ) ;
1287+
1288+ expect ( pageErrors ) . toEqual ( [ ] ) ;
1289+ } finally {
1290+ await coverageReporter . stop ( page ) ;
1291+ await server . close ( ) ;
1292+ }
1293+ } ) ;
1294+
11571295 test ( "uses First-Class Tool V2 theme contract" , async ( { page } ) => {
11581296 const server = await openWorkspaceManagerV2 ( page ) ;
11591297 const pageErrors = [ ] ;
0 commit comments