diff options
Diffstat (limited to 'preact-router/test')
-rw-r--r-- | preact-router/test/dist.js | 39 | ||||
-rw-r--r-- | preact-router/test/dom.js | 293 | ||||
-rw-r--r-- | preact-router/test/index.js | 249 | ||||
-rw-r--r-- | preact-router/test/match.tsx | 26 | ||||
-rw-r--r-- | preact-router/test/router.tsx | 36 | ||||
-rw-r--r-- | preact-router/test/tsconfig.json | 13 | ||||
-rw-r--r-- | preact-router/test/util.js | 110 |
7 files changed, 766 insertions, 0 deletions
diff --git a/preact-router/test/dist.js b/preact-router/test/dist.js new file mode 100644 index 0000000..0017e6f --- /dev/null +++ b/preact-router/test/dist.js @@ -0,0 +1,39 @@ +import { h } from 'preact'; +import assertCloneOf from '../test_helpers/assert-clone-of'; + +const router = require('../'); +const { Router, Link, route } = router; + +chai.use(assertCloneOf); + +describe('dist', () => { + it('should export Router, Link and route', () => { + expect(Router).to.be.a('function'); + expect(Link).to.be.a('function'); + expect(route).to.be.a('function'); + expect(router).to.equal(Router); + }); + + describe('Router', () => { + it('should be instantiable', () => { + let router = new Router({}); + let children = [ + <foo path="/" />, + <foo path="/foo" />, + <foo path="/foo/bar" /> + ]; + + expect( + router.render({ children }, { url:'/foo' }) + ).to.be.cloneOf(children[1]); + + expect( + router.render({ children }, { url:'/' }) + ).to.be.cloneOf(children[0]); + + expect( + router.render({ children }, { url:'/foo/bar' }) + ).to.be.cloneOf(children[2]); + }); + }); +}); diff --git a/preact-router/test/dom.js b/preact-router/test/dom.js new file mode 100644 index 0000000..ccec629 --- /dev/null +++ b/preact-router/test/dom.js @@ -0,0 +1,293 @@ +import { Router, Link, route } from 'src'; +import { Match, Link as ActiveLink } from 'src/match'; +import { h, render } from 'preact'; +import { act } from 'preact/test-utils'; + +const Empty = () => null; + +function fireEvent(on, type) { + let e = document.createEvent('Event'); + e.initEvent(type, true, true); + on.dispatchEvent(e); +} + +describe('dom', () => { + let scratch, $, mount; + + before( () => { + scratch = document.createElement('div'); + document.body.appendChild(scratch); + $ = s => scratch.querySelector(s); + mount = jsx => {render(jsx, scratch); return scratch.lastChild;}; + }); + + beforeEach( () => { + // manually reset the URL before every test + history.replaceState(null, null, '/'); + fireEvent(window, 'popstate'); + }); + + afterEach( () => { + mount(<Empty />); + scratch.innerHTML = ''; + }); + + after( () => { + document.body.removeChild(scratch); + }); + + describe('<Link />', () => { + it('should render a normal link', () => { + expect( + mount(<Link href="/foo" bar="baz">hello</Link>).outerHTML + ).to.eql( + mount(<a href="/foo" bar="baz">hello</a>).outerHTML + ); + }); + + it('should route when clicked', () => { + let onChange = sinon.spy(); + mount( + <div> + <Link href="/foo">foo</Link> + <Router onChange={onChange}> + <div default /> + </Router> + </div> + ); + onChange.resetHistory(); + act(() => { + $('a').click(); + }); + expect(onChange) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch({ url:'/foo' }); + }); + }); + + describe('<a>', () => { + it('should route for existing routes', () => { + let onChange = sinon.spy(); + mount( + <div> + <a href="/foo">foo</a> + <Router onChange={onChange}> + <div default /> + </Router> + </div> + ); + onChange.resetHistory(); + act(() => { + $('a').click(); + }); + // fireEvent($('a'), 'click'); + expect(onChange) + .to.have.been.calledOnce + .and.to.have.been.calledWithMatch({ url:'/foo' }); + }); + + it('should not intercept non-preact elements', () => { + let onChange = sinon.spy(); + mount( + <div> + <div dangerouslySetInnerHTML={{ __html: `<a href="#foo">foo</a>` }} /> + <Router onChange={onChange}> + <div default /> + </Router> + </div> + ); + onChange.resetHistory(); + act(() => { + $('a').click(); + }); + expect(onChange).not.to.have.been.called; + expect(location.href).to.contain('#foo'); + }); + }); + + describe('Router', () => { + it('should add and remove children', () => { + class A { + componentWillMount() {} + componentWillUnmount() {} + render(){ return <div />; } + } + sinon.spy(A.prototype, 'componentWillMount'); + sinon.spy(A.prototype, 'componentWillUnmount'); + mount( + <Router> + <A path="/foo" /> + </Router> + ); + expect(A.prototype.componentWillMount).not.to.have.been.called; + act(() => { + route('/foo'); + }); + expect(A.prototype.componentWillMount).to.have.been.calledOnce; + expect(A.prototype.componentWillUnmount).not.to.have.been.called; + act(() => { + route('/bar'); + }); + expect(A.prototype.componentWillMount).to.have.been.calledOnce; + expect(A.prototype.componentWillUnmount).to.have.been.calledOnce; + }); + + it('should support re-routing', done => { + class A { + componentWillMount() { + route('/b'); + } + render(){ return <div class="a" />; } + } + class B { + componentWillMount(){} + render(){ return <div class="b" />; } + } + sinon.spy(A.prototype, 'componentWillMount'); + sinon.spy(B.prototype, 'componentWillMount'); + mount( + <Router> + <A path="/a" /> + <B path="/b" /> + </Router> + ); + expect(A.prototype.componentWillMount).not.to.have.been.called; + act(() => { + route('/a'); + }); + expect(A.prototype.componentWillMount).to.have.been.calledOnce; + A.prototype.componentWillMount.resetHistory(); + expect(location.pathname).to.equal('/b'); + setTimeout( () => { + expect(A.prototype.componentWillMount).not.to.have.been.called; + expect(B.prototype.componentWillMount).to.have.been.calledOnce; + expect(scratch).to.have.deep.property('firstElementChild.className', 'b'); + done(); + }, 10); + }); + + it('should not carry over the previous value of a query parameter', () => { + class A { + render({ bar }){ return <p>bar is {bar}</p>; } + } + let routerRef; + mount( + <Router ref={r => routerRef = r}> + <A path="/foo" /> + </Router> + ); + act(() => { + route('/foo'); + }); + expect(routerRef.base.outerHTML).to.eql('<p>bar is </p>'); + act(() => { + route('/foo?bar=5'); + }); + expect(routerRef.base.outerHTML).to.eql('<p>bar is 5</p>'); + act(() => { + route('/foo'); + }); + expect(routerRef.base.outerHTML).to.eql('<p>bar is </p>'); + }); + }); + + describe('preact-router/match', () => { + describe('<Match>', () => { + it('should invoke child function with match status when routing', done => { + let spy1 = sinon.spy(), + spy2 = sinon.spy(), + spy3 = sinon.spy(); + mount( + <div> + <Router /> + <Match path="/foo">{spy1}</Match> + <Match path="/bar">{spy2}</Match> + <Match path="/bar/:param">{spy3}</Match> + </div> + ); + + expect(spy1, 'spy1 /foo').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/', url:'/' }); + expect(spy2, 'spy2 /foo').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/', url:'/' }); + expect(spy3, 'spy3 /foo').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/', url:'/' }); + + spy1.resetHistory(); + spy2.resetHistory(); + spy3.resetHistory(); + + route('/foo'); + + setTimeout( () => { + expect(spy1, 'spy1 /foo').to.have.been.calledOnce.and.calledWithMatch({ matches: true, path:'/foo', url:'/foo' }); + expect(spy2, 'spy2 /foo').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/foo', url:'/foo' }); + expect(spy3, 'spy3 /foo').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/foo', url:'/foo' }); + spy1.resetHistory(); + spy2.resetHistory(); + spy3.resetHistory(); + + route('/foo?bar=5'); + + setTimeout( () => { + expect(spy1, 'spy1 /foo?bar=5').to.have.been.calledOnce.and.calledWithMatch({ matches: true, path:'/foo', url:'/foo?bar=5' }); + expect(spy2, 'spy2 /foo?bar=5').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/foo', url:'/foo?bar=5' }); + expect(spy3, 'spy3 /foo?bar=5').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/foo', url:'/foo?bar=5' }); + spy1.resetHistory(); + spy2.resetHistory(); + spy3.resetHistory(); + + route('/bar'); + + setTimeout( () => { + expect(spy1, 'spy1 /bar').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/bar', url:'/bar' }); + expect(spy2, 'spy2 /bar').to.have.been.calledOnce.and.calledWithMatch({ matches: true, path:'/bar', url:'/bar' }); + expect(spy3, 'spy3 /bar').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/bar', url:'/bar' }); + spy1.resetHistory(); + spy2.resetHistory(); + spy3.resetHistory(); + + route('/bar/123'); + + setTimeout( () => { + expect(spy1, 'spy1 /bar/123').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/bar/123', url:'/bar/123' }); + expect(spy2, 'spy2 /bar/123').to.have.been.calledOnce.and.calledWithMatch({ matches: false, path:'/bar/123', url:'/bar/123' }); + expect(spy3, 'spy3 /bar/123').to.have.been.calledOnce.and.calledWithMatch({ matches: true, path:'/bar/123', url:'/bar/123' }); + + done(); + }, 20); + }, 20); + }, 20); + }, 20); + }); + }); + + describe('<Link>', () => { + it('should render with active class when active', done => { + mount( + <div> + <Router /> + <ActiveLink activeClassName="active" path="/foo">foo</ActiveLink> + <ActiveLink activeClassName="active" class="bar" path="/bar">bar</ActiveLink> + </div> + ); + route('/foo'); + + setTimeout( () => { + expect(scratch.innerHTML).to.eql('<div><a class="active">foo</a><a class="bar">bar</a></div>'); + + route('/foo?bar=5'); + + setTimeout( () => { + expect(scratch.innerHTML).to.eql('<div><a class="active">foo</a><a class="bar">bar</a></div>'); + + route('/bar'); + + setTimeout( () => { + expect(scratch.innerHTML).to.eql('<div><a class="">foo</a><a class="bar active">bar</a></div>'); + + done(); + }); + }); + }); + }); + }); + }); +}); diff --git a/preact-router/test/index.js b/preact-router/test/index.js new file mode 100644 index 0000000..842eecb --- /dev/null +++ b/preact-router/test/index.js @@ -0,0 +1,249 @@ +import { Router, Link, route } from 'src'; +import { h, render } from 'preact'; +import assertCloneOf from '../test_helpers/assert-clone-of'; + +chai.use(assertCloneOf); + +describe('preact-router', () => { + it('should export Router, Link and route', () => { + expect(Router).to.be.a('function'); + expect(Link).to.be.a('function'); + expect(route).to.be.a('function'); + }); + + describe('Router', () => { + let scratch; + let router; + + beforeEach(() => { + scratch = document.createElement('div'); + document.body.appendChild(scratch); + }); + + afterEach(() => { + document.body.removeChild(scratch); + router.componentWillUnmount(); + }); + + it('should filter children based on URL', () => { + let children = [ + <foo path="/" />, + <foo path="/foo" />, + <foo path="/foo/bar" /> + ]; + + render( + ( + <Router ref={ref => (router = ref)}> + {children} + </Router> + ), + scratch + ); + + expect( + router.render({ children }, { url:'/foo' }) + ).to.be.cloneOf(children[1]); + + expect( + router.render({ children }, { url:'/' }) + ).to.be.cloneOf(children[0]); + + expect( + router.render({ children }, { url:'/foo/bar' }) + ).to.be.cloneOf(children[2]); + }); + + it('should support nested parameterized routes', () => { + let children = [ + <foo path="/foo" />, + <foo path="/foo/:bar" />, + <foo path="/foo/:bar/:baz" /> + ]; + + render( + ( + <Router ref={ref => (router = ref)}> + {children} + </Router> + ), + scratch + ); + + + expect( + router.render({ children }, { url:'/foo' }) + ).to.be.cloneOf(children[0]); + + expect( + router.render({ children }, { url:'/foo/bar' }) + ).to.be.cloneOf(children[1], { matches: { bar:'bar' }, url:'/foo/bar' }); + + expect( + router.render({ children }, { url:'/foo/bar/baz' }) + ).be.cloneOf(children[2], { matches: { bar:'bar', baz:'baz' }, url:'/foo/bar/baz' }); + }); + + it('should support default routes', () => { + let children = [ + <foo default />, + <foo path="/" />, + <foo path="/foo" /> + ]; + + render( + ( + <Router ref={ref => (router = ref)}> + {children} + </Router> + ), + scratch + ); + + expect( + router.render({ children }, { url:'/foo' }) + ).to.be.cloneOf(children[2]); + + expect( + router.render({ children }, { url:'/' }) + ).to.be.cloneOf(children[1]); + + expect( + router.render({ children }, { url:'/asdf/asdf' }) + ).to.be.cloneOf(children[0], { matches: {}, url:'/asdf/asdf' }); + }); + + it('should support initial route prop', () => { + let children = [ + <foo default />, + <foo path="/" />, + <foo path="/foo" /> + ]; + + render( + ( + <Router url="/foo" ref={ref => (router = ref)}> + {children} + </Router> + ), + scratch + ); + + expect( + router.render({ children }, router.state) + ).to.be.cloneOf(children[2]); + + render(null, scratch); + + render( + ( + <Router ref={ref => (router = ref)}> + {children} + </Router> + ), + scratch + ); + + expect(router).to.have.deep.property('state.url', location.pathname + (location.search || '')); + }); + + it('should support custom history', () => { + let push = sinon.spy(); + let replace = sinon.spy(); + let listen = sinon.spy(); + let getCurrentLocation = sinon.spy(() => ({pathname: '/initial'})); + + let children = [ + <index path="/" />, + <foo path="/foo" />, + <bar path="/bar" /> + ]; + + render( + ( + <Router history={{ push, replace, getCurrentLocation, listen }} ref={ref => (router = ref)}> + {children} + </Router> + ), + scratch + ); + + router.componentWillMount(); + + router.render(router.props, router.state); + expect(getCurrentLocation, 'getCurrentLocation').to.have.been.calledOnce; + expect(router).to.have.deep.property('state.url', '/initial'); + + route('/foo'); + expect(push, 'push').to.have.been.calledOnce.and.calledWith('/foo'); + + route('/bar', true); + expect(replace, 'replace').to.have.been.calledOnce.and.calledWith('/bar'); + + router.componentWillUnmount(); + }); + }); + + describe('route()', () => { + let router; + let scratch; + + beforeEach(() => { + scratch = document.createElement('div'); + document.body.appendChild(scratch); + + render( + ( + <Router url="/foo" ref={ref => (router = ref)}> + <foo path="/" /> + <foo path="/foo" /> + </Router> + ), + scratch + ); + + sinon.spy(router, 'routeTo'); + }); + + afterEach(() => { + router.componentWillUnmount(); + document.body.removeChild(scratch); + }); + + it('should return true for existing route', () => { + router.routeTo.resetHistory(); + expect(route('/')).to.equal(true); + expect(router.routeTo) + .to.have.been.calledOnce + .and.calledWithExactly('/'); + + router.routeTo.resetHistory(); + expect(route('/foo')).to.equal(true); + expect(router.routeTo) + .to.have.been.calledOnce + .and.calledWithExactly('/foo'); + }); + + it('should return false for missing route', () => { + router.routeTo.resetHistory(); + expect(route('/asdf')).to.equal(false); + expect(router.routeTo) + .to.have.been.calledOnce + .and.calledWithExactly('/asdf'); + }); + + it('should return true for fallback route', () => { + let oldChildren = router.props.children; + router.props.children = [ + <foo default />, + ...oldChildren + ]; + + router.routeTo.resetHistory(); + expect(route('/asdf')).to.equal(true); + expect(router.routeTo) + .to.have.been.calledOnce + .and.calledWithExactly('/asdf'); + }); + }); +}); diff --git a/preact-router/test/match.tsx b/preact-router/test/match.tsx new file mode 100644 index 0000000..8e1c175 --- /dev/null +++ b/preact-router/test/match.tsx @@ -0,0 +1,26 @@ +import { h } from 'preact'; +import { Link, RoutableProps } from '../'; +import { Match } from '../match'; + +function ChildComponent({}: {}) { + return <div></div>; +} + +function LinkComponent({}: {}) { + return ( + <div> + <Link href="/a" /> + <Link activeClassName="active" href="/b" /> + </div> + ); +} + +function MatchComponent({}: {}) { + return ( + <Match path="/b"> + {({ matches, path, url }) => matches && ( + <ChildComponent /> + )} + </Match> + ); +}
\ No newline at end of file diff --git a/preact-router/test/router.tsx b/preact-router/test/router.tsx new file mode 100644 index 0000000..fceeea5 --- /dev/null +++ b/preact-router/test/router.tsx @@ -0,0 +1,36 @@ +import { h, render, Component, FunctionalComponent } from 'preact'; +import Router, { Route, RoutableProps } from '../'; + +class ClassComponent extends Component<{}, {}> { + render() { + return <div></div>; + } +} + +const SomeFunctionalComponent: FunctionalComponent<{}> = ({}) => { + return <div></div>; +}; + +function RouterWithComponents() { + return ( + <Router> + <div default></div> + <ClassComponent default /> + <SomeFunctionalComponent default /> + <div path="/a"></div> + <ClassComponent path="/b" /> + <SomeFunctionalComponent path="/c" /> + </Router> + ) +} + +function RouterWithRoutes() { + return ( + <Router> + <Route default component={ClassComponent} /> + <Route default component={SomeFunctionalComponent} /> + <Route path="/a" component={ClassComponent} /> + <Route path="/b" component={SomeFunctionalComponent} /> + </Router> + ); +}
\ No newline at end of file diff --git a/preact-router/test/tsconfig.json b/preact-router/test/tsconfig.json new file mode 100644 index 0000000..4096c8c --- /dev/null +++ b/preact-router/test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "noEmit": true, + "jsx": "react", + "jsxFactory": "h" + }, + "files": [ + "router.tsx", + "match.tsx", + "../src/index.d.ts", + "../src/match.d.ts" + ] +}
\ No newline at end of file diff --git a/preact-router/test/util.js b/preact-router/test/util.js new file mode 100644 index 0000000..69921b7 --- /dev/null +++ b/preact-router/test/util.js @@ -0,0 +1,110 @@ +import { exec, pathRankSort, prepareVNodeForRanking, segmentize, rank } from 'src/util'; + +const strip = str => segmentize(str).join('/'); + +describe('util', () => { + describe('strip', () => { + it('should strip preceeding slashes', () => { + expect(strip('')).to.equal(''); + expect(strip('/')).to.equal(''); + expect(strip('/a')).to.equal('a'); + expect(strip('//a')).to.equal('a'); + expect(strip('//a/')).to.equal('a'); + }); + + it('should strip trailing slashes', () => { + expect(strip('')).to.equal(''); + expect(strip('/')).to.equal(''); + expect(strip('a/')).to.equal('a'); + expect(strip('/a//')).to.equal('a'); + }); + }); + + describe('rank', () => { + it('should return rank of path segments', () => { + expect(rank('')).to.equal('5'); + expect(rank('/')).to.equal('5'); + expect(rank('//')).to.equal('5'); + expect(rank('a/b/c')).to.equal('555'); + expect(rank('/a/b/c/')).to.equal('555'); + expect(rank('/:a/b?/:c?/:d*/:e+')).to.eql('45312'); + }); + }); + + describe('segmentize', () => { + it('should split path on slashes', () => { + expect(segmentize('')).to.eql(['']); + expect(segmentize('/')).to.eql(['']); + expect(segmentize('//')).to.eql(['']); + expect(segmentize('a/b/c')).to.eql(['a','b','c']); + expect(segmentize('/a/b/c/')).to.eql(['a','b','c']); + }); + }); + + describe('pathRankSort', () => { + it('should sort by highest rank first', () => { + let paths = arr => arr.map( path => ({ props:{path}} ) ); + let clean = vnode => { delete vnode.rank; delete vnode.index; return vnode; }; + + expect( + paths(['/:a*', '/a', '/:a+', '/:a?', '/a/:b*']).filter(prepareVNodeForRanking).sort(pathRankSort).map(clean) + ).to.eql( + paths(['/a/:b*', '/a', '/:a?', '/:a+', '/:a*']) + ); + }); + + it('should return default routes last', () => { + let paths = arr => arr.map( path => ({props:{path}}) ); + let clean = vnode => { delete vnode.rank; delete vnode.index; return vnode; }; + + let defaultPath = {props:{default:true}}; + let p = paths(['/a/b/', '/a/b', '/', 'b']); + p.splice(2,0,defaultPath); + + expect( + p.filter(prepareVNodeForRanking).sort(pathRankSort).map(clean) + ).to.eql( + paths(['/a/b/', '/a/b', '/', 'b']).concat(defaultPath) + ); + }); + }); + + describe('exec', () => { + it('should match explicit equality', () => { + expect(exec('/','/', {})).to.eql({}); + expect(exec('/a', '/a', {})).to.eql({}); + expect(exec('/a', '/b', {})).to.eql(false); + expect(exec('/a/b', '/a/b', {})).to.eql({}); + expect(exec('/a/b', '/a/a', {})).to.eql(false); + expect(exec('/a/b', '/b/b', {})).to.eql(false); + }); + + it('should match param segments', () => { + expect(exec('/', '/:foo', {})).to.eql(false); + expect(exec('/bar', '/:foo', {})).to.eql({ foo:'bar' }); + }); + + it('should match optional param segments', () => { + expect(exec('/', '/:foo?', {})).to.eql({ foo:'' }); + expect(exec('/bar', '/:foo?', {})).to.eql({ foo:'bar' }); + expect(exec('/', '/:foo?/:bar?', {})).to.eql({ foo:'', bar:'' }); + expect(exec('/bar', '/:foo?/:bar?', {})).to.eql({ foo:'bar', bar:'' }); + expect(exec('/bar', '/:foo?/bar', {})).to.eql(false); + expect(exec('/foo/bar', '/:foo?/bar', {})).to.eql({ foo:'foo' }); + }); + + it('should match splat param segments', () => { + expect(exec('/', '/:foo*', {})).to.eql({ foo:'' }); + expect(exec('/a', '/:foo*', {})).to.eql({ foo:'a' }); + expect(exec('/a/b', '/:foo*', {})).to.eql({ foo:'a/b' }); + expect(exec('/a/b/c', '/:foo*', {})).to.eql({ foo:'a/b/c' }); + }); + + it('should match required splat param segments', () => { + expect(exec('/', '/:foo+', {})).to.eql(false); + expect(exec('/a', '/:foo+', {})).to.eql({ foo:'a' }); + expect(exec('/a/b', '/:foo+', {})).to.eql({ foo:'a/b' }); + expect(exec('/a/b/c', '/:foo+', {})).to.eql({ foo:'a/b/c' }); + }); + }); +}); |