Skip to content

Commit ff391fb

Browse files
committed
Use view transitions for failed form submissions
View transitions don't work when forms return 422 or 500, because `formSubmissionFailedWithResponse` calls `view.renderPage()` directly. This change implements `ViewTransitioner` and wraps the existing render and scroll calls with `.renderChange`. Reviewing without whitespace changes (`-w`) is recommended. The new test is similar to `drive_view_transition_tests.js`, but I had to add a `/reject/view-transition` route to the test server.
1 parent f34af4c commit ff391fb

File tree

6 files changed

+99
-8
lines changed

6 files changed

+99
-8
lines changed

src/core/drive/navigator.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FormSubmission } from "./form_submission"
33
import { expandURL } from "../url"
44
import { Visit } from "./visit"
55
import { PageSnapshot } from "./page_snapshot"
6+
import { ViewTransitioner } from "./view_transitioner"
67

78
export class Navigator {
89
constructor(delegate) {
@@ -94,14 +95,19 @@ export class Navigator {
9495

9596
if (responseHTML) {
9697
const snapshot = PageSnapshot.fromHTMLString(responseHTML)
97-
if (fetchResponse.serverError) {
98-
await this.view.renderError(snapshot, this.currentVisit)
99-
} else {
100-
await this.view.renderPage(snapshot, false, true, this.currentVisit)
101-
}
102-
if (snapshot.refreshScroll !== "preserve") {
103-
this.view.scrollToTop()
104-
}
98+
await new ViewTransitioner().renderChange(
99+
this.view.shouldTransitionTo(snapshot),
100+
async () => {
101+
if (fetchResponse.serverError) {
102+
await this.view.renderError(snapshot, this.currentVisit)
103+
} else {
104+
await this.view.renderPage(snapshot, false, true, this.currentVisit)
105+
}
106+
if (snapshot.refreshScroll !== "preserve") {
107+
this.view.scrollToTop()
108+
}
109+
}
110+
)
105111
this.view.clearSnapshotCache()
106112
}
107113
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Form with View Transition</title>
6+
<meta name="turbo-view-transition" content="true" />
7+
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
8+
<script src="/src/tests/fixtures/test.js"></script>
9+
</head>
10+
<body>
11+
<h1>Form with View Transition</h1>
12+
<form id="form-422" action="/__turbo/reject/view-transition" method="post">
13+
<input type="hidden" name="status" value="422" />
14+
<input id="submit-422" type="submit" value="Submit (422)" />
15+
</form>
16+
<form id="form-500" action="/__turbo/reject/view-transition" method="post">
17+
<input type="hidden" name="status" value="500" />
18+
<input id="submit-500" type="submit" value="Submit (500)" />
19+
</form>
20+
</body>
21+
</html>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<html>
2+
<head>
3+
<title>Unprocessable Content with View Transition</title>
4+
<meta name="turbo-view-transition" content="true" />
5+
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
6+
<script src="/src/tests/fixtures/test.js"></script>
7+
</head>
8+
<body>
9+
<h1>Unprocessable Content</h1>
10+
<p>The form submission failed with validation errors.</p>
11+
</body>
12+
</html>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html>
2+
<head>
3+
<title>Internal Server Error</title>
4+
<meta name="turbo-view-transition" content="true" />
5+
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script>
6+
</head>
7+
<body>
8+
<h1>Internal Server Error</h1>
9+
10+
<turbo-frame id="frame">
11+
<h2>Frame: Internal Server Error</h2>
12+
</turbo-frame>
13+
</body>
14+
</html>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect, test } from "@playwright/test"
2+
import { nextBody } from "../helpers/page"
3+
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto("/src/tests/fixtures/form_view_transition.html")
6+
7+
await page.evaluate(`
8+
document.startViewTransition = (callback) => {
9+
window.startViewTransitionCalled = true
10+
callback()
11+
}
12+
`)
13+
})
14+
15+
test("form submission with 422 response triggers view transition", async ({ page }) => {
16+
await page.click("#submit-422")
17+
await nextBody(page)
18+
19+
await expect(page.locator("h1")).toHaveText("Unprocessable Content")
20+
const called = await page.evaluate(`window.startViewTransitionCalled`)
21+
await expect(called).toEqual(true)
22+
})
23+
24+
test("form submission with 500 response triggers view transition", async ({ page }) => {
25+
await page.click("#submit-500")
26+
await nextBody(page)
27+
28+
await expect(page.locator("h1")).toHaveText("Internal Server Error")
29+
const called = await page.evaluate(`window.startViewTransitionCalled`)
30+
await expect(called).toEqual(true)
31+
})

src/tests/server.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ router.post("/reject/morph", (request, response) => {
6868
response.status(parseInt(status || "422", 10)).sendFile(fixture)
6969
})
7070

71+
router.post("/reject/view-transition", (request, response) => {
72+
const { status } = request.body
73+
const fixture = path.join(__dirname, `../../src/tests/fixtures/form_view_transition_${status}.html`)
74+
75+
response.status(parseInt(status || "422", 10)).sendFile(fixture)
76+
})
77+
7178
router.post("/reject", (request, response) => {
7279
const { status } = request.body
7380
const fixture = path.join(__dirname, `../../src/tests/fixtures/${status}.html`)

0 commit comments

Comments
 (0)