Introducing Playwright Fixtures - Users Factory (#2293)
* Fix not able to logout using the logout path * Add users fixture for e2e tests * typo Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
		
							parent
							
								
									2d2df2d4db
								
							
						
					
					
						commit
						0390ae9ee1
					
				
					 5 changed files with 135 additions and 3 deletions
				
			
		| 
						 | 
				
			
			@ -112,7 +112,8 @@ export default function Login({
 | 
			
		|||
                else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
 | 
			
		||||
              })
 | 
			
		||||
              .catch(() => setErrorMessage(errorMessages[ErrorCode.InternalServerError]));
 | 
			
		||||
          }}>
 | 
			
		||||
          }}
 | 
			
		||||
          data-testid="login-form">
 | 
			
		||||
          <input defaultValue={csrfToken || undefined} type="hidden" hidden {...form.register("csrfToken")} />
 | 
			
		||||
 | 
			
		||||
          <div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import { CheckIcon } from "@heroicons/react/outline";
 | 
			
		||||
import { GetServerSidePropsContext } from "next";
 | 
			
		||||
import { useSession, signOut } from "next-auth/react";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { useEffect } from "react";
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +17,8 @@ import { ssrInit } from "@server/lib/ssr";
 | 
			
		|||
type Props = inferSSRProps<typeof getServerSideProps>;
 | 
			
		||||
 | 
			
		||||
export default function Logout(props: Props) {
 | 
			
		||||
  const { data: session, status } = useSession();
 | 
			
		||||
  if (status === "authenticated") signOut({ redirect: false });
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (props.query?.survey === "true") {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										96
									
								
								apps/web/playwright/fixtures/users.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								apps/web/playwright/fixtures/users.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,96 @@
 | 
			
		|||
import type { Page } from "@playwright/test";
 | 
			
		||||
import { UserPlan } from "@prisma/client";
 | 
			
		||||
import type Prisma from "@prisma/client";
 | 
			
		||||
 | 
			
		||||
import { hashPassword } from "@calcom/lib/auth";
 | 
			
		||||
import { prisma } from "@calcom/prisma";
 | 
			
		||||
 | 
			
		||||
export interface UsersFixture {
 | 
			
		||||
  create: (opts?: CustomUserOpts) => Promise<UserFixture>;
 | 
			
		||||
  get: () => UserFixture[];
 | 
			
		||||
  logout: () => Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface UserFixture {
 | 
			
		||||
  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
 | 
			
		||||
export const createUsersFixture = (page: Page): UsersFixture => {
 | 
			
		||||
  let store = { users: [], page } as { users: UserFixture[]; page: typeof page };
 | 
			
		||||
  return {
 | 
			
		||||
    create: async (opts) => {
 | 
			
		||||
      const user = await prisma.user.create({
 | 
			
		||||
        data: await createUser(opts),
 | 
			
		||||
      });
 | 
			
		||||
      const userFixture = createUserFixture(user, store.page!);
 | 
			
		||||
      store.users.push(userFixture);
 | 
			
		||||
      return userFixture;
 | 
			
		||||
    },
 | 
			
		||||
    get: () => store.users,
 | 
			
		||||
    logout: async () => {
 | 
			
		||||
      await page.goto("/auth/logout");
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// creates the single user fixture
 | 
			
		||||
const createUserFixture = (user: Prisma.User, page: Page): UserFixture => {
 | 
			
		||||
  const store = { user, page };
 | 
			
		||||
 | 
			
		||||
  // self is a reflective method that return the Prisma object that references this fixture.
 | 
			
		||||
  const self = async () => (await prisma.user.findUnique({ where: { id: store.user.id } }))!;
 | 
			
		||||
  return {
 | 
			
		||||
    id: user.id,
 | 
			
		||||
    self,
 | 
			
		||||
    login: async () => login(await self(), store.page),
 | 
			
		||||
    // ths is for developemnt only aimed to inject debugging messages in the metadata field of the user
 | 
			
		||||
    debug: async (message) => {
 | 
			
		||||
      await prisma.user.update({ where: { id: store.user.id }, data: { metadata: { debug: message } } });
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type CustomUserOptsKeys = "username" | "plan" | "completedOnboarding" | "locale";
 | 
			
		||||
type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & { timeZone?: TimeZoneE };
 | 
			
		||||
 | 
			
		||||
// creates the actual user in the db.
 | 
			
		||||
const createUser = async (opts?: CustomUserOpts) => {
 | 
			
		||||
  // build a unique name for our user
 | 
			
		||||
  const uname =
 | 
			
		||||
    (opts?.username ?? opts?.plan?.toLocaleLowerCase() ?? UserPlan.PRO.toLowerCase()) + "-" + Date.now();
 | 
			
		||||
  return {
 | 
			
		||||
    username: uname,
 | 
			
		||||
    name: (opts?.username ?? opts?.plan ?? UserPlan.PRO).toUpperCase(),
 | 
			
		||||
    plan: opts?.plan ?? UserPlan.PRO,
 | 
			
		||||
    email: `${uname}@example.com`,
 | 
			
		||||
    password: await hashPassword(uname),
 | 
			
		||||
    emailVerified: new Date(),
 | 
			
		||||
    completedOnboarding: opts?.completedOnboarding ?? true,
 | 
			
		||||
    timeZone: opts?.timeZone ?? TimeZoneE.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");
 | 
			
		||||
 | 
			
		||||
  // 2 seconds of delay before returning to help the session loading well
 | 
			
		||||
  await page.waitForTimeout(2000);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								apps/web/playwright/lib/fixtures.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/web/playwright/lib/fixtures.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
import { test as base } from "@playwright/test";
 | 
			
		||||
 | 
			
		||||
import { createUsersFixture } from "../fixtures/users";
 | 
			
		||||
import type { UsersFixture } from "../fixtures/users";
 | 
			
		||||
 | 
			
		||||
interface Fixtures {
 | 
			
		||||
  users: UsersFixture;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const test = base.extend<Fixtures>({
 | 
			
		||||
  users: async ({ page }, use, testInfo) => {
 | 
			
		||||
    // instantiate the fixture
 | 
			
		||||
    const usersFixture = createUsersFixture(page);
 | 
			
		||||
    // use the fixture within the test
 | 
			
		||||
    await use(usersFixture);
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +1,28 @@
 | 
			
		|||
import { test } from "@playwright/test";
 | 
			
		||||
import { expect } from "@playwright/test";
 | 
			
		||||
 | 
			
		||||
import { test } from "./lib/fixtures";
 | 
			
		||||
 | 
			
		||||
test.describe("Login tests", () => {
 | 
			
		||||
  // Using logged in state from globalSteup
 | 
			
		||||
  test.use({ storageState: "playwright/artifacts/proStorageState.json" });
 | 
			
		||||
 | 
			
		||||
  test("login with pro@example.com", async ({ page }) => {
 | 
			
		||||
  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]");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("Should logout using the logout path", async ({ page, users }) => {
 | 
			
		||||
    // creates a user and login
 | 
			
		||||
    const pro = await users.create();
 | 
			
		||||
    await pro.login();
 | 
			
		||||
 | 
			
		||||
    // users.logout() action uses the logout route "/auth/logout" to clear the session
 | 
			
		||||
    await users.logout();
 | 
			
		||||
 | 
			
		||||
    // check if we are at the login page
 | 
			
		||||
    await page.goto("/");
 | 
			
		||||
    await expect(page.locator(`[data-testid=login-form]`)).toBeVisible();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue