Skip to content

Commit

Permalink
Dispatch turbo:fetch-request-error during Visits
Browse files Browse the repository at this point in the history
Follow-up to hotwired#640
Related to hotwired/turbo-site#110

When a `Visit` results in a `fetch` error (like when the browser is
offline), dispatch a `turbo:fetch-request-error` event in the same style
as `<turbo-frame>`- and `<form>`-initiated `fetch` errors.

For the sake of consistency, also make the `TurboFetchRequestErrorEvent`
cancelable.

Along with the implementation change, this commit also adds test
coverage to ensure that the `Event.target` is correct for
`<turbo-frame>` and `<form>` error events.
  • Loading branch information
seanpdoyle committed Aug 16, 2022
1 parent 204d253 commit 133df88
Show file tree
Hide file tree
Showing 9 changed files with 41 additions and 17 deletions.
5 changes: 0 additions & 5 deletions src/core/drive/form_submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { FetchResponse } from "../../http/fetch_response"
import { expandURL } from "../url"
import { dispatch, getAttribute, getMetaContent } from "../../util"
import { StreamMessage } from "../streams/stream_message"
import { TurboFetchRequestErrorEvent } from "../session"

export interface FormSubmissionDelegate {
formSubmissionStarted(formSubmission: FormSubmission): void
Expand Down Expand Up @@ -197,10 +196,6 @@ export class FormSubmission {

requestErrored(request: FetchRequest, error: Error) {
this.result = { success: false, error }
dispatch<TurboFetchRequestErrorEvent>("turbo:fetch-request-error", {
target: this.formElement,
detail: { request, error },
})
this.delegate.formSubmissionErrored(this, error)
}

Expand Down
6 changes: 1 addition & 5 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { FrameRenderer } from "./frame_renderer"
import { session } from "../index"
import { isAction, Action } from "../types"
import { VisitOptions } from "../drive/visit"
import { TurboBeforeFrameRenderEvent, TurboFetchRequestErrorEvent } from "../session"
import { TurboBeforeFrameRenderEvent } from "../session"
import { StreamMessage } from "../streams/stream_message"

export type TurboFrameMissingEvent = CustomEvent<{ fetchResponse: FetchResponse }>
Expand Down Expand Up @@ -255,10 +255,6 @@ export class FrameController

requestErrored(request: FetchRequest, error: Error) {
console.error(error)
dispatch<TurboFetchRequestErrorEvent>("turbo:fetch-request-error", {
target: this.element,
detail: { request, error },
})
this.resolveVisitPromise()
}

Expand Down
7 changes: 5 additions & 2 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export {
TurboBeforeRenderEvent,
TurboBeforeVisitEvent,
TurboClickEvent,
TurboFetchRequestErrorEvent,
TurboFrameLoadEvent,
TurboFrameRenderEvent,
TurboLoadEvent,
Expand All @@ -29,7 +28,11 @@ export {

export { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submission"
export { TurboFrameMissingEvent } from "./frames/frame_controller"
export { TurboBeforeFetchRequestEvent, TurboBeforeFetchResponseEvent } from "../http/fetch_request"
export {
TurboBeforeFetchRequestEvent,
TurboBeforeFetchResponseEvent,
TurboFetchRequestErrorEvent,
} from "../http/fetch_request"
export { TurboBeforeStreamRenderEvent } from "../elements/stream_element"

export { StreamActions } from "./streams/stream_actions"
Expand Down
2 changes: 0 additions & 2 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { FrameElement } from "../elements/frame_element"
import { FrameViewRenderOptions } from "./frames/frame_view"
import { FetchResponse } from "../http/fetch_response"
import { Preloader, PreloaderDelegate } from "./drive/preloader"
import { FetchRequest } from "../http/fetch_request"

export type FormMode = "on" | "off" | "optin"
export type TimingData = unknown
Expand All @@ -31,7 +30,6 @@ export type TurboBeforeVisitEvent = CustomEvent<{ url: string }>
export type TurboClickEvent = CustomEvent<{ url: string; originalEvent: MouseEvent }>
export type TurboFrameLoadEvent = CustomEvent
export type TurboBeforeFrameRenderEvent = CustomEvent<{ newFrame: FrameElement } & FrameViewRenderOptions>
export type TurboFetchRequestErrorEvent = CustomEvent<{ request: FetchRequest; error: Error }>
export type TurboFrameRenderEvent = CustomEvent<{ fetchResponse: FetchResponse }>
export type TurboLoadEvent = CustomEvent<{ url: string; timing: TimingData }>
export type TurboRenderEvent = CustomEvent
Expand Down
18 changes: 17 additions & 1 deletion src/http/fetch_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export type TurboBeforeFetchRequestEvent = CustomEvent<{
export type TurboBeforeFetchResponseEvent = CustomEvent<{
fetchResponse: FetchResponse
}>
export type TurboFetchRequestErrorEvent = CustomEvent<{
request: FetchRequest
error: Error
}>

export interface FetchRequestDelegate {
referrer?: URL
Expand Down Expand Up @@ -107,7 +111,9 @@ export class FetchRequest {
return await this.receive(response)
} catch (error) {
if ((error as Error).name !== "AbortError") {
this.delegate.requestErrored(this, error as Error)
if (this.willDelegateErrorHandling(error as Error)) {
this.delegate.requestErrored(this, error as Error)
}
throw error
}
} finally {
Expand Down Expand Up @@ -175,4 +181,14 @@ export class FetchRequest {
})
if (event.defaultPrevented) await requestInterception
}

private willDelegateErrorHandling(error: Error) {
const event = dispatch<TurboFetchRequestErrorEvent>("turbo:fetch-request-error", {
target: this.target as EventTarget,
cancelable: true,
detail: { request: this, error: error },
})

return !event.defaultPrevented
}
}
2 changes: 1 addition & 1 deletion src/tests/fixtures/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ <h1>Form</h1>
<input type="hidden" name="status" value="422">
<input type="submit" style="margin-top:1000vh">
</form>
<form class="internal_server_error" action="/__turbo/reject" method="post">
<form id="reject-form" class="internal_server_error" action="/__turbo/reject" method="post">
<input type="hidden" name="status" value="500">
<input type="submit">
</form>
Expand Down
6 changes: 6 additions & 0 deletions src/tests/functional/form_submission_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,12 @@ test("test invalid form submission with server error status", async ({ page }) =
assert.notOk(await hasSelector(page, "#frame form.reject"), "replaces entire page")
})

test("test form submission with network error", async ({ page }) => {
await page.context().setOffline(true)
await page.click("#reject-form [type=submit]")
await nextEventOnTarget(page, "reject-form", "turbo:fetch-request-error")
})

test("test submitter form submission reads button attributes", async ({ page }) => {
const button = await page.locator("#submitter form button[type=submit][formmethod=post]")
await button.click()
Expand Down
2 changes: 1 addition & 1 deletion src/tests/functional/frame_navigation_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test("test frame navigation emits fetch-request-error event when offline", async
await page.goto("/src/tests/fixtures/tabs.html")
await page.context().setOffline(true)
await page.click("#tab-2")
await nextEventNamed(page, "turbo:fetch-request-error")
await nextEventOnTarget(page, "tab-frame", "turbo:fetch-request-error")
})

test("test promoted frame navigation updates the URL before rendering", async ({ page }) => {
Expand Down
10 changes: 10 additions & 0 deletions src/tests/functional/visit_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isScrolledToTop,
nextBeat,
nextEventNamed,
nextEventOnTarget,
noNextAttributeMutationNamed,
readEventLogs,
scrollToSelector,
Expand Down Expand Up @@ -253,6 +254,15 @@ test("test can scroll to element after history-initiated turbo:visit", async ({
assert(await isScrolledToSelector(page, "#" + id), "scrolls after history-initiated turbo:load")
})

test("test Visit with network error", async ({ page }) => {
await page.evaluate(() => {
addEventListener("turbo:fetch-request-error", (event: Event) => event.preventDefault())
})
await page.context().setOffline(true)
await page.click("#same-origin-link")
await nextEventOnTarget(page, "html", "turbo:fetch-request-error")
})

async function visitLocation(page: Page, location: string) {
return page.evaluate((location) => window.Turbo.visit(location), location)
}

0 comments on commit 133df88

Please sign in to comment.