diff --git a/apps/web/components/Shell.tsx b/apps/web/components/Shell.tsx index 26240406..85e702e6 100644 --- a/apps/web/components/Shell.tsx +++ b/apps/web/components/Shell.tsx @@ -288,7 +288,9 @@ export default function Shell(props: { -
+
@@ -299,7 +301,9 @@ export default function Shell(props: { © {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"} {process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? "h" : "sh"} - -{user && user.plan} + + -{user && user.plan} +
diff --git a/apps/web/playwright/fixtures/types.ts b/apps/web/playwright/fixtures/types.ts new file mode 100644 index 00000000..5631999d --- /dev/null +++ b/apps/web/playwright/fixtures/types.ts @@ -0,0 +1,4 @@ +export enum TimeZoneEnum { + USA = "America/Phoenix", + UK = "Europe/London", +} diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index dd8e83aa..796d1016 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -5,30 +5,15 @@ import type Prisma from "@prisma/client"; import { hashPassword } from "@calcom/lib/auth"; import { prisma } from "@calcom/prisma"; -export interface UsersFixture { - create: (opts?: CustomUserOpts) => Promise; - get: () => UserFixture[]; - logout: () => Promise; -} +import { TimeZoneEnum } from "./types"; -interface UserFixture { - id: number; - self: () => Promise; - login: () => Promise; - debug: (message: Record) => Promise; -} - -// An alias for the hard to remember timezones strings -export enum TimeZoneE { - USA = "America/Phoenix", - UK = "Europe/London", -} +type UserFixture = ReturnType; // creates a user fixture instance and stores the collection -export const createUsersFixture = (page: Page): UsersFixture => { +export const createUsersFixture = (page: Page) => { let store = { users: [], page } as { users: UserFixture[]; page: typeof page }; return { - create: async (opts) => { + create: async (opts?: CustomUserOpts) => { const user = await prisma.user.create({ data: await createUser(opts), }); @@ -43,8 +28,10 @@ export const createUsersFixture = (page: Page): UsersFixture => { }; }; +type JSONValue = string | number | boolean | { [x: string]: JSONValue } | Array; + // creates the single user fixture -const createUserFixture = (user: Prisma.User, page: Page): UserFixture => { +const createUserFixture = (user: Prisma.User, page: Page) => { const store = { user, page }; // self is a reflective method that return the Prisma object that references this fixture. @@ -52,16 +39,16 @@ const createUserFixture = (user: Prisma.User, page: Page): UserFixture => { return { id: user.id, self, - login: async () => login(await self(), store.page), + login: async () => login({ ...(await self()), password: user.username }, store.page), // ths is for developemnt only aimed to inject debugging messages in the metadata field of the user - debug: async (message) => { + debug: async (message: string | Record) => { await prisma.user.update({ where: { id: store.user.id }, data: { metadata: { debug: message } } }); }, }; }; -type CustomUserOptsKeys = "username" | "plan" | "completedOnboarding" | "locale"; -type CustomUserOpts = Partial> & { timeZone?: TimeZoneE }; +type CustomUserOptsKeys = "username" | "password" | "plan" | "completedOnboarding" | "locale"; +type CustomUserOpts = Partial> & { timeZone?: TimeZoneEnum }; // creates the actual user in the db. const createUser = async (opts?: CustomUserOpts) => { @@ -76,21 +63,28 @@ const createUser = async (opts?: CustomUserOpts) => { password: await hashPassword(uname), emailVerified: new Date(), completedOnboarding: opts?.completedOnboarding ?? true, - timeZone: opts?.timeZone ?? TimeZoneE.UK, + timeZone: opts?.timeZone ?? TimeZoneEnum.UK, locale: opts?.locale ?? "en", }; }; // login using a replay of an E2E routine. -async function login(user: Prisma.User, page: Page) { - await page.goto("/auth/logout"); - await page.goto("/"); - await page.click('input[name="email"]'); - await page.fill('input[name="email"]', user.email); - await page.press('input[name="email"]', "Tab"); - await page.fill('input[name="password"]', user.username!); - await page.press('input[name="password"]', "Enter"); +export async function login( + user: Pick & Partial>, + page: Page +) { + // get locators + const loginLocator = await page.locator("[data-testid=login-form]"); + const emailLocator = await loginLocator.locator("#email"); + const passwordLocator = await loginLocator.locator("#password"); + const signInLocator = await loginLocator.locator('[type="submit"]'); - // 2 seconds of delay before returning to help the session loading well + //login + await page.goto("/"); + await emailLocator.fill(user.email ?? `${user.username}@example.com`); + await passwordLocator.fill(user.password ?? user.username!); + await signInLocator.click(); + + // 2 seconds of delay to give the session enough time for a clean load await page.waitForTimeout(2000); } diff --git a/apps/web/playwright/lib/fixtures.ts b/apps/web/playwright/lib/fixtures.ts index 5fb1a6cc..615347a9 100644 --- a/apps/web/playwright/lib/fixtures.ts +++ b/apps/web/playwright/lib/fixtures.ts @@ -1,17 +1,14 @@ import { test as base } from "@playwright/test"; import { createUsersFixture } from "../fixtures/users"; -import type { UsersFixture } from "../fixtures/users"; interface Fixtures { - users: UsersFixture; + users: ReturnType; } export const test = base.extend({ - users: async ({ page }, use, testInfo) => { - // instantiate the fixture + users: async ({ page }, use) => { const usersFixture = createUsersFixture(page); - // use the fixture within the test await use(usersFixture); }, }); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index 2e9f8e2f..d651264d 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -86,3 +86,13 @@ export async function selectSecondAvailableTimeSlotNextMonth(page: Page) { await page.click('[data-testid="day"][data-disabled="false"]'); await page.locator('[data-testid="time"]').nth(1).click(); } + +// Provide an standalone localize utility not managed by next-i18n +export async function localize(locale: string) { + const localeModule = `../../public/static/locales/${locale}/common.json`; + const localeMap = await import(localeModule); + return (message: string) => { + if (message in localeMap) return localeMap[message]; + throw "No locale found for the given entry message"; + }; +} diff --git a/apps/web/playwright/login.test.ts b/apps/web/playwright/login.test.ts index 9523932a..8674b9c0 100644 --- a/apps/web/playwright/login.test.ts +++ b/apps/web/playwright/login.test.ts @@ -1,19 +1,85 @@ import { expect } from "@playwright/test"; +import { UserPlan } from "@prisma/client"; +import { ErrorCode } from "@lib/auth"; + +import { login } from "./fixtures/users"; import { test } from "./lib/fixtures"; +import { localize } from "./lib/testUtils"; -test.describe("Login tests", () => { - // Using logged in state from globalSteup - test.use({ storageState: "playwright/artifacts/proStorageState.json" }); +test.describe("Login and logout tests", () => { + // Test login with all plans + const plans = [UserPlan.PRO, UserPlan.FREE, UserPlan.TRIAL]; + plans.forEach((plan) => { + test(`Should login with a ${plan} account`, async ({ page, users }) => { + // Create user and login + const pro = await users.create({ plan }); + await pro.login(); - test("Login with pro@example.com", async ({ page }) => { - // Try to go homepage - await page.goto("/"); - // It should redirect you to the event-types page - await page.waitForSelector("[data-testid=event-types]"); + const shellLocator = await page.locator(`[data-testid=dashboard-shell]`); + + // expects the home page for an authorized user + await page.goto("/"); + await expect(shellLocator).toBeVisible(); + + // Asserts to read the tested plan + const planLocator = shellLocator.locator(`[data-testid=plan-${plan.toLowerCase()}]`); + await expect(planLocator).toBeVisible(); + await await expect(planLocator).toHaveText; + + // When TRIAL check if the TRIAL banner is visible + if (plan === UserPlan.TRIAL) { + await expect(page.locator(`[data-testid=trial-banner]`)).toBeVisible(); + } + }); }); - test("Should logout using the logout path", async ({ page, users }) => { + test("Should warn when user does not exist", async ({ page, users }) => { + const alertMessage = (await localize("en"))("no_account_exists"); + + // Login with a non-existent user + const never = "never"; + await login({ username: never }, page); + + // assert for the visibility of the localized alert message + await expect(page.locator(`text=${alertMessage}`)).toBeVisible(); + }); + + test("Should warn when password is incorrect", async ({ page, users }) => { + const alertMessage = (await localize("en"))("incorrect_password"); + // by default password===username with the users fixture + const pro = await users.create({ username: "pro" }); + + // login with a wrong password + await login({ username: "pro", password: "wrong" }, page); + + // assert for the visibility of the localized alert message + await expect(page.locator(`text=${alertMessage}`)).toBeVisible(); + }); + + test("Should logout", async ({ page, users }) => { + const signOutLabel = (await localize("en"))("sign_out"); + const userDropdownDisclose = async () => + (await page.locator("[data-testid=user-dropdown-trigger]")).click(); + + // creates a user and login + const pro = await users.create(); + await pro.login(); + + // disclose and click the sign out button from the user dropdown + await userDropdownDisclose(); + const signOutBtn = await page.locator(`text=${signOutLabel}`); + await signOutBtn.click(); + + // 2s of delay to assure the session is cleared + await page.waitForTimeout(2000); + + // Reroute to the home page to check if the login form shows up + await page.goto("/"); + await expect(page.locator(`[data-testid=login-form]`)).toBeVisible(); + }); + + test("Should logout using the logout route", async ({ page, users }) => { // creates a user and login const pro = await users.create(); await pro.login(); diff --git a/apps/web/playwright/trial.test.ts b/apps/web/playwright/trial.test.ts index c5099391..ea54ae99 100644 --- a/apps/web/playwright/trial.test.ts +++ b/apps/web/playwright/trial.test.ts @@ -1,15 +1,7 @@ import { expect, test } from "@playwright/test"; +import { todo } from "./lib/testUtils"; + test.describe("Trial account tests", () => { - // Using logged in state from globalSteup - test.use({ storageState: "playwright/artifacts/trialStorageState.json" }); - - test("Trial banner should be visible to TRIAL users", async ({ page }) => { - // Try to go homepage - await page.goto("/"); - // It should redirect you to the event-types page - await page.waitForSelector("[data-testid=event-types]"); - - await expect(page.locator(`[data-testid=trial-banner]`)).toBeVisible(); - }); + todo("Add tests with a TRIAL account"); });