UNPKG

mithril-query

Version:

Query mithril virtual dom for testing purposes

269 lines (237 loc) 6.49 kB
'use strict' const m = require('mithril/render/hyperscript') const domino = require('domino') const Event = require('domino/lib/Event') const code = require('yields-keycode') const Vnode = require('mithril/render/vnode') const formatHtml = require('pretty-html-log').highlight function isString(thing) { return Object.prototype.toString.call(thing) === '[object String]' } function isArray(thing) { return Object.prototype.toString.call(thing) === '[object Array]' } function isComponent(thing) { return !!( (thing && (typeof thing === 'object' && thing.view)) || isFunction(thing) || isClass(thing) ) } function isFunction(thing) { return typeof thing === 'function' && !isClass(thing) } function isBabelTranspiledClass(thing) { const code = thing.toString().replace(/^[^{]+{/, '') return ( // Regular Babel transpiled class /(?:^|\s+)_classCallCheck\(/.test(code) || // Babel with @babel/transform-runtime and Webpack /(?:^|\s+)_[^\s]+_classCallCheck__[^\s()]+\(/.test(code) || // Babel with @babel/transform-runtime (useESModules: true) and Webpack /(?:^|\s+)Object\(_[^\s]+_classCallCheck__[^\s()]+\)\(/.test(code) ) } function isClass(thing) { return ( typeof thing === 'function' && (/^\s*class\s/.test(thing.toString()) || // ES6 class isBabelTranspiledClass(thing)) // Babel class ) } function consoleLogHtml(els) { // eslint-disable-next-line no-console console.log(els.map(el => formatHtml(el.outerHTML)).join('---------\n')) } function scan(api) { const rootEl = api.rootEl function find(selectorString, node) { return Array.prototype.slice.call(node.querySelectorAll(selectorString)) } function first(selector) { const node = rootEl.querySelector(selector) if (!node) { throw new Error('No element matches ' + selector) } return node } function has(selector) { return find(selector, rootEl).length > 0 } function contains(value, node) { return !!find(':contains(' + value + ')', node).length } function shouldHaveAtLeast(minCount, selector) { const actualCount = find(selector, rootEl).length if (actualCount < minCount) { throw new Error( 'Wrong count of elements that matches "' + selector + '"\n expected: >=' + minCount + '\n actual: ' + actualCount ) } return true } function shouldHave(expectedCount, selector) { if (!selector) { return isArray(expectedCount) ? shouldHaveCollection(expectedCount) : shouldHaveAtLeast(1, expectedCount) } const actualCount = find(selector, rootEl).length if (actualCount !== expectedCount) { throw new Error( 'Wrong count of elements that matches "' + selector + '"\n expected: ' + expectedCount + '\n actual: ' + actualCount ) } return true } function shouldHaveCollection(selectors) { selectors.forEach(function(selector) { shouldHaveAtLeast(1, selector) }) return true } function shouldNotHave(selector) { shouldHave(0, selector) return true } function shouldContain(string) { if (!contains(string, rootEl)) { throw new Error('Expected "' + string + '" not found!') } return true } function shouldNotContain(string) { if (contains(string, rootEl)) { throw new Error('Unexpected "' + string + '" found!') } return true } function setValue(selector, string, eventData = {}) { const el = first(selector) el.value = string const inputEvent = new Event('input', eventData) const changeEvent = new Event('change', eventData) const keyupEvent = new Event('keyup', eventData) el.dispatchEvent(inputEvent) el.dispatchEvent(changeEvent) el.dispatchEvent(keyupEvent) if ( inputEvent.redraw !== false && changeEvent.redraw !== false && keyupEvent.redraw !== false ) { api.redraw() } } function trigger(eventName) { return function(selector, eventData) { const event = new Event(eventName, eventData) const el = first(selector) el.dispatchEvent(event) if (event.redraw !== false) { api.redraw() } } } function triggerKey(eventName) { const fire = trigger(eventName) return function handleEvent(selector, key, eventData = {}) { const keyCode = isString(key) ? code(key) : key const defaultEvent = { altKey: false, shiftKey: false, ctrlKey: false, type: eventName, keyCode, which: keyCode, } fire(selector, { ...defaultEvent, ...eventData }) } } shouldHave.at = { least: shouldHaveAtLeast, } api.first = first api.has = has api.contains = function(value) { return contains(value, rootEl) } api.find = function(selector) { return find(selector, rootEl) } api.setValue = setValue ;[ 'focus', 'click', 'blur', 'mousedown', 'mouseup', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave', 'mousemove', 'pointerdown', 'pointerup', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointermove', 'pointercancel', 'contextmenu', ].map(function(eventName) { api[eventName] = trigger(eventName) }) api.keydown = triggerKey('keydown') api.keypress = triggerKey('keypress') api.keyup = triggerKey('keyup') api.trigger = function(selector, eventName, event, silent) { trigger(eventName)(selector, event, silent) } api.should = { not: { have: shouldNotHave, contain: shouldNotContain, }, have: shouldHave, contain: shouldContain, } api.log = function(selector, logFn = consoleLogHtml) { logFn(api.find(selector)) return api } return api } module.exports = function init(componentOrRootNode, nodeOrAttrs) { const $window = domino.createWindow('') const render = require('mithril/render/render')($window) let rootNode = { view: () => { return isComponent(componentOrRootNode) ? m(componentOrRootNode, nodeOrAttrs) : componentOrRootNode }, } const redraw = () => render($window.document.body, Vnode(rootNode)) redraw() const onremove = () => { componentOrRootNode = null redraw() } return scan({ redraw, onremove, rootEl: $window.document.body, }) }