test(e2e): add Playwright smoke for FilesTab split

Walks the real UI end-to-end:
1. Creates + registers a workspace on the platform
2. Opens the detail side panel
3. Clicks the Files tab (force-click since it's in an overflow-x bar)
4. Asserts all 3 split components render:
   - FilesToolbar: "+ New" + "Upload" buttons
   - FileTree: the config.yaml seeded by the default template
   - FileEditor: "Select a file to edit" empty-state

Saves screenshots at /tmp/filestab-{1,2,3}-*.png for manual review.

Run: cd canvas && npx playwright test e2e/filestab-smoke.spec.ts

Requires platform on :8080 + canvas on :3000.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-13 18:14:54 -07:00
parent c71cd39ee7
commit 235b4b192b
3 changed files with 150 additions and 0 deletions

View File

@ -0,0 +1,84 @@
import { test, expect } from "@playwright/test";
/**
* Smoke test for the PR #10 FilesTab split. Exercises the UI end-to-end:
* - creates a workspace on the platform
* - opens the detail panel
* - switches to the Files tab
* - confirms tree, toolbar, and editor panels render (the three extracted
* sibling components: FileTree, FilesToolbar, FileEditor)
* - saves a screenshot for visual review
*
* Requires platform on :8080 and canvas on :3000.
*/
test("FilesTab renders after split", async ({ page, request }) => {
// Clean slate
const { workspaces } = await request
.get("http://localhost:8080/workspaces")
.then(async (r) => ({ workspaces: (await r.json()) as Array<{ id: string }> }));
for (const w of workspaces) {
await request.delete(`http://localhost:8080/workspaces/${w.id}?confirm=true`);
}
// Create a workspace
const created = await request
.post("http://localhost:8080/workspaces", {
data: { name: "FilesTab Smoke", tier: 1, runtime: "langgraph" },
headers: { "Content-Type": "application/json" },
})
.then((r) => r.json());
const wsId = created.id as string;
// Register so status flips online (so detail panel content loads cleanly)
await request.post("http://localhost:8080/registry/register", {
data: { id: wsId, url: "http://localhost:9999", agent_card: { name: "Smoke", skills: [] } },
headers: { "Content-Type": "application/json" },
});
await page.goto("/");
await expect(page).toHaveTitle(/Molecule AI/);
// Screenshot: landing
await page.screenshot({ path: "/tmp/filestab-1-landing.png", fullPage: false });
// Dismiss any onboarding overlay if present (best-effort)
const skip = page.getByText(/skip guide/i).first();
if (await skip.isVisible().catch(() => false)) await skip.click();
// Click the workspace node — title text is unique
const node = page.getByText("FilesTab Smoke").first();
await node.waitFor({ timeout: 10_000 });
await node.click();
// Side panel should open
await page.waitForTimeout(300);
await page.screenshot({ path: "/tmp/filestab-2-panel.png", fullPage: false });
// Switch to Files tab. The tab bar overflows-x and buttons off-screen
// resist the usual click path. Use Playwright's force-click on the
// hidden button; this fires a real React onClick.
// Tab button text is "⊞ Files" (icon + label). Use hasText substring.
const filesBtn = page.locator("button").filter({ hasText: "Files" });
await filesBtn.first().scrollIntoViewIfNeeded();
await filesBtn.first().click({ force: true });
await page.waitForTimeout(1200); // let files API load + render the 3 split components
await page.screenshot({ path: "/tmp/filestab-3-files.png", fullPage: false });
// Hard assertion: all three split components are visible.
// FilesToolbar: "+ New", "Upload", "Export", "Clear" buttons.
// FileTree: the config.yaml file from the Go provisioner's default template.
// FileEditor: the empty-state placeholder "Select a file to edit".
const toolbarNew = page.getByRole("button", { name: /new/i });
const toolbarUpload = page.getByRole("button", { name: /upload/i });
const treeFile = page.getByText("config.yaml");
const editorEmpty = page.getByText(/select a file/i);
await expect(toolbarNew.first()).toBeVisible({ timeout: 5_000 });
await expect(toolbarUpload.first()).toBeVisible({ timeout: 5_000 });
await expect(treeFile.first()).toBeVisible({ timeout: 5_000 });
await expect(editorEmpty.first()).toBeVisible({ timeout: 5_000 });
// Cleanup
await request.delete(`http://localhost:8080/workspaces/${wsId}?confirm=true`);
});

View File

@ -24,6 +24,7 @@
"zustand": "^5.0.0" "zustand": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.59.1",
"@testing-library/jest-dom": "^6.6.0", "@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
@ -488,6 +489,23 @@
"url": "https://github.com/sponsors/Boshen" "url": "https://github.com/sponsors/Boshen"
} }
}, },
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@radix-ui/primitive": { "node_modules/@radix-ui/primitive": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
@ -3885,6 +3903,53 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.8", "version": "8.5.8",
"funding": [ "funding": [

View File

@ -26,6 +26,7 @@
"zustand": "^5.0.0" "zustand": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.59.1",
"@testing-library/jest-dom": "^6.6.0", "@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",