From 911e34725b7e9d4f95f0e472e9593ba59fe7f725 Mon Sep 17 00:00:00 2001 From: Danish-Belal Date: Sun, 15 Mar 2026 13:33:20 +0530 Subject: [PATCH 1/3] Fix findLinkFromClickTarget for SVG elements (fixes #1510) SVGAElement exposes href as SVGAnimatedString which does not implement startsWith(). Use getAttribute('href') which returns a string for both HTMLAnchorElement and SVGAElement. Add tests for clicking SVG links (same-origin and hash-only). Made-with: Cursor --- src/tests/fixtures/navigation.html | 1 + src/tests/functional/navigation_tests.js | 19 +++++++++++++++++++ src/util.js | 4 +++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/tests/fixtures/navigation.html b/src/tests/fixtures/navigation.html index 81e98793b..6fdfbd5f1 100644 --- a/src/tests/fixtures/navigation.html +++ b/src/tests/fixtures/navigation.html @@ -48,6 +48,7 @@

Navigation

Same-origin download link

Same-origin link inside SVG element Cross-origin link inside SVG element + SVG link with hash-only href

Same-origin data-turbo-method=get link

Same-origin data-turbo-action=replace link with post method

Disabled turbo-frame

diff --git a/src/tests/functional/navigation_tests.js b/src/tests/functional/navigation_tests.js index 25d11b887..ff55e0f01 100644 --- a/src/tests/functional/navigation_tests.js +++ b/src/tests/functional/navigation_tests.js @@ -310,6 +310,25 @@ test("following a cross-origin link inside an SVG element", async ({ page }) => expect(await visitAction(page)).toEqual("load") }) +test("clicking a same-origin link inside an SVG element", async ({ page }) => { + await page.click("#same-origin-link-inside-svg-element") + + await expect(page).toHaveURL(withPathname("/src/tests/fixtures/one.html")) + expect(await visitAction(page)).toEqual("load") +}) + +test("clicking an SVG link with hash-only href scrolls to anchor without a visit", async ({ page }) => { + expect( + await willChangeBody(page, async () => { + await page.click("#svg-hash-link") + }) + ).not.toBeTruthy() + + await expect(page).toHaveURL(withPathname("/src/tests/fixtures/navigation.html")) + await expect(page).toHaveURL(withHash("#main")) + expect(await isScrolledToSelector(page, "#main"), "scrolled to #main").toEqual(true) +}) + test("clicking the back button", async ({ page }) => { await page.click("#same-origin-unannotated-link") await nextEventNamed(page, "turbo:load") diff --git a/src/util.js b/src/util.js index 9a50cb387..167934703 100644 --- a/src/util.js +++ b/src/util.js @@ -253,7 +253,9 @@ export function findLinkFromClickTarget(target) { const link = findClosestRecursively(target, "a[href], a[xlink\\:href]") if (!link) return null - if (link.href.startsWith("#")) return null + // Use getAttribute for href check: SVGAElement exposes href as SVGAnimatedString (no startsWith) + const href = link.getAttribute("href") ?? link.getAttribute("xlink:href") ?? "" + if (href.startsWith("#")) return null if (link.hasAttribute("download")) return null const linkTarget = link.getAttribute("target") From 19b72300e644fff883d9c8d93346d3e1fea9a395 Mon Sep 17 00:00:00 2001 From: Danish-Belal Date: Sun, 15 Mar 2026 13:51:48 +0530 Subject: [PATCH 2/3] Also fix SVG href in prefetch observer (same bug as findLinkFromClickTarget) Add getLinkHrefString() helper and use in linkToTheSamePage to prevent TypeError when hovering SVG links with hash href. Add prefetch test. Made-with: Cursor --- src/observers/link_prefetch_observer.js | 5 +++-- src/tests/fixtures/hover_to_prefetch.html | 6 ++++++ src/tests/functional/link_prefetch_observer_tests.js | 5 +++++ src/util.js | 12 +++++++++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 5f5f66d43..9ce4567da 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -2,7 +2,8 @@ import { getLocationForLink } from "../core/url" import { dispatch, getMetaContent, - findClosestRecursively + findClosestRecursively, + getLinkHrefString } from "../util" import { FetchMethod, FetchRequest } from "../http/fetch_request" @@ -158,7 +159,7 @@ const unfetchableLink = (link) => { } const linkToTheSamePage = (link) => { - return (link.pathname + link.search === document.location.pathname + document.location.search) || link.href.startsWith("#") + return (link.pathname + link.search === document.location.pathname + document.location.search) || getLinkHrefString(link).startsWith("#") } const linkOptsOut = (link) => { diff --git a/src/tests/fixtures/hover_to_prefetch.html b/src/tests/fixtures/hover_to_prefetch.html index bd0f6f944..1873cb665 100644 --- a/src/tests/fixtures/hover_to_prefetch.html +++ b/src/tests/fixtures/hover_to_prefetch.html @@ -42,6 +42,12 @@ Hover to prefetch me Won't prefetch when hovering me + + + + SVG hash link + + Won't prefetch when hovering me Won't prefetch when hovering me diff --git a/src/tests/functional/link_prefetch_observer_tests.js b/src/tests/functional/link_prefetch_observer_tests.js index a40ea16dc..0782f1ac1 100644 --- a/src/tests/functional/link_prefetch_observer_tests.js +++ b/src/tests/functional/link_prefetch_observer_tests.js @@ -121,6 +121,11 @@ test("it doesn't prefetch the page when link has a hash as a href", async ({ pag await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_hash" }) }) +test("it doesn't prefetch when hovering SVG link with hash href", async ({ page }) => { + await goTo({ page, path: "/hover_to_prefetch.html" }) + await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_hash_inside_svg" }) +}) + test("it doesn't prefetch the page when link has a ftp protocol", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_ftp_protocol" }) diff --git a/src/util.js b/src/util.js index 167934703..7543ee85d 100644 --- a/src/util.js +++ b/src/util.js @@ -249,13 +249,19 @@ export function doesNotTargetIFrame(name) { } } +/** + * Returns href as string for both HTMLAnchorElement and SVGAElement. + * SVGAElement exposes href as SVGAnimatedString which has no startsWith(). + */ +export function getLinkHrefString(link) { + return link.getAttribute("href") ?? link.getAttribute("xlink:href") ?? "" +} + export function findLinkFromClickTarget(target) { const link = findClosestRecursively(target, "a[href], a[xlink\\:href]") if (!link) return null - // Use getAttribute for href check: SVGAElement exposes href as SVGAnimatedString (no startsWith) - const href = link.getAttribute("href") ?? link.getAttribute("xlink:href") ?? "" - if (href.startsWith("#")) return null + if (getLinkHrefString(link).startsWith("#")) return null if (link.hasAttribute("download")) return null const linkTarget = link.getAttribute("target") From d168a0d079816393dd0321e0909f3a3f9efc5522 Mon Sep 17 00:00:00 2001 From: Danish-Belal Date: Mon, 16 Mar 2026 23:11:39 +0530 Subject: [PATCH 3/3] Fix findLinkFromClickTarget for SVG elements (fixes #1510) --- src/tests/fixtures/navigation.html | 2 +- src/tests/functional/link_prefetch_observer_tests.js | 2 +- src/tests/functional/navigation_tests.js | 11 ++++------- src/util.js | 6 ++++-- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/tests/fixtures/navigation.html b/src/tests/fixtures/navigation.html index 6fdfbd5f1..15fd1e5af 100644 --- a/src/tests/fixtures/navigation.html +++ b/src/tests/fixtures/navigation.html @@ -48,7 +48,7 @@

Navigation

Same-origin download link

Same-origin link inside SVG element Cross-origin link inside SVG element - SVG link with hash-only href + SVG link with hash-only href

Same-origin data-turbo-method=get link

Same-origin data-turbo-action=replace link with post method

Disabled turbo-frame

diff --git a/src/tests/functional/link_prefetch_observer_tests.js b/src/tests/functional/link_prefetch_observer_tests.js index 0782f1ac1..520a6eeba 100644 --- a/src/tests/functional/link_prefetch_observer_tests.js +++ b/src/tests/functional/link_prefetch_observer_tests.js @@ -121,7 +121,7 @@ test("it doesn't prefetch the page when link has a hash as a href", async ({ pag await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_hash" }) }) -test("it doesn't prefetch when hovering SVG link with hash href", async ({ page }) => { +test("it doesn't prefetch the page when SVG link has a hash as a href", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" }) await assertNotPrefetchedOnHover({ page, selector: "#anchor_with_hash_inside_svg" }) }) diff --git a/src/tests/functional/navigation_tests.js b/src/tests/functional/navigation_tests.js index ff55e0f01..5017ca970 100644 --- a/src/tests/functional/navigation_tests.js +++ b/src/tests/functional/navigation_tests.js @@ -310,22 +310,19 @@ test("following a cross-origin link inside an SVG element", async ({ page }) => expect(await visitAction(page)).toEqual("load") }) -test("clicking a same-origin link inside an SVG element", async ({ page }) => { +test("following a same-origin SVG link", async ({ page }) => { await page.click("#same-origin-link-inside-svg-element") await expect(page).toHaveURL(withPathname("/src/tests/fixtures/one.html")) expect(await visitAction(page)).toEqual("load") }) -test("clicking an SVG link with hash-only href scrolls to anchor without a visit", async ({ page }) => { - expect( - await willChangeBody(page, async () => { - await page.click("#svg-hash-link") - }) - ).not.toBeTruthy() +test("following a same-origin SVG anchored link", async ({ page }) => { + await page.click("#same-origin-anchored-svg-link") await expect(page).toHaveURL(withPathname("/src/tests/fixtures/navigation.html")) await expect(page).toHaveURL(withHash("#main")) + expect(await visitAction(page)).toEqual("load") expect(await isScrolledToSelector(page, "#main"), "scrolled to #main").toEqual(true) }) diff --git a/src/util.js b/src/util.js index 7543ee85d..c9ce9362a 100644 --- a/src/util.js +++ b/src/util.js @@ -250,8 +250,10 @@ export function doesNotTargetIFrame(name) { } /** - * Returns href as string for both HTMLAnchorElement and SVGAElement. - * SVGAElement exposes href as SVGAnimatedString which has no startsWith(). + * Returns consistently the href attribute value as a string for both HTMLAnchorElement and SVGAElement. + * HTMLAnchorElement href property returns an absolute URL if the attribute contains a valid relative URL. + * SVGAElement exposes href as SVGAnimatedString which does not implement String methods. + * getAttribute() will return the proper value of the attribute in both cases. */ export function getLinkHrefString(link) { return link.getAttribute("href") ?? link.getAttribute("xlink:href") ?? ""