From 527d8959900dc515aad196e69041c23e1bb8b0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20T=C3=B6rnqvist?= Date: Tue, 2 Apr 2019 10:51:42 +0200 Subject: [PATCH] Add browser tests --- .travis.yml | 5 + index.js | 2 +- package.json | 6 +- test/browser.js | 263 ++++++++++++++++++++++++++++++++++++++++ test.js => test/node.js | 61 +++------- 5 files changed, 291 insertions(+), 46 deletions(-) create mode 100644 test/browser.js rename test.js => test/node.js (77%) 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/index.js b/index.js index 48d125a8..0feed849 100644 --- a/index.js +++ b/index.js @@ -138,11 +138,11 @@ Choo.prototype.start = function () { } this._setCache(this.state) + this._matchRoute() this._stores.forEach(function (initStore) { initStore(self.state) }) - this._matchRoute() this._tree = this._prerender(this.state) assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + this.state.href) diff --git a/package.json b/package.json index 127b17cb..b6690799 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "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,6 +54,7 @@ "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", @@ -62,6 +65,7 @@ "spok": "^0.9.1", "standard": "^11.0.1", "tape": "^4.6.3", + "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..24e86323 --- /dev/null +++ b/test/browser.js @@ -0,0 +1,263 @@ +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 location on store init', function (t) { + t.plan(6) + var app = choo() + var container = init('/foo/bar/file.txt?bin=baz') + app.use(store) + app.route('/:first/:second/*', function (state, emit) { + return html`
` + }) + app.mount(container) + + function store (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') + } +}) + +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 77% rename from test.js rename to test/node.js index 859fd7e8..213958eb 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,45 +168,20 @@ 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 location on render', function (t) { t.plan(6) var app = choo() - app.route('/:foo', function (state, emit) { - t.equal(state.href, '/foo', 'state has href') - t.equal(state.route, ':foo', 'state has route') + 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, { foo: 'foo' }, 'params match') + t.deepEqual(state.params, params, 'params match') t.ok(state.hasOwnProperty('query'), 'state has query') - t.deepEqual(state.query, { bar: 'baz' }, 'query match') + t.deepEqual(state.query, { bin: 'baz' }, 'query match') return html`
` }) - app.toString('/foo?bar=baz') + app.toString('/foo/bar/file.txt?bin=baz') t.end() }) @@ -214,24 +189,22 @@ tape('state should include location on store init', function (t) { t.plan(6) var app = choo() app.use(store) - app.route('/:foo', function (state, emit) { + app.route('/:first/:second/*', function (state, emit) { return html`
` }) - app.toString('/foo?bar=baz') + app.toString('/foo/bar/file.txt?bin=baz') function store (state, emit) { - t.equal(state.href, '/foo', 'state has href') - t.equal(state.route, ':foo', 'state has route') + 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, { foo: 'foo' }, 'params match') + t.deepEqual(state.params, params, 'params match') t.ok(state.hasOwnProperty('query'), 'state has query') - t.deepEqual(state.query, { bar: 'baz' }, 'query match') + t.deepEqual(state.query, { bin: 'baz' }, 'query match') } }) -// 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()