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