2026-04-03 02:19:48 +02:00
import { expect , test , type Page } from "@playwright/test" ;
2026-04-03 02:19:41 +02:00
import { createModelInstance } from "../../src/assets/model-instances" ;
import { createProjectAssetStorageKey , type ModelAssetRecord } from "../../src/assets/project-assets" ;
import { createBoxBrush } from "../../src/document/brushes" ;
import { createEmptySceneDocument } from "../../src/document/scene-document" ;
import { createPlayerStartEntity } from "../../src/entities/entity-instances" ;
import {
clickViewport ,
getEditorStoreSnapshot ,
getViewportCanvas ,
replaceSceneDocument
} from "./viewport-test-helpers" ;
const modelAsset = {
id : "asset-model-transform-e2e" ,
kind : "model" ,
sourceName : "transform-fixture.glb" ,
mimeType : "model/gltf-binary" ,
storageKey : createProjectAssetStorageKey ( "asset-model-transform-e2e" ) ,
byteLength : 72 ,
metadata : {
kind : "model" ,
format : "glb" ,
sceneName : "Transform Fixture" ,
nodeCount : 1 ,
meshCount : 1 ,
materialNames : [ ] ,
textureNames : [ ] ,
animationNames : [ ] ,
boundingBox : {
min : {
x : - 0.5 ,
y : 0 ,
z : - 0.5
} ,
max : {
x : 0.5 ,
y : 1 ,
z : 0.5
} ,
size : {
x : 1 ,
y : 1 ,
z : 1
}
} ,
warnings : [ ]
}
} satisfies ModelAssetRecord ;
2026-04-03 02:19:48 +02:00
async function seedTransformScene ( page : Page ) {
2026-04-03 02:19:41 +02:00
const brush = createBoxBrush ( {
id : "brush-transform-main" ,
name : "Brush Transform Fixture" ,
center : {
x : 0 ,
y : 1 ,
z : 0
}
} ) ;
const playerStart = createPlayerStartEntity ( {
id : "entity-player-start-transform" ,
name : "Player Start Fixture" ,
position : {
x : 4 ,
y : 0 ,
z : - 4
} ,
yawDegrees : 0
} ) ;
const modelInstance = createModelInstance ( {
id : "model-instance-transform-main" ,
assetId : modelAsset.id ,
name : "Model Transform Fixture" ,
position : {
x : - 4 ,
y : 0 ,
z : 4
}
} ) ;
await replaceSceneDocument ( page , {
. . . createEmptySceneDocument ( { name : "Transform Foundation Fixture" } ) ,
brushes : {
[ brush . id ] : brush
} ,
assets : {
[ modelAsset . id ] : modelAsset
} ,
entities : {
[ playerStart . id ] : playerStart
} ,
modelInstances : {
[ modelInstance . id ] : modelInstance
}
} ) ;
return {
brush ,
playerStart ,
modelInstance
} ;
}
2026-04-03 02:19:48 +02:00
async function getViewportCenter ( page : Page , panelId : "topLeft" | "topRight" | "bottomLeft" | "bottomRight" ) {
2026-04-03 02:19:41 +02:00
const viewportCanvas = getViewportCanvas ( page , panelId ) ;
const bounds = await viewportCanvas . boundingBox ( ) ;
if ( bounds === null ) {
throw new Error ( ` Viewport ${ panelId } is not visible. ` ) ;
}
return {
x : bounds.x + bounds . width * 0.5 ,
y : bounds.y + bounds . height * 0.5
} ;
}
async function commitKeyboardMove (
2026-04-03 02:19:48 +02:00
page : Page ,
2026-04-03 02:19:41 +02:00
panelId : "topLeft" | "topRight" | "bottomLeft" | "bottomRight" ,
delta : { x : number ; y : number } ,
axisConstraint ? : "x" | "y" | "z"
) {
const center = await getViewportCenter ( page , panelId ) ;
await page . mouse . move ( center . x , center . y ) ;
await page . keyboard . press ( "g" ) ;
if ( axisConstraint !== undefined ) {
await page . keyboard . press ( axisConstraint ) ;
}
await page . mouse . move ( center . x + delta . x , center . y + delta . y ) ;
await page . mouse . click ( center . x + delta . x , center . y + delta . y ) ;
}
test ( "viewport picking defaults to whole-brush selection and axis-constrained brush move commits through the shared transform session" , async ( { page } ) = > {
await page . goto ( "/" ) ;
await page . evaluate ( ( storageKey ) = > {
window . localStorage . removeItem ( storageKey ) ;
} , "webeditor3d.scene-document-draft" ) ;
await page . reload ( ) ;
const fixtures = await seedTransformScene ( page ) ;
await page . getByTestId ( "viewport-layout-quad" ) . click ( ) ;
await clickViewport ( page , "topRight" ) ;
const selectedSnapshot = await getEditorStoreSnapshot ( page ) ;
expect ( selectedSnapshot . selection ) . toEqual ( {
kind : "brushes" ,
ids : [ fixtures . brush . id ]
} ) ;
await commitKeyboardMove ( page , "topRight" , { x : 160 , y : 0 } , "x" ) ;
const movedSnapshot = await getEditorStoreSnapshot ( page ) ;
expect ( movedSnapshot . viewportTransientState . transformSession ) . toEqual ( {
kind : "none"
} ) ;
expect ( movedSnapshot . document . brushes [ fixtures . brush . id ] . center . y ) . toBe ( fixtures . brush . center . y ) ;
expect ( movedSnapshot . document . brushes [ fixtures . brush . id ] . center . z ) . toBe ( fixtures . brush . center . z ) ;
expect ( movedSnapshot . document . brushes [ fixtures . brush . id ] . center . x ) . not . toBe ( fixtures . brush . center . x ) ;
} ) ;
test ( "keyboard move commits an entity translation through the shared transform controller" , async ( { page } ) = > {
await page . goto ( "/" ) ;
await page . evaluate ( ( storageKey ) = > {
window . localStorage . removeItem ( storageKey ) ;
} , "webeditor3d.scene-document-draft" ) ;
await page . reload ( ) ;
const fixtures = await seedTransformScene ( page ) ;
await page . getByRole ( "button" , { name : fixtures.playerStart.name as string } ) . click ( ) ;
await commitKeyboardMove ( page , "topLeft" , { x : 100 , y : - 60 } ) ;
const snapshot = await getEditorStoreSnapshot ( page ) ;
expect ( snapshot . viewportTransientState . transformSession ) . toEqual ( {
kind : "none"
} ) ;
expect ( snapshot . document . entities [ fixtures . playerStart . id ] . position ) . not . toEqual ( fixtures . playerStart . position ) ;
} ) ;
test ( "escape cancels an active entity transform session without committing preview changes" , async ( { page } ) = > {
await page . goto ( "/" ) ;
await page . evaluate ( ( storageKey ) = > {
window . localStorage . removeItem ( storageKey ) ;
} , "webeditor3d.scene-document-draft" ) ;
await page . reload ( ) ;
const fixtures = await seedTransformScene ( page ) ;
await page . getByRole ( "button" , { name : fixtures.playerStart.name as string } ) . click ( ) ;
const center = await getViewportCenter ( page , "topLeft" ) ;
await page . mouse . move ( center . x , center . y ) ;
await page . keyboard . press ( "g" ) ;
await page . mouse . move ( center . x + 140 , center . y + 40 ) ;
await page . keyboard . press ( "Escape" ) ;
const snapshot = await getEditorStoreSnapshot ( page ) ;
expect ( snapshot . viewportTransientState . transformSession ) . toEqual ( {
kind : "none"
} ) ;
expect ( snapshot . document . entities [ fixtures . playerStart . id ] . position ) . toEqual ( fixtures . playerStart . position ) ;
} ) ;
test ( "keyboard move commits a model instance translation through the shared transform controller" , async ( { page } ) = > {
await page . goto ( "/" ) ;
await page . evaluate ( ( storageKey ) = > {
window . localStorage . removeItem ( storageKey ) ;
} , "webeditor3d.scene-document-draft" ) ;
await page . reload ( ) ;
const fixtures = await seedTransformScene ( page ) ;
await page . getByRole ( "button" , { name : fixtures.modelInstance.name as string } ) . click ( ) ;
await commitKeyboardMove ( page , "topLeft" , { x : - 120 , y : 80 } ) ;
const snapshot = await getEditorStoreSnapshot ( page ) ;
expect ( snapshot . viewportTransientState . transformSession ) . toEqual ( {
kind : "none"
} ) ;
expect ( snapshot . document . modelInstances [ fixtures . modelInstance . id ] . position ) . not . toEqual ( fixtures . modelInstance . position ) ;
} ) ;