|
- import { test, expect, type Page } from '@playwright/test'
- import {
- action, call, closeSession, diagnostics, evidence, exportDocument,
- freshScene, openEditor, ready, save, shot, signIn,
- } from './scene-editor-closure-helpers'
-
- const templates = [
- { id: 'depot-1', name: '真实装备车库', type: '车间', count: 1, builtIn: 'vehicle-depot' },
- { id: 'terrain-1', name: '真实野外训练场', type: '室外', count: 4, builtIn: 'field-terrain' },
- { id: 'workshop-1', name: '综合检验车间', type: '车间', count: 0 },
- { id: 'workshop-real', name: '演示维修车间(可拆分)', type: '车间', count: 74, builtIn: 'maintenance-workshop' },
- { id: 'workshop-2', name: '总成拆卸车间', type: '车间', count: 3, types: ['equipment', 'workbench', 'shelf'] },
- { id: 'workshop-3', name: '部件分解车间', type: '车间', count: 3, types: ['workbench', 'shelf', 'camera'] },
- { id: 'workshop-4', name: '清洗检测车间', type: '车间', count: 4, types: ['diagnostic', 'workbench', 'camera', 'safety-zone'] },
- { id: 'workshop-5', name: '复装调试车间', type: '车间', count: 4, types: ['diagnostic', 'workbench', 'light', 'worker'] },
- { id: 'outdoor-1', name: '室外综合训练场', type: '室外', count: 3, types: ['equipment', 'safety-zone', 'light'] },
- ]
- const basics = ['diagnostic', 'equipment', 'workbench', 'shelf', 'safety-zone', 'camera', 'worker', 'light']
- const tab = (page: Page, name: string) => page.locator(`[data-left-tab="${name}"]`)
- const card = (page: Page, id: string) => page.locator(`[data-preset-id="${id}"]`)
- const field = (page: Page, name: string) => page.locator(`[data-inspector-field="${name}"]`)
- async function selectTemplate(page: Page, id: string) {
- await tab(page, 'scenes').click()
- await page.locator('#se-search').fill('')
- await card(page, id).click()
- const dialog = page.getByRole('dialog', { name: '切换场景模板' })
- if (await dialog.isVisible()) await dialog.getByRole('button', { name: '放弃修改并切换' }).click()
- await expect.poll(async () => {
- const d = await diagnostics(page)
- return d?.currentPreset?.id === id && d.pendingLoads === 0
- }, { timeout: 120_000 }).toBe(true)
- await expect(card(page, id)).toHaveClass(/active/)
- }
- async function getSaved(token: string, id: string) {
- const response = await call(`tran/v1/content/projects/${id}`, token)
- expect(response.status).toBe(200)
- expect(response.data.currentVersion).toBeTruthy()
- return response.data.currentVersion
- }
- async function realDrag(page: Page, source: ReturnType<Page['locator']>, x = 0.48, y = 0.57) {
- await source.scrollIntoViewIfNeeded()
- const start = await source.boundingBox(), target = await page.locator('#se-viewport canvas').boundingBox()
- expect(start).not.toBeNull(); expect(target).not.toBeNull()
- await page.mouse.move(start!.x + start!.width / 2, start!.y + start!.height / 2)
- await page.mouse.down()
- await page.mouse.move(start!.x + start!.width / 2 + 12, start!.y + start!.height / 2, { steps: 3 })
- await page.mouse.move(target!.x + target!.width * x, target!.y + target!.height * y, { steps: 12 })
- await page.mouse.up()
- }
- async function clearSelectionForScreenshot(page: Page) {
- const canvas = page.locator('#se-viewport canvas'), box = await canvas.boundingBox()
- expect(box).not.toBeNull()
- await canvas.click({ position: { x: box!.width - 28, y: 95 } })
- await expect.poll(async () => (await diagnostics(page)).selectedSceneId).toBe('')
- await expect(page.locator('.se-toast, .el-message')).toHaveCount(0)
- }
- test.afterEach(async ({ page }) => { await closeSession(page) })
-
- for (const [index, preset] of templates.entries()) {
- test(`场景模板实际加载、保存重开:${preset.name}`, async ({ page }, info) => {
- const token = await signIn(page), created = await freshScene(token, 'fox', preset.id)
- const id = created.project.id
- await openEditor(page, id)
- await selectTemplate(page, preset.id)
- const loaded = await diagnostics(page)
- expect(loaded.currentPreset).toEqual({ id: preset.id, name: preset.name, type: preset.type })
- expect(loaded.objectCount).toBe(preset.count)
- if (preset.types) expect(loaded.objects.map((o: any) => o.type).sort()).toEqual([...preset.types].sort())
- await expect(page.locator('#se-preset-badge')).toContainText(`${preset.type}环境`)
- await save(page)
- const saved = await getSaved(token, id)
- expect(saved.content.presetId).toBe(preset.id)
- expect(saved.content.environment.presetId).toBe(preset.id)
- if (preset.builtIn) expect(saved.content.objects.some((o: any) => o.builtinAssetId === preset.builtIn)).toBe(true)
- await page.reload(); await ready(page)
- const reopened = await diagnostics(page)
- expect(reopened.currentPreset.id).toBe(preset.id)
- expect(reopened.objectCount).toBe(preset.count)
- expect(reopened.dirty).toBe(false)
- await tab(page, 'scenes').click()
- await expect(card(page, preset.id)).toHaveClass(/active/)
- await expect(page.locator('#se-preset-badge')).toContainText(`${preset.type}环境`)
- const name = `${20 + index}-template-${preset.id}`
- await shot(page, info, name)
- evidence(`template-${preset.id}`, { kind: 'e2e', status: 'PASS', testTitle: info.title,
- featureIds: [`template.${preset.id}`], projectId: id, presetId: preset.id,
- objectCount: reopened.objectCount, bounds: reopened.contentBounds,
- scope: '真实模板按钮、选中标签、环境字段、种子对象、PUT 后刷新重开;截图验证对应模板实际画面。', screenshots: [`${name}.png`] })
- })
- }
-
- test('模板搜索与未保存切换确认保留和放弃', async ({ page }, info) => {
- const token = await signIn(page), created = await freshScene(token, 'fox', 'template-confirm')
- await openEditor(page, created.project.id)
- const original = await diagnostics(page), root = original.objects.find((o: any) => o.targetProjectId)
- await page.locator(`[data-object-id="${root.sceneId}"]`).first().click()
- await field(page, 'name').fill('模板确认保留对象'); await field(page, 'name').press('Tab')
- await tab(page, 'scenes').click()
- await page.locator('#se-search').fill('清洗')
- await expect(page.locator('.se-scene-card:visible')).toHaveCount(1)
- await expect(card(page, 'workshop-4')).toBeVisible()
- await page.locator('#se-search').fill('无匹配模板-closure')
- await expect(page.locator('.se-scene-card:visible')).toHaveCount(0)
- await page.locator('#se-search').fill(''); await expect(page.locator('.se-scene-card:visible')).toHaveCount(9)
- await card(page, 'workshop-1').click()
- const dialog = page.getByRole('dialog', { name: '切换场景模板' })
- await expect(dialog).toBeVisible()
- await dialog.getByRole('button', { name: '继续编辑' }).click()
- expect((await diagnostics(page)).currentPreset.id).toBe(original.currentPreset.id)
- expect((await diagnostics(page)).objects.some((o: any) => o.name === '模板确认保留对象')).toBe(true)
- await card(page, 'workshop-1').click(); await dialog.getByRole('button', { name: '放弃修改并切换' }).click()
- await expect.poll(async () => (await diagnostics(page)).objectCount).toBe(0)
- await expect(card(page, 'workshop-1')).toHaveClass(/active/)
- await save(page)
- const saved = await getSaved(token, created.project.id)
- expect(saved.content.presetId).toBe('workshop-1')
- await shot(page, info, '29-template-confirm-empty')
- evidence('template-confirm', { kind: 'e2e', status: 'PASS', testTitle: info.title,
- featureIds: ['panel.left-tabs', 'search.templates', 'template.switch-cancel'], projectId: created.project.id,
- scope: '中文/无匹配/清空搜索;真实取消保留对象,真实确认放弃并切换空白模板后保存。', screenshots: ['29-template-confirm-empty.png'] })
- })
-
- test('八种基础资源逐项放置、真实拖放、搜索和保存重开', async ({ page }, info) => {
- const token = await signIn(page), created = await freshScene(token, 'fox', 'basic-assets')
- await openEditor(page, created.project.id); await selectTemplate(page, 'workshop-1')
- await tab(page, 'assets').click()
- await page.locator('#se-search').fill('检修工作台')
- await expect(page.locator('.se-asset:visible')).toHaveCount(1)
- await expect(page.locator('[data-asset-id="workbench"]')).toBeVisible()
- await page.locator('#se-search').fill('无匹配资源-closure')
- await expect(page.locator('.se-asset:visible')).toHaveCount(0)
- await page.locator('#se-search').fill('')
- for (const [index, id] of basics.entries()) {
- await page.locator(`[data-asset-id="${id}"]`).dblclick()
- await expect.poll(async () => (await diagnostics(page)).objectCount).toBe(index + 1)
- await field(page, 'x').fill(String((index % 4) * 4 - 6)); await field(page, 'x').press('Tab')
- await field(page, 'z').fill(String(Math.floor(index / 4) * 5 - 2)); await field(page, 'z').press('Tab')
- }
- await realDrag(page, page.locator('[data-asset-id="workbench"]'), 0.7, 0.65)
- await expect.poll(async () => (await diagnostics(page)).objectCount).toBe(9)
- const placed = await diagnostics(page), dragged = placed.objects.at(-1)
- expect(dragged.type).toBe('workbench')
- expect(dragged.position.some((v: number) => Math.abs(v) > 0.1)).toBe(true)
- for (const axis of [0, 2]) expect(dragged.position[axis] * 2).toBeCloseTo(Math.round(dragged.position[axis] * 2), 5)
- await expect(page.locator('#se-drop-hint')).not.toHaveClass(/show/)
- expect(new Set(placed.objects.map((o: any) => o.sceneId)).size).toBe(9)
- expect(new Set(placed.objects.map((o: any) => o.deviceId)).size).toBe(9)
- await save(page); await page.reload(); await ready(page)
- const reopened = await diagnostics(page)
- expect(reopened.objects.map((o: any) => ({ type: o.type, position: o.position, deviceId: o.deviceId })))
- .toEqual(placed.objects.map((o: any) => ({ type: o.type, position: o.position, deviceId: o.deviceId })))
- await tab(page, 'outline').click()
- await expect(page.locator('.se-object-summary button')).toHaveCount(6)
- await expect(page.locator('.se-object-summary-note')).toContainText('共 9 个对象,此处显示前 6 个')
- const firstSummary = page.locator('.se-object-summary button').first()
- const firstSummaryId = await firstSummary.getAttribute('data-object-id')
- await firstSummary.click()
- expect((await diagnostics(page)).selectedSceneId).toBe(firstSummaryId)
- await expect(field(page, 'name')).toHaveValue(reopened.objects.find((o: any) => o.sceneId === firstSummaryId).name)
- await shot(page, info, '30-eight-basic-assets-reopened')
- evidence('basic-assets', { kind: 'e2e', status: 'PASS', testTitle: info.title,
- featureIds: [...basics.map(id => `asset.${id}`), 'assets.basic-double-click', 'assets.basic-drop', 'search.assets', 'panel.objects'],
- projectId: created.project.id, dragged: { position: dragged.position, type: dragged.type },
- scope: '8 种基础资源分别双击放置并分开布置;真实指针拖入第 9 个工作台、吸附检查、唯一编号、保存刷新一致。照明为现有资源外观,不冒称连接真实灯控。', screenshots: ['30-eight-basic-assets-reopened.png'] })
- })
-
- test('两种整体场地加载、重复放置阻止与显式删除后替换', async ({ page }, info) => {
- const token = await signIn(page), created = await freshScene(token, 'fox', 'venue-assets')
- await openEditor(page, created.project.id); await selectTemplate(page, 'workshop-1')
- await tab(page, 'assets').click()
- await page.locator('[data-asset-id="vehicle-depot"]').dblclick()
- await expect.poll(async () => { const d = await diagnostics(page); return d.objectCount === 1 && d.pendingLoads === 0 }, { timeout: 120000 }).toBe(true)
- const depot = await exportDocument(page)
- expect(depot.objects.some((o: any) => o.builtinAssetId === 'vehicle-depot')).toBe(true)
- await page.locator('[data-asset-id="field-terrain"]').dblclick()
- expect((await diagnostics(page)).objectCount).toBe(1)
- expect((await exportDocument(page)).objects.some((o: any) => o.builtinAssetId === 'field-terrain')).toBe(false)
- await save(page); await page.reload(); await ready(page)
- await shot(page, info, '31-venue-depot-reopened')
- const root = (await diagnostics(page)).objects[0]
- await page.locator(`[data-object-id="${root.sceneId}"]`).first().click()
- await action(page, 'delete').click()
- await expect.poll(async () => (await diagnostics(page)).objectCount).toBe(0)
- await tab(page, 'assets').click(); await page.locator('[data-asset-id="field-terrain"]').dblclick()
- await expect.poll(async () => { const d = await diagnostics(page); return d.objectCount === 1 && d.pendingLoads === 0 }, { timeout: 120000 }).toBe(true)
- await save(page); await page.reload(); await ready(page)
- const saved = await getSaved(token, created.project.id)
- expect(saved.content.objects.some((o: any) => o.builtinAssetId === 'field-terrain')).toBe(true)
- expect(saved.content.objects.some((o: any) => o.builtinAssetId === 'vehicle-depot')).toBe(false)
- const terrain = await diagnostics(page)
- expect(terrain.contentBounds.size[1]).toBeGreaterThan(1)
- await shot(page, info, '32-venue-terrain-reopened')
- evidence('venue-assets', { kind: 'e2e', status: 'PASS', testTitle: info.title,
- featureIds: ['asset.vehicle-depot', 'asset.field-terrain', 'object.delete'], projectId: created.project.id,
- scope: '车库和起伏地形真实加载并分别保存重开;已有整体场地时阻止叠加,显式删除旧场地后才能替换。', screenshots: ['31-venue-depot-reopened.png', '32-venue-terrain-reopened.png'] })
- })
-
- test('模型成果狐狸卡车双击与真实拖入、树搜索和部件隐藏重开', async ({ page }, info) => {
- const token = await signIn(page), created = await freshScene(token, 'truck', 'published-model-assets')
- await openEditor(page, created.project.id); await selectTemplate(page, 'workshop-1')
- await tab(page, 'assets').click()
- const fox = page.locator('.se-editor-asset').filter({ hasText: /狐狸/ }).first()
- const truck = page.locator('.se-editor-asset').filter({ hasText: /牛奶卡车/ }).first()
- await expect(fox).toContainText('已发布'); await expect(truck).toContainText('已发布')
- const foxProjectId = await fox.getAttribute('data-target-project-id'), foxVersionId = await fox.getAttribute('data-target-version-id')
- const truckProjectId = await truck.getAttribute('data-target-project-id'), truckVersionId = await truck.getAttribute('data-target-version-id')
- await fox.dblclick()
- await expect.poll(async () => { const d = await diagnostics(page); return d.objectCount === 1 && d.pendingLoads === 0 }).toBe(true)
- await field(page, 'x').fill('-3'); await field(page, 'x').press('Tab')
- await action(page, 'focus').click()
- await clearSelectionForScreenshot(page)
- await shot(page, info, '33-fox-placed-visible')
- await realDrag(page, truck, 0.7, 0.6)
- await expect.poll(async () => { const d = await diagnostics(page); return d.objectCount === 2 && d.pendingLoads === 0 }).toBe(true)
- await field(page, 'x').fill('3'); await field(page, 'x').press('Tab')
- await action(page, 'focus').click()
- const placed = await diagnostics(page), foxRoot = placed.objects.find((o: any) => String(o.targetProjectId) === foxProjectId)
- const truckRoot = placed.objects.find((o: any) => String(o.targetProjectId) === truckProjectId)
- expect(String(foxRoot.targetVersionId)).toBe(foxVersionId); expect(String(truckRoot.targetVersionId)).toBe(truckVersionId)
- expect(foxRoot.parts.length).toBeGreaterThan(0); expect(truckRoot.parts.length).toBeGreaterThan(0)
- await tab(page, 'outline').click()
- await page.locator('#se-search').fill(truckRoot.name)
- await expect(page.locator(`.se-tree-row[data-object-id="${truckRoot.sceneId}"]`)).toBeVisible()
- await expect(page.locator(`.se-tree-row[data-object-id="${foxRoot.sceneId}"]`)).toHaveCount(0)
- const part = truckRoot.parts.find((p: any) => p.capabilities.includes('transform'))
- expect(part).toBeTruthy()
- await page.locator('#se-search').fill(part.name)
- await expect(page.locator(`.se-tree-row[data-object-id="${part.sceneId}"]`)).toBeVisible()
- await page.locator('#se-search').fill('无匹配对象-closure'); await expect(page.locator('.se-tree-row')).toHaveCount(0)
- await page.locator('#se-search').fill('')
- await page.locator(`.se-tree-row[data-object-id="${part.sceneId}"]`).click()
- await expect(field(page, 'visible')).toBeEnabled(); await field(page, 'visible').uncheck()
- await expect.poll(async () => (await diagnostics(page)).objects.find((o: any) => o.sceneId === truckRoot.sceneId).parts.find((p: any) => p.sceneId === part.sceneId).visible).toBe(false)
- await save(page); await page.reload(); await ready(page)
- const reopened = await diagnostics(page), savedTruck = reopened.objects.find((o: any) => o.sceneId === truckRoot.sceneId)
- expect(savedTruck.parts.find((p: any) => p.sceneId === part.sceneId).visible).toBe(false)
- await page.locator(`.se-tree-row[data-object-id="${part.sceneId}"]`).click()
- await expect(field(page, 'visible')).not.toBeChecked()
- // Restore the deliberately hidden body part so the retained screenshot is a complete model.
- await field(page, 'visible').check(); await save(page)
- await page.locator(`.se-tree-row[data-object-id="${truckRoot.sceneId}"]`).click(); await action(page, 'focus').click()
- await clearSelectionForScreenshot(page)
- await shot(page, info, '34-truck-placed-part-restored')
- const saved = await getSaved(token, created.project.id)
- expect(saved.dependencies.filter((d: any) => d.kind === 'SCENE_MODEL' || d.relationType === 'SCENE_MODEL').length).toBe(2)
- evidence('published-model-assets', { kind: 'e2e', status: 'PASS', testTitle: info.title,
- featureIds: ['assets.catalog', 'assets.model-double-click', 'assets.model-drop', 'search.tree', 'part.source', 'property.visible'],
- projectId: created.project.id, sources: [{ projectId: foxProjectId, versionId: foxVersionId }, { projectId: truckProjectId, versionId: truckVersionId }],
- scope: 'API 已发布目录,狐狸双击和卡车真实拖入;固定来源版本、树根/部件搜索;卡车刚体部件隐藏保存重开,最后恢复可见。基线树没有折叠按钮,不将搜索伪称折叠。', screenshots: ['33-fox-placed-visible.png', '34-truck-placed-part-restored.png'] })
- })
|