473 lines
21 KiB
TypeScript
473 lines
21 KiB
TypeScript
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
|
|
|
test.describe('AI Design V2 workspace', () => {
|
|
test('removes the inspiration entry and redirects its retired route to Canvas', async ({ launchElectronApp }) => {
|
|
test.setTimeout(90_000);
|
|
const app = await launchElectronApp({ skipSetup: true });
|
|
|
|
try {
|
|
const page = await getStableWindow(app);
|
|
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
|
|
|
await app.evaluate(({ ipcMain }) => {
|
|
const respond = (json: unknown) => ({
|
|
ok: true,
|
|
data: { status: 200, ok: true, json },
|
|
});
|
|
|
|
ipcMain.removeHandler('hostapi:fetch');
|
|
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string }) => {
|
|
const path = request.path ?? '';
|
|
if (path === '/api/works/image-workspace') {
|
|
return respond({
|
|
success: true,
|
|
data: {
|
|
capabilities: { conversation: true, generation: true, image: true, video: true },
|
|
workspaces: [],
|
|
},
|
|
});
|
|
}
|
|
return { ok: false, error: { message: `Unexpected E2E Host API request: ${path}` } };
|
|
});
|
|
});
|
|
|
|
await page.getByTestId('ai-module-option-painting').click();
|
|
await expect(page.getByLabel('正在加载 AI 设计')).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /获取.*灵感/u })).toHaveCount(0);
|
|
|
|
await page.evaluate(() => {
|
|
window.location.hash = '#/image-prompts';
|
|
});
|
|
await expect.poll(() => page.evaluate(() => window.location.hash)).toBe('#/image-canvas');
|
|
await expect(page.getByLabel('正在加载 AI 设计')).toBeVisible();
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
|
|
test('keeps conversation and youth-friendly direct edits on one canonical design through production confirmation', async ({ launchElectronApp }) => {
|
|
test.setTimeout(120_000);
|
|
const app = await launchElectronApp({ skipSetup: true });
|
|
|
|
try {
|
|
const page = await getStableWindow(app);
|
|
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
|
|
|
await app.evaluate(({ ipcMain }) => {
|
|
const summary = {
|
|
workspaceId: 'workspace-1',
|
|
clientWorkspaceId: 'client-workspace-1',
|
|
title: '咖啡机新品主视觉',
|
|
directionId: 'direction-1',
|
|
sessionId: 'session-1',
|
|
directionRevision: 4,
|
|
specificationRevision: 3,
|
|
workspaceViewRevision: 6,
|
|
updatedAt: '2026-08-30T10:00:00.000Z',
|
|
};
|
|
const workspace = {
|
|
workspace: summary,
|
|
form: {
|
|
schemaVersion: 1,
|
|
workspaceId: 'workspace-1',
|
|
directionId: 'direction-1',
|
|
directionRevision: 4,
|
|
rawTurnSequence: 1,
|
|
specificationRevision: 3,
|
|
workspaceViewRevision: 6,
|
|
specificationRevisionId: 'specification-revision-3',
|
|
specificationDigest: 'a'.repeat(64),
|
|
specification: {
|
|
schema_version: 1,
|
|
values: {
|
|
intent: {
|
|
media: 'image',
|
|
creation_intent: 'create',
|
|
purpose: '新品发布主视觉',
|
|
channel: '小红书首图',
|
|
audience: '年轻消费者',
|
|
},
|
|
content: {
|
|
concept: '把便携咖啡机呈现为城市通勤中的精密工具',
|
|
subjects: [],
|
|
exact_content: [],
|
|
},
|
|
visual: {},
|
|
layout: { regions: [] },
|
|
references: [],
|
|
image: {
|
|
operation_intent: 'new_generation',
|
|
preserve_instructions: [],
|
|
change_instructions: [],
|
|
},
|
|
video: { shots: [] },
|
|
output: {
|
|
aspect_ratio: '3:4',
|
|
orientation: 'portrait',
|
|
delivery_format: 'PNG',
|
|
variant_count: 1,
|
|
variant_purposes: [],
|
|
},
|
|
constraints: {
|
|
must_include: [],
|
|
must_preserve: [],
|
|
must_avoid: [],
|
|
legal_notes: [],
|
|
safety_notes: [],
|
|
acceptance_criteria: [],
|
|
},
|
|
},
|
|
field_decisions: {
|
|
'intent.media': {
|
|
resolution: 'resolved',
|
|
provenance: 'user',
|
|
locked: true,
|
|
changed_by: 'interaction-1',
|
|
origin_turn_id: 'turn-1',
|
|
},
|
|
'output.aspect_ratio': {
|
|
resolution: 'resolved',
|
|
provenance: 'user',
|
|
locked: false,
|
|
changed_by: 'interaction-1',
|
|
origin_turn_id: 'turn-1',
|
|
},
|
|
},
|
|
},
|
|
decisionPrompts: [],
|
|
activeQuotes: [] as Array<Record<string, unknown>>,
|
|
recentChangeSet: { interaction_id: 'interaction-1', changes: [] },
|
|
},
|
|
turns: [{
|
|
turnId: 'turn-1',
|
|
rawTurnSequence: 1,
|
|
userMessage: '做一张便携咖啡机的小红书主视觉',
|
|
assistantMessage: '明白了:你想做一张适合小红书的便携咖啡机图片。',
|
|
}],
|
|
tasks: [] as Array<Record<string, unknown>>,
|
|
assets: [],
|
|
};
|
|
const respond = (json: unknown) => ({
|
|
ok: true,
|
|
data: { status: 200, ok: true, json },
|
|
});
|
|
const envelope = (data: unknown) => respond({ success: true, data });
|
|
const e2eSession = {
|
|
accessToken: 'e2e-design-access-token',
|
|
tokenType: 'Bearer',
|
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
lastActiveAt: Date.now(),
|
|
canRefresh: false,
|
|
};
|
|
const e2eGlobal = globalThis as typeof globalThis & {
|
|
__designE2eAuthReady?: boolean;
|
|
__designE2eProfileReady?: boolean;
|
|
};
|
|
e2eGlobal.__designE2eAuthReady = false;
|
|
e2eGlobal.__designE2eProfileReady = false;
|
|
|
|
ipcMain.removeHandler('hostapi:fetch');
|
|
ipcMain.handle('hostapi:fetch', async (_event, request: {
|
|
path?: string;
|
|
method?: string;
|
|
body?: string | null;
|
|
}) => {
|
|
const path = request.path ?? '';
|
|
if (path === '/api/auth/session/sync') {
|
|
return respond({ success: true, session: e2eSession });
|
|
}
|
|
if (path === '/api/auth/me') {
|
|
e2eGlobal.__designE2eAuthReady = true;
|
|
return respond({
|
|
success: true,
|
|
moduleAccess: { programming: true, design: true, robot: true },
|
|
});
|
|
}
|
|
if (path === '/api/works/user/agent-profile') {
|
|
e2eGlobal.__designE2eProfileReady = true;
|
|
return respond({
|
|
success: true,
|
|
profile: {
|
|
display_name: 'E2E 设计用户',
|
|
age: null,
|
|
gender: null,
|
|
avatar_url: null,
|
|
share_age_with_agents: false,
|
|
share_gender_with_agents: false,
|
|
analysis_enabled: true,
|
|
completed: true,
|
|
version: 1,
|
|
updated_at: '2026-08-30T10:00:00.000Z',
|
|
},
|
|
});
|
|
}
|
|
if (path === '/api/works/image-workspace') {
|
|
return envelope({
|
|
capabilities: { conversation: true, generation: true, image: true, video: true },
|
|
workspaces: [workspace.workspace],
|
|
});
|
|
}
|
|
if (path === '/api/works/image-workspace/workspaces/workspace-1' && request.method === 'GET') {
|
|
return envelope(workspace);
|
|
}
|
|
if (path === '/api/works/image-workspace/workspaces/workspace-1/commands') {
|
|
const command = JSON.parse(request.body ?? '{}') as {
|
|
kind?: string;
|
|
clientOperationId?: string;
|
|
input?: {
|
|
kind?: string;
|
|
message?: string;
|
|
operations?: Array<{ kind?: string; path?: string; value?: unknown }>;
|
|
};
|
|
};
|
|
|
|
if (command.kind === 'apply_input') {
|
|
if (command.input?.kind === 'direct_edit') {
|
|
for (const operation of command.input.operations ?? []) {
|
|
if (operation.kind === 'set' && operation.path === 'output.aspect_ratio') {
|
|
workspace.form.specification.values.output.aspect_ratio = String(operation.value ?? '');
|
|
}
|
|
if (operation.kind === 'set' && operation.path === 'output.orientation') {
|
|
workspace.form.specification.values.output.orientation = String(operation.value ?? '');
|
|
}
|
|
}
|
|
}
|
|
if (command.input?.kind === 'chat' && command.input.message) {
|
|
workspace.form.rawTurnSequence += 1;
|
|
workspace.turns.push({
|
|
turnId: `turn-${workspace.form.rawTurnSequence}`,
|
|
rawTurnSequence: workspace.form.rawTurnSequence,
|
|
userMessage: command.input.message,
|
|
assistantMessage: '记住了:主体更简洁,也会为标题留出空间。',
|
|
});
|
|
}
|
|
workspace.form.directionRevision += 1;
|
|
workspace.form.specificationRevision += 1;
|
|
workspace.form.workspaceViewRevision += 1;
|
|
workspace.form.specificationRevisionId = `specification-revision-${workspace.form.specificationRevision}`;
|
|
workspace.workspace.directionRevision = workspace.form.directionRevision;
|
|
workspace.workspace.specificationRevision = workspace.form.specificationRevision;
|
|
workspace.workspace.workspaceViewRevision = workspace.form.workspaceViewRevision;
|
|
}
|
|
|
|
if (command.kind === 'request_quote') {
|
|
workspace.form.activeQuotes = [{
|
|
quoteId: 'quote-1',
|
|
status: 'offered',
|
|
specificationRevision: workspace.form.specificationRevision,
|
|
specificationRevisionId: workspace.form.specificationRevisionId,
|
|
specificationDigest: workspace.form.specificationDigest,
|
|
medium: 'image',
|
|
compilerVersion: 'design-compiler-v2',
|
|
outputSummary: {
|
|
medium: 'image',
|
|
strategy: 'direct',
|
|
aspectRatio: workspace.form.specification.values.output.aspect_ratio,
|
|
outputCount: 1,
|
|
deliveryFormat: 'PNG',
|
|
durationSeconds: null,
|
|
requiredOutputRoles: ['primary'],
|
|
},
|
|
warnings: [],
|
|
quotedDesignPoints: 12,
|
|
maximumCustomerChargeAtoms: 1200,
|
|
expiresAt: '2030-01-02T03:04:05.000Z',
|
|
}];
|
|
}
|
|
|
|
if (command.kind === 'confirm_generation') {
|
|
const offeredQuote = workspace.form.activeQuotes[0] ?? {};
|
|
workspace.form.activeQuotes = [{ ...offeredQuote, status: 'consumed' }];
|
|
workspace.tasks = [{
|
|
taskId: 'task-1',
|
|
quoteId: 'quote-1',
|
|
status: 'queued',
|
|
taskRevision: 1,
|
|
specificationRevision: workspace.form.specificationRevision,
|
|
specificationRevisionId: workspace.form.specificationRevisionId,
|
|
specificationDigest: workspace.form.specificationDigest,
|
|
medium: 'image',
|
|
compilerVersion: 'design-compiler-v2',
|
|
outputSummary: offeredQuote.outputSummary,
|
|
maximumCustomerChargeAtoms: 1200,
|
|
customerBilling: {
|
|
quotedAtoms: 1200,
|
|
heldAtoms: 1200,
|
|
chargedAtoms: 0,
|
|
refundedAtoms: 0,
|
|
pendingResolutionAtoms: 0,
|
|
},
|
|
customerHoldExpiresAt: '2030-01-02T03:04:05.000Z',
|
|
resolutionDeadlineAt: '2030-01-02T03:09:05.000Z',
|
|
progress: { stage: 'queued', completedRequiredSteps: 0, totalRequiredSteps: 1 },
|
|
executionHealth: 'normal',
|
|
cancellation: { state: 'available', outcome: null },
|
|
issues: [],
|
|
resultAssetIds: [],
|
|
nextActions: ['cancel'],
|
|
createdAt: '2026-08-30T10:01:00.000Z',
|
|
}];
|
|
}
|
|
|
|
return envelope({
|
|
clientOperationId: command.clientOperationId ?? 'operation-e2e',
|
|
runId: `run-${command.clientOperationId ?? 'e2e'}`,
|
|
workspace,
|
|
});
|
|
}
|
|
return { ok: false, error: { message: `Unexpected E2E Host API request: ${path}` } };
|
|
});
|
|
});
|
|
|
|
await page.addInitScript(() => {
|
|
const now = Date.now();
|
|
localStorage.setItem('niancode-settings', JSON.stringify({
|
|
state: { sidebarCollapsed: true },
|
|
version: 0,
|
|
}));
|
|
localStorage.setItem('niancode-auth', JSON.stringify({
|
|
state: {
|
|
authBase: 'https://biz.nianxx.cn/auth/',
|
|
clientId: 'app',
|
|
accessToken: 'e2e-design-access-token',
|
|
tokenType: 'Bearer',
|
|
expiresAt: now + 60 * 60 * 1000,
|
|
lastActiveAt: now,
|
|
canRefresh: false,
|
|
legacyRefreshToken: null,
|
|
user: {
|
|
username: 'e2e-design-user',
|
|
userId: 'e2e-user-1',
|
|
tenantId: null,
|
|
deptId: null,
|
|
authorities: [],
|
|
},
|
|
moduleAccess: { programming: true, design: true, robot: true },
|
|
},
|
|
version: 2,
|
|
}));
|
|
});
|
|
await page.reload();
|
|
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
|
await expect.poll(async () => await app.evaluate(() => Boolean(
|
|
(globalThis as typeof globalThis & { __designE2eAuthReady?: boolean }).__designE2eAuthReady,
|
|
))).toBe(true);
|
|
|
|
await page.getByTestId('ai-module-option-painting').click();
|
|
await expect(page.getByTestId('image-workspace')).toBeVisible();
|
|
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
|
|
await expect(page.getByTestId('image-workspace-conversation')).toHaveClass(/bg-background/);
|
|
await expect(page.getByTestId('design-message-composer')).toHaveClass(/max-w-\[50rem\]/);
|
|
await expect(page.getByTestId('design-message-composer-surface')).toHaveClass(/chat-composer-surface/);
|
|
await expect(page.getByTestId('sidebar-image-workspace')).toBeVisible();
|
|
await expect(page.getByTestId('image-workspace-history-rail')).toBeVisible();
|
|
await expect(page.getByTestId('image-workspace-works-rail')).toHaveCount(0);
|
|
await expect(page.getByTestId('titlebar-sidebar-toggle')).toHaveCount(0);
|
|
await expect(page.getByRole('heading', { name: '和 AI 一起完善创作' })).toHaveCount(0);
|
|
await expect(page.getByText('当前设计项目的制作记录')).toHaveCount(0);
|
|
await expect(page.getByText('实时同步')).toHaveCount(0);
|
|
await expect(page.getByRole('button', { name: '刷新作品' })).toHaveCount(0);
|
|
await expect(page.getByRole('heading', { name: '制作方案' })).toBeVisible();
|
|
await expect(page.getByTestId('youth-creation-card')).not.toHaveClass(/border|shadow-sm/);
|
|
await expect(page.getByRole('textbox', { name: '创作提示词' })).toHaveValue(
|
|
'把便携咖啡机呈现为城市通勤中的精密工具',
|
|
);
|
|
await expect(page.getByText('Living Form')).toHaveCount(0);
|
|
await expect(page.getByText('目标与用途')).toHaveCount(0);
|
|
await expect(page.locator('[data-testid^="sidebar-image-conversation-"]')).toHaveCount(0);
|
|
|
|
const layout = await page.evaluate(() => {
|
|
const conversation = document.querySelector('[data-testid="image-workspace-conversation"]')
|
|
?.getBoundingClientRect();
|
|
const projectSidebar = document.querySelector('[data-testid="sidebar"]')
|
|
?.getBoundingClientRect();
|
|
const workspace = document.querySelector('[data-testid="image-workspace"]')
|
|
?.getBoundingClientRect();
|
|
const historyRail = document.querySelector('[data-testid="image-workspace-history-rail"]')
|
|
?.getBoundingClientRect();
|
|
const workspaceHeader = document.querySelector('[data-testid="image-workspace-header"]')
|
|
?.getBoundingClientRect();
|
|
const historyHeader = document.querySelector('[data-testid="image-workspace-history-header"]')
|
|
?.getBoundingClientRect();
|
|
return conversation && projectSidebar && workspace && historyRail && workspaceHeader && historyHeader
|
|
? {
|
|
conversationHeight: conversation.height,
|
|
conversationWidth: conversation.width,
|
|
sidebarWidth: projectSidebar.width,
|
|
horizontalGap: Math.abs(projectSidebar.right - conversation.left),
|
|
historyRailWidth: historyRail.width,
|
|
historyRailHeight: historyRail.height,
|
|
historyTopGap: Math.abs(workspace.top - historyRail.top),
|
|
historyBottomGap: Math.abs(workspace.bottom - historyRail.bottom),
|
|
historyHorizontalGap: Math.abs(conversation.right - historyRail.left),
|
|
workspaceHeaderHeight: workspaceHeader.height,
|
|
historyHeaderHeight: historyHeader.height,
|
|
headerTopGap: Math.abs(workspaceHeader.top - historyHeader.top),
|
|
headerBottomGap: Math.abs(workspaceHeader.bottom - historyHeader.bottom),
|
|
}
|
|
: null;
|
|
});
|
|
expect(layout).not.toBeNull();
|
|
expect(layout!.conversationHeight).toBeGreaterThan(300);
|
|
expect(layout!.sidebarWidth).toBe(256);
|
|
expect(layout!.horizontalGap).toBeLessThanOrEqual(1);
|
|
expect(layout!.historyRailWidth).toBe(256);
|
|
expect(layout!.historyRailWidth).toBe(layout!.sidebarWidth);
|
|
expect(layout!.historyRailHeight).toBeGreaterThan(500);
|
|
expect(layout!.historyTopGap).toBeLessThanOrEqual(1);
|
|
expect(layout!.historyBottomGap).toBeLessThanOrEqual(1);
|
|
expect(layout!.historyHorizontalGap).toBeLessThanOrEqual(1);
|
|
expect(layout!.workspaceHeaderHeight).toBe(40);
|
|
expect(layout!.historyHeaderHeight).toBe(40);
|
|
expect(layout!.headerTopGap).toBeLessThanOrEqual(1);
|
|
expect(layout!.headerBottomGap).toBeLessThanOrEqual(1);
|
|
expect(layout!.conversationWidth).toBeGreaterThan(600);
|
|
|
|
const desktopViewport = page.viewportSize();
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
await expect(page.getByTestId('sidebar')).toBeHidden();
|
|
await expect(page.getByTestId('image-workspace-history-rail')).toHaveCount(0);
|
|
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
|
|
await expect(page.getByTestId('youth-creation-card')).toBeVisible();
|
|
await page.getByRole('button', { name: '项目', exact: true }).click();
|
|
await expect(page.getByTestId('image-workspace-works-rail')).toBeVisible();
|
|
await expect(page.getByRole('dialog')).toHaveClass(/left-0/);
|
|
await page.keyboard.press('Escape');
|
|
|
|
await page.setViewportSize({ width: 768, height: 800 });
|
|
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
|
|
await expect(page.getByRole('button', { name: '项目', exact: true })).toBeVisible();
|
|
|
|
await page.setViewportSize(desktopViewport ?? { width: 1280, height: 800 });
|
|
await expect(page.getByTestId('sidebar-image-workspace')).toBeVisible();
|
|
await expect(page.getByTestId('image-workspace-history-rail')).toBeVisible();
|
|
await expect(page.getByTestId('image-workspace-works-rail')).toHaveCount(0);
|
|
|
|
await page.getByRole('combobox', { name: '画幅' }).selectOption('16:9');
|
|
await expect(page.getByRole('combobox', { name: '画幅' })).toHaveValue('16:9');
|
|
|
|
const followUp = '主体更克制,留出品牌标题空间';
|
|
await page.getByLabel('告诉 AI 你想创作什么').fill(followUp);
|
|
await page.getByLabel('告诉 AI 你想创作什么').press('Enter');
|
|
await expect(page.getByTestId('image-workspace-conversation')).toContainText(followUp);
|
|
await expect(page.getByTestId('image-workspace-conversation'))
|
|
.toContainText('记住了:主体更简洁,也会为标题留出空间。');
|
|
|
|
await page.getByRole('button', { name: '准备制作方案', exact: true }).click();
|
|
await expect(page.getByRole('button', { name: '确认并开始制作' })).toBeVisible();
|
|
await expect(page.getByText(/预计使用/)).toContainText('12');
|
|
await expect(page.getByRole('textbox', { name: '创作提示词' })).toBeVisible();
|
|
await expect(page.getByText(/规格版本|编译器|生成方式|不可变/)).toHaveCount(0);
|
|
|
|
await page.getByRole('button', { name: '确认并开始制作' }).click();
|
|
await expect(page.getByTestId('image-workspace-history-rail').getByTestId('design-plan-history-task-1')).toBeVisible();
|
|
await expect(page.getByTestId('image-workspace-conversation').getByTestId('design-plan-history-task-1')).toHaveCount(0);
|
|
await expect(page.getByText('等待制作')).toBeVisible();
|
|
await expect(page.getByTestId('youth-creation-card')).toHaveCount(0);
|
|
await expect(page.getByText('1200', { exact: true })).toHaveCount(0);
|
|
} finally {
|
|
await closeElectronApp(app);
|
|
}
|
|
});
|
|
});
|