Files
makelore/tests/e2e/image-workspace-v2.spec.ts

456 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
test.describe('AI Design V2 workspace', () => {
test('keeps Prompt Museum cards usable when relative media fails', 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 image = {
url: '/api/image-prompt-museum/e2e-entry/media/thumbnail',
width: 1200,
height: 900,
alt: 'E2E museum thumbnail',
};
const card = {
id: 'e2e-entry',
slug: 'e2e-entry',
title: '相对媒体 E2E',
summary: '验证图片失败不会阻断卡片。',
thumbnail: image,
categories: [],
model: { id: 'e2e-model', name: 'E2E Model' },
language: 'zh-CN',
attribution: {
author: { name: 'E2E 作者' },
source: { name: 'E2E 来源' },
license: { name: '测试许可', attributionText: 'E2E attribution' },
},
publishedAt: '2026-08-18T00:00:00.000Z',
updatedAt: '2026-08-18T00:00:00.000Z',
};
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: [],
},
});
}
if (path === '/api/works/image-prompt-museum') {
return respond({
success: true,
data: {
items: [card],
facets: { useCases: [], styles: [], subjects: [] },
nextCursor: null,
total: 1,
},
});
}
if (path === '/api/works/image-prompt-museum/e2e-entry') {
return respond({
success: true,
data: {
...card,
prompt: 'Create a resilient image card.',
variables: [],
images: [image],
requiresReferenceImages: false,
},
});
}
if (path === '/api/works/image-prompt-museum/e2e-entry/media/thumbnail') {
return respond({ mimeType: 'text/html', dataBase64: 'PGgxPnVuc2FmZTwvaDE+' });
}
return { ok: false, error: { message: `Unexpected E2E Host API request: ${path}` } };
});
});
await page.getByTestId('ai-module-option-painting').click();
const imageSidebar = page.getByTestId('sidebar-image-workspace');
await expect(imageSidebar.getByRole('button', { name: '获取灵感' })).toBeVisible();
await imageSidebar.getByRole('button', { name: '获取灵感' }).click();
const cardButton = page.getByRole('button', { name: '查看 相对媒体 E2E' });
await expect(cardButton).toBeVisible();
await expect(page.getByLabel('E2E museum thumbnail加载失败')).toBeVisible();
await expect(cardButton).toBeEnabled();
await cardButton.click();
await expect(page.getByRole('button', { name: '关闭详情' })).toBeVisible();
} finally {
await closeElectronApp(app);
}
});
test('keeps conversation and direct edits on one Living Form through immutable 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, learning: 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 (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-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, learning: 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.poll(async () => await app.evaluate(() => Boolean(
(globalThis as typeof globalThis & { __designE2eProfileReady?: boolean }).__designE2eProfileReady,
))).toBe(true);
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
await expect(page.getByTestId('image-workspace-living-form')).toBeVisible();
await expect(page.getByText('Living Form')).toBeVisible();
await expect(page.getByText('目标与用途')).toBeVisible();
await expect(page.getByText('内容与叙事')).toBeVisible();
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 form = document.querySelector('[data-testid="image-workspace-living-form"]')
?.getBoundingClientRect();
return conversation && form
? { conversationHeight: conversation.height, formHeight: form.height, topGap: Math.abs(conversation.top - form.top) }
: null;
});
expect(layout).not.toBeNull();
expect(layout!.conversationHeight).toBeGreaterThan(300);
expect(Math.abs(layout!.conversationHeight - layout!.formHeight)).toBeLessThanOrEqual(1);
expect(layout!.topGap).toBeLessThanOrEqual(1);
const ratioInput = page.getByLabel('画幅比例');
await ratioInput.fill('16:9');
await expect(ratioInput).toHaveValue('16:9');
await page.getByRole('button', { name: '保存', exact: true }).click();
await expect(page.getByText('方向 5')).toBeVisible();
const followUp = '主体更克制,留出品牌标题空间';
await page.getByLabel('给设计 Agent 留言').fill(followUp);
await page.getByLabel('给设计 Agent 留言').press('Enter');
await expect(page.getByTestId('image-workspace-conversation')).toContainText(followUp);
await expect(page.getByTestId('image-workspace-conversation'))
.toContainText('已把这条补充写入同一份设计规格。');
await page.getByRole('button', { name: '编译生成方案' }).click();
await expect(page.getByText('报价已冻结')).toBeVisible();
await expect(page.getByText('对应规格版本 5确认后不会再读取当前表单的新修改。')).toBeVisible();
await expect(page.getByText('确认即按此不可变报价创建任务,不会在客户端拼接或改写提示词。')).toBeVisible();
await expect(page.getByLabel(/Prompt|提示词/i)).toHaveCount(0);
await page.getByRole('button', { name: '确认生成' }).click();
await expect(page.getByText('生成任务')).toBeVisible();
await expect(page.getByText('图片任务')).toBeVisible();
await expect(page.getByText('排队中')).toBeVisible();
await expect(page.getByText('正常')).toBeVisible();
await expect(page.getByText('1200', { exact: true })).toHaveCount(0);
} finally {
await closeElectronApp(app);
}
});
});