Refactor login tests (#2337)

Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Demian Caldelas 2022-04-06 12:13:09 -03:00 committed by GitHub
parent 7fd65ceb8a
commit 551892fa30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 128 additions and 61 deletions

View file

@ -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">
&copy; {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"} &copy; {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>

View file

@ -0,0 +1,4 @@
export enum TimeZoneEnum {
USA = "America/Phoenix",
UK = "Europe/London",
}

View file

@ -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);
} }

View file

@ -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);
}, },
}); });

View file

@ -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";
};
}

View file

@ -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();

View file

@ -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();
});
}); });