Preserve me!
diff --git a/src/tests/functional/autofocus_tests.js b/src/tests/functional/autofocus_tests.js
index 011562ceb..9cc49afe3 100644
--- a/src/tests/functional/autofocus_tests.js
+++ b/src/tests/functional/autofocus_tests.js
@@ -1,150 +1,65 @@
-import { test } from "@playwright/test"
-import { assert } from "chai"
-import { hasSelector, nextBeat } from "../helpers/page"
+import { expect, test } from "@playwright/test"
test.beforeEach(async ({ page }) => {
await page.goto("/src/tests/fixtures/autofocus.html")
})
test("autofocus first autofocus element on load", async ({ page }) => {
- await nextBeat()
- assert.ok(
- await hasSelector(page, "#first-autofocus-element:focus"),
- "focuses the first [autofocus] element on the page"
- )
- assert.notOk(
- await hasSelector(page, "#second-autofocus-element:focus"),
- "focuses the first [autofocus] element on the page"
- )
+ await expect(page.locator("#first-autofocus-element")).toBeFocused()
})
test("autofocus first [autofocus] element on visit", async ({ page }) => {
await page.goto("/src/tests/fixtures/navigation.html")
await page.click("#autofocus-link")
- await nextBeat()
-
- assert.ok(
- await hasSelector(page, "#first-autofocus-element:focus"),
- "focuses the first [autofocus] element on the page"
- )
- assert.notOk(
- await hasSelector(page, "#second-autofocus-element:focus"),
- "focuses the first [autofocus] element on the page"
- )
+ await expect(page.locator("#first-autofocus-element")).toBeFocused()
})
test("navigating a frame with a descendant link autofocuses [autofocus]:first-of-type", async ({ page }) => {
await page.click("#frame-inner-link")
- await nextBeat()
-
- assert.ok(
- await hasSelector(page, "#frames-form-first-autofocus-element:focus"),
- "focuses the first [autofocus] element in frame"
- )
- assert.notOk(
- await hasSelector(page, "#frames-form-second-autofocus-element:focus"),
- "focuses the first [autofocus] element in frame"
- )
+ await expect(page.locator("#frames-form-first-autofocus-element")).toBeFocused()
})
test("autofocus visible [autofocus] element on visit with inert elements", async ({ page }) => {
await page.click("#autofocus-inert-link")
- await nextBeat()
-
- assert.notOk(
- await hasSelector(page, "#dialog-autofocus-element:focus"),
- "autofocus element is ignored in a closed dialog"
- )
- assert.notOk(
- await hasSelector(page, "#details-autofocus-element:focus"),
- "autofocus element is ignored in a closed details"
- )
- assert.notOk(
- await hasSelector(page, "#hidden-autofocus-element:focus"),
- "autofocus element is ignored in a hidden div"
- )
- assert.notOk(
- await hasSelector(page, "#inert-autofocus-element:focus"),
- "autofocus element is ignored in an inert div"
- )
- assert.notOk(
- await hasSelector(page, "#disabled-autofocus-element:focus"),
- "autofocus element is ignored when disabled"
- )
- assert.ok(
- await hasSelector(page, "#visible-autofocus-element:focus"),
- "focuses the visible [autofocus] element on the page"
- )
+ await expect(page.locator("#visible-autofocus-element")).toBeFocused()
})
test("navigating a frame with a link targeting the frame autofocuses [autofocus]:first-of-type", async ({
page
}) => {
await page.click("#frame-outer-link")
- await nextBeat()
-
- assert.ok(
- await hasSelector(page, "#frames-form-first-autofocus-element:focus"),
- "focuses the first [autofocus] element in frame"
- )
- assert.notOk(
- await hasSelector(page, "#frames-form-second-autofocus-element:focus"),
- "focuses the first [autofocus] element in frame"
- )
+ await expect(page.locator("#frames-form-first-autofocus-element")).toBeFocused()
})
test("navigating a frame with a turbo-frame targeting the frame autofocuses [autofocus]:first-of-type", async ({
page
}) => {
await page.click("#drives-frame-target-link")
- await nextBeat()
-
- assert.ok(
- await hasSelector(page, "#frames-form-first-autofocus-element:focus"),
- "focuses the first [autofocus] element in frame"
- )
- assert.notOk(
- await hasSelector(page, "#frames-form-second-autofocus-element:focus"),
- "focuses the first [autofocus] element in frame"
- )
+ await expect(page.locator("#frames-form-first-autofocus-element")).toBeFocused()
})
test("receiving a Turbo Stream message with an [autofocus] element when the activeElement is the document", async ({ page }) => {
await page.evaluate(() => {
- if (document.activeElement instanceof HTMLElement) {
- document.activeElement.blur()
- }
+ document.activeElement.blur()
window.Turbo.renderStreamMessage(`
`)
})
- await nextBeat()
-
- assert.ok(
- await hasSelector(page, "#autofocus-from-stream:focus"),
- "focuses the [autofocus] element in from the turbo-stream"
- )
+ await expect(page.locator("#autofocus-from-stream")).toBeFocused()
})
test("autofocus from a Turbo Stream message does not leak a placeholder [id]", async ({ page }) => {
await page.evaluate(() => {
- if (document.activeElement instanceof HTMLElement) {
- document.activeElement.blur()
- }
+ document.activeElement.blur()
window.Turbo.renderStreamMessage(`
`)
})
- await nextBeat()
-
- assert.ok(
- await hasSelector(page, "#container-from-stream input:focus"),
- "focuses the [autofocus] element in from the turbo-stream"
- )
+ await expect(page.locator("#container-from-stream input")).toBeFocused()
})
test("receiving a Turbo Stream message with an [autofocus] element when an element within the document has focus", async ({ page }) => {
@@ -155,10 +70,5 @@ test("receiving a Turbo Stream message with an [autofocus] element when an eleme
`)
})
- await nextBeat()
-
- assert.ok(
- await hasSelector(page, "#first-autofocus-element:focus"),
- "focuses the first [autofocus] element on the page"
- )
+ await expect(page.locator("#first-autofocus-element")).toBeFocused()
})
diff --git a/src/tests/functional/link_prefetch_observer_tests.js b/src/tests/functional/link_prefetch_observer_tests.js
index a1b60c0f2..4d814fe5f 100644
--- a/src/tests/functional/link_prefetch_observer_tests.js
+++ b/src/tests/functional/link_prefetch_observer_tests.js
@@ -62,6 +62,37 @@ test("it doesn't prefetch the page when link has data-turbo=false", async ({ pag
await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_turbo_false" })
})
+test("allows to cancel prefetch requests with custom logic", async ({ page }) => {
+ await goTo({ page, path: "/hover_to_prefetch.html" })
+
+ await assertPrefetchedOnHover({ page, selector: "#anchor_for_prefetch" })
+
+ await page.evaluate(() => {
+ document.body.addEventListener("turbo:before-prefetch", (event) => {
+ if (event.target.hasAttribute("data-remote")) {
+ event.preventDefault()
+ }
+ })
+ })
+
+ await assertNotPrefetchedOnHover({ page, selector: "#anchor_for_prefetch" })
+})
+
+test("it doesn't prefetch UJS links", async ({ page }) => {
+ await goTo({ page, path: "/hover_to_prefetch.html" })
+ await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_remote_true" })
+})
+
+test("it doesn't prefetch data-turbo-stream links", async ({ page }) => {
+ await goTo({ page, path: "/hover_to_prefetch.html" })
+ await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_turbo_stream" })
+})
+
+test("it doesn't prefetch data-turbo-confirm links", async ({ page }) => {
+ await goTo({ page, path: "/hover_to_prefetch.html" })
+ await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_turbo_confirm" })
+})
+
test("it doesn't prefetch the page when link has the same location", async ({ page }) => {
await goTo({ page, path: "/hover_to_prefetch.html" })
await assertNotPrefetchedOnHover({ page, selector: "#anchor_for_same_location" })
@@ -137,17 +168,6 @@ test("it caches the request for 1 millisecond when turbo-prefetch-cache-time is
await assertPrefetchedOnHover({ page, selector: "#anchor_for_prefetch" })
})
-test("it adds text/vnd.turbo-stream.html header to the Accept header when link has data-turbo-stream", async ({
- page
-}) => {
- await goTo({ page, path: "/hover_to_prefetch.html" })
- await assertPrefetchedOnHover({ page, selector: "#anchor_with_turbo_stream", callback: (request) => {
- const headers = request.headers()["accept"].split(",").map((header) => header.trim())
-
- assert.includeMembers(headers, ["text/vnd.turbo-stream.html", "text/html", "application/xhtml+xml"])
- }})
-})
-
test("it prefetches links with inner elements", async ({ page }) => {
await goTo({ page, path: "/hover_to_prefetch.html" })
await assertPrefetchedOnHover({ page, selector: "#anchor_with_inner_elements" })
@@ -311,11 +331,6 @@ test("it resets the cache when a link is hovered", async ({ page }) => {
assert.equal(requestCount, 2)
})
-test("it prefetches page on touchstart", async ({ page }) => {
- await goTo({ page, path: "/hover_to_prefetch.html" })
- await assertPrefetchedOnTouchstart({ page, selector: "#anchor_for_prefetch" })
-})
-
test("it does not make a network request when clicking on a link that has been prefetched", async ({ page }) => {
await goTo({ page, path: "/hover_to_prefetch.html" })
await hoverSelector({ page, selector: "#anchor_for_prefetch" })
@@ -335,26 +350,6 @@ test("it follows the link using the cached response when clicking on a link that
assert.equal(await page.title(), "Prefetched Page")
})
-const assertPrefetchedOnTouchstart = async ({ page, selector, callback }) => {
- let requestMade = false
-
- page.on("request", (request) => {
- callback && callback(request)
- requestMade = true
- })
-
- const selectorXY = await page.$eval(selector, (el) => {
- const { x, y } = el.getBoundingClientRect()
- return { x, y }
- })
-
- await page.touchscreen.tap(selectorXY.x, selectorXY.y)
-
- await sleep(100)
-
- assertRequestMade(requestMade)
-}
-
const assertPrefetchedOnHover = async ({ page, selector, callback }) => {
let requestMade = false
diff --git a/src/tests/functional/loading_tests.js b/src/tests/functional/loading_tests.js
index f9c28f361..4390dfd57 100644
--- a/src/tests/functional/loading_tests.js
+++ b/src/tests/functional/loading_tests.js
@@ -119,17 +119,6 @@ test("navigating away from a page does not reload its frames", async ({ page })
assert.equal(requestLogs.length, 1)
})
-test("removing the [complete] attribute of an eager frame reloads the content", async ({ page }) => {
- await nextEventOnTarget(page, "frame", "turbo:frame-load")
- await page.evaluate(() => document.querySelector("#loading-eager turbo-frame")?.removeAttribute("complete"))
- await nextEventOnTarget(page, "frame", "turbo:frame-load")
-
- assert.ok(
- await hasSelector(page, "#loading-eager turbo-frame[complete]"),
- "sets the [complete] attribute after re-loading"
- )
-})
-
test("changing [src] attribute on a [complete] frame with loading=lazy defers navigation", async ({ page }) => {
await page.click("#loading-lazy summary")
await nextEventOnTarget(page, "hello", "turbo:frame-load")
diff --git a/src/tests/functional/morph_stream_action_tests.js b/src/tests/functional/morph_stream_action_tests.js
new file mode 100644
index 000000000..b4f04c9d7
--- /dev/null
+++ b/src/tests/functional/morph_stream_action_tests.js
@@ -0,0 +1,48 @@
+import { test, expect } from "@playwright/test"
+import { nextEventOnTarget, noNextEventOnTarget } from "../helpers/page"
+
+test("dispatches a turbo:before-morph-element & turbo:morph-element for each morph stream action", async ({ page }) => {
+ await page.goto("/src/tests/fixtures/morph_stream_action.html")
+
+ await page.evaluate(() => {
+ window.Turbo.renderStreamMessage(`
+
+
+
+
Morphed
+
+
+
+ `)
+ })
+
+ await nextEventOnTarget(page, "message_1", "turbo:before-morph-element")
+ await nextEventOnTarget(page, "message_1", "turbo:morph-element")
+ await expect(page.locator("#message_1")).toHaveText("Morphed")
+})
+
+test("preventing a turbo:before-morph-element prevents the morph", async ({ page }) => {
+ await page.goto("/src/tests/fixtures/morph_stream_action.html")
+
+ await page.evaluate(() => {
+ addEventListener("turbo:before-morph-element", (event) => {
+ event.preventDefault()
+ })
+ })
+
+ await page.evaluate(() => {
+ window.Turbo.renderStreamMessage(`
+
+
+
+
Morphed
+
+
+
+ `)
+ })
+
+ await nextEventOnTarget(page, "message_1", "turbo:before-morph-element")
+ await noNextEventOnTarget(page, "message_1", "turbo:morph-element")
+ await expect(page.locator("#message_1")).toHaveText("Morph me")
+})
diff --git a/src/tests/functional/navigation_tests.js b/src/tests/functional/navigation_tests.js
index cdbc90e22..e1aa7e757 100644
--- a/src/tests/functional/navigation_tests.js
+++ b/src/tests/functional/navigation_tests.js
@@ -379,6 +379,7 @@ test("following a redirection", async ({ page }) => {
await nextBody(page)
assert.equal(pathname(page.url()), "/src/tests/fixtures/one.html")
assert.equal(await visitAction(page), "replace")
+ await waitUntilNoSelector(page, ".turbo-progress-bar")
})
test("clicking the back button after redirection", async ({ page }) => {
diff --git a/src/tests/functional/page_refresh_tests.js b/src/tests/functional/page_refresh_tests.js
index 06a041787..c5c116c08 100644
--- a/src/tests/functional/page_refresh_tests.js
+++ b/src/tests/functional/page_refresh_tests.js
@@ -33,6 +33,7 @@ test("async page refresh with turbo-stream", async ({ page }) => {
await expect(page.locator("#title")).not.toHaveText("Updated")
await expect(page.locator("#title")).toHaveText("Page to be refreshed")
+ expect(await noNextEventNamed(page, "turbo:before-cache")).toBeTruthy()
})
test("dispatches a turbo:before-morph-element and turbo:morph-element event for each morphed element", async ({ page }) => {
@@ -183,6 +184,34 @@ test("frames marked with refresh='morph' are excluded from full page morphing",
await expect(page.locator("#refresh-morph")).toHaveText("Loaded morphed frame")
})
+test("navigated frames without refresh attribute are reset after morphing", async ({ page }) => {
+ await page.goto("/src/tests/fixtures/page_refresh.html")
+
+ await page.click("#refresh-after-navigation-link")
+
+ await nextBeat()
+
+ assert.ok(
+ await hasSelector(page, "#refresh-after-navigation-content"),
+ "navigates theframe"
+ )
+
+ await page.click("#form-submit")
+
+ await nextEventNamed(page, "turbo:render", { renderMethod: "morph" })
+ await nextBeat()
+
+ assert.ok(
+ await hasSelector(page, "#refresh-after-navigation-link"),
+ "resets the frame"
+ )
+
+ assert.notOk(
+ await hasSelector(page, "#refresh-after-navigation-content"),
+ "does not reload the frame"
+ )
+})
+
test("it preserves the scroll position when the turbo-refresh-scroll meta tag is 'preserve'", async ({ page }) => {
await page.goto("/src/tests/fixtures/page_refresh.html")
diff --git a/src/tests/unit/stream_element_tests.js b/src/tests/unit/stream_element_tests.js
index 21a9ca8aa..0d3e04b61 100644
--- a/src/tests/unit/stream_element_tests.js
+++ b/src/tests/unit/stream_element_tests.js
@@ -196,3 +196,45 @@ test("test action=refresh discarded when matching request id", async () => {
assert.ok(document.body.hasAttribute("data-modified"))
})
+
+test("action=morph", async () => {
+ const templateElement = createTemplateElement(`
Hello Turbo Morphed
`)
+ const element = createStreamElement("morph", "hello", templateElement)
+
+ assert.equal(subject.find("div#hello")?.textContent, "Hello Turbo")
+
+ subject.append(element)
+ await nextAnimationFrame()
+
+ assert.notOk(subject.find("div#hello"))
+ assert.equal(subject.find("h1#hello")?.textContent, "Hello Turbo Morphed")
+})
+
+test("action=morph with text content change", async () => {
+ const templateElement = createTemplateElement(`
Hello Turbo Morphed
`)
+ const element = createStreamElement("morph", "hello", templateElement)
+
+ assert.equal(subject.find("div#hello")?.textContent, "Hello Turbo")
+
+ subject.append(element)
+ await nextAnimationFrame()
+
+ assert.ok(subject.find("div#hello"))
+ assert.equal(subject.find("div#hello")?.textContent, "Hello Turbo Morphed")
+})
+
+test("action=morph children-only", async () => {
+ const templateElement = createTemplateElement(`
Hello Turbo Morphed
`)
+ const element = createStreamElement("morph", "hello", templateElement)
+ const target = subject.find("div#hello")
+ assert.equal(target?.textContent, "Hello Turbo")
+ element.setAttribute("children-only", true)
+
+ subject.append(element)
+
+ await nextAnimationFrame()
+
+ assert.ok(subject.find("div#hello"))
+ assert.ok(subject.find("div#hello > h1#hello-child-element"))
+ assert.equal(subject.find("div#hello > h1#hello-child-element").textContent, "Hello Turbo Morphed")
+})
diff --git a/yarn.lock b/yarn.lock
index 48b78418d..5b147bcc4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1916,9 +1916,9 @@ inherits@2.0.3:
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
ip@^1.1.5:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48"
- integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.9.tgz#8dfbcc99a754d07f425310b86a99546b1151e396"
+ integrity sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==
ipaddr.js@1.9.1:
version "1.9.1"