diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index 53a585331..b8d0aa6bc 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -2,7 +2,7 @@ import { FrameElement, FrameElementDelegate, FrameLoadingStyle } from "../../ele import { FetchMethod, FetchRequest, FetchRequestDelegate, FetchRequestHeaders } from "../../http/fetch_request" import { FetchResponse } from "../../http/fetch_response" import { AppearanceObserver, AppearanceObserverDelegate } from "../../observers/appearance_observer" -import { clearBusyState, getAttribute, parseHTMLDocument, markAsBusy } from "../../util" +import { clearBusyState, dispatch, getAttribute, parseHTMLDocument, markAsBusy } from "../../util" import { FormSubmission, FormSubmissionDelegate } from "../drive/form_submission" import { Visit, VisitDelegate } from "../drive/visit" import { Snapshot } from "../snapshot" @@ -111,8 +111,16 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest const renderer = new FrameRenderer(this.view.snapshot, snapshot, false) if (this.view.renderPromise) await this.view.renderPromise await this.view.render(renderer) - session.frameRendered(fetchResponse, this.element) - session.frameLoaded(this.element) + if (snapshot.element.delegate.isActive) { + session.frameRendered(fetchResponse, this.element) + session.frameLoaded(this.element) + } else { + const responseHTML = html + const { redirected, statusCode, response: { url } } = fetchResponse + const response = { redirected, responseHTML, statusCode } + + dispatch("turbo:frame-missing", { target: this.element, detail: { response, url } }) + } } } catch (error) { console.error(error) @@ -286,19 +294,13 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest let element const id = CSS.escape(this.id) - try { - if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) { - return element - } - - if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) { - await element.loaded - return await this.extractForeignFrameElement(element) - } + if (element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)) { + return element + } - console.error(`Response has no matching element`) - } catch (error) { - console.error(error) + if (element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)) { + await element.loaded + return await this.extractForeignFrameElement(element) } return new FrameElement() diff --git a/src/elements/frame_element.ts b/src/elements/frame_element.ts index bfde5d66c..d2e52fab9 100644 --- a/src/elements/frame_element.ts +++ b/src/elements/frame_element.ts @@ -12,6 +12,7 @@ export interface FrameElementDelegate { linkClickIntercepted(element: Element, url: string): void loadResponse(response: FetchResponse): void isLoading: boolean + isActive: boolean } /** diff --git a/src/tests/fixtures/frames.html b/src/tests/fixtures/frames.html index acfc17257..5e90de308 100644 --- a/src/tests/fixtures/frames.html +++ b/src/tests/fixtures/frames.html @@ -78,6 +78,10 @@

Frames: #nested-child

Missing frame +
+ + +
diff --git a/src/tests/fixtures/test.js b/src/tests/fixtures/test.js index 0cc3ea174..880f7be12 100644 --- a/src/tests/fixtures/test.js +++ b/src/tests/fixtures/test.js @@ -29,4 +29,5 @@ "turbo:visit", "turbo:frame-load", "turbo:frame-render", + "turbo:frame-missing", ]) diff --git a/src/tests/functional/frame_tests.ts b/src/tests/functional/frame_tests.ts index 979b3074e..6f58336af 100644 --- a/src/tests/functional/frame_tests.ts +++ b/src/tests/functional/frame_tests.ts @@ -19,8 +19,8 @@ export class FrameTests extends TurboDriveTestCase { async "test a frame whose src references itself does not infinitely loop"() { await this.clickSelector("#frame-self") - await this.nextEventOnTarget("frame", "turbo:frame-render") - await this.nextEventOnTarget("frame", "turbo:frame-load") + await this.nextEventOnTarget("frame", "turbo:before-fetch-request") + await this.nextEventOnTarget("frame", "turbo:before-fetch-response") const otherEvents = await this.eventLogChannel.read() this.assert.equal(otherEvents.length, 0, "no more events") @@ -37,8 +37,14 @@ export class FrameTests extends TurboDriveTestCase { async "test following a link to a page without a matching frame results in an empty frame"() { await this.clickSelector("#missing a") - await this.nextBeat + + const { response, url } = await this.nextEventOnTarget("missing", "turbo:frame-missing") + this.assert.notOk(await this.innerHTMLForSelector("#missing")) + this.assert.equal((new URL(url)).pathname, "/src/tests/fixtures/frames/frame.html") + this.assert.equal(response.statusCode, "200") + this.assert.notOk(response.redirected) + this.assert.ok(response.responseHTML) } async "test following a link within a frame with a target set navigates the target frame"() { @@ -407,6 +413,26 @@ export class FrameTests extends TurboDriveTestCase { this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") } + async "test navigating frame resulting in response without matching frame can be re-purposed to navigate entire page"() { + await this.proposeVisitWhenFrameIsMissingInResponse() + await this.clickSelector("#missing a") + await this.nextBody + + this.assert.notOk(await this.hasSelector("#missing")) + this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Frames: #frame") + this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") + } + + async "test submitting frame resulting in response without matching frame can be re-purposed to navigate entire page"() { + await this.proposeVisitWhenFrameIsMissingInResponse() + await this.clickSelector("#missing button") + await this.nextBody + + this.assert.notOk(await this.hasSelector("#missing")) + this.assert.equal(await (await this.querySelector("h1")).getVisibleText(), "Frames: #frame") + this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html") + } + async "test turbo:before-fetch-request fires on the frame element"() { await this.clickSelector("#hello a") this.assert.ok(await this.nextEventOnTarget("frame", "turbo:before-fetch-request")) @@ -420,6 +446,16 @@ export class FrameTests extends TurboDriveTestCase { get frameScriptEvaluationCount(): Promise { return this.evaluate(() => window.frameScriptEvaluationCount) } + + proposeVisitWhenFrameIsMissingInResponse(): Promise { + return this.evaluate(() => { + addEventListener("turbo:frame-missing", (event: Event) => { + const { detail: { url, response } } = event as CustomEvent + + window.Turbo.visit(url, { response }) + }) + }) + } } declare global {