Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,34 @@ const PORT = process.env.PORT || '3000'

export default defineConfig({
testDir: './tests/e2e',
timeout: 30 * 1000,
// Increase overall test timeout for complex flows
timeout: 60 * 1000,
expect: {
timeout: 10 * 1000,
// Increase expect timeout for slow renders
timeout: 15 * 1000,
},
fullyParallel: true,
// Run tests in series in CI for better database isolation
fullyParallel: !process.env.CI,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
// Increase retries for flaky test recovery
retries: process.env.CI ? 3 : 1,
// Single worker in CI for database isolation
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
reporter: process.env.CI ? [['html'], ['github']] : 'html',
// Global setup to clean fixtures before test run
globalSetup: './tests/setup/playwright-global-setup.ts',
use: {
baseURL: `http://localhost:${PORT}/`,
// Capture trace on first retry for debugging
trace: 'on-first-retry',
// Screenshot on failure for debugging
screenshot: 'only-on-failure',
// Video on first retry to help debug flaky tests
video: 'on-first-retry',
// Increase action timeout for slow interactions
actionTimeout: 15 * 1000,
// Increase navigation timeout for slow page loads
navigationTimeout: 30 * 1000,
},

projects: [
Expand All @@ -34,6 +50,8 @@ export default defineConfig({
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
// Increase server startup timeout
timeout: 120 * 1000,
env: {
PORT,
},
Expand Down
49 changes: 34 additions & 15 deletions tests/e2e/2fa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,64 @@ test('Users can add 2FA to their account and use it when logging in', async ({
}) => {
const password = faker.internet.password()
const user = await login({ password })

await page.goto('/settings/profile')
await page.waitForLoadState('domcontentloaded')

await page.getByRole('link', { name: /enable 2fa/i }).click()
const enable2faLink = page.getByRole('link', { name: /enable 2fa/i })
await enable2faLink.waitFor({ state: 'visible' })
await enable2faLink.click()

await expect(page).toHaveURL(`/settings/profile/two-factor`)
await page.waitForLoadState('domcontentloaded')

const main = page.getByRole('main')
await main.getByRole('button', { name: /enable 2fa/i }).click()
const otpUriString = await main
.getByLabel(/One-Time Password URI/i)
.innerText()
const enable2faButton = main.getByRole('button', { name: /enable 2fa/i })
await enable2faButton.waitFor({ state: 'visible' })
await enable2faButton.click()

const otpUriLabel = main.getByLabel(/One-Time Password URI/i)
await otpUriLabel.waitFor({ state: 'visible' })
const otpUriString = await otpUriLabel.innerText()

const otpUri = new URL(otpUriString)
const options = Object.fromEntries(otpUri.searchParams)

await main
.getByRole('textbox', { name: /code/i })
.fill(generateTOTP(options).otp)
const codeInput = main.getByRole('textbox', { name: /code/i })
await codeInput.waitFor({ state: 'visible' })
await codeInput.fill(generateTOTP(options).otp)
await main.getByRole('button', { name: /submit/i }).click()

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 ?? user.username }).click()
await page.getByRole('button', { name: /logout/i }).click()
const userMenuLink = page.getByRole('link', { name: user.name ?? user.username })
await userMenuLink.waitFor({ state: 'visible' })
await userMenuLink.click()

const logoutButton = page.getByRole('button', { name: /logout/i })
await logoutButton.waitFor({ state: 'visible' })
await logoutButton.click()
await expect(page).toHaveURL(`/`)

await page.goto('/login')
await page.waitForLoadState('domcontentloaded')
await expect(page).toHaveURL(`/login`)
await page.getByRole('textbox', { name: /username/i }).fill(user.username)

const usernameInput = page.getByRole('textbox', { name: /username/i })
await usernameInput.waitFor({ state: 'visible' })
await usernameInput.fill(user.username)
await page.getByLabel(/^password$/i).fill(password)
await page.getByRole('button', { name: /log in/i }).click()

await page
.getByRole('textbox', { name: /code/i })
.fill(generateTOTP(options).otp)
// Wait for 2FA page to load
const totpCodeInput = page.getByRole('textbox', { name: /code/i })
await totpCodeInput.waitFor({ state: 'visible', timeout: 15000 })
await totpCodeInput.fill(generateTOTP(options).otp)

await page.getByRole('button', { name: /submit/i }).click()

await expect(
page.getByRole('link', { name: user.name ?? user.username }),
).toBeVisible()
).toBeVisible({ timeout: 15000 })
})
3 changes: 2 additions & 1 deletion tests/e2e/error-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { expect, test } from '#tests/playwright-utils.ts'
test('Test root error boundary caught', async ({ page }) => {
const pageUrl = '/does-not-exist'
const res = await page.goto(pageUrl)
await page.waitForLoadState('domcontentloaded')

expect(res?.status()).toBe(404)
await expect(page.getByText(/We can't find this page/i)).toBeVisible()
await expect(page.getByText(/We can't find this page/i)).toBeVisible({ timeout: 15000 })
})
Loading
Loading