feat: 完善图像工作区与创作工具体验
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
52
tests/e2e/language-chinese-only.spec.ts
Normal file
52
tests/e2e/language-chinese-only.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user