diff --git a/frontend/cypress/e2e/boards_list.cy.ts b/frontend/cypress/e2e/boards_list.cy.ts index b1585cd2..ee075937 100644 --- a/frontend/cypress/e2e/boards_list.cy.ts +++ b/frontend/cypress/e2e/boards_list.cy.ts @@ -1,18 +1,12 @@ /// +import { setupCommonPageTestHooks } from "../support/testHooks"; + describe("/boards", () => { const apiBase = "**/api/v1"; const email = "local-auth-user@example.com"; - const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); - - beforeEach(() => { - Cypress.config("defaultCommandTimeout", 20_000); - }); - - afterEach(() => { - Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); - }); + setupCommonPageTestHooks(apiBase); it("auth negative: signed-out user is shown local auth login", () => { cy.visit("/boards"); @@ -21,7 +15,7 @@ describe("/boards", () => { ); }); - it("happy path: signed-in user sees boards list", () => { + it("happy path: signed-in user sees boards list and create button", () => { cy.intercept("GET", `${apiBase}/organizations/me/member*`, { statusCode: 200, body: { @@ -52,9 +46,7 @@ describe("/boards", () => { cy.intercept("GET", `${apiBase}/organizations/me/list*`, { statusCode: 200, - body: [ - { id: "o1", name: "Personal", role: "owner", is_active: true }, - ], + body: [{ id: "o1", name: "Personal", role: "owner", is_active: true }], }).as("organizations"); cy.intercept("GET", `${apiBase}/boards*`, { @@ -98,5 +90,6 @@ describe("/boards", () => { cy.contains(/boards/i).should("be.visible"); cy.contains("Demo Board").should("be.visible"); + cy.contains("a", /create board/i).should("be.visible"); }); }); diff --git a/frontend/cypress/e2e/global_approvals.cy.ts b/frontend/cypress/e2e/global_approvals.cy.ts new file mode 100644 index 00000000..7e0d7fb4 --- /dev/null +++ b/frontend/cypress/e2e/global_approvals.cy.ts @@ -0,0 +1,81 @@ +/// + +import { setupCommonPageTestHooks } from "../support/testHooks"; + +describe("Global approvals", () => { + const apiBase = "**/api/v1"; + + setupCommonPageTestHooks(apiBase); + + it("can render a pending approval and approve it", () => { + const approval = { + id: "a1", + board_id: "b1", + action_type: "task.closeout", + status: "pending", + confidence: 92, + created_at: "2026-02-14T00:00:00Z", + task_id: "t1", + task_ids: ["t1"], + payload: { + task_id: "t1", + title: "Close task", + reason: "Merged and ready to close", + }, + }; + + cy.intercept("GET", `${apiBase}/boards*`, { + statusCode: 200, + body: { + items: [ + { + id: "b1", + name: "Testing", + group_id: null, + objective: null, + success_metrics: null, + target_date: null, + updated_at: "2026-02-14T00:00:00Z", + created_at: "2026-02-10T00:00:00Z", + }, + ], + }, + }).as("boardsList"); + + cy.intercept("GET", `${apiBase}/boards/b1/approvals*`, { + statusCode: 200, + body: { items: [approval] }, + }).as("approvalsList"); + + cy.intercept("PATCH", `${apiBase}/boards/b1/approvals/a1`, { + statusCode: 200, + body: { ...approval, status: "approved" }, + }).as("approvalUpdate"); + + cy.loginWithLocalAuth(); + cy.visit("/approvals"); + cy.waitForAppLoaded(); + + cy.wait( + [ + "@usersMe", + "@organizationsList", + "@orgMeMember", + "@boardsList", + "@approvalsList", + ], + { timeout: 20_000 }, + ); + + // Pending approval should be visible in the list. + cy.contains(/unapproved tasks/i).should("be.visible"); + // Action type is humanized as "Task · Closeout" in the UI. + cy.contains(/task\s*(?:·|\u00b7|\u2022)?\s*closeout/i).should("be.visible"); + + cy.contains("button", /^approve$/i).click(); + cy.wait("@approvalUpdate", { timeout: 20_000 }); + + // Status badge should flip to approved. + cy.contains(/approved/i).should("be.visible"); + }); +}); diff --git a/frontend/cypress/e2e/skill_packs_sync.cy.ts b/frontend/cypress/e2e/skill_packs_sync.cy.ts new file mode 100644 index 00000000..d0cb05fb --- /dev/null +++ b/frontend/cypress/e2e/skill_packs_sync.cy.ts @@ -0,0 +1,48 @@ +/// + +import { setupCommonPageTestHooks } from "../support/testHooks"; + +describe("Skill packs", () => { + const apiBase = "**/api/v1"; + + setupCommonPageTestHooks(apiBase); + + it("can sync a pack and surface warnings", () => { + cy.intercept("GET", `${apiBase}/skills/packs*`, { + statusCode: 200, + body: [ + { + id: "p1", + name: "OpenClaw Skills", + description: "Test pack", + source_url: "https://github.com/openclaw/skills", + branch: "main", + skill_count: 12, + updated_at: "2026-02-14T00:00:00Z", + created_at: "2026-02-10T00:00:00Z", + }, + ], + }).as("packsList"); + + cy.intercept("POST", `${apiBase}/skills/packs/p1/sync*`, { + statusCode: 200, + body: { + warnings: ["1 skill skipped (missing SKILL.md)"], + }, + }).as("packSync"); + + cy.loginWithLocalAuth(); + cy.visit("/skills/packs"); + cy.waitForAppLoaded(); + + cy.wait(["@usersMe", "@organizationsList", "@orgMeMember", "@packsList"], { + timeout: 20_000, + }); + cy.contains(/openclaw skills/i).should("be.visible"); + + cy.contains("button", /^sync$/i).click(); + cy.wait("@packSync", { timeout: 20_000 }); + + cy.contains(/skill skipped/i).should("be.visible"); + }); +}); diff --git a/frontend/cypress/support/testHooks.ts b/frontend/cypress/support/testHooks.ts new file mode 100644 index 00000000..4414132f --- /dev/null +++ b/frontend/cypress/support/testHooks.ts @@ -0,0 +1,77 @@ +/// + +type CommonPageTestHooksOptions = { + timeoutMs?: number; + orgMemberRole?: string; + organizationId?: string; + organizationName?: string; + userId?: string; + userEmail?: string; + userName?: string; +}; + +export function setupCommonPageTestHooks( + apiBase: string, + options: CommonPageTestHooksOptions = {}, +): void { + const { + timeoutMs = 20_000, + orgMemberRole = "owner", + organizationId = "org1", + organizationName = "Testing Org", + userId = "u1", + userEmail = "local-auth-user@example.com", + userName = "Local User", + } = options; + const originalDefaultCommandTimeout = Cypress.config("defaultCommandTimeout"); + + beforeEach(() => { + Cypress.config("defaultCommandTimeout", timeoutMs); + + cy.intercept("GET", "**/healthz", { + statusCode: 200, + body: { ok: true }, + }).as("healthz"); + + cy.intercept("GET", `${apiBase}/users/me*`, { + statusCode: 200, + body: { + id: userId, + clerk_user_id: "local-auth-user", + email: userEmail, + name: userName, + preferred_name: userName, + timezone: "UTC", + }, + }).as("usersMe"); + + cy.intercept("GET", `${apiBase}/organizations/me/list*`, { + statusCode: 200, + body: [ + { + id: organizationId, + name: organizationName, + is_active: true, + role: orgMemberRole, + }, + ], + }).as("organizationsList"); + + cy.intercept("GET", `${apiBase}/organizations/me/member*`, { + statusCode: 200, + body: { + id: "membership-1", + organization_id: organizationId, + user_id: userId, + role: orgMemberRole, + all_boards_read: true, + all_boards_write: true, + board_access: [], + }, + }).as("orgMeMember"); + }); + + afterEach(() => { + Cypress.config("defaultCommandTimeout", originalDefaultCommandTimeout); + }); +}