diff --git a/.gitignore b/.gitignore index f930c2cd..b3b1382a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ build test/fixtures/build.js npm-debug.log .DS_Store +yarn.lock /.vscode /isorender diff --git a/.travis.yml b/.travis.yml index 90090b51..f1d5a325 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ node_js: before_script: - npm run build - npm run build-test + - npm run lint script: - npm run test-server - npm run test-browser-sauce diff --git a/lib/component-utils/proxy-component.js b/lib/component-utils/proxy-component.js index 4248f388..d13a95df 100644 --- a/lib/component-utils/proxy-component.js +++ b/lib/component-utils/proxy-component.js @@ -9,7 +9,7 @@ import {h} from '../dom-patcher'; * @extends Component */ class ProxyComponent extends Component { - get config(){ + get config() { return Object.assign({ template: ({$component}) => { return h($component.getTargetElementTag(), { @@ -72,7 +72,7 @@ class ProxyComponent extends Component { * } * } */ - allowEvent(ev) { + allowEvent(ev) { // eslint-disable-line no-unused-vars return true; } diff --git a/lib/component.js b/lib/component.js index 94529b19..8c3b68be 100644 --- a/lib/component.js +++ b/lib/component.js @@ -1,8 +1,7 @@ import cuid from 'cuid'; -import pick from 'lodash.pick'; import WebComponent from 'webcomponent'; -import { EMPTY_DIV, DOMPatcher, h } from './dom-patcher'; +import {EMPTY_DIV, DOMPatcher, h} from './dom-patcher'; import Router from './router'; const DOCUMENT_FRAGMENT_NODE = 11; @@ -108,7 +107,7 @@ class Component extends WebComponent { */ findPanelParentByTagName(tagName) { tagName = tagName.toLowerCase(); - for (let node = this.$panelParent; !!node; node = node.$panelParent) { + for (let node = this.$panelParent; node; node = node.$panelParent) { if (node.tagName.toLowerCase() === tagName) { return node; } @@ -166,7 +165,7 @@ class Component extends WebComponent { * return state.largeResultSetID !== this._cachedResultID; * } */ - shouldUpdate(state) { + shouldUpdate(state) { // eslint-disable-line no-unused-vars return true; } @@ -202,10 +201,10 @@ class Component extends WebComponent { this.panelID = cuid(); this._config = Object.assign({}, { - css: '', + css: ``, helpers: {}, routes: {}, - template: () => { throw Error('No template provided by Component subclass'); }, + template: () => { throw Error(`No template provided by Component subclass`); }, updateSync: false, useShadowDom: false, }, this.config); @@ -222,13 +221,13 @@ class Component extends WebComponent { this.isStateShared = false; } - if (this.getConfig('useShadowDom')) { - this.el = this.attachShadow({mode: 'open'}); - this.styleTag = document.createElement('style'); - this.styleTag.innerHTML = this.getConfig('css'); + if (this.getConfig(`useShadowDom`)) { + this.el = this.attachShadow({mode: `open`}); + this.styleTag = document.createElement(`style`); + this.styleTag.innerHTML = this.getConfig(`css`); this.el.appendChild(this.styleTag); - } else if (this.getConfig('css')) { - throw Error('"useShadowDom" config option must be set in order to use "css" config.'); + } else if (this.getConfig(`css`)) { + throw Error(`"useShadowDom" config option must be set in order to use "css" config.`); } else { this.el = this; } @@ -248,11 +247,11 @@ class Component extends WebComponent { this.$panelChildren = new Set(); - if (typeof this.$panelParentID !== 'undefined') { + if (typeof this.$panelParentID !== `undefined`) { this.isPanelChild = true; // find $panelParent for (let node = this.parentNode; node && !this.$panelParent; node = node.parentNode) { - if (node.nodeType === DOCUMENT_FRAGMENT_NODE) { // handle shadow-root + if (node.nodeType === DOCUMENT_FRAGMENT_NODE) { // handle shadow-root node = node.host; } if (node.panelID === this.$panelParentID) { @@ -280,20 +279,20 @@ class Component extends WebComponent { const newState = Object.assign( {}, - this.getConfig('defaultState'), + this.getConfig(`defaultState`), this.state, - this.getJSONAttribute('data-state'), + this.getJSONAttribute(`data-state`), this._stateFromAttributes() ); Object.assign(this.state, newState); - if (Object.keys(this.getConfig('routes')).length) { + if (Object.keys(this.getConfig(`routes`)).length) { this.router = new Router(this, {historyMethod: this.historyMethod}); this.navigate(window.location.hash); } this.domPatcher = new DOMPatcher(this.state, this._render.bind(this), { - updateMode: this.getConfig('updateSync') ? 'sync': 'async', + updateMode: this.getConfig(`updateSync`) ? `sync`: `async`, }); this.el.appendChild(this.domPatcher.el); this.initialized = true; @@ -320,7 +319,7 @@ class Component extends WebComponent { } attributeChangedCallback(attr, oldVal, newVal) { - if (attr === 'style-override') { + if (attr === `style-override`) { this._applyStyles(newVal); } if (this.isPanelRoot && this.initialized) { @@ -329,8 +328,8 @@ class Component extends WebComponent { } _applyStyles(styleOverride) { - if (this.getConfig('useShadowDom')) { - this.styleTag.innerHTML = this.getConfig('css') + (styleOverride || ''); + if (this.getConfig(`useShadowDom`)) { + this.styleTag.innerHTML = this.getConfig(`css`) + (styleOverride || ``); } } @@ -341,20 +340,20 @@ class Component extends WebComponent { toString() { try { return `${this.tagName}#${this.panelID}`; - } catch(e) { - return 'UNKNOWN COMPONENT'; + } catch (e) { + return `UNKNOWN COMPONENT`; } } _render(state) { if (this.shouldUpdate(state)) { try { - this._rendered = this.getConfig('template')(Object.assign({}, state, { + this._rendered = this.getConfig(`template`)(Object.assign({}, state, { $app: this.appState, $component: this, $helpers: this.helpers, })); - } catch(e) { + } catch (e) { this.logError(`Error while rendering ${this.toString()}`, this, e.stack); } } @@ -411,7 +410,7 @@ class Component extends WebComponent { } else { // update DOM, router, descendants etc. - const updateHash = '$fragment' in stateUpdate && stateUpdate.$fragment !== this[store].$fragment; + const updateHash = `$fragment` in stateUpdate && stateUpdate.$fragment !== this[store].$fragment; const cascadeFromRoot = cascade && !this.isPanelRoot; const updateOptions = {cascade, store}; const rootOptions = {exclude: this, cascade, store}; diff --git a/lib/dom-patcher.js b/lib/dom-patcher.js index bbec815a..802f5c33 100644 --- a/lib/dom-patcher.js +++ b/lib/dom-patcher.js @@ -7,12 +7,12 @@ import snabbdom from 'snabbdom'; import h from 'snabbdom/h'; -import snabbAttributes from 'snabbdom/modules/attributes'; -import snabbDataset from 'snabbdom/modules/dataset'; -import snabbDelayedClass from 'snabbdom-delayed-class'; +import snabbAttributes from 'snabbdom/modules/attributes'; +import snabbDataset from 'snabbdom/modules/dataset'; +import snabbDelayedClass from 'snabbdom-delayed-class'; import snabbEventlisterners from 'snabbdom/modules/eventlisteners'; -import snabbProps from 'snabbdom/modules/props'; -import snabbStyle from 'snabbdom/modules/style'; +import snabbProps from 'snabbdom/modules/props'; +import snabbStyle from 'snabbdom/modules/style'; const patch = snabbdom.init([ snabbAttributes, @@ -23,24 +23,24 @@ const patch = snabbdom.init([ snabbStyle, ]); -export const EMPTY_DIV = h('div'); +export const EMPTY_DIV = h(`div`); export {h}; export class DOMPatcher { constructor(initialState, renderFunc, options={}) { - this.updateMode = options.updateMode || 'async'; + this.updateMode = options.updateMode || `async`; this.state = Object.assign({}, initialState); this.renderFunc = renderFunc; this.vnode = this.renderFunc(this.state); // prepare root element - const tagName = this.vnode.sel.split(/[#\.]/)[0]; - const classMatches = this.vnode.sel.match(/\.[^\.#]+/g); - const idMatch = this.vnode.sel.match(/#[^\.#]+/); + const tagName = this.vnode.sel.split(/[#.]/)[0]; + const classMatches = this.vnode.sel.match(/\.[^.#]+/g); + const idMatch = this.vnode.sel.match(/#[^.#]+/); this.el = document.createElement(tagName); if (classMatches) { - this.el.className = classMatches.map(c => c.slice(1)).join(' '); + this.el.className = classMatches.map(c => c.slice(1)).join(` `); } if (idMatch) { this.el.id = idMatch[0].slice(1); @@ -56,13 +56,13 @@ export class DOMPatcher { this.pendingState = newState; switch (this.updateMode) { - case 'async': + case `async`: if (!this.pending) { this.pending = true; requestAnimationFrame(() => this.render()); } break; - case 'sync': + case `sync`: this.render(); break; } diff --git a/lib/isorender/dom-shims.js b/lib/isorender/dom-shims.js index 41bf4899..7ae748ef 100644 --- a/lib/isorender/dom-shims.js +++ b/lib/isorender/dom-shims.js @@ -1,3 +1,4 @@ +/* eslint-env node */ /** * Node.js polyfill for rendering Panel components without a browser. * Makes the following objects globally available: @@ -27,7 +28,7 @@ import requestAnimationFrame from 'raf'; global.requestAnimationFrame = global.requestAnimationFrame || requestAnimationFrame; // patch DOM insertion functions to call connectedCallback on Custom Elements -['appendChild', 'insertBefore', 'replaceChild'].forEach(funcName => { +[`appendChild`, `insertBefore`, `replaceChild`].forEach(funcName => { const origFunc = Element.prototype[funcName]; Element.prototype[funcName] = function() { const child = origFunc.apply(this, arguments); @@ -76,7 +77,7 @@ Document.prototype.createElement = function(tagName) { el = originalCreateElement(...arguments); } return el; -} +}; global.customElements = global.customElements || { define(tagName, proto) { @@ -86,5 +87,5 @@ global.customElements = global.customElements || { } else { registeredElements[tagName] = proto; } - } + }, }; diff --git a/lib/router.js b/lib/router.js index 935294d2..fb33a8ae 100644 --- a/lib/router.js +++ b/lib/router.js @@ -1,3 +1,7 @@ +function stripHash(fragment) { + return fragment.replace(/^#*/, ``); +} + // just the necessary bits of Backbone router+history export default class Router { constructor(app, options={}) { @@ -5,27 +9,27 @@ export default class Router { const routerWindow = this.window = options.window || window; this.app = app; - const routeDefs = this.app.getConfig('routes'); + const routeDefs = this.app.getConfig(`routes`); // https://github.com/jashkenas/backbone/blob/d682061a/backbone.js#L1476-L1479 // Cached regular expressions for matching named param parts and splatted // parts of route strings. const optionalParam = /\((.*?)\)/g; - const namedParam = /(\(\?)?:\w+/g; - const splatParam = /\*\w+/g; - const escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + const namedParam = /(\(\?)?:\w+/g; + const splatParam = /\*\w+/g; + const escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; // eslint-disable-line no-useless-escape this.compiledRoutes = Object.keys(routeDefs).map(routeExpr => { // https://github.com/jashkenas/backbone/blob/d682061a/backbone.js#L1537-L1547 let expr = routeExpr - .replace(escapeRegExp, '\\$&') - .replace(optionalParam, '(?:$1)?') - .replace(namedParam, (match, optional) => optional ? match : '([^/?]+)') - .replace(splatParam, '([^?]*?)'); - expr = new RegExp('^' + expr + '(?:\\?([\\s\\S]*))?$'); + .replace(escapeRegExp, `\\$&`) + .replace(optionalParam, `(?:$1)?`) + .replace(namedParam, (match, optional) => optional ? match : `([^/?]+)`) + .replace(splatParam, `([^?]*?)`); + expr = new RegExp(`^` + expr + `(?:\\?([\\s\\S]*))?$`); // hook up route handler function let handler = routeDefs[routeExpr]; - if (typeof handler === 'string') { + if (typeof handler === `string`) { // reference to another handler rather than its own function handler = routeDefs[handler]; } @@ -34,9 +38,9 @@ export default class Router { }); const navigateToHash = () => this.navigate(routerWindow.location.hash); - routerWindow.addEventListener('popstate', () => navigateToHash()); + routerWindow.addEventListener(`popstate`, () => navigateToHash()); - this.historyMethod = options.historyMethod || 'pushState'; + this.historyMethod = options.historyMethod || `pushState`; const origChangeState = routerWindow.history[this.historyMethod]; routerWindow.history[this.historyMethod] = function() { origChangeState.apply(routerWindow.history, arguments); @@ -89,7 +93,3 @@ export default class Router { } } } - -function stripHash(fragment) { - return fragment.replace(/^#*/, ''); -} diff --git a/package.json b/package.json index 71ecb933..0a2e3a13 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build": "babel lib -d build && cp -r build/isorender .", "build-test": "webpack --config test/browser/webpack.config.js", "docs": "rm -rf docs && jsdoc lib lib/component-utils lib/isorender -t node_modules/minami -R README-API.md -d docs", + "lint": "eslint devtools hot lib scripts test --ignore-pattern 'test/browser/build/'", "prepublishOnly": "npm run build", "publish-devtools": "node scripts/publish-devtools.js", "test": "npm run build-test && npm run test-server && npm run test-browser-local", @@ -58,8 +59,8 @@ "babel-preset-es2015": "^6.6.0", "chai": "^3.5.0", "chrome-store-api": "^1.0.5", - "eslint": "4.12.1", - "eslint-config-mixpanel": "3.1.0", + "eslint": "4.18.1", + "eslint-config-mixpanel": "3.5.0", "jsdoc": "^3.5.5", "minami": "^1.1.1", "mocha": "^2.5.3", diff --git a/scripts/publish-devtools.js b/scripts/publish-devtools.js index 17beed32..2711fee5 100644 --- a/scripts/publish-devtools.js +++ b/scripts/publish-devtools.js @@ -22,7 +22,7 @@ const manifestPath = `${devtoolsDir}/manifest.json`; // Main wrapped in async since top-level await is not supported (async function() { - try{ + try { // Write new version to manifest const manifest = JSON.parse(fs.readFileSync(manifestPath, `utf-8`)); console.log(`Current manifest:\n`, manifest); diff --git a/test/browser/component-utils/proxy-component.js b/test/browser/component-utils/proxy-component.js index e6b37fea..b7ab4e05 100644 --- a/test/browser/component-utils/proxy-component.js +++ b/test/browser/component-utils/proxy-component.js @@ -19,7 +19,7 @@ describe(`ProxyComponent`, function() { }); it(`re-dispatches events from child that are not composed`, function(done) { - el.addEventListener(`nonComposedEvent`, () => done()) + el.addEventListener(`nonComposedEvent`, () => done()); el.setAttribute(`send-non-composed`, ``); }); diff --git a/test/fixtures/attr-reflection-app.js b/test/fixtures/attr-reflection-app.js index 15c3203d..60d917c0 100644 --- a/test/fixtures/attr-reflection-app.js +++ b/test/fixtures/attr-reflection-app.js @@ -1,10 +1,10 @@ -import { Component, h } from '../../lib'; +import {Component, h} from '../../lib'; export class AttrReflectionApp extends Component { get config() { return { - template: state => h('div', {class: {'attr-app': true}}, [ - h('p', `Value of attribute wombats: ${this.getAttribute('wombats')}`), + template: () => h(`div`, {class: {'attr-app': true}}, [ + h(`p`, `Value of attribute wombats: ${this.getAttribute(`wombats`)}`), ]), }; } diff --git a/test/fixtures/breakable-app.js b/test/fixtures/breakable-app.js index 56261f89..0b15db56 100644 --- a/test/fixtures/breakable-app.js +++ b/test/fixtures/breakable-app.js @@ -1,11 +1,11 @@ -import { Component, h } from '../../lib'; +import {Component, h} from '../../lib'; export class BreakableApp extends Component { get config() { return { - template: state => h('div', {class: {foo: true}}, [ + template: state => h(`div`, {class: {foo: true}}, [ // this will throw if state.foo does not exist - h('p', `Value of foo.bar: ${state.foo.bar}`), + h(`p`, `Value of foo.bar: ${state.foo.bar}`), ]), }; } diff --git a/test/fixtures/css-no-shadow-app.js b/test/fixtures/css-no-shadow-app.js index fb9b0765..95a56881 100644 --- a/test/fixtures/css-no-shadow-app.js +++ b/test/fixtures/css-no-shadow-app.js @@ -1,12 +1,11 @@ -import { Component, h } from '../../lib'; +import {Component, h} from '../../lib'; export class CssNoShadowApp extends Component { get config() { return { - css: 'color: blue;', - - template: state => h('div', {class: {foo: true}}, [ - h('p', `Hello`), + css: `color: blue;`, + template: () => h(`div`, {class: {foo: true}}, [ + h(`p`, `Hello`), ]), }; } diff --git a/test/fixtures/index.js b/test/fixtures/index.js index efb65094..ca7ba1e9 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -8,15 +8,15 @@ import {ProxyApp, EventProducer} from './proxy-app'; import {ShadowDomApp} from './shadow-dom-app'; import {SimpleApp} from './simple-app'; -customElements.define('attr-reflection-app', AttrReflectionApp); -customElements.define('breakable-app', BreakableApp); +customElements.define(`attr-reflection-app`, AttrReflectionApp); +customElements.define(`breakable-app`, BreakableApp); customElements.define(`controlled-app`, ControlledApp); -customElements.define('css-no-shadow-app', CssNoShadowApp); +customElements.define(`css-no-shadow-app`, CssNoShadowApp); customElements.define(`event-producer`, EventProducer); -customElements.define('nested-app', NestedApp); -customElements.define('nested-child', NestedChild); +customElements.define(`nested-app`, NestedApp); +customElements.define(`nested-child`, NestedChild); customElements.define(`nested-partial-state-parent`, NestedPartialStateParent); customElements.define(`nested-partial-state-child`, NestedPartialStateChild); customElements.define(`proxy-app`, ProxyApp); -customElements.define('shadow-dom-app', ShadowDomApp); -customElements.define('simple-app', SimpleApp); +customElements.define(`shadow-dom-app`, ShadowDomApp); +customElements.define(`simple-app`, SimpleApp); diff --git a/test/fixtures/nested-app.js b/test/fixtures/nested-app.js index 95d7dbd1..a6854189 100644 --- a/test/fixtures/nested-app.js +++ b/test/fixtures/nested-app.js @@ -1,10 +1,10 @@ -import { Component, h } from '../../lib'; +import {Component, h} from '../../lib'; export class NestedApp extends Component { get config() { return { defaultState: { - title: 'test', + title: `test`, }, hooks: { @@ -12,9 +12,9 @@ export class NestedApp extends Component { postUpdate: () => this.nestedPostTitle = this.state.title, }, - template: state => h('div', {class: {'nested-foo': true}}, [ - h('h1', `Nested app: ${state.title}`), - this.child('nested-child', {attrs: {'state-animal': 'llama'}}), + template: state => h(`div`, {class: {'nested-foo': true}}, [ + h(`h1`, `Nested app: ${state.title}`), + this.child(`nested-child`, {attrs: {'state-animal': `llama`}}), ]), }; } @@ -23,9 +23,9 @@ export class NestedApp extends Component { export class NestedChild extends Component { get config() { return { - template: state => h('div', {class: {'nested-foo-child': true}}, [ - h('p', `parent title: ${state.title}`), - h('p', `animal: ${state.animal}`), + template: state => h(`div`, {class: {'nested-foo-child': true}}, [ + h(`p`, `parent title: ${state.title}`), + h(`p`, `animal: ${state.animal}`), ]), }; } diff --git a/test/fixtures/nested-partial-state-app.js b/test/fixtures/nested-partial-state-app.js index 86bcad49..e8413843 100644 --- a/test/fixtures/nested-partial-state-app.js +++ b/test/fixtures/nested-partial-state-app.js @@ -1,4 +1,4 @@ -import { Component, h } from '../../lib'; +import {Component, h} from '../../lib'; export class NestedPartialStateParent extends Component { get config() { diff --git a/test/fixtures/proxy-app.js b/test/fixtures/proxy-app.js index 91daf855..1bbfc71b 100644 --- a/test/fixtures/proxy-app.js +++ b/test/fixtures/proxy-app.js @@ -10,7 +10,7 @@ export class ProxyApp extends ProxyComponent { getTargetElementTag() { // This element will change its behavior if passed an href. - return !!this.getAttribute(`href`) ? `a` : `event-producer`; + return this.getAttribute(`href`) ? `a` : `event-producer`; } get observedEvents() { @@ -25,16 +25,16 @@ export class ProxyApp extends ProxyComponent { export class EventProducer extends Component { get config() { return { - template:() => h(`div`, `I make things happen!`), + template: () => h(`div`, `I make things happen!`), useShadowDom: true, }; } attributeChangedCallback(name) { if (name === `send-non-composed`) { - this.dispatchEvent(new CustomEvent(`nonComposedEvent`, { bubbles: true, composed: false })); + this.dispatchEvent(new CustomEvent(`nonComposedEvent`, {bubbles: true, composed: false})); } else if (name === `send-composed`) { - this.dispatchEvent(new CustomEvent(`composedEvent`, { bubbles: true, composed: true })); + this.dispatchEvent(new CustomEvent(`composedEvent`, {bubbles: true, composed: true})); } } diff --git a/test/fixtures/shadow-dom-app.js b/test/fixtures/shadow-dom-app.js index 2c1cdd0d..3256d27f 100644 --- a/test/fixtures/shadow-dom-app.js +++ b/test/fixtures/shadow-dom-app.js @@ -1,12 +1,11 @@ -import { Component, h } from '../../lib'; +import {Component, h} from '../../lib'; export class ShadowDomApp extends Component { get config() { return { - css: 'color: blue;', - - template: state => h('div', {class: {foo: true}}, [ - h('p', `Hello`), + css: `color: blue;`, + template: () => h(`div`, {class: {foo: true}}, [ + h(`p`, `Hello`), ]), useShadowDom: true, diff --git a/test/fixtures/simple-app.js b/test/fixtures/simple-app.js index b14da90b..e2b3cafb 100644 --- a/test/fixtures/simple-app.js +++ b/test/fixtures/simple-app.js @@ -1,11 +1,11 @@ -import { Component, h } from '../../lib'; +import {Component, h} from '../../lib'; export class SimpleApp extends Component { get config() { return { defaultState: { - foo: 'bar', - baz: 'qux', + foo: `bar`, + baz: `qux`, }, helpers: { @@ -17,16 +17,16 @@ export class SimpleApp extends Component { postUpdate: () => this.postFoo = this.state.foo, }, - template: state => h('div', {class: {foo: true}}, [ - h('p', `Value of foo: ${state.foo}`), - h('p', `Value of baz: ${state.baz}`), - h('p', `Foo capitalized: ${state.$helpers.capitalize(state.foo)}`), + template: state => h(`div`, {class: {foo: true}}, [ + h(`p`, `Value of foo: ${state.foo}`), + h(`p`, `Value of baz: ${state.baz}`), + h(`p`, `Foo capitalized: ${state.$helpers.capitalize(state.foo)}`), ]), }; } shouldUpdate(state) { // I simply refuse to say "Value of foo: meow" - return !!state.foo && state.foo !== 'meow'; + return !!state.foo && state.foo !== `meow`; } } diff --git a/test/server/component.js b/test/server/component.js index c2200dcc..3dd76027 100644 --- a/test/server/component.js +++ b/test/server/component.js @@ -1,62 +1,63 @@ +/* eslint-env mocha */ import '../../lib/isorender/dom-shims'; -import { expect } from 'chai'; +import {expect} from 'chai'; import requestAnimationFrameCB from 'raf'; -import { SimpleApp } from '../fixtures/simple-app'; -import { NestedApp, NestedChild } from '../fixtures/nested-app'; -import { AttrReflectionApp } from '../fixtures/attr-reflection-app'; -customElements.define('nested-app', NestedApp); -customElements.define('nested-child', NestedChild); -customElements.define('simple-app', SimpleApp); -customElements.define('attr-reflection-app', AttrReflectionApp); +import {SimpleApp} from '../fixtures/simple-app'; +import {NestedApp, NestedChild} from '../fixtures/nested-app'; +import {AttrReflectionApp} from '../fixtures/attr-reflection-app'; +customElements.define(`nested-app`, NestedApp); +customElements.define(`nested-child`, NestedChild); +customElements.define(`simple-app`, SimpleApp); +customElements.define(`attr-reflection-app`, AttrReflectionApp); const raf = () => new Promise(requestAnimationFrameCB); -describe('Server-side component renderer', function() { - it('can register and create components with document.createElement', function() { - const el = document.createElement('simple-app'); +describe(`Server-side component renderer`, function() { + it(`can register and create components with document.createElement`, function() { + const el = document.createElement(`simple-app`); expect(el.state).to.eql({}); el.connectedCallback(); - expect(el.state).to.eql({foo: 'bar', baz: 'qux'}); + expect(el.state).to.eql({foo: `bar`, baz: `qux`}); }); - it('supports class instantiation', function() { + it(`supports class instantiation`, function() { const el = new SimpleApp(); expect(el.state).to.eql({}); el.connectedCallback(); - expect(el.state).to.eql({foo: 'bar', baz: 'qux'}); + expect(el.state).to.eql({foo: `bar`, baz: `qux`}); }); - it('renders a simple component', async function() { + it(`renders a simple component`, async function() { const el = new SimpleApp(); el.connectedCallback(); await raf(); const html = el.innerHTML; - expect(html.toLowerCase()).to.contain('
'); - expect(html).to.contain('Value of foo: bar'); - expect(html).to.contain('Foo capitalized: Bar'); + expect(html.toLowerCase()).to.contain(`
`); + expect(html).to.contain(`Value of foo: bar`); + expect(html).to.contain(`Foo capitalized: Bar`); }); - it('renders updates', async function() { + it(`renders updates`, async function() { const el = new SimpleApp(); el.connectedCallback(); await raf(); - expect(el.textContent).to.contain('Value of foo: bar'); - expect(el.textContent).to.contain('Foo capitalized: Bar'); - el.update({foo: 'new value'}); + expect(el.textContent).to.contain(`Value of foo: bar`); + expect(el.textContent).to.contain(`Foo capitalized: Bar`); + el.update({foo: `new value`}); await raf(); - expect(el.textContent).to.contain('Value of foo: new value'); - expect(el.textContent).to.contain('Foo capitalized: New value'); + expect(el.textContent).to.contain(`Value of foo: new value`); + expect(el.textContent).to.contain(`Foo capitalized: New value`); }); - it('renders nested components', async function() { + it(`renders nested components`, async function() { const el = new NestedApp(); el.connectedCallback(); @@ -64,70 +65,70 @@ describe('Server-side component renderer', function() { // check DOM structure expect(el.childNodes).to.have.lengthOf(1); - expect(el.childNodes[0].className).to.equal('nested-foo'); + expect(el.childNodes[0].className).to.equal(`nested-foo`); expect(el.childNodes[0].childNodes).to.have.lengthOf(2); const nestedChild = el.childNodes[0].childNodes[1]; expect(nestedChild.childNodes).to.have.lengthOf(1); - expect(nestedChild.childNodes[0].className).to.equal('nested-foo-child'); + expect(nestedChild.childNodes[0].className).to.equal(`nested-foo-child`); expect(nestedChild.childNodes[0].childNodes).to.have.lengthOf(2); // check content/HTML output const html = el.innerHTML; - expect(html.toLowerCase()).to.contain('
'); - expect(html).to.contain('Nested app: test'); - expect(html.toLowerCase()).to.contain('
'); - expect(html).to.contain('parent title: test'); - expect(html).to.contain('animal: llama'); + expect(html.toLowerCase()).to.contain(`
`); + expect(html).to.contain(`Nested app: test`); + expect(html.toLowerCase()).to.contain(`
`); + expect(html).to.contain(`parent title: test`); + expect(html).to.contain(`animal: llama`); }); - it('updates nested components', async function() { + it(`updates nested components`, async function() { const el = new NestedApp(); el.connectedCallback(); await raf(); const nestedChild = el.childNodes[0].childNodes[1]; - expect(nestedChild.state.title).to.equal('test'); - nestedChild.update({title: 'meow'}); + expect(nestedChild.state.title).to.equal(`test`); + nestedChild.update({title: `meow`}); await raf(); - expect(el.state.title).to.equal('meow'); - expect(el.innerHTML).to.contain('Nested app: meow'); - expect(nestedChild.innerHTML).to.contain('parent title: meow'); - el.update({title: 'something else'}); + expect(el.state.title).to.equal(`meow`); + expect(el.innerHTML).to.contain(`Nested app: meow`); + expect(nestedChild.innerHTML).to.contain(`parent title: meow`); + el.update({title: `something else`}); await raf(); - expect(nestedChild.innerHTML).to.contain('parent title: something else'); + expect(nestedChild.innerHTML).to.contain(`parent title: something else`); }); - it('renders attributes', async function() { + it(`renders attributes`, async function() { const el = new AttrReflectionApp(); - el.setAttribute('wombats', '15'); + el.setAttribute(`wombats`, `15`); el.connectedCallback(); await raf(); const html = el.innerHTML; - expect(html.toLowerCase()).to.contain('
'); - expect(html).to.contain('Value of attribute wombats: 15'); + expect(html.toLowerCase()).to.contain(`
`); + expect(html).to.contain(`Value of attribute wombats: 15`); }); - it('reacts to attribute updates', async function() { + it(`reacts to attribute updates`, async function() { const el = new AttrReflectionApp(); - el.setAttribute('wombats', '15'); + el.setAttribute(`wombats`, `15`); el.connectedCallback(); await raf(); - expect(el.innerHTML).to.contain('Value of attribute wombats: 15'); - el.setAttribute('wombats', '32'); + expect(el.innerHTML).to.contain(`Value of attribute wombats: 15`); + el.setAttribute(`wombats`, `32`); await raf(); - expect(el.innerHTML).to.contain('Value of attribute wombats: 32'); - expect(el.innerHTML).not.to.contain('15'); + expect(el.innerHTML).to.contain(`Value of attribute wombats: 32`); + expect(el.innerHTML).not.to.contain(`15`); }); }); diff --git a/test/server/dom-patcher.js b/test/server/dom-patcher.js index 519a2859..94ab622b 100644 --- a/test/server/dom-patcher.js +++ b/test/server/dom-patcher.js @@ -1,134 +1,136 @@ +/* eslint-env mocha */ +/* eslint-disable no-unused-expressions */ // complains about .to.be.ok import '../../lib/isorender/dom-shims'; -import { expect } from 'chai'; +import {expect} from 'chai'; import requestAnimationFrameCB from 'raf'; -import { DOMPatcher, h } from '../../lib/dom-patcher'; +import {DOMPatcher, h} from '../../lib/dom-patcher'; const raf = () => new Promise(requestAnimationFrameCB); -describe('dom-patcher', function() { - context('when first initialized', function() { - const fooState = {foo: 'bar'}; - const domPatcher = new DOMPatcher(fooState, state => h('div', `Value of foo: ${state.foo}`)); +describe(`dom-patcher`, function() { + context(`when first initialized`, function() { + const fooState = {foo: `bar`}; + const domPatcher = new DOMPatcher(fooState, state => h(`div`, `Value of foo: ${state.foo}`)); - it('applies initial state', function() { + it(`applies initial state`, function() { expect(domPatcher.state).to.eql(fooState); }); - it('copies the initial state', function() { + it(`copies the initial state`, function() { expect(domPatcher.state).to.eql(fooState); expect(domPatcher.state).not.to.equal(fooState); }); - it('defaults to async mode', function() { - expect(domPatcher.updateMode).to.eql('async'); + it(`defaults to async mode`, function() { + expect(domPatcher.updateMode).to.eql(`async`); }); - it('creates a target DOM element', function() { + it(`creates a target DOM element`, function() { expect(domPatcher.el).to.be.ok; expect(domPatcher.el).to.be.an.instanceOf(Node); }); - it('applies the first patch immediately', function() { - expect(domPatcher.el.textContent).to.eql('Value of foo: bar'); + it(`applies the first patch immediately`, function() { + expect(domPatcher.el.textContent).to.eql(`Value of foo: bar`); }); }); - describe('target DOM element', function() { + describe(`target DOM element`, function() { let el; function patcherEl(renderFunc) { return new DOMPatcher({}, renderFunc).el; } - it('matches the tag type of the vtree root', function() { - el = patcherEl(() => h('div')); - expect(el.tagName).to.eql('div'); + it(`matches the tag type of the vtree root`, function() { + el = patcherEl(() => h(`div`)); + expect(el.tagName).to.eql(`div`); - el = patcherEl(() => h('span')); - expect(el.tagName).to.eql('span'); + el = patcherEl(() => h(`span`)); + expect(el.tagName).to.eql(`span`); }); - it('applies classes from the vtree root', function() { - el = patcherEl(() => h('span.foo')); - expect(el.className).to.eql('foo'); + it(`applies classes from the vtree root`, function() { + el = patcherEl(() => h(`span.foo`)); + expect(el.className).to.eql(`foo`); - el = patcherEl(() => h('span.foo.bar')); - expect(el.className).to.eql('foo bar'); + el = patcherEl(() => h(`span.foo.bar`)); + expect(el.className).to.eql(`foo bar`); }); - it('applies id from the vtree root', function() { - el = patcherEl(() => h('span')); + it(`applies id from the vtree root`, function() { + el = patcherEl(() => h(`span`)); expect(el.id).to.be.empty; - el = patcherEl(() => h('span#foo')); - expect(el.id).to.eql('foo'); + el = patcherEl(() => h(`span#foo`)); + expect(el.id).to.eql(`foo`); }); - it('combines classes and id from the vtree root', function() { - el = patcherEl(() => h('span#foo.bar')); - expect(el.id).to.eql('foo'); - expect(el.className).to.eql('bar'); + it(`combines classes and id from the vtree root`, function() { + el = patcherEl(() => h(`span#foo.bar`)); + expect(el.id).to.eql(`foo`); + expect(el.className).to.eql(`bar`); - el = patcherEl(() => h('span#foo.bar.baz')); - expect(el.id).to.eql('foo'); - expect(el.className).to.eql('bar baz'); + el = patcherEl(() => h(`span#foo.bar.baz`)); + expect(el.id).to.eql(`foo`); + expect(el.className).to.eql(`bar baz`); }); }); - context('in sync mode', function() { - it('renders updates immediately', function() { + context(`in sync mode`, function() { + it(`renders updates immediately`, function() { const domPatcher = new DOMPatcher( - {foo: 'bar'}, - state => h('div', `Value of foo: ${state.foo}`), - {updateMode: 'sync'} + {foo: `bar`}, + state => h(`div`, `Value of foo: ${state.foo}`), + {updateMode: `sync`} ); - expect(domPatcher.el.textContent).to.eql('Value of foo: bar'); - domPatcher.update({foo: 'moo'}); - expect(domPatcher.el.textContent).to.eql('Value of foo: moo'); + expect(domPatcher.el.textContent).to.eql(`Value of foo: bar`); + domPatcher.update({foo: `moo`}); + expect(domPatcher.el.textContent).to.eql(`Value of foo: moo`); expect(domPatcher.pending).to.be.false; expect(domPatcher.rendering).to.be.false; }); }); - context('in async mode', function() { + context(`in async mode`, function() { let domPatcher; beforeEach(function() { domPatcher = new DOMPatcher( - {foo: 'bar'}, - state => h('div', `Value of foo: ${state.foo}`), - {updateMode: 'async'} + {foo: `bar`}, + state => h(`div`, `Value of foo: ${state.foo}`), + {updateMode: `async`} ); }); - it('does not render updates immediately', function() { - expect(domPatcher.el.textContent).to.eql('Value of foo: bar'); - domPatcher.update({foo: 'moo'}); - expect(domPatcher.el.textContent).to.eql('Value of foo: bar'); + it(`does not render updates immediately`, function() { + expect(domPatcher.el.textContent).to.eql(`Value of foo: bar`); + domPatcher.update({foo: `moo`}); + expect(domPatcher.el.textContent).to.eql(`Value of foo: bar`); expect(domPatcher.pending).to.be.true; }); - it('renders updates on the next animation frame', async function() { - expect(domPatcher.el.textContent).to.eql('Value of foo: bar'); - domPatcher.update({foo: 'moo'}); + it(`renders updates on the next animation frame`, async function() { + expect(domPatcher.el.textContent).to.eql(`Value of foo: bar`); + domPatcher.update({foo: `moo`}); await raf(); - expect(domPatcher.el.textContent).to.eql('Value of foo: moo'); + expect(domPatcher.el.textContent).to.eql(`Value of foo: moo`); }); - it('applies only the last state in a given frame', async function() { - expect(domPatcher.el.textContent).to.eql('Value of foo: bar'); - domPatcher.update({foo: 'moo'}); - expect(domPatcher.el.textContent).to.eql('Value of foo: bar'); - domPatcher.update({foo: 'whew'}); - expect(domPatcher.el.textContent).to.eql('Value of foo: bar'); + it(`applies only the last state in a given frame`, async function() { + expect(domPatcher.el.textContent).to.eql(`Value of foo: bar`); + domPatcher.update({foo: `moo`}); + expect(domPatcher.el.textContent).to.eql(`Value of foo: bar`); + domPatcher.update({foo: `whew`}); + expect(domPatcher.el.textContent).to.eql(`Value of foo: bar`); await raf(); - expect(domPatcher.el.textContent).to.eql('Value of foo: whew'); + expect(domPatcher.el.textContent).to.eql(`Value of foo: whew`); }); }); });