diff --git a/src/core/frames/frame_controller.ts b/src/core/frames/frame_controller.ts
index 94bd8fba5..8aae1c14b 100644
--- a/src/core/frames/frame_controller.ts
+++ b/src/core/frames/frame_controller.ts
@@ -186,7 +186,8 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
// Form submission delegate
formSubmissionStarted(formSubmission: FormSubmission) {
-
+ const frame = this.findFrameElement(formSubmission.formElement)
+ frame.setAttribute("busy", "")
}
formSubmissionSucceededWithResponse(formSubmission: FormSubmission, response: FetchResponse) {
@@ -203,7 +204,8 @@ export class FrameController implements AppearanceObserverDelegate, FetchRequest
}
formSubmissionFinished(formSubmission: FormSubmission) {
-
+ const frame = this.findFrameElement(formSubmission.formElement)
+ frame.removeAttribute("busy")
}
// View delegate
diff --git a/src/tests/fixtures/form.html b/src/tests/fixtures/form.html
index f37246f5a..d724d5ad0 100644
--- a/src/tests/fixtures/form.html
+++ b/src/tests/fixtures/form.html
@@ -115,10 +115,15 @@
-
+
+
Frame: Form
diff --git a/src/tests/fixtures/test.js b/src/tests/fixtures/test.js
index 40031bd16..36f02dcff 100644
--- a/src/tests/fixtures/test.js
+++ b/src/tests/fixtures/test.js
@@ -9,7 +9,15 @@
function eventListener(event) {
eventLogs.push([event.type, event.detail])
}
+ window.mutationLogs = []
+ new MutationObserver((mutations) => {
+ for (const { attributeName, target } of mutations.filter(({ type }) => type == "attributes")) {
+ if (target instanceof Element) {
+ mutationLogs.push([attributeName, target.id, target.getAttribute(attributeName)])
+ }
+ }
+ }).observe(document, { subtree: true, childList: true, attributes: true })
})([
"turbo:before-cache",
"turbo:before-render",
diff --git a/src/tests/functional/form_submission_tests.ts b/src/tests/functional/form_submission_tests.ts
index 916022bef..34a22a440 100644
--- a/src/tests/functional/form_submission_tests.ts
+++ b/src/tests/functional/form_submission_tests.ts
@@ -185,6 +185,23 @@ export class FormSubmissionTests extends TurboDriveTestCase {
this.assert.equal(await this.attributeForSelector("#frame", "src"), url.href, "redirects the target frame")
}
+ async "test frame form submission toggles the ancestor frame's [busy] attribute"() {
+ await this.clickSelector("#frame form.redirect input[type=submit]")
+
+ this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame")
+ this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] from the #frame")
+ }
+
+ async "test frame form submission toggles the target frame's [busy] attribute"() {
+ await this.clickSelector('#targets-frame form.frame [type="submit"]')
+
+ this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame")
+
+ const title = await this.querySelector("#frame h2")
+ this.assert.equal(await title.getVisibleText(), "Frame: Loaded")
+ this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] from the #frame")
+ }
+
async "test frame form submission with empty created response"() {
const htmlBefore = await this.outerHTMLForSelector("#frame")
const button = await this.querySelector("#frame form.created input[type=submit]")
@@ -300,7 +317,7 @@ export class FormSubmissionTests extends TurboDriveTestCase {
async "test form submission targets disabled frame"() {
this.remote.execute(() => document.getElementById("frame")?.setAttribute("disabled", ""))
- await this.clickSelector('#targets-frame [type="submit"]')
+ await this.clickSelector('#targets-frame form.one [type="submit"]')
await this.nextBody
this.assert.equal(await this.pathname, "/src/tests/fixtures/one.html")
diff --git a/src/tests/functional/frame_tests.ts b/src/tests/functional/frame_tests.ts
index 426bd1d4b..a6decd6dd 100644
--- a/src/tests/functional/frame_tests.ts
+++ b/src/tests/functional/frame_tests.ts
@@ -25,6 +25,13 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(otherEvents.length, 0, "no more events")
}
+ async "test following a link driving a frame toggles the [busy] attribute"() {
+ await this.clickSelector("#hello a")
+
+ this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), "", "sets [busy] on the #frame")
+ this.assert.equal(await this.nextAttributeMutationNamed("frame", "busy"), null, "removes [busy] from the #frame")
+ }
+
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
diff --git a/src/tests/helpers/turbo_drive_test_case.ts b/src/tests/helpers/turbo_drive_test_case.ts
index 90a81fe3e..1f9f80f77 100644
--- a/src/tests/helpers/turbo_drive_test_case.ts
+++ b/src/tests/helpers/turbo_drive_test_case.ts
@@ -3,9 +3,11 @@ import { RemoteChannel } from "./remote_channel"
import { Element } from "@theintern/leadfoot"
type EventLog = [string, any]
+type MutationLog = [string, string | null, string | null]
export class TurboDriveTestCase extends FunctionalTestCase {
eventLogChannel: RemoteChannel = new RemoteChannel(this.remote, "eventLogs")
+ mutationLogChannel: RemoteChannel = new RemoteChannel(this.remote, "mutationLogs")
lastBody?: Element
async beforeTest() {
@@ -38,6 +40,16 @@ export class TurboDriveTestCase extends FunctionalTestCase {
return !records.some(([name]) => name == eventName)
}
+ async nextAttributeMutationNamed(elementId: string, attributeName: string): Promise {
+ let record: MutationLog | undefined
+ while (!record) {
+ const records = await this.mutationLogChannel.read(1)
+ record = records.find(([name, id]) => name == attributeName && id == elementId)
+ }
+ const attributeValue = record[2]
+ return attributeValue
+ }
+
get nextBody(): Promise {
return (async () => {
let body