Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
83e0596
Add Playwright E2E testing foundation
busbyk Jan 29, 2026
af0be0f
Merge branch 'main' into tests
busbyk Jan 29, 2026
2e6c80c
wip rbac tests
busbyk Jan 30, 2026
f01750a
docs/notes
busbyk Jan 30, 2026
0c0dd86
Merge branch 'main' into tests
rchlfryn Feb 13, 2026
81f5a88
Refactor logout button
rchlfryn Feb 13, 2026
13eb1fa
Working login tests
rchlfryn Feb 13, 2026
5403d80
Working tenant cookie edge case tests
rchlfryn Feb 14, 2026
db68fb5
Fix global collection tests
rchlfryn Feb 14, 2026
b83b39b
Working non-tenant tests
rchlfryn Feb 14, 2026
c2ed355
Working payload global tests
rchlfryn Feb 14, 2026
ae5dfd2
Consolidate performLogin to single helper
rchlfryn Feb 14, 2026
d54c2b4
Add retries for login flow
rchlfryn Feb 15, 2026
0182d2b
Working role based tests
rchlfryn Feb 15, 2026
126be01
Working tenant required tests
rchlfryn Feb 15, 2026
f9b524d
Update testing doc
rchlfryn Feb 15, 2026
70b9497
See if log out test passes
rchlfryn Feb 15, 2026
aabf72f
Fix linter errors
rchlfryn Feb 15, 2026
3c23ff5
Add gha job for e2e tests
rchlfryn Feb 15, 2026
e7b02ba
Update ci e2e config
rchlfryn Feb 15, 2026
dea18d6
wrap login in try
rchlfryn Feb 15, 2026
f4e7414
Attempt to fix login for gha
rchlfryn Feb 15, 2026
1a12b1c
Still debugging login
rchlfryn Feb 15, 2026
d013d9b
Add fixme to flakey login test
rchlfryn Feb 15, 2026
24ae94d
Changing logout test
rchlfryn Feb 15, 2026
9b8c649
Skip logout test - need to fix
rchlfryn Feb 15, 2026
c14b63f
Add unit tests for core frontend utilities
rchlfryn Feb 15, 2026
c4bd0ce
Add frontend page load e2e tests
rchlfryn Feb 15, 2026
69fbd03
Update page test to check a few more things
rchlfryn Feb 15, 2026
96e71c9
Update testing doc
rchlfryn Feb 16, 2026
e7722cc
Readd logout tests to run on CI
rchlfryn Feb 16, 2026
2e437b8
Fix linter errors for validateSlug
rchlfryn Feb 16, 2026
40c16b8
Move utility tests to their own folder
rchlfryn Feb 16, 2026
f69695e
Move breadcrumbs to its component file
rchlfryn Feb 16, 2026
1c26f1f
Remove type assertions from utility tests
rchlfryn Feb 16, 2026
7b69760
Remove type assertions from server tests
rchlfryn Feb 16, 2026
5ee2628
Remove type assertion from breadcrumbs test
rchlfryn Feb 16, 2026
29975bf
Fix flakey test
rchlfryn Feb 16, 2026
13325f5
Remove flakey line
rchlfryn Feb 16, 2026
4fb0811
Flakey test
rchlfryn Feb 16, 2026
9116aee
Only run e2e test on merge
rchlfryn Feb 16, 2026
7e33098
update jest config
rchlfryn Feb 16, 2026
be0217b
Update formatAuthor test and use the function in AuthorAvatar
rchlfryn Feb 16, 2026
6a815db
Fix getAuthorInitials to not have space
rchlfryn Feb 16, 2026
010ab8d
Merge pull request #903 from NWACus/tests
rchlfryn Feb 16, 2026
0343895
Merge pull request #944 from NWACus/utility-tests
rchlfryn Feb 16, 2026
04aed76
Merge branch 'main' into frontend-page-load-tests
rchlfryn Feb 16, 2026
bea285f
Fix build from #945
rchlfryn Feb 17, 2026
75369d7
Merge branch 'frontend-page-load-tests' of github.com:NWACus/web into…
rchlfryn Feb 17, 2026
5ebe218
Merge pull request #945 from NWACus/frontend-page-load-tests
rchlfryn Feb 17, 2026
d370ef0
Switch to dynamicParams=true so centers not pre-built at build time can
rchlfryn Feb 17, 2026
c34ee93
Replace invariant crashes with notFound() calls
rchlfryn Feb 17, 2026
b739b06
Remove debugging
rchlfryn Feb 17, 2026
c5e7693
Merge branch 'main' into fryan/revert-926-revert-1.7.0
rchlfryn Feb 17, 2026
63d25fe
Merge branch 'fryan/revert-926-revert-1.7.0' of github.com:NWACus/web…
rchlfryn Feb 17, 2026
b5dcbfc
Resolve merge conflicts with main
rchlfryn Feb 17, 2026
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
46 changes: 46 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,52 @@ jobs:
- name: 🧪 Run tests
run: pnpm test

e2e:
name: e2e
if: github.event_name == 'merge_group'
environment: Preview
runs-on: ubuntu-latest
env:
DATABASE_URI: 'file:./dev.db'
PAYLOAD_SECRET: ${{ secrets.PAYLOAD_SECRET }}
ALLOW_SIMPLE_PASSWORDS: 'true'
LOCAL_FLAG_ENABLE_LOCAL_PRODUCTION_BUILDS: 'true'
steps:
- name: 🏗 Setup repo
uses: actions/checkout@v4
- name: 🏗 Setup pnpm
uses: pnpm/action-setup@v4
- name: 🏗 Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: pnpm
- name: 📦 Install dependencies
run: pnpm ii
shell: bash
- name: 🎭 Install Playwright Chromium
run: pnpm exec playwright install --with-deps chromium
- name: 📦 Opt out of image optimization
run: "sed -i 's/unoptimized: false/unoptimized: true/' next.config.js"
shell: bash
- name: 🌱 Seed database
run: pnpm seed:standalone
- name: 🔨 Build
run: pnpm build
- name: 🚀 Start server
run: pnpm start &
- name: ⏳ Wait for server
run: timeout 120 bash -c 'until curl -sf http://localhost:3000/admin/login > /dev/null; do sleep 2; done'
- name: 🧪 Run E2E tests
run: pnpm test:e2e
- name: 📤 Upload Playwright report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 14

migrations-check:
name: migrations-check
runs-on: ubuntu-latest
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ tsconfig.tsbuildinfo
.cursor
.claude
plans/

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
42 changes: 42 additions & 0 deletions __tests__/builders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BuiltInPage, Page, Post, Tenant } from '@/payload-types'

/**
* Factory helpers that provide defaults for all required fields,
* so tests only specify the fields they care about.
*/

export function buildBuiltInPage(fields: Partial<BuiltInPage>): BuiltInPage {
return { id: 0, title: '', url: '', tenant: 0, updatedAt: '', createdAt: '', ...fields }
}

export function buildPage(fields: Partial<Page>): Page {
return {
id: 0,
title: '',
layout: [],
slug: '',
tenant: 0,
updatedAt: '',
createdAt: '',
...fields,
}
}

export function buildTenant(fields: Partial<Tenant>): Tenant {
return { id: 0, name: '', slug: 'dvac', updatedAt: '', createdAt: '', ...fields }
}

export function buildPost(fields: Partial<Post>): Post {
return {
id: 0,
tenant: 0,
title: '',
content: {
root: { type: 'root', children: [], direction: null, format: '', indent: 0, version: 1 },
},
slug: '',
updatedAt: '',
createdAt: '',
...fields,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ import { BreadcrumbProvider } from '@/providers/BreadcrumbProvider'
import { NotFoundProvider } from '@/providers/NotFoundProvider'
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { useSelectedLayoutSegments } from 'next/navigation'

jest.mock('next/navigation', () => ({
useSelectedLayoutSegments: jest.fn(),
}))

import { useSelectedLayoutSegments } from 'next/navigation'

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const mockUseSelectedLayoutSegments = useSelectedLayoutSegments as jest.MockedFunction<
typeof useSelectedLayoutSegments
>
const mockUseSelectedLayoutSegments = jest.mocked(useSelectedLayoutSegments)

describe('Breadcrumbs', () => {
afterEach(() => {
Expand Down
12 changes: 12 additions & 0 deletions __tests__/client/utilities/extractID.client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { extractID } from '@/utilities/extractID'
import { buildBuiltInPage } from '../../builders'

describe('extractID', () => {
it('extracts id from an object with an id property', () => {
expect(extractID(buildBuiltInPage({ id: 42 }))).toBe(42)
})

it('returns the value directly when given a number', () => {
expect(extractID(42)).toBe(42)
})
})
51 changes: 51 additions & 0 deletions __tests__/client/utilities/formatAuthors.client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { formatAuthors } from '@/utilities/formatAuthors'

const author = (name: string) => ({ id: name, name })
const noName = () => ({ id: 'no-name' })

describe('formatAuthors', () => {
it('returns empty string for empty array', () => {
expect(formatAuthors([])).toBe('')
})

it('returns empty string when all authors have no name', () => {
expect(formatAuthors([noName(), noName()])).toBe('')
})

it('returns the name for a single author', () => {
expect(formatAuthors([author('Alice Smith')])).toBe('Alice Smith')
})

it('joins two authors with "&"', () => {
expect(formatAuthors([author('Alice Smith'), author('Bob Jones')])).toBe(
'Alice Smith & Bob Jones',
)
})

it('joins three authors with commas and "&"', () => {
expect(
formatAuthors([author('Alice Smith'), author('Bob Jones'), author('Charlie Brown')]),
).toBe('Alice Smith, Bob Jones & Charlie Brown')
})

it('joins four authors with commas and "&"', () => {
expect(
formatAuthors([
author('Alice Smith'),
author('Bob Jones'),
author('Charlie Brown'),
author('Diana Prince'),
]),
).toBe('Alice Smith, Bob Jones, Charlie Brown & Diana Prince')
})

it('filters out authors without names before formatting', () => {
expect(formatAuthors([author('Alice Smith'), noName(), author('Bob Jones')])).toBe(
'Alice Smith & Bob Jones',
)
})

it('returns single author when filtering leaves only one', () => {
expect(formatAuthors([noName(), author('Alice Smith'), noName()])).toBe('Alice Smith')
})
})
19 changes: 19 additions & 0 deletions __tests__/client/utilities/getAuthorInitials.client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getAuthorInitials } from '@/utilities/getAuthorInitials'

describe('getAuthorInitials', () => {
it('returns initials for a two-part name', () => {
expect(getAuthorInitials('John Doe')).toBe('JD')
})

it('returns initial for a single name', () => {
expect(getAuthorInitials('Alice')).toBe('A')
})

it('returns initials for a three-part name', () => {
expect(getAuthorInitials('Mary Jane Watson')).toBe('MJW')
})

it('handles single character names', () => {
expect(getAuthorInitials('A B')).toBe('AB')
})
})
40 changes: 40 additions & 0 deletions __tests__/client/utilities/getRelativeTime.client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getRelativeTime } from '@/utilities/getRelativeTime'

describe('getRelativeTime', () => {
beforeEach(() => {
jest.useFakeTimers()
jest.setSystemTime(new Date('2025-01-15T12:00:00Z'))
})

afterEach(() => {
jest.useRealTimers()
})

it('returns "today" for the current date', () => {
expect(getRelativeTime('2025-01-15T08:00:00Z')).toBe('today')
})

it('returns "yesterday" for one day ago', () => {
expect(getRelativeTime('2025-01-14T12:00:00Z')).toBe('yesterday')
})

it('returns "X days ago" for 2-6 days ago', () => {
expect(getRelativeTime('2025-01-12T12:00:00Z')).toBe('3 days ago')
})

it('returns "1 weeks ago" for 7 days ago', () => {
expect(getRelativeTime('2025-01-08T12:00:00Z')).toBe('1 weeks ago')
})

it('returns "3 weeks ago" for 21 days ago', () => {
expect(getRelativeTime('2024-12-25T12:00:00Z')).toBe('3 weeks ago')
})

it('returns "1 months ago" for 30+ days', () => {
expect(getRelativeTime('2024-12-15T12:00:00Z')).toBe('1 months ago')
})

it('returns "1 years ago" for 365+ days', () => {
expect(getRelativeTime('2024-01-10T12:00:00Z')).toBe('1 years ago')
})
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BuiltInPage, Page, Post } from '@/payload-types'
import { handleReferenceURL } from '@/utilities/handleReferenceURL'
import { buildBuiltInPage, buildPage, buildPost } from '../../builders'

describe('handleReferenceURL', () => {
describe('when type is external', () => {
Expand All @@ -19,8 +19,7 @@ describe('handleReferenceURL', () => {
type: 'internal',
reference: {
relationTo: 'builtInPages',
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: { url: '/built-in-page' } as BuiltInPage,
value: buildBuiltInPage({ url: '/built-in-page' }),
},
})

Expand All @@ -32,8 +31,7 @@ describe('handleReferenceURL', () => {
type: 'internal',
reference: {
relationTo: 'pages',
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: { slug: 'test-page' } as Page,
value: buildPage({ slug: 'test-page' }),
},
})

Expand All @@ -45,8 +43,7 @@ describe('handleReferenceURL', () => {
type: 'internal',
reference: {
relationTo: 'posts',
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: { slug: 'my-blog-post' } as Post,
value: buildPost({ slug: 'my-blog-post' }),
},
})

Expand All @@ -61,8 +58,7 @@ describe('handleReferenceURL', () => {
type: 'internal',
reference: {
relationTo: 'pages',
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: { slug: '' } as Page,
value: buildPage({ slug: '' }),
},
})

Expand Down
35 changes: 35 additions & 0 deletions __tests__/client/utilities/isAbsoluteUrl.client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import isAbsoluteUrl from '@/utilities/isAbsoluteUrl'

describe('isAbsoluteUrl', () => {
it('returns true for https URL', () => {
expect(isAbsoluteUrl('https://example.com')).toBe(true)
})

it('returns true for http URL', () => {
expect(isAbsoluteUrl('http://example.com')).toBe(true)
})

it('returns true for URL with path', () => {
expect(isAbsoluteUrl('https://example.com/path/to/page')).toBe(true)
})

it('returns false for relative path', () => {
expect(isAbsoluteUrl('/images/photo.jpg')).toBe(false)
})

it('returns false for bare string', () => {
expect(isAbsoluteUrl('not-a-url')).toBe(false)
})

it('returns false for null', () => {
expect(isAbsoluteUrl(null)).toBe(false)
})

it('returns false for undefined', () => {
expect(isAbsoluteUrl(undefined)).toBe(false)
})

it('returns false for empty string', () => {
expect(isAbsoluteUrl('')).toBe(false)
})
})
57 changes: 57 additions & 0 deletions __tests__/client/utilities/normalizePath.client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { normalizePath } from '@/utilities/path'

describe('normalizePath', () => {
describe('default options (no leading slash)', () => {
it('strips leading slash from a simple path', () => {
expect(normalizePath('/hello')).toBe('hello')
})

it('strips multiple leading slashes', () => {
expect(normalizePath('///hello')).toBe('hello')
})

it('strips trailing slashes', () => {
expect(normalizePath('hello/')).toBe('hello')
})

it('strips both leading and trailing slashes', () => {
expect(normalizePath('/hello/')).toBe('hello')
})

it('handles paths without slashes', () => {
expect(normalizePath('hello')).toBe('hello')
})

it('handles multi-segment paths', () => {
expect(normalizePath('/blog/post-1/')).toBe('blog/post-1')
})

it('handles empty string', () => {
expect(normalizePath('')).toBe('')
})
})

describe('with ensureLeadingSlash', () => {
const opts = { ensureLeadingSlash: true }

it('keeps existing leading slash', () => {
expect(normalizePath('/hello', opts)).toBe('/hello')
})

it('adds leading slash when missing', () => {
expect(normalizePath('hello', opts)).toBe('/hello')
})

it('collapses multiple leading slashes to one', () => {
expect(normalizePath('///hello', opts)).toBe('/hello')
})

it('strips trailing slashes', () => {
expect(normalizePath('/hello/', opts)).toBe('/hello')
})

it('handles multi-segment paths', () => {
expect(normalizePath('blog/post-1/', opts)).toBe('/blog/post-1')
})
})
})
Loading
Loading