diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index 53a585331..b85ba3bcb 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -111,8 +111,12 @@ 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 { + session.frameMissing(fetchResponse, this.element) + } } } catch (error) { console.error(error) @@ -286,19 +290,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/core/session.ts b/src/core/session.ts index 70bd16133..b808a53ba 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -267,6 +267,10 @@ export class Session implements FormSubmitObserverDelegate, HistoryDelegate, Lin this.notifyApplicationAfterFrameRender(fetchResponse, frame); } + async frameMissing(fetchResponse: FetchResponse, target: FrameElement) { + dispatch("turbo:frame-missing", { target, detail: { fetchResponse } }) + } + // Application events applicationAllowsFollowingLinkToLocation(link: Element, location: URL) { 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..40debb498 100644 --- a/src/tests/fixtures/frames.html +++ b/src/tests/fixtures/frames.html @@ -9,6 +9,14 @@ addEventListener("click", ({ target }) => { if (target.id == "add-turbo-action-to-frame") { target.closest("turbo-frame")?.setAttribute("data-turbo-action", "advance") + } else if (target.id == "propose-visit-when-frame-missing") { + addEventListener("turbo:frame-missing", async (event) => { + const { detail: { fetchResponse } } = event + const { location, redirected, statusCode, responseHTML } = fetchResponse + const response = { redirected, statusCode, responseHTML: await responseHTML } + + window.Turbo.visit(location, { response }) + }, { once: true }) } }) @@ -78,6 +86,10 @@

Frames: #nested-child

Missing frame +
+ + +
@@ -104,5 +116,7 @@

Frames: #nested-child

+ + 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..95300a8a5 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,11 @@ 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 { fetchResponse } = await this.nextEventOnTarget("missing", "turbo:frame-missing") + this.assert.notOk(await this.innerHTMLForSelector("#missing")) + this.assert.ok(fetchResponse) } async "test following a link within a frame with a target set navigates the target frame"() { @@ -407,6 +410,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.nextEventNamed("turbo:load") + + 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.nextEventNamed("turbo:load") + + 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 +443,10 @@ export class FrameTests extends TurboDriveTestCase { get frameScriptEvaluationCount(): Promise { return this.evaluate(() => window.frameScriptEvaluationCount) } + + proposeVisitWhenFrameIsMissingInResponse(): Promise { + return this.clickSelector("#propose-visit-when-frame-missing") + } } declare global {