diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index e276a8fe2..437565565 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -20,7 +20,7 @@ import { import { FormSubmission, FormSubmissionDelegate } from "../drive/form_submission" import { Snapshot } from "../snapshot" import { ViewDelegate, ViewRenderOptions } from "../view" -import { getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url" +import { Locatable, getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url" import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer" import { FrameView } from "./frame_view" import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer" @@ -32,7 +32,8 @@ import { VisitOptions } from "../drive/visit" import { TurboBeforeFrameRenderEvent, TurboFetchRequestErrorEvent } from "../session" import { StreamMessage } from "../streams/stream_message" -export type TurboFrameMissingEvent = CustomEvent<{ fetchResponse: FetchResponse }> +type VisitFallback = (location: Response | Locatable, options: Partial) => Promise +export type TurboFrameMissingEvent = CustomEvent<{ response: Response; visit: VisitFallback }> export class FrameController implements @@ -169,8 +170,9 @@ export class FrameController session.frameRendered(fetchResponse, this.element) session.frameLoaded(this.element) this.fetchResponseLoaded(fetchResponse) - } else if (this.sessionWillHandleMissingFrame(fetchResponse)) { - await session.frameMissing(this.element, fetchResponse) + } else if (this.willHandleFrameMissingFromResponse(fetchResponse)) { + console.error(`Response has no matching element`) + this.element.innerHTML = "" } } } catch (error) { @@ -398,12 +400,25 @@ export class FrameController } } - private sessionWillHandleMissingFrame(fetchResponse: FetchResponse) { + private willHandleFrameMissingFromResponse(fetchResponse: FetchResponse): boolean { this.element.setAttribute("complete", "") + const response = fetchResponse.response + const visit = async (url: Locatable | Response, options: Partial = {}) => { + if (url instanceof Response) { + const wrapped = new FetchResponse(url) + const responseHTML = await wrapped.responseHTML + const { location, redirected, statusCode } = wrapped + + session.visit(location, { response: { redirected, statusCode, responseHTML }, ...options }) + } else { + session.visit(url, options) + } + } + const event = dispatch("turbo:frame-missing", { target: this.element, - detail: { fetchResponse }, + detail: { response, visit }, cancelable: true, }) diff --git a/src/core/session.ts b/src/core/session.ts index 6df0c9b62..24261c901 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -313,15 +313,6 @@ export class Session this.notifyApplicationAfterFrameRender(fetchResponse, frame) } - async frameMissing(frame: FrameElement, fetchResponse: FetchResponse): Promise { - console.warn(`A matching frame for #${frame.id} was missing from the response, transforming into full-page Visit.`) - - const responseHTML = await fetchResponse.responseHTML - const { location, redirected, statusCode } = fetchResponse - - return this.visit(location, { response: { redirected, statusCode, responseHTML } }) - } - // Application events applicationAllowsFollowingLinkToLocation(link: Element, location: URL, ev: MouseEvent) { diff --git a/src/tests/functional/frame_tests.ts b/src/tests/functional/frame_tests.ts index 04db9f003..ea1898efa 100644 --- a/src/tests/functional/frame_tests.ts +++ b/src/tests/functional/frame_tests.ts @@ -119,30 +119,22 @@ test("test following a link to a page without a matching frame dispatches a turb await page.click("#missing a") await noNextEventOnTarget(page, "missing", "turbo:frame-render") await noNextEventOnTarget(page, "missing", "turbo:frame-load") - const { fetchResponse } = await nextEventOnTarget(page, "missing", "turbo:frame-missing") - await noNextEventNamed(page, "turbo:before-fetch-request") - await nextEventNamed(page, "turbo:load") - - assert.ok(fetchResponse, "dispatchs turbo:frame-missing with event.detail.fetchResponse") - assert.equal(pathname(page.url()), "/src/tests/fixtures/frames/frame.html", "navigates the page") - - await page.goBack() - await nextEventNamed(page, "turbo:load") + const { response } = await nextEventOnTarget(page, "missing", "turbo:frame-missing") - assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html") - assert.ok(await innerHTMLForSelector(page, "#missing")) + assert.ok(response, "dispatches turbo:frame-missing with event.detail.response") + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html", "stays on the page") + assert.equal(await page.locator("#missing").innerHTML(), "", "blanks the contents when not canceled") }) -test("test following a link to a page without a matching frame dispatches a turbo:frame-missing event that can be cancelled", async ({ +test("test the turbo:frame-missing event following a link to a page without a matching frame can be handled", async ({ page, }) => { await page.locator("#missing").evaluate((frame) => { frame.addEventListener( "turbo:frame-missing", (event) => { - event.preventDefault() - if (event.target instanceof HTMLElement) { + event.preventDefault() event.target.textContent = "Overridden" } }, @@ -155,6 +147,37 @@ test("test following a link to a page without a matching frame dispatches a turb assert.equal(await page.textContent("#missing"), "Overridden") }) +test("test the turbo:frame-missing event following a link to a page without a matching frame can drive a Visit", async ({ + page, +}) => { + await page.locator("#missing").evaluate((frame) => { + frame.addEventListener( + "turbo:frame-missing", + (event) => { + if (event instanceof CustomEvent) { + event.preventDefault() + const { response, visit } = event.detail + + visit(response) + } + }, + { once: true } + ) + }) + await page.click("#missing a") + await nextEventOnTarget(page, "missing", "turbo:frame-missing") + await nextEventNamed(page, "turbo:load") + + assert.equal(await page.textContent("h1"), "Frames: #frame") + assert.notOk(await hasSelector(page, "turbo-frame#missing")) + + await page.goBack() + await nextEventNamed(page, "turbo:load") + + assert.equal(pathname(page.url()), "/src/tests/fixtures/frames.html") + assert.equal(await innerHTMLForSelector(page, "#missing"), "") +}) + test("test following a link to a page with a matching frame does not dispatch a turbo:frame-missing event", async ({ page, }) => {