test: Bookings: Add more automated tests for organization (#13576)

* Avoid selecting unused props

* Add automated tests

* Add existing user invite and booking

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
Hariom Balhara
2024-02-08 16:20:00 +00:00
committed by GitHub
co-authored by Joe Au-Yeung
parent c0c40185fc
commit 1293588ac4
12 changed files with 702 additions and 228 deletions
+34 -6
View File
@@ -8,14 +8,14 @@ test.describe("Org", () => {
const response = await page.goto("https://i.cal.com/embed");
expect(response?.status()).toBe(200);
await page.screenshot({ path: "screenshot.jpg" });
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/peer/embed");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
@@ -23,14 +23,14 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
const response = await page.goto("https://i.cal.com/sales/embed");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
@@ -38,9 +38,10 @@ test.describe("Org", () => {
expect(response?.status()).toBe(200);
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
await expectPageToBeServerSideRendered(page);
await expectPageToBeRenderedWithEmbedSsr(page);
});
});
test.describe("Dynamic Group Booking", () => {
test("Dynamic Group booking link should load", async ({ page }) => {
const users = [
@@ -63,12 +64,39 @@ test.describe("Org", () => {
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
});
});
test("Organization Homepage - Has Engineering and Marketing Teams", async ({ page }) => {
const response = await page.goto("https://i.cal.com");
expect(response?.status()).toBe(200);
await expect(page.locator("text=Cal.com")).toBeVisible();
await expect(page.locator("text=Engineering")).toBeVisible();
await expect(page.locator("text=Marketing")).toBeVisible();
});
test.describe("Browse the Engineering Team", async () => {
test("By User Navigation", async ({ page }) => {
await page.goto("https://i.cal.com");
await page.click('text="Engineering"');
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
});
test("By /team/engineering", async ({ page }) => {
await page.goto("https://i.cal.com/team/engineering");
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
});
test("By /engineering", async ({ page }) => {
await page.goto("https://i.cal.com/engineering");
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
});
});
});
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
async function expectPageToBeServerSideRendered(page: Page) {
async function expectPageToBeRenderedWithEmbedSsr(page: Page) {
expect(
await page.evaluate(() => {
//@ts-expect-error - __NEXT_DATA__ is a global variable defined by Next.js
return window.__NEXT_DATA__.props.pageProps.isEmbed;
})
).toBe(true);
+2
View File
@@ -13,6 +13,7 @@ export const createEmailsFixture = () => {
if (IS_MAILHOG_ENABLED) {
const mailhogAPI = mailhog();
return {
messages: mailhogAPI.messages.bind(mailhogAPI),
search: (query: string, kind?: string, start?: number, limit?: number) => {
if (kind === "from" || kind === "to") {
if (!hasUUID(query)) {
@@ -27,6 +28,7 @@ export const createEmailsFixture = () => {
};
} else {
return {
messages: unimplemented,
search: unimplemented,
deleteMessage: unimplemented,
};
+7 -2
View File
@@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
import type { Team } from "@prisma/client";
import { prisma } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const getRandomSlug = () => `org-${Math.random().toString(36).substring(7)}`;
@@ -15,8 +16,12 @@ export const createOrgsFixture = (page: Page) => {
slug: opts.slug || getRandomSlug(),
requestedSlug: opts.requestedSlug,
});
store.orgs.push(org);
return org;
const orgWithMetadata = {
...org,
metadata: teamMetadataSchema.parse(org.metadata),
};
store.orgs.push(orgWithMetadata);
return orgWithMetadata;
},
get: () => store.orgs,
deleteAll: async () => {
+4
View File
@@ -196,6 +196,9 @@ export const createUsersFixture = (
password: opts?.password ?? uname,
};
},
/**
* In case organizationId is passed, it simulates a scenario where a nonexistent user is added to an organization.
*/
create: async (
opts?:
| (CustomUserOpts & {
@@ -726,6 +729,7 @@ const createUser = (
throw new Error("Missing role for user in organization");
}
return {
organizationId,
profiles: {
create: {
uid: ProfileRepository.generateProfileUid(),
+7 -1
View File
@@ -225,7 +225,10 @@ export async function getEmailsReceivedByUser({
if (!emails) return null;
const matchingEmails = await emails.search(userEmail, "to");
if (!matchingEmails?.total) {
console.log(`No emails received by ${userEmail}`);
console.log(
`No emails received by ${userEmail}. All emails sent to:`,
(await emails.messages())?.items.map((e) => e.to)
);
}
return matchingEmails;
}
@@ -358,6 +361,9 @@ export async function doOnOrgDomain(
"x-cal-force-slug": orgSlug,
});
await callback({ page });
await page.setExtraHTTPHeaders({
"x-cal-force-slug": "",
});
}
// When App directory is there, this is the 404 page text. We should work on fixing the 404 page as it changed due to app directory.
@@ -0,0 +1,488 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { test } from "../lib/fixtures";
import {
bookTimeSlot,
doOnOrgDomain,
NotFoundPageTextAppDir,
selectFirstAvailableTimeSlotNextMonth,
testName,
} from "../lib/testUtils";
import { expectExistingUserToBeInvitedToOrganization } from "../team/expects";
import { acceptTeamOrOrgInvite, inviteExistingUserToOrganization } from "./lib/inviteUser";
test.describe("Bookings", () => {
test.afterEach(({ orgs, users }) => {
orgs.deleteAll();
users.deleteAll();
});
test.describe("Team Event", () => {
test("Can create a booking for Collective EventType", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
const { team } = await owner.getFirstTeamMembership();
const teamEvent = await owner.getFirstTeamEvent(team.id);
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookTeamEvent({ page, team, event: teamEvent });
// All the teammates should be in the booking
for (const teammate of teamMatesObj.concat([{ name: owner.name || "" }])) {
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
}
}
);
// TODO: Assert whether the user received an email
});
test("Can create a booking for Round Robin EventType", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.ROUND_ROBIN,
}
);
const { team } = await owner.getFirstTeamMembership();
const teamEvent = await owner.getFirstTeamEvent(team.id);
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookTeamEvent({ page, team, event: teamEvent });
// Since all the users have the same leastRecentlyBooked value
// Anyone of the teammates could be the Host of the booking.
const chosenUser = await page.getByTestId("booking-host-name").textContent();
expect(chosenUser).not.toBeNull();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(teamMatesObj.concat([{ name: owner.name! }]).some(({ name }) => name === chosenUser)).toBe(
true
);
}
);
// TODO: Assert whether the user received an email
});
test("Can access booking page with event slug and team page in lowercase/uppercase/mixedcase", async ({
page,
orgs,
users,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
const { team } = await owner.getFirstTeamMembership();
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
const teamSlugUpperCase = team.slug?.toUpperCase();
const teamEventSlugUpperCase = teamEventSlug.toUpperCase();
// This is the most closest to the actual user flow as org1.cal.com maps to /org/orgSlug
await page.goto(`/org/${org.slug}/${teamSlugUpperCase}/${teamEventSlugUpperCase}`);
await page.waitForSelector("[data-testid=day]");
});
});
test.describe("User Event", () => {
test("Can create a booking", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const event = await user.getFirstEventAsOwner();
await page.goto(`/${user.username}/${event.slug}`);
// Shouldn't be servable on the non-org domain
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user, event });
}
);
});
test.describe("User Event with same slug as another user's", () => {
test("booking is created for first user when first user is booked", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1 = await users.create({
username: "user1",
name: "User 1",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2 = await users.create({
username: "user2",
name: "User2",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user1Event = await user1.getFirstEventAsOwner();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user: user1, event: user1Event });
}
);
});
test("booking is created for second user when second user is booked", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1 = await users.create({
username: "user1",
name: "User 1",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2 = await users.create({
username: "user2",
name: "User2",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2Event = await user2.getFirstEventAsOwner();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user: user2, event: user2Event });
}
);
});
});
});
test.describe("Scenario with same username in and outside organization", () => {
test("Can create a booking for user with same username in and outside organization", async ({
page,
users,
orgs,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const username = "john";
const userInsideOrganization = await users.create({
username,
name: "John Inside Organization",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
eventTypes: [
{
title: "John Inside Org's Meeting",
slug: "john-inside-org-meeting",
length: 15,
},
],
});
const userOutsideOrganization = await users.create({
username,
name: "John Outside Organization",
eventTypes: [
{
title: "John Outside Org's Meeting",
slug: "john-outside-org-meeting",
length: 15,
},
],
});
const eventForUserInsideOrganization = await userInsideOrganization.getFirstEventAsOwner();
const eventForUserOutsideOrganization = await userOutsideOrganization.getFirstEventAsOwner();
// John Inside Org's meeting can't be accessed on userOutsideOrganization's namespace
await expectPageToBeNotFound({
page,
url: `/${userOutsideOrganization.username}/john-inside-org-meeting`,
});
await bookUserEvent({ page, user: userOutsideOrganization, event: eventForUserOutsideOrganization });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
// John Outside Org's meeting can't be accessed on userInsideOrganization's namespaces
await expectPageToBeNotFound({
page,
url: `/${userInsideOrganization.username}/john-outside-org-meeting`,
});
await bookUserEvent({ page, user: userInsideOrganization, event: eventForUserInsideOrganization });
}
);
});
});
test.describe("Inviting an existing user and then", () => {
test("create a booking on new link", async ({ page, browser, users, orgs, emails }) => {
const org = await orgs.create({
name: "TestOrg",
});
const owner = await users.create({
username: "owner",
name: "owner",
organizationId: org.id,
roleInOrganization: MembershipRole.OWNER,
});
const userOutsideOrganization = await users.create({
username: "john",
name: "John Outside Organization",
});
await owner.apiLogin();
const { invitedUserEmail } = await inviteExistingUserToOrganization({
page,
organizationId: org.id,
user: userOutsideOrganization,
usersFixture: users,
});
const inviteLink = await expectExistingUserToBeInvitedToOrganization(page, emails, invitedUserEmail);
if (!inviteLink) {
throw new Error("Invite link not found");
}
const usernameInOrg = getOrgUsernameFromEmail(
invitedUserEmail,
org.metadata?.orgAutoAcceptEmail ?? null
);
const usernameOutsideOrg = userOutsideOrganization.username;
// Before invite is accepted the booking page isn't available
await expectPageToBeNotFound({ page, url: `/${usernameInOrg}` });
await userOutsideOrganization.apiLogin();
await acceptTeamOrOrgInvite(page);
await test.step("Book through new link", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({
page,
user: {
username: usernameInOrg,
name: userOutsideOrganization.name,
},
event: await userOutsideOrganization.getFirstEventAsOwner(),
});
}
);
});
await test.step("Booking through old link redirects to new link on org domain", async () => {
const event = await userOutsideOrganization.getFirstEventAsOwner();
await expectRedirectToOrgDomain({
page,
org,
eventSlug: `/${usernameOutsideOrg}/${event.slug}`,
expectedEventSlug: `/${usernameInOrg}/${event.slug}`,
});
// As the redirection correctly happens, the booking would work too which we have verified in previous step. But we can't test that with org domain as that domain doesn't exist.
});
});
});
});
async function bookUserEvent({
page,
user,
event,
}: {
page: Page;
user: {
username: string | null;
name: string | null;
};
event: { slug: string; title: string };
}) {
await page.goto(`/${user.username}/${event.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
const BookingTitle = `${event.title} between ${user.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
async function bookTeamEvent({
page,
team,
event,
}: {
page: Page;
team: {
slug: string | null;
name: string | null;
};
event: { slug: string; title: string };
}) {
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
// So, we are using /team in the URL to access the team booking
// There are separate tests to verify that the next.config.js rewrites are working
// Also there are additional checkly tests that verify absolute e2e flow. They are in __checks__/organization.spec.ts
await page.goto(`/team/${team.slug}/${event.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) {
await page.goto(`${url}`);
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
}
async function expectRedirectToOrgDomain({
page,
org,
eventSlug,
expectedEventSlug,
}: {
page: Page;
org: { slug: string | null };
eventSlug: string;
expectedEventSlug: string;
}) {
if (!org.slug) {
throw new Error("Org slug is not defined");
}
page.goto(eventSlug).catch((e) => {
console.log("Expected navigation error to happen");
});
const orgSlug = org.slug;
const orgRedirectUrl = await new Promise(async (resolve) => {
page.on("request", (request) => {
if (request.isNavigationRequest()) {
const requestedUrl = request.url();
console.log("Requested navigation to", requestedUrl);
// Resolve on redirection to org domain
if (requestedUrl.includes(orgSlug)) {
resolve(requestedUrl);
}
}
});
});
expect(orgRedirectUrl).toContain(`${getOrgFullOrigin(org.slug)}${expectedEventSlug}`);
}
@@ -0,0 +1,57 @@
import type { Page } from "@playwright/test";
import type { createUsersFixture } from "playwright/fixtures/users";
export const inviteUserToOrganization = async ({
page,
organizationId,
email,
usersFixture,
}: {
page: Page;
organizationId: number;
email: string;
usersFixture: ReturnType<typeof createUsersFixture>;
}) => {
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
const invitedUserEmail = usersFixture.trackEmail({
username: email.split("@")[0],
domain: email.split("@")[1],
});
await inviteAnEmail(page, invitedUserEmail);
return { invitedUserEmail };
};
export const inviteExistingUserToOrganization = async ({
page,
organizationId,
user,
usersFixture,
}: {
page: Page;
organizationId: number;
user: {
email: string;
};
usersFixture: ReturnType<typeof createUsersFixture>;
}) => {
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
await inviteAnEmail(page, user.email);
await page.waitForSelector('[data-testid="toast-success"]');
return { invitedUserEmail: user.email };
};
export async function acceptTeamOrOrgInvite(page: Page) {
await page.goto("/settings/teams");
await page.click('[data-testid^="accept-invitation"]');
await page.waitForLoadState("networkidle");
}
async function inviteAnEmail(page: Page, invitedUserEmail: string) {
await page.locator('button:text("Add")').click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.locator('button:text("Send invite")').click();
await page.waitForLoadState("networkidle");
}
@@ -377,7 +377,7 @@ async function signupFromInviteLink({
return { email };
}
async function signupFromEmailInviteLink({
export async function signupFromEmailInviteLink({
browser,
inviteLink,
expectedUsername,
@@ -385,8 +385,8 @@ async function signupFromEmailInviteLink({
}: {
browser: Browser;
inviteLink: string;
expectedUsername: string;
expectedEmail: string;
expectedUsername?: string;
expectedEmail?: string;
}) {
// Follow invite link in new window
const context = await browser.newContext();
@@ -396,10 +396,14 @@ async function signupFromEmailInviteLink({
await signupPage.locator(`[data-testid="signup-usernamefield"]`).waitFor({ state: "visible" });
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toBeDisabled();
// await for value. initial value is ""
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toHaveValue(expectedUsername);
if (expectedUsername) {
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toHaveValue(expectedUsername);
}
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toBeDisabled();
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toHaveValue(expectedEmail);
if (expectedEmail) {
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toHaveValue(expectedEmail);
}
await signupPage.waitForLoadState("networkidle");
// Check required fields
@@ -0,0 +1,80 @@
import { expect } from "@playwright/test";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { test } from "../lib/fixtures";
import { fillStripeTestCheckout } from "../lib/testUtils";
test.describe("Teams", () => {
test.afterEach(({ orgs, users }) => {
orgs.deleteAll();
users.deleteAll();
});
test("Can create teams via Wizard", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
organizationId: org.id,
roleInOrganization: MembershipRole.ADMIN,
});
const inviteeEmail = `${user.username}+invitee@example.com`;
await user.apiLogin();
await page.goto("/teams");
await test.step("Can create team", async () => {
// Click text=Create Team
await page.locator("text=Create a new Team").click();
await page.waitForURL((url) => url.pathname === "/settings/teams/new");
// Fill input[name="name"]
await page.locator('input[name="name"]').fill(`${user.username}'s Team`);
// Click text=Continue
await page.click("[type=submit]");
// TODO: Figure out a way to make this more reliable
// eslint-disable-next-line playwright/no-conditional-in-test
if (IS_TEAM_BILLING_ENABLED) await fillStripeTestCheckout(page);
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i);
await page.waitForSelector('[data-testid="pending-member-list"]');
expect(await page.getByTestId("pending-member-item").count()).toBe(1);
});
await test.step("Can add members", async () => {
await page.getByTestId("new-member-button").click();
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
await page.getByTestId("invite-new-member-button").click();
await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
// locator.count() does not await for the expected number of elements
// https://github.com/microsoft/playwright/issues/14278
// using toHaveCount() is more reliable
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
});
await test.step("Can remove members", async () => {
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);
// Cleanup here since this user is created without our fixtures.
await prisma.user.delete({ where: { email: inviteeEmail } });
});
await test.step("Can finish team creation", async () => {
await page.getByTestId("publish-button").click();
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
});
await test.step("Can disband team", async () => {
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
await page.getByTestId("disband-team-button").click();
await page.getByTestId("dialog-confirmation").click();
await page.waitForURL("/teams");
expect(await page.locator(`text=${user.username}'s Team`).count()).toEqual(0);
});
});
});
+13 -3
View File
@@ -10,7 +10,7 @@ export async function expectInvitationEmailToBeReceived(
page: Page,
emails: ReturnType<typeof createEmailsFixture>,
userEmail: string,
subject: string,
subject?: string | null,
returnLink?: string
) {
if (!emails) return null;
@@ -21,10 +21,20 @@ export async function expectInvitationEmailToBeReceived(
expect(receivedEmails?.total).toBe(1);
const [firstReceivedEmail] = (receivedEmails as Messages).items;
expect(firstReceivedEmail.subject).toBe(subject);
if (subject) {
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");
}
export async function expectExistingUserToBeInvitedToOrganization(
page: Page,
emails: ReturnType<typeof createEmailsFixture>,
userEmail: string,
subject?: string | null
) {
return expectInvitationEmailToBeReceived(page, emails, userEmail, subject, "settings/team");
}
+1 -210
View File
@@ -2,15 +2,13 @@ import { expect } from "@playwright/test";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { SchedulingType } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
import {
bookTimeSlot,
doOnOrgDomain,
fillStripeTestCheckout,
NotFoundPageTextAppDir,
selectFirstAvailableTimeSlotNextMonth,
testName,
todo,
@@ -282,210 +280,3 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
todo("Reschedule a Collective EventType booking");
todo("Reschedule a Round Robin EventType booking");
});
test.describe("Teams - Org", () => {
test.afterEach(({ orgs, users }) => {
orgs.deleteAll();
users.deleteAll();
});
test("Can create teams via Wizard", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
organizationId: org.id,
roleInOrganization: MembershipRole.ADMIN,
});
const inviteeEmail = `${user.username}+invitee@example.com`;
await user.apiLogin();
await page.goto("/teams");
await test.step("Can create team", async () => {
// Click text=Create Team
await page.locator("text=Create a new Team").click();
await page.waitForURL((url) => url.pathname === "/settings/teams/new");
// Fill input[name="name"]
await page.locator('input[name="name"]').fill(`${user.username}'s Team`);
// Click text=Continue
await page.click("[type=submit]");
// TODO: Figure out a way to make this more reliable
// eslint-disable-next-line playwright/no-conditional-in-test
if (IS_TEAM_BILLING_ENABLED) await fillStripeTestCheckout(page);
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i);
await page.waitForSelector('[data-testid="pending-member-list"]');
expect(await page.getByTestId("pending-member-item").count()).toBe(1);
});
await test.step("Can add members", async () => {
await page.getByTestId("new-member-button").click();
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
await page.getByTestId("invite-new-member-button").click();
await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
// locator.count() does not await for the expected number of elements
// https://github.com/microsoft/playwright/issues/14278
// using toHaveCount() is more reliable
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
});
await test.step("Can remove members", async () => {
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);
// Cleanup here since this user is created without our fixtures.
await prisma.user.delete({ where: { email: inviteeEmail } });
});
await test.step("Can finish team creation", async () => {
await page.getByTestId("publish-button").click();
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
});
await test.step("Can disband team", async () => {
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
await page.getByTestId("disband-team-button").click();
await page.getByTestId("dialog-confirmation").click();
await page.waitForURL("/teams");
expect(await page.locator(`text=${user.username}'s Team`).count()).toEqual(0);
});
});
test("Can create a booking for Collective EventType", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
const { team } = await owner.getFirstTeamMembership();
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
// All the teammates should be in the booking
for (const teammate of teamMatesObj.concat([{ name: owner.name || "" }])) {
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
}
}
);
// TODO: Assert whether the user received an email
});
test("Can create a booking for Round Robin EventType", async ({ page, users }) => {
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{ username: "pro-user", name: "pro-user" },
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.ROUND_ROBIN,
}
);
const { team } = await owner.getFirstTeamMembership();
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
// The person who booked the meeting should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
// The title of the booking
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
// Since all the users have the same leastRecentlyBooked value
// Anyone of the teammates could be the Host of the booking.
const chosenUser = await page.getByTestId("booking-host-name").textContent();
expect(chosenUser).not.toBeNull();
expect(teamMatesObj.concat([{ name: owner.name! }]).some(({ name }) => name === chosenUser)).toBe(true);
// TODO: Assert whether the user received an email
});
test("Can access booking page with event slug and team page in lowercase/uppercase/mixedcase", async ({
page,
orgs,
users,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
const { team } = await owner.getFirstTeamMembership();
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
const teamSlugUpperCase = team.slug?.toUpperCase();
const teamEventSlugUpperCase = teamEventSlug.toUpperCase();
// This is the most closest to the actual user flow as org1.cal.com maps to /org/orgSlug
await page.goto(`/org/${org.slug}/${teamSlugUpperCase}/${teamEventSlugUpperCase}`);
await page.waitForSelector("[data-testid=day]");
});
});
@@ -123,7 +123,6 @@ export const getPublicEvent = async (
usernameList,
orgSlug: org,
});
console.log("getPublicEvent - dynamic", usersInOrgContext);
const users = usersInOrgContext;
const defaultEvent = getDefaultEvent(eventSlug);