diff --git a/src/core/drive/form_submission.ts b/src/core/drive/form_submission.ts index babeb1259..6ff0d336e 100644 --- a/src/core/drive/form_submission.ts +++ b/src/core/drive/form_submission.ts @@ -161,7 +161,7 @@ export class FormSubmission { } if (this.requestAcceptsTurboStreamResponse(request)) { - headers["Accept"] = [StreamMessage.contentType, headers["Accept"]].join(", ") + request.acceptResponseType(StreamMessage.contentType) } } diff --git a/src/core/drive/visit.ts b/src/core/drive/visit.ts index a0f92293c..6d709f2ff 100644 --- a/src/core/drive/visit.ts +++ b/src/core/drive/visit.ts @@ -1,5 +1,5 @@ import { Adapter } from "../native/adapter" -import { FetchMethod, FetchRequest, FetchRequestDelegate } from "../../http/fetch_request" +import { FetchMethod, FetchRequest, FetchRequestDelegate, FetchRequestHeaders } from "../../http/fetch_request" import { FetchResponse } from "../../http/fetch_response" import { History } from "./history" import { getAnchor } from "../url" @@ -8,6 +8,7 @@ import { PageSnapshot } from "./page_snapshot" import { Action, ResolvingFunctions } from "../types" import { getHistoryMethodForAction, uuid } from "../../util" import { PageView } from "./page_view" +import { StreamMessage } from "../streams/stream_message" export interface VisitDelegate { readonly adapter: Adapter @@ -49,6 +50,7 @@ export type VisitOptions = { restorationIdentifier?: string shouldCacheSnapshot: boolean frame?: string + acceptsStreamResponse: boolean } const defaultOptions: VisitOptions = { @@ -58,6 +60,7 @@ const defaultOptions: VisitOptions = { willRender: true, updateHistory: true, shouldCacheSnapshot: true, + acceptsStreamResponse: false, } export type VisitResponse = { @@ -96,6 +99,7 @@ export class Visit implements FetchRequestDelegate { response?: VisitResponse scrolled = false shouldCacheSnapshot = true + acceptsStreamResponse = false snapshotHTML?: string snapshotCached = false state = VisitState.initialized @@ -121,6 +125,7 @@ export class Visit implements FetchRequestDelegate { willRender, updateHistory, shouldCacheSnapshot, + acceptsStreamResponse, } = { ...defaultOptions, ...options, @@ -136,6 +141,7 @@ export class Visit implements FetchRequestDelegate { this.updateHistory = updateHistory this.scrolled = !willRender this.shouldCacheSnapshot = shouldCacheSnapshot + this.acceptsStreamResponse = acceptsStreamResponse } get adapter() { @@ -335,6 +341,12 @@ export class Visit implements FetchRequestDelegate { // Fetch request delegate + prepareHeadersForRequest(headers: FetchRequestHeaders, request: FetchRequest) { + if (this.acceptsStreamResponse) { + request.acceptResponseType(StreamMessage.contentType) + } + } + requestStarted() { this.startRequest() } diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts index 85254930d..94f8c42e6 100644 --- a/src/core/frames/frame_controller.ts +++ b/src/core/frames/frame_controller.ts @@ -30,6 +30,7 @@ import { session } from "../index" import { isAction, Action } from "../types" import { VisitOptions } from "../drive/visit" import { TurboBeforeFrameRenderEvent } from "../session" +import { StreamMessage } from "../streams/stream_message" export class FrameController implements @@ -59,6 +60,7 @@ export class FrameController private frame?: FrameElement readonly restorationIdentifier: string private previousFrameElement?: FrameElement + private currentNavigationElement?: Element constructor(element: FrameElement) { this.element = element @@ -217,8 +219,12 @@ export class FrameController // Fetch request delegate - prepareHeadersForRequest(headers: FetchRequestHeaders, _request: FetchRequest) { + prepareHeadersForRequest(headers: FetchRequestHeaders, request: FetchRequest) { headers["Turbo-Frame"] = this.id + + if (this.currentNavigationElement?.hasAttribute("data-turbo-stream")) { + request.acceptResponseType(StreamMessage.contentType) + } } requestStarted(_request: FetchRequest) { @@ -340,7 +346,9 @@ export class FrameController this.proposeVisitIfNavigatedWithAction(frame, element, submitter) - frame.src = url + this.withCurrentNavigationElement(element, () => { + frame.src = url + }) } private proposeVisitIfNavigatedWithAction(frame: FrameElement, element: Element, submitter?: HTMLElement) { @@ -505,6 +513,12 @@ export class FrameController callback() this.ignoredAttributes.delete(attributeName) } + + private withCurrentNavigationElement(element: Element, callback: () => void) { + this.currentNavigationElement = element + callback() + delete this.currentNavigationElement + } } function getFrameElementById(id: string | null) { diff --git a/src/core/session.ts b/src/core/session.ts index 23fcc1eaa..ac8408ca9 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -191,7 +191,9 @@ export class Session followedLinkToLocation(link: Element, location: URL) { const action = this.getActionForLink(link) - this.visit(location.href, { action }) + const acceptsStreamResponse = link.hasAttribute("data-turbo-stream") + + this.visit(location.href, { action, acceptsStreamResponse }) } // Navigator delegate diff --git a/src/http/fetch_request.ts b/src/http/fetch_request.ts index 35952fc7e..459a9b786 100644 --- a/src/http/fetch_request.ts +++ b/src/http/fetch_request.ts @@ -158,6 +158,10 @@ export class FetchRequest { return this.abortController.signal } + acceptResponseType(mimeType: string) { + this.headers["Accept"] = [mimeType, this.headers["Accept"]].join(", ") + } + private async allowRequestToBeIntercepted(fetchOptions: RequestInit) { const requestInterception = new Promise((resolve) => (this.resolveRequestPromise = resolve)) const event = dispatch("turbo:before-fetch-request", { diff --git a/src/observers/form_link_click_observer.ts b/src/observers/form_link_click_observer.ts index 6ff364de8..c6949a851 100644 --- a/src/observers/form_link_click_observer.ts +++ b/src/observers/form_link_click_observer.ts @@ -25,7 +25,7 @@ export class FormLinkClickObserver implements LinkClickObserverDelegate { willFollowLinkToLocation(link: Element, location: URL, originalEvent: MouseEvent): boolean { return ( this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && - (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream")) + link.hasAttribute("data-turbo-method") ) } diff --git a/src/tests/fixtures/form.html b/src/tests/fixtures/form.html index b149d1811..32a20c7ae 100644 --- a/src/tests/fixtures/form.html +++ b/src/tests/fixtures/form.html @@ -296,7 +296,6 @@

Frame: Form

Method link outside frame
Stream link outside frame - Stream link (no method) outside frame
Method link within form outside frame
Stream link within form outside frame diff --git a/src/tests/fixtures/visit.html b/src/tests/fixtures/visit.html index 138a72c36..d138fd384 100644 --- a/src/tests/fixtures/visit.html +++ b/src/tests/fixtures/visit.html @@ -19,6 +19,7 @@

Visit


one.html


+

Stream link with ?key=value

diff --git a/src/tests/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts index 4957068ae..ed5ea5a67 100644 --- a/src/tests/functional/form_submission_tests.ts +++ b/src/tests/functional/form_submission_tests.ts @@ -886,17 +886,10 @@ test("test stream link GET method form submission inside frame", async ({ page } test("test stream link inside frame", async ({ page }) => { await page.click("#stream-link-inside-frame") - const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") - - assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) -}) - -test("test stream link outside frame", async ({ page }) => { - await page.click("#stream-link-outside-frame") - - const { fetchOptions } = await nextEventNamed(page, "turbo:before-fetch-request") + const { fetchOptions, url } = await nextEventNamed(page, "turbo:before-fetch-request") assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + assert.equal(getSearchParam(url, "content"), "Link!") }) test("test link method form submission within form inside frame", async ({ page }) => { diff --git a/src/tests/functional/visit_tests.ts b/src/tests/functional/visit_tests.ts index c804adeb3..e03fbc65c 100644 --- a/src/tests/functional/visit_tests.ts +++ b/src/tests/functional/visit_tests.ts @@ -2,6 +2,7 @@ import { Page, test } from "@playwright/test" import { assert } from "chai" import { get } from "http" import { + getSearchParam, isScrolledToSelector, isScrolledToTop, nextBeat, @@ -178,6 +179,14 @@ test("test turbo:before-fetch-response open new site", async ({ page }) => { assert.isTrue(fetchResponseResult.responseHTML.indexOf("An element with an ID") > -1) }) +test("test visits with data-turbo-stream include MIME type & search params", async ({ page }) => { + await page.click("#stream-link") + const { fetchOptions, url } = await nextEventNamed(page, "turbo:before-fetch-request") + + assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html")) + assert.equal(getSearchParam(url, "key"), "value") +}) + test("test cache does not override response after redirect", async ({ page }) => { await page.evaluate(() => { const cachedElement = document.createElement("some-cached-element")