summaryrefslogtreecommitdiff
path: root/preact-router/test
diff options
context:
space:
mode:
Diffstat (limited to 'preact-router/test')
-rw-r--r--preact-router/test/dist.js39
-rw-r--r--preact-router/test/dom.js293
-rw-r--r--preact-router/test/index.js249
-rw-r--r--preact-router/test/match.tsx26
-rw-r--r--preact-router/test/router.tsx36
-rw-r--r--preact-router/test/tsconfig.json13
-rw-r--r--preact-router/test/util.js110
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' });
+ });
+ });
+});