Summary
Lightpanda performs same-document navigations correctly in-page (history.pushState/replaceState update window.location; fragment changes and popstate work), but it never emits the CDP Page.navigatedWithinDocument event. As a result, any CDP client (Playwright/Puppeteer) never learns about in-document URL changes: the frame's tracked URL goes stale and SPA route changes are invisible to automation. Full-document navigations are unaffected (they correctly emit Page.frameNavigated).
Environment
lightpanda serve, driven over CDP. Reproduced on 1.0.0-dev; the gap is present at current HEAD.
- Client: Playwright 1.59 via
chromium.connectOverCDP('ws://127.0.0.1:9222').
Expected vs actual
A real Chromium fires Page.navigatedWithinDocument { frameId, url } whenever a same-document navigation happens — history.pushState/replaceState, history.back/forward/go within the same document, fragment (#) navigation, and same-document anchor clicks. Playwright uses this to update frame.url() / page.url().
Lightpanda: the in-page location updates, but no CDP event is sent, so page.url() stays at the pre-pushState value indefinitely, and expect(page).toHaveURL(...) / page.waitForURL(...) never observe the change.
Minimal repro
await page.goto('http://x.test/a'); // mock via page.route
await page.evaluate(() => history.pushState({}, '', '/b'));
// Expected: Page.navigatedWithinDocument fires, page.url() === 'http://x.test/b'
// Actual: no event; location.pathname is '/b' (in-page OK) but page.url() stays '/a'
Same result for location.hash = '#x' and for history.back() after a pushState.
Real-world impact
SPA frameworks (e.g. react-router v7 createBrowserRouter) route client-side via history.pushState. Because the route change is never reported over CDP, automated tests can't assert on the URL after navigation. Anchor-click navigation itself works (it does an async full-document load via scheduleNavigation, which emits Page.frameNavigated); the gap is specifically the same-document path.
Where it's missing in the source
Three code paths change frame.url in place without a document reload, and none emit a CDP event (grep -rn navigatedWithinDocument src/ returns nothing):
src/browser/webapi/History.zig — pushState / replaceState set frame.url + reinit window._location.
src/browser/Frame.zig — the same-document fragment short-circuit in scheduleNavigationWithArena (anchor clicks, location.hash).
src/browser/webapi/navigation/Navigation.zig — navigateInner's same-document .push / .replace / .traverse branches (the Navigation API and history.back/forward/go).
By contrast, full-document navigations dispatch .frame_navigated → src/cdp/domains/page.zig:frameNavigated → Page.frameNavigated. There is no same-document analogue.
CDP shape to emit
Page.navigatedWithinDocument
frameId: <main frame id, same one used for Page.frameNavigated>
url: <new absolute URL>
navigationType: "historyApi" | "fragment" | "other"
Implementation
I have a fix (with CDP-level regression tests, full suite green):
bebsworthy@8a51a0b
(branch: fix/cdp-navigated-within-document)
A new Notification.FrameNavigatedWithinDocument event (frame_id, url, navigation_type) is dispatched from the three paths above via a Frame.notifyNavigatedWithinDocument helper and handled in page.zig, which emits Page.navigatedWithinDocument { frameId, url, navigationType }. Unlike frameNavigated, it does not clear/recreate the execution context or send DOM.documentUpdated — the document is unchanged, matching Chrome. navigationType maps to historyApi (pushState/replaceState, Navigation API, traversal) or fragment (anchor / location.hash).
Related issues
Summary
Lightpanda performs same-document navigations correctly in-page (
history.pushState/replaceStateupdatewindow.location; fragment changes andpopstatework), but it never emits the CDPPage.navigatedWithinDocumentevent. As a result, any CDP client (Playwright/Puppeteer) never learns about in-document URL changes: the frame's tracked URL goes stale and SPA route changes are invisible to automation. Full-document navigations are unaffected (they correctly emitPage.frameNavigated).Environment
lightpanda serve, driven over CDP. Reproduced on1.0.0-dev; the gap is present at currentHEAD.chromium.connectOverCDP('ws://127.0.0.1:9222').Expected vs actual
A real Chromium fires
Page.navigatedWithinDocument { frameId, url }whenever a same-document navigation happens —history.pushState/replaceState,history.back/forward/gowithin the same document, fragment (#) navigation, and same-document anchor clicks. Playwright uses this to updateframe.url()/page.url().Lightpanda: the in-page
locationupdates, but no CDP event is sent, sopage.url()stays at the pre-pushState value indefinitely, andexpect(page).toHaveURL(...)/page.waitForURL(...)never observe the change.Minimal repro
Same result for
location.hash = '#x'and forhistory.back()after apushState.Real-world impact
SPA frameworks (e.g. react-router v7
createBrowserRouter) route client-side viahistory.pushState. Because the route change is never reported over CDP, automated tests can't assert on the URL after navigation. Anchor-click navigation itself works (it does an async full-document load viascheduleNavigation, which emitsPage.frameNavigated); the gap is specifically the same-document path.Where it's missing in the source
Three code paths change
frame.urlin place without a document reload, and none emit a CDP event (grep -rn navigatedWithinDocument src/returns nothing):src/browser/webapi/History.zig—pushState/replaceStatesetframe.url+ reinitwindow._location.src/browser/Frame.zig— the same-document fragment short-circuit inscheduleNavigationWithArena(anchor clicks,location.hash).src/browser/webapi/navigation/Navigation.zig—navigateInner's same-document.push/.replace/.traversebranches (the Navigation API andhistory.back/forward/go).By contrast, full-document navigations dispatch
.frame_navigated→src/cdp/domains/page.zig:frameNavigated→Page.frameNavigated. There is no same-document analogue.CDP shape to emit
Implementation
I have a fix (with CDP-level regression tests, full suite green):
bebsworthy@8a51a0b
(branch:
fix/cdp-navigated-within-document)A new
Notification.FrameNavigatedWithinDocumentevent (frame_id,url,navigation_type) is dispatched from the three paths above via aFrame.notifyNavigatedWithinDocumenthelper and handled inpage.zig, which emitsPage.navigatedWithinDocument { frameId, url, navigationType }. UnlikeframeNavigated, it does not clear/recreate the execution context or sendDOM.documentUpdated— the document is unchanged, matching Chrome.navigationTypemaps tohistoryApi(pushState/replaceState, Navigation API, traversal) orfragment(anchor /location.hash).Related issues
page.url()unchanged over CDP (closest existing match).navigatedWithinDocumentevent.pushStateupdatelocationJS-side, but added no CDP event (the JS-side precedent this builds on).