Você não pode selecionar mais de 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.
 
 
 
 

155 linhas
13 KiB

  1. import { test, expect } from '@playwright/test'
  2. import path from 'node:path'
  3. import { signIn, freshScene, openEditor, ready, diagnostics, action, save, exportDocument, shot, evidence, closeSession, call, work } from './scene-editor-closure-helpers'
  4. test.afterEach(async ({ page }) => closeSession(page))
  5. const choose = async (page:any,id:string) => page.locator(`#se-tree [data-object-id="${id}"]`).click()
  6. const rename = async (page:any,value:string) => { await page.locator('[data-inspector-field="name"]').fill(value); await page.locator('[data-inspector-field="name"]').press('Tab') }
  7. const uploadJSON = (page:any,content:unknown) => page.locator('#se-project-file').setInputFiles({name:'scene-review.json',mimeType:'application/json',buffer:Buffer.from(typeof content==='string'?content:JSON.stringify(content))})
  8. test('工程 JSON 与小 GLB:真实上传、损坏拒绝、引用失败回滚、相机参数往返', async ({ page },info) => {
  9. const token=await signIn(page), copy=await freshScene(token,'truck','导入导出'), id=copy.project.id
  10. await openEditor(page,id); const initial=await diagnostics(page), exported=await exportDocument(page)
  11. expect(exported.schema).toBe('edit3dv4.scene'); expect(JSON.stringify(exported)).not.toContain('blob:')
  12. for(const invalid of ['{invalid', {schema:'another.kind',objects:[]}]) {
  13. await uploadJSON(page,invalid); await expect(page.locator('.se-toast.warn').last()).toBeVisible()
  14. expect((await diagnostics(page)).objects).toEqual(initial.objects)
  15. }
  16. const missing=structuredClone(exported), model=missing.objects.find((o:any)=>o.targetProjectId==='156')
  17. model.assetCode='SCENE-REVIEW-NOT-EXIST'
  18. await uploadJSON(page,missing)
  19. await expect(page.locator('.se-toast.warn').last()).toContainText(/失败/)
  20. await ready(page); expect((await diagnostics(page)).objects).toEqual(initial.objects)
  21. await shot(page,info,'10-invalid-reference-rolled-back')
  22. await expect(page.locator('.se-toast.warn')).toHaveCount(0)
  23. await page.locator('#se-model-file').setInputFiles({name:'broken.glb',mimeType:'model/gltf-binary',buffer:Buffer.from('broken')})
  24. await expect(page.locator('.se-toast.warn').last()).toContainText('模型导入失败'); await ready(page)
  25. expect((await diagnostics(page)).objectCount).toBe(initial.objectCount)
  26. await expect(page.locator('.se-toast.warn')).toHaveCount(0)
  27. const uploadEndpoint=`**/api/tran/v1/content/projects/${id}/assets/upload`
  28. await page.route(uploadEndpoint,route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:503,message:'验收模拟资源上传失败'})}))
  29. const asset=path.resolve(work,'../../文档/素材/01-Box.glb')
  30. await page.locator('#se-model-file').setInputFiles(asset)
  31. await expect(page.locator('.se-toast.warn').last()).toContainText(/模型导入失败.*503/); await ready(page)
  32. expect((await diagnostics(page)).objectCount).toBe(initial.objectCount); await page.unroute(uploadEndpoint)
  33. await expect(page.locator('.se-toast.warn')).toHaveCount(0)
  34. await page.locator('#se-model-file').setInputFiles(asset)
  35. await expect.poll(async()=> (await diagnostics(page)).objectCount,{timeout:60000}).toBe(initial.objectCount+1)
  36. await ready(page)
  37. const loaded=await diagnostics(page), added=loaded.objects.find((o:any)=>!initial.objects.some((b:any)=>b.sceneId===o.sceneId))
  38. expect(added.assetCode).toBeTruthy(); await choose(page,added.sceneId); await action(page,'focus').click()
  39. await save(page); await shot(page,info,'11-small-glb-real-upload')
  40. const document=await exportDocument(page)
  41. document.rendering.camera.position=[12,10,16]; document.rendering.camera.target=[0,1,0]
  42. document.rendering.toneMappingExposure=0.9;document.rendering.fog={color:0xaabbcc,near:60,far:240}
  43. document.rendering.lights.hemisphere.intensity=1.4;document.rendering.lights.directional.intensity=2.9
  44. await uploadJSON(page,document); await ready(page)
  45. await expect.poll(async()=> (await diagnostics(page)).dirty).toBe(true)
  46. await save(page); await page.reload(); await ready(page)
  47. const reopened=await diagnostics(page)
  48. expect(reopened.objectCount).toBe(initial.objectCount+1)
  49. for(let i=0;i<3;i++) expect(reopened.cameraPosition[i]).toBeCloseTo(document.rendering.camera.position[i],3)
  50. const after=(await call(`tran/v1/content/projects/${id}`,token)).data
  51. expect(after.currentVersion.content.rendering.toneMappingExposure).toBe(0.9)
  52. expect(after.currentVersion.content.rendering.fog).toEqual(document.rendering.fog)
  53. expect(after.currentVersion.content.rendering.lights.hemisphere.intensity).toBe(1.4)
  54. const savedAsset=after.currentVersion.assets.find((a:any)=>a.code===added.assetCode)
  55. expect(savedAsset.type).toBe('MODEL_FILE'); expect(savedAsset.status).toBe('READY')
  56. const savedObject=after.currentVersion.content.objects.find((o:any)=>o.sceneId===added.sceneId)
  57. expect(savedObject.resourceUrl).toMatch(/^content:\/\/asset\/scene\//)
  58. await shot(page,info,'12-json-glb-reopened')
  59. evidence('json-glb-roundtrip',{projectId:id,initial,added,savedObject,reopened,rendering:after.currentVersion.content.rendering})
  60. })
  61. test('保存失败与封面降级:503 保留草稿,封面上传失败保留旧图,恢复后可重试', async ({ page },info) => {
  62. const token=await signIn(page), copy=await freshScene(token,'fox','失败恢复'), id=copy.project.id
  63. await openEditor(page,id); const initial=await diagnostics(page), fox=initial.objects.find((o:any)=>o.targetProjectId==='157')
  64. await choose(page,fox.sceneId); await rename(page,'失败后继续保存的狐狸')
  65. const putEndpoint=`**/api/tran/v1/content/projects/${id}`
  66. await page.route(putEndpoint,route=>route.request().method()==='PUT'?route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:503,message:'验收模拟保存服务不可用'})}):route.continue())
  67. await action(page,'save').click(); await expect(page.locator('.se-toast.warn').last()).toContainText(/保存失败/)
  68. expect((await diagnostics(page)).dirty).toBe(true)
  69. expect((await call(`tran/v1/content/projects/${id}`,token)).data.project.version).toBe(copy.project.version)
  70. await shot(page,info,'13-save-failure-keeps-draft'); await page.unroute(putEndpoint)
  71. const uploadEndpoint=`**/api/tran/v1/content/projects/${id}/assets/upload`
  72. await page.route(uploadEndpoint,route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:503,message:'验收模拟封面上传失败'})}))
  73. await action(page,'save').click(); await expect(page.locator('.se-toast.warn').last()).toContainText(/封面/)
  74. await expect.poll(async()=> (await diagnostics(page)).dirty).toBe(false)
  75. const degraded=(await call(`tran/v1/content/projects/${id}`,token)).data
  76. expect(degraded.project.coverUri).toBe(copy.project.coverUri)
  77. expect(degraded.currentVersion.content.objects.find((o:any)=>o.sceneId===fox.sceneId).name).toBe('失败后继续保存的狐狸')
  78. await shot(page,info,'14-cover-failure-keeps-old-image'); await page.unroute(uploadEndpoint)
  79. await rename(page,'网络恢复后的狐狸'); await save(page); await page.reload(); await ready(page)
  80. const recovered=await diagnostics(page); expect(recovered.objects.find((o:any)=>o.sceneId===fox.sceneId).name).toBe('网络恢复后的狐狸')
  81. await shot(page,info,'15-save-retry-success')
  82. evidence('save-cover-failure',{projectId:id,originalVersion:copy.project.version,degradedVersion:degraded.project.version,coverPreserved:true,recovered})
  83. })
  84. test('同版本恢复与 409:真实未保存草稿重载、服务器新版本保护与隔离下载', async ({ page },info) => {
  85. const token=await signIn(page), copy=await freshScene(token,'truck','恢复冲突'), id=copy.project.id
  86. await openEditor(page,id); const first=await diagnostics(page), truck=first.objects.find((o:any)=>o.targetProjectId==='156')
  87. await choose(page,truck.sceneId); await rename(page,'真实未保存卡车恢复')
  88. await expect.poll(()=>page.evaluate(()=>Object.keys(localStorage).filter(k=>k.includes('scene')&&k.includes('recovery')).length)).toBeGreaterThan(0)
  89. await page.reload()
  90. await expect(page.getByRole('button',{name:/恢复.*(副本|草稿|修改)|使用恢复/})).toBeVisible({timeout:60000})
  91. await page.getByRole('button',{name:/恢复.*(副本|草稿|修改)|使用恢复/}).click(); await ready(page)
  92. expect((await diagnostics(page)).dirty).toBe(true)
  93. expect((await call(`tran/v1/content/projects/${id}`,token)).data.project.version).toBe(copy.project.version)
  94. await shot(page,info,'16-real-unsaved-recovery'); await save(page)
  95. const current=(await call(`tran/v1/content/projects/${id}`,token)).data
  96. await choose(page,truck.sceneId); await rename(page,'旧会话冲突中的卡车')
  97. const external=await call(`tran/v1/content/projects/${id}`,token,{
  98. ...current.project, name:current.project.name, content:current.currentVersion.content,
  99. assets:current.currentVersion.assets, dependencies:current.currentVersion.dependencies, version:current.project.version,
  100. },'PUT')
  101. expect(external.status).toBe(200)
  102. await action(page,'save').click(); await expect(page.locator('.se-toast.warn').last()).toContainText(/其他人|版本|冲突/)
  103. expect((await diagnostics(page)).dirty).toBe(true)
  104. await shot(page,info,'17-conflict-does-not-overwrite')
  105. await page.reload(); await expect(page.locator('.el-message-box')).toBeVisible({timeout:60000})
  106. const prompt=await page.locator('.el-message-box').innerText()
  107. await shot(page,info,'18-stale-recovery-explicit-decision')
  108. const downloadPromise=page.waitForEvent('download')
  109. await page.getByRole('button',{name:/下载/}).last().click(); const download=await downloadPromise
  110. const file=path.join(work,`scene-conflict-${id}.json`); await download.saveAs(file)
  111. await ready(page)
  112. expect((await diagnostics(page)).objects.find((o:any)=>o.sceneId===truck.sceneId).name).toBe('真实未保存卡车恢复')
  113. evidence('real-recovery-conflict',{projectId:id,prompt,originalVersion:copy.project.version,newVersion:external.data.project.version,recovered:await diagnostics(page)})
  114. })
  115. test('发布:前端绑定校验、保存与发布失败阻断、真实固定版本供训练目录使用', async ({ page },info) => {
  116. const token=await signIn(page), copy=await freshScene(token,'truck','发布'), id=copy.project.id
  117. await openEditor(page,id); const initial=await diagnostics(page), truck=initial.objects.find((o:any)=>o.targetProjectId==='156')
  118. await choose(page,truck.sceneId); const input=page.locator('[data-inspector-field="deviceId"]')
  119. await input.fill(''); await input.press('Tab'); await action(page,'verify').click()
  120. await expect(page.locator('#se-bottom-content')).toContainText(/设备/)
  121. await action(page,'publish').click(); await expect(page.locator('.se-toast.warn').last()).toContainText(/检查未通过/)
  122. expect((await call(`tran/v1/content/projects/${id}`,token)).data.project.status).toBe('DRAFT')
  123. await input.fill('SCENE-TRUCK-CLOSURE'); await input.press('Tab')
  124. const endpoint=`**/api/tran/v1/content/projects/${id}/publish`
  125. await page.route(endpoint,route=>route.fulfill({status:503,contentType:'application/json',body:JSON.stringify({code:503,message:'验收模拟发布失败'})}))
  126. await action(page,'publish').click(); await expect(page.locator('.se-toast.warn').last()).toContainText(/发布失败/)
  127. expect((await call(`tran/v1/content/projects/${id}`,token)).data.project.status).toBe('DRAFT')
  128. await shot(page,info,'19-publish-failure-stays-draft'); await page.unroute(endpoint)
  129. await action(page,'publish').click(); await expect(page.locator('#se-save-state')).toHaveText('已发布',{timeout:60000})
  130. const published=(await call(`tran/v1/content/projects/${id}`,token)).data
  131. expect(published.project.status).toBe('PUBLISHED'); expect(published.project.publishedVersionId).toBe(published.currentVersion.id)
  132. const modelDependency=published.currentVersion.dependencies.find((d:any)=>d.targetProjectId==='156')
  133. expect(modelDependency.targetVersionId).toBe(truck.targetVersionId); expect(modelDependency.required).toBe(true)
  134. const catalog=await call('tran/v1/content/catalog?type=SCENE&size=100',token)
  135. expect(catalog.status).toBe(200)
  136. const row=(catalog.data.records||catalog.data).find((r:any)=>r.projectId===id)
  137. expect(row).toBeTruthy()
  138. await shot(page,info,'20-scene-published-fixed-version')
  139. await action(page,'back').click();await expect(page).toHaveURL(/\/content\/scenes(?:\?|$)/)
  140. await page.getByPlaceholder('搜索场景名称或编码').fill(copy.project.name)
  141. await page.getByRole('button',{name:'搜索',exact:true}).click()
  142. await expect(page.locator('.el-loading-mask:visible')).toHaveCount(0)
  143. await expect(page.locator('.asset-row-list > article')).toHaveCount(1)
  144. const card=page.locator('.asset-row-list > article').filter({hasText:copy.project.name})
  145. await expect(card).toHaveCount(1);await expect(card).toContainText('已发布')
  146. await expect.poll(()=>card.locator('img').evaluate((img:any)=>img.complete&&img.naturalWidth>0)).toBe(true)
  147. await shot(page,info,'21-published-scene-cover-in-list')
  148. evidence('publish-fixed-version',{projectId:id,versionId:published.currentVersion.id,dependency:modelDependency,catalog:row,coverUri:published.project.coverUri})
  149. })