diff --git a/tests/e2e/2fa.test.ts b/tests/e2e/2fa.test.ts index 318eab229..50473cfed 100644 --- a/tests/e2e/2fa.test.ts +++ b/tests/e2e/2fa.test.ts @@ -4,11 +4,12 @@ import { expect, test } from '#tests/playwright-utils.ts' test('Users can add 2FA to their account and use it when logging in', async ({ page, + navigate, login, }) => { const password = faker.internet.password() const user = await login({ password }) - await page.goto('/settings/profile') + await navigate('/settings/profile') await page.getByRole('link', { name: /enable 2fa/i }).click() @@ -40,7 +41,7 @@ test('Users can add 2FA to their account and use it when logging in', async ({ await page.getByRole('menuitem', { name: /logout/i }).click() await expect(page).toHaveURL(`/`) - await page.goto('/login') + await navigate('/login') await expect(page).toHaveURL(`/login`) await page.getByRole('textbox', { name: /username/i }).fill(user.username) await page.getByLabel(/^password$/i).fill(password) diff --git a/tests/e2e/error-boundary.test.ts b/tests/e2e/error-boundary.test.ts index 5254f0f23..7a8e79f27 100644 --- a/tests/e2e/error-boundary.test.ts +++ b/tests/e2e/error-boundary.test.ts @@ -1,8 +1,8 @@ import { expect, test } from '#tests/playwright-utils.ts' -test('Test root error boundary caught', async ({ page }) => { +test('Test root error boundary caught', async ({ page, navigate }) => { const pageUrl = '/does-not-exist' - const res = await page.goto(pageUrl) + const res = await navigate(pageUrl as any) expect(res?.status()).toBe(404) await expect(page.getByText(/We can't find this page/i)).toBeVisible() diff --git a/tests/e2e/note-images.test.ts b/tests/e2e/note-images.test.ts index e9ee74447..eea596e33 100644 --- a/tests/e2e/note-images.test.ts +++ b/tests/e2e/note-images.test.ts @@ -3,9 +3,13 @@ import { type NoteImage, type Note } from '@prisma/client' import { prisma } from '#app/utils/db.server.ts' import { expect, test } from '#tests/playwright-utils.ts' -test('Users can create note with an image', async ({ page, login }) => { +test('Users can create note with an image', async ({ + page, + navigate, + login, +}) => { const user = await login() - await page.goto(`/users/${user.username}/notes`) + await navigate('/users/:username/notes', { username: user.username }) const newNote = createNote() const altText = 'cute koala' @@ -28,9 +32,13 @@ test('Users can create note with an image', async ({ page, login }) => { ).toBeVisible() }) -test('Users can create note with multiple images', async ({ page, login }) => { +test('Users can create note with multiple images', async ({ + page, + navigate, + login, +}) => { const user = await login() - await page.goto(`/users/${user.username}/notes`) + await navigate('/users/:username/notes', { username: user.username }) const newNote = createNote() const altText1 = 'cute koala' @@ -60,7 +68,7 @@ test('Users can create note with multiple images', async ({ page, login }) => { await expect(page.getByAltText(altText2)).toBeVisible() }) -test('Users can edit note image', async ({ page, login }) => { +test('Users can edit note image', async ({ page, navigate, login }) => { const user = await login() const note = await prisma.note.create({ @@ -70,7 +78,10 @@ test('Users can edit note image', async ({ page, login }) => { ownerId: user.id, }, }) - await page.goto(`/users/${user.username}/notes/${note.id}`) + await navigate('/users/:username/notes/:noteId', { + username: user.username, + noteId: note.id, + }) // edit the image await page.getByRole('link', { name: 'Edit', exact: true }).click() @@ -86,7 +97,7 @@ test('Users can edit note image', async ({ page, login }) => { await expect(page.getByAltText(updatedImage.altText)).toBeVisible() }) -test('Users can delete note image', async ({ page, login }) => { +test('Users can delete note image', async ({ page, navigate, login }) => { const user = await login() const note = await prisma.note.create({ @@ -96,7 +107,10 @@ test('Users can delete note image', async ({ page, login }) => { ownerId: user.id, }, }) - await page.goto(`/users/${user.username}/notes/${note.id}`) + await navigate('/users/:username/notes/:noteId', { + username: user.username, + noteId: note.id, + }) await expect(page.getByRole('heading', { name: note.title })).toBeVisible() const images = page @@ -118,6 +132,7 @@ function createNote() { content: faker.lorem.paragraphs(3), } satisfies Omit } + function createNoteWithImage() { return { ...createNote(), diff --git a/tests/e2e/notes.test.ts b/tests/e2e/notes.test.ts index ca4ee63cf..44af6cf04 100644 --- a/tests/e2e/notes.test.ts +++ b/tests/e2e/notes.test.ts @@ -2,9 +2,9 @@ import { faker } from '@faker-js/faker' import { prisma } from '#app/utils/db.server.ts' import { expect, test } from '#tests/playwright-utils.ts' -test('Users can create notes', async ({ page, login }) => { +test('Users can create notes', async ({ page, navigate, login }) => { const user = await login() - await page.goto(`/users/${user.username}/notes`) + await navigate('/users/:username/notes', { username: user.username }) const newNote = createNote() await page.getByRole('link', { name: /New Note/i }).click() @@ -17,14 +17,17 @@ test('Users can create notes', async ({ page, login }) => { await expect(page).toHaveURL(new RegExp(`/users/${user.username}/notes/.*`)) }) -test('Users can edit notes', async ({ page, login }) => { +test('Users can edit notes', async ({ page, navigate, login }) => { const user = await login() const note = await prisma.note.create({ select: { id: true }, data: { ...createNote(), ownerId: user.id }, }) - await page.goto(`/users/${user.username}/notes/${note.id}`) + await navigate('/users/:username/notes/:noteId', { + username: user.username, + noteId: note.id, + }) // edit the note await page.getByRole('link', { name: 'Edit', exact: true }).click() @@ -41,14 +44,17 @@ test('Users can edit notes', async ({ page, login }) => { ).toBeVisible() }) -test('Users can delete notes', async ({ page, login }) => { +test('Users can delete notes', async ({ page, navigate, login }) => { const user = await login() const note = await prisma.note.create({ select: { id: true }, data: { ...createNote(), ownerId: user.id }, }) - await page.goto(`/users/${user.username}/notes/${note.id}`) + await navigate('/users/:username/notes/:noteId', { + username: user.username, + noteId: note.id, + }) // find links with href prefix const noteLinks = page diff --git a/tests/e2e/onboarding.test.ts b/tests/e2e/onboarding.test.ts index 423956d8f..ce98657b9 100644 --- a/tests/e2e/onboarding.test.ts +++ b/tests/e2e/onboarding.test.ts @@ -10,7 +10,12 @@ import { USERNAME_MIN_LENGTH, } from '#app/utils/user-validation' import { readEmail } from '#tests/mocks/utils.ts' -import { createUser, expect, test as base } from '#tests/playwright-utils.ts' +import { + createUser, + expect, + test as base, + type AppPages, +} from '#tests/playwright-utils.ts' const URL_REGEX = /(?https?:\/\/[^\s$.?#].[^\s]*)/ const CODE_REGEX = /Here's your verification code: (?[\d\w]+)/ @@ -40,10 +45,10 @@ const test = base.extend<{ }, }) -test('onboarding with link', async ({ page, getOnboardingData }) => { +test('onboarding with link', async ({ page, navigate, getOnboardingData }) => { const onboardingData = getOnboardingData() - await page.goto('/') + await navigate('/') await page.getByRole('link', { name: /log in/i }).click() await expect(page).toHaveURL(`/login`) @@ -67,9 +72,9 @@ test('onboarding with link', async ({ page, getOnboardingData }) => { expect(email.to).toBe(onboardingData.email.toLowerCase()) expect(email.from).toBe('hello@epicstack.dev') expect(email.subject).toMatch(/welcome/i) - const onboardingUrl = extractUrl(email.text) + const onboardingUrl = extractUrl(email.text) as AppPages invariant(onboardingUrl, 'Onboarding URL not found') - await page.goto(onboardingUrl) + await navigate(onboardingUrl) await expect(page).toHaveURL(/\/verify/) @@ -109,10 +114,14 @@ test('onboarding with link', async ({ page, getOnboardingData }) => { await expect(page).toHaveURL(`/`) }) -test('onboarding with a short code', async ({ page, getOnboardingData }) => { +test('onboarding with a short code', async ({ + page, + navigate, + getOnboardingData, +}) => { const onboardingData = getOnboardingData() - await page.goto('/signup') + await navigate('/signup') const emailTextbox = page.getByRole('textbox', { name: /email/i }) await emailTextbox.click() @@ -137,6 +146,7 @@ test('onboarding with a short code', async ({ page, getOnboardingData }) => { test('completes onboarding after GitHub OAuth given valid user details', async ({ page, + navigate, prepareGitHubUser, }) => { const ghUser = await prepareGitHubUser() @@ -148,7 +158,7 @@ test('completes onboarding after GitHub OAuth given valid user details', async ( }), ).toBeNull() - await page.goto('/signup') + await navigate('/signup') await page.getByRole('button', { name: /signup with github/i }).click() await expect(page).toHaveURL(/\/onboarding\/github/) @@ -186,6 +196,7 @@ test('completes onboarding after GitHub OAuth given valid user details', async ( test('logs user in after GitHub OAuth if they are already registered', async ({ page, + navigate, prepareGitHubUser, }) => { const ghUser = await prepareGitHubUser() @@ -214,7 +225,7 @@ test('logs user in after GitHub OAuth if they are already registered', async ({ }) expect(connection).toBeNull() - await page.goto('/signup') + await navigate('/signup') await page.getByRole('button', { name: /signup with github/i }).click() await expect(page).toHaveURL(`/`) @@ -235,11 +246,12 @@ test('logs user in after GitHub OAuth if they are already registered', async ({ test('shows help texts on entering invalid details on onboarding page after GitHub OAuth', async ({ page, + navigate, prepareGitHubUser, }) => { const ghUser = await prepareGitHubUser() - await page.goto('/signup') + await navigate('/signup') await page.getByRole('button', { name: /signup with github/i }).click() await expect(page).toHaveURL(/\/onboarding\/github/) @@ -322,11 +334,11 @@ test('shows help texts on entering invalid details on onboarding page after GitH await expect(page.getByText(/thanks for signing up/i)).toBeVisible() }) -test('login as existing user', async ({ page, insertNewUser }) => { +test('login as existing user', async ({ page, navigate, insertNewUser }) => { const password = faker.internet.password() const user = await insertNewUser({ password }) invariant(user.name, 'User name not found') - await page.goto('/login') + await navigate('/login') await page.getByRole('textbox', { name: /username/i }).fill(user.username) await page.getByLabel(/^password$/i).fill(password) await page.getByRole('button', { name: /log in/i }).click() @@ -335,11 +347,15 @@ test('login as existing user', async ({ page, insertNewUser }) => { await expect(page.getByRole('link', { name: user.name })).toBeVisible() }) -test('reset password with a link', async ({ page, insertNewUser }) => { +test('reset password with a link', async ({ + page, + navigate, + insertNewUser, +}) => { const originalPassword = faker.internet.password() const user = await insertNewUser({ password: originalPassword }) invariant(user.name, 'User name not found') - await page.goto('/login') + await navigate('/login') await page.getByRole('link', { name: /forgot password/i }).click() await expect(page).toHaveURL('/forgot-password') @@ -356,9 +372,9 @@ test('reset password with a link', async ({ page, insertNewUser }) => { expect(email.subject).toMatch(/password reset/i) expect(email.to).toBe(user.email.toLowerCase()) expect(email.from).toBe('hello@epicstack.dev') - const resetPasswordUrl = extractUrl(email.text) + const resetPasswordUrl = extractUrl(email.text) as AppPages invariant(resetPasswordUrl, 'Reset password URL not found') - await page.goto(resetPasswordUrl) + await navigate(resetPasswordUrl) await expect(page).toHaveURL(/\/verify/) @@ -389,9 +405,13 @@ test('reset password with a link', async ({ page, insertNewUser }) => { await expect(page.getByRole('link', { name: user.name })).toBeVisible() }) -test('reset password with a short code', async ({ page, insertNewUser }) => { +test('reset password with a short code', async ({ + page, + navigate, + insertNewUser, +}) => { const user = await insertNewUser() - await page.goto('/login') + await navigate('/login') await page.getByRole('link', { name: /forgot password/i }).click() await expect(page).toHaveURL('/forgot-password') diff --git a/tests/e2e/passkey.test.ts b/tests/e2e/passkey.test.ts index 9b221bf1f..2c21861a1 100644 --- a/tests/e2e/passkey.test.ts +++ b/tests/e2e/passkey.test.ts @@ -18,7 +18,11 @@ async function setupWebAuthn(page: any) { return { client, authenticatorId: result.authenticatorId } } -test('Users can register and use passkeys', async ({ page, login }) => { +test('Users can register and use passkeys', async ({ + page, + navigate, + login, +}) => { const user = await login() const { client, authenticatorId } = await setupWebAuthn(page) @@ -31,7 +35,7 @@ test('Users can register and use passkeys', async ({ page, login }) => { 'No credentials should exist initially', ).toHaveLength(0) - await page.goto('/settings/profile/passkeys') + await navigate('/settings/profile/passkeys') const passkeyRegisteredPromise = new Promise((resolve) => { client.once('WebAuthn.credentialAdded', () => resolve()) @@ -58,8 +62,8 @@ test('Users can register and use passkeys', async ({ page, login }) => { await expect(page).toHaveURL(`/`) // Try logging in with passkey - await page.goto('/login') - const signCount1 = afterRegistrationCredentials.credentials[0].signCount + await navigate('/login') + const signCount1 = afterRegistrationCredentials.credentials[0]!.signCount const passkeyAssertedPromise = new Promise((resolve) => { client.once('WebAuthn.credentialAsserted', () => resolve()) @@ -85,12 +89,12 @@ test('Users can register and use passkeys', async ({ page, login }) => { authenticatorId, }) expect(afterLoginCredentials.credentials).toHaveLength(1) - expect(afterLoginCredentials.credentials[0].signCount).toBeGreaterThan( + expect(afterLoginCredentials.credentials[0]?.signCount).toBeGreaterThan( signCount1, ) // Go to passkeys page and delete the passkey - await page.goto('/settings/profile/passkeys') + await navigate('/settings/profile/passkeys') await page.getByRole('button', { name: /delete/i }).click() // Verify the passkey is no longer listed on the page @@ -109,7 +113,7 @@ test('Users can register and use passkeys', async ({ page, login }) => { await expect(page).toHaveURL(`/`) // Try logging in with the deleted passkey - await page.goto('/login') + await navigate('/login') const deletedPasskeyAssertedPromise = new Promise((resolve) => { client.once('WebAuthn.credentialAsserted', () => resolve()) }) @@ -125,11 +129,15 @@ test('Users can register and use passkeys', async ({ page, login }) => { await expect(page).toHaveURL(`/login`) }) -test('Failed passkey verification shows error', async ({ page, login }) => { +test('Failed passkey verification shows error', async ({ + page, + navigate, + login, +}) => { const password = faker.internet.password() await login({ password }) const { client, authenticatorId } = await setupWebAuthn(page) - await page.goto('/settings/profile/passkeys') + await navigate('/settings/profile/passkeys') // Try to register with failed verification await client.send('WebAuthn.setUserVerified', { diff --git a/tests/e2e/search.test.ts b/tests/e2e/search.test.ts index 270eb4ea8..90b55d0de 100644 --- a/tests/e2e/search.test.ts +++ b/tests/e2e/search.test.ts @@ -1,9 +1,9 @@ import { invariant } from '@epic-web/invariant' import { expect, test } from '#tests/playwright-utils.ts' -test('Search from home page', async ({ page, insertNewUser }) => { +test('Search from home page', async ({ page, navigate, insertNewUser }) => { const newUser = await insertNewUser() - await page.goto('/') + await navigate('/') await page.getByRole('searchbox', { name: /search/i }).fill(newUser.username) await page.getByRole('button', { name: /search/i }).click() diff --git a/tests/e2e/settings-profile.test.ts b/tests/e2e/settings-profile.test.ts index b1d4abdcb..f50bccbc1 100644 --- a/tests/e2e/settings-profile.test.ts +++ b/tests/e2e/settings-profile.test.ts @@ -7,9 +7,9 @@ import { expect, test, createUser, waitFor } from '#tests/playwright-utils.ts' const CODE_REGEX = /Here's your verification code: (?[\d\w]+)/ -test('Users can update their basic info', async ({ page, login }) => { +test('Users can update their basic info', async ({ page, navigate, login }) => { await login() - await page.goto('/settings/profile') + await navigate('/settings/profile') const newUserData = createUser() @@ -21,11 +21,11 @@ test('Users can update their basic info', async ({ page, login }) => { await page.getByRole('button', { name: /^save/i }).click() }) -test('Users can update their password', async ({ page, login }) => { +test('Users can update their password', async ({ page, navigate, login }) => { const oldPassword = faker.internet.password() const newPassword = faker.internet.password() const user = await login({ password: oldPassword }) - await page.goto('/settings/profile') + await navigate('/settings/profile') await page.getByRole('link', { name: /change password/i }).click() @@ -52,9 +52,13 @@ test('Users can update their password', async ({ page, login }) => { ).toEqual({ id: user.id }) }) -test('Users can update their profile photo', async ({ page, login }) => { +test('Users can update their profile photo', async ({ + page, + navigate, + login, +}) => { const user = await login() - await page.goto('/settings/profile') + await navigate('/settings/profile') const beforeSrc = await page .getByRole('main') @@ -86,11 +90,15 @@ test('Users can update their profile photo', async ({ page, login }) => { expect(beforeSrc).not.toEqual(afterSrc) }) -test('Users can change their email address', async ({ page, login }) => { +test('Users can change their email address', async ({ + page, + navigate, + login, +}) => { const preUpdateUser = await login() const newEmailAddress = faker.internet.email().toLowerCase() expect(preUpdateUser.email).not.toEqual(newEmailAddress) - await page.goto('/settings/profile') + await navigate('/settings/profile') await page.getByRole('link', { name: /change email/i }).click() await page.getByRole('textbox', { name: /new email/i }).fill(newEmailAddress) await page.getByRole('button', { name: /send confirmation/i }).click() diff --git a/tests/playwright-utils.ts b/tests/playwright-utils.ts index 324dedc33..0fe8fb0ea 100644 --- a/tests/playwright-utils.ts +++ b/tests/playwright-utils.ts @@ -1,5 +1,6 @@ -import { test as base } from '@playwright/test' +import { test as base, type Response } from '@playwright/test' import { type User as UserModel } from '@prisma/client' +import { href, type Register } from 'react-router' import * as setCookieParser from 'set-cookie-parser' import { getPasswordHash, @@ -63,11 +64,21 @@ async function getOrInsertUser({ } } +export type AppPages = keyof Register['pages'] + export const test = base.extend<{ + navigate: ( + ...args: Parameters> + ) => Promise insertNewUser(options?: GetOrInsertUserOptions): Promise login(options?: GetOrInsertUserOptions): Promise prepareGitHubUser(): Promise }>({ + navigate: async ({ page }, use) => { + await use((...args) => { + return page.goto(href(...args)) + }) + }, insertNewUser: async ({}, use) => { let userId: string | undefined = undefined await use(async (options) => {