Refactor login tests (#2337)
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
parent
7fd65ceb8a
commit
551892fa30
7 changed files with 128 additions and 61 deletions
|
@ -288,7 +288,9 @@ export default function Shell(props: {
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<TrialBanner />
|
<TrialBanner />
|
||||||
<div className="rounded-sm pb-2 pl-3 pt-2 pr-2 hover:bg-gray-100 lg:mx-2 lg:pl-2">
|
<div
|
||||||
|
className="rounded-sm pb-2 pl-3 pt-2 pr-2 hover:bg-gray-100 lg:mx-2 lg:pl-2"
|
||||||
|
data-testid="user-dropdown-trigger">
|
||||||
<span className="hidden lg:inline">
|
<span className="hidden lg:inline">
|
||||||
<UserDropdown />
|
<UserDropdown />
|
||||||
</span>
|
</span>
|
||||||
|
@ -299,7 +301,9 @@ export default function Shell(props: {
|
||||||
<small style={{ fontSize: "0.5rem" }} className="mx-3 mt-1 mb-2 hidden opacity-50 lg:block">
|
<small style={{ fontSize: "0.5rem" }} className="mx-3 mt-1 mb-2 hidden opacity-50 lg:block">
|
||||||
© {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"}
|
© {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"}
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? "h" : "sh"}
|
{process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? "h" : "sh"}
|
||||||
<span className="lowercase">-{user && user.plan}</span>
|
<span className="lowercase" data-testid={`plan-${user?.plan.toLowerCase()}`}>
|
||||||
|
-{user && user.plan}
|
||||||
|
</span>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
4
apps/web/playwright/fixtures/types.ts
Normal file
4
apps/web/playwright/fixtures/types.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export enum TimeZoneEnum {
|
||||||
|
USA = "America/Phoenix",
|
||||||
|
UK = "Europe/London",
|
||||||
|
}
|
|
@ -5,30 +5,15 @@ import type Prisma from "@prisma/client";
|
||||||
import { hashPassword } from "@calcom/lib/auth";
|
import { hashPassword } from "@calcom/lib/auth";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
export interface UsersFixture {
|
import { TimeZoneEnum } from "./types";
|
||||||
create: (opts?: CustomUserOpts) => Promise<UserFixture>;
|
|
||||||
get: () => UserFixture[];
|
|
||||||
logout: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserFixture {
|
type UserFixture = ReturnType<typeof createUserFixture>;
|
||||||
id: number;
|
|
||||||
self: () => Promise<Prisma.User>;
|
|
||||||
login: () => Promise<void>;
|
|
||||||
debug: (message: Record<string, any>) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// An alias for the hard to remember timezones strings
|
|
||||||
export enum TimeZoneE {
|
|
||||||
USA = "America/Phoenix",
|
|
||||||
UK = "Europe/London",
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates a user fixture instance and stores the collection
|
// 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 };
|
let store = { users: [], page } as { users: UserFixture[]; page: typeof page };
|
||||||
return {
|
return {
|
||||||
create: async (opts) => {
|
create: async (opts?: CustomUserOpts) => {
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: await createUser(opts),
|
data: await createUser(opts),
|
||||||
});
|
});
|
||||||
|
@ -43,8 +28,10 @@ export const createUsersFixture = (page: Page): UsersFixture => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type JSONValue = string | number | boolean | { [x: string]: JSONValue } | Array<JSONValue>;
|
||||||
|
|
||||||
// creates the single user fixture
|
// creates the single user fixture
|
||||||
const createUserFixture = (user: Prisma.User, page: Page): UserFixture => {
|
const createUserFixture = (user: Prisma.User, page: Page) => {
|
||||||
const store = { user, page };
|
const store = { user, page };
|
||||||
|
|
||||||
// self is a reflective method that return the Prisma object that references this fixture.
|
// 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 {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
self,
|
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
|
// 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<string, JSONValue>) => {
|
||||||
await prisma.user.update({ where: { id: store.user.id }, data: { metadata: { debug: message } } });
|
await prisma.user.update({ where: { id: store.user.id }, data: { metadata: { debug: message } } });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type CustomUserOptsKeys = "username" | "plan" | "completedOnboarding" | "locale";
|
type CustomUserOptsKeys = "username" | "password" | "plan" | "completedOnboarding" | "locale";
|
||||||
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & { timeZone?: TimeZoneE };
|
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & { timeZone?: TimeZoneEnum };
|
||||||
|
|
||||||
// creates the actual user in the db.
|
// creates the actual user in the db.
|
||||||
const createUser = async (opts?: CustomUserOpts) => {
|
const createUser = async (opts?: CustomUserOpts) => {
|
||||||
|
@ -76,21 +63,28 @@ const createUser = async (opts?: CustomUserOpts) => {
|
||||||
password: await hashPassword(uname),
|
password: await hashPassword(uname),
|
||||||
emailVerified: new Date(),
|
emailVerified: new Date(),
|
||||||
completedOnboarding: opts?.completedOnboarding ?? true,
|
completedOnboarding: opts?.completedOnboarding ?? true,
|
||||||
timeZone: opts?.timeZone ?? TimeZoneE.UK,
|
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
||||||
locale: opts?.locale ?? "en",
|
locale: opts?.locale ?? "en",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// login using a replay of an E2E routine.
|
// login using a replay of an E2E routine.
|
||||||
async function login(user: Prisma.User, page: Page) {
|
export async function login(
|
||||||
await page.goto("/auth/logout");
|
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||||
await page.goto("/");
|
page: Page
|
||||||
await page.click('input[name="email"]');
|
) {
|
||||||
await page.fill('input[name="email"]', user.email);
|
// get locators
|
||||||
await page.press('input[name="email"]', "Tab");
|
const loginLocator = await page.locator("[data-testid=login-form]");
|
||||||
await page.fill('input[name="password"]', user.username!);
|
const emailLocator = await loginLocator.locator("#email");
|
||||||
await page.press('input[name="password"]', "Enter");
|
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);
|
await page.waitForTimeout(2000);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import { test as base } from "@playwright/test";
|
import { test as base } from "@playwright/test";
|
||||||
|
|
||||||
import { createUsersFixture } from "../fixtures/users";
|
import { createUsersFixture } from "../fixtures/users";
|
||||||
import type { UsersFixture } from "../fixtures/users";
|
|
||||||
|
|
||||||
interface Fixtures {
|
interface Fixtures {
|
||||||
users: UsersFixture;
|
users: ReturnType<typeof createUsersFixture>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const test = base.extend<Fixtures>({
|
export const test = base.extend<Fixtures>({
|
||||||
users: async ({ page }, use, testInfo) => {
|
users: async ({ page }, use) => {
|
||||||
// instantiate the fixture
|
|
||||||
const usersFixture = createUsersFixture(page);
|
const usersFixture = createUsersFixture(page);
|
||||||
// use the fixture within the test
|
|
||||||
await use(usersFixture);
|
await use(usersFixture);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -86,3 +86,13 @@ export async function selectSecondAvailableTimeSlotNextMonth(page: Page) {
|
||||||
await page.click('[data-testid="day"][data-disabled="false"]');
|
await page.click('[data-testid="day"][data-disabled="false"]');
|
||||||
await page.locator('[data-testid="time"]').nth(1).click();
|
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";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,19 +1,85 @@
|
||||||
import { expect } from "@playwright/test";
|
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 { test } from "./lib/fixtures";
|
||||||
|
import { localize } from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe("Login tests", () => {
|
test.describe("Login and logout tests", () => {
|
||||||
// Using logged in state from globalSteup
|
// Test login with all plans
|
||||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
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 }) => {
|
const shellLocator = await page.locator(`[data-testid=dashboard-shell]`);
|
||||||
// Try to go homepage
|
|
||||||
|
// expects the home page for an authorized user
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
// It should redirect you to the event-types page
|
await expect(shellLocator).toBeVisible();
|
||||||
await page.waitForSelector("[data-testid=event-types]");
|
|
||||||
|
// 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
|
// creates a user and login
|
||||||
const pro = await users.create();
|
const pro = await users.create();
|
||||||
await pro.login();
|
await pro.login();
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { todo } from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe("Trial account tests", () => {
|
test.describe("Trial account tests", () => {
|
||||||
// Using logged in state from globalSteup
|
todo("Add tests with a TRIAL account");
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue