Files
payloadcms/test/admin-root/e2e.spec.ts
Jacob Fletcher dfb0021545 fix: client config context inheritance (#13790)
Redirecting from login to any non-auth collection crashes the page with
the following error:

```
Cannot read properties of null (reading 'fields')
```

TL;DR: the page-level config context was threading stale methods from
the root config provider.

#### Background

The client config is now gated behind authentication as of #13714, so it
changes based on authentication status. If the root layout is mounted
before authentication, it puts the unauthenticated client config into
state for the entire app to consume.

On login, the root layout does not re-render, so the page itself needs
to generate a fresh client config and sync it up.

This leads to race conditions, however, where if the login page included
a `?redirect=` param, the redirect would take place _before_ the
page-level client config could sync to the layout, and ultimately crash
the page. This was addressed in #13786.

While this fixed redirects to the "users" collection, this collection is
_already_ included in the client config (soon to be omitted by #13785).
So if you redirect to any other collection, the above error occurs.

#### Problem

The page-level config context is only overriding the `config` property,
keeping stale methods from the root config provider. This means calling
`getEntityConfig` during this moment in the time would reference the
stale config, although `config` itself would be fresh.

#### Solution

Wrap the page with an entirely new context provider. Do not thread
inherited methods from the root provider, this way all new methods get
instantiated using the fresh config.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211332845301596
2025-09-12 17:43:03 +00:00

149 lines
4.7 KiB
TypeScript

import type { BrowserContext, Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import * as path from 'path'
import { adminRoute } from 'shared.js'
import { fileURLToPath } from 'url'
import {
ensureCompilationIsDone,
initPageConsoleErrorCatch,
saveDocAndAssert,
// throttleTest,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { login } from '../helpers/e2e/auth/login.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
let context: BrowserContext
test.describe('Admin Panel (Root)', () => {
let page: Page
let url: AdminUrlUtil
test.beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
const { serverURL } = await initPayloadE2ENoConfig({ dirname })
url = new AdminUrlUtil(serverURL, 'posts', {
admin: adminRoute,
})
context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({
customRoutes: {
admin: adminRoute,
},
page,
serverURL,
noAutoLogin: true,
})
await login({ page, serverURL, customRoutes: { admin: adminRoute } })
await ensureCompilationIsDone({
customRoutes: {
admin: adminRoute,
},
page,
serverURL,
})
})
// test.beforeEach(async () => {
// await throttleTest({
// page,
// context,
// delay: 'Fast 4G',
// })
// })
test('should redirect `${adminRoute}/collections` to `${adminRoute}', async () => {
const collectionsURL = `${url.admin}/collections`
await page.goto(collectionsURL)
// Should redirect to dashboard
await expect.poll(() => page.url()).toBe(`${url.admin}`)
})
test('renders admin panel at root', async () => {
await page.goto(url.admin)
const pageURL = page.url()
expect(pageURL).toBe(url.admin)
expect(pageURL).not.toContain('/admin')
})
test('collection — navigates to list view', async () => {
await page.goto(url.list)
const pageURL = page.url()
expect(pageURL).toContain(url.list)
expect(pageURL).not.toContain('/admin')
})
test('collection — renders versions list', async () => {
await page.goto(url.create)
const textField = page.locator('#field-text')
await textField.fill('test')
await saveDocAndAssert(page)
const versionsTab = page.locator('a.doc-tab[href$="/versions"]')
await versionsTab.click()
const firstRow = page.locator('tbody .row-1')
await expect(firstRow).toBeVisible()
})
test('collection - should hide Copy To Locale button when localization is false', async () => {
await page.goto(url.create)
const textField = page.locator('#field-text')
await textField.fill('test')
await saveDocAndAssert(page)
await page.locator('.doc-controls__popup >> .popup-button').click()
await expect(page.locator('#copy-locale-data__button')).toBeHidden()
})
test('global — navigates to edit view', async () => {
await page.goto(url.global('menu'))
const pageURL = page.url()
expect(pageURL).toBe(url.global('menu'))
expect(pageURL).not.toContain('/admin')
})
test('global — renders versions list', async () => {
await page.goto(url.global('menu'))
const textField = page.locator('#field-globalText')
await textField.fill('updated global text')
await saveDocAndAssert(page)
await page.goto(`${url.global('menu')}/versions`)
const firstRow = page.locator('tbody .row-1')
await expect(firstRow).toBeVisible()
})
test('ui - should render default payload favicons', async () => {
await page.goto(url.admin)
const favicons = page.locator('link[rel="icon"][type="image/png"]')
await expect(favicons).toHaveCount(2)
await expect(favicons.nth(0)).toHaveAttribute('sizes', '32x32')
await expect(favicons.nth(1)).toHaveAttribute('sizes', '32x32')
await expect(favicons.nth(1)).toHaveAttribute('media', '(prefers-color-scheme: dark)')
await expect(favicons.nth(1)).toHaveAttribute('href', /\/payload-favicon-light\.[a-z\d]+\.png/)
})
test('config.admin.theme should restrict the theme', async () => {
await page.goto(url.account)
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
await expect(page.locator('#field-theme')).toBeHidden()
await expect(page.locator('#field-theme-auto')).toBeHidden()
})
test('should mount custom root views', async () => {
await page.goto(`${url.admin}/custom-view`)
await expect(page.locator('#custom-view')).toBeVisible()
})
})