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');
});