molecule-core/canvas/e2e/filestab-smoke.spec.ts
Hongming Wang 235b4b192b 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>
2026-04-13 18:14:54 -07:00

85 lines
3.6 KiB
TypeScript

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`);
});