feat(next): regenerate live preview url on save (#13631)
Closes #12785. Although your live preview URL can be dynamic based on document data, it is never recalculated after initial mount. This means if your URL is dependent of document data that was just changed, such as a "slug" field, the URL of the iframe does not reflect that change as expected until the window is refreshed or you navigate back. This also means that server-side live preview will crash when your front-end attempts to query using a slug that no longer exists. Here's the general flow: slug changes, autosave runs, iframe refreshes (url has old slug), 404. Now, we execute your live preview function on submit within form state, and the window responds to the new URL as expected, refreshing itself without losing its connection. Here's the result: https://github.com/user-attachments/assets/7dd3b147-ab6c-4103-8b2f-14d6bc889625 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211094211063140
This commit is contained in:
@@ -20,7 +20,11 @@ export default withBundleAnalyzer(
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
remotePatterns: [
|
||||
{
|
||||
hostname: 'localhost',
|
||||
},
|
||||
],
|
||||
},
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
|
||||
3
test/helpers/e2e/live-preview/index.ts
Normal file
3
test/helpers/e2e/live-preview/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { selectLivePreviewBreakpoint } from './selectLivePreviewBreakpoint.js'
|
||||
export { selectLivePreviewZoom } from './selectLivePreviewZoom.js'
|
||||
export { toggleLivePreview } from './toggleLivePreview.js'
|
||||
29
test/helpers/e2e/live-preview/selectLivePreviewBreakpoint.ts
Normal file
29
test/helpers/e2e/live-preview/selectLivePreviewBreakpoint.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Page } from 'playwright'
|
||||
|
||||
import { POLL_TOPASS_TIMEOUT } from 'playwright.config.js'
|
||||
import { expect } from 'playwright/test'
|
||||
|
||||
export const selectLivePreviewBreakpoint = async (page: Page, breakpointLabel: string) => {
|
||||
const breakpointSelector = page.locator(
|
||||
'.live-preview-toolbar-controls__breakpoint button.popup-button',
|
||||
)
|
||||
|
||||
await expect(() => expect(breakpointSelector).toBeTruthy()).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
await breakpointSelector.first().click()
|
||||
|
||||
await page
|
||||
.locator(`.live-preview-toolbar-controls__breakpoint button.popup-button-list__button`)
|
||||
.filter({ hasText: breakpointLabel })
|
||||
.click()
|
||||
|
||||
await expect(breakpointSelector).toContainText(breakpointLabel)
|
||||
|
||||
const option = page.locator(
|
||||
'.live-preview-toolbar-controls__breakpoint button.popup-button-list__button--selected',
|
||||
)
|
||||
|
||||
await expect(option).toHaveText(breakpointLabel)
|
||||
}
|
||||
33
test/helpers/e2e/live-preview/selectLivePreviewZoom.ts
Normal file
33
test/helpers/e2e/live-preview/selectLivePreviewZoom.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Page } from 'playwright'
|
||||
|
||||
import { exactText } from 'helpers.js'
|
||||
import { POLL_TOPASS_TIMEOUT } from 'playwright.config.js'
|
||||
import { expect } from 'playwright/test'
|
||||
|
||||
export const selectLivePreviewZoom = async (page: Page, zoomLabel: string) => {
|
||||
const zoomSelector = page.locator('.live-preview-toolbar-controls__zoom button.popup-button')
|
||||
|
||||
await expect(() => expect(zoomSelector).toBeTruthy()).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
await zoomSelector.first().click()
|
||||
|
||||
const zoomOption = page.locator(
|
||||
'.live-preview-toolbar-controls__zoom button.popup-button-list__button',
|
||||
{
|
||||
hasText: exactText(zoomLabel),
|
||||
},
|
||||
)
|
||||
|
||||
expect(zoomOption).toBeTruthy()
|
||||
await zoomOption.click()
|
||||
|
||||
await expect(zoomSelector).toContainText(zoomLabel)
|
||||
|
||||
const option = page.locator(
|
||||
'.live-preview-toolbar-controls__zoom button.popup-button-list__button--selected',
|
||||
)
|
||||
|
||||
await expect(option).toHaveText(zoomLabel)
|
||||
}
|
||||
29
test/helpers/e2e/live-preview/toggleLivePreview.ts
Normal file
29
test/helpers/e2e/live-preview/toggleLivePreview.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Page } from 'playwright'
|
||||
|
||||
import { expect } from 'playwright/test'
|
||||
|
||||
export const toggleLivePreview = async (
|
||||
page: Page,
|
||||
options?: {
|
||||
targetState?: 'off' | 'on'
|
||||
},
|
||||
): Promise<void> => {
|
||||
const toggler = page.locator('#live-preview-toggler')
|
||||
await expect(toggler).toBeVisible()
|
||||
|
||||
const isActive = await toggler.evaluate((el) =>
|
||||
el.classList.contains('live-preview-toggler--active'),
|
||||
)
|
||||
|
||||
if (isActive && (options?.targetState === 'off' || !options?.targetState)) {
|
||||
await toggler.click()
|
||||
await expect(toggler).not.toHaveClass(/live-preview-toggler--active/)
|
||||
await expect(page.locator('iframe.live-preview-iframe')).toBeHidden()
|
||||
}
|
||||
|
||||
if (!isActive && (options?.targetState === 'on' || !options?.targetState)) {
|
||||
await toggler.click()
|
||||
await expect(toggler).toHaveClass(/live-preview-toggler--active/)
|
||||
await expect(page.locator('iframe.live-preview-iframe')).toBeVisible()
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type Args = {
|
||||
|
||||
export default async function SSRAutosavePage({ params: paramsPromise }: Args) {
|
||||
const { slug = '' } = await paramsPromise
|
||||
|
||||
const data = await getDoc<Page>({
|
||||
slug,
|
||||
collection: ssrAutosavePagesSlug,
|
||||
|
||||
@@ -27,6 +27,7 @@ export const Pages: CollectionConfig = {
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
unique: true,
|
||||
required: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
|
||||
@@ -36,6 +36,7 @@ export const SSRAutosave: CollectionConfig = {
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
|
||||
@@ -11,6 +11,11 @@ import type { PayloadTestSDK } from '../helpers/sdk/index.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import {
|
||||
selectLivePreviewBreakpoint,
|
||||
selectLivePreviewZoom,
|
||||
toggleLivePreview,
|
||||
} from '../helpers/e2e/live-preview/index.js'
|
||||
import { navigateToDoc, navigateToTrashedDoc } from '../helpers/e2e/navigateToDoc.js'
|
||||
import { deletePreferences } from '../helpers/e2e/preferences.js'
|
||||
import { waitForAutoSaveToRunAndComplete } from '../helpers/e2e/waitForAutoSaveToRunAndComplete.js'
|
||||
@@ -23,9 +28,6 @@ import {
|
||||
goToCollectionLivePreview,
|
||||
goToGlobalLivePreview,
|
||||
goToTrashedLivePreview,
|
||||
selectLivePreviewBreakpoint,
|
||||
selectLivePreviewZoom,
|
||||
toggleLivePreview,
|
||||
} from './helpers.js'
|
||||
import {
|
||||
collectionLevelConfigSlug,
|
||||
@@ -50,7 +52,7 @@ describe('Live Preview', () => {
|
||||
let pagesURLUtil: AdminUrlUtil
|
||||
let postsURLUtil: AdminUrlUtil
|
||||
let ssrPagesURLUtil: AdminUrlUtil
|
||||
let ssrAutosavePostsURLUtil: AdminUrlUtil
|
||||
let ssrAutosavePagesURLUtil: AdminUrlUtil
|
||||
let payload: PayloadTestSDK<Config>
|
||||
let user: any
|
||||
|
||||
@@ -61,7 +63,7 @@ describe('Live Preview', () => {
|
||||
pagesURLUtil = new AdminUrlUtil(serverURL, pagesSlug)
|
||||
postsURLUtil = new AdminUrlUtil(serverURL, postsSlug)
|
||||
ssrPagesURLUtil = new AdminUrlUtil(serverURL, ssrPagesSlug)
|
||||
ssrAutosavePostsURLUtil = new AdminUrlUtil(serverURL, ssrAutosavePagesSlug)
|
||||
ssrAutosavePagesURLUtil = new AdminUrlUtil(serverURL, ssrAutosavePagesSlug)
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
@@ -105,7 +107,7 @@ describe('Live Preview', () => {
|
||||
await expect(page.locator('iframe.live-preview-iframe')).toBeHidden()
|
||||
})
|
||||
|
||||
test('collection - does not enable live preview is collections that are not configured', async () => {
|
||||
test('collection - does not enable live preview in collections that are not configured', async () => {
|
||||
const usersURL = new AdminUrlUtil(serverURL, 'users')
|
||||
await navigateToDoc(page, usersURL)
|
||||
const toggler = page.locator('#live-preview-toggler')
|
||||
@@ -159,9 +161,10 @@ describe('Live Preview', () => {
|
||||
await goToCollectionLivePreview(page, pagesURLUtil)
|
||||
const iframe = page.locator('iframe.live-preview-iframe')
|
||||
await expect(iframe).toBeVisible()
|
||||
await expect.poll(async () => iframe.getAttribute('src')).toMatch(/\/live-preview/)
|
||||
})
|
||||
|
||||
test('collection csr — re-renders iframe client-side when form state changes', async () => {
|
||||
test('collection csr — iframe reflects form state on change', async () => {
|
||||
await goToCollectionLivePreview(page, pagesURLUtil)
|
||||
|
||||
const titleField = page.locator('#field-title')
|
||||
@@ -242,7 +245,65 @@ describe('Live Preview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('collection ssr — re-render iframe when save is made', async () => {
|
||||
test('collection csr — retains live preview connection after iframe src has changed', async () => {
|
||||
const initialTitle = 'This is a test'
|
||||
|
||||
const testDoc = await payload.create({
|
||||
collection: pagesSlug,
|
||||
data: {
|
||||
title: initialTitle,
|
||||
slug: 'csr-test',
|
||||
hero: {
|
||||
type: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await page.goto(pagesURLUtil.edit(testDoc.id))
|
||||
|
||||
await toggleLivePreview(page, {
|
||||
targetState: 'on',
|
||||
})
|
||||
|
||||
const titleField = page.locator('#field-title')
|
||||
const iframe = page.locator('iframe.live-preview-iframe')
|
||||
|
||||
await expect(iframe).toBeVisible()
|
||||
const pattern1 = new RegExp(`/live-preview/${testDoc.slug}`)
|
||||
await expect.poll(async () => iframe.getAttribute('src')).toMatch(pattern1)
|
||||
|
||||
const slugField = page.locator('#field-slug')
|
||||
const newSlug = `${testDoc.slug}-2`
|
||||
await slugField.fill(newSlug)
|
||||
await saveDocAndAssert(page)
|
||||
|
||||
// expect the iframe to have a new src that reflects the updated slug
|
||||
await expect(iframe).toBeVisible()
|
||||
const pattern2 = new RegExp(`/live-preview/${newSlug}`)
|
||||
await expect.poll(async () => iframe.getAttribute('src')).toMatch(pattern2)
|
||||
|
||||
const frame = page.frameLocator('iframe.live-preview-iframe').first()
|
||||
|
||||
const renderedPageTitleLocator = `#${renderedPageTitleID}`
|
||||
|
||||
await expect(() =>
|
||||
expect(frame.locator(renderedPageTitleLocator)).toHaveText(`For Testing: ${initialTitle}`),
|
||||
).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
// edit the title and check the iframe updates
|
||||
const newTitle = `${initialTitle} (Edited)`
|
||||
await titleField.fill(newTitle)
|
||||
|
||||
await expect(() =>
|
||||
expect(frame.locator(renderedPageTitleLocator)).toHaveText(`For Testing: ${newTitle}`),
|
||||
).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
})
|
||||
|
||||
test('collection ssr — iframe reflects form state on save', async () => {
|
||||
await goToCollectionLivePreview(page, ssrPagesURLUtil)
|
||||
|
||||
const titleField = page.locator('#field-title')
|
||||
@@ -305,9 +366,7 @@ describe('Live Preview', () => {
|
||||
targetState: 'off',
|
||||
})
|
||||
|
||||
await toggleLivePreview(page, {
|
||||
targetState: 'on',
|
||||
})
|
||||
await toggleLivePreview(page)
|
||||
|
||||
// The iframe should still be showing the updated title
|
||||
await expect(frame.locator(renderedPageTitleLocator)).toHaveText(
|
||||
@@ -326,8 +385,63 @@ describe('Live Preview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('collection ssr — re-render iframe when autosave is made', async () => {
|
||||
await goToCollectionLivePreview(page, ssrAutosavePostsURLUtil)
|
||||
test('collection ssr — retains live preview connection after iframe src has changed', async () => {
|
||||
const initialTitle = 'This is a test'
|
||||
|
||||
const testDoc = await payload.create({
|
||||
collection: ssrAutosavePagesSlug,
|
||||
data: {
|
||||
title: initialTitle,
|
||||
slug: 'ssr-test',
|
||||
hero: {
|
||||
type: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await page.goto(ssrAutosavePagesURLUtil.edit(testDoc.id))
|
||||
|
||||
await toggleLivePreview(page, {
|
||||
targetState: 'on',
|
||||
})
|
||||
|
||||
const titleField = page.locator('#field-title')
|
||||
const iframe = page.locator('iframe.live-preview-iframe')
|
||||
|
||||
const slugField = page.locator('#field-slug')
|
||||
const newSlug = `${testDoc.slug}-2`
|
||||
await slugField.fill(newSlug)
|
||||
await waitForAutoSaveToRunAndComplete(page)
|
||||
|
||||
// expect the iframe to have a new src that reflects the updated slug
|
||||
await expect(iframe).toBeVisible()
|
||||
const pattern = new RegExp(`/live-preview/${ssrAutosavePagesSlug}/${newSlug}`)
|
||||
await expect.poll(async () => iframe.getAttribute('src')).toMatch(pattern)
|
||||
|
||||
const frame = page.frameLocator('iframe.live-preview-iframe').first()
|
||||
|
||||
const renderedPageTitleLocator = `#${renderedPageTitleID}`
|
||||
|
||||
await expect(() =>
|
||||
expect(frame.locator(renderedPageTitleLocator)).toHaveText(`For Testing: ${initialTitle}`),
|
||||
).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
// edit the title and check the iframe updates
|
||||
const newTitle = `${initialTitle} (Edited)`
|
||||
await titleField.fill(newTitle)
|
||||
await waitForAutoSaveToRunAndComplete(page)
|
||||
|
||||
await expect(() =>
|
||||
expect(frame.locator(renderedPageTitleLocator)).toHaveText(`For Testing: ${newTitle}`),
|
||||
).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
})
|
||||
|
||||
test('collection ssr — iframe reflects form state on autosave', async () => {
|
||||
await goToCollectionLivePreview(page, ssrAutosavePagesURLUtil)
|
||||
|
||||
const titleField = page.locator('#field-title')
|
||||
const frame = page.frameLocator('iframe.live-preview-iframe').first()
|
||||
@@ -407,8 +521,6 @@ describe('Live Preview', () => {
|
||||
test('global — can edit fields', async () => {
|
||||
await goToGlobalLivePreview(page, 'header', serverURL)
|
||||
const field = page.locator('input#field-navItems__0__link__newTab') //field-navItems__0__link__newTab
|
||||
await expect(field).toBeVisible()
|
||||
await expect(field).toBeEnabled()
|
||||
await field.check()
|
||||
await saveDocAndAssert(page)
|
||||
})
|
||||
|
||||
@@ -2,37 +2,11 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { exactText } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { toggleLivePreview } from '../helpers/e2e/live-preview/toggleLivePreview.js'
|
||||
import { navigateToDoc, navigateToTrashedDoc } from '../helpers/e2e/navigateToDoc.js'
|
||||
import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js'
|
||||
|
||||
export const toggleLivePreview = async (
|
||||
page: Page,
|
||||
options?: {
|
||||
targetState?: 'off' | 'on'
|
||||
},
|
||||
): Promise<void> => {
|
||||
const toggler = page.locator('#live-preview-toggler')
|
||||
await expect(toggler).toBeVisible()
|
||||
|
||||
const isActive = await toggler.evaluate((el) =>
|
||||
el.classList.contains('live-preview-toggler--active'),
|
||||
)
|
||||
|
||||
if (isActive && (options?.targetState === 'off' || !options?.targetState)) {
|
||||
await toggler.click()
|
||||
await expect(toggler).not.toHaveClass(/live-preview-toggler--active/)
|
||||
await expect(page.locator('iframe.live-preview-iframe')).toBeHidden()
|
||||
}
|
||||
|
||||
if (!isActive && (options?.targetState === 'on' || !options?.targetState)) {
|
||||
await toggler.click()
|
||||
await expect(toggler).toHaveClass(/live-preview-toggler--active/)
|
||||
await expect(page.locator('iframe.live-preview-iframe')).toBeVisible()
|
||||
}
|
||||
}
|
||||
|
||||
export const goToCollectionLivePreview = async (
|
||||
page: Page,
|
||||
urlUtil: AdminUrlUtil,
|
||||
@@ -65,59 +39,6 @@ export const goToGlobalLivePreview = async (
|
||||
})
|
||||
}
|
||||
|
||||
export const selectLivePreviewBreakpoint = async (page: Page, breakpointLabel: string) => {
|
||||
const breakpointSelector = page.locator(
|
||||
'.live-preview-toolbar-controls__breakpoint button.popup-button',
|
||||
)
|
||||
|
||||
await expect(() => expect(breakpointSelector).toBeTruthy()).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
await breakpointSelector.first().click()
|
||||
|
||||
await page
|
||||
.locator(`.live-preview-toolbar-controls__breakpoint button.popup-button-list__button`)
|
||||
.filter({ hasText: breakpointLabel })
|
||||
.click()
|
||||
|
||||
await expect(breakpointSelector).toContainText(breakpointLabel)
|
||||
|
||||
const option = page.locator(
|
||||
'.live-preview-toolbar-controls__breakpoint button.popup-button-list__button--selected',
|
||||
)
|
||||
|
||||
await expect(option).toHaveText(breakpointLabel)
|
||||
}
|
||||
|
||||
export const selectLivePreviewZoom = async (page: Page, zoomLabel: string) => {
|
||||
const zoomSelector = page.locator('.live-preview-toolbar-controls__zoom button.popup-button')
|
||||
|
||||
await expect(() => expect(zoomSelector).toBeTruthy()).toPass({
|
||||
timeout: POLL_TOPASS_TIMEOUT,
|
||||
})
|
||||
|
||||
await zoomSelector.first().click()
|
||||
|
||||
const zoomOption = page.locator(
|
||||
'.live-preview-toolbar-controls__zoom button.popup-button-list__button',
|
||||
{
|
||||
hasText: exactText(zoomLabel),
|
||||
},
|
||||
)
|
||||
|
||||
expect(zoomOption).toBeTruthy()
|
||||
await zoomOption.click()
|
||||
|
||||
await expect(zoomSelector).toContainText(zoomLabel)
|
||||
|
||||
const option = page.locator(
|
||||
'.live-preview-toolbar-controls__zoom button.popup-button-list__button--selected',
|
||||
)
|
||||
|
||||
await expect(option).toHaveText(zoomLabel)
|
||||
}
|
||||
|
||||
export const ensureDeviceIsCentered = async (page: Page) => {
|
||||
const main = page.locator('.live-preview-window__main')
|
||||
const iframe = page.locator('iframe.live-preview-iframe')
|
||||
|
||||
@@ -45,7 +45,11 @@ export default withBundleAnalyzer(
|
||||
]
|
||||
},
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
remotePatterns: [
|
||||
{
|
||||
hostname: 'localhost',
|
||||
},
|
||||
],
|
||||
},
|
||||
webpack: (webpackConfig) => {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
|
||||
Reference in New Issue
Block a user