diff --git a/frontend/cypress/e2e/board_tasks.cy.ts b/frontend/cypress/e2e/board_tasks.cy.ts new file mode 100644 index 00000000..937682f8 --- /dev/null +++ b/frontend/cypress/e2e/board_tasks.cy.ts @@ -0,0 +1,140 @@ +/// + +describe("/boards/:id task board", () => { + const apiBase = "**/api/v1"; + const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; + + const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); + + beforeEach(() => { + Cypress.config("defaultCommandTimeout", 20_000); + }); + + afterEach(() => { + Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); + }); + + function stubEmptySse() { + // Any SSE endpoint should not hang the UI in tests. + cy.intercept("GET", `${apiBase}/**/stream*`, { + statusCode: 200, + headers: { "content-type": "text/event-stream" }, + body: "", + }); + } + + it("auth negative: signed-out user is redirected to sign-in", () => { + cy.visit("/boards/b1"); + cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + }); + + it("happy path: renders tasks from snapshot and can create a task (stubbed)", () => { + stubEmptySse(); + + cy.intercept("GET", `${apiBase}/boards/b1/snapshot*`, { + statusCode: 200, + body: { + board: { + id: "b1", + name: "Demo Board", + slug: "demo-board", + description: "Demo", + gateway_id: "g1", + board_group_id: null, + board_type: "general", + objective: null, + success_metrics: null, + target_date: null, + goal_confirmed: true, + goal_source: "test", + organization_id: "o1", + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + }, + tasks: [ + { + id: "t1", + board_id: "b1", + title: "Inbox task", + description: "", + status: "inbox", + priority: "medium", + due_at: null, + assigned_agent_id: null, + depends_on_task_ids: [], + created_by_user_id: null, + in_progress_at: null, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + blocked_by_task_ids: [], + is_blocked: false, + assignee: null, + approvals_count: 0, + approvals_pending_count: 0, + }, + ], + agents: [], + approvals: [], + chat_messages: [], + pending_approvals_count: 0, + }, + }).as("snapshot"); + + cy.intercept("GET", `${apiBase}/boards/b1/group-snapshot*`, { + statusCode: 200, + body: { group: null, boards: [] }, + }); + + cy.intercept("POST", `${apiBase}/boards/b1/tasks`, (req) => { + // Minimal assertion the UI sends expected fields. + expect(req.body).to.have.property("title"); + req.reply({ + statusCode: 200, + body: { + id: "t2", + board_id: "b1", + title: req.body.title, + description: req.body.description ?? "", + status: "inbox", + priority: req.body.priority ?? "medium", + due_at: null, + assigned_agent_id: null, + depends_on_task_ids: [], + created_by_user_id: null, + in_progress_at: null, + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + blocked_by_task_ids: [], + is_blocked: false, + assignee: null, + approvals_count: 0, + approvals_pending_count: 0, + }, + }); + }).as("createTask"); + + cy.visit("/sign-in"); + cy.clerkLoaded(); + cy.clerkSignIn({ strategy: "email_code", identifier: email }); + + cy.visit("/boards/b1"); + cy.waitForAppLoaded(); + + cy.wait(["@snapshot"]); + + // Existing task visible. + cy.contains("Inbox task").should("be.visible"); + + // Open create task flow. + cy.contains("button", /create task|new task|add task|\+/i) + .first() + .click({ force: true }); + + cy.get("input").filter('[placeholder*="Title"], [name*="title"], [id*="title"], input[type="text"]').first().type("New task"); + + cy.contains("button", /create|save|add/i).click({ force: true }); + cy.wait(["@createTask"]); + + cy.contains("New task").should("be.visible"); + }); +}); diff --git a/frontend/cypress/e2e/boards_list.cy.ts b/frontend/cypress/e2e/boards_list.cy.ts new file mode 100644 index 00000000..e8a01387 --- /dev/null +++ b/frontend/cypress/e2e/boards_list.cy.ts @@ -0,0 +1,68 @@ +/// + +describe("/boards", () => { + const apiBase = "**/api/v1"; + const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; + + const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); + + beforeEach(() => { + Cypress.config("defaultCommandTimeout", 20_000); + }); + + afterEach(() => { + Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); + }); + + it("auth negative: signed-out user is redirected to sign-in", () => { + cy.visit("/boards"); + cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + }); + + it("happy path: signed-in user sees boards list", () => { + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { + items: [ + { + id: "b1", + name: "Demo Board", + slug: "demo-board", + description: "Demo", + gateway_id: "g1", + board_group_id: null, + board_type: "general", + objective: null, + success_metrics: null, + target_date: null, + goal_confirmed: true, + goal_source: "test", + organization_id: "o1", + created_at: "2026-02-11T00:00:00Z", + updated_at: "2026-02-11T00:00:00Z", + }, + ], + total: 1, + limit: 200, + offset: 0, + }, + }).as("boards"); + + cy.intercept("GET", `${apiBase}/board-groups*`, { + statusCode: 200, + body: { items: [], total: 0, limit: 200, offset: 0 }, + }).as("boardGroups"); + + cy.visit("/sign-in"); + cy.clerkLoaded(); + cy.clerkSignIn({ strategy: "email_code", identifier: email }); + + cy.visit("/boards"); + cy.waitForAppLoaded(); + + cy.wait(["@boards", "@boardGroups"]); + + cy.contains(/boards/i).should("be.visible"); + cy.contains("Demo Board").should("be.visible"); + }); +});