Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 

118 строки
6.6 KiB

  1. import { test, expect, type Frame, type Page } from '@playwright/test'
  2. import { signIn, openEditor, readyFrame, saveEditor, selectMesh, shot, closeSession, evidence, fixture, writeFixture, call } from './model-editor-closure-helpers'
  3. test.afterEach(async ({ page }) => { await closeSession(page) })
  4. async function copy(token: string, label: string) {
  5. const source = fixture().originals.fox.id
  6. const original = await call(`tran/v1/content/projects/${source}`, token)
  7. const result = await call(`tran/v1/content/projects/${source}/copy`, token, { name: `闭环验收-${label}-${Date.now()}`, version: original.data.project.version })
  8. expect(result.status).toBe(200)
  9. const id = String(result.data.project.id)
  10. const data = fixture(); data.extraProjectIds.push(id); writeFixture(data)
  11. return id
  12. }
  13. async function pose(frame: Frame) {
  14. return frame.evaluate(() => {
  15. const o = (window as any).editorApp.modelPivot
  16. return { position: o.position.toArray(), rotation: o.rotation.toArray().slice(0, 3), scale: o.scale.toArray() }
  17. })
  18. }
  19. async function dragGizmo(page: Page, frame: Frame, mode: string) {
  20. await frame.locator(`[data-transform="${mode}"]`).click()
  21. const box = await frame.locator('#render-host canvas').first().boundingBox()
  22. expect(box).toBeTruthy()
  23. const points = await frame.evaluate(mode => {
  24. const e = (window as any).editorApp
  25. const handles = e.transform._gizmo.gizmo[mode].children.filter((o: any) => o.visible && o.name === 'X')
  26. const points: { x: number; y: number }[] = []
  27. for (const handle of handles) {
  28. const attr = handle.geometry?.attributes?.position
  29. if (!attr) continue
  30. handle.geometry.computeBoundingBox()
  31. const center = handle.geometry.boundingBox.getCenter(e.camera.position.clone())
  32. const candidates = [center]
  33. if (mode === 'rotate') for (let i = 0; i < attr.count; i += Math.max(1, Math.floor(attr.count / 24))) candidates.push(e.camera.position.clone().fromBufferAttribute(attr, i))
  34. for (const v of candidates) {
  35. v.applyMatrix4(handle.matrixWorld).project(e.camera)
  36. points.push({ x: (v.x + 1) / 2, y: (1 - v.y) / 2 })
  37. }
  38. }
  39. return points
  40. }, mode)
  41. let hit: { x: number; y: number } | undefined
  42. for (const point of points) {
  43. if (point.x < .05 || point.x > .95 || point.y < .05 || point.y > .95) continue
  44. const p = { x: box!.x + point.x * box!.width, y: box!.y + point.y * box!.height }
  45. await page.mouse.move(p.x, p.y)
  46. if (await frame.evaluate(() => (window as any).editorApp.transform.axis === 'X')) { hit = p; break }
  47. }
  48. expect(hit, `Real ${mode} X handle can be hovered`).toBeTruthy()
  49. await page.mouse.down()
  50. expect(await frame.evaluate(() => (window as any).editorApp.transform.dragging)).toBe(true)
  51. await page.mouse.move(hit!.x + 46, hit!.y - 31, { steps: 12 })
  52. await page.mouse.up()
  53. expect(await frame.evaluate(() => (window as any).editorApp.transform.dragging)).toBe(false)
  54. }
  55. test('真实三维移动、旋转、缩放手柄拖动及撤销重做保存重开', async ({ page }, info) => {
  56. const token = await signIn(page)
  57. const id = await copy(token, '三维手柄')
  58. let frame = await openEditor(page, id)
  59. await selectMesh(frame)
  60. await frame.locator('[data-selection-scope="whole"]').click()
  61. await frame.locator('[data-action="focus"]').first().click()
  62. let previous = await pose(frame)
  63. const changes: unknown[] = []
  64. for (const [mode, key] of [['translate', 'position'], ['rotate', 'rotation'], ['scale', 'scale']] as const) {
  65. await dragGizmo(page, frame, mode)
  66. const next = await pose(frame)
  67. expect(next[key]).not.toEqual(previous[key])
  68. changes.push({ mode, before: previous[key], after: next[key] })
  69. previous = next
  70. }
  71. await frame.locator('[data-action="undo"]').first().click()
  72. await expect.poll(async () => (await pose(frame)).scale).not.toEqual(previous.scale)
  73. await frame.locator('[data-action="redo"]').first().click()
  74. await expect.poll(async () => (await pose(frame)).scale).toEqual(previous.scale)
  75. await saveEditor(frame)
  76. await shot(page, info, '51-transform-gizmos')
  77. await page.reload(); frame = await readyFrame(page)
  78. const reopened = await pose(frame)
  79. for (const key of ['position', 'rotation', 'scale'] as const) for (let i = 0; i < 3; i++) expect(reopened[key][i]).toBeCloseTo(previous[key][i], 5)
  80. evidence('51-gizmos', { status: 'PASS', kind: 'e2e+runtime-readback', testTitle: info.title, featureIds: ['view.move', 'view.rotate', 'view.scale'], projectId: id, changes, reopened })
  81. })
  82. test('未保存离开:关闭确认保留工作、保存失败留在原页、重试保存后离开', async ({ page }, info) => {
  83. const token = await signIn(page)
  84. const id = await copy(token, '离开保护')
  85. const frame = await openEditor(page, id)
  86. await selectMesh(frame)
  87. await frame.locator('#custom-resource-code').fill('UNSAVED-LEAVE-CLOSURE')
  88. await frame.locator('#custom-resource-code').dispatchEvent('change')
  89. const list = page.getByRole('link', { name: '模型制作', exact: true })
  90. await list.click()
  91. const dialog = page.locator('.el-message-box').filter({ hasText: '当前模型有未保存修改' })
  92. await expect(dialog).toBeVisible()
  93. await dialog.locator('.el-message-box__headerbtn').click()
  94. await expect(dialog).toBeHidden()
  95. await expect(page).toHaveURL(new RegExp(`/models/${id}/edit`))
  96. await expect(frame.locator('#custom-resource-code')).toHaveValue('UNSAVED-LEAVE-CLOSURE')
  97. const endpoint = `**/api/tran/v1/content/projects/${id}`
  98. await page.route(endpoint, async route => route.request().method() === 'PUT'
  99. ? route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 503, message: '验收模拟保存失败' }) }) : route.continue())
  100. await list.click()
  101. await dialog.getByRole('button', { name: '保存并离开', exact: true }).click()
  102. await expect.poll(() => frame.evaluate(() => (window as any).editorApp.apiState)).toBe('error')
  103. await expect(page).toHaveURL(new RegExp(`/models/${id}/edit`))
  104. await expect(frame.locator('#custom-resource-code')).toHaveValue('UNSAVED-LEAVE-CLOSURE')
  105. await shot(page, info, '52-leave-save-failure')
  106. await page.unroute(endpoint)
  107. await list.click()
  108. await dialog.getByRole('button', { name: '保存并离开', exact: true }).click()
  109. await expect(page).toHaveURL(/\/content\/models$/)
  110. const stored = await call(`tran/v1/content/projects/${id}`, token)
  111. expect(stored.status, stored.message).toBe(200)
  112. expect(JSON.stringify(stored.data.currentVersion.content)).toContain('UNSAVED-LEAVE-CLOSURE')
  113. evidence('52-leave', { status: 'PASS', kind: 'e2e+api', testTitle: info.title, featureIds: ['project.leave'], projectId: id, closeKeptDraft: true, failureKeptDraft: true, retriedSavePersisted: true })
  114. })