mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 19:16:17 +00:00
* feat(frontend): move capability management out of settings * fix(frontend): address capability center review feedback
263 lines
8.8 KiB
TypeScript
263 lines
8.8 KiB
TypeScript
import { mkdir } from "node:fs/promises";
|
||
import path from "node:path";
|
||
|
||
import { expect, test, type Page } from "@playwright/test";
|
||
|
||
import { mockLangGraphAPI } from "./utils/mock-api";
|
||
|
||
const skills = [
|
||
{
|
||
name: "deep-research",
|
||
description:
|
||
"Search, cross-check sources, and write a detailed research report.",
|
||
},
|
||
{
|
||
name: "data-analysis",
|
||
description: "Analyze structured data and create charts.",
|
||
},
|
||
{
|
||
name: "academic-paper-review",
|
||
description:
|
||
"Review research papers, methods, contributions, and limitations.",
|
||
},
|
||
{
|
||
name: "ppt-generation",
|
||
description: "Create presentations from ideas and reference materials.",
|
||
},
|
||
{
|
||
name: "frontend-design",
|
||
description: "Build polished frontend interfaces.",
|
||
},
|
||
{
|
||
name: "image-generation",
|
||
description: "Generate images from a written description.",
|
||
},
|
||
{
|
||
name: "podcast-generation",
|
||
description: "Create podcast scripts and audio.",
|
||
},
|
||
{
|
||
name: "skill-creator",
|
||
description: "Create reusable skills for new tasks.",
|
||
},
|
||
{
|
||
name: "skill-reviewer",
|
||
description: "Review skill quality and report potential issues.",
|
||
},
|
||
].map((skill) => ({
|
||
...skill,
|
||
category: "public",
|
||
enabled: true,
|
||
license: "MIT",
|
||
}));
|
||
|
||
async function mockCatalog(page: Page) {
|
||
mockLangGraphAPI(page, { skills, threads: [] });
|
||
await page.route("**/api/mcp/config", (route) =>
|
||
route.fulfill({
|
||
json: {
|
||
mcp_servers: {
|
||
GitHub: {
|
||
description:
|
||
"搜索代码与仓库,查看 Issue 和 Pull Request,协助推进开发工作。",
|
||
enabled: true,
|
||
type: "http",
|
||
url: "https://example.test/github",
|
||
},
|
||
Notion: {
|
||
description: "搜索工作空间里的笔记和文档,整理资料,创建新的页面。",
|
||
enabled: true,
|
||
type: "http",
|
||
url: "https://example.test/notion",
|
||
},
|
||
"Brave Search": {
|
||
description: "搜索互联网上的信息,为研究、写作和决策补充最新资料。",
|
||
enabled: true,
|
||
command: "example-search",
|
||
},
|
||
Filesystem: {
|
||
description: "访问已授权的文件夹,读取文件内容并整理本地工作资料。",
|
||
enabled: true,
|
||
command: "example-files",
|
||
},
|
||
PostgreSQL: {
|
||
description: "查询数据库中的业务数据,探索表结构,辅助数据分析。",
|
||
enabled: false,
|
||
command: "example-database",
|
||
},
|
||
},
|
||
},
|
||
}),
|
||
);
|
||
}
|
||
|
||
async function screenshot(page: Page, name: string) {
|
||
const directory = process.env.CAPABILITY_SCREENSHOT_DIR;
|
||
if (!directory) return;
|
||
await mkdir(directory, { recursive: true });
|
||
await page.screenshot({ path: path.join(directory, name), fullPage: true });
|
||
}
|
||
|
||
test("catalog navigation, search, details, and migrated settings", async ({
|
||
page,
|
||
baseURL,
|
||
}) => {
|
||
test.setTimeout(90_000);
|
||
const errors: string[] = [];
|
||
page.on("pageerror", (error) => errors.push(error.message));
|
||
await page.setViewportSize({ width: 1512, height: 1060 });
|
||
await page
|
||
.context()
|
||
.addCookies([{ name: "locale", value: "zh-CN", url: baseURL! }]);
|
||
await mockCatalog(page);
|
||
await page.goto("/workspace/capabilities");
|
||
await expect(
|
||
page.getByRole("heading", { name: "能力中心", exact: true }),
|
||
).toBeVisible();
|
||
await expect(page.locator("article")).toHaveCount(6);
|
||
await expect(page.locator("a[href='/workspace/capabilities']")).toBeVisible();
|
||
await screenshot(page, "capability-center-plugins.png");
|
||
|
||
await page
|
||
.getByRole("textbox", { name: "搜索插件名称或用途" })
|
||
.fill("Notion");
|
||
await expect(page.locator("article")).toHaveCount(1);
|
||
await page.getByRole("button", { name: "编辑 Notion", exact: true }).click();
|
||
await expect(page.getByRole("dialog")).toBeVisible();
|
||
await expect(
|
||
page.getByRole("textbox", { name: "MCP 服务器 JSON 定义" }),
|
||
).toHaveValue(/example.test\/notion/);
|
||
await page.keyboard.press("Escape");
|
||
await page.getByRole("tab", { name: "技能", exact: true }).click();
|
||
await expect(page.locator("article")).toHaveCount(9);
|
||
await expect(
|
||
page.getByRole("button", { name: "查看详情 深度研究", exact: true }),
|
||
).toBeVisible();
|
||
await page.setViewportSize({ width: 1512, height: 1270 });
|
||
await screenshot(page, "capability-center-skills.png");
|
||
await page
|
||
.getByRole("button", { name: "查看详情 深度研究", exact: true })
|
||
.click();
|
||
await expect(
|
||
page.getByRole("dialog").getByText(skills[0]!.description),
|
||
).toBeVisible();
|
||
await page.keyboard.press("Escape");
|
||
await page.getByRole("tab", { name: "社区", exact: true }).click();
|
||
await expect(page.getByText("从社区带来新的技能")).toBeVisible();
|
||
|
||
await page.goto("/workspace/capabilities?settings=appearance");
|
||
const settings = page.getByRole("dialog", { name: "设置", exact: true });
|
||
await expect(settings).toBeVisible();
|
||
for (const name of ["工具", "集成", "技能"]) {
|
||
await expect(
|
||
settings.getByRole("button", { name, exact: true }),
|
||
).toHaveCount(0);
|
||
}
|
||
expect(errors).toEqual([]);
|
||
});
|
||
|
||
test("the skills catalog fits a mobile viewport", async ({ page }) => {
|
||
await page.setViewportSize({ width: 390, height: 844 });
|
||
await mockCatalog(page);
|
||
await page.goto("/workspace/capabilities?tab=skills");
|
||
await expect(page).toHaveURL(/workspace\/capabilities\?tab=skills/);
|
||
await expect(page.locator("article")).toHaveCount(9);
|
||
expect(
|
||
await page.evaluate(
|
||
() => document.documentElement.scrollWidth <= innerWidth,
|
||
),
|
||
).toBe(true);
|
||
await page
|
||
.getByRole("button", { name: "Toggle Sidebar", exact: true })
|
||
.click();
|
||
await expect(
|
||
page.getByRole("link", { name: "Capability Center" }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("MCP access errors preserve the independently available Lark integration", async ({
|
||
page,
|
||
}) => {
|
||
await mockCatalog(page);
|
||
await page.route("**/api/mcp/config", (route) =>
|
||
route.fulfill({ status: 403, json: { detail: "Admin only" } }),
|
||
);
|
||
await page.goto("/workspace/capabilities");
|
||
await expect(
|
||
page.getByRole("alert").filter({ hasText: "Admin privileges" }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.locator("article").filter({ hasText: "Lark / Feishu" }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByRole("button", { name: "Add MCP plugin" }),
|
||
).toHaveCount(0);
|
||
});
|
||
|
||
test("Community search gives feedback and clearing it restores import guidance", async ({
|
||
page,
|
||
}) => {
|
||
await mockCatalog(page);
|
||
await page.goto("/workspace/capabilities?tab=skills");
|
||
await expect(page.locator("article")).toHaveCount(9);
|
||
const search = page.getByRole("textbox", {
|
||
name: "Search skills by name or purpose",
|
||
});
|
||
await search.fill("nonexistent-query");
|
||
await expect(
|
||
page.getByText("No matches found", { exact: true }),
|
||
).toBeVisible();
|
||
await page.getByRole("tab", { name: "Community", exact: true }).click();
|
||
await expect(search).toHaveValue("nonexistent-query");
|
||
await expect(
|
||
page.getByText("No matches found", { exact: true }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByText("Bring a skill from the community", { exact: true }),
|
||
).toHaveCount(0);
|
||
await search.fill(" ");
|
||
await expect(
|
||
page.getByText("Bring a skill from the community", { exact: true }),
|
||
).toBeVisible();
|
||
await search.fill("");
|
||
await page.getByRole("tab", { name: "Built-in", exact: true }).click();
|
||
await expect(page.locator("article")).toHaveCount(9);
|
||
});
|
||
|
||
test("plugin filters remain usable after an MCP refetch fails", async ({
|
||
page,
|
||
}) => {
|
||
await mockCatalog(page);
|
||
let failRead = false;
|
||
await page.route("**/api/mcp/config", async (route) => {
|
||
if (route.request().method() === "PATCH") {
|
||
failRead = true;
|
||
return route.fulfill({ json: { mcp_servers: {} } });
|
||
}
|
||
if (failRead)
|
||
return route.fulfill({ status: 403, json: { detail: "Admin only" } });
|
||
return route.fallback();
|
||
});
|
||
await page.goto("/workspace/capabilities");
|
||
await expect(page.locator("article")).toHaveCount(6);
|
||
const installed = page.getByRole("tab", { name: "Installed", exact: true });
|
||
await installed.click();
|
||
await expect(page.locator("article")).toHaveCount(5);
|
||
await page
|
||
.getByRole("switch", { name: "Enabled GitHub", exact: true })
|
||
.click();
|
||
await expect(page.getByRole("alert")).toBeVisible();
|
||
await expect(installed).toHaveAttribute("aria-selected", "true");
|
||
await expect(
|
||
page.getByRole("button", { name: "Add MCP plugin" }),
|
||
).toHaveCount(0);
|
||
await page.getByRole("tab", { name: "All plugins", exact: true }).click();
|
||
await expect(
|
||
page.locator("article").filter({ hasText: "Lark / Feishu" }),
|
||
).toBeVisible();
|
||
await page
|
||
.getByRole("button", { name: "Configure Lark / Feishu", exact: true })
|
||
.click();
|
||
await expect(page.getByRole("dialog")).toBeVisible();
|
||
});
|