完善客户端模块与工作区能力
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-13 19:45:30 +08:00
parent 0148b91a15
commit 22add3f01f
97 changed files with 4756 additions and 3226 deletions

View File

@@ -1,7 +1,7 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('AI Design conversations', () => {
test('keeps generation tasks visible while conversations stay independent', async ({ launchElectronApp }) => {
test.describe('AI Design workspace', () => {
test('keeps the conversation and generation task panes full height', async ({ launchElectronApp }) => {
const app = await launchElectronApp({
imageWorkspaceMode: 'local',
skipSetup: true,
@@ -16,35 +16,99 @@ test.describe('AI Design conversations', () => {
await page.getByLabel('项目名称').fill('多会话验收项目');
await page.getByRole('button', { name: '创建项目' }).click();
const conversationSelect = page.getByTestId('image-canvas-conversation-select');
await expect(conversationSelect).toBeVisible();
await expect(conversationSelect.locator('option')).toHaveCount(1);
const firstConversationId = await conversationSelect.inputValue();
const projectCard = page.locator('[data-testid^="sidebar-image-project-"]').first();
await expect(projectCard).not.toContainText('1 个会话');
await expect(projectCard).not.toContainText('设计会话');
await expect(page.getByRole('button', { name: '收起 多会话验收项目 的会话' })).toBeVisible();
await expect(page.getByRole('button', { name: '重命名 多会话验收项目' })).toBeVisible();
await expect(page.locator('[data-testid^="sidebar-image-conversations-"]').first())
.not.toHaveClass(/border-t/);
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
await expect(page.getByTestId('image-workspace-conversation-fade')).toBeVisible();
await expect(page.getByTestId('design-task-list')).toBeVisible();
await expect(page.getByTestId('design-task-fade')).toBeVisible();
await expect(page.getByTestId('image-canvas-conversation-select')).toHaveCount(0);
const emptyState = page.getByTestId('image-workspace-empty-state');
await expect(emptyState).toContainText('今天一起描绘什么?');
await expect(emptyState.getByRole('img', { name: 'Makelore' })).toHaveCount(1);
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: '上传参考图' }).click();
await fileChooserPromise;
await expect(page.getByRole('dialog', { name: '选择视频首帧' })).toHaveCount(0);
const layoutBounds = await page.evaluate(() => {
const readBounds = (testId: string) => {
const element = document.querySelector(`[data-testid="${testId}"]`);
if (!element) return null;
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return {
top: rect.top,
bottom: rect.bottom,
borderLeftWidth: style.borderLeftWidth,
overflowY: style.overflowY,
};
};
return {
shell: readBounds('main-layout'),
canvas: readBounds('image-canvas-page'),
conversation: readBounds('image-workspace-conversation-pane'),
tasks: readBounds('design-task-list'),
taskScroll: readBounds('design-task-scroll'),
};
});
expect(layoutBounds.shell).not.toBeNull();
expect(layoutBounds.canvas).not.toBeNull();
expect(layoutBounds.conversation).not.toBeNull();
expect(layoutBounds.tasks).not.toBeNull();
expect(layoutBounds.taskScroll).not.toBeNull();
expect(Math.abs(layoutBounds.canvas!.top - layoutBounds.shell!.top)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.canvas!.bottom - layoutBounds.shell!.bottom)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.conversation!.top - layoutBounds.canvas!.top)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.conversation!.bottom - layoutBounds.canvas!.bottom)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.tasks!.top - layoutBounds.canvas!.top)).toBeLessThanOrEqual(1);
expect(Math.abs(layoutBounds.tasks!.bottom - layoutBounds.canvas!.bottom)).toBeLessThanOrEqual(1);
expect(layoutBounds.tasks!.borderLeftWidth).toBe('1px');
expect(layoutBounds.taskScroll!.overflowY).toBe('auto');
const firstPrompt = '为项目设计一张蓝色星空海报';
await page.getByLabel('设计需求').fill(firstPrompt);
await page.getByRole('button', { name: '发送给设计 Agent' }).click();
await expect(page.getByTestId('image-workspace-conversation')).toContainText(firstPrompt);
await expect(page.getByText('设计 Agent', { exact: true })).toHaveCount(0);
await expect(page.getByText('你', { exact: true })).toHaveCount(0);
await expect(page.getByTestId('image-workspace-message').first()).toHaveClass(/w-full/);
await page.getByRole('button', { name: '确认并开始生成' }).click();
await expect(page.getByRole('button', { name: '确认生成' })).toHaveCount(0);
await expect(page.getByRole('button', { name: '继续调整' })).toHaveCount(0);
const task = page.locator('[data-testid^="design-task-"]').first();
await expect(task).toContainText(firstPrompt);
await page.getByRole('button', { name: '新建设计会话' }).first().click();
await expect(conversationSelect.locator('option')).toHaveCount(2);
await expect(conversationSelect).not.toHaveValue(firstConversationId);
await expect(page.getByTestId('image-workspace-conversation')).not.toContainText(firstPrompt);
await expect(task).toContainText(firstPrompt);
const firstConversation = page.locator('[data-testid^="sidebar-image-conversation-"]').first();
await expect(firstConversation).toContainText(firstPrompt);
await expect(firstConversation).not.toContainText('新会话');
const firstConversationTestId = await page
.locator('[data-testid^="sidebar-image-conversation-"]')
.first()
.getAttribute('data-testid');
expect(firstConversationTestId).toBeTruthy();
await page.getByRole('button', { name: '新建设计会话' }).click();
await expect(page.locator('[data-testid^="sidebar-image-conversation-"]')).toHaveCount(2);
const secondPrompt = '这一条会话只讨论红色角色立绘';
await page.getByLabel('设计需求').fill(secondPrompt);
await page.getByRole('button', { name: '发送给设计 Agent' }).click();
await expect(page.getByTestId('image-workspace-conversation')).toContainText(secondPrompt);
await conversationSelect.selectOption(firstConversationId);
await page.getByTestId(firstConversationTestId!).click();
await expect(page.getByTestId('image-workspace-conversation')).toContainText(firstPrompt);
await expect(page.getByTestId('image-workspace-conversation')).not.toContainText(secondPrompt);
await expect(task).toContainText(firstPrompt);
} finally {
await closeElectronApp(app);
}

View File

@@ -1,6 +1,57 @@
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('Makelore module navigation without setup flow', () => {
test('keeps the four module entries in a proportional vertical stack while resizing the window', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
const moduleCards = page.locator('[data-testid^="ai-module-option-"]');
const readCardMetrics = async () => await moduleCards.evaluateAll((elements) => elements.map((element) => {
const bounds = element.getBoundingClientRect();
const image = element.querySelector('.module-option-card-image')?.getBoundingClientRect();
return {
left: bounds.left,
top: bounds.top,
width: bounds.width,
height: bounds.height,
imageWidth: image?.width ?? 0,
imageHeight: image?.height ?? 0,
};
}));
const initialMetrics = await readCardMetrics();
expect(initialMetrics).toHaveLength(4);
expect(Math.max(...initialMetrics.map((card) => card.left)) - Math.min(...initialMetrics.map((card) => card.left))).toBeLessThan(1);
expect(initialMetrics.map((card) => card.top)).toEqual(
[...initialMetrics].sort((top, bottom) => top.top - bottom.top).map((card) => card.top),
);
const initialWidth = initialMetrics[0]?.width ?? 0;
expect(initialMetrics[0]?.imageWidth ?? 0).toBeLessThan(initialWidth / 2);
expect(initialMetrics[0]?.imageHeight ?? 0).toBeGreaterThan((initialMetrics[0]?.height ?? 0) * 0.9);
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]?.setSize(1100, 700);
});
await expect.poll(async () => (await readCardMetrics())[0]?.width ?? 0).toBeLessThan(initialWidth - 1);
const resizedMetrics = await readCardMetrics();
expect(resizedMetrics).toHaveLength(4);
expect(Math.max(...resizedMetrics.map((card) => card.left)) - Math.min(...resizedMetrics.map((card) => card.left))).toBeLessThan(1);
expect(resizedMetrics.map((card) => card.top)).toEqual(
[...resizedMetrics].sort((top, bottom) => top.top - bottom.top).map((card) => card.top),
);
expect(resizedMetrics[0]!.imageWidth).toBeLessThan(resizedMetrics[0]!.width / 2);
expect(resizedMetrics[0]!.imageHeight).toBeGreaterThan(resizedMetrics[0]!.height * 0.9);
} finally {
await closeElectronApp(app);
}
});
test('opens the module chooser and switches between enabled modules', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
@@ -32,6 +83,8 @@ test.describe('Makelore module navigation without setup flow', () => {
await page.getByTestId('sidebar-module-switcher-trigger').click();
await expect(page.getByTestId('sidebar-module-programming')).toHaveAttribute('aria-current', 'page');
await expect(page.getByTestId('sidebar-module-learning')).toBeDisabled();
await expect(page.getByTestId('sidebar-module-robot')).toBeDisabled();
await expect(page.getByTestId('sidebar-module-robot')).toContainText('Robot 机器');
await page.getByTestId('sidebar-module-painting').click();
await expect(page.getByTestId('image-canvas-page')).toBeVisible();
await expect(page.getByTestId('image-workspace-unavailable')).toBeVisible();

View File

@@ -11,9 +11,9 @@ test.describe('Project configuration skills', () => {
try {
const userDataDir = await app.evaluate(({ app: electronApp }) => electronApp.getPath('userData'));
const skillDir = path.join(userDataDir, 'opencode', 'niancode-config', 'skills', 'pm-project-plan');
const skillDir = path.join(userDataDir, 'opencode', 'niancode-config', 'skills', 'agent-browser');
await mkdir(path.join(skillDir, 'references'), { recursive: true });
await writeFile(path.join(skillDir, 'SKILL.md'), '---\nname: pm-project-plan\ndescription: E2E project planning skill.\n---\n\n# Project planning\n');
await writeFile(path.join(skillDir, 'SKILL.md'), '---\nname: agent-browser\ndescription: E2E browser debugging skill.\n---\n\n# Browser debugging\n');
await writeFile(path.join(skillDir, 'references', 'guide.md'), '# Reference guide\n');
await app.evaluate(({ ipcMain }, selectedPath) => {
@@ -89,13 +89,13 @@ test.describe('Project configuration skills', () => {
await page.getByTestId('resource-card-skills').click();
await expect(page.getByTestId('superpowers-card')).toHaveCount(0);
await expect(page.getByRole('switch', { name: '启用 Superpowers' })).toHaveCount(0);
await expect(page.getByTestId('skill-card-pm-project-plan')).toBeVisible();
await page.getByTestId('skill-card-pm-project-plan').click();
await expect(page.getByTestId('skill-card-agent-browser')).toBeVisible();
await page.getByTestId('skill-card-agent-browser').click();
await expect(page.getByTestId('skill-detail-view')).toBeVisible();
await expect(page.getByTestId('skill-structure')).toContainText('references/guide.md');
await expect(page.getByTestId('skill-main-file')).toContainText('# Project planning');
await expect(page.getByTestId('skill-main-file')).toContainText('# Browser debugging');
await page.getByRole('button', { name: '返回技能列表' }).click();
await expect(page.getByTestId('skill-card-pm-project-plan')).toBeVisible();
await expect(page.getByTestId('skill-card-agent-browser')).toBeVisible();
await page.getByRole('button', { name: '关闭' }).click();
await expect(page.getByTestId('resource-card-publish')).toHaveCount(0);
await expect(page.getByRole('button', { name: '一键提交审核' })).toBeVisible();
@@ -106,6 +106,12 @@ test.describe('Project configuration skills', () => {
const maintenanceDialog = page.getByRole('dialog', { name: 'E2E伙伴 · 伙伴维护' });
await expect(maintenanceDialog).toBeVisible();
await expect(maintenanceDialog.getByLabel(/伙伴名称/)).toHaveValue('E2E伙伴');
await maintenanceDialog.getByRole('button', { name: '更换头像' }).click();
const avatarDialog = page.getByRole('dialog', { name: '选择头像' });
await expect(avatarDialog).toBeVisible();
await avatarDialog.getByLabel('上传伙伴头像').setInputFiles(path.join(process.cwd(), 'src/assets/agent-avatars/avatar-01.png'));
await expect(avatarDialog).toHaveCount(0);
await expect(maintenanceDialog.getByAltText('当前上传头像')).toBeVisible();
await expect(page.getByText('完成配置')).toHaveCount(0);
} finally {
await closeElectronApp(app);

View File

@@ -21,17 +21,23 @@ async function openSettingsFromSidebarAccountMenu(page: Page): Promise<void> {
await page.getByTestId('sidebar-nav-settings').click();
}
test.describe('NianCode developer proxy settings', () => {
test('keeps proxy save available when disabling proxy in developer mode', async ({ page }) => {
async function unlockDeveloperSettings(page: Page): Promise<void> {
await page.getByTestId('settings-reveal-additional').click();
await page.getByTestId('settings-admin-password-input').fill('zhiniankeji666');
await page.getByTestId('settings-admin-unlock-button').click();
await expect(page.getByTestId('settings-proxy-section')).toBeVisible();
}
test.describe('Makelore administrator-protected proxy settings', () => {
test('keeps proxy save available after administrator unlock', async ({ page }) => {
await completeSetup(page);
await openSettingsFromSidebarAccountMenu(page);
await expect(page.getByTestId('settings-page')).toBeVisible();
await page.getByTestId('settings-reveal-additional').click();
const devModeToggle = page.getByTestId('settings-dev-mode-switch');
await expect(devModeToggle).toBeVisible();
await ensureSwitchState(devModeToggle, true);
await expect(page.getByTestId('settings-proxy-section')).not.toBeVisible();
await unlockDeveloperSettings(page);
const proxySection = page.getByTestId('settings-proxy-section');
const proxyToggle = page.getByTestId('settings-proxy-toggle');