From 435adabec08536c8ffa52d9781ae98a5476d3d42 Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 8 Feb 2026 16:21:51 +0000 Subject: [PATCH] tests: make org stories deterministic via org creation --- frontend/cypress/e2e/organizations.cy.ts | 70 ++++++++++++++++++++---- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/frontend/cypress/e2e/organizations.cy.ts b/frontend/cypress/e2e/organizations.cy.ts index 13364567..42d24178 100644 --- a/frontend/cypress/e2e/organizations.cy.ts +++ b/frontend/cypress/e2e/organizations.cy.ts @@ -1,24 +1,72 @@ describe("Organizations (PR #61)", () => { const email = Cypress.env("CLERK_TEST_EMAIL") || "jane+clerk_test@example.com"; - beforeEach(() => { - // Story: user signs in via official Clerk Cypress commands. + it("negative: signed-out user is redirected to sign-in when opening /organization", () => { + cy.visit("/organization"); + cy.location("pathname", { timeout: 30_000 }).should("match", /\/sign-in/); + }); + + it("positive: org owner can create an org and invite a member (copy invite link)", () => { + // Story (positive): user signs in, creates a new org, then invites a teammate. cy.visit("/sign-in"); cy.clerkLoaded(); cy.clerkSignIn({ strategy: "email_code", identifier: email }); - }); - it("signed-in user can open /organization (and non-admin cannot invite)", () => { - // Story (negative): a signed-in non-admin should not be able to invite members. - // (CI test user may not be an org admin.) + // Go to a page that shows OrgSwitcher. + cy.visit("/boards"); + + // Create a new org via OrgSwitcher. + cy.contains(/org switcher/i, { timeout: 30_000 }).should("be.visible"); + + // Open select and click "Create new org". + cy.contains("button", /select organization/i) + .should("be.visible") + .click(); + + cy.contains(/create new org/i).should("be.visible").click(); + + const orgName = `Cypress Org ${Date.now()}`; + cy.get("#org-name").should("be.visible").clear().type(orgName); + cy.contains("button", /^create org$/i).should("be.visible").click(); + + // Creating org triggers a reload; ensure we land back in the app. + cy.location("pathname", { timeout: 60_000 }).should("match", /\/(boards|organization|activity)/); + + // Now visit organization admin page and create an invite. cy.visit("/organization"); - cy.contains(/members\s*&\s*invites/i, { timeout: 30_000 }).should("be.visible"); - cy.contains("button", /invite member/i) + cy.contains("button", /invite member/i).should("be.visible").click(); + + const invitedEmail = `cypress+invite-${Date.now()}@example.com`; + cy.get('input[type="email"]').should("be.visible").clear().type(invitedEmail); + + // Invite submit button text varies; match loosely. + cy.contains("button", /invite/i).should("be.visible").click(); + + // Confirm invite shows up in table. + cy.contains(invitedEmail, { timeout: 30_000 }).should("be.visible"); + + // Stub clipboard and verify "Copy link" emits /invite?token=... + cy.window().then((win) => { + if (!win.navigator.clipboard) { + // @ts-expect-error - allow defining clipboard in test runtime + win.navigator.clipboard = { writeText: () => Promise.resolve() }; + } + cy.stub(win.navigator.clipboard, "writeText").as("writeText"); + }); + + cy.contains("tr", invitedEmail) .should("be.visible") - .should("be.disabled") - .and("have.attr", "title") - .and("match", /only organization admins can invite/i); + .within(() => { + cy.contains("button", /copy link/i).click(); + }); + + cy.get("@writeText").should("have.been.calledOnce"); + cy.get("@writeText").should((writeText) => { + const stub = writeText as unknown as sinon.SinonStub; + const text = stub.getCall(0).args[0] as string; + expect(text).to.match(/\/invite\?token=/); + }); }); });