Skip to content

Commit 9897485

Browse files
committed
refactor: Finish up for now
1 parent 4f9a313 commit 9897485

File tree

2 files changed

+103
-82
lines changed

2 files changed

+103
-82
lines changed

src/router.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,36 @@ import { useContext, useMemo, useReducer, useLayoutEffect, useRef } from 'preact
1111
let scope;
1212

1313
/**
14-
* @param {string} href
14+
* @param {URL} url
1515
* @returns {boolean}
1616
*/
17-
function isInScope(href) {
17+
function isInScope(url) {
1818
return !scope || (typeof scope == 'string'
19-
? href.startsWith(scope)
20-
: scope.test(href)
19+
? url.pathname.startsWith(scope)
20+
: scope.test(url.pathname)
2121
);
2222
}
2323

24-
2524
/**
2625
* @param {string} state
2726
* @param {NavigateEvent} e
2827
*/
2928
function handleNav(state, e) {
30-
if (!e.canIntercept) return state;
31-
if (e.hashChange || e.downloadRequest !== null) return state;
32-
29+
// TODO: Double-check this can't fail to parse.
30+
// `.destination` is read-only, so I'm hoping it guarantees a valid URL.
3331
const url = new URL(e.destination.url);
34-
if (!isInScope(url.href)) {
32+
33+
if (
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)
40+
) {
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;
3544
return state;
3645
}
3746

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, Component, 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';
@@ -549,14 +549,13 @@ describe('Router', () => {
549549
expect(loadEnd).not.to.have.been.called;
550550
});
551551

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

556-
const clickHandler = sinon.fake(e => e.preventDefault());
557-
558-
const Route = sinon.fake(
559-
() => <div>
557+
const Route = () => (
558+
<div>
560559
{[...shouldIntercept, ...shouldNavigate].map((target, i) => {
561560
const url = '/' + i + '/' + target;
562561
if (target === null) return <a href={url}>target = {target + ''}</a>;
@@ -565,31 +564,32 @@ describe('Router', () => {
565564
</div>
566565
);
567566

568-
let pushState;
569-
570-
before(() => {
571-
pushState = sinon.spy(history, 'pushState');
572-
addEventListener('click', clickHandler);
573-
});
574-
575-
after(() => {
576-
pushState.restore();
577-
removeEventListener('click', clickHandler);
578-
});
567+
let triedToNavigate = false;
568+
const handler = (e) => {
569+
e.intercept();
570+
if (e['preact-iso-ignored']) {
571+
triedToNavigate = true;
572+
}
573+
}
579574

580575
beforeEach(async () => {
581-
render(
582-
<LocationProvider>
583-
<Router>
584-
<Route default />
585-
</Router>
586-
<ShallowLocation />
587-
</LocationProvider>,
588-
scratch
589-
);
590-
Route.resetHistory();
591-
clickHandler.resetHistory();
592-
pushState.resetHistory();
576+
const App = () => {
577+
useEffect(() => {
578+
navigation.addEventListener('navigate', handler);
579+
return () => navigation.removeEventListener('navigate', handler);
580+
}, []);
581+
582+
return (
583+
<LocationProvider>
584+
<Router>
585+
<Route default />
586+
</Router>
587+
<ShallowLocation />
588+
</LocationProvider>
589+
);
590+
}
591+
render(<App />, scratch);
592+
await sleep(10);
593593
});
594594

595595
const getName = target => (target == null ? 'no target attribute' : `target="${target}"`);
@@ -604,9 +604,9 @@ describe('Router', () => {
604604
el.click();
605605
await sleep(1);
606606
expect(loc).to.deep.include({ url });
607-
expect(Route).to.have.been.calledOnce;
608-
expect(pushState).to.have.been.calledWith(null, '', url);
609-
expect(clickHandler).to.have.been.called;
607+
expect(triedToNavigate).to.be.false;
608+
609+
triedToNavigate = false;
610610
});
611611
}
612612

@@ -618,9 +618,9 @@ describe('Router', () => {
618618
if (!el) throw Error(`Unable to find link: ${sel}`);
619619
el.click();
620620
await sleep(1);
621-
expect(Route).not.to.have.been.called;
622-
expect(pushState).not.to.have.been.called;
623-
expect(clickHandler).to.have.been.called;
621+
expect(triedToNavigate).to.be.true;
622+
623+
triedToNavigate = false;
624624
});
625625
}
626626
});
@@ -638,69 +638,81 @@ describe('Router', () => {
638638
</>
639639
);
640640

641-
it('should intercept clicks on links matching the `scope` props (string)', async () => {
642-
render(
643-
<LocationProvider scope="/app">
644-
<Links />
645-
<ShallowLocation />
646-
</LocationProvider>,
647-
scratch
648-
);
641+
let triedToNavigate = false;
642+
const handler = (e) => {
643+
e.intercept();
644+
if (e['preact-iso-ignored']) {
645+
triedToNavigate = true;
646+
}
647+
}
648+
649+
it('should support the `scope` prop (string)', async () => {
650+
const App = () => {
651+
useEffect(() => {
652+
navigation.addEventListener('navigate', handler);
653+
return () => navigation.removeEventListener('navigate', handler);
654+
}, []);
655+
656+
return (
657+
<LocationProvider scope="/app">
658+
<Links />
659+
<ShallowLocation />
660+
</LocationProvider>
661+
);
662+
}
663+
render(<App />, scratch);
664+
await sleep(10);
649665

650666
for (const url of shouldIntercept) {
651667
scratch.querySelector(`a[href="${url}"]`).click();
652668
await sleep(1);
653669
expect(loc).to.deep.include({ url });
654-
}
655-
});
670+
expect(triedToNavigate).to.be.false;
656671

657-
it.skip('should allow default browser navigation for links not matching the `scope` props (string)', async () => {
658-
render(
659-
<LocationProvider scope="app">
660-
<Links />
661-
<ShallowLocation />
662-
</LocationProvider>,
663-
scratch
664-
);
672+
triedToNavigate = false;
673+
}
665674

666675
for (const url of shouldNavigate) {
667676
scratch.querySelector(`a[href="${url}"]`).click();
668677
await sleep(1);
678+
expect(triedToNavigate).to.be.true;
669679

670-
// TODO: How to test this?
680+
triedToNavigate = false;
671681
}
672682
});
673683

674-
it('should intercept clicks on links matching the `scope` props (regex)', async () => {
675-
render(
676-
<LocationProvider scope={/^\/app/}>
677-
<Links />
678-
<ShallowLocation />
679-
</LocationProvider>,
680-
scratch
681-
);
684+
it('should support the `scope` prop (regex)', async () => {
685+
const App = () => {
686+
useEffect(() => {
687+
navigation.addEventListener('navigate', handler);
688+
return () => navigation.removeEventListener('navigate', handler);
689+
}, []);
690+
691+
return (
692+
<LocationProvider scope={/^\/app/}>
693+
<Links />
694+
<ShallowLocation />
695+
</LocationProvider>
696+
);
697+
}
698+
render(<App />, scratch);
699+
await sleep(10);
682700

683701
for (const url of shouldIntercept) {
684702
scratch.querySelector(`a[href="${url}"]`).click();
685703
await sleep(1);
686704
expect(loc).to.deep.include({ url });
687-
}
688-
});
705+
expect(triedToNavigate).to.be.false;
689706

690-
it.skip('should allow default browser navigation for links not matching the `scope` props (regex)', async () => {
691-
render(
692-
<LocationProvider scope={/^\/app/}>
693-
<Links />
694-
<ShallowLocation />
695-
</LocationProvider>,
696-
scratch
697-
);
707+
triedToNavigate = false;
708+
}
698709

699710
for (const url of shouldNavigate) {
700711
scratch.querySelector(`a[href="${url}"]`).click();
701712
await sleep(1);
713+
expect(triedToNavigate).to.be.true;
702714

703-
// TODO: How to test this?
715+
triedToNavigate = false;
704716
}
705717
});
706718
});

0 commit comments

Comments
 (0)