|
- import { test, expect } from '@playwright/test'
- import fs from 'node:fs'
- import path from 'node:path'
- import { signIn, freshScene, openEditor, ready, diagnostics, action, save, exportDocument, shot, evidence, closeSession, call, report, registerProject } from './scene-editor-closure-helpers'
-
- test.afterEach(async ({ page }) => closeSession(page))
- const field = (page: any, name: string) => page.locator(`[data-inspector-field="${name}"]`)
- const select = async (page: any, id: string) => page.locator(`#se-tree [data-object-id="${id}"]`).click()
- const rootModel = (d: any, id: string) => d.objects.find((o: any) => o.targetProjectId === id)
- const byId = (d: any, id: string) => d.objects.find((o: any) => o.sceneId === id)
- const setField = async (page: any, name: string, value: string) => { await field(page, name).fill(value); await field(page, name).press('Tab') }
-
- test('狐狸:真实细分权限、全部属性、清空绑定、蒙皮保护、相机与封面保存重开', async ({ page }, info) => {
- const token = await signIn(page), copy = await freshScene(token, 'fox', '属性封面'), id = copy.project.id
- await openEditor(page, id)
- const before = await diagnostics(page), fox = rootModel(before, '157')
- expect(before.readOnly).toBe(false); expect(fox.parts.length).toBeGreaterThan(0)
- await expect(action(page, 'new')).toBeEnabled(); await expect(action(page, 'save')).toBeEnabled()
- await expect(page.locator('.se-project')).toHaveAttribute('title', `当前场景:${copy.project.name}`)
- await action(page, 'project').click()
- await select(page, fox.sceneId); await action(page, 'focus').click()
- await shot(page, info, '01-fox-actual-scene')
- await setField(page, 'name', '闭环验收狐狸目标')
- for (const [name, value] of [['x','-6.3'],['y','0.25'],['z','1.2'],['rx','5.5'],['ry','-24.5'],['rz','3.2']]) await setField(page, name, value)
- for (const [name, value] of [['sx','0.036'],['sy','0.04'],['sz','0.038']]) await setField(page, name, value)
- await setField(page, 'sx', '0'); expect(byId(await diagnostics(page), fox.sceneId).scale[0]).toBeGreaterThan(0)
- await setField(page, 'sx', '-1'); expect(byId(await diagnostics(page), fox.sceneId).scale[0]).toBeGreaterThan(0)
- await setField(page, 'sx', '0.036')
- await field(page, 'visible').uncheck(); expect(byId(await diagnostics(page), fox.sceneId).visible).toBe(false)
- await field(page, 'visible').check()
- await field(page, 'operable').uncheck(); expect(byId(await diagnostics(page), fox.sceneId).operable).toBe(false)
- await field(page, 'operable').check()
- for (const semantic of ['实训装备','工位设施','安全区域','采集设备','训练人员','环境设施']) {
- await field(page, 'semantic').selectOption(semantic)
- expect(byId(await diagnostics(page), fox.sceneId).semantic).toBe(semantic)
- }
- await field(page, 'semantic').selectOption('训练人员')
- await setField(page, 'deviceId', ''); await setField(page, 'modelId', '')
- await page.locator('[data-bottom-tab="binding"]').click()
- await expect(page.locator('#se-bottom-content')).toContainText('训练人员')
- await expect(page.locator('#se-bottom-content')).toContainText('未设置')
- await save(page)
- const saved = (await call(`tran/v1/content/projects/${id}`, token)).data
- const sourceObject = saved.currentVersion.content.objects.find((o: any) => o.sceneId === fox.sceneId)
- expect(sourceObject.deviceId).toBe(''); expect(sourceObject.modelId).toBe('')
- expect(saved.project.coverUri).toMatch(/^(content|fsvc):/)
- const cover = saved.currentVersion.assets.find((a: any) => a.code === 'SCENE_COVER')
- expect(cover.storageUri).toBe(saved.project.coverUri)
- const image = await page.request.get(`/api/tran/v1/content/projects/${id}/assets/${cover.id}/content`, { headers: { Authorization: `Bearer ${token}` } })
- expect(image.ok()).toBe(true)
- fs.mkdirSync(path.join(report, 'covers'), { recursive: true }); fs.writeFileSync(path.join(report, 'covers/fox.jpg'), await image.body())
- const storedCamera = saved.currentVersion.content.rendering.camera
- await page.reload(); await ready(page)
- const reopened = await diagnostics(page), restored = byId(reopened, fox.sceneId)
- expect(restored.position).toEqual(sourceObject.position); expect(restored.scale).toEqual(sourceObject.scale)
- expect(restored.rotation).toEqual(sourceObject.rotation); expect(restored.deviceId).toBe(''); expect(restored.modelId).toBe('')
- for (let i=0;i<3;i++) expect(reopened.cameraPosition[i]).toBeCloseTo(storedCamera.position[i], 4)
- await select(page, fox.sceneId)
- const part = restored.parts[0]; await select(page, part.sceneId)
- await expect(field(page, 'x')).toBeDisabled(); await expect(field(page, 'visible')).toBeDisabled()
- await expect(page.locator('#se-inspector')).toContainText('蒙皮部件仅支持查看与高亮')
- await action(page, 'delete').click(); expect(byId(await diagnostics(page), fox.sceneId).parts[0].sceneDeleted).toBe(false)
- await shot(page, info, '02-fox-reopened-skin-protection')
- evidence('fox-properties-cover', { projectId:id, original:before, savedObject:sourceObject, cover:{ id:cover.id,storageUri:cover.storageUri,sizeBytes:cover.sizeBytes }, reopened })
- })
-
- test('牛奶卡车:刚性部件独立变换、软删除、历史与保存基线一致', async ({ page }, info) => {
- const token = await signIn(page), copy = await freshScene(token, 'truck', '部件历史'), id = copy.project.id
- await openEditor(page, id)
- for(const mode of ['full','xray','hidden']) {
- const control=page.locator(`[data-garage-enclosure-mode="${mode}"]`)
- await control.click();await expect(control).toHaveAttribute('aria-pressed','true')
- const document=await exportDocument(page)
- expect(document.objects.find((o:any)=>o.name==='车库').enclosureMode).toBe(mode)
- await shot(page,info,`07-garage-${mode}`)
- }
- await save(page);await page.reload();await ready(page)
- await expect(page.locator('[data-garage-enclosure-mode="hidden"]')).toHaveAttribute('aria-pressed','true')
- const originalCount=(await diagnostics(page)).objectCount
- await page.locator('[data-left-tab="assets"]').click()
- await page.locator('[data-editor-asset-id][data-target-project-id="156"]').dblclick()
- await expect.poll(async()=>(await diagnostics(page)).objectCount).toBe(originalCount+1);await ready(page)
- await page.locator('[data-left-tab="outline"]').click()
- const initial = await diagnostics(page), truck = rootModel(initial, '156')
- const secondTruck=initial.objects.filter((o:any)=>o.targetProjectId==='156')[1]
- expect(secondTruck.sceneId).not.toBe(truck.sceneId)
- await select(page, truck.sceneId); await action(page, 'focus').click()
- const part = truck.parts.find((p: any) => p.capabilities.includes('transform'))
- expect(part).toBeTruthy(); await select(page, part.sceneId)
- await expect(field(page, 'x')).toBeEnabled()
- await setField(page, 'name', '独立部件验收')
- await setField(page, 'x', String(Number((part.position[0]+0.3).toFixed(3))))
- const changed = await diagnostics(page)
- expect(byId(changed,truck.sceneId).position).toEqual(truck.position)
- await save(page)
- await action(page, 'delete').click()
- expect(byId(await diagnostics(page),truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).sceneDeleted).toBe(true)
- await action(page, 'undo').click(); await ready(page)
- await expect.poll(async()=>byId(await diagnostics(page),truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).sceneDeleted).toBe(false)
- await page.locator('.se-project').focus(); await page.keyboard.press('Control+Shift+Z')
- await expect.poll(async()=>byId(await diagnostics(page),truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).sceneDeleted).toBe(true)
- await page.keyboard.press('Control+z')
- await expect.poll(async()=>byId(await diagnostics(page),truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).sceneDeleted).toBe(false)
- await select(page, truck.sceneId); await setField(page,'name','保存后的卡车B'); await save(page)
- await action(page,'undo').click()
- await expect.poll(async()=> (await diagnostics(page)).dirty).toBe(true)
- await action(page,'redo').click(); await expect.poll(async()=> (await diagnostics(page)).dirty).toBe(false)
- await select(page, truck.sceneId)
- await field(page,'name').fill('输入框自身撤销'); await field(page,'name').press('Control+z')
- expect((await diagnostics(page)).objectCount).toBe(initial.objectCount)
- await field(page,'name').press('Tab'); await setField(page,'name','卡车部件闭环完成')
- await field(page,'name').press('Control+s'); await expect.poll(async()=> (await diagnostics(page)).dirty).toBe(false)
- await page.reload(); await ready(page)
- const reopened = await diagnostics(page), savedPart=byId(reopened,truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId)
- expect(reopened.objects.map((o:any)=>o.sceneId)).toEqual(initial.objects.map((o:any)=>o.sceneId))
- expect(byId(reopened,secondTruck.sceneId).parts).toEqual(secondTruck.parts)
- expect(savedPart.position).toEqual(byId(changed,truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).position)
- expect(savedPart.name).toBe('独立部件验收')
- await select(page, truck.sceneId); await action(page,'focus').click()
- await shot(page,info,'03-truck-part-history-reopened')
- evidence('truck-part-history',{projectId:id,partId:part.sceneId,initial,changed,reopened})
- })
-
- test('新建独立场景:取消、失败、保护原工程以及成功新 ID', async ({ page },info) => {
- const token=await signIn(page), copy=await freshScene(token,'fox','新建'), id=copy.project.id
- await openEditor(page,id); const initial=await diagnostics(page), fox=rootModel(initial,'157')
- await select(page,fox.sceneId); await setField(page,'name','新建前未保存狐狸')
- await action(page,'new').click(); await page.getByRole('button',{name:'取消',exact:true}).click()
- expect(page.url()).toContain(`/${id}/edit`); expect(byId(await diagnostics(page),fox.sceneId).name).toBe('新建前未保存狐狸')
- const endpoint='**/api/tran/v1/content/projects'
- await page.route(endpoint,route=>route.request().method()==='POST'?route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:503,message:'验收模拟创建暂不可用'})}):route.continue())
- await action(page,'new').click(); await page.locator('.el-message-box__input input').fill('场景闭环-新建失败演练')
- await page.getByRole('button',{name:'创建并编辑',exact:true}).click()
- await page.getByRole('button',{name:'保存并离开',exact:true}).click()
- await expect(page.locator('.el-message--error')).toContainText('创建暂不可用')
- expect(page.url()).toContain(`/${id}/edit`); expect(byId(await diagnostics(page),fox.sceneId).name).toBe('新建前未保存狐狸')
- await expect(page.locator('.el-message--error')).toHaveCount(0)
- await setField(page,'name','新建失败仍可恢复的狐狸')
- const recovery = () => page.evaluate(projectId => Object.entries(localStorage)
- .filter(([key]) => key.includes(`edit3dv4:scene-api-recovery:${projectId}:session:`))
- .map(([,value]) => JSON.parse(value)), id)
- await expect.poll(async()=>(await recovery()).length).toBeGreaterThan(0)
- await action(page,'new').click(); await page.locator('.el-message-box__input input').fill('场景闭环-放弃后创建失败演练')
- await page.getByRole('button',{name:'创建并编辑',exact:true}).click()
- await page.getByRole('button',{name:'放弃修改',exact:true}).click()
- await expect(page.locator('.el-message--error')).toContainText('创建暂不可用')
- expect((await diagnostics(page)).dirty).toBe(true)
- expect((await recovery())[0].document.objects.find((o:any)=>o.sceneId===fox.sceneId).name).toBe('新建失败仍可恢复的狐狸')
- expect((await call(`tran/v1/content/projects/${id}`,token)).data.currentVersion.content.objects.find((o:any)=>o.sceneId===fox.sceneId).name).toBe('新建前未保存狐狸')
- await shot(page,info,'04-new-failure-original-preserved'); await page.unroute(endpoint)
- await action(page,'new').click(); await page.locator('.el-message-box__input input').fill('场景闭环-真正新建工程')
- const response=page.waitForResponse(r=>r.url().endsWith('/api/tran/v1/content/projects')&&r.request().method()==='POST')
- await page.getByRole('button',{name:'创建并编辑',exact:true}).click()
- await page.getByRole('button',{name:'放弃修改',exact:true}).click()
- const result=await(await response).json(); registerProject(result.data.project)
- const newId=result.data.project.id; expect(newId).not.toBe(id)
- await expect(page).toHaveURL(new RegExp(`/content/scenes/${newId}/edit`)); await ready(page); await save(page)
- expect(await recovery()).toEqual([])
- await page.reload(); await ready(page); await shot(page,info,'05-new-separate-scene')
- await openEditor(page,id)
- expect(byId(await diagnostics(page),fox.sceneId).name).toBe('新建前未保存狐狸')
- expect((await diagnostics(page)).dirty).toBe(false)
- await openEditor(page,newId)
- const previous=(await call(`tran/v1/content/projects/${id}`,token)).data
- expect(previous.currentVersion.content.objects.find((o:any)=>o.sceneId===fox.sceneId).name).toBe('新建前未保存狐狸')
- evidence('new-project',{sourceId:id,createdId:newId,sourceName:previous.project.name,current:await diagnostics(page)})
- })
-
- test('只读管理员:可浏览预览与导出,模板/资源/键盘/接口不能改场景', async ({ page },info) => {
- const token=await signIn(page,'admin')
- await openEditor(page,'160'); const before=await diagnostics(page)
- expect(before.readOnly).toBe(true)
- for(const name of ['new','open','save','undo','redo','snap','delete','publish']) await expect(action(page,name)).toBeDisabled()
- await page.locator('[data-left-tab="scenes"]').click()
- await expect(page.locator('[data-preset-id]').first()).toBeDisabled()
- await page.locator('[data-left-tab="assets"]').click()
- await expect(page.locator('[data-import-model]')).toBeDisabled()
- expect(await page.locator('[data-editor-asset-id]').first().getAttribute('draggable')).toBe('false')
- await page.locator('[data-left-tab="outline"]').click(); const fox=rootModel(before,'157'); await select(page,fox.sceneId)
- await expect(field(page,'name')).toBeDisabled(); await action(page,'focus').click()
- await action(page,'preview').click(); expect((await diagnostics(page)).previewing).toBe(true)
- await page.keyboard.press('Delete'); await page.keyboard.press('w'); await page.keyboard.press('Control+z')
- expect((await diagnostics(page)).objects).toEqual(before.objects)
- await page.keyboard.press('Escape'); expect((await diagnostics(page)).previewing).toBe(false)
- const exported=await exportDocument(page); expect(exported.objects.length).toBe(before.objectCount)
- const original=(await call('tran/v1/content/projects/160',token)).data
- const denied=await call('tran/v1/content/projects/160',token,{...original.project,content:original.currentVersion.content,
- dependencies:original.currentVersion.dependencies,assets:original.currentVersion.assets},'PUT'); expect(denied.status).toBe(403)
- await shot(page,info,'06-readonly-scene-permissions')
- evidence('readonly',{before,after:await diagnostics(page),directWriteStatus:denied.status})
- })
|