import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; import { JSDOM } from "jsdom"; import type { Messages } from "mailhog"; import path from "path"; import { uuid } from "short-uuid"; import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; import type { createEmailsFixture } from "../fixtures/emails"; import { test } from "../lib/fixtures"; import { fillStripeTestCheckout } from "../lib/testUtils"; import { getEmailsReceivedByUser } from "../lib/testUtils"; import { gotoPathAndExpectRedirectToOrgDomain } from "./lib/gotoPathAndExpectRedirectToOrgDomain"; async function expectEmailWithSubject( page: Page, emails: ReturnType, userEmail: string, subject: string ) { if (!emails) return null; // eslint-disable-next-line playwright/no-wait-for-timeout await page.waitForTimeout(2000); const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail }); const allEmails = (receivedEmails as Messages).items; const email = allEmails.find((email) => email.subject === subject); if (!email) { throw new Error(`Email with subject ${subject} not found`); } const dom = new JSDOM(email.html); return dom; } export async function expectOrganizationCreationEmailToBeSent({ page, emails, userEmail, orgSlug, }: { page: Page; emails: ReturnType; userEmail: string; orgSlug: string; }) { const dom = await expectEmailWithSubject(page, emails, userEmail, "Your organization has been created"); const document = dom?.window?.document; expect(document?.querySelector(`[href*=${orgSlug}]`)).toBeTruthy(); return dom; } async function expectOrganizationCreationEmailToBeSentWithLinks({ page, emails, userEmail, oldUsername, newUsername, orgSlug, }: { page: Page; emails: ReturnType; userEmail: string; oldUsername: string; newUsername: string; orgSlug: string; }) { const dom = await expectOrganizationCreationEmailToBeSent({ page, emails, userEmail, orgSlug, }); const document = dom?.window.document; const links = document?.querySelectorAll(`[data-testid="organization-link-info"] [href]`); if (!links) { throw new Error(`data-testid="organization-link-info doesn't have links`); } expect((links[0] as unknown as HTMLAnchorElement).href).toContain(oldUsername); expect((links[1] as unknown as HTMLAnchorElement).href).toContain(newUsername); } export async function expectEmailVerificationEmailToBeSent( page: Page, emails: ReturnType, userEmail: string ) { const subject = "BLS cal: Verify your account"; return expectEmailWithSubject(page, emails, userEmail, subject); } test.afterAll(({ users, orgs }) => { users.deleteAll(); orgs.deleteAll(); }); function capitalize(text: string) { if (!text) { return text; } return text.charAt(0).toUpperCase() + text.slice(1); } test.describe("Organization", () => { test("Admin should be able to create an org where an existing user is made an owner", async ({ page, users, emails, }) => { const appLevelAdmin = await users.create({ role: "ADMIN", }); await appLevelAdmin.apiLogin(); const orgOwnerUsernamePrefix = "owner"; const orgOwnerEmail = users.trackEmail({ username: orgOwnerUsernamePrefix, domain: `example.com`, }); const orgOwnerUser = await users.create({ username: orgOwnerUsernamePrefix, email: orgOwnerEmail, role: "ADMIN", }); const orgOwnerUsernameOutsideOrg = orgOwnerUser.username; const orgOwnerUsernameInOrg = orgOwnerEmail.split("@")[0]; const orgName = capitalize(`${orgOwnerUser.username}`); const orgSlug = `myOrg-${uuid()}`.toLowerCase(); await page.goto("/settings/organizations/new"); await page.waitForLoadState("networkidle"); await test.step("Basic info", async () => { // Check required fields await page.locator("button[type=submit]").click(); await expect(page.locator(".text-red-700")).toHaveCount(3); // Happy path await fillAndSubmitFirstStepAsAdmin(page, orgOwnerEmail, orgName, orgSlug); }); await expectOrganizationCreationEmailToBeSentWithLinks({ page, emails, userEmail: orgOwnerEmail, oldUsername: orgOwnerUsernameOutsideOrg || "", newUsername: orgOwnerUsernameInOrg, orgSlug, }); await test.step("About the organization", async () => { // Choosing an avatar await page.locator('button:text("Upload")').click(); const fileChooserPromise = page.waitForEvent("filechooser"); await page.getByText("Choose a file...").click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png")); await page.locator('button:text("Save")').click(); // About text await page.locator('textarea[name="about"]').fill("This is a testing org"); await page.locator("button[type=submit]").click(); // Waiting to be in next step URL await page.waitForURL("/settings/organizations/*/onboard-members"); }); await test.step("On-board administrators", async () => { await page.waitForSelector('[data-testid="pending-member-list"]'); expect(await page.getByTestId("pending-member-item").count()).toBe(1); const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` }); //can add members await page.getByTestId("new-member-button").click(); await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail); await page.getByTestId("invite-new-member-button").click(); await expect(page.locator(`li:has-text("${adminEmail}")`)).toBeVisible(); // TODO: Check if invited admin received the invitation email // await expectInvitationEmailToBeReceived( // page, // emails, // adminEmail, // `${orgName}'s admin invited you to join the organization ${orgName} on BLS cal` // ); await expect(page.getByTestId("pending-member-item")).toHaveCount(2); // can remove members await expect(page.getByTestId("pending-member-item")).toHaveCount(2); const lastRemoveMemberButton = page.getByTestId("remove-member-button").last(); await lastRemoveMemberButton.click(); await page.waitForLoadState("networkidle"); await expect(page.getByTestId("pending-member-item")).toHaveCount(1); await page.getByTestId("publish-button").click(); // Waiting to be in next step URL await page.waitForURL("/settings/organizations/*/add-teams"); }); await test.step("Create teams", async () => { // Filling one team await page.locator('input[name="teams.0.name"]').fill("Marketing"); // Adding another team await page.locator('button:text("Add a team")').click(); await page.locator('input[name="teams.1.name"]').fill("Sales"); // Finishing the creation wizard await page.getByTestId("continue_or_checkout").click(); await page.waitForURL("/event-types"); }); await test.step("Login as org owner and pay", async () => { // eslint-disable-next-line playwright/no-skipped-test test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled"); await orgOwnerUser.apiLogin(); await page.goto("/event-types"); const upgradeButton = await page.getByTestId("upgrade_org_banner_button"); await expect(upgradeButton).toBeVisible(); await upgradeButton.click(); // Check that stripe checkout is present const expectedUrl = "https://checkout.stripe.com"; await page.waitForURL((url) => url.href.startsWith(expectedUrl)); const url = page.url(); // Check that the URL matches the expected URL expect(url).toContain(expectedUrl); await fillStripeTestCheckout(page); await page.waitForLoadState("networkidle"); const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button"); await expect(upgradeButtonHidden).toBeHidden(); }); // Verify that the owner's old username redirect is properly set await gotoPathAndExpectRedirectToOrgDomain({ page, org: { slug: orgSlug, }, path: `/${orgOwnerUsernameOutsideOrg}`, expectedPath: `/${orgOwnerUsernameInOrg}`, }); }); test("Admin should be able to create an org where the owner doesn't exist yet", async ({ page, users, emails, }) => { const appLevelAdmin = await users.create({ role: "ADMIN", }); await appLevelAdmin.apiLogin(); const orgOwnerUsername = `owner`; const orgName = capitalize(`${orgOwnerUsername}`); const orgSlug = `myOrg-${uuid()}`.toLowerCase(); const orgOwnerEmail = users.trackEmail({ username: orgOwnerUsername, domain: `example.com`, }); await page.goto("/settings/organizations/new"); await page.waitForLoadState("networkidle"); await test.step("Basic info", async () => { // Check required fields await page.locator("button[type=submit]").click(); await expect(page.locator(".text-red-700")).toHaveCount(3); // Happy path await fillAndSubmitFirstStepAsAdmin(page, orgOwnerEmail, orgName, orgSlug); }); const dom = await expectOrganizationCreationEmailToBeSent({ page, emails, userEmail: orgOwnerEmail, orgSlug, }); expect(dom?.window.document.querySelector(`[href*=${orgSlug}]`)).toBeTruthy(); await expectEmailVerificationEmailToBeSent(page, emails, orgOwnerEmail); // Rest of the steps remain same as org creation with existing user as owner. So skipping them }); test("User can create and upgrade a org", async ({ page, users, emails }) => { // eslint-disable-next-line playwright/no-skipped-test test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled"); const stringUUID = uuid(); const orgOwnerUsername = `owner-${stringUUID}`; const targetOrgEmail = users.trackEmail({ username: orgOwnerUsername, domain: `example.com`, }); const orgOwnerUser = await users.create({ username: orgOwnerUsername, email: targetOrgEmail, }); await orgOwnerUser.apiLogin(); const orgName = capitalize(`${orgOwnerUsername}`); await page.goto("/settings/organizations/new"); await page.waitForLoadState("networkidle"); await test.step("Basic info", async () => { // These values are infered due to an existing user being signed expect(await page.locator("input[name=name]").inputValue()).toBe("Example"); expect(await page.locator("input[name=slug]").inputValue()).toBe("example"); await page.locator("input[name=name]").fill(orgName); await page.locator("input[name=slug]").fill(orgOwnerUsername); await page.locator("button[type=submit]").click(); await page.waitForLoadState("networkidle"); }); await test.step("About the organization", async () => { // Choosing an avatar await page.locator('button:text("Upload")').click(); const fileChooserPromise = page.waitForEvent("filechooser"); await page.getByText("Choose a file...").click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png")); await page.locator('button:text("Save")').click(); // About text await page.locator('textarea[name="about"]').fill("This is a testing org"); await page.locator("button[type=submit]").click(); // Waiting to be in next step URL await page.waitForURL("/settings/organizations/*/onboard-members"); }); await test.step("On-board administrators", async () => { await page.waitForSelector('[data-testid="pending-member-list"]'); expect(await page.getByTestId("pending-member-item").count()).toBe(1); const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` }); //can add members await page.getByTestId("new-member-button").click(); await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail); await page.getByTestId("invite-new-member-button").click(); await expect(page.locator(`li:has-text("${adminEmail}")`)).toBeVisible(); // TODO: Check if invited admin received the invitation email // await expectInvitationEmailToBeReceived( // page, // emails, // adminEmail, // `${orgName}'s admin invited you to join the organization ${orgName} on BLS cal` // ); await expect(page.getByTestId("pending-member-item")).toHaveCount(2); // can remove members await expect(page.getByTestId("pending-member-item")).toHaveCount(2); const lastRemoveMemberButton = page.getByTestId("remove-member-button").last(); await lastRemoveMemberButton.click(); await page.waitForLoadState("networkidle"); await expect(page.getByTestId("pending-member-item")).toHaveCount(1); await page.getByTestId("publish-button").click(); // Waiting to be in next step URL await page.waitForURL("/settings/organizations/*/add-teams"); }); await test.step("Create teams", async () => { // Filling one team await page.locator('input[name="teams.0.name"]').fill("Marketing"); // Adding another team await page.locator('button:text("Add a team")').click(); await page.locator('input[name="teams.1.name"]').fill("Sales"); // Finishing the creation wizard await page.getByTestId("continue_or_checkout").click(); }); await test.step("Login as org owner and pay", async () => { // eslint-disable-next-line playwright/no-skipped-test test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled"); await orgOwnerUser.apiLogin(); await page.goto("/event-types"); const upgradeButton = await page.getByTestId("upgrade_org_banner_button"); await expect(upgradeButton).toBeVisible(); await upgradeButton.click(); // Check that stripe checkout is present const expectedUrl = "https://checkout.stripe.com"; await page.waitForURL((url) => url.href.startsWith(expectedUrl)); const url = page.url(); // Check that the URL matches the expected URL expect(url).toContain(expectedUrl); await fillStripeTestCheckout(page); await page.waitForLoadState("networkidle"); const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button"); await expect(upgradeButtonHidden).toBeHidden(); }); }); test("User gets prompted with >=3 teams to upgrade & can transfer existing teams to org", async ({ page, users, }) => { // eslint-disable-next-line playwright/no-skipped-test test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled"); const numberOfTeams = 3; const stringUUID = uuid(); const orgOwnerUsername = `owner-${stringUUID}`; const targetOrgEmail = users.trackEmail({ username: orgOwnerUsername, domain: `example.com`, }); const orgOwnerUser = await users.create( { username: orgOwnerUsername, email: targetOrgEmail, }, { hasTeam: true, numberOfTeams } ); await orgOwnerUser.apiLogin(); await page.goto("/teams"); await test.step("Has org self serve banner", async () => { // These values are infered due to an existing user being signed const selfServeButtonLocator = await page.getByTestId("setup_your_org_action_button"); await expect(selfServeButtonLocator).toBeVisible(); await selfServeButtonLocator.click(); await page.waitForURL("/settings/organizations/new"); }); await test.step("Basic info", async () => { // These values are infered due to an existing user being signed const slugLocator = await page.locator("input[name=slug]"); expect(await page.locator("input[name=name]").inputValue()).toBe("Example"); expect(await slugLocator.inputValue()).toBe("example"); await slugLocator.fill(`example-${stringUUID}`); await page.locator("button[type=submit]").click(); await page.waitForLoadState("networkidle"); }); await test.step("About the organization", async () => { // Choosing an avatar await page.locator('button:text("Upload")').click(); const fileChooserPromise = page.waitForEvent("filechooser"); await page.getByText("Choose a file...").click(); const fileChooser = await fileChooserPromise; await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png")); await page.locator('button:text("Save")').click(); // About text await page.locator('textarea[name="about"]').fill("This is a testing org"); await page.locator("button[type=submit]").click(); // Waiting to be in next step URL await page.waitForURL("/settings/organizations/*/onboard-members"); }); await test.step("On-board administrators", async () => { await page.waitForSelector('[data-testid="pending-member-list"]'); expect(await page.getByTestId("pending-member-item").count()).toBe(1); const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` }); //can add members await page.getByTestId("new-member-button").click(); await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail); await page.getByTestId("invite-new-member-button").click(); await expect(page.locator(`li:has-text("${adminEmail}")`)).toBeVisible(); // TODO: Check if invited admin received the invitation email // await expectInvitationEmailToBeReceived( // page, // emails, // adminEmail, // `${orgName}'s admin invited you to join the organization ${orgName} on BLS cal` // ); await expect(page.getByTestId("pending-member-item")).toHaveCount(2); // can remove members await expect(page.getByTestId("pending-member-item")).toHaveCount(2); const lastRemoveMemberButton = page.getByTestId("remove-member-button").last(); await lastRemoveMemberButton.click(); await page.waitForLoadState("networkidle"); await expect(page.getByTestId("pending-member-item")).toHaveCount(1); await page.getByTestId("publish-button").click(); // Waiting to be in next step URL await page.waitForURL("/settings/organizations/*/add-teams"); }); await test.step("Move existing teams to org", async () => { // No easy way to get all team checkboxes so we fill all checkboxes on the page in const foundCheckboxes = page.locator('input[type="checkbox"]'); const count = await foundCheckboxes.count(); for (let i = 0; i < count; i++) { const checkbox = foundCheckboxes.nth(i); await checkbox.click(); } }); await test.step("Create teams", async () => { // Filling one team await page.locator('input[name="teams.0.name"]').fill("Marketing"); // Adding another team await page.locator('button:text("Add a team")').click(); await page.locator('input[name="teams.1.name"]').fill("Sales"); // Finishing the creation wizard await page.getByTestId("continue_or_checkout").click(); }); await test.step("Login as org owner and pay", async () => { // eslint-disable-next-line playwright/no-skipped-test test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled"); await orgOwnerUser.apiLogin(); await page.goto("/event-types"); const upgradeButton = await page.getByTestId("upgrade_org_banner_button"); await expect(upgradeButton).toBeVisible(); await upgradeButton.click(); // Check that stripe checkout is present const expectedUrl = "https://checkout.stripe.com"; await page.waitForURL((url) => url.href.startsWith(expectedUrl)); const url = page.url(); // Check that the URL matches the expected URL expect(url).toContain(expectedUrl); await fillStripeTestCheckout(page); await page.waitForLoadState("networkidle"); const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button"); await expect(upgradeButtonHidden).toBeHidden(); }); await test.step("Ensure correctnumberOfTeams are migrated", async () => { // eslint-disable-next-line playwright/no-skipped-test await page.goto("/teams"); await page.waitForLoadState("networkidle"); const teamListItems = await page.getByTestId("team-list-item-link").all(); // Number of teams migrated + the two created in the create teams step expect(teamListItems.length).toBe(numberOfTeams + 2); }); }); }); async function fillAndSubmitFirstStepAsAdmin( page: Page, targetOrgEmail: string, orgName: string, orgSlug: string ) { await page.locator("input[name=orgOwnerEmail]").fill(targetOrgEmail); // Since we are admin fill in this infomation instead of deriving it await page.locator("input[name=name]").fill(orgName); await page.locator("input[name=slug]").fill(orgSlug); // Fill in seat infomation await page.locator("input[name=seats]").fill("30"); await page.locator("input[name=pricePerSeat]").fill("30"); await Promise.all([ page.waitForResponse("**/api/trpc/organizations/create**"), page.locator("button[type=submit]").click(), ]); }