Skip to content

Commit d2893d7

Browse files
rmacklindevelopit
authored andcommitted
Avoid in-place mutation of the attributes of Router's children (#169)
* Include test_helpers directory in lint task * Declare chai global in eslint config * Create assert-clone-of chai plugin for use in tests * Update tests to use assertCloneOf * Add test to verify that query parameters aren't carried over from previous route * Avoid in-place mutation of the attributes of Router's children All credit for this change goes to @developit (who sent it over Slack)
1 parent 13e44be commit d2893d7

File tree

7 files changed

+62
-24
lines changed

7 files changed

+62
-24
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
}
1818
},
1919
"globals": {
20+
"chai": true,
2021
"sinon": true,
2122
"expect": true
2223
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"minify": "uglifyjs $npm_package_main -cm -o $npm_package_minified_main -p relative --in-source-map ${npm_package_main}.map --source-map ${npm_package_minified_main}.map",
1616
"size": "size=$(gzip-size $npm_package_minified_main) && echo \"gzip size: $size / $(pretty-bytes $size)\"",
1717
"test": "npm-run-all lint build test:karma",
18-
"lint": "eslint {src,test}",
18+
"lint": "eslint {src,test,test_helpers}",
1919
"test:karma": "karma start --single-run",
2020
"test:watch": "karma start",
2121
"prepublish": "npm-run-all build test",

src/index.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { h, Component } from 'preact';
1+
import { cloneElement, h, Component } from 'preact';
22
import { exec, pathRankSort } from './util';
33

44
let customHistory = null;
@@ -207,23 +207,24 @@ class Router extends Component {
207207
}
208208

209209
getMatchingChildren(children, url, invoke) {
210-
return children.slice().sort(pathRankSort).filter( ({ attributes }) => {
211-
let path = attributes.path,
212-
matches = exec(url, path, attributes);
210+
return children.slice().sort(pathRankSort).map( vnode => {
211+
let path = vnode.attributes.path,
212+
matches = exec(url, path, vnode.attributes);
213213
if (matches) {
214214
if (invoke!==false) {
215-
attributes.url = url;
216-
attributes.matches = matches;
215+
let newProps = { url, matches };
217216
// copy matches onto props
218217
for (let i in matches) {
219218
if (matches.hasOwnProperty(i)) {
220-
attributes[i] = matches[i];
219+
newProps[i] = matches[i];
221220
}
222221
}
222+
return cloneElement(vnode, newProps);
223223
}
224-
return true;
224+
return vnode;
225225
}
226-
});
226+
return false;
227+
}).filter(Boolean);
227228
}
228229

229230
render({ children, onChange }, { url }) {

test/dist.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { h } from 'preact';
2+
import assertCloneOf from '../test_helpers/assert-clone-of';
3+
24
const router = require('../');
35
const { Router, Link, route } = router;
46

7+
chai.use(assertCloneOf);
8+
59
describe('dist', () => {
610
it('should export Router, Link and route', () => {
711
expect(Router).to.be.a('function');
@@ -21,15 +25,15 @@ describe('dist', () => {
2125

2226
expect(
2327
router.render({ children }, { url:'/foo' })
24-
).to.equal(children[1]);
28+
).to.be.cloneOf(children[1]);
2529

2630
expect(
2731
router.render({ children }, { url:'/' })
28-
).to.equal(children[0]);
32+
).to.be.cloneOf(children[0]);
2933

3034
expect(
3135
router.render({ children }, { url:'/foo/bar' })
32-
).to.equal(children[2]);
36+
).to.be.cloneOf(children[2]);
3337
});
3438
});
3539
});

test/dom.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,23 @@ describe('dom', () => {
151151
done();
152152
}, 10);
153153
});
154+
155+
it('should not carry over the previous value of a query parameter', () => {
156+
class A {
157+
render({ bar }){ return <p>bar is {bar}</p>; }
158+
}
159+
let routerRef;
160+
mount(
161+
<Router ref={r => routerRef = r}>
162+
<A path="/foo" />
163+
</Router>
164+
);
165+
route('/foo');
166+
expect(routerRef.base.outerHTML).to.eql('<p>bar is </p>');
167+
route('/foo?bar=5');
168+
expect(routerRef.base.outerHTML).to.eql('<p>bar is 5</p>');
169+
route('/foo');
170+
expect(routerRef.base.outerHTML).to.eql('<p>bar is </p>');
171+
});
154172
});
155173
});

test/index.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { Router, Link, route } from 'src';
22
import { h } from 'preact';
3+
import assertCloneOf from '../test_helpers/assert-clone-of';
4+
5+
chai.use(assertCloneOf);
36

47
describe('preact-router', () => {
58
it('should export Router, Link and route', () => {
@@ -19,15 +22,15 @@ describe('preact-router', () => {
1922

2023
expect(
2124
router.render({ children }, { url:'/foo' })
22-
).to.equal(children[1]);
25+
).to.be.cloneOf(children[1]);
2326

2427
expect(
2528
router.render({ children }, { url:'/' })
26-
).to.equal(children[0]);
29+
).to.be.cloneOf(children[0]);
2730

2831
expect(
2932
router.render({ children }, { url:'/foo/bar' })
30-
).to.equal(children[2]);
33+
).to.be.cloneOf(children[2]);
3134
});
3235

3336
it('should support nested parameterized routes', () => {
@@ -40,16 +43,15 @@ describe('preact-router', () => {
4043

4144
expect(
4245
router.render({ children }, { url:'/foo' })
43-
).to.equal(children[0]);
46+
).to.be.cloneOf(children[0]);
4447

4548
expect(
4649
router.render({ children }, { url:'/foo/bar' })
47-
).to.equal(children[1]).and.have.deep.property('attributes.bar', 'bar');
50+
).to.be.cloneOf(children[1], { matches: { bar:'bar' }, url:'/foo/bar' });
4851

4952
expect(
5053
router.render({ children }, { url:'/foo/bar/baz' })
51-
).equal(children[2]).and.have.deep.property('attributes')
52-
.which.contains.all.keys({ bar:'bar', baz:'baz' });
54+
).be.cloneOf(children[2], { matches: { bar:'bar', baz:'baz' }, url:'/foo/bar/baz' });
5355
});
5456

5557
it('should support default routes', () => {
@@ -62,15 +64,15 @@ describe('preact-router', () => {
6264

6365
expect(
6466
router.render({ children }, { url:'/foo' })
65-
).to.equal(children[2]);
67+
).to.be.cloneOf(children[2]);
6668

6769
expect(
6870
router.render({ children }, { url:'/' })
69-
).to.equal(children[1]);
71+
).to.be.cloneOf(children[1]);
7072

7173
expect(
7274
router.render({ children }, { url:'/asdf/asdf' })
73-
).to.equal(children[0]);
75+
).to.be.cloneOf(children[0], { matches: {}, url:'/asdf/asdf' });
7476
});
7577

7678
it('should support initial route prop', () => {
@@ -83,7 +85,7 @@ describe('preact-router', () => {
8385

8486
expect(
8587
router.render({ children }, router.state)
86-
).to.equal(children[2]);
88+
).to.be.cloneOf(children[2]);
8789

8890
expect(new Router({})).to.have.deep.property('state.url', location.pathname + (location.search || ''));
8991
});

test_helpers/assert-clone-of.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { cloneElement } from 'preact';
2+
3+
export default function assertCloneOf({ Assertion }) {
4+
if (Assertion.__assertCloneOfMounted === true) return;
5+
Assertion.__assertCloneOfMounted = true;
6+
7+
Assertion.addMethod('cloneOf', function(routeJsx, { matches = {}, url = this._obj.attributes.path } = {}) {
8+
const vnode = this._obj;
9+
const clonedRoute = cloneElement(routeJsx, { url, matches, ...matches });
10+
new chai.Assertion(vnode).to.be.eql(clonedRoute);
11+
});
12+
}

0 commit comments

Comments
 (0)