deer-flow/frontend/tests/e2e/skill-export.spec.ts
Ryker_Feng f52818fe5e
feat(skills): export custom skill packages with revision-bound preview (#5332)
* feat(skills): export custom skill packages with revision preview

* docs(gateway): keep export guidance within size budget

* ci: retry checks after transient uv setup download failure

* docs: focus skill export agent guidance on maintenance invariants

* fix(skills): handle export disconnects and bound archive transfers

* docs(gateway): remove redundant export guidance to fit merged budget

* fix(skills): reset export idle deadline after transfer progress
2026-09-11 16:21:23 +08:00

145 lines
4.4 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { mockLangGraphAPI } from "./utils/mock-api";
const preview = {
skill_name: "demo",
revision: "a".repeat(64),
can_export: true,
file_count: 1,
directory_count: 1,
total_bytes: 20,
files: [{ path: "SKILL.md", type: "file", size: 20, executable: false }],
requirements: {
compatibility: "Python 3.12",
allowed_tools: ["bash"],
required_secrets: [{ name: "DEMO_TOKEN", optional: false }],
},
warnings: [],
blockers: [],
};
test("custom export previews, handles stale content and downloads only after refresh", async ({
page,
}) => {
mockLangGraphAPI(page, {
skills: [
{
name: "demo",
description: "Synthetic UI fixture",
category: "custom",
enabled: false,
},
{
name: "public-demo",
description: "Public fixture",
category: "public",
},
],
});
let previews = 0,
downloads = 0;
await page.route("**/api/skills/custom/demo/export-manifest", (route) => {
previews++;
return route.fulfill({ json: preview });
});
await page.route("**/api/skills/custom/demo/export?*", (route) => {
downloads++;
expect(
new URL(route.request().url()).searchParams.get("expected_revision"),
).toBe(preview.revision);
return downloads === 1
? route.fulfill({
status: 409,
json: { detail: { code: "skill_changed", message: "Changed" } },
})
: route.fulfill({
contentType: "application/zip",
body: Buffer.from("synthetic transport fixture"),
});
});
await page.goto("/workspace/chats/new?settings=skills");
await expect(
page.getByRole("button", { name: "Export public-demo" }),
).toHaveCount(0);
await page.getByRole("tab", { name: "Custom", exact: true }).click();
await page.getByRole("button", { name: "Export demo", exact: true }).click();
const dialog = page.getByRole("dialog", {
name: "Export skill",
exact: true,
});
await expect(dialog.getByText("DEMO_TOKEN (required)")).toBeVisible();
await dialog.getByRole("button", { name: "Download .skill" }).click();
await expect(
dialog.getByText(
"The skill changed. Refresh the file list before downloading.",
),
).toBeVisible();
await expect(
dialog.getByRole("button", { name: "Download .skill" }),
).toBeDisabled();
const beforeRefresh = previews;
await dialog.getByRole("button", { name: "Refresh file list" }).click();
await expect(
dialog.getByRole("button", { name: "Download .skill" }),
).toBeEnabled();
const download = page.waitForEvent("download");
await dialog.getByRole("button", { name: "Download .skill" }).click();
expect((await download).suggestedFilename()).toBe("demo.skill");
await expect(
dialog.getByText("File handed to your browser for download."),
).toBeVisible();
expect(previews).toBe(beforeRefresh + 1);
expect(downloads).toBe(2);
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
await expect(
page.getByRole("dialog", { name: "Settings", exact: true }),
).toBeVisible();
});
test("mobile manifest blockers are readable and cannot download", async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 });
mockLangGraphAPI(page, {
skills: [
{ name: "demo", description: "Synthetic UI fixture", category: "custom" },
],
});
await page.route("**/api/skills/custom/demo/export-manifest", (route) =>
route.fulfill({
json: {
...preview,
revision: null,
can_export: false,
blockers: [
{
code: "skill_export_link",
message: "Linked files or directories cannot be exported.",
path: "scripts/linked",
},
],
},
}),
);
await page.goto("/workspace/chats/new?settings=skills");
await page.getByRole("tab", { name: "Custom", exact: true }).click();
await page.getByRole("button", { name: "Export demo", exact: true }).click();
const dialog = page.getByRole("dialog", {
name: "Export skill",
exact: true,
});
await expect(
dialog.getByText("This package cannot be exported"),
).toBeVisible();
await expect(
dialog.getByRole("button", { name: "Download .skill" }),
).toHaveCount(0);
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
).toBe(true);
});