Skip to content

Commit

Permalink
Communicate Visit direction with html[data-turbo-visit-direction] (h…
Browse files Browse the repository at this point in the history
…otwired#1007)

* Add direction detail to turbo:visit events

* Test restorationIndex is persisted between reloads

* Add data-turbo-visit-direction to html element

* Versatile Visit direction API.
Use html[data-turbo-visit-direction] attribute to communicate visit direction rather than using an event detail

* Remove "test" prefix from test description

* Fix typos

* Remove "test" prefix from test description

* Clean up assertions

* Read the mutation logs to assert the direction attribute change

It's less dependent on the timing of the mutation. Even if the attribute
is no longer present, we can still assert that it was present at some
point.

---------

Co-authored-by: Alberto Fernández-Capel <[email protected]>
  • Loading branch information
domchristie and afcapel committed Jul 15, 2024
1 parent 04978a4 commit 8769416
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 6 deletions.
12 changes: 9 additions & 3 deletions src/core/drive/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export class History {
restorationData = {}
started = false
pageLoaded = false
currentIndex = 0

constructor(delegate) {
this.delegate = delegate
Expand All @@ -15,6 +16,7 @@ export class History {
if (!this.started) {
addEventListener("popstate", this.onPopState, false)
addEventListener("load", this.onPageLoad, false)
this.currentIndex = history.state?.turbo?.restorationIndex || 0
this.started = true
this.replace(new URL(window.location.href))
}
Expand All @@ -37,7 +39,9 @@ export class History {
}

update(method, location, restorationIdentifier = uuid()) {
const state = { turbo: { restorationIdentifier } }
if (method === history.pushState) ++this.currentIndex

const state = { turbo: { restorationIdentifier, restorationIndex: this.currentIndex } }
method.call(history, state, "", location.href)
this.location = location
this.restorationIdentifier = restorationIdentifier
Expand Down Expand Up @@ -81,9 +85,11 @@ export class History {
const { turbo } = event.state || {}
if (turbo) {
this.location = new URL(window.location.href)
const { restorationIdentifier } = turbo
const { restorationIdentifier, restorationIndex } = turbo
this.restorationIdentifier = restorationIdentifier
this.delegate.historyPoppedToLocationWithRestorationIdentifier(this.location, restorationIdentifier)
const direction = restorationIndex > this.currentIndex ? "forward" : "back"
this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location, restorationIdentifier, direction)
this.currentIndex = restorationIndex
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/core/drive/visit.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export const SystemStatusCode = {
contentTypeMismatch: -2
}

export const Direction = {
advance: "forward",
restore: "back",
replace: "none"
}

export class Visit {
identifier = uuid() // Required by turbo-ios
timingMetrics = {}
Expand Down Expand Up @@ -65,7 +71,8 @@ export class Visit {
willRender,
updateHistory,
shouldCacheSnapshot,
acceptsStreamResponse
acceptsStreamResponse,
direction
} = {
...defaultOptions,
...options
Expand All @@ -83,6 +90,7 @@ export class Visit {
this.scrolled = !willRender
this.shouldCacheSnapshot = shouldCacheSnapshot
this.acceptsStreamResponse = acceptsStreamResponse
this.direction = direction || Direction[action]
}

get adapter() {
Expand Down
7 changes: 5 additions & 2 deletions src/core/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,12 @@ export class Session {

// History delegate

historyPoppedToLocationWithRestorationIdentifier(location, restorationIdentifier) {
historyPoppedToLocationWithRestorationIdentifierAndDirection(location, restorationIdentifier, direction) {
if (this.enabled) {
this.navigator.startVisit(location, restorationIdentifier, {
action: "restore",
historyChanged: true
historyChanged: true,
direction
})
} else {
this.adapter.pageInvalidated({
Expand Down Expand Up @@ -199,6 +200,7 @@ export class Session {
visitStarted(visit) {
if (!visit.acceptsStreamResponse) {
markAsBusy(document.documentElement)
this.view.markVisitDirection(visit.direction)
}
extendURLWithDeprecatedProperties(visit.location)
if (!visit.silent) {
Expand All @@ -207,6 +209,7 @@ export class Session {
}

visitCompleted(visit) {
this.view.unmarkVisitDirection()
clearBusyState(document.documentElement)
this.notifyApplicationAfterPageLoad(visit.getTimingMetrics())
}
Expand Down
8 changes: 8 additions & 0 deletions src/core/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ export class View {
}
}

markVisitDirection(direction) {
this.element.setAttribute("data-turbo-visit-direction", direction)
}

unmarkVisitDirection() {
this.element.removeAttribute("data-turbo-visit-direction")
}

async renderSnapshot(renderer) {
await renderer.render()
}
Expand Down
1 change: 1 addition & 0 deletions src/tests/fixtures/visit.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<section>
<h1>Visit</h1>
<p><a id="same-origin-link" href="/src/tests/fixtures/one.html">Same-origin link</a></p>
<p><a id="same-origin-replace-link" href="/src/tests/fixtures/one.html" data-turbo-action="replace">Same-origin replace link</a></p>
<p><a id="same-origin-link-search-params" href="/src/tests/fixtures/one.html?key=value">Same-origin link with ?key=value</a></p>
<p><a id="sample-response" href="/src/tests/fixtures/one.html">Sample response</a></p>
<p><a id="same-page-link" href="/src/tests/fixtures/visit.html">Same page link</a></p>
Expand Down
52 changes: 52 additions & 0 deletions src/tests/functional/visit_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import {
getSearchParam,
isScrolledToSelector,
isScrolledToTop,
nextAttributeMutationNamed,
nextBeat,
nextEventNamed,
noNextAttributeMutationNamed,
pathname,
readEventLogs,
resetMutationLogs,
scrollToSelector,
visitAction,
waitUntilNoSelector,
willChangeBody
} from "../helpers/page"

Expand Down Expand Up @@ -220,6 +223,55 @@ test("Visit with network error", async ({ page }) => {
await nextEventNamed(page, "turbo:fetch-request-error")
})

test("Visit direction data attribute when clicking a link", async ({ page }) => {
page.click("#same-origin-link")
await assertVisitDirectionAttribute(page, "forward")
})

test("Visit direction data attribute when navigating back", async ({ page }) => {
await page.click("#same-origin-link")
await nextEventNamed(page, "turbo:load")

await resetMutationLogs(page)

page.goBack()

await assertVisitDirectionAttribute(page, "back")
})

test("Visit direction attribute when navigating forward", async ({ page }) => {
await page.click("#same-origin-link")
await nextEventNamed(page, "turbo:load")
await page.goBack()
await nextEventNamed(page, "turbo:load")

page.goForward()

await assertVisitDirectionAttribute(page, "forward")
})

test("Visit direction attribute on a replace visit", async ({ page }) => {
page.click("#same-origin-replace-link")

await assertVisitDirectionAttribute(page, "none")
})

test("Turbo history state after a reload", async ({ page }) => {
await page.click("#same-origin-link")
await nextEventNamed(page, "turbo:load")
await page.reload()
assert.equal(
await page.evaluate(() => window.history.state.turbo.restorationIndex),
1,
"restorationIndex is persisted between reloads"
)
})

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

async function assertVisitDirectionAttribute(page, direction) {
assert.equal(await nextAttributeMutationNamed(page, "html", "data-turbo-visit-direction"), direction)
await waitUntilNoSelector(page, "[data-turbo-visit-direction]")
}
6 changes: 6 additions & 0 deletions src/tests/helpers/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ export function propertyForSelector(page, selector, propertyName) {
return page.locator(selector).evaluate((element, propertyName) => element[propertyName], propertyName)
}

export function resetMutationLogs(page) {
return page.evaluate(() => {
window.mutationLogs = []
})
}

async function readArray(page, identifier, length) {
return page.evaluate(
({ identifier, length }) => {
Expand Down

0 comments on commit 8769416

Please sign in to comment.