mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
fix(live-preview): gate do bridge pelo contexto de preview — desbloqueia o verify (hash #novidades)
O bridge do Payload live preview montava em toda página pública e disparava router.refresh() no mount; quando o RSC chegava depois do clique no CTA do hero, o HistoryUpdater do App Router regravava a URL canônica sem hash. Agora o bridge só monta em iframe/popup (window.parent/opener), com unit de regressão, e2e determinístico (segura o _rsc e libera depois do clique) e entrada no manifesto de afetados.
This commit is contained in:
parent
0a8b0774d3
commit
e1a0240d3a
4 changed files with 110 additions and 1 deletions
|
|
@ -93,6 +93,14 @@ export const E2E_AFFECTED_MANIFEST = [
|
|||
prefixes: ['src/app/(frontend)'],
|
||||
specs: ['frontend'],
|
||||
},
|
||||
{
|
||||
// The Payload live-preview bridge mounts on every `(frontend)` page and
|
||||
// owns when a soft refresh may run; a diff there (or in the layout that
|
||||
// mounts it) can silently drop the in-page anchors, so it wakes the hero
|
||||
// owner (`frontend`) and the novidades anchor spec (`campaignNewsletter`).
|
||||
prefixes: ['src/components/RefreshRouteOnSave.tsx', 'src/app/(frontend)/layout.tsx'],
|
||||
specs: ['frontend', 'campaignNewsletter'],
|
||||
},
|
||||
{
|
||||
// S19/S29 — the share-link surface: the `[type]` branch serves the OG card,
|
||||
// the instant handoff and the announcement page (`.ics` included), and the
|
||||
|
|
|
|||
|
|
@ -1,10 +1,28 @@
|
|||
'use client'
|
||||
import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* The Payload live-preview bridge belongs to the preview surface the admin
|
||||
* opens (an iframe in the edit view, or the popup variant). The package fires
|
||||
* `router.refresh()` as soon as it mounts to pick up the saved data, and the
|
||||
* App Router answers a refresh by rewriting the canonical URL — which drops
|
||||
* any `#hash` applied between the click and the refresh (the hero CTA →
|
||||
* `#novidades` deploy-verify race). A standalone public load must not pay that
|
||||
* refresh, so the bridge mounts only when there is a preview parent
|
||||
* (`window.parent` for the iframe, `window.opener` for the popup); the render
|
||||
* stays `null` on every other page view.
|
||||
*/
|
||||
export const RefreshRouteOnSave: React.FC = () => {
|
||||
const router = useRouter()
|
||||
const [previewing, setPreviewing] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setPreviewing(window.parent !== window || window.opener != null)
|
||||
}, [])
|
||||
|
||||
if (!previewing) return null
|
||||
|
||||
return <PayloadLivePreview refresh={() => router.refresh()} serverURL="http://localhost:3000" />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,6 +241,39 @@ test.describe('Campaign home novidades hash scroll', () => {
|
|||
await page.goto(`/?e2e=scroll-${scrollSuffix}#novidades`)
|
||||
await expect(page.locator('[data-home-section="newsletter"]')).toBeInViewport()
|
||||
})
|
||||
|
||||
test('hero CTA keeps the hash when an on-mount refresh lands late', async ({ page }) => {
|
||||
// The Payload live-preview bridge used to call `router.refresh()` on every
|
||||
// standalone load; when the soft refresh landed after the CTA click, the
|
||||
// App Router rewrote the canonical URL and dropped `#novidades` (the
|
||||
// deploy-verify race). Hold the home soft refresh until after the click so
|
||||
// the race is deterministic: outside the admin preview there must be no
|
||||
// refresh at all, and the hash survives.
|
||||
let releaseRefresh: (() => void) | undefined
|
||||
const refreshGate = new Promise<void>((resolve) => {
|
||||
releaseRefresh = resolve
|
||||
})
|
||||
|
||||
await page.route(
|
||||
(url) => url.searchParams.has('_rsc'),
|
||||
async (route) => {
|
||||
if (new URL(route.request().url()).pathname !== '/') {
|
||||
await route.continue()
|
||||
return
|
||||
}
|
||||
await refreshGate
|
||||
await route.continue()
|
||||
},
|
||||
)
|
||||
|
||||
await page.goto(`/?e2e=refresh-${scrollSuffix}`)
|
||||
await page.locator('[data-cta="secondary"]').click()
|
||||
await expect(page).toHaveURL(/#novidades/)
|
||||
|
||||
releaseRefresh?.()
|
||||
await page.waitForTimeout(1_000)
|
||||
await expect(page).toHaveURL(/#novidades/)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Campaign home novidades hash scroll — desktop', () => {
|
||||
|
|
|
|||
50
tests/unit/refreshRouteOnSave.unit.spec.tsx
Normal file
50
tests/unit/refreshRouteOnSave.unit.spec.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const router = vi.hoisted(() => ({ refresh: vi.fn() }))
|
||||
|
||||
vi.mock('next/navigation.js', () => ({ useRouter: () => router }))
|
||||
|
||||
const livePreview = vi.hoisted(() => ({ mounts: 0 }))
|
||||
|
||||
vi.mock('@payloadcms/live-preview-react', () => ({
|
||||
RefreshRouteOnSave: ({ refresh }: { refresh: () => void }) => {
|
||||
livePreview.mounts += 1
|
||||
refresh()
|
||||
return null
|
||||
},
|
||||
}))
|
||||
|
||||
import { RefreshRouteOnSave } from '@/components/RefreshRouteOnSave'
|
||||
|
||||
const setParentWindow = (value: Window) => {
|
||||
Object.defineProperty(window, 'parent', { configurable: true, value })
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
setParentWindow(window)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
router.refresh.mockReset()
|
||||
livePreview.mounts = 0
|
||||
})
|
||||
|
||||
describe('RefreshRouteOnSave', () => {
|
||||
it('does not mount the preview bridge (nor refresh) on a standalone public load', () => {
|
||||
render(<RefreshRouteOnSave />)
|
||||
|
||||
expect(livePreview.mounts).toBe(0)
|
||||
expect(router.refresh).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('mounts the bridge and refreshes inside the admin preview iframe', () => {
|
||||
setParentWindow({} as Window)
|
||||
|
||||
render(<RefreshRouteOnSave />)
|
||||
|
||||
expect(livePreview.mounts).toBe(1)
|
||||
expect(router.refresh).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue