diff --git a/src/plugins/router/router.js b/src/plugins/router/router.js index 37e143c..3a1c5af 100644 --- a/src/plugins/router/router.js +++ b/src/plugins/router/router.js @@ -137,7 +137,8 @@ export default function({ $data, addScopeToNode, directive, magic, reactive }) { || e.shiftKey || e.defaultPrevented || e.button > 0 - || is_blank) { + || is_blank + || is_external(link)) { return; } @@ -191,7 +192,7 @@ export default function({ $data, addScopeToNode, directive, magic, reactive }) { // if (link) { - return router.history.resolve(link.href) === router.values.path; + return !is_external(link) && router.history.resolve(link.href) === router.values.path; } if (el._r_routerlink_init) { @@ -215,6 +216,16 @@ function is_anchor_element(el) { return el.tagName.toUpperCase() === "A"; } +// +// A link to another origin or a non-HTTP scheme, such as "mailto:" or "tel:", +// leads outside the application and is left to the browser. +// The protocol and host are compared instead of the origin, +// which browsers report differently for file: URLs ("null" or "file://"). +// +function is_external(link) { + return link.protocol !== location.protocol || link.host !== location.host; +} + function get_anchor_element(el) { if (is_anchor_element(el)) { return el; diff --git a/tests/playwright/assets/page.html b/tests/playwright/assets/page.html index b7bbe68..e39714e 100644 --- a/tests/playwright/assets/page.html +++ b/tests/playwright/assets/page.html @@ -20,6 +20,7 @@ + diff --git a/tests/playwright/x-router.spec.js b/tests/playwright/x-router.spec.js new file mode 100644 index 0000000..d2250d3 --- /dev/null +++ b/tests/playwright/x-router.spec.js @@ -0,0 +1,60 @@ +import { expect, test } from "@playwright/test"; +import { set_html } from "./assets/utils"; + +// The page is opened from a file: URL, where the html5 history cannot change the path, +// so the tests use the hash history + +// Records whether the router took over the click, and keeps the browser from following the link +const capture_clicks = page => page.evaluate(() => { + document.addEventListener("click", e => { + window.prevented = e.defaultPrevented; + e.preventDefault(); + }); +}); + +test.describe("x-router: link", () => { + test("external links are left to the browser", async ({ page }) => { + await set_html(page, ` +
+ Example + Mail + Phone +
`); + + await capture_clicks(page); + + for (let link of await page.locator("a").all()) { + await link.click(); + expect(await page.evaluate(() => window.prevented)).toBe(false); + expect(await page.evaluate(() => location.hash)).toBe(""); + } + }); + + test("internal link is handled by the router", async ({ page }) => { + await set_html(page, ` +
+ About +
`); + + await capture_clicks(page); + + await page.locator("a").click(); + expect(await page.evaluate(() => window.prevented)).toBe(true); + expect(await page.evaluate(() => location.hash)).toBe("#/about"); + }); +}); + +test.describe("x-router: $active", () => { + test("external link with the current path is not active", async ({ page }) => { + await set_html(page, ` +
+ + Home + Example +
`); + + // Wait until the router activates the current route + await expect(page.locator("#home")).toHaveClass("active"); + await expect(page.locator("#example")).not.toHaveClass("active"); + }); +});