Skip to content

Commit de6c19f

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 18235d6 commit de6c19f

File tree

6 files changed

+100
-8
lines changed

6 files changed

+100
-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, getAnchor, getRequestURL } 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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { expect, test } from "@playwright/test"
2+
import { assert } from "chai"
3+
import { nextBody } from "../helpers/page"
4+
5+
test.beforeEach(async ({ page }) => {
6+
await page.goto("/src/tests/fixtures/form_view_transition.html")
7+
8+
await page.evaluate(`
9+
document.startViewTransition = (callback) => {
10+
window.startViewTransitionCalled = true
11+
callback()
12+
}
13+
`)
14+
})
15+
16+
test("form submission with 422 response triggers view transition", async ({ page }) => {
17+
await page.click("#submit-422")
18+
await nextBody(page)
19+
20+
await expect(page.locator("h1")).toHaveText("Unprocessable Content")
21+
const called = await page.evaluate(`window.startViewTransitionCalled`)
22+
assert.isTrue(called, "view transition was triggered")
23+
})
24+
25+
test("form submission with 500 response triggers view transition", async ({ page }) => {
26+
await page.click("#submit-500")
27+
await nextBody(page)
28+
29+
await expect(page.locator("h1")).toHaveText("Internal Server Error")
30+
const called = await page.evaluate(`window.startViewTransitionCalled`)
31+
assert.isTrue(called, "view transition was triggered")
32+
})

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)