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
	
	 Demian Caldelas
						Demian Caldelas