From 85465dbf83cfc70486043666c08270636b6dafb7 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Wed, 13 Sep 2023 16:51:12 -0600 Subject: [PATCH] use playwright fixtures instead of hooks --- .eslintrc.cjs | 2 + tests/db-utils.ts | 27 ------ tests/e2e/2fa.test.ts | 13 +-- tests/e2e/error-boundary.test.ts | 2 +- tests/e2e/notes.test.ts | 15 ++-- tests/e2e/onboarding.test.ts | 49 ++++++----- tests/e2e/search.test.ts | 7 +- tests/e2e/settings-profile.test.ts | 27 +++--- tests/playwright-utils.ts | 132 ++++++++++++++++++++--------- 9 files changed, 147 insertions(+), 127 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c963d6310..5dae40cdb 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -10,6 +10,8 @@ module.exports = { 'prettier', ], rules: { + // playwright requires destructuring in fixtures even if you don't use anything 🤷‍♂️ + 'no-empty-pattern': 'off', '@typescript-eslint/consistent-type-imports': [ 'warn', { diff --git a/tests/db-utils.ts b/tests/db-utils.ts index 5e864773e..8f5062e16 100644 --- a/tests/db-utils.ts +++ b/tests/db-utils.ts @@ -2,8 +2,6 @@ import fs from 'node:fs' import { faker } from '@faker-js/faker' import bcrypt from 'bcryptjs' import { UniqueEnforcer } from 'enforce-unique' -import { getPasswordHash } from '#app/utils/auth.server.ts' -import { prisma } from '#app/utils/db.server.ts' const uniqueUsernameEnforcer = new UniqueEnforcer() @@ -38,31 +36,6 @@ export function createPassword(password: string = faker.internet.password()) { } } -export const insertedUsers = new Set() - -export async function insertNewUser({ - username, - password, - email, -}: { username?: string; password?: string; email?: string } = {}) { - const userData = createUser() - username ??= userData.username - password ??= userData.username - email ??= userData.email - const user = await prisma.user.create({ - select: { id: true, name: true, username: true, email: true }, - data: { - ...userData, - email, - username, - roles: { connect: { name: 'user' } }, - password: { create: { hash: await getPasswordHash(password) } }, - }, - }) - insertedUsers.add(user.id) - return user as typeof user & { name: string } -} - let noteImages: Array>> | undefined export async function getNoteImages() { if (noteImages) return noteImages diff --git a/tests/e2e/2fa.test.ts b/tests/e2e/2fa.test.ts index 88d72d391..e70a5a400 100644 --- a/tests/e2e/2fa.test.ts +++ b/tests/e2e/2fa.test.ts @@ -1,14 +1,13 @@ import { generateTOTP } from '@epic-web/totp' import { faker } from '@faker-js/faker' -import { expect, test } from '@playwright/test' -import { insertNewUser, loginPage } from '#tests/playwright-utils.ts' +import { expect, test } from '#tests/playwright-utils.ts' test('Users can add 2FA to their account and use it when logging in', async ({ page, + login, }) => { const password = faker.internet.password() - const user = await insertNewUser({ password }) - await loginPage({ page, user }) + const user = await login({ password }) await page.goto('/settings/profile') await page.getByRole('link', { name: /enable 2fa/i }).click() @@ -31,7 +30,7 @@ test('Users can add 2FA to their account and use it when logging in', async ({ await expect(main).toHaveText(/You have enabled two-factor authentication./i) await expect(main.getByRole('link', { name: /disable 2fa/i })).toBeVisible() - await page.getByRole('link', { name: user.name }).click() + await page.getByRole('link', { name: user.name ?? user.username }).click() await page.getByRole('button', { name: /logout/i }).click() await expect(page).toHaveURL(`/`) @@ -47,5 +46,7 @@ test('Users can add 2FA to their account and use it when logging in', async ({ await page.getByRole('button', { name: /submit/i }).click() - await expect(page.getByRole('link', { name: user.name })).toBeVisible() + await expect( + page.getByRole('link', { name: user.name ?? user.username }), + ).toBeVisible() }) diff --git a/tests/e2e/error-boundary.test.ts b/tests/e2e/error-boundary.test.ts index c3f841f2b..ee67bc15c 100644 --- a/tests/e2e/error-boundary.test.ts +++ b/tests/e2e/error-boundary.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test' +import { expect, test } from '#tests/playwright-utils.ts' test('Test root error boundary caught', async ({ page }) => { await page.goto('/does-not-exist') diff --git a/tests/e2e/notes.test.ts b/tests/e2e/notes.test.ts index 46cf52dd2..449a224fb 100644 --- a/tests/e2e/notes.test.ts +++ b/tests/e2e/notes.test.ts @@ -1,10 +1,9 @@ import { faker } from '@faker-js/faker' -import { expect, test } from '@playwright/test' import { prisma } from '#app/utils/db.server.ts' -import { loginPage } from '#tests/playwright-utils.ts' +import { expect, test } from '#tests/playwright-utils.ts' -test('Users can create notes', async ({ page }) => { - const user = await loginPage({ page }) +test('Users can create notes', async ({ page, login }) => { + const user = await login() await page.goto(`/users/${user.username}/notes`) const newNote = createNote() @@ -18,8 +17,8 @@ test('Users can create notes', async ({ page }) => { await expect(page).toHaveURL(new RegExp(`/users/${user.username}/notes/.*`)) }) -test('Users can edit notes', async ({ page }) => { - const user = await loginPage({ page }) +test('Users can edit notes', async ({ page, login }) => { + const user = await login() const note = await prisma.note.create({ select: { id: true }, @@ -42,8 +41,8 @@ test('Users can edit notes', async ({ page }) => { ).toBeVisible() }) -test('Users can delete notes', async ({ page }) => { - const user = await loginPage({ page }) +test('Users can delete notes', async ({ page, login }) => { + const user = await login() const note = await prisma.note.create({ select: { id: true }, diff --git a/tests/e2e/onboarding.test.ts b/tests/e2e/onboarding.test.ts index d632d8f84..a9cda5c4d 100644 --- a/tests/e2e/onboarding.test.ts +++ b/tests/e2e/onboarding.test.ts @@ -1,9 +1,8 @@ import { faker } from '@faker-js/faker' -import { expect, test } from '@playwright/test' import { prisma } from '#app/utils/db.server.ts' import { invariant } from '#app/utils/misc.tsx' import { readEmail } from '#tests/mocks/utils.ts' -import { createUser, insertNewUser } from '#tests/playwright-utils.ts' +import { createUser, expect, test as base } from '#tests/playwright-utils.ts' const urlRegex = /(?https?:\/\/[^\s$.?#].[^\s]*)/ function extractUrl(text: string) { @@ -11,25 +10,30 @@ function extractUrl(text: string) { return match?.groups?.url } -function getOnboardingData() { - const userData = createUser() - const onboardingData = { - ...userData, - password: faker.internet.password(), +const test = base.extend<{ + getOnboardingData(): { + username: string + name: string + email: string + password: string } - return onboardingData -} - -const usernamesToDelete = new Set() - -test.afterEach(async () => { - for (const username of usernamesToDelete) { - await prisma.user.delete({ where: { username } }) - } - usernamesToDelete.clear() +}>({ + getOnboardingData: async ({}, use) => { + const userData = createUser() + await use(() => { + const onboardingData = { + ...userData, + password: faker.internet.password(), + } + return onboardingData + }) + await prisma.user + .delete({ where: { username: userData.username } }) + .catch(() => {}) + }, }) -test('onboarding with link', async ({ page }) => { +test('onboarding with link', async ({ page, getOnboardingData }) => { const onboardingData = getOnboardingData() await page.goto('/') @@ -78,7 +82,6 @@ test('onboarding with link', async ({ page }) => { await page.getByLabel(/remember me/i).check() - usernamesToDelete.add(onboardingData.username) await page.getByRole('button', { name: /Create an account/i }).click() await expect(page).toHaveURL(`/`) @@ -93,7 +96,7 @@ test('onboarding with link', async ({ page }) => { await expect(page).toHaveURL(`/`) }) -test('onboarding with a short code', async ({ page }) => { +test('onboarding with a short code', async ({ page, getOnboardingData }) => { const onboardingData = getOnboardingData() await page.goto('/signup') @@ -124,7 +127,7 @@ test('onboarding with a short code', async ({ page }) => { await expect(page).toHaveURL(`/onboarding`) }) -test('login as existing user', async ({ page }) => { +test('login as existing user', async ({ page, insertNewUser }) => { const password = faker.internet.password() const user = await insertNewUser({ password }) invariant(user.name, 'User name not found') @@ -137,7 +140,7 @@ test('login as existing user', async ({ page }) => { await expect(page.getByRole('link', { name: user.name })).toBeVisible() }) -test('reset password with a link', async ({ page }) => { +test('reset password with a link', async ({ page, insertNewUser }) => { const originalPassword = faker.internet.password() const user = await insertNewUser({ password: originalPassword }) invariant(user.name, 'User name not found') @@ -190,7 +193,7 @@ test('reset password with a link', async ({ page }) => { await expect(page.getByRole('link', { name: user.name })).toBeVisible() }) -test('reset password with a short code', async ({ page }) => { +test('reset password with a short code', async ({ page, insertNewUser }) => { const user = await insertNewUser() await page.goto('/login') diff --git a/tests/e2e/search.test.ts b/tests/e2e/search.test.ts index 72201f030..37ce1119c 100644 --- a/tests/e2e/search.test.ts +++ b/tests/e2e/search.test.ts @@ -1,7 +1,7 @@ -import { expect, test } from '@playwright/test' -import { insertNewUser } from '#tests/playwright-utils.ts' +import { invariant } from '#app/utils/misc.tsx' +import { expect, test } from '#tests/playwright-utils.ts' -test('Search from home page', async ({ page }) => { +test('Search from home page', async ({ page, insertNewUser }) => { const newUser = await insertNewUser() await page.goto('/') @@ -14,6 +14,7 @@ test('Search from home page', async ({ page }) => { await expect(page.getByText('Epic Notes Users')).toBeVisible() const userList = page.getByRole('main').getByRole('list') await expect(userList.getByRole('listitem')).toHaveCount(1) + invariant(newUser.name, 'User name not found') await expect(page.getByAltText(newUser.name)).toBeVisible() await page.getByRole('searchbox', { name: /search/i }).fill('__nonexistent__') diff --git a/tests/e2e/settings-profile.test.ts b/tests/e2e/settings-profile.test.ts index 91e1c23c2..29b55d889 100644 --- a/tests/e2e/settings-profile.test.ts +++ b/tests/e2e/settings-profile.test.ts @@ -1,18 +1,12 @@ import { faker } from '@faker-js/faker' -import { expect, test } from '@playwright/test' import { verifyUserPassword } from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' import { invariant } from '#app/utils/misc.tsx' import { readEmail } from '#tests/mocks/utils.ts' -import { - createUser, - insertNewUser, - loginPage, - waitFor, -} from '#tests/playwright-utils.ts' - -test('Users can update their basic info', async ({ page }) => { - await loginPage({ page }) +import { expect, test , createUser, waitFor } from '#tests/playwright-utils.ts' + +test('Users can update their basic info', async ({ page, login }) => { + await login() await page.goto('/settings/profile') const newUserData = createUser() @@ -25,11 +19,10 @@ test('Users can update their basic info', async ({ page }) => { await page.getByRole('button', { name: /^save/i }).click() }) -test('Users can update their password', async ({ page }) => { +test('Users can update their password', async ({ page, login }) => { const oldPassword = faker.internet.password() const newPassword = faker.internet.password() - const user = await insertNewUser({ password: oldPassword }) - await loginPage({ page, user }) + const user = await login({ password: oldPassword }) await page.goto('/settings/profile') await page.getByRole('link', { name: /change password/i }).click() @@ -57,8 +50,8 @@ test('Users can update their password', async ({ page }) => { ).toEqual({ id: user.id }) }) -test('Users can update their profile photo', async ({ page }) => { - const user = await loginPage({ page }) +test('Users can update their profile photo', async ({ page, login }) => { + const user = await login() await page.goto('/settings/profile') const beforeSrc = await page @@ -87,8 +80,8 @@ test('Users can update their profile photo', async ({ page }) => { expect(beforeSrc).not.toEqual(afterSrc) }) -test('Users can change their email address', async ({ page }) => { - const preUpdateUser = await loginPage({ page }) +test('Users can change their email address', async ({ page, login }) => { + const preUpdateUser = await login() const newEmailAddress = faker.internet.email().toLowerCase() expect(preUpdateUser.email).not.toEqual(newEmailAddress) await page.goto('/settings/profile') diff --git a/tests/playwright-utils.ts b/tests/playwright-utils.ts index cb17274ea..b9157e8da 100644 --- a/tests/playwright-utils.ts +++ b/tests/playwright-utils.ts @@ -1,47 +1,102 @@ -import { test, type Page } from '@playwright/test' +import { test as base } from '@playwright/test' +import { type User as UserModel } from '@prisma/client' import * as setCookieParser from 'set-cookie-parser' -import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts' +import { + getPasswordHash, + getSessionExpirationDate, + sessionKey, +} from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' import { sessionStorage } from '#app/utils/session.server.ts' -import { insertNewUser, insertedUsers } from './db-utils.ts' +import { createUser } from './db-utils.ts' export * from './db-utils.ts' -export async function loginPage({ - page, - user: givenUser, -}: { - page: Page - user?: { id: string } -}) { - const user = givenUser - ? await prisma.user.findUniqueOrThrow({ - where: { id: givenUser.id }, - select: { - id: true, - email: true, - username: true, - name: true, - }, - }) - : await insertNewUser() - const session = await prisma.session.create({ - data: { - expirationDate: getSessionExpirationDate(), - userId: user.id, - }, - select: { id: true }, - }) +type GetOrInsertUserOptions = { + id?: string + username?: UserModel['username'] + password?: string + email?: UserModel['email'] +} + +type User = { + id: string + email: string + username: string + name: string | null +} - const cookieSession = await sessionStorage.getSession() - cookieSession.set(sessionKey, session.id) - const cookieConfig = setCookieParser.parseString( - await sessionStorage.commitSession(cookieSession), - ) as any - await page.context().addCookies([{ ...cookieConfig, domain: 'localhost' }]) - return user as typeof user & { name: string } +async function getOrInsertUser({ + id, + username, + password, + email, +}: GetOrInsertUserOptions = {}): Promise { + const select = { id: true, email: true, username: true, name: true } + if (id) { + return await prisma.user.findUniqueOrThrow({ + select, + where: { id: id }, + }) + } else { + const userData = createUser() + username ??= userData.username + password ??= userData.username + email ??= userData.email + return await prisma.user.create({ + select, + data: { + ...userData, + email, + username, + roles: { connect: { name: 'user' } }, + password: { create: { hash: await getPasswordHash(password) } }, + }, + }) + } } +export const test = base.extend<{ + insertNewUser(options?: GetOrInsertUserOptions): Promise + login(options?: GetOrInsertUserOptions): Promise +}>({ + insertNewUser: async ({}, use) => { + let userId: string | undefined = undefined + await use(async options => { + const user = await getOrInsertUser(options) + userId = user.id + return user + }) + await prisma.user.delete({ where: { id: userId } }).catch(() => {}) + }, + login: async ({ page }, use) => { + let userId: string | undefined = undefined + await use(async options => { + const user = await getOrInsertUser(options) + userId = user.id + const session = await prisma.session.create({ + data: { + expirationDate: getSessionExpirationDate(), + userId: user.id, + }, + select: { id: true }, + }) + + const cookieSession = await sessionStorage.getSession() + cookieSession.set(sessionKey, session.id) + const cookieConfig = setCookieParser.parseString( + await sessionStorage.commitSession(cookieSession), + ) as any + await page + .context() + .addCookies([{ ...cookieConfig, domain: 'localhost' }]) + return user + }) + await prisma.user.delete({ where: { id: userId } }).catch(() => {}) + }, +}) +export const { expect } = test + /** * This allows you to wait for something (like an email to be available). * @@ -69,10 +124,3 @@ export async function waitFor( } throw lastError } - -test.afterEach(async () => { - await prisma.user.deleteMany({ - where: { id: { in: Array.from(insertedUsers) } }, - }) - insertedUsers.clear() -})