feat: 完善图像工作区与创作工具体验
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-16 14:08:02 +08:00
parent bfcb88cfef
commit 26b52d76e3
92 changed files with 16678 additions and 2975 deletions

View File

@@ -237,7 +237,7 @@ export const test = base.extend<ElectronFixtures>({
apps: new Set(),
defaultSkipSetup: ![
'app-smoke.spec.ts',
'language-russian.spec.ts',
'language-chinese-only.spec.ts',
].includes(basename(testInfo.file)),
homeDir,
rootDir,

View File

@@ -2,6 +2,7 @@ import { closeElectronApp, expect, getStableWindow, test } from './fixtures/elec
test.describe('AI Design workspace', () => {
test('keeps the conversation and generation task panes full height', async ({ launchElectronApp }) => {
test.setTimeout(150_000);
const app = await launchElectronApp({
imageWorkspaceMode: 'local',
skipSetup: true,
@@ -12,6 +13,10 @@ test.describe('AI Design workspace', () => {
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-painting').click();
const imageSidebar = page.getByTestId('sidebar-image-workspace');
await expect(imageSidebar.getByTestId('sidebar-open-image-prompt-museum')).toBeVisible();
await expect(imageSidebar.getByTestId('sidebar-create-image-project')).toBeVisible();
await page.getByTestId('sidebar-create-image-project').click();
await page.getByLabel('项目名称').fill('多会话验收项目');
await page.getByRole('button', { name: '创建项目' }).click();
@@ -81,11 +86,17 @@ test.describe('AI Design workspace', () => {
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/);
const firstQuote = page.locator('[data-testid^="design-quote-"]').first();
await expect(firstQuote.getByRole('button', { name: '继续调整' })).toBeVisible();
await expect(firstQuote).not.toContainText('提示词模式');
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();
const task = page
.locator('[data-testid^="design-task-local-task-"]')
.filter({ hasText: firstPrompt })
.first();
await expect(task).toContainText(firstPrompt);
await expect(task).toContainText(firstPrompt);
const firstConversation = page.locator('[data-testid^="sidebar-image-conversation-"]').first();
@@ -98,17 +109,65 @@ test.describe('AI Design workspace', () => {
.getAttribute('data-testid');
expect(firstConversationTestId).toBeTruthy();
await projectCard.hover();
await page.getByRole('button', { name: '新建设计会话' }).click();
await expect(page.locator('[data-testid^="sidebar-image-conversation-"]')).toHaveCount(2);
const secondPrompt = '这一条会话只讨论红色角色立绘';
const secondPrompt = '这一条会话只讨论红色角色动态视频';
await page.getByLabel('设计需求').fill(secondPrompt);
await page.getByRole('button', { name: '发送给设计 Agent' }).click();
await expect(page.getByTestId('image-workspace-conversation')).toContainText(secondPrompt);
await expect(page.getByTestId('design-quote-final-prompt')).toHaveValue(secondPrompt);
await page.getByTestId('design-quote-final-prompt').fill('用户编辑后的动态视频完整提示词');
await page.getByTestId('design-quote-resolution').selectOption('720P');
await page.getByTestId('design-quote-aspect-ratio').selectOption('9:16');
await page.getByTestId('design-quote-duration').selectOption('4');
await expect(page.getByRole('button', { name: '重新报价中' })).toBeDisabled();
await expect(page.getByRole('button', { name: '确认并开始生成' })).toBeEnabled({ timeout: 10_000 });
await page.getByRole('button', { name: '确认并开始生成' }).click();
await expect(page.getByRole('button', { name: '确认并开始生成' })).toHaveCount(0);
await page.getByTestId(firstConversationTestId!).click();
await expect(page.getByTestId('image-workspace-conversation')).toContainText(firstPrompt);
await expect(page.getByTestId('image-workspace-conversation')).not.toContainText(secondPrompt);
// The local adapter deterministically advances queued -> running -> succeeded as the
// task fallback reconciles. The delete confirmation also explains what happens to
// queued and running tasks on the server.
await task.getByRole('button', { name: '查看生图任务详情' }).click();
const taskDetails = page.getByRole('dialog', { name: '生成内容详情' });
await expect(taskDetails).toContainText('已完成', { timeout: 40_000 });
await taskDetails.getByRole('button', { name: '关闭生成内容详情' }).click();
await projectCard.hover();
await page.getByRole('button', { name: '删除 多会话验收项目' }).click();
const deleteDialog = page.getByRole('dialog', { name: '删除项目' });
await expect(deleteDialog).toContainText('未提交的任务会被取消并释放预留积分');
await expect(deleteDialog).toContainText('已提交或运行中的任务会继续后台结算');
await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeDisabled();
await expect(projectCard).toBeVisible();
await deleteDialog.getByTestId('delete-image-project-name-input').fill('多会话验收');
await expect(deleteDialog.getByText('项目名称必须完全匹配。')).toBeVisible();
await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeDisabled();
await expect(projectCard).toBeVisible();
await deleteDialog.getByTestId('delete-image-project-name-input').fill('多会话验收项目');
await expect(deleteDialog.getByTestId('confirm-delete-image-project')).toBeEnabled();
await deleteDialog.getByTestId('confirm-delete-image-project').click();
await expect(deleteDialog).toHaveCount(0);
await expect(page.locator('[data-testid^="sidebar-image-project-"]')).toHaveCount(0);
await expect(imageSidebar).toContainText('还没有设计项目');
await expect(page.getByRole('heading', { name: '创建第一个设计项目' })).toBeVisible();
await imageSidebar.getByTestId('sidebar-open-image-prompt-museum').click();
await expect(page.getByTestId('image-prompt-museum-page')).toBeVisible();
await expect(page.getByRole('heading', { name: '获取灵感' })).toBeVisible();
await expect(page.getByText('提示词博物馆', { exact: true })).toHaveCount(0);
await expect(page.getByTestId('titlebar-page-title')).toHaveText('获取灵感');
await expect(page.getByTestId('titlebar-logo')).toHaveCount(1);
await expect(page.getByTestId('image-prompt-museum-filters')).toHaveClass(/sticky/);
await expect(page.getByTestId('image-prompt-museum-filters')).toHaveClass(/top-0/);
} finally {
await closeElectronApp(app);
}

View File

@@ -0,0 +1,52 @@
import type { Page } from '@playwright/test';
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
async function openSettingsFromSidebarAccountMenu(page: Page): Promise<void> {
await page.getByTestId('sidebar-member-menu-trigger').click();
await page.getByTestId('sidebar-nav-settings').click();
}
test.describe('Chinese-only UI language', () => {
test('shows only Chinese in the setup wizard', async ({ launchElectronApp }) => {
const app = await launchElectronApp();
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('setup-page')).toBeVisible();
await expect(page.getByRole('button', { name: '中文', exact: true })).toBeVisible();
await expect(page.getByRole('button', { name: 'English', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: '日本語', exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Русский', exact: true })).toHaveCount(0);
} finally {
await closeElectronApp(app);
}
});
test('keeps only Chinese in Settings', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
await openSettingsFromSidebarAccountMenu(page);
await expect(page).toHaveURL(/\/settings$/);
const languageSelect = page.locator('#language');
await expect(languageSelect).toHaveValue('zh');
await expect(languageSelect.locator('option')).toHaveCount(1);
await expect(languageSelect.locator('option')).toHaveText('中文');
await expect(page.getByText('配置您的 Makelore 体验', { exact: true })).toHaveCount(0);
await expect(page.getByText('自定义外观和风格', { exact: true })).toHaveCount(0);
await expect(page.getByText('Check the local runtime status or restart it when needed.', { exact: true })).toHaveCount(0);
await expect(page.getByText('保持 Makelore 最新', { exact: true })).toHaveCount(0);
await expect(page.getByText('开启自动更新后,更新将自动下载并安装。', { exact: true })).toHaveCount(0);
} finally {
await closeElectronApp(app);
}
});
});

View File

@@ -1,120 +0,0 @@
import type { Page } from '@playwright/test';
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
async function openSettingsFromSidebarAccountMenu(page: Page): Promise<void> {
await page.getByTestId('sidebar-member-menu-trigger').click();
await page.getByTestId('sidebar-nav-settings').click();
}
async function expectSidebarSettingsLabel(page: Page, label: string): Promise<void> {
await page.getByTestId('sidebar-member-menu-trigger').click();
await expect(page.getByTestId('sidebar-nav-settings')).toContainText(label);
await page.keyboard.press('Escape');
}
test.describe('Russian language localization', () => {
test('shows Russian language option in setup wizard', async ({ launchElectronApp }) => {
const app = await launchElectronApp();
try {
const page = await getStableWindow(app);
// Should see the setup wizard
await expect(page.getByTestId('setup-page')).toBeVisible();
// Should have Russian language button visible
const russianButton = page.locator('button', { hasText: 'Русский' });
await expect(russianButton).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
test('can switch to Russian language in setup wizard', async ({ launchElectronApp }) => {
const app = await launchElectronApp();
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('setup-page')).toBeVisible();
// Click Russian language button
const russianButton = page.locator('button', { hasText: 'Русский' });
await russianButton.click();
// Verify UI renders in Russian by checking for Russian-only text
// "Добро пожаловать" is unique to Russian and won't appear in English
await expect(page.locator('h2')).toContainText('Добро пожаловать');
} finally {
await closeElectronApp(app);
}
});
test('Russian language persists after skipping setup', async ({ launchElectronApp }) => {
const app = await launchElectronApp();
let relaunchedApp: Awaited<ReturnType<typeof launchElectronApp>> | null = null;
let firstAppClosed = false;
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('setup-page')).toBeVisible();
// Switch to Russian
const russianButton = page.locator('button', { hasText: 'Русский' });
await russianButton.click();
// Skip setup
await page.getByTestId('setup-skip-button').click();
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await closeElectronApp(app);
firstAppClosed = true;
relaunchedApp = await launchElectronApp({ skipSetup: true });
const relaunchedPage = await getStableWindow(relaunchedApp);
await expect(relaunchedPage.getByTestId('ai-module-selection-page')).toBeVisible();
await relaunchedPage.getByTestId('ai-module-option-programming').click();
await expect(relaunchedPage.getByTestId('main-layout')).toBeVisible();
// Bypass authentication only for the relaunch so the persisted setting
// can be verified without weakening the production login gate.
await openSettingsFromSidebarAccountMenu(relaunchedPage);
await expect(relaunchedPage).toHaveURL(/\/settings$/);
await expect(relaunchedPage.getByRole('heading', { name: 'Настройки', exact: true })).toBeVisible();
// Verify sidebar shows Russian text (not English)
// "Настройки" is Russian-only, English is "Settings"
await expectSidebarSettingsLabel(relaunchedPage, 'Настройки');
} finally {
if (relaunchedApp) await closeElectronApp(relaunchedApp);
if (!firstAppClosed) await closeElectronApp(app);
}
});
test('can switch to Russian in Settings page', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
await page.getByTestId('ai-module-option-programming').click();
await expect(page.getByTestId('main-layout')).toBeVisible();
// Navigate to Settings (in English by default after skipSetup)
await openSettingsFromSidebarAccountMenu(page);
await expect(page).toHaveURL(/\/settings$/);
// Select Russian from the current settings control.
await page.locator('#language').selectOption('ru');
await expect(page.getByRole('heading', { name: 'Настройки', exact: true })).toBeVisible();
// Verify sidebar switched to Russian
// "Настройки" is Russian-only, English is "Settings"
await expectSidebarSettingsLabel(page, 'Настройки');
} finally {
await closeElectronApp(app);
}
});
});

View File

@@ -12,9 +12,13 @@ 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', 'agent-browser');
const gameEngineSkillDir = path.join(userDataDir, 'opencode', 'niancode-config', 'skills', 'game-engine');
await mkdir(path.join(skillDir, 'references'), { recursive: true });
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 mkdir(path.join(gameEngineSkillDir, 'assets'), { recursive: true });
await writeFile(path.join(gameEngineSkillDir, 'SKILL.md'), '---\nname: game-engine\ndescription: E2E game engine skill.\n---\n\n# Game Engine Skill\n\n## Makelore 使用边界\n');
await writeFile(path.join(gameEngineSkillDir, 'assets', '2d-platform-game.md'), '# 2D Platform Game Template\n');
await app.evaluate(({ ipcMain }, selectedPath) => {
ipcMain.removeHandler('dialog:open');
@@ -114,6 +118,11 @@ test.describe('Project configuration skills', () => {
await expect(page.getByTestId('skill-main-file')).toContainText('# Browser debugging');
await page.getByRole('button', { name: '返回技能列表' }).locator('svg').click();
await expect(page.getByTestId('skill-card-agent-browser')).toBeVisible();
await expect(page.getByTestId('skill-card-game-engine')).toBeVisible();
await page.getByTestId('skill-card-game-engine').click();
await expect(page.getByTestId('skill-structure')).toContainText('assets/2d-platform-game.md');
await expect(page.getByTestId('skill-main-file')).toContainText('Makelore 使用边界');
await page.getByRole('button', { name: '返回技能列表' }).locator('svg').click();
await page.getByRole('button', { name: '关闭' }).click();
await expect(page.getByTestId('resource-card-publish')).toHaveCount(0);
await expect(page.getByRole('button', { name: '一键提交审核' })).toBeVisible();