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
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
import type { AdminRoutes } from 'helpers.js'
|
|
import type { Config } from 'payload'
|
|
import type { Page } from 'playwright/test'
|
|
|
|
import { devUser } from 'credentials.js'
|
|
import { getRoutes } from 'helpers.js'
|
|
import { formatAdminURL, wait } from 'payload/shared'
|
|
import { POLL_TOPASS_TIMEOUT } from 'playwright.config.js'
|
|
import { expect } from 'playwright/test'
|
|
|
|
import { openNav } from '../toggleNav.js'
|
|
|
|
type LoginArgs = {
|
|
customAdminRoutes?: AdminRoutes
|
|
customRoutes?: Config['routes']
|
|
data?: {
|
|
email: string
|
|
password: string
|
|
}
|
|
page: Page
|
|
serverURL: string
|
|
}
|
|
|
|
export async function login(args: LoginArgs): Promise<void> {
|
|
const { customAdminRoutes, customRoutes, data = devUser, page, serverURL } = args
|
|
|
|
const {
|
|
admin: {
|
|
routes: { createFirstUser, login: incomingLoginRoute, logout: incomingLogoutRoute } = {},
|
|
},
|
|
routes: { admin: incomingAdminRoute } = {},
|
|
} = getRoutes({ customAdminRoutes, customRoutes })
|
|
|
|
const logoutRoute = formatAdminURL({
|
|
serverURL,
|
|
adminRoute: incomingAdminRoute,
|
|
path: incomingLogoutRoute,
|
|
})
|
|
|
|
await page.goto(logoutRoute)
|
|
await wait(500)
|
|
|
|
const adminRoute = formatAdminURL({ serverURL, adminRoute: incomingAdminRoute, path: '' })
|
|
const loginRoute = formatAdminURL({
|
|
serverURL,
|
|
adminRoute: incomingAdminRoute,
|
|
path: incomingLoginRoute,
|
|
})
|
|
const createFirstUserRoute = formatAdminURL({
|
|
serverURL,
|
|
adminRoute: incomingAdminRoute,
|
|
path: createFirstUser,
|
|
})
|
|
|
|
await page.goto(loginRoute)
|
|
await wait(500)
|
|
await page.fill('#field-email', data.email)
|
|
await page.fill('#field-password', data.password)
|
|
await wait(500)
|
|
await page.click('[type=submit]')
|
|
await page.waitForURL(adminRoute)
|
|
|
|
await expect(() => expect(page.url()).not.toContain(loginRoute)).toPass({
|
|
timeout: POLL_TOPASS_TIMEOUT,
|
|
})
|
|
|
|
await expect(() => expect(page.url()).not.toContain(createFirstUserRoute)).toPass({
|
|
timeout: POLL_TOPASS_TIMEOUT,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Logs a user in by navigating via click-ops instead of using page.goto()
|
|
*/
|
|
export async function loginClientSide(args: LoginArgs): Promise<void> {
|
|
const { customAdminRoutes, customRoutes, data = devUser, page, serverURL } = args
|
|
const {
|
|
routes: { admin: incomingAdminRoute } = {},
|
|
admin: { routes: { login: incomingLoginRoute, createFirstUser } = {} },
|
|
} = getRoutes({ customAdminRoutes, customRoutes })
|
|
|
|
const adminRoute = formatAdminURL({ serverURL, adminRoute: incomingAdminRoute, path: '' })
|
|
const loginRoute = formatAdminURL({
|
|
serverURL,
|
|
adminRoute: incomingAdminRoute,
|
|
path: incomingLoginRoute,
|
|
})
|
|
const createFirstUserRoute = formatAdminURL({
|
|
serverURL,
|
|
adminRoute: incomingAdminRoute,
|
|
path: createFirstUser,
|
|
})
|
|
|
|
if ((await page.locator('#nav-toggler').count()) > 0) {
|
|
// a user is already logged in - log them out
|
|
await openNav(page)
|
|
await expect(page.locator('.nav__controls [aria-label="Log out"]')).toBeVisible()
|
|
await page.locator('.nav__controls [aria-label="Log out"]').click()
|
|
|
|
if (await page.locator('dialog#leave-without-saving').isVisible()) {
|
|
await page.locator('dialog#leave-without-saving #confirm-action').click()
|
|
}
|
|
|
|
await page.waitForURL(loginRoute)
|
|
}
|
|
|
|
await wait(500)
|
|
await page.fill('#field-email', data.email)
|
|
await page.fill('#field-password', data.password)
|
|
await wait(500)
|
|
await page.click('[type=submit]')
|
|
|
|
await expect(page.locator('.step-nav__home')).toBeVisible()
|
|
if ((await page.locator('a.step-nav__home').count()) > 0) {
|
|
await page.locator('a.step-nav__home').click()
|
|
}
|
|
|
|
await page.waitForURL(adminRoute)
|
|
|
|
await expect(() => expect(page.url()).not.toContain(loginRoute)).toPass({
|
|
timeout: POLL_TOPASS_TIMEOUT,
|
|
})
|
|
await expect(() => expect(page.url()).not.toContain(createFirstUserRoute)).toPass({
|
|
timeout: POLL_TOPASS_TIMEOUT,
|
|
})
|
|
}
|