Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 

192 linhas
15 KiB

  1. import { test, expect } from '@playwright/test'
  2. import fs from 'node:fs'
  3. import path from 'node:path'
  4. import { signIn, freshScene, openEditor, ready, diagnostics, action, save, exportDocument, shot, evidence, closeSession, call, report, registerProject } from './scene-editor-closure-helpers'
  5. test.afterEach(async ({ page }) => closeSession(page))
  6. const field = (page: any, name: string) => page.locator(`[data-inspector-field="${name}"]`)
  7. const select = async (page: any, id: string) => page.locator(`#se-tree [data-object-id="${id}"]`).click()
  8. const rootModel = (d: any, id: string) => d.objects.find((o: any) => o.targetProjectId === id)
  9. const byId = (d: any, id: string) => d.objects.find((o: any) => o.sceneId === id)
  10. const setField = async (page: any, name: string, value: string) => { await field(page, name).fill(value); await field(page, name).press('Tab') }
  11. test('狐狸:真实细分权限、全部属性、清空绑定、蒙皮保护、相机与封面保存重开', async ({ page }, info) => {
  12. const token = await signIn(page), copy = await freshScene(token, 'fox', '属性封面'), id = copy.project.id
  13. await openEditor(page, id)
  14. const before = await diagnostics(page), fox = rootModel(before, '157')
  15. expect(before.readOnly).toBe(false); expect(fox.parts.length).toBeGreaterThan(0)
  16. await expect(action(page, 'new')).toBeEnabled(); await expect(action(page, 'save')).toBeEnabled()
  17. await expect(page.locator('.se-project')).toHaveAttribute('title', `当前场景:${copy.project.name}`)
  18. await action(page, 'project').click()
  19. await select(page, fox.sceneId); await action(page, 'focus').click()
  20. await shot(page, info, '01-fox-actual-scene')
  21. await setField(page, 'name', '闭环验收狐狸目标')
  22. 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)
  23. for (const [name, value] of [['sx','0.036'],['sy','0.04'],['sz','0.038']]) await setField(page, name, value)
  24. await setField(page, 'sx', '0'); expect(byId(await diagnostics(page), fox.sceneId).scale[0]).toBeGreaterThan(0)
  25. await setField(page, 'sx', '-1'); expect(byId(await diagnostics(page), fox.sceneId).scale[0]).toBeGreaterThan(0)
  26. await setField(page, 'sx', '0.036')
  27. await field(page, 'visible').uncheck(); expect(byId(await diagnostics(page), fox.sceneId).visible).toBe(false)
  28. await field(page, 'visible').check()
  29. await field(page, 'operable').uncheck(); expect(byId(await diagnostics(page), fox.sceneId).operable).toBe(false)
  30. await field(page, 'operable').check()
  31. for (const semantic of ['实训装备','工位设施','安全区域','采集设备','训练人员','环境设施']) {
  32. await field(page, 'semantic').selectOption(semantic)
  33. expect(byId(await diagnostics(page), fox.sceneId).semantic).toBe(semantic)
  34. }
  35. await field(page, 'semantic').selectOption('训练人员')
  36. await setField(page, 'deviceId', ''); await setField(page, 'modelId', '')
  37. await page.locator('[data-bottom-tab="binding"]').click()
  38. await expect(page.locator('#se-bottom-content')).toContainText('训练人员')
  39. await expect(page.locator('#se-bottom-content')).toContainText('未设置')
  40. await save(page)
  41. const saved = (await call(`tran/v1/content/projects/${id}`, token)).data
  42. const sourceObject = saved.currentVersion.content.objects.find((o: any) => o.sceneId === fox.sceneId)
  43. expect(sourceObject.deviceId).toBe(''); expect(sourceObject.modelId).toBe('')
  44. expect(saved.project.coverUri).toMatch(/^(content|fsvc):/)
  45. const cover = saved.currentVersion.assets.find((a: any) => a.code === 'SCENE_COVER')
  46. expect(cover.storageUri).toBe(saved.project.coverUri)
  47. const image = await page.request.get(`/api/tran/v1/content/projects/${id}/assets/${cover.id}/content`, { headers: { Authorization: `Bearer ${token}` } })
  48. expect(image.ok()).toBe(true)
  49. fs.mkdirSync(path.join(report, 'covers'), { recursive: true }); fs.writeFileSync(path.join(report, 'covers/fox.jpg'), await image.body())
  50. const storedCamera = saved.currentVersion.content.rendering.camera
  51. await page.reload(); await ready(page)
  52. const reopened = await diagnostics(page), restored = byId(reopened, fox.sceneId)
  53. expect(restored.position).toEqual(sourceObject.position); expect(restored.scale).toEqual(sourceObject.scale)
  54. expect(restored.rotation).toEqual(sourceObject.rotation); expect(restored.deviceId).toBe(''); expect(restored.modelId).toBe('')
  55. for (let i=0;i<3;i++) expect(reopened.cameraPosition[i]).toBeCloseTo(storedCamera.position[i], 4)
  56. await select(page, fox.sceneId)
  57. const part = restored.parts[0]; await select(page, part.sceneId)
  58. await expect(field(page, 'x')).toBeDisabled(); await expect(field(page, 'visible')).toBeDisabled()
  59. await expect(page.locator('#se-inspector')).toContainText('蒙皮部件仅支持查看与高亮')
  60. await action(page, 'delete').click(); expect(byId(await diagnostics(page), fox.sceneId).parts[0].sceneDeleted).toBe(false)
  61. await shot(page, info, '02-fox-reopened-skin-protection')
  62. evidence('fox-properties-cover', { projectId:id, original:before, savedObject:sourceObject, cover:{ id:cover.id,storageUri:cover.storageUri,sizeBytes:cover.sizeBytes }, reopened })
  63. })
  64. test('牛奶卡车:刚性部件独立变换、软删除、历史与保存基线一致', async ({ page }, info) => {
  65. const token = await signIn(page), copy = await freshScene(token, 'truck', '部件历史'), id = copy.project.id
  66. await openEditor(page, id)
  67. for(const mode of ['full','xray','hidden']) {
  68. const control=page.locator(`[data-garage-enclosure-mode="${mode}"]`)
  69. await control.click();await expect(control).toHaveAttribute('aria-pressed','true')
  70. const document=await exportDocument(page)
  71. expect(document.objects.find((o:any)=>o.name==='车库').enclosureMode).toBe(mode)
  72. await shot(page,info,`07-garage-${mode}`)
  73. }
  74. await save(page);await page.reload();await ready(page)
  75. await expect(page.locator('[data-garage-enclosure-mode="hidden"]')).toHaveAttribute('aria-pressed','true')
  76. const originalCount=(await diagnostics(page)).objectCount
  77. await page.locator('[data-left-tab="assets"]').click()
  78. await page.locator('[data-editor-asset-id][data-target-project-id="156"]').dblclick()
  79. await expect.poll(async()=>(await diagnostics(page)).objectCount).toBe(originalCount+1);await ready(page)
  80. await page.locator('[data-left-tab="outline"]').click()
  81. const initial = await diagnostics(page), truck = rootModel(initial, '156')
  82. const secondTruck=initial.objects.filter((o:any)=>o.targetProjectId==='156')[1]
  83. expect(secondTruck.sceneId).not.toBe(truck.sceneId)
  84. await select(page, truck.sceneId); await action(page, 'focus').click()
  85. const part = truck.parts.find((p: any) => p.capabilities.includes('transform'))
  86. expect(part).toBeTruthy(); await select(page, part.sceneId)
  87. await expect(field(page, 'x')).toBeEnabled()
  88. await setField(page, 'name', '独立部件验收')
  89. await setField(page, 'x', String(Number((part.position[0]+0.3).toFixed(3))))
  90. const changed = await diagnostics(page)
  91. expect(byId(changed,truck.sceneId).position).toEqual(truck.position)
  92. await save(page)
  93. await action(page, 'delete').click()
  94. expect(byId(await diagnostics(page),truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).sceneDeleted).toBe(true)
  95. await action(page, 'undo').click(); await ready(page)
  96. await expect.poll(async()=>byId(await diagnostics(page),truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).sceneDeleted).toBe(false)
  97. await page.locator('.se-project').focus(); await page.keyboard.press('Control+Shift+Z')
  98. await expect.poll(async()=>byId(await diagnostics(page),truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).sceneDeleted).toBe(true)
  99. await page.keyboard.press('Control+z')
  100. await expect.poll(async()=>byId(await diagnostics(page),truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).sceneDeleted).toBe(false)
  101. await select(page, truck.sceneId); await setField(page,'name','保存后的卡车B'); await save(page)
  102. await action(page,'undo').click()
  103. await expect.poll(async()=> (await diagnostics(page)).dirty).toBe(true)
  104. await action(page,'redo').click(); await expect.poll(async()=> (await diagnostics(page)).dirty).toBe(false)
  105. await select(page, truck.sceneId)
  106. await field(page,'name').fill('输入框自身撤销'); await field(page,'name').press('Control+z')
  107. expect((await diagnostics(page)).objectCount).toBe(initial.objectCount)
  108. await field(page,'name').press('Tab'); await setField(page,'name','卡车部件闭环完成')
  109. await field(page,'name').press('Control+s'); await expect.poll(async()=> (await diagnostics(page)).dirty).toBe(false)
  110. await page.reload(); await ready(page)
  111. const reopened = await diagnostics(page), savedPart=byId(reopened,truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId)
  112. expect(reopened.objects.map((o:any)=>o.sceneId)).toEqual(initial.objects.map((o:any)=>o.sceneId))
  113. expect(byId(reopened,secondTruck.sceneId).parts).toEqual(secondTruck.parts)
  114. expect(savedPart.position).toEqual(byId(changed,truck.sceneId).parts.find((p:any)=>p.sceneId===part.sceneId).position)
  115. expect(savedPart.name).toBe('独立部件验收')
  116. await select(page, truck.sceneId); await action(page,'focus').click()
  117. await shot(page,info,'03-truck-part-history-reopened')
  118. evidence('truck-part-history',{projectId:id,partId:part.sceneId,initial,changed,reopened})
  119. })
  120. test('新建独立场景:取消、失败、保护原工程以及成功新 ID', async ({ page },info) => {
  121. const token=await signIn(page), copy=await freshScene(token,'fox','新建'), id=copy.project.id
  122. await openEditor(page,id); const initial=await diagnostics(page), fox=rootModel(initial,'157')
  123. await select(page,fox.sceneId); await setField(page,'name','新建前未保存狐狸')
  124. await action(page,'new').click(); await page.getByRole('button',{name:'取消',exact:true}).click()
  125. expect(page.url()).toContain(`/${id}/edit`); expect(byId(await diagnostics(page),fox.sceneId).name).toBe('新建前未保存狐狸')
  126. const endpoint='**/api/tran/v1/content/projects'
  127. await page.route(endpoint,route=>route.request().method()==='POST'?route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:503,message:'验收模拟创建暂不可用'})}):route.continue())
  128. await action(page,'new').click(); await page.locator('.el-message-box__input input').fill('场景闭环-新建失败演练')
  129. await page.getByRole('button',{name:'创建并编辑',exact:true}).click()
  130. await page.getByRole('button',{name:'保存并离开',exact:true}).click()
  131. await expect(page.locator('.el-message--error')).toContainText('创建暂不可用')
  132. expect(page.url()).toContain(`/${id}/edit`); expect(byId(await diagnostics(page),fox.sceneId).name).toBe('新建前未保存狐狸')
  133. await expect(page.locator('.el-message--error')).toHaveCount(0)
  134. await setField(page,'name','新建失败仍可恢复的狐狸')
  135. const recovery = () => page.evaluate(projectId => Object.entries(localStorage)
  136. .filter(([key]) => key.includes(`edit3dv4:scene-api-recovery:${projectId}:session:`))
  137. .map(([,value]) => JSON.parse(value)), id)
  138. await expect.poll(async()=>(await recovery()).length).toBeGreaterThan(0)
  139. await action(page,'new').click(); await page.locator('.el-message-box__input input').fill('场景闭环-放弃后创建失败演练')
  140. await page.getByRole('button',{name:'创建并编辑',exact:true}).click()
  141. await page.getByRole('button',{name:'放弃修改',exact:true}).click()
  142. await expect(page.locator('.el-message--error')).toContainText('创建暂不可用')
  143. expect((await diagnostics(page)).dirty).toBe(true)
  144. expect((await recovery())[0].document.objects.find((o:any)=>o.sceneId===fox.sceneId).name).toBe('新建失败仍可恢复的狐狸')
  145. expect((await call(`tran/v1/content/projects/${id}`,token)).data.currentVersion.content.objects.find((o:any)=>o.sceneId===fox.sceneId).name).toBe('新建前未保存狐狸')
  146. await shot(page,info,'04-new-failure-original-preserved'); await page.unroute(endpoint)
  147. await action(page,'new').click(); await page.locator('.el-message-box__input input').fill('场景闭环-真正新建工程')
  148. const response=page.waitForResponse(r=>r.url().endsWith('/api/tran/v1/content/projects')&&r.request().method()==='POST')
  149. await page.getByRole('button',{name:'创建并编辑',exact:true}).click()
  150. await page.getByRole('button',{name:'放弃修改',exact:true}).click()
  151. const result=await(await response).json(); registerProject(result.data.project)
  152. const newId=result.data.project.id; expect(newId).not.toBe(id)
  153. await expect(page).toHaveURL(new RegExp(`/content/scenes/${newId}/edit`)); await ready(page); await save(page)
  154. expect(await recovery()).toEqual([])
  155. await page.reload(); await ready(page); await shot(page,info,'05-new-separate-scene')
  156. await openEditor(page,id)
  157. expect(byId(await diagnostics(page),fox.sceneId).name).toBe('新建前未保存狐狸')
  158. expect((await diagnostics(page)).dirty).toBe(false)
  159. await openEditor(page,newId)
  160. const previous=(await call(`tran/v1/content/projects/${id}`,token)).data
  161. expect(previous.currentVersion.content.objects.find((o:any)=>o.sceneId===fox.sceneId).name).toBe('新建前未保存狐狸')
  162. evidence('new-project',{sourceId:id,createdId:newId,sourceName:previous.project.name,current:await diagnostics(page)})
  163. })
  164. test('只读管理员:可浏览预览与导出,模板/资源/键盘/接口不能改场景', async ({ page },info) => {
  165. const token=await signIn(page,'admin')
  166. await openEditor(page,'160'); const before=await diagnostics(page)
  167. expect(before.readOnly).toBe(true)
  168. for(const name of ['new','open','save','undo','redo','snap','delete','publish']) await expect(action(page,name)).toBeDisabled()
  169. await page.locator('[data-left-tab="scenes"]').click()
  170. await expect(page.locator('[data-preset-id]').first()).toBeDisabled()
  171. await page.locator('[data-left-tab="assets"]').click()
  172. await expect(page.locator('[data-import-model]')).toBeDisabled()
  173. expect(await page.locator('[data-editor-asset-id]').first().getAttribute('draggable')).toBe('false')
  174. await page.locator('[data-left-tab="outline"]').click(); const fox=rootModel(before,'157'); await select(page,fox.sceneId)
  175. await expect(field(page,'name')).toBeDisabled(); await action(page,'focus').click()
  176. await action(page,'preview').click(); expect((await diagnostics(page)).previewing).toBe(true)
  177. await page.keyboard.press('Delete'); await page.keyboard.press('w'); await page.keyboard.press('Control+z')
  178. expect((await diagnostics(page)).objects).toEqual(before.objects)
  179. await page.keyboard.press('Escape'); expect((await diagnostics(page)).previewing).toBe(false)
  180. const exported=await exportDocument(page); expect(exported.objects.length).toBe(before.objectCount)
  181. const original=(await call('tran/v1/content/projects/160',token)).data
  182. const denied=await call('tran/v1/content/projects/160',token,{...original.project,content:original.currentVersion.content,
  183. dependencies:original.currentVersion.dependencies,assets:original.currentVersion.assets},'PUT'); expect(denied.status).toBe(403)
  184. await shot(page,info,'06-readonly-scene-permissions')
  185. evidence('readonly',{before,after:await diagnostics(page),directWriteStatus:denied.status})
  186. })