From 78fd9225ba67ff9b7fa1cfd68a1dc019f44caa70 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Sat, 1 Mar 2025 15:25:21 -0600 Subject: [PATCH 1/6] chore: Add nav types as devDep --- package-lock.json | 8 ++++++++ package.json | 1 + src/internal.d.ts | 1 + 3 files changed, 10 insertions(+) diff --git a/package-lock.json b/package-lock.json index b3ed775..9f90a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "chai": "^5.1.1", "htm": "^3.1.1", "kleur": "^4.1.5", + "navigation-api-types": "^0.6.1", "preact": "^10.26.5", "preact-render-to-string": "^6.5.11", "sinon": "^18.0.0", @@ -3028,6 +3029,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/navigation-api-types": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/navigation-api-types/-/navigation-api-types-0.6.1.tgz", + "integrity": "sha512-e1BbABfPRKLkBbZfAVuRFR2CLFWOtSt8e0ryJivjvLdw8yxD7ASPgyfl+klcGYvrPcP4zhOtZ4KpmQcEo1FgQw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "dev": true, diff --git a/package.json b/package.json index e1e61fa..e2f8847 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "chai": "^5.1.1", "htm": "^3.1.1", "kleur": "^4.1.5", + "navigation-api-types": "^0.6.1", "preact": "^10.26.5", "preact-render-to-string": "^6.5.11", "sinon": "^18.0.0", diff --git a/src/internal.d.ts b/src/internal.d.ts index eb1ca3a..b120caf 100644 --- a/src/internal.d.ts +++ b/src/internal.d.ts @@ -1,3 +1,4 @@ +/// import { Component } from 'preact'; export interface AugmentedComponent extends Component { From 895b644ac154e210da99018342c06ea3baca9f37 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Sat, 1 Mar 2025 18:14:32 -0600 Subject: [PATCH 2/6] refactor: Switch to Navigation API --- src/router.d.ts | 1 - src/router.js | 63 +++++++++++++------------------------------------ 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/router.d.ts b/src/router.d.ts index 1140aad..1d51195 100644 --- a/src/router.d.ts +++ b/src/router.d.ts @@ -43,7 +43,6 @@ interface LocationHook { path: string; pathParams: Record; searchParams: Record; - route: (url: string, replace?: boolean) => void; } export const useLocation: () => LocationHook; diff --git a/src/router.js b/src/router.js index 3161f63..511452a 100644 --- a/src/router.js +++ b/src/router.js @@ -7,8 +7,6 @@ import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact * @typedef {import('./internal.d.ts').VNode} VNode */ -/** @type {boolean} */ -let push; /** @type {string | RegExp | undefined} */ let scope; @@ -23,45 +21,23 @@ function isInScope(href) { ); } + /** * @param {string} state - * @param {MouseEvent | PopStateEvent | { url: string, replace?: boolean }} action + * @param {NavigateEvent} e */ -function handleNav(state, action) { - let url = ''; - push = undefined; - if (action && action.type === 'click') { - // ignore events the browser takes care of already: - if (action.ctrlKey || action.metaKey || action.altKey || action.shiftKey || action.button !== 0) { - return state; - } - - const link = action.composedPath().find(el => el.nodeName == 'A' && el.href), - href = link && link.getAttribute('href'); - if ( - !link || - link.origin != location.origin || - /^#/.test(href) || - !/^(_?self)?$/i.test(link.target) || - !isInScope(href) - ) { - return state; - } +function handleNav(state, e) { + if (!e.canIntercept) return state; + if (e.hashChange || e.downloadRequest !== null) return state; - push = true; - action.preventDefault(); - url = link.href.replace(location.origin, ''); - } else if (action && action.url) { - push = !action.replace; - url = action.url; - } else { - url = location.pathname + location.search; + const url = new URL(e.destination.url); + if (!isInScope(url.href)) { + return state; } - if (push === true) history.pushState(null, '', url); - else if (push === false) history.replaceState(null, '', url); - return url; -}; + e.intercept(); + return url.href.replace(url.origin, ''); +} export const exec = (url, route, matches = {}) => { url = url.split('/').filter(Boolean); @@ -99,7 +75,6 @@ export const exec = (url, route, matches = {}) => { export function LocationProvider(props) { const [url, route] = useReducer(handleNav, location.pathname + location.search); if (props.scope) scope = props.scope; - const wasPush = push === true; const value = useMemo(() => { const u = new URL(url, location.origin); @@ -110,18 +85,14 @@ export function LocationProvider(props) { path, pathParams: {}, searchParams: Object.fromEntries(u.searchParams), - route: (url, replace) => route({ url, replace }), - wasPush }; }, [url]); useLayoutEffect(() => { - addEventListener('click', route); - addEventListener('popstate', route); + navigation.addEventListener('navigate', route); return () => { - removeEventListener('click', route); - removeEventListener('popstate', route); + navigation.removeEventListener('navigate', route); }; }, []); @@ -133,7 +104,7 @@ const RESOLVED = Promise.resolve(); export function Router(props) { const [c, update] = useReducer(c => c + 1, 0); - const { url, path, pathParams, searchParams, wasPush } = useLocation(); + const { url, path, pathParams, searchParams } = useLocation(); if (!url) { throw new Error(`preact-iso's must be used within a , see: https://github.com/preactjs/preact-iso#locationprovider`); } @@ -257,7 +228,7 @@ export function Router(props) { // The route is loaded and rendered. if (prevRoute.current !== path) { - if (wasPush) scrollTo(0, 0); + scrollTo(0, 0); if (props.onRouteChange) props.onRouteChange(url); prevRoute.current = path; @@ -265,7 +236,7 @@ export function Router(props) { if (props.onLoadEnd && isLoading.current) props.onLoadEnd(url); isLoading.current = false; - }, [path, wasPush, c]); + }, [path, c]); // Note: cur MUST render first in order to set didSuspend & prev. return routeChanged @@ -282,7 +253,7 @@ const RenderRef = ({ r }) => r.current; Router.Provider = LocationProvider; LocationProvider.ctx = createContext( - /** @type {import('./router.d.ts').LocationHook & { wasPush: boolean }} */ ({}) + /** @type {import('./router.d.ts').LocationHook}} */ ({}) ); const RouterContext = createContext( /** @type {{ rest: string }} */ ({}) From 4f9a31311db97b5d49a64d079a72c60ea31fa721 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Sat, 1 Mar 2025 18:14:42 -0600 Subject: [PATCH 3/6] test: Progress, though still incomplete --- test/router.test.js | 125 +++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 78 deletions(-) diff --git a/test/router.test.js b/test/router.test.js index 8885731..239c69b 100644 --- a/test/router.test.js +++ b/test/router.test.js @@ -66,7 +66,8 @@ describe('Router', () => { scratch ); - loc.route('/a/'); + navigation.navigate('/a/'); + await sleep(1); expect(loc).to.deep.include({ @@ -195,7 +196,7 @@ describe('Router', () => { }); Home.resetHistory(); - loc.route('/profiles'); + navigation.navigate('/profiles'); await sleep(1); expect(scratch).to.have.property('textContent', 'Profiles'); @@ -211,7 +212,7 @@ describe('Router', () => { }); Profiles.resetHistory(); - loc.route('/profiles/bob'); + navigation.navigate('/profiles/bob'); await sleep(1); expect(scratch).to.have.property('textContent', 'Profile: bob'); @@ -229,7 +230,7 @@ describe('Router', () => { }); Profile.resetHistory(); - loc.route('/other?a=b&c=d'); + navigation.navigate('/other?a=b&c=d'); await sleep(1); expect(scratch).to.have.property('textContent', 'Fallback'); @@ -283,7 +284,7 @@ describe('Router', () => { expect(A).to.have.been.calledWith({ path: '/', searchParams: {}, pathParams: {} }); A.resetHistory(); - loc.route('/b'); + navigation.navigate('/b'); expect(scratch).to.have.property('innerHTML', '

A

hello

'); expect(A).not.to.have.been.called; @@ -303,18 +304,18 @@ describe('Router', () => { expect(B).to.have.been.calledWith({ path: '/b', searchParams: {}, pathParams: {} }); B.resetHistory(); - loc.route('/c'); - loc.route('/c?1'); - loc.route('/c'); + navigation.navigate('/c'); + navigation.navigate('/c?1'); + navigation.navigate('/c'); expect(scratch).to.have.property('innerHTML', '

B

hello

'); expect(B).not.to.have.been.called; await sleep(1); - loc.route('/c'); - loc.route('/c?2'); - loc.route('/c'); + navigation.navigate('/c'); + navigation.navigate('/c?2'); + navigation.navigate('/c'); expect(scratch).to.have.property('innerHTML', '

B

hello

'); // We should never re-invoke while loading (that would be a remount of the old route): @@ -332,7 +333,7 @@ describe('Router', () => { C.resetHistory(); B.resetHistory(); - loc.route('/b'); + navigation.navigate('/b'); await sleep(1); expect(scratch).to.have.property('innerHTML', '

B

hello

'); @@ -342,7 +343,7 @@ describe('Router', () => { A.resetHistory(); B.resetHistory(); - loc.route('/'); + navigation.navigate('/'); await sleep(1); expect(scratch).to.have.property('innerHTML', '

A

hello

'); @@ -386,21 +387,21 @@ describe('Router', () => { expect(renderRefCount).to.equal(2); renderRefCount = 0; - loc.route('/b/a'); + navigation.navigate('/b/a'); await sleep(10); expect(scratch).to.have.property('innerHTML', '

b/a

'); expect(renderRefCount).to.equal(4); renderRefCount = 0; - loc.route('/b/b'); + navigation.navigate('/b/b'); await sleep(10); expect(scratch).to.have.property('innerHTML', '

b/b

'); expect(renderRefCount).to.equal(1); renderRefCount = 0; - loc.route('/'); + navigation.navigate('/'); await sleep(10); expect(scratch).to.have.property('innerHTML', '

a

'); @@ -490,7 +491,8 @@ describe('Router', () => { loadEnd.resetHistory(); routeChange.resetHistory(); - loc.route('/b'); + navigation.navigate('/b'); + await sleep(1); expect(loadStart).to.have.been.calledWith('/b'); @@ -547,7 +549,7 @@ describe('Router', () => { expect(loadEnd).not.to.have.been.called; }); - describe('intercepted VS external links', () => { + describe.only('intercepted VS external links', () => { const shouldIntercept = [null, '', '_self', 'self', '_SELF']; const shouldNavigate = ['_top', '_parent', '_blank', 'custom', '_BLANK']; @@ -627,8 +629,6 @@ describe('Router', () => { const shouldIntercept = ['/app', '/app/deeper']; const shouldNavigate = ['/site', '/site/deeper']; - const clickHandler = sinon.fake(e => e.preventDefault()); - const Links = () => ( <> Internal Link @@ -638,23 +638,6 @@ describe('Router', () => { ); - let pushState; - - before(() => { - pushState = sinon.spy(history, 'pushState'); - addEventListener('click', clickHandler); - }); - - after(() => { - pushState.restore(); - removeEventListener('click', clickHandler); - }); - - beforeEach(async () => { - clickHandler.resetHistory(); - pushState.resetHistory(); - }); - it('should intercept clicks on links matching the `scope` props (string)', async () => { render( @@ -668,15 +651,10 @@ describe('Router', () => { scratch.querySelector(`a[href="${url}"]`).click(); await sleep(1); expect(loc).to.deep.include({ url }); - expect(pushState).to.have.been.calledWith(null, '', url); - expect(clickHandler).to.have.been.called; - - pushState.resetHistory(); - clickHandler.resetHistory(); } }); - it('should allow default browser navigation for links not matching the `scope` props (string)', async () => { + it.skip('should allow default browser navigation for links not matching the `scope` props (string)', async () => { render( @@ -688,11 +666,8 @@ describe('Router', () => { for (const url of shouldNavigate) { scratch.querySelector(`a[href="${url}"]`).click(); await sleep(1); - expect(pushState).not.to.have.been.called; - expect(clickHandler).to.have.been.called; - pushState.resetHistory(); - clickHandler.resetHistory(); + // TODO: How to test this? } }); @@ -709,15 +684,10 @@ describe('Router', () => { scratch.querySelector(`a[href="${url}"]`).click(); await sleep(1); expect(loc).to.deep.include({ url }); - expect(pushState).to.have.been.calledWith(null, '', url); - expect(clickHandler).to.have.been.called; - - pushState.resetHistory(); - clickHandler.resetHistory(); } }); - it('should allow default browser navigation for links not matching the `scope` props (regex)', async () => { + it.skip('should allow default browser navigation for links not matching the `scope` props (regex)', async () => { render( @@ -729,11 +699,8 @@ describe('Router', () => { for (const url of shouldNavigate) { scratch.querySelector(`a[href="${url}"]`).click(); await sleep(1); - expect(pushState).not.to.have.been.called; - expect(clickHandler).to.have.been.called; - pushState.resetHistory(); - clickHandler.resetHistory(); + // TODO: How to test this? } }); }); @@ -741,7 +708,14 @@ describe('Router', () => { it('should scroll to top when navigating forward', async () => { const scrollTo = sinon.spy(window, 'scrollTo'); - const Route = sinon.fake(() => ); + const Route = sinon.fake( + () => ( +
+ link +
+ ) + ); + render( @@ -756,7 +730,7 @@ describe('Router', () => { expect(Route).to.have.been.calledOnce; Route.resetHistory(); - loc.route('/programmatic'); + navigation.navigate('/programmatic'); await sleep(1); expect(loc).to.deep.include({ url: '/programmatic' }); @@ -779,14 +753,13 @@ describe('Router', () => { }); it('should ignore clicks on document fragment links', async () => { - const pushState = sinon.spy(history, 'pushState'); - const Route = sinon.fake( () => ); + render( @@ -799,7 +772,6 @@ describe('Router', () => { scratch ); - expect(Route).to.have.been.calledOnce; Route.resetHistory(); scratch.querySelector('a[href="#foo"]').click(); @@ -808,7 +780,6 @@ describe('Router', () => { // NOTE: we don't (currently) propagate in-page anchor navigations into context, to avoid useless renders. expect(loc).to.deep.include({ url: '/' }); expect(Route).not.to.have.been.called; - expect(pushState).not.to.have.been.called; expect(location.hash).to.equal('#foo'); scratch.querySelector('a[href="/other#bar"]').click(); @@ -816,14 +787,10 @@ describe('Router', () => { expect(Route).to.have.been.calledOnce; expect(loc).to.deep.include({ url: '/other#bar', path: '/other' }); - expect(pushState).to.have.been.called; expect(location.hash).to.equal('#bar'); - - pushState.restore(); }); it('should normalize children', async () => { - const pushState = sinon.spy(history, 'pushState'); const Route = sinon.fake(() => foo); const routes = ['/foo', '/bar']; @@ -846,9 +813,6 @@ describe('Router', () => { expect(Route).to.have.been.calledOnce; expect(loc).to.deep.include({ url: '/foo#foo', path: '/foo' }); - expect(pushState).to.have.been.called; - - pushState.restore(); }); it('should match nested routes', async () => { @@ -905,25 +869,30 @@ describe('Router', () => { }); it('should replace the current URL', async () => { - const pushState = sinon.spy(history, 'pushState'); - const replaceState = sinon.spy(history, 'replaceState'); - render( + null} /> null} /> + null} /> , scratch ); - loc.route("/foo", true); - expect(pushState).not.to.have.been.called; - expect(replaceState).to.have.been.calledWith(null, "", "/foo"); + navigation.navigate('/foo'); + navigation.navigate('/bar', { history: 'replace' }); + + const entries = navigation.entries(); + + // Top of the stack + const last = new URL(entries[entries.length - 1].url); + expect(last.pathname).to.equal('/bar'); - pushState.restore(); - replaceState.restore(); + // Entry before + const secondLast = new URL(entries[entries.length - 2].url); + expect(secondLast.pathname).to.equal('/'); }); it('should support using `Router` as an implicit suspense boundary', async () => { From 98974850317d4268d987ddfd94fc512b0c7fa90b Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Wed, 5 Mar 2025 02:36:06 -0600 Subject: [PATCH 4/6] refactor: Finish up for now --- src/router.js | 27 +++++--- test/router.test.js | 158 ++++++++++++++++++++++++-------------------- 2 files changed, 103 insertions(+), 82 deletions(-) diff --git a/src/router.js b/src/router.js index 511452a..685fa60 100644 --- a/src/router.js +++ b/src/router.js @@ -11,27 +11,36 @@ import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact let scope; /** - * @param {string} href + * @param {URL} url * @returns {boolean} */ -function isInScope(href) { +function isInScope(url) { return !scope || (typeof scope == 'string' - ? href.startsWith(scope) - : scope.test(href) + ? url.pathname.startsWith(scope) + : scope.test(url.pathname) ); } - /** * @param {string} state * @param {NavigateEvent} e */ function handleNav(state, e) { - if (!e.canIntercept) return state; - if (e.hashChange || e.downloadRequest !== null) return state; - + // TODO: Double-check this can't fail to parse. + // `.destination` is read-only, so I'm hoping it guarantees a valid URL. const url = new URL(e.destination.url); - if (!isInScope(url.href)) { + + if ( + !e.canIntercept || + e.hashChange || + e.downloadRequest !== null || + // Not yet implemented by Chrome, but coming? + //!/^(_?self)?$/i.test(/** @type {HTMLAnchorElement} */ (e.sourceElement).target) || + !isInScope(url) + ) { + // We only set this for our tests, it's otherwise very difficult to + // determine if a navigation was intercepted or not externally. + e['preact-iso-ignored'] = true; return state; } diff --git a/test/router.test.js b/test/router.test.js index 239c69b..bdfd002 100644 --- a/test/router.test.js +++ b/test/router.test.js @@ -1,5 +1,5 @@ import { h, Fragment, render, Component, hydrate, options } from 'preact'; -import { useState } from 'preact/hooks'; +import { useEffect, useState } from 'preact/hooks'; import * as chai from 'chai'; import * as sinon from 'sinon'; import sinonChai from 'sinon-chai'; @@ -549,14 +549,13 @@ describe('Router', () => { expect(loadEnd).not.to.have.been.called; }); - describe.only('intercepted VS external links', () => { + // TODO: Relies on upcoming property being added to navigation events + describe.skip('intercepted VS external links', () => { const shouldIntercept = [null, '', '_self', 'self', '_SELF']; const shouldNavigate = ['_top', '_parent', '_blank', 'custom', '_BLANK']; - const clickHandler = sinon.fake(e => e.preventDefault()); - - const Route = sinon.fake( - () =>
+ const Route = () => ( +
{[...shouldIntercept, ...shouldNavigate].map((target, i) => { const url = '/' + i + '/' + target; if (target === null) return target = {target + ''}; @@ -565,31 +564,32 @@ describe('Router', () => {
); - let pushState; - - before(() => { - pushState = sinon.spy(history, 'pushState'); - addEventListener('click', clickHandler); - }); - - after(() => { - pushState.restore(); - removeEventListener('click', clickHandler); - }); + let triedToNavigate = false; + const handler = (e) => { + e.intercept(); + if (e['preact-iso-ignored']) { + triedToNavigate = true; + } + } beforeEach(async () => { - render( - - - - - - , - scratch - ); - Route.resetHistory(); - clickHandler.resetHistory(); - pushState.resetHistory(); + const App = () => { + useEffect(() => { + navigation.addEventListener('navigate', handler); + return () => navigation.removeEventListener('navigate', handler); + }, []); + + return ( + + + + + + + ); + } + render(, scratch); + await sleep(10); }); const getName = target => (target == null ? 'no target attribute' : `target="${target}"`); @@ -604,9 +604,9 @@ describe('Router', () => { el.click(); await sleep(1); expect(loc).to.deep.include({ url }); - expect(Route).to.have.been.calledOnce; - expect(pushState).to.have.been.calledWith(null, '', url); - expect(clickHandler).to.have.been.called; + expect(triedToNavigate).to.be.false; + + triedToNavigate = false; }); } @@ -618,9 +618,9 @@ describe('Router', () => { if (!el) throw Error(`Unable to find link: ${sel}`); el.click(); await sleep(1); - expect(Route).not.to.have.been.called; - expect(pushState).not.to.have.been.called; - expect(clickHandler).to.have.been.called; + expect(triedToNavigate).to.be.true; + + triedToNavigate = false; }); } }); @@ -638,69 +638,81 @@ describe('Router', () => { ); - it('should intercept clicks on links matching the `scope` props (string)', async () => { - render( - - - - , - scratch - ); + let triedToNavigate = false; + const handler = (e) => { + e.intercept(); + if (e['preact-iso-ignored']) { + triedToNavigate = true; + } + } + + it('should support the `scope` prop (string)', async () => { + const App = () => { + useEffect(() => { + navigation.addEventListener('navigate', handler); + return () => navigation.removeEventListener('navigate', handler); + }, []); + + return ( + + + + + ); + } + render(, scratch); + await sleep(10); for (const url of shouldIntercept) { scratch.querySelector(`a[href="${url}"]`).click(); await sleep(1); expect(loc).to.deep.include({ url }); - } - }); + expect(triedToNavigate).to.be.false; - it.skip('should allow default browser navigation for links not matching the `scope` props (string)', async () => { - render( - - - - , - scratch - ); + triedToNavigate = false; + } for (const url of shouldNavigate) { scratch.querySelector(`a[href="${url}"]`).click(); await sleep(1); + expect(triedToNavigate).to.be.true; - // TODO: How to test this? + triedToNavigate = false; } }); - it('should intercept clicks on links matching the `scope` props (regex)', async () => { - render( - - - - , - scratch - ); + it('should support the `scope` prop (regex)', async () => { + const App = () => { + useEffect(() => { + navigation.addEventListener('navigate', handler); + return () => navigation.removeEventListener('navigate', handler); + }, []); + + return ( + + + + + ); + } + render(, scratch); + await sleep(10); for (const url of shouldIntercept) { scratch.querySelector(`a[href="${url}"]`).click(); await sleep(1); expect(loc).to.deep.include({ url }); - } - }); + expect(triedToNavigate).to.be.false; - it.skip('should allow default browser navigation for links not matching the `scope` props (regex)', async () => { - render( - - - - , - scratch - ); + triedToNavigate = false; + } for (const url of shouldNavigate) { scratch.querySelector(`a[href="${url}"]`).click(); await sleep(1); + expect(triedToNavigate).to.be.true; - // TODO: How to test this? + triedToNavigate = false; } }); }); From 4a823d7a4c4e0a69ff257bf3ab8ff5cbf24b5d07 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Wed, 5 Mar 2025 17:58:48 -0600 Subject: [PATCH 5/6] test: Add tests for forwards & backwards nav --- test/router.test.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/router.test.js b/test/router.test.js index bdfd002..de58636 100644 --- a/test/router.test.js +++ b/test/router.test.js @@ -946,6 +946,34 @@ describe('Router', () => { expect(scratch).to.have.property('textContent', 'data'); }); + it('should support navigating backwards and forwards', async () => { + render( + + + null} /> + null} /> + + + , + scratch + ); + + navigation.navigate('/foo'); + await sleep(10); + + expect(loc).to.deep.include({ url: '/foo', path: '/foo', searchParams: {} }); + + navigation.back(); + await sleep(10); + + expect(loc).to.deep.include({ url: '/', path: '/', searchParams: {} }); + + navigation.forward(); + await sleep(10); + + expect(loc).to.deep.include({ url: '/foo', path: '/foo', searchParams: {} }); + }); + it('should intercept clicks on links inside open shadow DOM', async () => { const shadowlink = document.createElement('a'); shadowlink.href = '/shadow'; From d401af050c51f4c865e617fe05720c776306efe1 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Sun, 10 Aug 2025 18:13:38 -0500 Subject: [PATCH 6/6] test: Correct shadow DOM link test --- test/router.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/router.test.js b/test/router.test.js index de58636..52b3045 100644 --- a/test/router.test.js +++ b/test/router.test.js @@ -978,7 +978,6 @@ describe('Router', () => { const shadowlink = document.createElement('a'); shadowlink.href = '/shadow'; shadowlink.textContent = 'Shadow Link'; - shadowlink.addEventListener('click', e => e.preventDefault()); const attachShadow = (el) => { if (!el || el.shadowRoot) return;