mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
eca7c7ab98
* fix(web): align runtime error messages with approved product copy * test(chat): give retry deduplication scenario a Cloud owner * test(chat): use the supported Cloud model preference shape * test(chat): align CLI recovery scenarios with fixed Cloud actions * test(e2e): align Cloud error layout with fixed recovery actions * test(e2e): use ordinary Cloud failure for error-card layout * test(web): preserve ChatPane helpers and align recovery fixtures * test(e2e): resize error-card layout through the native separator * test(e2e): reject missing error-card action geometry --------- Co-authored-by: lefarcen <935902669@qq.com>
161 lines
5.8 KiB
TypeScript
161 lines
5.8 KiB
TypeScript
import { expect, test } from '@/playwright/suite';
|
|
import {
|
|
fulfillAgentsRoute,
|
|
routeSuccessfulRuns,
|
|
suppressWhatsNew,
|
|
} from '@/playwright/mock-factory';
|
|
import { mockAmrPersonalWorkspace } from '@/playwright/amr';
|
|
import { openNewProjectModal as openNewProjectModalFromProjects } from '@/playwright/rail';
|
|
import type { Page } from '@playwright/test';
|
|
import { T } from '@/timeouts';
|
|
|
|
const STORAGE_KEY = 'open-design:config';
|
|
|
|
test.describe.configure({ timeout: T.xlong });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
/*
|
|
* 版本更新弹窗会**盖住整条顶部 chrome**,而这条用例要从那里点开新建项目。
|
|
*
|
|
* 它不是一直如此:共享 `Dialog` 的遮罩原来是 `z-index: 100`,压不过
|
|
* `.workspace-tabs-chrome`(120),于是弹窗开着也照样点得到侧栏开关 ——
|
|
* 这条用例一直在**借那个缺陷**过关。`8d0b542d0a`(产品裁决 2026-09-07「那都修掉」)
|
|
* 把 `packages/components/src/dialog.module.css` 的 `.backdrop` 抬到 1500,
|
|
* 遮罩终于兑现了「我后面的东西都失效」这句承诺,这条用例随之卡死在
|
|
* `ensureRailOpen` 的那次点击上(`timeout: 0`,一直等到整条用例超时)。
|
|
*
|
|
* 处置和其余 17 个走 `applyStandardMocks` 的 UI 文件一致:把这条**与本用例无关**
|
|
* 的发版公告挡在外面。本用例考的是「空流应当显示批准的失败标题而不是 Done」。
|
|
*/
|
|
await suppressWhatsNew(page);
|
|
await page.route('**/api/integrations/vela/status*', async (route) => {
|
|
await route.fulfill({
|
|
json: {
|
|
loggedIn: true,
|
|
profile: 'local',
|
|
configPath: '/tmp/.amr/config.json',
|
|
user: { id: 'api-empty-response', email: 'api-empty-response@example.com' },
|
|
},
|
|
});
|
|
});
|
|
await mockAmrPersonalWorkspace(page);
|
|
await page.addInitScript((key) => {
|
|
window.localStorage.setItem(
|
|
key,
|
|
JSON.stringify({
|
|
mode: 'api',
|
|
apiProtocol: 'openai',
|
|
apiKey: 'test-byok-key',
|
|
baseUrl: 'https://api.deepseek.com',
|
|
model: 'deepseek-v4-flash',
|
|
agentId: null,
|
|
skillId: null,
|
|
designSystemId: null,
|
|
onboardingCompleted: true,
|
|
agentModels: {},
|
|
privacyDecisionAt: 1,
|
|
telemetry: { metrics: false, content: false, artifactManifest: false },
|
|
}),
|
|
);
|
|
}, STORAGE_KEY);
|
|
|
|
await page.route('**/api/app-config', async (route) => {
|
|
if (route.request().method() !== 'GET') {
|
|
await route.continue();
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
json: {
|
|
config: {
|
|
onboardingCompleted: true,
|
|
agentId: null,
|
|
skillId: null,
|
|
designSystemId: null,
|
|
agentModels: {},
|
|
privacyDecisionAt: 1,
|
|
telemetry: { metrics: false, content: false, artifactManifest: false },
|
|
},
|
|
},
|
|
});
|
|
});
|
|
await page.route('**/api/agents**', async (route) => {
|
|
await fulfillAgentsRoute(route, [
|
|
{
|
|
id: 'byok-opencode',
|
|
name: 'BYOK OpenCode',
|
|
bin: 'opencode',
|
|
available: true,
|
|
version: 'test',
|
|
models: [{ id: 'default', label: 'Default' }],
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
test('[P0] @critical API empty stream shows approved failure status instead of Done', async ({ page }) => {
|
|
const runRequests = await routeSuccessfulRuns(page, { runIdPrefix: 'api-empty-response-run' });
|
|
|
|
await gotoEntryHome(page);
|
|
await createProject(page, 'API empty response smoke');
|
|
await expectWorkspaceReady(page);
|
|
await sendPrompt(page, 'Create a login page');
|
|
|
|
await runRequests.expectCount(1);
|
|
await expect(page.locator('.assistant-label', { hasText: 'The task could not be completed' })).toBeVisible();
|
|
await expect(page.getByText('This task failed to run. Please retry. If it fails again, please contact support.', { exact: true }).first()).toBeVisible();
|
|
await expect(page.locator('.assistant-label', { hasText: 'Done' })).toHaveCount(0);
|
|
});
|
|
|
|
async function createProject(page: Page, name: string) {
|
|
await openNewProjectModal(page);
|
|
await page.getByTestId('new-project-tab-prototype').click();
|
|
await page.getByTestId('new-project-name').fill(name);
|
|
await page.getByTestId('create-project').click();
|
|
}
|
|
|
|
async function gotoEntryHome(page: Page) {
|
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
await waitForLoadingToClear(page);
|
|
const privacyDialog = page.getByRole('dialog').filter({ hasText: 'Help us improve OpenDesign' });
|
|
if (await privacyDialog.isVisible()) {
|
|
await privacyDialog.getByRole('button', { name: /I get it|not now|got it|don't share/i }).click();
|
|
await expect(privacyDialog).toHaveCount(0);
|
|
}
|
|
await expect(page.getByTestId('home-hero')).toBeVisible();
|
|
await expect(page.getByTestId('home-hero-input')).toBeVisible();
|
|
}
|
|
|
|
async function openNewProjectModal(page: Page) {
|
|
await openNewProjectModalFromProjects(page);
|
|
}
|
|
|
|
async function expectWorkspaceReady(page: Page) {
|
|
await waitForLoadingToClear(page);
|
|
await expect(page).toHaveURL(/\/projects\//, { timeout: T.long });
|
|
await expect(page.getByTestId('chat-composer')).toBeVisible();
|
|
await expect(page.getByTestId('chat-composer-input')).toBeVisible();
|
|
await expect(page.getByTestId('file-workspace')).toBeVisible();
|
|
}
|
|
|
|
async function sendPrompt(page: Page, prompt: string) {
|
|
const input = page.getByTestId('chat-composer-input');
|
|
const sendButton = page.getByTestId('chat-send');
|
|
await expect(input).toBeVisible({ timeout: 3_000 });
|
|
await input.fill(prompt);
|
|
await expect(sendButton).toBeEnabled();
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(response) => {
|
|
const url = new URL(response.url());
|
|
return url.pathname === '/api/runs' && response.request().method() === 'POST';
|
|
},
|
|
{ timeout: T.long },
|
|
),
|
|
sendButton.click(),
|
|
]);
|
|
}
|
|
|
|
async function waitForLoadingToClear(page: Page) {
|
|
await page.getByText('Loading OpenDesign…').waitFor({ state: 'hidden', timeout: T.medium });
|
|
}
|