Skip to content

Commit

Permalink
GET Forms: fire submit-start and submit-end
Browse files Browse the repository at this point in the history
Closes #421
Closes #122

---

Fire the same sequence of events for  `<form method="get">` submissions
as for `<form method="post">` submissions.

The [FormSubmission.prepareHeadersForRequest][] already accounts for
`[method="get"]` forms by omitting the `Accept:` header with the custom
`turbo-stream` MIME type.

Test changes
---

The `eventLogs` mechanisms we have in place declared in
`src/tests/fixutres/test.js` cannot properly serialize `Element`
instances, so adding `turbo:submit-start` and `turbo:submit-end`
listeners to serialize events for our test suite isn't possible in the
current configuration. To that end, this commit adds assertions for
`<form>` submit event sequences for all events except those two. In
their place, the suite adds listeners to set `[data-form-submit-start]`
and `[data-form-submit-end]` to the `<html>` element when they fire.

[FormSubmission.prepareHeadersForRequest]: https://github.com/hotwired/turbo/blob/58d2261274533a80a2c5efda7da211b3f20efcbb/src/core/drive/form_submission.ts#L136-L144
  • Loading branch information
seanpdoyle committed Oct 14, 2021
1 parent 58d2261 commit 89c85d6
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 29 deletions.
9 changes: 3 additions & 6 deletions src/core/drive/navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@ export class Navigator {
this.stop()
this.formSubmission = new FormSubmission(this, form, submitter, true)

if (this.formSubmission.isIdempotent) {
this.proposeVisit(this.formSubmission.fetchRequest.url, { action: this.getActionForFormSubmission(this.formSubmission) })
} else {
this.formSubmission.start()
}
this.formSubmission.start()
}

stop() {
Expand Down Expand Up @@ -93,7 +89,8 @@ export class Navigator {
}

const { statusCode } = fetchResponse
const visitOptions = { response: { statusCode, responseHTML } }
const action = this.getActionForFormSubmission(formSubmission)
const visitOptions = { action, response: { statusCode, responseHTML } }
this.proposeVisit(fetchResponse.location, visitOptions)
}
}
Expand Down
10 changes: 3 additions & 7 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,9 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest

this.reloadable = false
this.formSubmission = new FormSubmission(this, element, submitter)
if (this.formSubmission.fetchRequest.isIdempotent) {
this.navigateFrame(element, this.formSubmission.fetchRequest.url.href, submitter)
} else {
const { fetchRequest } = this.formSubmission
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest)
this.formSubmission.start()
}
const { fetchRequest } = this.formSubmission
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest)
this.formSubmission.start()
}

// Fetch request delegate
Expand Down
17 changes: 16 additions & 1 deletion src/tests/fixtures/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ <h1>Form</h1>
<form action="/__turbo/redirect" method="post" class="redirect">
<input type="hidden" name="path" value="/src/tests/fixtures/form.html">
<input type="hidden" name="greeting" value="Hello from a redirect">
<input type="submit">
<input id="standard-post-form-submit" type="submit" value="form[method=post]">
</form>
<form action="/__turbo/redirect" method="get" class="redirect">
<input type="hidden" name="path" value="/src/tests/fixtures/form.html">
<input type="hidden" name="greeting" value="Hello from a redirect">
<input id="standard-get-form-submit" type="submit" value="form[method=get]">
</form>
<form action="/__turbo/messages" method="post" class="created">
<input type="hidden" name="content" value="Hello!">
Expand Down Expand Up @@ -155,6 +160,16 @@ <h1>Form</h1>
<button type="submit">Submit</button>
</form>

<form action="/__turbo/redirect" method="post" data-turbo-frame="frame">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
<button id="targets-frame-post-form-submit" type="submit">targets-frame form[method=post]</button>
</form>

<form action="/__turbo/redirect" method="get" data-turbo-frame="frame">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
<button id="targets-frame-get-form-submit" type="submit">targets-frame form[method=get]</button>
</form>

<form action="/__turbo/redirect" method="post" data-turbo-frame="frame" class="frame">
<input type="hidden" name="path" value="/src/tests/fixtures/frames/frame.html">
<button type="submit">Submit</button>
Expand Down
111 changes: 96 additions & 15 deletions src/tests/functional/form_submission_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export class FormSubmissionTests extends TurboDriveTestCase {
async setup() {
await this.goToLocation("/src/tests/fixtures/form.html")
await this.remote.execute(() => {
addEventListener("turbo:submit-start", () => document.documentElement.setAttribute("data-form-submitted", ""), { once: true })
addEventListener("turbo:submit-start", () => document.documentElement.setAttribute("data-form-submit-start", ""), { once: true })
addEventListener("turbo:submit-end", () => document.documentElement.setAttribute("data-form-submit-end", ""), { once: true })
})
}

Expand All @@ -26,15 +27,15 @@ export class FormSubmissionTests extends TurboDriveTestCase {

this.assert.equal(await this.getAlertText(), "Are you sure?")
await this.acceptAlert()
this.assert.ok(await this.formSubmitted)
this.assert.ok(await this.formSubmitStarted)
}

async "test form submission with confirmation cancelled"() {
await this.clickSelector("#standard form.confirm input[type=submit]")

this.assert.equal(await this.getAlertText(), "Are you sure?")
await this.dismissAlert()
this.assert.notOk(await this.formSubmitted)
this.assert.notOk(await this.formSubmitStarted)
}

async "test from submission with confirmation overriden"() {
Expand All @@ -44,7 +45,7 @@ export class FormSubmissionTests extends TurboDriveTestCase {

this.assert.equal(await this.getAlertText(), "Overriden message")
await this.acceptAlert()
this.assert.ok(await this.formSubmitted)
this.assert.ok(await this.formSubmitStarted)
}

async "test standard form submission does not render a progress bar before expiring the delay"() {
Expand All @@ -58,22 +59,64 @@ export class FormSubmissionTests extends TurboDriveTestCase {
await this.clickSelector("#standard form.redirect input[type=submit]")
await this.nextBody

this.assert.ok(await this.formSubmitted)
this.assert.ok(await this.formSubmitStarted)
this.assert.equal(await this.pathname, "/src/tests/fixtures/form.html")
this.assert.equal(await this.visitAction, "advance")
this.assert.equal(await this.getSearchParam("greeting"), "Hello from a redirect")
}

async "test standard POST form submission events"() {
await this.clickSelector("#standard-post-form-submit")

this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start")

const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request")

this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html"))

await this.nextEventNamed("turbo:before-fetch-response")

this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end")

await this.nextEventNamed("turbo:before-visit")
await this.nextEventNamed("turbo:visit")
await this.nextEventNamed("turbo:before-cache")
await this.nextEventNamed("turbo:before-render")
await this.nextEventNamed("turbo:render")
await this.nextEventNamed("turbo:load")
}

async "test standard GET form submission"() {
await this.clickSelector("#standard form.greeting input[type=submit]")
await this.nextBody

this.assert.notOk(await this.formSubmitted)
this.assert.ok(await this.formSubmitStarted)
this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html")
this.assert.equal(await this.visitAction, "replace")
this.assert.equal(await this.getSearchParam("greeting"), "Hello from a form")
}

async "test standard GET form submission events"() {
await this.clickSelector("#standard-get-form-submit")

this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start")

const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request")

this.assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html"))

await this.nextEventNamed("turbo:before-fetch-response")

this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end")

await this.nextEventNamed("turbo:before-visit")
await this.nextEventNamed("turbo:visit")
await this.nextEventNamed("turbo:before-cache")
await this.nextEventNamed("turbo:before-render")
await this.nextEventNamed("turbo:render")
await this.nextEventNamed("turbo:load")
}

async "test standard GET form submission appending keys"() {
await this.goToLocation("/src/tests/fixtures/form.html?query=1")
await this.clickSelector("#standard form.conflicting-values input[type=submit]")
Expand Down Expand Up @@ -281,6 +324,40 @@ export class FormSubmissionTests extends TurboDriveTestCase {
this.assert.equal(await title.getVisibleText(), "One")
}

async "test frame POST form targetting frame submission"() {
await this.clickSelector("#targets-frame-post-form-submit")

this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start")

const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request")

this.assert.ok(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html"))
this.assert.equal("frame", fetchOptions.headers["Turbo-Frame"])

await this.nextEventNamed("turbo:before-fetch-response")

this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end")

await this.nextEventNamed("turbo:frame-render")
}

async "test frame GET form targetting frame submission"() {
await this.clickSelector("#targets-frame-get-form-submit")

this.assert.ok(await this.formSubmitStarted, "fires turbo:submit-start")

const { fetchOptions } = await this.nextEventNamed("turbo:before-fetch-request")

this.assert.notOk(fetchOptions.headers["Accept"].includes("text/vnd.turbo-stream.html"))
this.assert.equal("frame", fetchOptions.headers["Turbo-Frame"])

await this.nextEventNamed("turbo:before-fetch-response")

this.assert.ok(await this.formSubmitEnded, "fires turbo:submit-end")

await this.nextEventNamed("turbo:frame-render")
}

async "test frame form GET submission from submitter referencing another frame"() {
await this.clickSelector("#frame form[method=get] [type=submit][data-turbo-frame=hello]")
await this.nextBeat
Expand Down Expand Up @@ -409,49 +486,49 @@ export class FormSubmissionTests extends TurboDriveTestCase {
await this.nextBody
await this.querySelector("#element-id")

this.assert.notOk(await this.formSubmitted)
this.assert.notOk(await this.formSubmitStarted)
}

async "test frame form submission with [data-turbo=false] on the submitter"() {
await this.clickSelector('#frame form:not([data-turbo]) input[data-turbo="false"]')
await this.nextBody
await this.querySelector("#element-id")

this.assert.notOk(await this.formSubmitted)
this.assert.notOk(await this.formSubmitStarted)
}

async "test form submission with [data-turbo=false] on the form"() {
await this.clickSelector('#turbo-false form[data-turbo="false"] input[type=submit]')
await this.nextBody
await this.querySelector("#element-id")

this.assert.notOk(await this.formSubmitted)
this.assert.notOk(await this.formSubmitStarted)
}

async "test form submission with [data-turbo=false] on the submitter"() {
await this.clickSelector('#turbo-false form:not([data-turbo]) input[data-turbo="false"]')
await this.nextBody
await this.querySelector("#element-id")

this.assert.notOk(await this.formSubmitted)
this.assert.notOk(await this.formSubmitStarted)
}

async "test form submission skipped within method=dialog"() {
await this.clickSelector('#dialog-method [type="submit"]')
await this.nextBeat

this.assert.notOk(await this.formSubmitted)
this.assert.notOk(await this.formSubmitStarted)
}

async "test form submission skipped with submitter formmethod=dialog"() {
await this.clickSelector('#dialog-formmethod [formmethod="dialog"]')
await this.nextBeat

this.assert.notOk(await this.formSubmitted)
this.assert.notOk(await this.formSubmitStarted)
}

async "test form submission targets disabled frame"() {
this.remote.execute(() => document.getElementById("frame")?.setAttribute("disabled", ""))
await this.remote.execute(() => document.getElementById("frame")?.setAttribute("disabled", ""))
await this.clickSelector('#targets-frame form.one [type="submit"]')
await this.nextBody

Expand Down Expand Up @@ -574,8 +651,12 @@ export class FormSubmissionTests extends TurboDriveTestCase {
this.assert.ok(await this.nextEventOnTarget("form_one", "turbo:before-fetch-response"))
}

get formSubmitted(): Promise<boolean> {
return this.hasSelector("html[data-form-submitted]")
get formSubmitStarted(): Promise<boolean> {
return this.hasSelector("html[data-form-submit-start]")
}

get formSubmitEnded(): Promise<boolean> {
return this.hasSelector("html[data-form-submit-end]")
}
}

Expand Down

0 comments on commit 89c85d6

Please sign in to comment.