Skip to content

Commit 16a7e61

Browse files
committed
refactor: Finish up for now
1 parent 03b3bd1 commit 16a7e61

File tree

2 files changed

+108
-80
lines changed

2 files changed

+108
-80
lines changed

src/router.js

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,37 @@ import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact
1010
/** @type {string | RegExp | undefined} */
1111
let scope;
1212

13+
/**
14+
* @param {URL} url
15+
* @returns {boolean}
16+
*/
17+
function isInScope(url) {
18+
return !scope || (typeof scope == 'string'
19+
? url.pathname.startsWith(scope)
20+
: scope.test(url.pathname)
21+
);
22+
}
23+
1324
/**
1425
* @param {string} state
1526
* @param {NavigateEvent} e
1627
*/
1728
function handleNav(state, e) {
18-
if (!e.canIntercept) return state;
19-
if (e.hashChange || e.downloadRequest !== null) return state;
20-
29+
// TODO: Double-check this can't fail to parse.
30+
// `.destination` is read-only, so I'm hoping it guarantees a valid URL.
2131
const url = new URL(e.destination.url);
32+
2233
if (
23-
scope && (typeof scope == 'string'
24-
? !url.pathname.startsWith(scope)
25-
: !scope.test(url.pathname)
26-
)
34+
!e.canIntercept ||
35+
e.hashChange ||
36+
e.downloadRequest !== null ||
37+
// Not yet implemented by Chrome, but coming?
38+
//!/^(_?self)?$/i.test(/** @type {HTMLAnchorElement} */ (e.sourceElement).target) ||
39+
!isInScope(url)
2740
) {
41+
// We only set this for our tests, it's otherwise very difficult to
42+
// determine if a navigation was intercepted or not externally.
43+
e['preact-iso-ignored'] = true;
2844
return state;
2945
}
3046

test/router.test.js

Lines changed: 85 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { h, Fragment, render, hydrate, options } from 'preact';
2-
import { useState } from 'preact/hooks';
2+
import { useEffect, useState } from 'preact/hooks';
33
import * as chai from 'chai';
44
import * as sinon from 'sinon';
55
import sinonChai from 'sinon-chai';
@@ -510,14 +510,13 @@ describe('Router', () => {
510510
expect(loadEnd).not.to.have.been.called;
511511
});
512512

513-
describe.only('intercepted VS external links', () => {
513+
// TODO: Relies on upcoming property being added to navigation events
514+
describe.skip('intercepted VS external links', () => {
514515
const shouldIntercept = [null, '', '_self', 'self', '_SELF'];
515516
const shouldNavigate = ['_top', '_parent', '_blank', 'custom', '_BLANK'];
516517

517-
const clickHandler = sinon.fake(e => e.preventDefault());
518-
519-
const Route = sinon.fake(
520-
() => <div>
518+
const Route = () => (
519+
<div>
521520
{[...shouldIntercept, ...shouldNavigate].map((target, i) => {
522521
const url = '/' + i + '/' + target;
523522
if (target === null) return <a href={url}>target = {target + ''}</a>;
@@ -526,31 +525,32 @@ describe('Router', () => {
526525
</div>
527526
);
528527

529-
let pushState;
530-
531-
before(() => {
532-
pushState = sinon.spy(history, 'pushState');
533-
addEventListener('click', clickHandler);
534-
});
535-
536-
after(() => {
537-
pushState.restore();
538-
removeEventListener('click', clickHandler);
539-
});
528+
let triedToNavigate = false;
529+
const handler = (e) => {
530+
e.intercept();
531+
if (e['preact-iso-ignored']) {
532+
triedToNavigate = true;
533+
}
534+
}
540535

541536
beforeEach(async () => {
542-
render(
543-
<LocationProvider>
544-
<Router>
545-
<Route default />
546-
</Router>
547-
<ShallowLocation />
548-
</LocationProvider>,
549-
scratch
550-
);
551-
Route.resetHistory();
552-
clickHandler.resetHistory();
553-
pushState.resetHistory();
537+
const App = () => {
538+
useEffect(() => {
539+
navigation.addEventListener('navigate', handler);
540+
return () => navigation.removeEventListener('navigate', handler);
541+
}, []);
542+
543+
return (
544+
<LocationProvider>
545+
<Router>
546+
<Route default />
547+
</Router>
548+
<ShallowLocation />
549+
</LocationProvider>
550+
);
551+
}
552+
render(<App />, scratch);
553+
await sleep(10);
554554
});
555555

556556
const getName = target => (target == null ? 'no target attribute' : `target="${target}"`);
@@ -565,9 +565,9 @@ describe('Router', () => {
565565
el.click();
566566
await sleep(1);
567567
expect(loc).to.deep.include({ url });
568-
expect(Route).to.have.been.calledOnce;
569-
expect(pushState).to.have.been.calledWith(null, '', url);
570-
expect(clickHandler).to.have.been.called;
568+
expect(triedToNavigate).to.be.false;
569+
570+
triedToNavigate = false;
571571
});
572572
}
573573

@@ -579,9 +579,9 @@ describe('Router', () => {
579579
if (!el) throw Error(`Unable to find link: ${sel}`);
580580
el.click();
581581
await sleep(1);
582-
expect(Route).not.to.have.been.called;
583-
expect(pushState).not.to.have.been.called;
584-
expect(clickHandler).to.have.been.called;
582+
expect(triedToNavigate).to.be.true;
583+
584+
triedToNavigate = false;
585585
});
586586
}
587587
});
@@ -599,69 +599,81 @@ describe('Router', () => {
599599
</>
600600
);
601601

602-
it('should intercept clicks on links matching the `scope` props (string)', async () => {
603-
render(
604-
<LocationProvider scope="/app">
605-
<Links />
606-
<ShallowLocation />
607-
</LocationProvider>,
608-
scratch
609-
);
602+
let triedToNavigate = false;
603+
const handler = (e) => {
604+
e.intercept();
605+
if (e['preact-iso-ignored']) {
606+
triedToNavigate = true;
607+
}
608+
}
609+
610+
it('should support the `scope` prop (string)', async () => {
611+
const App = () => {
612+
useEffect(() => {
613+
navigation.addEventListener('navigate', handler);
614+
return () => navigation.removeEventListener('navigate', handler);
615+
}, []);
616+
617+
return (
618+
<LocationProvider scope="/app">
619+
<Links />
620+
<ShallowLocation />
621+
</LocationProvider>
622+
);
623+
}
624+
render(<App />, scratch);
625+
await sleep(10);
610626

611627
for (const url of shouldIntercept) {
612628
scratch.querySelector(`a[href="${url}"]`).click();
613629
await sleep(1);
614630
expect(loc).to.deep.include({ url });
615-
}
616-
});
631+
expect(triedToNavigate).to.be.false;
617632

618-
it.skip('should allow default browser navigation for links not matching the `scope` props (string)', async () => {
619-
render(
620-
<LocationProvider scope="app">
621-
<Links />
622-
<ShallowLocation />
623-
</LocationProvider>,
624-
scratch
625-
);
633+
triedToNavigate = false;
634+
}
626635

627636
for (const url of shouldNavigate) {
628637
scratch.querySelector(`a[href="${url}"]`).click();
629638
await sleep(1);
639+
expect(triedToNavigate).to.be.true;
630640

631-
// TODO: How to test this?
641+
triedToNavigate = false;
632642
}
633643
});
634644

635-
it('should intercept clicks on links matching the `scope` props (regex)', async () => {
636-
render(
637-
<LocationProvider scope={/^\/app/}>
638-
<Links />
639-
<ShallowLocation />
640-
</LocationProvider>,
641-
scratch
642-
);
645+
it('should support the `scope` prop (regex)', async () => {
646+
const App = () => {
647+
useEffect(() => {
648+
navigation.addEventListener('navigate', handler);
649+
return () => navigation.removeEventListener('navigate', handler);
650+
}, []);
651+
652+
return (
653+
<LocationProvider scope={/^\/app/}>
654+
<Links />
655+
<ShallowLocation />
656+
</LocationProvider>
657+
);
658+
}
659+
render(<App />, scratch);
660+
await sleep(10);
643661

644662
for (const url of shouldIntercept) {
645663
scratch.querySelector(`a[href="${url}"]`).click();
646664
await sleep(1);
647665
expect(loc).to.deep.include({ url });
648-
}
649-
});
666+
expect(triedToNavigate).to.be.false;
650667

651-
it.skip('should allow default browser navigation for links not matching the `scope` props (regex)', async () => {
652-
render(
653-
<LocationProvider scope={/^\/app/}>
654-
<Links />
655-
<ShallowLocation />
656-
</LocationProvider>,
657-
scratch
658-
);
668+
triedToNavigate = false;
669+
}
659670

660671
for (const url of shouldNavigate) {
661672
scratch.querySelector(`a[href="${url}"]`).click();
662673
await sleep(1);
674+
expect(triedToNavigate).to.be.true;
663675

664-
// TODO: How to test this?
676+
triedToNavigate = false;
665677
}
666678
});
667679
});

0 commit comments

Comments
 (0)