From 4a9cd4007ac0b0a1fc3ee83e5543c18cccec5a5f Mon Sep 17 00:00:00 2001 From: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:56:43 +0530 Subject: [PATCH] chore: Add team invite tests (#12425) Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen --- apps/web/playwright/team/expects.ts | 29 ++++ .../playwright/team/team-invitation.e2e.ts | 124 ++++++++++++++++++ .../ee/teams/components/MemberListItem.tsx | 9 +- .../components/form/inputs/HintOrErrors.tsx | 9 +- 4 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 apps/web/playwright/team/expects.ts create mode 100644 apps/web/playwright/team/team-invitation.e2e.ts diff --git a/apps/web/playwright/team/expects.ts b/apps/web/playwright/team/expects.ts new file mode 100644 index 0000000000..43e02063f6 --- /dev/null +++ b/apps/web/playwright/team/expects.ts @@ -0,0 +1,29 @@ +import type { Page } from "@playwright/test"; +import { expect } from "@playwright/test"; +import { JSDOM } from "jsdom"; +import type { API, Messages } from "mailhog"; + +import { getEmailsReceivedByUser } from "../lib/testUtils"; + +export async function expectInvitationEmailToBeReceived( + page: Page, + emails: API | undefined, + userEmail: string, + subject: string, + returnLink?: string +) { + if (!emails) return null; + + // eslint-disable-next-line playwright/no-wait-for-timeout + await page.waitForTimeout(10000); + const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail }); + expect(receivedEmails?.total).toBe(1); + + const [firstReceivedEmail] = (receivedEmails as Messages).items; + + expect(firstReceivedEmail.subject).toBe(subject); + if (!returnLink) return; + const dom = new JSDOM(firstReceivedEmail.html); + const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`); + return anchor?.getAttribute("href"); +} diff --git a/apps/web/playwright/team/team-invitation.e2e.ts b/apps/web/playwright/team/team-invitation.e2e.ts new file mode 100644 index 0000000000..95505bf279 --- /dev/null +++ b/apps/web/playwright/team/team-invitation.e2e.ts @@ -0,0 +1,124 @@ +import { expect } from "@playwright/test"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; + +import { test } from "../lib/fixtures"; +import { localize } from "../lib/testUtils"; +import { expectInvitationEmailToBeReceived } from "./expects"; + +test.describe.configure({ mode: "parallel" }); + +test.afterEach(async ({ users, emails, clipboard }) => { + clipboard.reset(); + await users.deleteAll(); + emails?.deleteAll(); +}); + +test.describe("Team", () => { + test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => { + const t = await localize("en"); + const teamOwner = await users.create(undefined, { hasTeam: true }); + const { team } = await teamOwner.getFirstTeam(); + await teamOwner.apiLogin(); + await page.goto(`/settings/teams/${team.id}/members`); + await page.waitForLoadState("networkidle"); + + await test.step("To the team by email (external user)", async () => { + const invitedUserEmail = `rick_${Date.now()}@domain-${Date.now()}.com`; + await page.locator(`button:text("${t("add")}")`).click(); + await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); + await page.locator(`button:text("${t("send_invite")}")`).click(); + await page.waitForLoadState("networkidle"); + const inviteLink = await expectInvitationEmailToBeReceived( + page, + emails, + invitedUserEmail, + `${team.name}'s admin invited you to join the team ${team.name} on Cal.com`, + "signup?token" + ); + + //Check newly invited member exists and is pending + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(1); + + // eslint-disable-next-line playwright/no-conditional-in-test + if (!inviteLink) return null; + + // Follow invite link to new window + const context = await browser.newContext(); + const newPage = await context.newPage(); + await newPage.goto(inviteLink); + await newPage.waitForLoadState("networkidle"); + + // Check required fields + await newPage.locator("button[type=submit]").click(); + await expect(newPage.locator('[data-testid="hint-error"]')).toHaveCount(3); + await newPage.locator("input[name=password]").fill(`P4ssw0rd!`); + await newPage.locator("button[type=submit]").click(); + await newPage.waitForURL("/getting-started?from=signup"); + await newPage.close(); + await context.close(); + + // Check newly invited member is not pending anymore + await page.bringToFront(); + await page.goto(`/settings/teams/${team.id}/members`); + await page.waitForLoadState("networkidle"); + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(0); + }); + + await test.step("To the team by invite link", async () => { + const user = await users.create({ + email: `user-invite-${Date.now()}@domain.com`, + password: "P4ssw0rd!", + }); + await page.locator(`button:text("${t("add")}")`).click(); + await page.locator(`[data-testid="copy-invite-link-button"]`).click(); + const inviteLink = await clipboard.get(); + await page.waitForLoadState("networkidle"); + + const context = await browser.newContext(); + const inviteLinkPage = await context.newPage(); + await inviteLinkPage.goto(inviteLink); + await inviteLinkPage.waitForLoadState("domcontentloaded"); + + await inviteLinkPage.locator("button[type=submit]").click(); + await expect(inviteLinkPage.locator('[data-testid="field-error"]')).toHaveCount(2); + + await inviteLinkPage.locator("input[name=email]").fill(user.email); + await inviteLinkPage.locator("input[name=password]").fill(user.username || "P4ssw0rd!"); + await inviteLinkPage.locator("button[type=submit]").click(); + + await inviteLinkPage.waitForURL(`${WEBAPP_URL}/teams**`); + }); + }); + + test("Invitation (verified)", async ({ browser, page, users, emails }) => { + const t = await localize("en"); + const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true }); + const { team } = await teamOwner.getFirstTeam(); + await teamOwner.apiLogin(); + await page.goto(`/settings/teams/${team.id}/members`); + await page.waitForLoadState("networkidle"); + + await test.step("To the organization by email (internal user)", async () => { + const invitedUserEmail = `rick@example.com`; + await page.locator(`button:text("${t("add")}")`).click(); + await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); + await page.locator(`button:text("${t("send_invite")}")`).click(); + await page.waitForLoadState("networkidle"); + await expectInvitationEmailToBeReceived( + page, + emails, + invitedUserEmail, + `${teamOwner.name} invited you to join the team ${team.name} on Cal.com` + ); + + await expect( + page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`) + ).toHaveCount(1); + }); + }); +}); diff --git a/packages/features/ee/teams/components/MemberListItem.tsx b/packages/features/ee/teams/components/MemberListItem.tsx index 2b356747ca..2f2bacfa32 100644 --- a/packages/features/ee/teams/components/MemberListItem.tsx +++ b/packages/features/ee/teams/components/MemberListItem.tsx @@ -152,7 +152,14 @@ export default function MemberListItem(props: Props) { {props.member.role && }
- + {props.member.email} {bookingLink && ( diff --git a/packages/ui/components/form/inputs/HintOrErrors.tsx b/packages/ui/components/form/inputs/HintOrErrors.tsx index a2115f7c56..adc3ce6fca 100644 --- a/packages/ui/components/form/inputs/HintOrErrors.tsx +++ b/packages/ui/components/form/inputs/HintOrErrors.tsx @@ -50,7 +50,10 @@ export function HintsOrErrors({ return (
  • + data-testid="hint-error" + className={ + error !== undefined ? (submitted ? "bg-yellow-200 text-red-700" : "") : "text-green-600" + }> {error !== undefined ? ( submitted ? ( @@ -72,7 +75,9 @@ export function HintsOrErrors({ // errors exist, not custom ones, just show them as is if (fieldErrors) { return ( -
    +