Files
claw3d/tests/e2e/kanban-board.spec.ts
Luke The Dev a997f13601 feat(kanban): Interactive Kanban board with real-time task tracking (#83)
* feat(kanban): add Kanban board with task-manager skill, modal UI, and desk clutter

Implement a full Kanban board system for tracking agent tasks:
- Add task-manager skill with shared JSON task store for persistence
- Render board as a floating modal over the live 3D office (not immersive)
- Auto-create tasks from actionable user messages with heuristic filtering
- Sync task status through OpenClaw agent lifecycle events
- Collapse task details panel by default, expand on card click
- Add dynamic desk clutter (papers, folders, etc.) reflecting active task count
- Exclude done tasks from desk clutter count
- Extract KANBAN_CLUTTER_OFFSET for easy positioning adjustment
- Add install flow with progress bar for the task-manager skill
- Include unit and e2e test coverage

Made-with: Cursor

* feat(kanban): production-harden task board with AI-free classification, resilient persistence, and modal UX

- Harden shared task store with atomic writes, payload size limits, and server-side enum validation
- Add client resilience: request timeouts (AbortController), exponential backoff retries, poll deduplication
- Implement optimistic UI with rollback on all card mutations (update, move, archive)
- Add modal accessibility: focus trap, Escape to close, aria-modal, keyboard card navigation
- Trust OpenClaw agent lifecycle phase=start as task classification signal instead of regex heuristics
- Keep regex heuristic only as lightweight filter for direct chat events (conversational noise)
- Expand verb recognition with typo tolerance and broader action vocabulary
- Create tasks from agent runs even when no chat event is received (external channel support)
- Merge dual header bars into single bar; reposition close button outside modal corner
- Exclude done tasks from desk clutter count; make clutter position configurable via KANBAN_CLUTTER_OFFSET
- Update default furniture layout to match user configuration
- Ensure kanban_board furniture persists in local storage across sessions
- Add comprehensive test coverage for store, API route, and controller logic

Made-with: Cursor

---------

Co-authored-by: iamlukethedev <lucas.guilherme@smartwayslfl.com>
2026-03-30 22:58:18 -05:00

60 lines
2.1 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { stubStudioRoute } from "./helpers/studioRoute";
test.skip(
process.env.CLAW3D_E2E_GATEWAY !== "1",
"Requires a reachable gateway-backed office shell."
);
test.beforeEach(async ({ page }) => {
await stubStudioRoute(page);
});
test("creates and edits a kanban card from HQ", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Open headquarters sidebar" }).click();
await page.getByRole("tab", { name: "Kanban" }).click();
await page.getByRole("button", { name: "New Task" }).click();
const titleInput = page.getByLabel("Title");
await expect(titleInput).toHaveValue("New task");
await titleInput.fill("Create marketing website");
await page.getByLabel("Description").fill("Landing page for the spring campaign.");
await page.getByLabel("Status").selectOption("in_progress");
await expect(page.getByText("Create marketing website")).toBeVisible();
await expect(titleInput).toHaveValue("Create marketing website");
});
test("persists kanban cards to studio settings", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Open headquarters sidebar" }).click();
await page.getByRole("tab", { name: "Kanban" }).click();
await page.getByRole("button", { name: "New Task" }).click();
await page.getByLabel("Title").fill("Persistent task card");
const request = await page.waitForRequest((req) => {
if (!req.url().includes("/api/studio") || req.method() !== "PUT") {
return false;
}
const payload = JSON.parse(req.postData() ?? "{}") as {
taskBoard?: Record<string, { cards?: Array<{ title?: string }> }>;
};
const entries = Object.values(payload.taskBoard ?? {});
return entries.some((entry) =>
(entry.cards ?? []).some((card) => card.title === "Persistent task card")
);
});
const payload = JSON.parse(request.postData() ?? "{}") as {
taskBoard?: Record<string, { cards?: Array<{ title?: string }> }>;
};
expect(
Object.values(payload.taskBoard ?? {}).some((entry) =>
(entry.cards ?? []).some((card) => card.title === "Persistent task card")
)
).toBe(true);
});