import { test, expect, type Page } from '@playwright/test'; import { createServer, type ViteDevServer } from 'vite'; import { resolve } from 'node:path'; import { EMPTY_CLOUD_CONFIGURATION, type CloudAgentDraft } from '../../shared/cloud-agents'; let server: ViteDevServer; let baseUrl: string; test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' }); // Render the real workspace and keep all service calls at the Main IPC boundary. // This suite never starts Electron, opens a native file picker or calls a live service. test.beforeAll(async () => { const entry = resolve('tests/e2e/__cloud-agent-platform-avatars-harness.tsx'); server = await createServer({ configFile: false, cacheDir: 'node_modules/.vite-cloud-agent-platform-avatars', resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' }, server: { host: '127.0.0.1', port: 0 }, plugins: [{ name: 'isolated-cloud-agent-platform-avatars', enforce: 'pre', resolveId(id) { if (id === '/__cloud-agent-platform-avatars-harness.tsx') return entry; }, load(id) { if (id === entry) return ` import { createRoot } from 'react-dom/client'; import { createMemoryRouter, RouterProvider } from 'react-router-dom'; import CloudAgents from '/src/pages/CloudAgents/index.tsx'; import '/src/styles/globals.css'; const router = createMemoryRouter([{ path: '/cloud-agents', element: }], { initialEntries: ['/cloud-agents?view=mine'] }); createRoot(document.getElementById('root')).render(
); `; if (id.endsWith('/src/stores/auth.ts')) return 'export function useAuthStore(select) { return select({ user: { userId: "avatar-fixture" } }); }'; }, configureServer(vite) { vite.middlewares.use((req, res, next) => { if (req.url !== '/' && !req.url?.startsWith('/?')) return next(); res.setHeader('Content-Type', 'text/html'); res.end('
'); }); }, }], }); await server.listen(); baseUrl = server.resolvedUrls!.local[0]; }); test.afterAll(async () => { await server?.close(); }); const makeAgent = (character: string, name: string, published: boolean, extra: Partial = {}): CloudAgentDraft => ({ slug: 'ml-' + character.repeat(32), name, purpose: `让${name}按照我的要求帮助我。`, system_prompt: '用简单清楚的中文回答,先理解我的问题。', draft_revision: 2, updated_at: '2026-10-12T00:00:00Z', configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'fixture-model' }, published_version: published ? 1 : null, enabled: true, ...extra, }); const readyAgent = makeAgent('a', '我的故事搭档', true); const draftAgent = makeAgent('b', '我的英语陪练', false); const disabledAgent = makeAgent('c', '暂停使用的助手', true, { enabled: false }); type FixtureCall = { path: string; method: string; input: Record | null }; type AvatarFixture = { agents: CloudAgentDraft[]; calls: FixtureCall[]; failPreset: boolean; failSave: boolean }; type FixtureWindow = Window & { __agentAvatarFixture: AvatarFixture }; async function installFixture(page: Page, agents: CloudAgentDraft[]) { page.on('pageerror', error => { throw error; }); await page.addInitScript(({ agents, configuration }) => { const scope = window as unknown as FixtureWindow; scope.__agentAvatarFixture = { agents, calls: [], failPreset: false, failSave: false }; const state = scope.__agentAvatarFixture; const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json: structuredClone(json) } }); const failure = (message: string) => ({ ok: true, data: { status: 503, ok: false, json: { error: message } } }); const avatarUrl = (icon: string | null | undefined) => icon ? '/src/assets/agent-avatars/' + icon.split('preset-')[1] : null; Object.defineProperty(window, 'electron', { value: { ipcRenderer: { invoke: async (channel: string, request?: { path?: string; method?: string; body?: string }) => { if (channel === 'hostapi:token') return 'fixture'; if (channel === 'hostapi:base-url') return 'http://fixture.invalid'; if (channel !== 'hostapi:fetch') throw new Error('Unexpected fixture IPC ' + channel); const path = request?.path ?? ''; const method = request?.method ?? 'GET'; const input = request?.body ? JSON.parse(request.body) as Record : null; state.calls.push({ path, method, input }); if (path === '/api/cloud-agents/recovery') return result({ recent: null, pending: [] }); if (path === '/api/cloud-agents/recovery/recent') return result({ saved: true }); if (path === '/api/cloud-agents/avatars/preset') { if (state.failPreset) return failure('平台头像暂时不可用'); const id = String(input?.id); if (!/^avatar-(0[1-9]|1[0-6])$/.test(id)) throw new Error('Unexpected preset ' + id); return result({ icon: '/api/storage/public/images/preset-' + id + '.png', icon_url: '/src/assets/agent-avatars/' + id + '.png' }); } if (path.startsWith('/api/cloud-agents/agents')) { const url = new URL(path, 'http://fixture.invalid'); if (url.pathname === '/api/cloud-agents/agents') { if (method === 'POST') { const created = { ...configuration, ...input, slug: 'ml-' + 'd'.repeat(32), system_prompt: '', draft_revision: 1, enabled: true, published_version: null, icon_url: avatarUrl(input?.icon as string | null), updated_at: '2026-10-12T00:00:00Z' }; state.agents.unshift(created as CloudAgentDraft); return result(created); } return result({ agents: state.agents, next_cursor: null }); } const agent = state.agents.find(item => item.slug === url.pathname.split('/')[4]); if (!agent) throw new Error('Unknown fixture Agent ' + url.pathname); if (method === 'PATCH') { if (state.failSave) return failure('保存头像失败,请重试'); Object.assign(agent, input, { draft_revision: agent.draft_revision + 1, icon_url: avatarUrl(input?.icon as string | null) }); } return result(agent); } if (path === '/api/cloud-agents/actions') { const args = input?.input as { slug?: string } | undefined; const agent = state.agents.find(item => item.slug === args?.slug) ?? state.agents[0]; switch (input?.operation) { case 'catalog': return result({ models: [{ id: 'fixture-model', name: '创作模型' }], resources: {}, pricing: null }); case 'resources': return result({ mcps: [], skills: [], subagents: [] }); case 'knowledge': return result({ databases: [], models: [] }); case 'budget': return result({ agent_slug: agent?.slug, request_limit_points: null, daily_limit_points: null, daily_committed_points: '0.00', timezone: 'Asia/Shanghai', unit: '词元点数', resets_at: '2026-10-13T00:00:00+08:00' }); case 'access': return result({ published_version: agent?.published_version, enabled: agent?.enabled, share_url: 'niancode://agents/' + agent?.slug, api_url: 'http://fixture.invalid', grants: [], applications: [], versions: agent?.published_version ? [{ version: 1, draft_revision: agent.draft_revision, created_at: '2026-10-12T00:00:00Z' }] : [] }); case 'threads': return result({ threads: [], next_offset: null }); default: throw new Error('Unexpected fixture operation ' + String(input?.operation)); } } throw new Error('Unexpected fixture request ' + path); }, } } }); }, { agents, configuration: { configuration: { ...EMPTY_CLOUD_CONFIGURATION, model: 'fixture-model' } } }); } async function expectNoOverflow(page: Page) { expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); } async function expectImagesLoaded(page: Page, selector = 'img') { await expect.poll(() => page.locator(selector).evaluateAll(images => images.every(image => { const img = image as HTMLImageElement; return img.complete && img.naturalWidth > 0; }))).toBe(true); } async function expectConfigurationBounds(page: Page) { await expectNoOverflow(page); await expect.poll(() => page.getByTestId('cloud-agent-editor').getByLabel('名称', { exact: true }) .evaluate(element => element.getBoundingClientRect().width)).toBeGreaterThanOrEqual(96); // Document width alone misses controls clipped by the editor's overflow container. const clipped = await page.getByTestId('cloud-agent-editor').evaluate(editor => { const scroll = editor.querySelector('.agent-config-scroll')!; const scrollBounds = scroll.getBoundingClientRect(); return [...editor.querySelectorAll('button, input, textarea, select, h2, summary, .agent-config-card')] .filter(element => element.getClientRects().length > 0 && !element.classList.contains('sr-only')) .flatMap(element => { const bounds = element.getBoundingClientRect(); const insideScroll = scroll.contains(element); const left = insideScroll ? scrollBounds.left : 0; const right = insideScroll ? scrollBounds.right : innerWidth; return bounds.left < left - 1 || bounds.right > right + 1 ? [{ element: element.tagName, label: element.getAttribute('aria-label') ?? element.textContent?.trim(), left: bounds.left, right: bounds.right }] : []; }); }); expect(clipped).toEqual([]); } for (const width of [375, 768, 1024, 1440]) { test(`introduction cards and one-page advanced settings fit and scroll at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 850 }); await installFixture(page, [draftAgent]); await page.goto(baseUrl); await page.getByRole('button', { name: '继续制作', exact: true }).click(); const editor = page.getByTestId('cloud-agent-editor'); const tabs = editor.getByRole('tablist', { name: '编辑设置', exact: true }); await expect(tabs.getByRole('tab')).toHaveCount(2); await expect(editor.getByRole('heading', { name: '一句话介绍', exact: true })).toBeVisible(); await expect(editor.getByRole('heading', { name: '提示词', exact: true })).toBeVisible(); await expect(editor.locator('section.agent-config-card[data-tone="blue"]')).toHaveCSS('background-color', 'rgb(241, 246, 255)'); await expect(editor.locator('.agent-config-prompt[data-tone="lilac"]')).toHaveCSS('background-color', 'rgb(246, 242, 255)'); await editor.getByLabel('一句话介绍', { exact: true }).focus(); await expect(editor.getByLabel('一句话介绍', { exact: true })).toBeFocused(); await page.keyboard.press('Tab'); await expect(editor.getByLabel('提示词', { exact: true })).toBeFocused(); await expectConfigurationBounds(page); if (width === 375 || width === 1440) { await page.screenshot({ path: test.info().outputPath(`agent-introduction-prompt-${width}.png`), animations: 'disabled' }); } await tabs.getByRole('tab', { name: '高级设置', exact: true }).click(); const advanced = editor.locator('.agent-advanced-settings'); await expect(tabs.getByRole('tab', { name: '高级设置', exact: true })).toHaveAttribute('aria-selected', 'true'); await expect(advanced.getByRole('navigation')).toHaveCount(0); await expect(advanced.getByRole('tablist')).toHaveCount(0); const headings = ['模型', '工具箱(Tool)', '做事手册(Skill)', 'MCP 服务', '子智能体', '知识库']; for (const name of headings) await expect(advanced.getByRole('heading', { name, exact: true })).toBeVisible(); await expect(advanced.getByRole('heading').first()).toHaveText('模型'); await expect(advanced.getByLabel('模型', { exact: true })).toHaveAttribute('required', ''); for (const copy of ['选它做事时能用的工具。', '给它一本手册,照着方法做。', '管理我的云端能力']) { await expect(advanced.getByText(copy, { exact: true })).toHaveCount(0); } for (const name of ['费用上限', '运行限制']) await expect(advanced.getByRole('heading', { name, exact: true })).toHaveCount(0); await expect(advanced.getByLabel('模型', { exact: true })).toBeInViewport(); await expectConfigurationBounds(page); if (width === 375 || width === 1440) { await page.screenshot({ path: test.info().outputPath(`agent-advanced-upper-${width}.png`), animations: 'disabled' }); } await advanced.getByText('管理我的知识文档', { exact: true }).scrollIntoViewIfNeeded(); await expect(advanced.getByText('管理我的知识文档', { exact: true })).toBeInViewport(); await expectConfigurationBounds(page); if (width === 375 || width === 1440) { await page.screenshot({ path: test.info().outputPath(`agent-advanced-lower-${width}.png`), animations: 'disabled' }); } // Returning from the bottom resets the view to the introduction, without changing a draft. await tabs.getByRole('tab', { name: '介绍与提示词', exact: true }).click(); await expect(editor.getByLabel('一句话介绍', { exact: true })).toBeInViewport(); await expect(editor.getByLabel('提示词', { exact: true })).toHaveValue(draftAgent.system_prompt); const calls = await page.evaluate(() => (window as unknown as FixtureWindow).__agentAvatarFixture.calls); expect(calls.filter(call => call.method === 'PATCH' || call.path === '/api/cloud-agents/avatars/preset' || (call.method === 'POST' && call.path === '/api/cloud-agents/agents'))).toEqual([]); expect(calls.filter(call => call.path === '/api/cloud-agents/actions') .every(call => ['catalog', 'knowledge', 'access'].includes(String(call.input?.operation)))).toBe(true); }); } for (const width of [375, 1366]) { test(`pastel Agent cards and explicit chat/adjust controls remain usable at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 850 }); await installFixture(page, [readyAgent, draftAgent, disabledAgent]); await page.goto(baseUrl); const cards = page.getByRole('article'); await expect(cards).toHaveCount(3); const tones = { '0': 'rgb(241, 246, 255)', '1': 'rgb(246, 242, 255)', '2': 'rgb(255, 245, 233)' }; for (const [tone, color] of Object.entries(tones)) await expect(page.locator(`article[data-tone="${tone}"]`)).toHaveCSS('background-color', color); const ready = page.getByRole('article', { name: readyAgent.name, exact: true }); await expect(ready.getByRole('button', { name: '和它聊聊', exact: true })).toBeVisible(); await expect(ready.getByRole('button', { name: '调整', exact: true })).toBeVisible(); await expect(page.getByRole('article', { name: draftAgent.name, exact: true }).getByRole('button', { name: '继续制作', exact: true })).toBeVisible(); await expect(page.getByRole('article', { name: disabledAgent.name, exact: true }).getByRole('button', { name: '查看与调整', exact: true })).toBeVisible(); await expectImagesLoaded(page); await expectNoOverflow(page); await page.screenshot({ path: test.info().outputPath(`agent-cards-${width}.png`), fullPage: true, animations: 'disabled' }); await ready.getByRole('button', { name: '和它聊聊', exact: true }).click(); await expect(page.getByRole('region', { name: '智能体对话', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: /^(聊天|自动任务|使用与接入)$/ })).toHaveCount(0); await page.getByRole('button', { name: '我的智能体', exact: true }).click(); await ready.getByRole('button', { name: '调整', exact: true }).click(); await expect(page.getByRole('tab', { name: '介绍与提示词', exact: true })).toHaveAttribute('aria-selected', 'true'); await expect(page.getByLabel('提示词', { exact: true })).toHaveValue(readyAgent.system_prompt); await expectNoOverflow(page); }); test(`sixteen platform portraits preview, create and reopen at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 850 }); await installFixture(page, []); await page.goto(baseUrl); await page.getByRole('button', { name: '创建我的第一个智能体', exact: true }).click(); await page.getByLabel('名称', { exact: true }).fill('像素伙伴助手'); const presets = page.getByRole('group', { name: '平台像素头像', exact: true }); await expect(presets.getByRole('button')).toHaveCount(16); await expectImagesLoaded(page); await expectNoOverflow(page); await presets.getByRole('button', { name: '像素伙伴 7', exact: true }).click(); await expect(presets.getByRole('button', { name: '像素伙伴 7', exact: true })).toHaveAttribute('aria-pressed', 'true'); await expect(presets.getByRole('button', { pressed: true })).toHaveCount(1); await expect(page.getByRole('img', { name: '像素伙伴助手头像', exact: true })).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-07.png'); await page.screenshot({ path: test.info().outputPath(`platform-avatar-picker-${width}.png`), fullPage: true, animations: 'disabled' }); await page.getByRole('button', { name: '下一步', exact: true }).click(); await expect(page.getByTestId('cloud-agent-editor')).toBeVisible(); const calls = await page.evaluate(() => (window as unknown as FixtureWindow).__agentAvatarFixture.calls); const created = calls.find(call => call.path === '/api/cloud-agents/agents' && call.method === 'POST')!; expect(created.input?.icon).toBe('/api/storage/public/images/preset-avatar-07.png'); expect(created.input).not.toHaveProperty('icon_url'); await expect(page.getByRole('button', { name: '修改头像', exact: true }).getByRole('img')).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-07.png'); await expectNoOverflow(page); await page.getByRole('button', { name: '我的智能体', exact: true }).click(); const card = page.getByRole('article', { name: '像素伙伴助手', exact: true }); await expect(card.getByRole('img')).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-07.png'); await card.getByRole('button', { name: '继续制作', exact: true }).click(); await expect(page.getByRole('button', { name: '修改头像', exact: true }).getByRole('img')).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-07.png'); }); } test('preset and save failures retain the existing avatar; successful save reopens persisted portrait', async ({ page }) => { await page.setViewportSize({ width: 1366, height: 850 }); const icon = '/api/storage/public/images/preset-avatar-03.png'; await installFixture(page, [{ ...draftAgent, icon, icon_url: '/src/assets/agent-avatars/avatar-03.png' }]); await page.goto(baseUrl); await page.getByRole('button', { name: '继续制作', exact: true }).click(); await page.getByRole('button', { name: '修改头像', exact: true }).click(); const dialog = page.getByRole('dialog', { name: '修改头像', exact: true }); await expect(dialog.getByRole('group', { name: '平台像素头像', exact: true }).getByRole('button')).toHaveCount(16); await page.evaluate(() => { (window as unknown as FixtureWindow).__agentAvatarFixture.failPreset = true; }); await dialog.getByRole('button', { name: '像素伙伴 4', exact: true }).click(); await expect(dialog.getByRole('alert')).toContainText('平台头像暂时不可用'); await expect(dialog.getByRole('img', { name: draftAgent.name + '头像', exact: true })).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-03.png'); await expect(page.getByRole('button', { name: '已保存', exact: true, includeHidden: true })).toBeDisabled(); await page.evaluate(() => { (window as unknown as FixtureWindow).__agentAvatarFixture.failPreset = false; }); await dialog.getByRole('button', { name: '像素伙伴 4', exact: true }).click(); await expect(dialog.getByRole('img', { name: draftAgent.name + '头像', exact: true })).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-04.png'); await expectImagesLoaded(page); await page.screenshot({ path: test.info().outputPath('existing-agent-platform-avatar-dialog.png'), animations: 'disabled' }); await dialog.getByRole('button', { name: '完成', exact: true }).click(); await page.evaluate(() => { (window as unknown as FixtureWindow).__agentAvatarFixture.failSave = true; }); await page.getByRole('button', { name: '保存草稿', exact: true }).click(); await expect(page.getByRole('alert')).toContainText('保存头像失败,请重试'); await expect(page.getByRole('button', { name: '修改头像', exact: true }).getByRole('img')).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-04.png'); expect(await page.evaluate(() => (window as unknown as FixtureWindow).__agentAvatarFixture.agents[0].icon)).toBe(icon); await page.evaluate(() => { (window as unknown as FixtureWindow).__agentAvatarFixture.failSave = false; }); await page.getByRole('button', { name: '保存草稿', exact: true }).click(); await expect(page.getByRole('button', { name: '已保存', exact: true })).toBeDisabled(); const calls = await page.evaluate(() => (window as unknown as FixtureWindow).__agentAvatarFixture.calls); const saves = calls.filter(call => call.method === 'PATCH'); expect(saves).toHaveLength(2); expect(saves[1].input?.icon).toBe('/api/storage/public/images/preset-avatar-04.png'); expect(saves[1].input).not.toHaveProperty('icon_url'); await page.getByRole('button', { name: '我的智能体', exact: true }).click(); const card = page.getByRole('article', { name: draftAgent.name, exact: true }); await expect(card.getByRole('img')).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-04.png'); await card.getByRole('button', { name: '继续制作', exact: true }).click(); await expect(page.getByRole('button', { name: '修改头像', exact: true }).getByRole('img')).toHaveAttribute('src', '/src/assets/agent-avatars/avatar-04.png'); });