|
- import { test, expect, type Frame, type Page } from '@playwright/test'
- import { signIn, openEditor, readyFrame, saveEditor, selectMesh, shot, closeSession, evidence, fixture, writeFixture, call } from './model-editor-closure-helpers'
-
- test.afterEach(async ({ page }) => { await closeSession(page) })
- async function copy(token: string, label: string) {
- const source = fixture().originals.fox.id
- const original = await call(`tran/v1/content/projects/${source}`, token)
- const result = await call(`tran/v1/content/projects/${source}/copy`, token, { name: `闭环验收-${label}-${Date.now()}`, version: original.data.project.version })
- expect(result.status).toBe(200)
- const id = String(result.data.project.id)
- const data = fixture(); data.extraProjectIds.push(id); writeFixture(data)
- return id
- }
- async function pose(frame: Frame) {
- return frame.evaluate(() => {
- const o = (window as any).editorApp.modelPivot
- return { position: o.position.toArray(), rotation: o.rotation.toArray().slice(0, 3), scale: o.scale.toArray() }
- })
- }
- async function dragGizmo(page: Page, frame: Frame, mode: string) {
- await frame.locator(`[data-transform="${mode}"]`).click()
- const box = await frame.locator('#render-host canvas').first().boundingBox()
- expect(box).toBeTruthy()
- const points = await frame.evaluate(mode => {
- const e = (window as any).editorApp
- const handles = e.transform._gizmo.gizmo[mode].children.filter((o: any) => o.visible && o.name === 'X')
- const points: { x: number; y: number }[] = []
- for (const handle of handles) {
- const attr = handle.geometry?.attributes?.position
- if (!attr) continue
- handle.geometry.computeBoundingBox()
- const center = handle.geometry.boundingBox.getCenter(e.camera.position.clone())
- const candidates = [center]
- 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))
- for (const v of candidates) {
- v.applyMatrix4(handle.matrixWorld).project(e.camera)
- points.push({ x: (v.x + 1) / 2, y: (1 - v.y) / 2 })
- }
- }
- return points
- }, mode)
- let hit: { x: number; y: number } | undefined
- for (const point of points) {
- if (point.x < .05 || point.x > .95 || point.y < .05 || point.y > .95) continue
- const p = { x: box!.x + point.x * box!.width, y: box!.y + point.y * box!.height }
- await page.mouse.move(p.x, p.y)
- if (await frame.evaluate(() => (window as any).editorApp.transform.axis === 'X')) { hit = p; break }
- }
- expect(hit, `Real ${mode} X handle can be hovered`).toBeTruthy()
- await page.mouse.down()
- expect(await frame.evaluate(() => (window as any).editorApp.transform.dragging)).toBe(true)
- await page.mouse.move(hit!.x + 46, hit!.y - 31, { steps: 12 })
- await page.mouse.up()
- expect(await frame.evaluate(() => (window as any).editorApp.transform.dragging)).toBe(false)
- }
-
- test('真实三维移动、旋转、缩放手柄拖动及撤销重做保存重开', async ({ page }, info) => {
- const token = await signIn(page)
- const id = await copy(token, '三维手柄')
- let frame = await openEditor(page, id)
- await selectMesh(frame)
- await frame.locator('[data-selection-scope="whole"]').click()
- await frame.locator('[data-action="focus"]').first().click()
- let previous = await pose(frame)
- const changes: unknown[] = []
- for (const [mode, key] of [['translate', 'position'], ['rotate', 'rotation'], ['scale', 'scale']] as const) {
- await dragGizmo(page, frame, mode)
- const next = await pose(frame)
- expect(next[key]).not.toEqual(previous[key])
- changes.push({ mode, before: previous[key], after: next[key] })
- previous = next
- }
- await frame.locator('[data-action="undo"]').first().click()
- await expect.poll(async () => (await pose(frame)).scale).not.toEqual(previous.scale)
- await frame.locator('[data-action="redo"]').first().click()
- await expect.poll(async () => (await pose(frame)).scale).toEqual(previous.scale)
- await saveEditor(frame)
- await shot(page, info, '51-transform-gizmos')
- await page.reload(); frame = await readyFrame(page)
- const reopened = await pose(frame)
- 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)
- evidence('51-gizmos', { status: 'PASS', kind: 'e2e+runtime-readback', testTitle: info.title, featureIds: ['view.move', 'view.rotate', 'view.scale'], projectId: id, changes, reopened })
- })
-
- test('未保存离开:关闭确认保留工作、保存失败留在原页、重试保存后离开', async ({ page }, info) => {
- const token = await signIn(page)
- const id = await copy(token, '离开保护')
- const frame = await openEditor(page, id)
- await selectMesh(frame)
- await frame.locator('#custom-resource-code').fill('UNSAVED-LEAVE-CLOSURE')
- await frame.locator('#custom-resource-code').dispatchEvent('change')
- const list = page.getByRole('link', { name: '模型制作', exact: true })
- await list.click()
- const dialog = page.locator('.el-message-box').filter({ hasText: '当前模型有未保存修改' })
- await expect(dialog).toBeVisible()
- await dialog.locator('.el-message-box__headerbtn').click()
- await expect(dialog).toBeHidden()
- await expect(page).toHaveURL(new RegExp(`/models/${id}/edit`))
- await expect(frame.locator('#custom-resource-code')).toHaveValue('UNSAVED-LEAVE-CLOSURE')
- const endpoint = `**/api/tran/v1/content/projects/${id}`
- await page.route(endpoint, async route => route.request().method() === 'PUT'
- ? route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 503, message: '验收模拟保存失败' }) }) : route.continue())
- await list.click()
- await dialog.getByRole('button', { name: '保存并离开', exact: true }).click()
- await expect.poll(() => frame.evaluate(() => (window as any).editorApp.apiState)).toBe('error')
- await expect(page).toHaveURL(new RegExp(`/models/${id}/edit`))
- await expect(frame.locator('#custom-resource-code')).toHaveValue('UNSAVED-LEAVE-CLOSURE')
- await shot(page, info, '52-leave-save-failure')
- await page.unroute(endpoint)
- await list.click()
- await dialog.getByRole('button', { name: '保存并离开', exact: true }).click()
- await expect(page).toHaveURL(/\/content\/models$/)
- const stored = await call(`tran/v1/content/projects/${id}`, token)
- expect(stored.status, stored.message).toBe(200)
- expect(JSON.stringify(stored.data.currentVersion.content)).toContain('UNSAVED-LEAVE-CLOSURE')
- evidence('52-leave', { status: 'PASS', kind: 'e2e+api', testTitle: info.title, featureIds: ['project.leave'], projectId: id, closeKeptDraft: true, failureKeptDraft: true, retriedSavePersisted: true })
- })
|