import { expect, test } from '@playwright/test'; test.describe('Project Canvas Baseline Flow', () => { test('enter project, load canvas, edit prompt node, and trigger save', async ({ page }) => { const savePayloads: Array> = []; await page.route('**/api/v1/**', async (route) => { const req = route.request(); const url = new URL(req.url()); const path = url.pathname; const method = req.method(); if (method === 'GET' && path === '/api/v1/config/models') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, message: 'ok', data: { image: {}, video: {}, llm: {}, audio: {} }, }), }); } if (method === 'GET' && path === '/api/v1/projects/e2e-project') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, message: 'ok', data: { id: 'e2e-project', name: 'E2E Project', type: 'video', status: 'active', created_at: '2026-01-01T00:00:00.000Z', updated_at: '2026-01-01T00:00:00.000Z', episodes: [], assets: [], storyboards: [], generalCanvases: [], }, }), }); } if (method === 'GET' && path === '/api/v1/projects/e2e-project/canvases') { const canvasType = url.searchParams.get('canvas_type'); if (canvasType === 'general') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, message: 'ok', data: [] }), }); } } if (method === 'POST' && path === '/api/v1/projects/e2e-project/canvases') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, message: 'ok', data: { id: 'canvas-main-1', projectId: 'e2e-project', canvasType: 'general', relatedEntityType: null, relatedEntityId: null, name: '通用画布', description: '', orderIndex: 0, isPinned: false, tags: [], nodeCount: 0, accessCount: 0, createdAt: Date.now(), updatedAt: Date.now(), }, }), }); } if (method === 'GET' && path === '/api/v1/canvas') { return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, message: 'ok', data: { id: 'canvas-main-1', nodes: [], connections: [], history: [], }, }), }); } if (method === 'POST' && path === '/api/v1/canvas') { const payload = req.postDataJSON() as Record; savePayloads.push(payload); return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, message: 'ok', data: { id: 'canvas-main-1' } }), }); } return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, message: 'ok', data: {} }), }); }); await page.goto('/project/e2e-project'); await expect( page.getByPlaceholder('有什么可以帮您?(可输入 @ 选择素材)') ).toBeVisible(); await page.getByTestId('canvas-add-node-toggle').click(); await page.getByTestId('add-node-prompt_input').click(); const promptTextarea = page.getByTestId('prompt-input-textarea'); await expect(promptTextarea).toBeVisible(); await promptTextarea.fill('E2E: prompt update should be persisted'); await page.waitForTimeout(900); await page.keyboard.press('Control+S'); await expect.poll(() => savePayloads.length, { timeout: 10000 }).toBeGreaterThan(0); await expect.poll( () => savePayloads.some((payload) => { const nodes = (payload.nodes ?? []) as Array<{ data?: { type?: string; prompt?: string } }>; return nodes.some( (node) => node.data?.type === 'PROMPT_INPUT' && node.data?.prompt?.includes('E2E: prompt update should be persisted') ); }), { timeout: 10000 } ).toBeTruthy(); }); });