diff --git a/.travis.yml b/.travis.yml index a71ed287..825071b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,5 +21,10 @@ addons: - ubuntu-toolchain-r-test packages: - g++-4.8 + - xvfb +install: + - export DISPLAY=':99.0' + - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + - npm install script: npm run test # after_script: npm i -g codecov.io && cat ./coverage/lcov.info | codecov diff --git a/README.md b/README.md index fa05077e..4b5855e5 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ The new route will be a previous entry in the browser's history stack, and immediately afterward the`'navigate'` and `'render'`events will be emitted. Similar to [history.popState](http://devdocs.io/dom_events/popstate). (Note that `emit('popState')` will _not_ cause a popState action - use -`history.go(-1)` for that - this is different to the behaviour of `pushState` +`history.go(-1)` for that - this is different from the behaviour of `pushState` and `replaceState`!) ### `'DOMTitleChange'`|`state.events.DOMTITLECHANGE` @@ -262,14 +262,14 @@ An object _recommended_ to use for local component state. ### `state.cache(Component, id, [...args])` Generic class cache. Will lookup Component instance by id and create one if not -found. Usefull for working with statefull [components](#components). +found. Useful for working with stateful [components](#components). ## Routing Choo is an application level framework. This means that it takes care of everything related to routing and pathnames for you. ### Params -Params can be registered by prepending the routename with `:routename`, e.g. +Params can be registered by prepending the route name with `:routename`, e.g. `/foo/:bar/:baz`. The value of the param will be saved on `state.params` (e.g. `state.params.bar`). Wildcard routes can be registered with `*`, e.g. `/foo/*`. The value of the wildcard will be saved under `state.params.wildcard`. @@ -364,7 +364,7 @@ var html = require('choo/html') var mapboxgl = require('mapbox-gl') var Component = require('choo/component') -module.exports = class Button extends Component { +module.exports = class Map extends Component { constructor (id, state, emit) { super(id) this.local = state.components[id] = {} @@ -429,7 +429,7 @@ app.use(function (state, emitter) { When working with stateful components, one will need to keep track of component instances – `state.cache` does just that. The component cache is a function which takes a component class and a unique id (`string`) as it's first two -arguments. Any following arguments will be forwarded to the component contructor +arguments. Any following arguments will be forwarded to the component constructor together with `state` and `emit`. The default class cache is an LRU cache (using [nanolru][nanolru]), meaning it diff --git a/index.js b/index.js index ccf7ee5a..ad35df12 100644 --- a/index.js +++ b/index.js @@ -131,7 +131,7 @@ Choo.prototype.start = function () { var href = location.href var hash = location.hash if (href === window.location.href) { - if (!this._hashEnabled && hash) scrollToAnchor(hash) + if (!self._hashEnabled && hash) scrollToAnchor(hash) return } self.emitter.emit(self._events.PUSHSTATE, href) diff --git a/package.json b/package.json index 6f233d66..76a9b746 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "choo", - "version": "6.13.0", + "version": "6.13.3", "description": "A 4kb framework for creating sturdy frontend applications", "main": "index.js", "files": [ @@ -20,10 +20,12 @@ "scripts": { "build": "mkdir -p dist/ && browserify index -s Choo -p bundle-collapser/plugin > dist/bundle.js && browserify index -s Choo -p tinyify > dist/bundle.min.js && cat dist/bundle.min.js | gzip --best --stdout | wc -c | pretty-bytes", "deps": "dependency-check --entry ./html/index.js . && dependency-check . --extra --no-dev --entry ./html/index.js --entry ./component/index.js -i nanoassert", - "inspect": "browserify --full-paths index -g unassertify -g uglifyify | discify --open", + "inspect": "browserify --full-paths index -p tinyify | discify --open", "prepublishOnly": "npm run build", "start": "bankai start example", - "test": "standard && npm run deps && node test.js" + "test": "standard && npm run deps && npm run test:node && npm run test:browser", + "test:node": "node test/node.js | tap-format-spec", + "test:browser": "browserify test/browser.js | tape-run | tap-format-spec" }, "repository": "choojs/choo", "keywords": [ @@ -52,19 +54,18 @@ "xtend": "^4.0.1" }, "devDependencies": { + "@tap-format/spec": "^0.2.0", "@types/node": "^10.3.1", "browserify": "^16.2.2", "bundle-collapser": "^1.2.1", "dependency-check": "^3.1.0", - "discify": "^1.6.0", + "disc": "^1.3.3", "hyperscript": "^2.0.2", "pretty-bytes-cli": "^2.0.0", "spok": "^0.9.1", "standard": "^11.0.1", "tape": "^4.6.3", - "tinyify": "^2.2.0", - "uglify-es": "^3.0.17", - "uglifyify": "^5.0.0", - "unassertify": "^2.0.4" + "tape-run": "^5.0.0", + "tinyify": "^2.2.0" } } diff --git a/test/browser.js b/test/browser.js new file mode 100644 index 00000000..18728ed9 --- /dev/null +++ b/test/browser.js @@ -0,0 +1,242 @@ +var tape = require('tape') +var h = require('hyperscript') + +var html = require('../html') +var raw = require('../html/raw') +var choo = require('..') + +tape('should mount in the DOM', function (t) { + t.plan(1) + var app = choo() + var container = init('/', 'p') + app.route('/', function (state, emit) { + var strong = 'Hello filthy planet' + window.requestAnimationFrame(function () { + var exp = '

Hello filthy planet

' + t.equal(container.outerHTML, exp, 'result was OK') + }) + return html` +

${raw(strong)}

+ ` + }) + app.mount(container) +}) + +tape('should render with hyperscript', function (t) { + t.plan(1) + var app = choo() + var container = init('/', 'p') + app.route('/', function (state, emit) { + window.requestAnimationFrame(function () { + var exp = '

Hello filthy planet

' + t.equal(container.outerHTML, exp, 'result was OK') + }) + return h('p', h('strong', 'Hello filthy planet')) + }) + app.mount(container) +}) + +tape('should expose a public API', function (t) { + var app = choo() + + t.equal(typeof app.route, 'function', 'app.route prototype method exists') + t.equal(typeof app.toString, 'function', 'app.toString prototype method exists') + t.equal(typeof app.start, 'function', 'app.start prototype method exists') + t.equal(typeof app.mount, 'function', 'app.mount prototype method exists') + t.equal(typeof app.emitter, 'object', 'app.emitter prototype method exists') + + t.equal(typeof app.emit, 'function', 'app.emit instance method exists') + t.equal(typeof app.router, 'object', 'app.router instance object exists') + t.equal(typeof app.state, 'object', 'app.state instance object exists') + + t.end() +}) + +tape('should enable history and hash by defaut', function (t) { + var app = choo() + t.true(app._historyEnabled, 'history enabled') + t.true(app._hrefEnabled, 'href enabled') + t.end() +}) + +tape('router should pass state and emit to view', function (t) { + t.plan(2) + var app = choo() + var container = init() + app.route('/', function (state, emit) { + t.equal(typeof state, 'object', 'state is an object') + t.equal(typeof emit, 'function', 'emit is a function') + return html`
` + }) + app.mount(container) +}) + +tape('router should support a default route', function (t) { + t.plan(1) + var app = choo() + var container = init('/random') + app.route('*', function (state, emit) { + t.pass() + return html`
` + }) + app.mount(container) +}) + +tape('router should treat hashes as slashes by default', function (t) { + t.plan(1) + var app = choo() + var container = init('/account#security') + app.route('/account/security', function (state, emit) { + t.pass() + return html`
` + }) + app.mount(container) +}) + +tape('router should ignore hashes if hash is disabled', function (t) { + t.plan(1) + var app = choo({ hash: false }) + var container = init('/account#security') + app.route('/account', function (state, emit) { + t.pass() + return html`
` + }) + app.mount(container) +}) + +tape('cache should default to 100 instances', function (t) { + t.plan(1) + var app = choo() + var container = init() + app.route('/', function (state, emit) { + for (var i = 0; i <= 100; i++) state.cache(Component, i) + state.cache(Component, 0) + return html`
` + + function Component (id) { + if (id < i) t.pass('oldest instance was pruned when exceeding 100') + } + }) + app.mount(container) +}) + +tape('cache option should override number of max instances', function (t) { + t.plan(1) + var app = choo({ cache: 1 }) + var container = init() + app.route('/', function (state, emit) { + var instances = 0 + state.cache(Component, instances) + state.cache(Component, instances) + state.cache(Component, 0) + return html`
` + + function Component (id) { + if (id < instances) t.pass('oldest instance was pruned when exceeding 1') + instances++ + } + }) + app.mount(container) +}) + +tape('cache option should override default LRU cache', function (t) { + t.plan(2) + var cache = { + get (Component, id) { + t.pass('called get') + }, + set (Component, id) { + t.pass('called set') + } + } + var app = choo({ cache: cache }) + var container = init() + app.route('/', function (state, emit) { + state.cache(Component, 'foo') + return html`
` + }) + app.mount(container) + + function Component () {} +}) + +// built-in state + +tape('state should include events', function (t) { + t.plan(2) + var app = choo() + var container = init() + app.route('/', function (state, emit) { + t.ok(state.hasOwnProperty('events'), 'state has event property') + t.ok(Object.keys(state.events).length > 0, 'events object has keys') + return html`
` + }) + app.mount(container) +}) + +tape('state should include location on render', function (t) { + t.plan(6) + var app = choo() + var container = init('/foo/bar/file.txt?bin=baz') + app.route('/:first/:second/*', function (state, emit) { + var params = { first: 'foo', second: 'bar', wildcard: 'file.txt' } + t.equal(state.href, '/foo/bar/file.txt', 'state has href') + t.equal(state.route, ':first/:second/*', 'state has route') + t.ok(state.hasOwnProperty('params'), 'state has params') + t.deepEqual(state.params, params, 'params match') + t.ok(state.hasOwnProperty('query'), 'state has query') + t.deepEqual(state.query, { bin: 'baz' }, 'query match') + return html`
` + }) + app.mount(container) +}) + +tape('state should include title', function (t) { + t.plan(3) + document.title = 'foo' + var app = choo() + var container = init() + t.equal(app.state.title, 'foo', 'title is match') + app.use(function (state, emitter) { + emitter.on(state.events.DOMTITLECHANGE, function (title) { + t.equal(state.title, 'bar', 'title is changed in state') + t.equal(document.title, 'bar', 'title is changed in document') + }) + }) + app.route('/', function (state, emit) { + emit(state.events.DOMTITLECHANGE, 'bar') + return html`
` + }) + app.mount(container) +}) + +tape('state should include cache', function (t) { + t.plan(6) + var app = choo() + var container = init() + app.route('/', function (state, emit) { + t.equal(typeof state.cache, 'function', 'state has cache method') + var cached = state.cache(Component, 'foo', 'arg') + t.equal(cached, state.cache(Component, 'foo'), 'consecutive calls return same instance') + return html`
` + }) + app.mount(container) + + function Component (id, state, emit, arg) { + t.equal(id, 'foo', 'id was prefixed to constructor args') + t.equal(typeof state, 'object', 'state was prefixed to constructor args') + t.equal(typeof emit, 'function', 'emit was prefixed to constructor args') + t.equal(arg, 'arg', 'constructor args were forwarded') + } +}) + +// create application container and set location +// (str?, str?) -> Element +function init (location, type) { + location = location ? location.split('#') : ['/', ''] + window.history.replaceState({}, document.title, location[0]) + window.location.hash = location[1] || '' + var container = document.createElement(type || 'div') + document.body.appendChild(container) + return container +} diff --git a/test.js b/test/node.js similarity index 81% rename from test.js rename to test/node.js index a25bac5c..6d931a80 100644 --- a/test.js +++ b/test/node.js @@ -1,9 +1,9 @@ var tape = require('tape') var h = require('hyperscript') -var html = require('./html') -var raw = require('./html/raw') -var choo = require('./') +var html = require('../html') +var raw = require('../html/raw') +var choo = require('..') tape('should render on the server with nanohtml', function (t) { var app = choo() @@ -168,47 +168,23 @@ tape('state should include events', function (t) { t.end() }) -tape('state should include params', function (t) { - t.plan(4) - var app = choo() - app.route('/:resource/:id/*', function (state, emit) { - t.ok(state.hasOwnProperty('params'), 'state has params property') - t.equal(state.params.resource, 'users', 'resources param is users') - t.equal(state.params.id, '1', 'id param is 1') - t.equal(state.params.wildcard, 'docs/foo.txt', 'wildcard captures what remains') - return html`
` - }) - app.toString('/users/1/docs/foo.txt') - t.end() -}) - -tape('state should include query', function (t) { - t.plan(2) - var app = choo() - app.route('/', function (state, emit) { - t.ok(state.hasOwnProperty('query'), 'state has query property') - t.equal(state.query.page, '2', 'page querystring is 2') - return html`
` - }) - app.toString('/?page=2') - t.end() -}) - -tape('state should include href', function (t) { - t.plan(2) +tape('state should include location on render', function (t) { + t.plan(6) var app = choo() - app.route('/:resource/:id', function (state, emit) { - t.ok(state.hasOwnProperty('href'), 'state has href property') - t.equal(state.href, '/users/1', 'href is users/1') + app.route('/:first/:second/*', function (state, emit) { + var params = { first: 'foo', second: 'bar', wildcard: 'file.txt' } + t.equal(state.href, '/foo/bar/file.txt', 'state has href') + t.equal(state.route, ':first/:second/*', 'state has route') + t.ok(state.hasOwnProperty('params'), 'state has params') + t.deepEqual(state.params, params, 'params match') + t.ok(state.hasOwnProperty('query'), 'state has query') + t.deepEqual(state.query, { bin: 'baz' }, 'query match') return html`
` }) - app.toString('/users/1?page=2') // should ignore query + app.toString('/foo/bar/file.txt?bin=baz') t.end() }) -// TODO: Implement this using jsdom, as this only works when window is present -tape.skip('state should include title', function (t) {}) - tape('state should include cache', function (t) { t.plan(6) var app = choo()