From 39c52a897385dc55fe8cd3977ed3714ad73cc81b Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Thu, 1 Mar 2018 14:45:52 +0100 Subject: [PATCH 001/127] wip: first stab at map/assert --- karma.conf.js | 2 +- lib/actions.js | 4 ++ lib/finders.js | 8 ++++ lib/selector.js | 19 ++++++++- test/eventsSpec.js | 10 +++++ test/findersSpec.js | 44 ++++++++++++++++++++ test/mocha.opts | 1 - test/selectorSpec.js | 96 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 test/selectorSpec.js diff --git a/karma.conf.js b/karma.conf.js index e7409c0..6e8a7e1 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -23,7 +23,7 @@ module.exports = function (config) { browserify: { debug: true, extensions: ['.jsx'], - transform: ['babelify', [require('./utils/removeStrictFromSend'), {global: true}]] + transform: ['babelify', [require('./utils/removeStrictFromSend'), {global: true}, 'transform-regenerator']] }, client: { diff --git a/lib/actions.js b/lib/actions.js index e4b70ba..113e620 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -43,6 +43,10 @@ module.exports = { }).catch(errorHandler(new Error())) }, + assert: function (assertion, options) { + return this.resolve({allowMultiple: true}) + }, + select: function (options) { if (typeof options === 'string') { var o = arguments[1] || {} diff --git a/lib/finders.js b/lib/finders.js index 7a53a7f..228aec0 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -86,6 +86,14 @@ module.exports = { } }, + map: function (mapper) { + return this.addFinder({ + find: function (elements) { + return mapper(elements) + } + }) + }, + containing: function (selector, options) { var $ = this.get('$') var message = options && JSON.stringify(options) diff --git a/lib/selector.js b/lib/selector.js index 62334ab..3155746 100644 --- a/lib/selector.js +++ b/lib/selector.js @@ -1,14 +1,31 @@ var elementTester = require('./elementTester') +var Conditions = require('./conditions') var global = require('global') +var retry = require('trytryagain') function Selector (selector, finders, options) { this._selector = selector - this._finders = finders || [] + this._conditions = new Conditions() this._options = options || { visibleOnly: true, $: require('./jquery'), document: global.document, immediate: false, timeout: 1000 } this._handlers = [] this._elementTesters = elementTester } +function retryWithOptions (options) { + if (options && (options.timeout || options.interval)) { + return function (fn) { + return retry(fn, options) + } + } else { + return retry + } +} + +Selector.prototype.wait = function (options) { + var retry = retryWithOptions(options) + return this._conditions.wait(this.scope, {retry: retry}) +} + Selector.prototype.set = function (options) { var self = this Object.keys(options).forEach(function (key) { diff --git a/test/eventsSpec.js b/test/eventsSpec.js index 11e738f..2db0d94 100644 --- a/test/eventsSpec.js +++ b/test/eventsSpec.js @@ -37,9 +37,15 @@ describe('events', function () { }) }) + function assertBrowserHasFocus () { + demand(document.hasFocus(), 'the browser must be in focus for this test!').to.equal(true) + } + domTest('typeIn element should fire change and then blur event on input', function (browser, dom) { var firedEvents = [] + assertBrowserHasFocus() + dom.insert('') dom.insert('') @@ -64,6 +70,8 @@ describe('events', function () { domTest('click element should fire blur event on input', function (browser, dom) { var blurred = false + assertBrowserHasFocus() + dom.insert('') dom.insert('') @@ -83,6 +91,8 @@ describe('events', function () { domTest('select element should fire blur event on input', function (browser, dom, $) { var blurred = false + assertBrowserHasFocus() + dom.insert('') dom.insert('') dom.el.find('input').on('blur', function (e) { diff --git a/test/findersSpec.js b/test/findersSpec.js index 098bf59..6bc0efd 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -1,5 +1,7 @@ +require('babel-polyfill') var demand = require('must') var domTest = require('./domTest') +var retry = require('trytryagain') describe('find', function () { domTest('should eventually find an element', function (browser, dom) { @@ -208,4 +210,46 @@ describe('find', function () { return demand(promise).reject.with.error() }) }) + + describe.only('map', function () { + domTest('can map element values into objects', function (browser, dom) { + var promise = browser.find('.content').map(function (element) { + return { + a: element.find('.a').text(), + b: element.find('.b').text() + } + }).assert(function (content) { + demand(content).to.eql({ + a: 'Aye', + b: 'Bee' + }) + }) + + setTimeout(function () { + dom.insert('
Aye
Bee
') + }, 10) + + return promise + }) + + domTest('can map element values into objects', function (browser, dom) { + var promise = browser.find('.content').map(function (element) { + return { + a: element.find('.a').text(), + b: element.find('.b').text() + } + }).assert(function (content) { + demand(content).to.eql({ + a: 'Axe', + b: 'Bee' + }) + }) + + setTimeout(function () { + dom.insert('
Aye
Bee
') + }, 10) + + return demand(promise).reject.with.error('Axe') + }) + }) }) diff --git a/test/mocha.opts b/test/mocha.opts index 84e148a..e69de29 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1 +0,0 @@ ---require ./test/global --compilers js:babel-register diff --git a/test/selectorSpec.js b/test/selectorSpec.js new file mode 100644 index 0000000..03e0ab1 --- /dev/null +++ b/test/selectorSpec.js @@ -0,0 +1,96 @@ +var trytryagain = require('trytryagain') +const {expect} = require('chai') + +describe.only('map', function () { + function Conditions () { + this.mappers = [] + } + + Conditions.prototype.clone = function (mapper) { + const s = new Conditions() + s.mappers = this.mappers.slice() + s._input = this._input + return s + } + + Conditions.prototype.map = function (mapper) { + this.mappers.push(mapper) + } + + Conditions.prototype.wait = function (input, {retry = trytryagain} = {}) { + return retry(() => { + return this.mappers.reduce((input, mapper) => { + return mapper(input) + }, input) + }) + } + + it('can transform an input', async () => { + const s = new Conditions() + s.map(x => x + 1) + s.map(x => x + 2) + const result = await s.wait(1) + expect(result).to.equal(4) + }) + + it('returns input when there are no mappers', async () => { + const s = new Conditions() + const result = await s.wait(1) + expect(result).to.equal(1) + }) + + it('returns a result when there are no failures', async () => { + const s = new Conditions() + let ready = false + + s.map(x => { + if (!ready) { + ready = true + throw new Error('not ready') + } + return x + 1 + }) + + const result = await s.wait(1) + + expect(result).to.equal(2) + }) + + it('can override retry function', async () => { + const s = new Conditions() + let ready = false + const events = [] + + s.map(x => { + if (!ready) { + ready = true + throw new Error('not ready') + } + return x + 1 + }) + + const retry = fn => { + events.push('retry') + try { + fn() + } catch (e) { + events.push('fail ' + e.message) + } + + const result = fn() + events.push('pass') + return result + } + + events.push('await') + const result = await s.wait(1, {retry}) + + expect(result).to.equal(2) + expect(events).to.eql([ + 'await', + 'retry', + 'fail not ready', + 'pass' + ]) + }) +}) From 94a357c41021a8dfb90ba703c6ecdcaea772e8f1 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Thu, 8 Mar 2018 11:29:00 +0100 Subject: [PATCH 002/127] wip: midway through complete rewrite --- electron-mocha | 1 + lib/actions.js | 86 +++-- lib/assertions.js | 23 +- lib/conditions.js | 24 ++ lib/elementClick.js | 8 + lib/elementEnterText.js | 22 ++ lib/elementMatches.js | 3 + lib/elementSelect.js | 9 + lib/elementSubmit.js | 14 + lib/elementTriggerEvent.js | 52 +++ lib/elementVisible.js | 7 + lib/finders.js | 423 +++++++++----------- lib/selector.js | 173 ++++++--- package.json | 2 + test/actionsSpec.js | 296 +++++++------- test/assemblies/DomAssembly.js | 120 ++++++ test/conditionsSpec.js | 107 +++++ test/describeAssemblies.js | 18 + test/domSpec.js | 476 +++++++++++++++++++++++ test/findersSpec.js | 254 +++++------- test/selectorSpec.js | 96 ----- test/selectorsSpec.js | 361 +++++++++++++++++ yarn.lock | 687 ++++++++++++++++++++++++++++++++- 23 files changed, 2543 insertions(+), 719 deletions(-) create mode 100755 electron-mocha create mode 100644 lib/conditions.js create mode 100644 lib/elementClick.js create mode 100644 lib/elementEnterText.js create mode 100644 lib/elementMatches.js create mode 100644 lib/elementSelect.js create mode 100644 lib/elementSubmit.js create mode 100644 lib/elementTriggerEvent.js create mode 100644 lib/elementVisible.js create mode 100644 test/assemblies/DomAssembly.js create mode 100644 test/conditionsSpec.js create mode 100644 test/describeAssemblies.js create mode 100644 test/domSpec.js delete mode 100644 test/selectorSpec.js create mode 100644 test/selectorsSpec.js diff --git a/electron-mocha b/electron-mocha new file mode 100755 index 0000000..278a9d5 --- /dev/null +++ b/electron-mocha @@ -0,0 +1 @@ +yarn electron-mocha --renderer $@ diff --git a/lib/actions.js b/lib/actions.js index 113e620..8adef74 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -1,6 +1,11 @@ var debug = require('debug')('browser-monkey') var sendkeys = require('./sendkeys') var errorHandler = require('./errorHandler') +var elementSelect = require('./elementSelect') +var elementClick = require('./elementClick') +var elementSubmit = require('./elementSubmit') +var elementEnterText = require('./elementEnterText') + function notSillyBlankIEObject (element) { return Object.keys(element).length > 0 } @@ -26,6 +31,14 @@ module.exports = { } }, + handleEvent: function (event) { + if (this._options.eventHandler) { + return Promise.resolve(this._options.eventHandler.call(undefined, event)) + } else { + return Promise.resolve() + } + }, + click: function (options) { var self = this @@ -35,44 +48,33 @@ module.exports = { return self.enabled().element(options).then(function (element) { debug('click', element) - self.handleEvent({type: 'click', element: element}) - self.focus(element, options) - element.trigger('mousedown') - element.trigger('mouseup') - element.trigger('click') + return self.handleEvent({type: 'click', element: element}).then(function () { + elementClick(element) + }) }).catch(errorHandler(new Error())) }, - assert: function (assertion, options) { - return this.resolve({allowMultiple: true}) - }, - select: function (options) { if (typeof options === 'string') { var o = arguments[1] || {} o.text = options return this.select(o) } - var $ = this.get('$') - var self = this - return this.is('select').find('option', options).elements(options).then(function (optionElements) { - var optionElement = $(optionElements[0]) - var selectElement = optionElement.parent() - self.focus(selectElement, options) - optionElement.prop('selected', true) - optionElement.attr('selected', 'selected') - selectElement.val(optionElement.val()) + var self = this + return this.is('select').find('option', options).element().then(function (optionElement) { + var selectElement = optionElement.parentNode debug('select', selectElement) - self.handleEvent({ + + return self.handleEvent({ type: 'select option', - value: selectElement.val(), + value: selectElement.value, element: selectElement, optionElement: optionElement + }).then(function () { + elementSelect(selectElement, optionElement) }) - - selectElement.trigger('change') }).catch(errorHandler(new Error())) }, @@ -82,23 +84,22 @@ module.exports = { } var self = this - return this.element(options).then(function (element) { + return this.is(inputsSelector).element().then(function (element) { debug('typeIn', element, text) - assertCanTypeIntoElement(element) - self.focus(element, options) - self.handleEvent({type: 'typing', text: text, element: element}) - return sendkeys(element, text) + return self.handleEvent({type: 'typing', text: text, element: element}).then(function () { + elementEnterText(element, text) + }) }).catch(errorHandler(new Error())) }, - submit: function (options) { + submit: function () { var self = this - return this.element(options).then(function (element) { + return this.element().then(function (element) { debug('submit', element) - self.focus(element, options) - self.handleEvent({type: 'submit', element: element}) - return element.trigger('submit') + return self.handleEvent({type: 'submit', element: element}).then(function () { + elementSubmit(element) + }) }).catch(errorHandler(new Error())) }, @@ -180,16 +181,19 @@ function inferField (component, field) { } } +var inputsSelector = + 'input:not([type]), ' + + 'input[type=text], ' + + 'input[type=email], ' + + 'input[type=password], ' + + 'input[type=search], ' + + 'input[type=tel], ' + + 'input[type=url], ' + + 'input[type=number],' + + 'textarea' + function canTypeIntoElement (element) { - return element.is('input:not([type]), ' + - 'input[type=text], ' + - 'input[type=email], ' + - 'input[type=password], ' + - 'input[type=search], ' + - 'input[type=tel], ' + - 'input[type=url], ' + - 'input[type=number],' + - 'textarea') + return element.is(inputsSelector) } function assertCanTypeIntoElement (element) { diff --git a/lib/assertions.js b/lib/assertions.js index 1203f54..b249d62 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1,10 +1,19 @@ var Options = require('./options') var expectOneElement = require('./expectOneElement') var errorHandler = require('./errorHandler') +const {expect} = require('chai') module.exports = { - is: function (css) { - return this.addFinder(this.createElementTester({css: css})) + shouldEqual: function (value) { + return this.ensure(function (result) { + expect(result).to.equal(value) + }) + }, + + shouldDeepEqual: function (value) { + return this.ensure(function (result) { + expect(result).to.eql(value) + }) }, exists: function (options) { @@ -12,7 +21,7 @@ module.exports = { }, shouldExist: function (options) { - return this.resolve(options) + return this.some() .catch(errorHandler(new Error())) }, @@ -22,7 +31,7 @@ module.exports = { }, shouldNotExist: function (options) { - return this.notResolve(options) + return this.zero() .catch(errorHandler(new Error())) }, @@ -30,7 +39,11 @@ module.exports = { return this.shouldHave(options) }, - shouldHave: function (options) { + shouldHaveOld: function (options) { + return this.mapAll(function (elements) { + expectElements(elements) + elementExpectations(elements, options) + }) var self = this var resolveOptions = Options.remove(options, ['timeout', 'interval']) diff --git a/lib/conditions.js b/lib/conditions.js new file mode 100644 index 0000000..10c3b8a --- /dev/null +++ b/lib/conditions.js @@ -0,0 +1,24 @@ +var trytryagain = require('trytryagain') + +function Conditions () { + this.mappers = [] +} + +Conditions.prototype.clone = function (mapper) { + const s = new Conditions() + s.mappers = this.mappers.slice() + s._input = this._input + return s +} + +Conditions.prototype.map = function (mapper) { + this.mappers.push(mapper) +} + +Conditions.prototype.result = function (input, {retry = trytryagain, reducer = (input, map) => map(input)} = {}) { + return Promise.resolve(retry(() => { + return this.mappers.reduce(reducer, input) + })) +} + +module.exports = Conditions diff --git a/lib/elementClick.js b/lib/elementClick.js new file mode 100644 index 0000000..1c34d13 --- /dev/null +++ b/lib/elementClick.js @@ -0,0 +1,8 @@ +var elementTriggerEvent = require('./elementTriggerEvent') + +module.exports = function elementClick (element) { + elementTriggerEvent(element, 'mousedown') + elementTriggerEvent(element, 'mouseup') + element.click() + element.focus() +} diff --git a/lib/elementEnterText.js b/lib/elementEnterText.js new file mode 100644 index 0000000..46fc62b --- /dev/null +++ b/lib/elementEnterText.js @@ -0,0 +1,22 @@ +var elementTriggerEvent = require('./elementTriggerEvent') + +module.exports = function elementEnterText (element, text) { + element.focus() + + if (element.value !== '') { + elementTriggerEvent(element, 'keydown') + elementTriggerEvent(element, 'keypress') + element.value = '' + elementTriggerEvent(element, 'input') + elementTriggerEvent(element, 'keyup') + } + + text.split('').forEach(function (key, index) { + elementTriggerEvent(element, 'keydown', key) + elementTriggerEvent(element, 'keypress', key) + element.value = text.slice(0, index + 1) + elementTriggerEvent(element, 'input') + elementTriggerEvent(element, 'keyup', key) + }) + elementTriggerEvent(element, 'change') +} diff --git a/lib/elementMatches.js b/lib/elementMatches.js new file mode 100644 index 0000000..5222b15 --- /dev/null +++ b/lib/elementMatches.js @@ -0,0 +1,3 @@ +module.exports = function elementMatches (element, selector) { + return element.matches(selector) +} diff --git a/lib/elementSelect.js b/lib/elementSelect.js new file mode 100644 index 0000000..0591ae1 --- /dev/null +++ b/lib/elementSelect.js @@ -0,0 +1,9 @@ +var elementClick = require('./elementClick') +var elementTriggerEvent = require('./elementTriggerEvent') + +module.exports = function elementSelect (selectElement, optionElement) { + elementClick(selectElement) + selectElement.selectedIndex = optionElement.index + elementTriggerEvent(selectElement, 'input') + elementTriggerEvent(selectElement, 'change') +} diff --git a/lib/elementSubmit.js b/lib/elementSubmit.js new file mode 100644 index 0000000..de460f1 --- /dev/null +++ b/lib/elementSubmit.js @@ -0,0 +1,14 @@ +var elementTriggerEvent = require('./elementTriggerEvent') + +module.exports = function elementSubmit (element) { + element.focus() + elementTriggerEvent(element, 'keydown') + elementTriggerEvent(element, 'keypress') + var submitButton = element.form.querySelector('input[type="submit"], button[type="submit"]') + if (submitButton) { + submitButton.click() + } else { + elementTriggerEvent(element.form, 'submit') + } + elementTriggerEvent(element, 'keyup') +} diff --git a/lib/elementTriggerEvent.js b/lib/elementTriggerEvent.js new file mode 100644 index 0000000..8318241 --- /dev/null +++ b/lib/elementTriggerEvent.js @@ -0,0 +1,52 @@ +/* global Event KeyboardEvent MouseEvent */ + +module.exports = function (element, eventType, value) { + var creator = eventCreatorsByType[eventType] + + if (!creator) { + throw new Error('event type ' + JSON.stringify(eventType) + ' not recognised') + } + + var event = creator(value) + + element.dispatchEvent(event) +} + +function createMouseEvent (type) { + return new MouseEvent(type, {bubbles: true, cancelable: true}) +} + +function createEvent (type) { + return new Event(type, {bubbles: true, cancelable: false}) +} + +function createKeyboardEvent (type, key) { + return new KeyboardEvent(type, {bubbles: true, cancelable: true, key: key}) +} + +var eventCreatorsByType = { + mousedown: function () { + return createMouseEvent('mousedown') + }, + mouseup: function () { + return createMouseEvent('mouseup') + }, + change: function () { + return createEvent('change') + }, + input: function () { + return createEvent('input') + }, + keydown: function (key) { + return createKeyboardEvent('keydown', key) + }, + keyup: function (key) { + return createKeyboardEvent('keyup', key) + }, + keypress: function (key) { + return createKeyboardEvent('keypress', key) + }, + submit: function () { + return new Event('submit', {bubbles: true, cancelable: true}) + } +} diff --git a/lib/elementVisible.js b/lib/elementVisible.js new file mode 100644 index 0000000..a1e8517 --- /dev/null +++ b/lib/elementVisible.js @@ -0,0 +1,7 @@ +module.exports = function elementVisible (element) { + if (element.tagName === 'OPTION' && element.parentNode) { + return elementVisible(element.parentNode) + } else { + return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length) + } +} diff --git a/lib/finders.js b/lib/finders.js index 228aec0..a089cb8 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -1,291 +1,238 @@ -var Options = require('./options') -var expectOneElement = require('./expectOneElement') -var window = require('global') +var flatten = require('lowscore/flatten') +var expect = require('chai').expect +const elementVisible = require('./elementVisible') +const elementMatches = require('./elementMatches') module.exports = { - elementFinder: function (css) { - var $ = this.get('$') + find: function (selector, options) { var self = this - return { - find: function (element) { - var els = $(element).find(css) - if (self.get('visibleOnly')) { - els = els.filter(function (index) { - var el = this[index] || this - var ignoreVisibilityOfTags = ['OPTION'] - if (el && ignoreVisibilityOfTags.indexOf(el.tagName) !== -1) { - el = el.parentNode - } - return $(el).is(':visible') - }) - } - if (els.length > 0) { - return els - } - }, - - toString: function () { - return css - } - } - }, - addFinder: function (finder) { - var finders = this._finders && (this._finders.slice() || []) - finders.push(finder) - return this.clone({_finders: finders}) + return this.mapAll(function (elements) { + expectElements(elements) + return flatten( + elements.map(function (element) { + return findElements(element, selector, options, self._options) + }) + ) + }, 'find: ' + selector + (options ? ' ' + findOptionsToString(options) : '')) }, - createElementTester: function (criteria) { - var self = this - - if (typeof criteria === 'string') { - criteria = { css: criteria } - } + is: function (selector) { + return this.filter(function (element) { + return elementMatches(element, selector) + }, 'is: ' + selector) + }, - if (typeof criteria === 'function') { - criteria = { predicate: criteria } - } + text: function () { + return this.element().then(function (element) { + return element.innerText + }) + }, - return { - find: function ($el) { - var message = criteria.message - Object.keys(criteria).forEach(function (key) { - var value = criteria[key] - var tester = self._elementTesters[key] - - if (value !== undefined && tester !== undefined) { - tester.call(self, $el, message, value) - } - }) - return $el - }, + attr: function (name) { + return this.element().then(function (element) { + return element.getAttribute(name) + }) + }, - toString: function () { - return criteria.message || criteria.css || criteria.text - } - } + prop: function (name) { + return this.element().then(function (element) { + return element[name] + }) }, - find: function (selector, options) { - var message = JSON.stringify(options) - var scope = this.addFinder(this.elementFinder(selector)) - - if (options) { - var tester = this.createElementTester(options) - - return scope.filter(function (element) { - try { - return tester.find(element) - } catch (error) { - return false - } - }, message) - } else { - return scope - } + html: function () { + return this.element().then(function (element) { + return element.innerHTML + }) }, - map: function (mapper) { - return this.addFinder({ - find: function (elements) { - return mapper(elements) - } + val: function () { + return this.element().then(function (element) { + return element.value }) }, containing: function (selector, options) { - var $ = this.get('$') - var message = options && JSON.stringify(options) - var findElements = this.elementFinder(selector) - var finder - - if (options) { - var tester = this.createElementTester(options) - - finder = { - find: function (elements) { - var found = findElements.find(elements) - var tested = found.toArray().filter(function (element) { - try { - tester.find(element) - return true - } catch (error) { - return false - } - }) - - if (tested.length > 0) { - return tested - } - }, - - toString: function () { - return selector + (message ? ' ' + message : '') - } - } - } else { - finder = findElements - } - - return this.addFinder({ - find: function (elements) { - var els = Array.prototype.filter.call(elements, function (el) { - try { - return finder.find(el) - } catch (e) { - return false - } - }) + var self = this - if (els.length > 0) { - return $(els) - } - }, + return this.filter(function (element) { + return findElements(element, selector, options, self._options).length > 0 + }, 'containing: ' + selector + (options ? ' ' + findOptionsToString(options) : '')) + }, - toString: function () { - return ':has(' + finder.toString() + ')' + iframe: function () { + return this.map(function (element) { + if (isIframe(element)) { + return element.contentDocument.body + } else { + return element } - }) + }, 'iframe') }, - linkOrButton: function (label) { - return this.find( - 'a, button, input[type=submit], input[type=button], input[type=reset]', - {label: label}, - '[linkOrButton: ' + label + ']' - ) - }, + scope: function (scope) { + var selector = this.value([scope]) - button: function (label) { - return this.find( - 'button, input[type=submit], input[type=button], input[type=reset]', - {label: label}, - '[button: ' + label + ']' - ) + if (isIframe(scope)) { + return selector.iframe() + } else { + return selector + } }, - link: function (label) { - return this.find('a', {label: label}, '[link: ' + label + ']') + enabled: function () { + return this.filter(function (element) { + var tagName = element.tagName + return !((tagName === 'BUTTON' || tagName === 'INPUT') && element.disabled) + }, '[disabled=false]') }, - elements: function (options) { - options = Options.default(options, {allowMultiple: true}) - return this.resolve(options) + element: function () { + return this.one().then(function (element) { + if (isHTMLElement(element)) { + return element + } else { + throw new Error('expected DOM element') + } + }) }, - element: function (options) { - var $ = this.get('$') - return this.resolve(options).then(function (elements) { - return $(elements[0]) + elements: function () { + return this.some().then(function (elements) { + if (isArrayOfHTMLElements(elements)) { + return elements + } else { + throw new Error('expected DOM elements') + } }) }, - printFinders: function (finders) { - return finders.map(function (f) { return f.toString() }).join(' ').replace(/\s+:/g, ':') - }, + shouldHave: function (options) { + return this.mapAll(function (elements) { + expectElements(elements) + assertElementExpectations(elements, options) + }, findOptionsToString(options)) + } +} - findElements: function (options) { - var $ = this.get('$') - var self = this - var allowMultiple = Options.get(options, 'allowMultiple') +function toArray (i) { + return i instanceof Array ? i : [i] +} - function findWithFinder (el, finderIndex) { - var finder = self._finders[finderIndex] +var elementExpectations = { + text: function (elements, expected) { + var expectedArray = toArray(expected) - if (finder) { - var found = finder.find(el) + var actual = elements.map(function (element) { + return element.innerText + }) - if (!found) { - throw new Error('expected to find: ' + self.printFinders(self._finders)) - } + expect(actual).to.eql(expectedArray) + } +} - return findWithFinder(found, finderIndex + 1) - } else { - return el - } - }; - - function selector () { - var selector = self._selector - if ( - selector && - typeof window.Element !== 'undefined' && - selector instanceof window.Element && - selector.tagName === 'IFRAME' - ) { - return selector.contentDocument - } else if ( - selector && - typeof selector.prop === 'function' && - selector.prop('tagName') === 'IFRAME' - ) { - return selector[0].contentDocument - } else { - return selector || 'html' - } - } +function assertElementExpectations (elements, options) { + Object.keys(options).forEach(key => { + var expectation = elementExpectations[key] - var elements = findWithFinder($(selector()), 0) - if (!allowMultiple) { - expectOneElement(self, elements) + if (!expectation) { + throw new Error('no such expectation ' + key + ', try one of: ' + Object.keys(elementExpectations).join(', ')) } - return elements.toArray() - }, - - resolve: function (options) { - var self = this - var defaultTimeout = this.get('timeout') || 1000 - var retryOptions = Options.remove(options, ['timeout', 'interval']) - retryOptions.timeout = retryOptions.timeout || defaultTimeout + expectation(elements, options[key]) + }) +} - var result = this.retry(retryOptions, function () { - return self.findElements(options) +function isArrayOfHTMLElements (elements) { + return elements instanceof Array && + elements.every(function (element) { + return isHTMLElement(element) }) +} - return result - }, +function expectElements (elements) { + if (!isArrayOfHTMLElements(elements)) { + throwElementsError(elements, 'expected HTML elements') + } +} - notResolve: function (options) { - var self = this +function expectOneElement (elements) { + if (elements.length > 1) { + throwElementsError(elements, 'expected just one element') + } else if (elements.length === 0) { + throw new Error('expected at least one element') + } +} - return this.retry(options, function () { - var found = false - try { - self.findElements({allowMultiple: true}) - found = true - } catch (e) { - } - if (found) { - throw new Error("didn't expect to find element: " + self.printFinders(self._finders)) - } - }) +function throwElementsError (elements, message) { + var error = new Error(message) + error.elements = elements + throw error +} + +var findOptionFilters = { + text: function (element, text) { + return element.innerText.indexOf(text) !== -1 }, - filter: function (filter, message) { - var $ = this.get('$') - return this.addFinder({ - find: function (elements) { - var filteredElements = elements.toArray().filter(function (element) { - return filter($(element)) - }) - if (filteredElements && filteredElements.length > 0) { - return $(filteredElements) - } - }, + exactText: function (element, text) { + return element.innerText === text + }, - toString: function () { - return message || '[filter]' - } + index: function (element, expectedIndex, actualIndex) { + return expectedIndex === actualIndex + } +} + +function findByOptions (elements, options) { + var filters = Object.keys(options).map(function (key) { + var filter = findOptionFilters[key] + if (!filter) { + throw new Error('no such filter ' + key + ', try one of: ' + Object.keys(findOptionFilters).join(', ')) + } + return { + filter: filter, + expected: options[key] + } + }) + + return elements.filter(function (element, index) { + return filters.every(function (filter) { + return filter.filter(element, filter.expected, index) }) - }, + }) +} - enabled: function () { - return this.filter(function (element) { - var tagName = element.prop('tagName') - return !((tagName === 'BUTTON' || tagName === 'INPUT') && element.prop('disabled')) - }, '[disabled=false]') +function isHTMLElement (element, subclass) { + if (element.ownerDocument && element.ownerDocument.defaultView) { + // an element inside an iframe + return element instanceof element.ownerDocument.defaultView[subclass || 'HTMLElement'] + } else { + return false } +} + +function findElements (element, selector, findOptions, selectorOptions) { + var children = Array.prototype.slice.call(element.querySelectorAll(selector)) + + children = selectorOptions.visibleOnly + ? children.filter(elementVisible) + : children + + children = findOptions + ? findByOptions(children, findOptions) + : children + + return children +} + +function isIframe (element) { + return isHTMLElement(element, 'HTMLIFrameElement') +} +function findOptionsToString (options) { + return '(' + Object.keys(options).map(function (key) { + return key + ' = ' + JSON.stringify(options[key]) + }) + ')' } diff --git a/lib/selector.js b/lib/selector.js index 3155746..631c1c1 100644 --- a/lib/selector.js +++ b/lib/selector.js @@ -1,86 +1,171 @@ -var elementTester = require('./elementTester') var Conditions = require('./conditions') -var global = require('global') +var extend = require('lowscore/extend') var retry = require('trytryagain') -function Selector (selector, finders, options) { - this._selector = selector +function Selector () { this._conditions = new Conditions() - this._options = options || { visibleOnly: true, $: require('./jquery'), document: global.document, immediate: false, timeout: 1000 } - this._handlers = [] - this._elementTesters = elementTester + this._options = { + visibleOnly: true + } + this._value = undefined } -function retryWithOptions (options) { +function retryFromOptions (options) { if (options && (options.timeout || options.interval)) { return function (fn) { return retry(fn, options) } + } else if (options && options.retry) { + return options.retry } else { return retry } } -Selector.prototype.wait = function (options) { - var retry = retryWithOptions(options) - return this._conditions.wait(this.scope, {retry: retry}) +Selector.prototype.mapAll = function (mapper, name) { + return this.clone(function (clone) { + clone._conditions.map({ + name: name, + map: mapper + }) + }) } -Selector.prototype.set = function (options) { - var self = this - Object.keys(options).forEach(function (key) { - self._options[key] = options[key] +Selector.prototype.map = function (mapper, name) { + return this.clone(function (clone) { + clone._conditions.map({ + name: name, + map: function (elements) { + return elements.map(mapper) + } + }) }) - return this } -Selector.prototype.get = function (key) { - return this._options[key] +Selector.prototype.filter = function (filter, name) { + return this.clone(function (clone) { + clone._conditions.map({ + name: name, + map: function (elements) { + return elements.filter(filter) + } + }) + }) } -Selector.prototype.clone = function (extension) { - var clone = new this.constructor() - var self = this - - Object.keys(self).forEach(function (key) { - clone[key] = self[key] +Selector.prototype.options = function (options) { + return this.clone(function (clone) { + clone._options = extend({}, clone._options, options) }) +} - Object.keys(extension).forEach(function (key) { - clone[key] = extension[key] +Selector.prototype.then = function () { + var self = this + var retry = retryFromOptions(this._options) + var accumulator = {} + var promise = this._conditions.result(accumulator, { + retry: retry, + reducer: (accumulator, mapper, index) => { + if (index === 0) { + accumulator.value = self._value + accumulator.transforms = [] + } + accumulator.value = mapper.map(accumulator.value) + accumulator.transforms.push({ + value: accumulator.value, + name: mapper.name + }) + return accumulator + } + }).then(function (accumulator) { + return accumulator.value + }, function (error) { + var foundMessage = accumulator.transforms.map(function (transform) { + var v = transform.value instanceof Array + ? '[' + transform.value.length + ']' + : '(' + typeof transform.value + ')' + return transform.name + ' ' + v + }).join(', ') + throw new Error(error.message + ' (found: ' + foundMessage + ')') }) + return promise.then.apply(promise, arguments) +} - return clone +Selector.prototype.catch = function (fn) { + return this.then(undefined, fn) } -Selector.prototype.on = function (handler) { - this._handlers.push(handler) - return this +function copySelectorFields (from, to) { + to._conditions = from._conditions.clone() + to._value = from._value + to._options = from._options } -Selector.prototype.handleEvent = function () { - var args = arguments +Selector.prototype.clone = function (modifier) { + var clone = new this.constructor() + copySelectorFields(this, clone) + if (modifier) { + modifier(clone) + } + return clone +} - this._handlers.forEach(function (handler) { - handler.apply(undefined, args) +Selector.prototype.ensure = function (assertion) { + return this.mapAll(value => { + assertion(value) + return value }) } -Selector.prototype.scope = function (scope) { - if (scope instanceof Selector) { - return this.clone(scope) - } else { - return this.clone({_selector: scope}) - } +Selector.prototype.zero = function () { + return this.mapAll(results => { + if (results instanceof Array) { + if (results.length !== 0) { + throw new Error('expected no elements') + } else { + return results + } + } else { + return results + } + }, 'zero') +} + +Selector.prototype.one = function () { + return this.mapAll(results => { + if (results instanceof Array) { + if (results.length === 1) { + return results[0] + } else { + throw new Error('expected one element') + } + } else { + return results + } + }, 'one') } -Selector.prototype.extend = function (methods) { - return this.component(methods) +Selector.prototype.some = function () { + return this.mapAll(results => { + if (results instanceof Array && results.length !== 0) { + return results + } else { + throw new Error('expected some elements') + } + }, 'some') +} + +Selector.prototype.value = function (value) { + return this.clone(function (clone) { + clone._value = value + }) } Selector.prototype.component = function (methods) { + var self = this + function Component () { - Selector.apply(this, arguments) + copySelectorFields(self, this) } Component.prototype = new this.constructor() @@ -89,7 +174,7 @@ Selector.prototype.component = function (methods) { }) Component.prototype.constructor = Component - return new Component().scope(this) + return new Component() } module.exports = Selector diff --git a/package.json b/package.json index 667db12..bffa367 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "babelify": "^7.3.0", "browserify": "13.3.0", "detect-node": "^2.0.3", + "electron": "^1.8.2", + "electron-mocha": "^6.0.1", "express": "^4.15.2", "httpism": "^2.6.2", "hyperdom": "^0.5.0", diff --git a/test/actionsSpec.js b/test/actionsSpec.js index f466473..74cc69e 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -1,8 +1,20 @@ +console.log('here') +var describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') var demand = require('must') var domTest = require('./domTest') var retry = require('trytryagain') describe('actions', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + describe('clicking', function () { domTest('stack trace', function (browser, dom) { return browser.find('div') @@ -12,12 +24,12 @@ describe('actions', function () { mochaOnly: true }) - domTest('should eventually click an element', function (browser, dom, $) { + it('should eventually click an element', function () { var promise = browser.find('.element').click() var clicked = false - dom.eventuallyInsert( - $('
').on('click', function () { + assembly.eventuallyInsertHtml( + assembly.jQuery('
').on('click', function () { clicked = true }) ) @@ -27,72 +39,76 @@ describe('actions', function () { }) }) - domTest('sends mousedown mouseup and click events', function (browser, dom) { + it('sends mousedown mouseup and click events', function () { var events = [] - dom.insert('
').on('mousedown', function () { - events.push('mousedown') - }).on('mouseup', function () { - events.push('mouseup') - }).on('click', function () { - events.push('click') - }) + assembly.insertHtml( + assembly.jQuery('
').on('mousedown', function () { + events.push('mousedown') + }).on('mouseup', function () { + events.push('mouseup') + }).on('click', function () { + events.push('click') + }) + ) return browser.find('.element').click().then(function () { demand(events).to.eql(['mousedown', 'mouseup', 'click']) }) }) - domTest('mousedown mouseup and click events bubble up to parent', function (browser, dom) { + it('mousedown mouseup and click events bubble up to parent', function () { var events = [] - dom.insert('
inner
').on('mousedown', function () { - events.push('mousedown') - }).on('mouseup', function () { - events.push('mouseup') - }).on('click', function () { - events.push('click') - }) + assembly.insertHtml( + assembly.jQuery('
inner
').on('mousedown', function () { + events.push('mousedown') + }).on('mouseup', function () { + events.push('mouseup') + }).on('click', function () { + events.push('click') + }) + ) return browser.find('.inner-element').click().then(function () { demand(events).to.eql(['mousedown', 'mouseup', 'click']) }) }, {vdom: false}) - domTest('waits until checkbox is enabled before clicking', function (browser, dom) { + it('waits until checkbox is enabled before clicking', function () { var promise = browser.find('input[type=checkbox]').click() var clicked var buttonState = 'disabled' - var button = dom.insert('') + var button = assembly.jQuery(assembly.insertHtml('')) button.on('click', function () { clicked = buttonState }) - setTimeout(function () { + assembly.eventually(function () { button.prop('disabled', false) buttonState = 'enabled' - }, 10) + }) return promise.then(function () { demand(clicked).to.equal('enabled') }) }) - domTest('waits until button is enabled before clicking', function (browser, dom) { + it('waits until button is enabled before clicking', function () { var promise = browser.find('button', {text: 'a button'}).click() var clicked var buttonState = 'disabled' - var button = dom.insert('') + var button = assembly.jQuery(assembly.insertHtml('')) button.on('click', function () { clicked = buttonState }) - setTimeout(function () { + assembly.eventually(function () { button.prop('disabled', false) buttonState = 'enabled' - }, 10) + }) return promise.then(function () { demand(clicked).to.equal('enabled') @@ -110,33 +126,13 @@ describe('actions', function () { }) describe('text', function () { - domTest('respects timeout option', function (browser, dom, $) { - var promise = browser.find('.element').select({text: 'Second', timeout: 3}) - - dom.eventuallyInsert( - $('') - , 6) - - return demand(promise).reject.with.error() - }) - - domTest('respects timeout option, when passed separately from text', function (browser, dom, $) { - var promise = browser.find('.element').select('Second', {timeout: 3}) - - dom.eventuallyInsert( - $('') - , 6) - - return demand(promise).reject.with.error('expected to find: .element select option {"timeout":3,"text":"Second"}') - }) - - domTest('eventually selects an option element using the text', function (browser, dom, $) { + it('eventually selects an option element using the text', function () { var promise = browser.find('.element').select({text: 'Second'}) var selectedItem - dom.eventuallyInsert( - $('').on('change', function () { - selectedItem = $(this).find('option[selected]').text() + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function () { + selectedItem = assembly.jQuery(this).find(':selected').text() }) ) @@ -145,13 +141,13 @@ describe('actions', function () { }) }) - domTest('eventually selects an option element using the text, when text is passed as a string', function (browser, dom, $) { + it('eventually selects an option element using the text, when text is passed as a string', function () { var promise = browser.find('.element').select('Second') var selectedItem - dom.eventuallyInsert( - $('').on('change', function () { - selectedItem = $(this).find('option[selected]').text() + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function () { + selectedItem = assembly.jQuery(this).find(':selected').text() }) ) @@ -160,13 +156,13 @@ describe('actions', function () { }) }) - domTest('should eventually select an option element using a partial match', function (browser, dom, $) { + it('should eventually select an option element using a partial match', function () { var promise = browser.find('.element').select({text: 'Seco'}) var selectedItem - dom.eventuallyInsert( - $('').on('change', function (e) { - selectedItem = $(this).find('option[selected]').text() + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').text() }) ) @@ -175,56 +171,61 @@ describe('actions', function () { }) }) - domTest('selects the first match if multiple available', function (browser, dom, $) { + it('selects the option by index names are ambiguous', function () { var selectedItem - var select = dom.insert('').on('change', function (e) { - selectedItem = select.val() - }) + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + var select = e.target + selectedItem = select.value + }) + ) - return browser.find('select').select({text: 'Item'}).then(function () { - demand(selectedItem).to.equal('1') + return browser.find('select').select({index: 1}).then(function () { + demand(selectedItem).to.equal('2') }) }) - domTest('selects an option that eventually appears', function (browser, dom, $) { + it('selects an option that eventually appears', function () { var promise = browser.find('.element').select({text: 'Second'}) var selectedItem - var select = dom.insert('').on('change', function (e) { - selectedItem = $(this).find('option[selected]').text() - }) + var select = assembly.insertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) - setTimeout(function () { - select.append('') - }, 20) + assembly.eventuallyInsertHtml('', select) return promise.then(function () { demand(selectedItem).to.equal('Second') }) }) - domTest('errors when the specified option does not exist', function (browser, dom) { + it('errors when the specified option does not exist', function () { var promise = browser.find('.element').select({text: 'Does not exist'}) - dom.eventuallyInsert('') + assembly.eventuallyInsertHtml('') return demand(promise).reject.with.error() }) - domTest('errors when the input is not a select', function (browser, dom) { + it('errors when the input is not a select', function () { var promise = browser.find('.element').select({text: 'Whatevs'}) - dom.eventuallyInsert('
') - return demand(promise).reject.with.error(/to have css select/) + assembly.eventuallyInsertHtml('
') + return demand(promise).reject.with.error(/expected one element/) }) - domTest('selects an option using text that is falsy', function (browser, dom, $) { + it('selects an option using text that is falsy', function () { var promise = browser.find('.element').select({text: 0}) var selectedItem - dom.insert('').on('change', function (e) { - selectedItem = $(this).find('option[selected]').text() - }) + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) return promise.then(function () { demand(selectedItem).to.equal('0') @@ -233,26 +234,30 @@ describe('actions', function () { }) describe('exactText', function () { - domTest('should select an option using exact text that would otherwise match multiple options', function (browser, dom, $) { + it('should select an option using exact text that would otherwise match multiple options', function () { var promise = browser.find('.element').select({exactText: 'Mr'}) var selectedItem - dom.insert('').on('change', function (e) { - selectedItem = $(this).find('option[selected]').text() - }) + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) return promise.then(function () { demand(selectedItem).to.equal('Mr') }) }) - domTest('should select an option using exact text that is falsy', function (browser, dom, $) { - var promise = browser.find('.element').select({exactText: 0}) + it('should select an option using exact text that is falsy', function () { + var promise = browser.find('.element').select({exactText: ''}) var selectedItem - dom.insert('').on('change', function (e) { - selectedItem = $(this).find('option[selected]').text() - }) + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').val() + }) + ) return promise.then(function () { demand(selectedItem).to.equal('0') @@ -270,32 +275,36 @@ describe('actions', function () { mochaOnly: true }) - domTest('should submit the form', function (browser, dom) { + it('should submit the form', function () { var submitted = false var promise = browser.find('input').submit() - dom.insert('
').on('submit', function (ev) { - submitted = true - }) + assembly.eventuallyInsertHtml( + assembly.jQuery('
').on('submit', function (ev) { + submitted = true + }) + ) return promise.then(function () { demand(submitted).to.equal(true) }) }) - domTest('should submit the form when submit button is clicked', function (browser, dom) { + it('should submit the form when submit button is clicked', function () { var submitted = false var promise = browser.find('input').click() - dom.insert('
submit
').on('submit', function (ev) { - ev.preventDefault() - submitted = true - }) + assembly.eventuallyInsertHtml( + assembly.jQuery('
submit
').on('submit', function (ev) { + ev.preventDefault() + submitted = true + }) + ) return promise.then(function () { demand(submitted).to.equal(true) }) - }, {vdom: false}) + }) }) describe('typeIn', function () { @@ -320,11 +329,12 @@ describe('actions', function () { ] allowedToTypeInto.forEach(function (html) { - domTest('eventually enters text into: ' + html, function (browser, dom) { - var promise = browser.find('.element').typeIn('1234') - dom.eventuallyInsert(html) - return promise.then(function () { - demand(dom.el.find('.element').val()).to.equal('1234') + it('eventually enters text into: ' + html, function () { + var typeInPromise = browser.find('.element').typeIn('1234') + var inputPromise = assembly.eventuallyInsertHtml(html) + return Promise.all([typeInPromise, inputPromise]).then(function (elements) { + var input = elements[1] + demand(input.value).to.equal('1234') }) }) }) @@ -336,26 +346,33 @@ describe('actions', function () { ] notAllowedToTypeInto.forEach(function (html) { - domTest('rejects attempt to type into element: ' + html, function (browser, dom, $) { + it('rejects attempt to type into element: ' + html, function () { var promise = browser.find('.element').typeIn('whatevs') - dom.eventuallyInsert(html) - return demand(promise).reject.with.error('Cannot type into ' + $(html).prop('tagName')) + var inputPromise = assembly.eventuallyInsertHtml(html) + return inputPromise.then(function (input) { + return demand(promise).reject.with.error(/expected one element/) + }) }) }) - domTest('blanks out existing text when typing empty text', function (browser, dom) { + it('blanks out existing text when typing empty text', function () { var firedEvents = [] - dom.insert('') - .on('input', function (ev) { firedEvents.push('input') }) + assembly.insertHtml( + assembly.jQuery('') + .on('input', function (ev) { + firedEvents.push('input') + }) + ) return browser.find('.element').typeIn('').then(function () { return retry(function () { - demand(dom.el.find('input.element').val()).to.equal('') + demand(assembly.find('input.element').value).to.equal('') demand(firedEvents).to.eql(['input']) }) }) }) }) + describe('typeInHtml', function () { domTest('stack trace', function (browser, dom) { return browser.find('input') @@ -367,54 +384,60 @@ describe('actions', function () { }) describe('checkboxes', function () { - domTest('can check a checkbox by clicking on it', function (browser, dom, $) { - var checkbox = dom.insert('') + it('can check a checkbox by clicking on it', function () { + var checkbox = assembly.insertHtml('') var checked - checkbox.on('click', function (ev) { - checked = $(this).prop('checked') + checkbox.addEventListener('click', function (ev) { + checked = checkbox.checked }) - demand(checkbox.prop('checked')).to.equal(false) + demand(checkbox.checked).to.equal(false) var clicked = browser.find('.checkbox').click() return clicked.then(function () { - demand(checkbox.prop('checked')).to.equal(true) + demand(checkbox.checked).to.equal(true) demand(checked).to.equal(true) }).then(function () { return browser.find('.checkbox').click() }).then(function () { - demand(checkbox.prop('checked')).to.equal(false) + demand(checkbox.checked).to.equal(false) demand(checked).to.equal(false) }) }) - domTest('can check a checkbox by clicking its label', function (browser, dom, $) { - dom.insert('') - var checkbox = dom.el.find('input') + it('can check a checkbox by clicking its label', function () { + assembly.insertHtml('') + var checkbox = assembly.find('input') var checked - checkbox.on('click', function (ev) { - checked = $(this).prop('checked') + checkbox.addEventListener('click', function (ev) { + checked = checkbox.checked }) - demand(checkbox.prop('checked')).to.equal(false) + demand(checkbox.checked).to.equal(false) var clicked = browser.find('label').click() return clicked.then(function () { - demand(checkbox.prop('checked')).to.equal(true) + demand(checkbox.checked).to.equal(true) demand(checked).to.equal(true) }).then(function () { return browser.find('.checkbox').click() }).then(function () { - demand(checkbox.prop('checked')).to.equal(false) + demand(checkbox.checked).to.equal(false) demand(checked).to.equal(false) }) }) }) + describe('field', function () { + it('finds the input in the label with the text', () => { + + }) + }) + describe('fill', function () { - domTest('fills a component with the supplied values', function (browser, dom) { + it.only('fills a component with the supplied values', function () { var component = browser.component({ title: function () { return this.find('.title') @@ -423,15 +446,15 @@ describe('actions', function () { return this.find('.name') } }) - dom.eventuallyInsert('') - dom.eventuallyInsert('') + assembly.eventuallyInsertHtml('') + assembly.eventuallyInsertHtml('') return component.fill([ {name: 'title', action: 'select', options: {exactText: 'Mr'}}, {name: 'name', action: 'typeIn', options: {text: 'Joe'}} ]).then(function () { - demand(dom.el.find('.title').val()).to.equal('Mr') - demand(dom.el.find('.name').val()).to.equal('Joe') + demand(assembly.find('.title').value).to.equal('Mr') + demand(assembly.find('.name').value).to.equal('Joe') }) }) @@ -447,9 +470,9 @@ describe('actions', function () { return this.find('.agree') } }) - dom.eventuallyInsert('') - dom.eventuallyInsert('') - dom.eventuallyInsert('') + assembly.eventuallyInsertHtml('') + assembly.eventuallyInsertHtml('') + assembly.eventuallyInsertHtml('') return component.fill([ {select: 'title', text: 'Mrs'}, @@ -477,7 +500,7 @@ describe('actions', function () { return this.find('.title') } }) - dom.eventuallyInsert('') + assembly.eventuallyInsertHtml('') return component.fill([ { myAction: 'title' } @@ -516,4 +539,5 @@ describe('actions', function () { return demand(promise).reject.with.error("No field 'address' exists on this component") }) }) + }) }) diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.js new file mode 100644 index 0000000..fbd013a --- /dev/null +++ b/test/assemblies/DomAssembly.js @@ -0,0 +1,120 @@ +/* global location */ + +const createTestDiv = require('../../lib/createTestDiv') +const browserMonkey = require('../..') +const $ = require('jquery') +const pathUtils = require('path') +const trytryagain = require('trytryagain') +const {expect} = require('chai') + +module.exports = class DomAssembly { + constructor () { + this.delayedOperations = 0 + this.jQuery = $ + } + + div () { + this._div = createTestDiv() + } + + browserMonkey () { + this.div() + + this.retry = () => {} + + return browserMonkey.scope(this._div).options({ + retry: (retry) => { + return this.runRetry(retry) + } + }) + } + + async runRetry (fn) { + if (!this._normalRetry) { + return new Promise((resolve, reject) => { + let first = true + + this.retry = () => { + try { + resolve(fn()) + } catch (e) { + if (!this.delayedOperations && !first) { + reject(e) + } + } + } + this.retry() + first = false + + setTimeout(() => { + const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(m => /^eventually/.test(m)) + reject(new Error('no delayed actions made, use one of ' + methods.join(', '))) + }, 1000) + }) + } else { + return trytryagain(fn) + } + } + + localUrl (path) { + return location.protocol === 'file:' + ? 'file://' + path + : '/base/' + pathUtils.relative(pathUtils.join(__dirname, '../..'), path) + } + + useNormalRetry () { + this._normalRetry = true + } + + findAll (css) { + return Array.prototype.slice.call(this._div.querySelectorAll(css)) + } + + find (css) { + return this._div.querySelector(css) + } + + insertHtml (html) { + return $(html).appendTo(this._div).get(0) + } + + eventuallyDoNothing () { + return this.eventually(function () {}) + } + + eventually (fn) { + this.delayedOperations++ + return new Promise(resolve => { + setTimeout(() => { + this.delayedOperations-- + var result = fn() + this.retry() + resolve(result) + }) + }) + } + + eventuallyDeleteHtml (selector) { + return this.eventually(() => { + $(selector).remove() + }) + } + + eventuallyInsertHtml (html, selector) { + return this.eventually(() => { + var div = selector + ? $(this._div).find(selector).get(0) + : this._div + + return $(html).appendTo(div).get(0) + }) + } + + assertElementIsFocussed (element) { + expect(document.activeElement).to.equal(element) + } + + static hasDom () { + return true + } +} diff --git a/test/conditionsSpec.js b/test/conditionsSpec.js new file mode 100644 index 0000000..44fa96c --- /dev/null +++ b/test/conditionsSpec.js @@ -0,0 +1,107 @@ +const expect = require('must') +const Conditions = require('../lib/conditions') + +describe('map', function () { + describe('asynchronously', () => { + it('can transform an input', async () => { + const s = new Conditions() + s.map(x => x + 1) + s.map(x => x + 2) + const result = await s.result(1) + expect(result).to.equal(4) + }) + + it('will retry until there are no failures', async () => { + const s = new Conditions() + let ready = false + const events = [] + + s.map(x => { + if (!ready) { + ready = true + events.push('fail') + throw new Error('not ready') + } + events.push('pass') + return x + 1 + }) + + const result = await s.result(1) + + expect(result).to.equal(2) + expect(events).to.eql([ + 'fail', + 'pass' + ]) + }) + + it('can override retry function', async () => { + const s = new Conditions() + let ready = false + const events = [] + + s.map(x => { + if (!ready) { + ready = true + throw new Error('not ready') + } + return x + 1 + }) + + const retry = fn => { + events.push('retry') + try { + fn() + } catch (e) { + events.push('fail ' + e.message) + } + + const result = fn() + events.push('pass') + return result + } + + events.push('await') + const result = await s.result(1, {retry}) + + expect(result).to.equal(2) + expect(events).to.eql([ + 'await', + 'retry', + 'fail not ready', + 'pass' + ]) + }) + }) + + describe('synchronously', () => { + it('can transform an input', () => { + const s = new Conditions() + s.map(x => x + 1) + s.map(x => x + 2) + const result = s.result(1, {wait: false}) + expect(result).to.equal(4) + }) + + it('will retry until there are no failures', () => { + const s = new Conditions() + let ready = false + const events = [] + + s.map(x => { + if (!ready) { + ready = true + events.push('fail') + throw new Error('not ready') + } + events.push('pass') + return x + 1 + }) + + expect(() => s.result(1, {wait: false})).to.throw('not ready') + expect(events).to.eql([ + 'fail' + ]) + }) + }) +}) diff --git a/test/describeAssemblies.js b/test/describeAssemblies.js new file mode 100644 index 0000000..6e79a52 --- /dev/null +++ b/test/describeAssemblies.js @@ -0,0 +1,18 @@ +function selectAssemblies (assemblies) { + if (process.env.ASSEMBLIES) { + const selectedAssemblies = new Set(process.env.ASSEMBLIES.split(',')) + return assemblies.filter((assembly) => selectedAssemblies.has(assembly.name)) + } else { + return assemblies + } +} + +function describeAssemblies (assemblies, cb) { + selectAssemblies(assemblies).forEach(function (assembly) { + describe(`assembly: ${assembly.name}`, function () { + cb.call(this, assembly) + }) + }) +} + +module.exports = describeAssemblies diff --git a/test/domSpec.js b/test/domSpec.js new file mode 100644 index 0000000..a4d8692 --- /dev/null +++ b/test/domSpec.js @@ -0,0 +1,476 @@ +var describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +const elementVisible = require('../lib/elementVisible') +const elementClick = require('../lib/elementClick') +const elementSelect = require('../lib/elementSelect') +const elementMatches = require('../lib/elementMatches') +const elementSubmit = require('../lib/elementSubmit') +const elementTriggerEvent = require('../lib/elementTriggerEvent') +const elementEnterText = require('../lib/elementEnterText') +const {expect} = require('chai') + +describe('dom', () => { + describeAssemblies([DomAssembly], Assembly => { + var assembly + + beforeEach(() => { + assembly = new Assembly() + assembly.div() + }) + + describe('submitting', () => { + it('form with submit button receives submit event when element submitted inside it', () => { + var formEvents = [] + var inputEvents = [] + var buttonEvents = [] + + var form = assembly.insertHtml('
') + var input = assembly.find('input') + var button = assembly.find('button') + + ;['submit', 'keydown', 'keypress', 'click', 'keyup'].forEach(type => { + form.addEventListener(type, () => formEvents.push(type)) + input.addEventListener(type, () => inputEvents.push(type)) + button.addEventListener(type, () => buttonEvents.push(type)) + }) + + elementSubmit(input) + + expect(formEvents).to.eql([ + 'keydown', + 'keypress', + 'click', + 'submit', + 'keyup' + ]) + + expect(inputEvents).to.eql([ + 'keydown', + 'keypress', + 'keyup' + ]) + + expect(buttonEvents).to.eql([ + 'click' + ]) + + assembly.assertElementIsFocussed(input) + }) + + it('form without submit button receives submit event when element submitted inside it', () => { + var formEvents = [] + var inputEvents = [] + + var form = assembly.insertHtml('
') + var input = assembly.find('input') + + ;['submit', 'keydown', 'keypress', 'click', 'keyup'].forEach(type => { + form.addEventListener(type, () => formEvents.push(type)) + input.addEventListener(type, () => inputEvents.push(type)) + }) + + elementSubmit(input) + + expect(formEvents).to.eql([ + 'keydown', + 'keypress', + 'submit', + 'keyup' + ]) + + expect(inputEvents).to.eql([ + 'keydown', + 'keypress', + 'keyup' + ]) + + assembly.assertElementIsFocussed(input) + }) + }) + + describe('clicking', () => { + it('clicks an element and sends correct events', () => { + var events = [] + var element = assembly.insertHtml(' + `)) + + watchEvents(assembly.insertHtml(` + + `)) + + watchEvents(assembly.find('#x')) + watchEvents(assembly.find('#y')) + + watchEvents(assembly.insertHtml(` + + `)) + + watchEvents(assembly.insertHtml(` +
+

Form

+ + +
+ `)) + + watchEvents(assembly.find('#form-input')) + watchEvents(assembly.find('#form-button')) + }) + + eventNames.forEach(eventName => { + describe(eventName, () => { + it('can trigger ' + eventName + ' event', () => { + var element = assembly.insertHtml('
') + + var eventReceived = new Promise(resolve => { + element.addEventListener(eventName, function (event) { + resolve(event) + }) + }) + + elementTriggerEvent(element, eventName) + + return eventReceived + }) + + it('can trigger ' + eventName + ' event and have it bubble to its parent', () => { + var parent = assembly.insertHtml('
') + + var eventReceived = new Promise(resolve => { + parent.addEventListener(eventName, function (event) { + resolve(event) + }) + }) + + var child = assembly.find('#child') + + elementTriggerEvent(child, eventName) + + return eventReceived + }) + }) + }) + }) + + describe('visibility', () => { + it('when element is visible it returns true', () => { + var element = assembly.insertHtml('
hi
') + + expect(elementVisible(element)).to.equal(true) + }) + + it('when element is visible but empty returns true', () => { + var element = assembly.insertHtml('
') + + expect(elementVisible(element)).to.equal(true) + }) + + it('when element is display:none it returns false', () => { + var element = assembly.insertHtml('
hi
') + + expect(elementVisible(element)).to.equal(false) + }) + + it('when element is visible but parent is not', () => { + assembly.insertHtml('
hi
') + + var element = assembly.find('#child') + + expect(elementVisible(element)).to.equal(false) + }) + + it('when element is option and select is visible, returns true', () => { + assembly.insertHtml('') + + var element = assembly.find('#option') + + expect(elementVisible(element)).to.equal(true) + }) + + it('when element is option and select is not visible, returns false', () => { + assembly.insertHtml('') + + var element = assembly.find('#option') + + expect(elementVisible(element)).to.equal(false) + }) + }) + + describe('matches', function () { + describe('tagName', function () { + it('returns true when tagName matches', () => { + var element = assembly.insertHtml('
') + expect(elementMatches(element, 'div')).to.equal(true) + }) + + it('returns false when tagName doesnt match', () => { + var element = assembly.insertHtml('
') + expect(elementMatches(element, 'select')).to.equal(false) + }) + }) + + describe('class', function () { + it('returns true when class matches', () => { + var element = assembly.insertHtml('
') + expect(elementMatches(element, '.hobo')).to.equal(true) + }) + + it('returns false when class doesnt match', () => { + var element = assembly.insertHtml('
') + expect(elementMatches(element, '.hobo')).to.equal(false) + }) + }) + }) + }) +}) diff --git a/test/findersSpec.js b/test/findersSpec.js index 6bc0efd..667cd92 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -1,255 +1,211 @@ require('babel-polyfill') +var describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') var demand = require('must') -var domTest = require('./domTest') -var retry = require('trytryagain') +const pathUtils = require('path') describe('find', function () { - domTest('should eventually find an element', function (browser, dom) { + describeAssemblies([DomAssembly], Assembly => { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly + browser = assembly.browserMonkey() + }) + + it('should eventually find an element', function () { var promise = browser.find('.element').shouldExist() - dom.eventuallyInsert('
') + assembly.eventuallyInsertHtml('
') return promise }) - domTest('should eventually find an element, when collapsed into shouldFind(selector)', function (browser, dom) { + it('should eventually find an element, when collapsed into shouldFind(selector)', function () { var promise = browser.shouldFind('.element') - dom.eventuallyInsert('
') + assembly.eventuallyInsertHtml('
') return promise }) - domTest('should eventually find an element using a filter', function (browser, dom) { - var promise = browser.find('.element').filter(function (element) { - return element.hasClass('correct') + it('should eventually find an element using a filter', async function () { + var foundElement = browser.find('.element').filter(function (element) { + return element.classList.contains('correct') }, 'has class "correct"').element() - dom.insert('
') - dom.eventuallyInsert('
') + assembly.insertHtml('
') + const correctElement = await assembly.eventuallyInsertHtml('
') - return promise.then(function (element) { - demand(element.attr('class')).to.equal('element correct') - }) + demand(await foundElement).to.equal(correctElement) }) - domTest('should eventually find an element with the right text', function (browser, dom) { + it('should eventually find an element with the right text', function () { var promise = browser.find('.element', {text: 'green'}).element() - dom.insert('
') - dom.eventuallyInsert('
red
blue
green
') + assembly.insertHtml('
') + assembly.eventuallyInsertHtml('
red
blue
green
') return promise.then(function (element) { - demand(element.text()).to.equal('green') + demand(element).to.equal(assembly.find('#green')) }) }) - domTest('filter fails with the right message', function (browser, dom) { + it('filter fails with the right message', function () { var promise = browser.find('.element').filter(function (element) { - return element.hasClass('correct') + return element.classList.contains('correct') }, 'has class "correct"').element() - dom.insert('
') - dom.eventuallyInsert('
') + assembly.insertHtml('
') + assembly.eventuallyInsertHtml('
') return demand(promise).reject.with.error(/has class "correct"/) }) - domTest('should eventually find an element in an iframe', function (browser, dom) { - var iframe = document.createElement('iframe') - iframe.src = '/base/test/page1.html' - iframe.width = 700 - iframe.height = 1000 - dom.el.append(iframe) - var iframeScope = browser.scope(iframe) - return iframeScope.find('a', {text: 'page 2'}).click().then(function () { - return Promise.all([ - iframeScope.find('h1').shouldHave({text: 'Hello World'}), - iframeScope.shouldHave({text: 'Hello World'}) - ]) - }) - }, {vdom: false}) - - domTest('can find things in an iframe', function (browser, dom) { - var iframe = document.createElement('iframe') - iframe.src = '/base/test/page2.html' - iframe.width = 700 - iframe.height = 1000 - dom.el.append(iframe) - - return browser.find('iframe').element().then(function (iframe) { - return browser.scope(iframe).find('h1', {text: 'Hello World'}).shouldExist() - }) - }, {vdom: false}) + if (Assembly.hasDom()) { + it('should eventually find an element in an iframe', function () { + var iframe = document.createElement('iframe') + iframe.src = assembly.localUrl(pathUtils.join(__dirname, 'page1.html')) + iframe.width = 700 + iframe.height = 1000 + assembly.insertHtml(iframe) + var iframeScope = browser.scope(iframe) + + assembly.useNormalRetry() + + return iframeScope.find('a', {text: 'page 2'}).click().then(function () { + return Promise.all([ + iframeScope.find('h1').shouldHave({text: 'Hello World'}), + iframeScope.shouldHave({text: 'Hello World'}) + ]) + }) + }, {vdom: false}) - domTest('calls a function for each element found', function (browser, dom) { - var promise = browser.find('span').elements() + it('can find things in an iframe', function () { + var iframe = document.createElement('iframe') + iframe.src = assembly.localUrl(pathUtils.join(__dirname, 'page2.html')) + iframe.width = 700 + iframe.height = 1000 + assembly.insertHtml(iframe) - dom.insert('
ab
') + assembly.useNormalRetry() - return promise.then(function (elements) { - demand(elements.length).to.equal(2) - }) - }) + return browser.find('iframe').element().then(function (iframe) { + return browser.scope(iframe).find('h1', {text: 'Hello World'}).shouldExist() + }) + }, {vdom: false}) + } describe('visibility', function () { - domTest('should not find an element that is visually hidden', function (browser, dom) { - dom.insert('
hello world
') + it('should not find an element that is visually hidden', function () { + assembly.insertHtml('
hello world
') + + assembly.eventuallyDoNothing() return browser.find('.element > span').shouldNotExist() }, {vdom: false}) - domTest('should find an element that is visually hidden when visibleOnly = false', function (browser, dom) { - dom.insert('
hello world
') + it('should find an element that is visually hidden when visibleOnly = false', function () { + assembly.insertHtml('
hello world
') + + assembly.eventuallyDoNothing() - return browser.set({visibleOnly: false}).find('.element > span').shouldExist() + return browser.options({visibleOnly: false}).find('.element > span').shouldExist() }) - domTest('should find elements that are visually hidden because of how html renders them', function (browser, dom) { - dom.insert('') + it('should find elements that are visually hidden because of how html renders them', function () { + assembly.insertHtml('') + + assembly.eventuallyDoNothing() + return browser.find('select option').shouldHave({text: ['First', 'Second']}) }) }) describe('containing', function () { - domTest('eventually finds an element containing another element', function (browser, dom) { + it('eventually finds an element containing another element', function () { var promise = browser.find('.outer').containing('.inner').shouldExist() - setTimeout(function () { - dom.insert('
bad
') - dom.insert('
good
') - }, 10) + assembly.eventuallyInsertHtml( + '
bad
' + + '
good
' + ) return promise }) - domTest('element returns the outer element', function (browser, dom) { + it('element returns the outer element', function () { var promise = browser.find('.outer').containing('.inner').element() - setTimeout(function () { - dom.insert('
bad
') - dom.insert('
good
') - }, 10) + assembly.eventuallyInsertHtml( + '
bad
' + + '
good
' + ) return promise.then(function (element) { - demand(element.hasClass('outer')).to.eql(true) + demand(element).to.eql(assembly.find('#good')) }) }) - domTest('errors with a usable css selector if it cant find something', function (browser, dom) { + it('errors with a usable css selector if it cant find something', function () { var promise = browser.find('.outer').find('.not-there').element() - setTimeout(function () { - dom.insert('
bad
') - }, 200) + assembly.eventuallyInsertHtml('
bad
') - return demand(promise).reject.with.error(/expected to find: .outer .not-there/) + return demand(promise).reject.with.error(/expected one element/) }) - domTest('errors with a usable css selector if it cant find an element containing another', function (browser, dom) { + it('errors with a usable css selector if it cant find an element containing another', function () { var promise = browser.find('.outer').containing('.not-there').shouldExist() - setTimeout(function () { - dom.insert('
bad
') - }, 200) + assembly.eventuallyInsertHtml('
bad
') - return demand(promise).reject.with.error('expected to find: .outer:has(.not-there)') + return demand(promise).reject.with.error(/expected some elements/) }) - domTest("fails if it can't find an element containing another", function (browser, dom) { + it("fails if it can't find an element containing another", function () { var promise = browser.find('.outer').containing('.inner').shouldExist() - setTimeout(function () { - dom.insert('
bad
') - }, 200) + assembly.eventuallyInsertHtml('
bad
') - return demand(promise).reject.with.error('expected to find: .outer:has(.inner)') + return demand(promise).reject.with.error(/expected some elements/) }) - domTest('errors with a usable css selector if it cant find an element containing another', function (browser, dom) { + it('errors with a usable css selector if it cant find an element containing another', function () { var promise = browser.find('.outer').containing('.not-there').shouldExist() - setTimeout(function () { - dom.insert('
bad
') - }, 200) + assembly.eventuallyInsertHtml('
bad
') - return demand(promise).reject.with.error('expected to find: .outer:has(.not-there)') + return demand(promise).reject.with.error(/expected some elements/) }) - domTest("fails if it can't find an element containing another", function (browser, dom) { + it("fails if it can't find an element containing another", function () { var promise = browser.find('.outer').containing('.inner').shouldExist() - setTimeout(function () { - dom.insert('
bad
') - }, 10) + assembly.eventuallyInsertHtml('
bad
') return demand(promise).reject.with.error() }) }) + describe('chains', function () { - domTest('eventually finds the inner element, even if the outer element exists', function (browser, dom) { + it('eventually finds the inner element, even if the outer element exists', function () { + assembly.insertHtml('
') var promise = browser.find('.outer').find('.inner').shouldExist() - setTimeout(function () { - var outer = dom.insert('
') - setTimeout(function () { - outer.append('
good
') - }, 10) - }, 10) + assembly.eventuallyInsertHtml('
good
', '.outer') return promise }) - domTest('fails to find the inner element if it never arrives', function (browser, dom) { + it('fails to find the inner element if it never arrives', function () { var promise = browser.find('.outer').find('.inner').shouldExist() - setTimeout(function () { - dom.insert('
') - }, 10) + assembly.eventuallyInsertHtml('
') return demand(promise).reject.with.error() }) }) - - describe.only('map', function () { - domTest('can map element values into objects', function (browser, dom) { - var promise = browser.find('.content').map(function (element) { - return { - a: element.find('.a').text(), - b: element.find('.b').text() - } - }).assert(function (content) { - demand(content).to.eql({ - a: 'Aye', - b: 'Bee' - }) - }) - - setTimeout(function () { - dom.insert('
Aye
Bee
') - }, 10) - - return promise - }) - - domTest('can map element values into objects', function (browser, dom) { - var promise = browser.find('.content').map(function (element) { - return { - a: element.find('.a').text(), - b: element.find('.b').text() - } - }).assert(function (content) { - demand(content).to.eql({ - a: 'Axe', - b: 'Bee' - }) - }) - - setTimeout(function () { - dom.insert('
Aye
Bee
') - }, 10) - - return demand(promise).reject.with.error('Axe') - }) }) }) diff --git a/test/selectorSpec.js b/test/selectorSpec.js deleted file mode 100644 index 03e0ab1..0000000 --- a/test/selectorSpec.js +++ /dev/null @@ -1,96 +0,0 @@ -var trytryagain = require('trytryagain') -const {expect} = require('chai') - -describe.only('map', function () { - function Conditions () { - this.mappers = [] - } - - Conditions.prototype.clone = function (mapper) { - const s = new Conditions() - s.mappers = this.mappers.slice() - s._input = this._input - return s - } - - Conditions.prototype.map = function (mapper) { - this.mappers.push(mapper) - } - - Conditions.prototype.wait = function (input, {retry = trytryagain} = {}) { - return retry(() => { - return this.mappers.reduce((input, mapper) => { - return mapper(input) - }, input) - }) - } - - it('can transform an input', async () => { - const s = new Conditions() - s.map(x => x + 1) - s.map(x => x + 2) - const result = await s.wait(1) - expect(result).to.equal(4) - }) - - it('returns input when there are no mappers', async () => { - const s = new Conditions() - const result = await s.wait(1) - expect(result).to.equal(1) - }) - - it('returns a result when there are no failures', async () => { - const s = new Conditions() - let ready = false - - s.map(x => { - if (!ready) { - ready = true - throw new Error('not ready') - } - return x + 1 - }) - - const result = await s.wait(1) - - expect(result).to.equal(2) - }) - - it('can override retry function', async () => { - const s = new Conditions() - let ready = false - const events = [] - - s.map(x => { - if (!ready) { - ready = true - throw new Error('not ready') - } - return x + 1 - }) - - const retry = fn => { - events.push('retry') - try { - fn() - } catch (e) { - events.push('fail ' + e.message) - } - - const result = fn() - events.push('pass') - return result - } - - events.push('await') - const result = await s.wait(1, {retry}) - - expect(result).to.equal(2) - expect(events).to.eql([ - 'await', - 'retry', - 'fail not ready', - 'pass' - ]) - }) -}) diff --git a/test/selectorsSpec.js b/test/selectorsSpec.js new file mode 100644 index 0000000..fdfb9ab --- /dev/null +++ b/test/selectorsSpec.js @@ -0,0 +1,361 @@ +var describeAssemblies = require('./describeAssemblies') +const expect = require('must') +const DomAssembly = require('./assemblies/DomAssembly') + +describe('selectors', () => { + describeAssemblies([DomAssembly], Assembly => { + let assembly + let browserMonkey + + beforeEach(() => { + assembly = new Assembly() + browserMonkey = assembly.browserMonkey() + }) + + it('can find an element by DOM selector', async () => { + const selectedElementPromise = browserMonkey.find('.test').one() + + const insertedElementPromise = assembly.eventuallyInsertHtml( + `
` + ) + + const selectedElement = await selectedElementPromise + const insertedElement = await insertedElementPromise + + expect(selectedElement).to.equal(insertedElement) + }) + + describe('options', () => { + it('returns a new Browser Monkey object without modifying the current one', () => { + const withOptions = browserMonkey.options({a: 'a'}) + expect(browserMonkey._options.a).to.equal(undefined) + expect(withOptions._options.a).to.equal('a') + }) + }) + + describe('components', () => { + it('can add a method to the returned Browser Monkey object', () => { + const monkey = browserMonkey + .component({ + aMethod: function () { return 'aMethod' } + }) + + expect(monkey.aMethod()).to.equal('aMethod') + }) + + it('passes through the options', () => { + const monkey = browserMonkey + .options({a: 'a'}) + .component({ + aMethod: function () { return 'aMethod' } + }) + + expect(monkey._options.a).to.equal('a') + }) + + it('passes through the value', () => { + const monkey = browserMonkey + .value('a') + .component({ + aMethod: function () { return 'aMethod' } + }) + + expect(monkey._value).to.equal('a') + }) + + it('passes through the maps', async () => { + const monkey = browserMonkey + .value(1) + .mapAll(x => x + 1) + .component({ + aMethod: function () { return 'aMethod' } + }) + + expect(await monkey).to.equal(2) + }) + }) + + describe('zero', () => { + it('passes when there are no elements found', async () => { + assembly.insertHtml(` +
+
Sally
+
32 Yellow Drive
+
+ `) + + const contacts = browserMonkey + .find('.contact') + .zero() + + assembly.eventuallyDeleteHtml('.contact') + + await contacts + }) + + it('fails when there are more than one element found', async () => { + assembly.insertHtml(` +
+
Sally
+
32 Yellow Drive
+
+
+
Hi
+
Quick beer after work?
+
+ `) + + const contacts = browserMonkey + .find('.contact') + .zero() + + assembly.eventuallyDeleteHtml('.message') + + await expect(contacts).reject.with.error(/expected no elements/) + }) + }) + + describe('one', () => { + it('when there is one element, selects it', async () => { + const contacts = browserMonkey + .find('.contact') + .one() + + assembly.eventuallyInsertHtml(` +
+
Sally
+
32 Yellow Drive
+
+ `) + + expect(await contacts).to.eql(assembly.find('.contact:nth-child(1)')) + }) + + it('when there is more than one element, throws an error', async () => { + const contacts = browserMonkey + .find('.contact') + .one() + + assembly.eventuallyInsertHtml(` +
+
Sally
+
32 Yellow Drive
+
+
+
Bob
+
32 Red Drive
+
+ `) + + await expect(contacts).reject.with.error(/expected one element/) + }) + + it('when there no elements, throws an error', async () => { + const contacts = browserMonkey + .find('.contact') + .one() + + assembly.eventuallyInsertHtml(` +
+
+ `) + + await expect(contacts).reject.with.error(/expected one element/) + }) + + it('when there is already one result, no array, selects it', async () => { + const contacts = browserMonkey + .find('.contact') + .one() + .one() + + assembly.eventuallyInsertHtml(` +
+
Sally
+
32 Yellow Drive
+
+ `) + + expect(await contacts).to.eql(assembly.find('.contact:nth-child(1)')) + }) + }) + + describe('some', () => { + it('when there are one or more elements, selects them', async () => { + const contacts = browserMonkey + .find('.contact') + .some() + + assembly.eventuallyInsertHtml(` +
+
Sally
+
32 Yellow Drive
+
+
+
Bob
+
32 Red Drive
+
+ `) + + expect(await contacts).to.eql(assembly.findAll('.contact')) + }) + + it('when there no elements, throws an error', async () => { + const contacts = browserMonkey + .find('.contact') + .some() + + assembly.eventuallyInsertHtml(` +
+
+ `) + + await expect(contacts).reject.with.error(/expected some elements/) + }) + }) + + describe('map', () => { + it('can map elements', async () => { + const contacts = browserMonkey + .find('.contact') + .map(contact => ({ + name: contact.querySelector('.name').innerText, + address: contact.querySelector('.address').innerText + })) + .some() + + assembly.eventuallyInsertHtml(` +
+
Sally
+
32 Yellow Drive
+
+
+
Bob
+
32 Red Drive
+
+ `) + + expect(await contacts).to.eql([ + { + name: 'Sally', + address: '32 Yellow Drive' + }, + { + name: 'Bob', + address: '32 Red Drive' + } + ]) + }) + }) + + describe('value', () => { + it('value sets the value used in mapAll', async () => { + const monkey = browserMonkey + .value('a') + .mapAll(x => `value: ${x}`) + + expect(await monkey).to.eql('value: a') + }) + }) + + describe('scope', () => { + it('when scope is one element, sets the value to an array of one', () => { + const monkey = browserMonkey + .scope(document.body) + + expect(monkey._value).to.eql([document.body]) + }) + }) + + describe('ensure', () => { + it('waits for the value to eventually pass the assertion', async () => { + const hello = browserMonkey + .ensure(elements => { + expect(elements.some(element => element.innerText.includes('hello'))).to.equal(true) + }) + + assembly.eventuallyInsertHtml(` +
hello
+ `) + + await hello + }) + + it('eventually throws the last error if it never passes', async () => { + const hello = browserMonkey + .ensure(elements => { + expect(elements.some(element => element.innerText.includes('hello')), 'expected to see hello').to.equal(true) + }) + + assembly.eventuallyInsertHtml(` +
goodbye
+ `) + + await expect(hello).to.reject.with.error(/expected to see hello/) + }) + }) + + describe('mapAll', () => { + it('can map all the elements', async () => { + const contacts = browserMonkey + .find('.name') + .mapAll(names => { + return names.map(contact => contact.innerText).join(', ') + }) + .ensure(x => expect(x).to.not.be.empty()) + + assembly.eventuallyInsertHtml(` +
Sally
+
Bob
+ `) + + expect(await contacts).to.eql('Sally, Bob') + }) + }) + + describe('filter', () => { + it('can filter elements', async () => { + const contacts = browserMonkey + .find('.contact') + .filter(contact => { + return contact.querySelector('.name').innerText === 'Sally' + }) + .one() + + assembly.eventuallyInsertHtml(` +
+
Sally
+
32 Yellow Drive
+
+
+
Bob
+
32 Red Drive
+
+ `) + + expect(await contacts).to.eql(assembly.find('.contact:nth-child(1)')) + }) + }) + + describe('errors', () => { + it('shows what it was able to map', async () => { + const name = browserMonkey + .find('.container') + .find('.contact') + .find('.name') + .one() + + assembly.eventuallyInsertHtml(` +
+
+
+
+
+
+ `) + + await expect(name).to.reject.with.error('expected one element (found: .container [1], .contact [2], .name [0])') + }) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index f6fcf71..9683771 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,10 @@ dependencies: htmlparser2 "^3.9.0" +"@types/node@^8.0.24": + version "8.9.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.9.4.tgz#dfd327582a06c114eb6e0441fa3d6fab35edad48" + JSONStream@^1.0.3: version "1.3.1" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a" @@ -80,6 +84,15 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -138,6 +151,10 @@ array-filter@~0.0.0: version "0.0.1" resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -247,7 +264,11 @@ aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" -aws4@^1.2.1: +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" @@ -837,6 +858,10 @@ balanced-match@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + base64-arraybuffer@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz#474df4a9f2da24e05df3158c3b1db3c3cd46a154" @@ -919,6 +944,18 @@ boom@2.x.x: dependencies: hoek "2.x.x" +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + brace-expansion@^1.0.0: version "1.1.6" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" @@ -926,6 +963,13 @@ brace-expansion@^1.0.0: balanced-match "^0.4.1" concat-map "0.0.1" +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + braces@^0.1.2: version "0.1.5" resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" @@ -1170,7 +1214,7 @@ buffers@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" -builtin-modules@^1.1.1: +builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1200,6 +1244,17 @@ callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + camelize@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" @@ -1298,18 +1353,32 @@ combine-source-map@~0.7.1: lodash.memoize "~3.0.3" source-map "~0.5.3" +combined-stream@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" dependencies: delayed-stream "~1.0.0" +commander@2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + commander@2.9.0, commander@^2.8.1: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" dependencies: graceful-readlink ">= 1.0.0" +commander@^2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + commander@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-1.2.0.tgz#fd5713bfa153c7d6cc599378a5ab4c45c535029e" @@ -1340,6 +1409,14 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" +concat-stream@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concat-stream@^1.5.2, concat-stream@~1.5.0, concat-stream@~1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" @@ -1440,6 +1517,12 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + crypto-browserify@^3.0.0: version "3.11.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522" @@ -1461,6 +1544,12 @@ crypto-browserify@^3.0.0: dependencies: through X.X.X +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" @@ -1509,10 +1598,26 @@ debug@2.6.1: dependencies: ms "0.7.2" +debug@2.6.9, debug@^2.1.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + debug@~0.7.2: version "0.7.4" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" +decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + deep-eql@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" @@ -1628,6 +1733,10 @@ diff@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" +diff@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + diffie-hellman@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" @@ -1717,6 +1826,44 @@ ee-first@1.1.1: dependencies: kindof ">= 2.0.0 < 3" +electron-download@^3.0.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-3.3.0.tgz#2cfd54d6966c019c4d49ad65fbe65cc9cdef68c8" + dependencies: + debug "^2.2.0" + fs-extra "^0.30.0" + home-path "^1.0.1" + minimist "^1.2.0" + nugget "^2.0.0" + path-exists "^2.1.0" + rc "^1.1.2" + semver "^5.3.0" + sumchecker "^1.2.0" + +electron-mocha@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/electron-mocha/-/electron-mocha-6.0.1.tgz#3bab0fa6e48b106791a6b0564359007741de2c32" + dependencies: + commander "^2.14.1" + electron-window "^0.8.0" + fs-extra "^5.0.0" + mocha "^5.0.1" + which "^1.3.0" + +electron-window@^0.8.0: + version "0.8.1" + resolved "https://registry.yarnpkg.com/electron-window/-/electron-window-0.8.1.tgz#16ca187eb4870b0679274fc8299c5960e6ab2c5e" + dependencies: + is-electron-renderer "^2.0.0" + +electron@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.2.tgz#a817cd733c2972b3c7cc4f777caf6e424b88014d" + dependencies: + "@types/node" "^8.0.24" + electron-download "^3.0.1" + extract-zip "^1.0.3" + elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -1871,6 +2018,10 @@ es6-map@^0.1.3: es6-symbol "~3.1.1" event-emitter "~0.3.5" +es6-promise@^4.0.5: + version "4.2.4" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29" + es6-set@~0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" @@ -2177,16 +2328,37 @@ extend@3, extend@^3.0.0, extend@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" +extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" dependencies: is-extglob "^1.0.0" +extract-zip@^1.0.3: + version "1.6.6" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" + dependencies: + concat-stream "1.6.0" + debug "2.6.9" + mkdirp "0.5.0" + yauzl "2.4.1" + extsprintf@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -2203,6 +2375,12 @@ fbjs@^0.8.1, fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.9" +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -2299,6 +2477,14 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +form-data@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + forwarded@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" @@ -2313,6 +2499,24 @@ fs-access@^1.0.0: dependencies: null-check "^1.0.0" +fs-extra@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-extra@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-readdir-recursive@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" @@ -2381,6 +2585,10 @@ generate-object-property@^1.1.0: dependencies: is-property "^1.0.0" +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + get-stdin@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" @@ -2415,6 +2623,17 @@ glob@7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" @@ -2455,7 +2674,7 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.4: +graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -2469,6 +2688,10 @@ graceful-fs@~3.0.2: version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" + growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" @@ -2477,6 +2700,10 @@ har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + har-validator@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" @@ -2484,6 +2711,13 @@ har-validator@~4.2.1: ajv "^4.9.1" har-schema "^1.0.5" +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -2510,6 +2744,10 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -2539,6 +2777,19 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + hmac-drbg@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.0.tgz#3db471f45aae4a994a0688322171f51b8b91bee5" @@ -2555,6 +2806,10 @@ hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" +hoek@4.x.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -2562,6 +2817,14 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" +home-path@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/home-path/-/home-path-1.0.5.tgz#788b29815b12d53bacf575648476e6f9041d133f" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + htmlescape@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" @@ -2601,6 +2864,14 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + httpism@^2.6.2: version "2.7.0" resolved "https://registry.yarnpkg.com/httpism/-/httpism-2.7.0.tgz#a31861a2fa498296edc4cdbc2a86464fd900d448" @@ -2663,6 +2934,12 @@ imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -2678,7 +2955,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -2761,6 +3038,12 @@ is-buffer@^1.0.2, is-buffer@^1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + is-callable@^1.1.1, is-callable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" @@ -2788,6 +3071,10 @@ is-dotfile@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d" +is-electron-renderer@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-electron-renderer/-/is-electron-renderer-2.0.1.tgz#a469d056f975697c58c98c6023eb0aa79af895a2" + is-equal-shallow@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" @@ -2903,6 +3190,10 @@ is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + isarray@0.0.1, isarray@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -2919,6 +3210,10 @@ isexe@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0" +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" @@ -2973,6 +3268,10 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -3001,6 +3300,18 @@ json5@^0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" @@ -3139,6 +3450,12 @@ kind-of@^3.0.2: version "2.0.0" resolved "https://registry.yarnpkg.com/kindof/-/kindof-2.0.0.tgz#c335baf603a77cc37f8b406b73b6463fdbdf1abe" +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + labeled-stream-splicer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59" @@ -3172,6 +3489,16 @@ lie@3.1.1: dependencies: immediate "~3.0.5" +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + load-json-file@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" @@ -3280,6 +3607,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0: dependencies: js-tokens "^3.0.0" +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + lower-case@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" @@ -3296,6 +3630,10 @@ lru-cache@2.2.x: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + "match-stream@>= 0.0.2 < 1": version "0.0.2" resolved "https://registry.yarnpkg.com/match-stream/-/match-stream-0.0.2.tgz#99eb050093b34dffade421b9ac0b410a9cfa17cf" @@ -3307,6 +3645,21 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" +meow@^3.1.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -3348,6 +3701,10 @@ mime-db@~1.26.0: version "1.26.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + mime-types@2.1.11, mime-types@~2.1.11: version "2.1.11" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.11.tgz#c259c471bda808a85d6cd193b430a5fae4473b3c" @@ -3360,6 +3717,12 @@ mime-types@^2.1.12, mime-types@~2.1.13, mime-types@~2.1.7: dependencies: mime-db "~1.26.0" +mime-types@~2.1.17: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + dependencies: + mime-db "~1.33.0" + mime@1.3.4, mime@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -3384,11 +3747,17 @@ minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: dependencies: brace-expansion "^1.0.0" +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + minimist@0.0.8, minimist@~0.0.1: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@1.2.0, minimist@^1.1.0, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -3398,6 +3767,12 @@ mkdirp@0.5, mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp dependencies: minimist "0.0.8" +mkdirp@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" + dependencies: + minimist "0.0.8" + mocha@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.2.0.tgz#7dc4f45e5088075171a68896814e6ae9eb7a85e3" @@ -3414,6 +3789,21 @@ mocha@3.2.0: mkdirp "0.5.1" supports-color "3.1.2" +mocha@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.1.tgz#759b62c836b0732382a62b6b1fb245ec1bc943ac" + dependencies: + browser-stdout "1.3.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" + mkdirp "0.5.1" + supports-color "4.4.0" + module-deps@^4.0.8: version "4.1.1" resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.1.1.tgz#23215833f1da13fd606ccb8087b44852dcb821fd" @@ -3446,6 +3836,10 @@ ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + must@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/must/-/must-0.13.4.tgz#9d1389f458e894b2aa04032bfeb7a40714f78171" @@ -3507,6 +3901,15 @@ nopt@~3.0.6: dependencies: abbrev "1" +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-path@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a" @@ -3520,6 +3923,18 @@ npmlog@^4.0.1: gauge "~2.7.1" set-blocking "~2.0.0" +nugget@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0" + dependencies: + debug "^2.1.3" + minimist "^1.1.0" + pretty-bytes "^1.0.2" + progress-stream "^1.1.0" + request "^2.45.0" + single-line-log "^1.1.2" + throttleit "0.0.2" + null-check@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" @@ -3528,7 +3943,7 @@ number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" -oauth-sign@~0.8.1: +oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" @@ -3548,6 +3963,10 @@ object-keys@^1.0.10, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" +object-keys@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" + object.assign@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.0.4.tgz#b1c9cc044ef1b9fe63606fc141abbb32e14730cc" @@ -3747,7 +4166,7 @@ path-browserify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" -path-exists@^2.0.0: +path-exists@^2.0.0, path-exists@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" dependencies: @@ -3777,16 +4196,32 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + pbkdf2@^3.0.3: version "3.0.9" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.9.tgz#f2c4b25a600058b3c3773c086c37dbbee1ffe693" dependencies: create-hmac "^1.1.2" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -3840,6 +4275,13 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" +pretty-bytes@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" + dependencies: + get-stdin "^4.0.1" + meow "^3.1.0" + private@^0.1.6: version "0.1.7" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" @@ -3848,6 +4290,10 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + process@~0.11.0: version "0.11.9" resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" @@ -3856,6 +4302,13 @@ process@~0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" +progress-stream@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" + dependencies: + speedometer "~0.1.2" + through2 "~0.2.3" + progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -3896,7 +4349,7 @@ punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" -punycode@^1.3.2: +punycode@^1.3.2, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -3916,6 +4369,10 @@ qs@6.4.0, qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + querystring-es3@~0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -3951,6 +4408,15 @@ raw-body@~2.2.0: iconv-lite "0.4.15" unpipe "1.0.0" +rc@^1.1.2: + version "1.2.5" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + rc@~1.1.6: version "1.1.7" resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.7.tgz#c5ea564bb07aff9fd3a5b32e906c1d3a65940fea" @@ -3982,6 +4448,21 @@ read-only-stream@^2.0.0: dependencies: readable-stream "^2.0.2" +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.1.0, readable-stream@^2.1.5: version "2.2.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.3.tgz#9cf49463985df016c8ae8813097a9293a9b33729" @@ -3994,6 +4475,18 @@ read-only-stream@^2.0.0: string_decoder "~0.10.x" util-deprecate "~1.0.1" +readable-stream@^2.2.2: + version "2.3.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + readable-stream@~1.0.0, readable-stream@~1.0.2, readable-stream@~1.0.31: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -4003,6 +4496,15 @@ readable-stream@~1.0.0, readable-stream@~1.0.2, readable-stream@~1.0.31: isarray "0.0.1" string_decoder "~0.10.x" +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readable-stream@~2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -4049,6 +4551,13 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + regenerate@^1.2.1: version "1.3.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.2.tgz#d1941c67bad437e1be76433add5b385f95b19260" @@ -4108,6 +4617,33 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +request@^2.45.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + request@^2.79.0: version "2.81.0" resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" @@ -4201,6 +4737,14 @@ safe-buffer@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" +safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + semver@5.3.0, semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" @@ -4306,6 +4850,12 @@ signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +single-line-log@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364" + dependencies: + string-width "^1.0.1" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -4326,6 +4876,12 @@ sntp@1.x.x: dependencies: hoek "2.x.x" +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + socket.io-adapter@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-0.5.0.tgz#cb6d4bb8bec81e1078b99677f9ced0046066bb8b" @@ -4412,6 +4968,32 @@ source-map@~0.1.30: dependencies: amdefine ">=0.0.4" +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" + +speedometer@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -4512,7 +5094,13 @@ string_decoder@~0.10.0, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" -stringstream@~0.0.4: +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -4522,10 +5110,22 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -4536,12 +5136,25 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" +sumchecker@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-1.3.1.tgz#79bb3b4456dd04f18ebdbc0d703a1d1daec5105d" + dependencies: + debug "^2.2.0" + es6-promise "^4.0.5" + supports-color@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" dependencies: has-flag "^1.0.0" +supports-color@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -4588,6 +5201,10 @@ text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" +throttleit@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" + through2@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" @@ -4595,6 +5212,13 @@ through2@^2.0.0: readable-stream "^2.1.5" xtend "~4.0.1" +through2@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f" + dependencies: + readable-stream "~1.1.9" + xtend "~2.1.1" + "through@>=2.2.7 <3", through@X.X.X, through@^2.3.6, through@~2.3.4: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -4633,10 +5257,20 @@ tough-cookie@2.3.1, tough-cookie@~2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.1.tgz#99c77dfbb7d804249e8a299d4cb0fd81fef083fd" +tough-cookie@~2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -4684,7 +5318,7 @@ type-is@~1.6.14: media-typer "0.3.0" mime-types "~2.1.13" -typedarray@~0.0.5: +typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -4712,6 +5346,10 @@ uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -4773,12 +5411,23 @@ uuid@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" +uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + v8flags@^2.0.10: version "2.0.11" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.0.11.tgz#bca8f30f0d6d60612cc2c00641e6962d42ae6881" dependencies: user-home "^1.1.1" +validate-npm-package-license@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + vary@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" @@ -4853,6 +5502,12 @@ which@^1.2.1: dependencies: isexe "^1.1.1" +which@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + wide-align@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad" @@ -4915,6 +5570,18 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" +xtend@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" + dependencies: + object-keys "~0.4.0" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + dependencies: + fd-slicer "~1.0.1" + yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" From fbb5866b776abe77821df451fc9a966bd8dc74fb Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 12 Mar 2018 14:24:43 +0100 Subject: [PATCH 003/127] more rewriting... --- lib/actions.js | 1 - lib/assertions.js | 196 ++++++++++++----- lib/conditions.js | 24 --- lib/elementInnerText.js | 5 + lib/elementSelector.js | 9 + lib/elementTester.js | 9 +- lib/findOptionsToString.js | 5 + lib/finders.js | 105 ++------- lib/normaliseText.js | 3 + lib/selector.js | 45 ++-- package.json | 1 + test/actionsSpec.js | 23 +- test/assemblies/DomAssembly.js | 67 +++--- test/assemblies/domAssemblySpec.js | 40 ++++ test/assertionsSpec.js | 327 +++++++++++++---------------- test/domSpec.js | 48 ++++- test/selectorsSpec.js | 2 +- yarn.lock | 21 +- 18 files changed, 493 insertions(+), 438 deletions(-) delete mode 100644 lib/conditions.js create mode 100644 lib/elementInnerText.js create mode 100644 lib/elementSelector.js create mode 100644 lib/findOptionsToString.js create mode 100644 lib/normaliseText.js create mode 100644 test/assemblies/domAssemblySpec.js diff --git a/lib/actions.js b/lib/actions.js index 8adef74..d999594 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -117,7 +117,6 @@ module.exports = { fill: function (field) { var isArray = Object.prototype.toString.call(field) === '[object Array]' var component = this - var Promise = this.promise() return new Promise(function (resolve, reject) { if (isArray) { fillField(component, field) diff --git a/lib/assertions.js b/lib/assertions.js index b249d62..19f9068 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1,7 +1,12 @@ -var Options = require('./options') -var expectOneElement = require('./expectOneElement') +const elementInnerText = require('./elementInnerText') +const elementSelector = require('./elementSelector') var errorHandler = require('./errorHandler') -const {expect} = require('chai') +const chai = require('chai') +const inspect = require('chai/lib/chai/utils/inspect') +const expect = chai.expect +var object = require('lowscore/object') +const elementMatches = require('./elementMatches') +const findOptionsToString = require('./findOptionsToString') module.exports = { shouldEqual: function (value) { @@ -25,12 +30,19 @@ module.exports = { .catch(errorHandler(new Error())) }, - shouldFind: function (selector, findOptions, existOptions) { + shouldHave: function (options) { + return this.elements().ensure(function (elements) { + assertElementExpectations(elements, options) + }, findOptionsToString(options)) + .catch(errorHandler(new Error())) + }, + + shouldFind: function (selector, findOptions) { return this.find(selector, findOptions) - .shouldExist(existOptions) + .shouldExist() }, - shouldNotExist: function (options) { + shouldNotExist: function () { return this.zero() .catch(errorHandler(new Error())) }, @@ -39,63 +51,153 @@ module.exports = { return this.shouldHave(options) }, - shouldHaveOld: function (options) { - return this.mapAll(function (elements) { - expectElements(elements) - elementExpectations(elements, options) + shouldHaveElement: function (fn) { + return this.one().ensure(fn) + }, + + shouldHaveElements: function (fn, options) { + return this.some().ensure(fn) + }, + + shouldNotHave: function (options) { + return this.elements().ensure(function (elements) { + let thrownError + try { + assertElementExpectations(elements, options) + throw (thrownError = new chai.AssertionError('expected elements not to have: ' + JSON.stringify(options))) + } catch (e) { + // we expect assertion errors, but nothing else + if (!(e instanceof chai.AssertionError) || e === thrownError) { + throw e + } + } + }, findOptionsToString(options)) + .catch(errorHandler(new Error())) + } +} + +function toArray (i) { + return i instanceof Array ? i : [i] +} + +function assertInexactValues (actual, expected) { + var equal = actual.length === expected.length && + actual.every(function (a, index) { + if (expected[index] === '') { + return a === '' + } else { + return a.indexOf(expected[index]) !== -1 + } }) - var self = this - var resolveOptions = Options.remove(options, ['timeout', 'interval']) - resolveOptions.allowMultiple = true + if (!equal) { + var expectedMessage = inspect(expected) + var actualMessage = inspect(actual) + + throw new chai.AssertionError('expected ' + actualMessage + ' to contain ' + expectedMessage) + } +} + +var elementExpectations = { + text: function (elements, expected) { + var expectedArray = toArray(expected) - var additionalAssertions = Object.keys(options).filter(function (finderMethodName) { - return options[finderMethodName] && options[finderMethodName].constructor === Object && typeof self[finderMethodName] === 'function' + var actual = elements.map(function (element) { + return elementInnerText(element) }) - var additionalOptions = Options.remove(options, additionalAssertions) + assertInexactValues(actual, expectedArray) + }, - var assertions = additionalAssertions.map(function (finderMethodName) { - if (typeof self[finderMethodName] === 'function') { - return self[finderMethodName]().shouldHave(additionalOptions[finderMethodName]) - } + value: function (elements, expected) { + var expectedArray = toArray(expected) + + var actual = elements.map(function (element) { + return element.value }) - assertions.push(this.addFinder(this.createElementTester(options)).shouldExist(resolveOptions)) - return Promise.all(assertions) - .catch(errorHandler(new Error())) + assertInexactValues(actual, expectedArray) }, - shouldHaveElement: function (fn, options) { - var $ = this.get('$') - var self = this + exactValue: function (elements, expected) { + var expectedArray = toArray(expected) - return this.addFinder({ - find: function (elements) { - expectOneElement(self, elements) - elements.toArray().forEach(function (element) { - fn($(element)) - }) - return elements - } - }).shouldExist(options) + var actual = elements.map(function (element) { + return element.value + }) + + expect(actual).to.eql(expectedArray) }, - shouldHaveElements: function (fn, options) { - options = Options.default(options, {allowMultiple: true, trace: false}) + exactText: function (elements, expected) { + var expectedArray = toArray(expected) - return this.addFinder({ - find: function (elements) { - fn(elements.toArray()) - return elements - } - }).shouldExist(options) + var actual = elements.map(function (element) { + return elementInnerText(element) + }) + + expect(actual).to.eql(expectedArray) }, - shouldNotHave: function (options) { - var resolveOptions = Options.remove(options, ['timeout', 'interval']) - resolveOptions.allowMultiple = true + checked: function (elements, expected) { + var expectedArray = toArray(expected) + + var actual = elements.map(function (element) { + return element.checked + }) + + expect(actual).to.eql(expectedArray) + }, + + css: function (elements, expected) { + var expectedArray = toArray(expected) + + var equal = elements.length === expectedArray.length && + elements.every(function (element, index) { + return elementMatches(element, expectedArray[index]) + }) + + if (!equal) { + var actual = elements.map(function (element) { + return elementSelector(element) + }) + + var expectedMessage = inspect(expectedArray) + var actualMessage = inspect(actual) - return this.addFinder(this.createElementTester(options)).shouldNotExist(resolveOptions) + throw new chai.AssertionError('expected ' + actualMessage + ' to match css ' + expectedMessage) + } + }, + + length: function (elements, expected) { + expect(elements.length).to.equal(expected) + }, + + attributes: function (elements, expected) { + var expectedArray = toArray(expected) + + var actual = elements.map(function (element, index) { + var expected = expectedArray[index] + + if (expected) { + return object(Object.keys(expected).map(function (name) { + return [name, element.getAttribute(name)] + })) + } + }) + + expect(actual).to.eql(expectedArray) } } + +function assertElementExpectations (elements, options) { + Object.keys(options).forEach(key => { + var expectation = elementExpectations[key] + + if (!expectation) { + throw new Error('no such expectation ' + key + ', try one of: ' + Object.keys(elementExpectations).join(', ')) + } + + expectation(elements, options[key]) + }) +} diff --git a/lib/conditions.js b/lib/conditions.js deleted file mode 100644 index 10c3b8a..0000000 --- a/lib/conditions.js +++ /dev/null @@ -1,24 +0,0 @@ -var trytryagain = require('trytryagain') - -function Conditions () { - this.mappers = [] -} - -Conditions.prototype.clone = function (mapper) { - const s = new Conditions() - s.mappers = this.mappers.slice() - s._input = this._input - return s -} - -Conditions.prototype.map = function (mapper) { - this.mappers.push(mapper) -} - -Conditions.prototype.result = function (input, {retry = trytryagain, reducer = (input, map) => map(input)} = {}) { - return Promise.resolve(retry(() => { - return this.mappers.reduce(reducer, input) - })) -} - -module.exports = Conditions diff --git a/lib/elementInnerText.js b/lib/elementInnerText.js new file mode 100644 index 0000000..06fff80 --- /dev/null +++ b/lib/elementInnerText.js @@ -0,0 +1,5 @@ +const normaliseText = require('./normaliseText') + +module.exports = function elementInnerText (element, selector) { + return normaliseText(element.innerText) +} diff --git a/lib/elementSelector.js b/lib/elementSelector.js new file mode 100644 index 0000000..fe942f3 --- /dev/null +++ b/lib/elementSelector.js @@ -0,0 +1,9 @@ +module.exports = function elementSelector (element) { + var id = element.id + var classes = element.className.split(/ +/g).filter(Boolean).sort() + var tag = element.tagName.toLowerCase() + + return (id || '') + + tag + + (classes.length ? '.' + classes.join('.') : '') +} diff --git a/lib/elementTester.js b/lib/elementTester.js index 5c69418..c85e5e6 100644 --- a/lib/elementTester.js +++ b/lib/elementTester.js @@ -2,6 +2,7 @@ var chai = require('chai') var expect = chai.expect var elementsToString = require('./elementsToString') require('array.prototype.find').shim() +const normaliseText = require('./normaliseText') function assertElementProperties ($, elements, expected, getProperty, exact) { function assertion (actual, expected) { @@ -53,10 +54,6 @@ function assertElementProperties ($, elements, expected, getProperty, exact) { } } -function getNormalisedText (el) { - return (el.innerText() || '').replace(/ +/g, ' ').replace(/ *\r?\n */g, '\n') -} - function getValue (e, property) { var val = e.val() // Fails with missing value attribute in VDOM without 'string' test (returns SoftSetHook{value: ''}) @@ -75,7 +72,7 @@ module.exports = { } }, text: function ($el, message, text) { - assertElementProperties(this.get('$'), $el, text, function (e) { return getNormalisedText(e) }, text === '') + assertElementProperties(this.get('$'), $el, text, function (e) { return normaliseText(e) }, text === '') }, length: function ($el, message, length) { if ($el.length !== length) { @@ -102,7 +99,7 @@ module.exports = { } }, exactText: function ($el, message, exactText) { - assertElementProperties(this.get('$'), $el, exactText, function (e) { return getNormalisedText(e) }, true) + assertElementProperties(this.get('$'), $el, exactText, function (e) { return normaliseText(e) }, true) }, value: function ($el, message, value) { assertElementProperties(this.get('$'), $el, value, getValue, value === '') diff --git a/lib/findOptionsToString.js b/lib/findOptionsToString.js new file mode 100644 index 0000000..20fb834 --- /dev/null +++ b/lib/findOptionsToString.js @@ -0,0 +1,5 @@ +module.exports = function findOptionsToString (options) { + return '(' + Object.keys(options).map(function (key) { + return key + ' = ' + JSON.stringify(options[key]) + }) + ')' +} diff --git a/lib/finders.js b/lib/finders.js index a089cb8..0fc7cc3 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -1,7 +1,9 @@ var flatten = require('lowscore/flatten') -var expect = require('chai').expect +require('chai').config.truncateThreshold = 0 const elementVisible = require('./elementVisible') const elementMatches = require('./elementMatches') +const elementInnerText = require('./elementInnerText') +const findOptionsToString = require('./findOptionsToString') module.exports = { find: function (selector, options) { @@ -23,36 +25,6 @@ module.exports = { }, 'is: ' + selector) }, - text: function () { - return this.element().then(function (element) { - return element.innerText - }) - }, - - attr: function (name) { - return this.element().then(function (element) { - return element.getAttribute(name) - }) - }, - - prop: function (name) { - return this.element().then(function (element) { - return element[name] - }) - }, - - html: function () { - return this.element().then(function (element) { - return element.innerHTML - }) - }, - - val: function () { - return this.element().then(function (element) { - return element.value - }) - }, - containing: function (selector, options) { var self = this @@ -85,65 +57,24 @@ module.exports = { return this.filter(function (element) { var tagName = element.tagName return !((tagName === 'BUTTON' || tagName === 'INPUT') && element.disabled) - }, '[disabled=false]') + }, 'enabled') }, element: function () { - return this.one().then(function (element) { - if (isHTMLElement(element)) { - return element - } else { - throw new Error('expected DOM element') - } - }) + return this.elements().one() }, elements: function () { - return this.some().then(function (elements) { + return this.ensure(function (elements) { if (isArrayOfHTMLElements(elements)) { return elements } else { throw new Error('expected DOM elements') } - }) - }, - - shouldHave: function (options) { - return this.mapAll(function (elements) { - expectElements(elements) - assertElementExpectations(elements, options) - }, findOptionsToString(options)) + }, 'elements').some() } } -function toArray (i) { - return i instanceof Array ? i : [i] -} - -var elementExpectations = { - text: function (elements, expected) { - var expectedArray = toArray(expected) - - var actual = elements.map(function (element) { - return element.innerText - }) - - expect(actual).to.eql(expectedArray) - } -} - -function assertElementExpectations (elements, options) { - Object.keys(options).forEach(key => { - var expectation = elementExpectations[key] - - if (!expectation) { - throw new Error('no such expectation ' + key + ', try one of: ' + Object.keys(elementExpectations).join(', ')) - } - - expectation(elements, options[key]) - }) -} - function isArrayOfHTMLElements (elements) { return elements instanceof Array && elements.every(function (element) { @@ -157,14 +88,6 @@ function expectElements (elements) { } } -function expectOneElement (elements) { - if (elements.length > 1) { - throwElementsError(elements, 'expected just one element') - } else if (elements.length === 0) { - throw new Error('expected at least one element') - } -} - function throwElementsError (elements, message) { var error = new Error(message) error.elements = elements @@ -173,11 +96,15 @@ function throwElementsError (elements, message) { var findOptionFilters = { text: function (element, text) { - return element.innerText.indexOf(text) !== -1 + if (text === '') { + return elementInnerText(element) === text + } else { + return elementInnerText(element).indexOf(text) !== -1 + } }, exactText: function (element, text) { - return element.innerText === text + return elementInnerText(element) === text }, index: function (element, expectedIndex, actualIndex) { @@ -230,9 +157,3 @@ function findElements (element, selector, findOptions, selectorOptions) { function isIframe (element) { return isHTMLElement(element, 'HTMLIFrameElement') } - -function findOptionsToString (options) { - return '(' + Object.keys(options).map(function (key) { - return key + ' = ' + JSON.stringify(options[key]) - }) + ')' -} diff --git a/lib/normaliseText.js b/lib/normaliseText.js new file mode 100644 index 0000000..3e96a75 --- /dev/null +++ b/lib/normaliseText.js @@ -0,0 +1,3 @@ +module.exports = function normaliseText (text) { + return text.replace(/ +/g, ' ').replace(/ *\r?\n */g, '\n') +} diff --git a/lib/selector.js b/lib/selector.js index 631c1c1..6a674a5 100644 --- a/lib/selector.js +++ b/lib/selector.js @@ -1,9 +1,8 @@ -var Conditions = require('./conditions') var extend = require('lowscore/extend') var retry = require('trytryagain') function Selector () { - this._conditions = new Conditions() + this._conditions = [] this._options = { visibleOnly: true } @@ -24,7 +23,7 @@ function retryFromOptions (options) { Selector.prototype.mapAll = function (mapper, name) { return this.clone(function (clone) { - clone._conditions.map({ + clone._conditions.push({ name: name, map: mapper }) @@ -33,7 +32,7 @@ Selector.prototype.mapAll = function (mapper, name) { Selector.prototype.map = function (mapper, name) { return this.clone(function (clone) { - clone._conditions.map({ + clone._conditions.push({ name: name, map: function (elements) { return elements.map(mapper) @@ -44,7 +43,7 @@ Selector.prototype.map = function (mapper, name) { Selector.prototype.filter = function (filter, name) { return this.clone(function (clone) { - clone._conditions.map({ + clone._conditions.push({ name: name, map: function (elements) { return elements.filter(filter) @@ -63,21 +62,23 @@ Selector.prototype.then = function () { var self = this var retry = retryFromOptions(this._options) var accumulator = {} - var promise = this._conditions.result(accumulator, { - retry: retry, - reducer: (accumulator, mapper, index) => { - if (index === 0) { - accumulator.value = self._value - accumulator.transforms = [] - } - accumulator.value = mapper.map(accumulator.value) - accumulator.transforms.push({ - value: accumulator.value, - name: mapper.name - }) - return accumulator + + function reducer (accumulator, mapper, index) { + if (index === 0) { + accumulator.value = self._value + accumulator.transforms = [] } - }).then(function (accumulator) { + accumulator.value = mapper.map(accumulator.value) + accumulator.transforms.push({ + value: accumulator.value, + name: mapper.name + }) + return accumulator + } + + var promise = Promise.resolve(retry(function () { + return self._conditions.reduce(reducer, accumulator) + })).then(function (accumulator) { return accumulator.value }, function (error) { var foundMessage = accumulator.transforms.map(function (transform) { @@ -96,7 +97,7 @@ Selector.prototype.catch = function (fn) { } function copySelectorFields (from, to) { - to._conditions = from._conditions.clone() + to._conditions = from._conditions.slice() to._value = from._value to._options = from._options } @@ -110,11 +111,11 @@ Selector.prototype.clone = function (modifier) { return clone } -Selector.prototype.ensure = function (assertion) { +Selector.prototype.ensure = function (assertion, name) { return this.mapAll(value => { assertion(value) return value - }) + }, name || 'ensure') } Selector.prototype.zero = function () { diff --git a/package.json b/package.json index bffa367..5486d61 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "detect-node": "^2.0.3", "electron": "^1.8.2", "electron-mocha": "^6.0.1", + "escape-string-regexp": "1.0.5", "express": "^4.15.2", "httpism": "^2.6.2", "hyperdom": "^0.5.0", diff --git a/test/actionsSpec.js b/test/actionsSpec.js index 74cc69e..cc9c008 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -1,4 +1,3 @@ -console.log('here') var describeAssemblies = require('./describeAssemblies') const DomAssembly = require('./assemblies/DomAssembly') var demand = require('must') @@ -214,7 +213,7 @@ describe('actions', function () { it('errors when the input is not a select', function () { var promise = browser.find('.element').select({text: 'Whatevs'}) assembly.eventuallyInsertHtml('
') - return demand(promise).reject.with.error(/expected one element/) + return demand(promise).reject.with.error(/expected some elements/) }) it('selects an option using text that is falsy', function () { @@ -350,7 +349,7 @@ describe('actions', function () { var promise = browser.find('.element').typeIn('whatevs') var inputPromise = assembly.eventuallyInsertHtml(html) return inputPromise.then(function (input) { - return demand(promise).reject.with.error(/expected one element/) + return demand(promise).reject.with.error(/expected some elements/) }) }) }) @@ -437,7 +436,7 @@ describe('actions', function () { }) describe('fill', function () { - it.only('fills a component with the supplied values', function () { + it('fills a component with the supplied values', function () { var component = browser.component({ title: function () { return this.find('.title') @@ -458,7 +457,7 @@ describe('actions', function () { }) }) - domTest('can fill using shortcut syntax', function (browser, dom) { + it('can fill using shortcut syntax', function () { var component = browser.component({ title: function () { return this.find('.title') @@ -479,13 +478,13 @@ describe('actions', function () { {typeIn: 'name', options: {text: 'Joe'}}, {click: 'agree'} ]).then(function () { - demand(dom.el.find('.title').val()).to.equal('Mrs') - demand(dom.el.find('.name').val()).to.equal('Joe') - demand(dom.el.find('.agree input').prop('checked')).to.equal(true) + demand(assembly.find('.title').value).to.equal('Mrs') + demand(assembly.find('.name').value).to.equal('Joe') + demand(assembly.find('.agree input').checked).to.equal(true) }) }) - domTest('can execute actions on a component', function (browser, dom) { + it('can execute actions on a component', function () { var myActionRan = false var component = browser.component({ myAction: function () { @@ -509,7 +508,7 @@ describe('actions', function () { }) }) - domTest('throws an error if the action cannot be found', function (browser, dom) { + it('throws an error if the action cannot be found', function () { var component = browser.component({}) var promise = component.fill([ @@ -519,7 +518,7 @@ describe('actions', function () { return demand(promise).reject.with.error(/actionDoesNotExist/) }) - domTest('throws an error when trying to call an action on a field which does not exist', function (browser, dom) { + it('throws an error when trying to call an action on a field which does not exist', function () { var component = browser.component({}) var promise = component.fill([ @@ -529,7 +528,7 @@ describe('actions', function () { return demand(promise).reject.with.error("Field 'name' does not exist") }) - domTest('throws an error if the field does not exist', function (browser, dom) { + it('throws an error if the field does not exist', function () { var component = browser.component({}) var promise = component.fill( diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.js index fbd013a..30aa649 100644 --- a/test/assemblies/DomAssembly.js +++ b/test/assemblies/DomAssembly.js @@ -11,6 +11,8 @@ module.exports = class DomAssembly { constructor () { this.delayedOperations = 0 this.jQuery = $ + this.retries = [] + this.delayedActions = [] } div () { @@ -24,36 +26,45 @@ module.exports = class DomAssembly { return browserMonkey.scope(this._div).options({ retry: (retry) => { - return this.runRetry(retry) - } - }) - } - - async runRetry (fn) { - if (!this._normalRetry) { - return new Promise((resolve, reject) => { - let first = true + return new Promise((resolve, reject) => { + const success = result => { + resolve(result) + const index = this.retries.indexOf(retrier) + if (index !== -1) { + this.retries.splice(index, 1) + } + } - this.retry = () => { - try { - resolve(fn()) - } catch (e) { - if (!this.delayedOperations && !first) { + const retrier = () => { + try { + success(retry()) + } catch (e) { reject(e) } } - } - this.retry() - first = false - - setTimeout(() => { - const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)).filter(m => /^eventually/.test(m)) - reject(new Error('no delayed actions made, use one of ' + methods.join(', '))) - }, 1000) - }) - } else { - return trytryagain(fn) + + this.retries.push(retrier) + + this.tick() + }) + } + }) + } + + async tick () { + if (this.ticking) { + return } + + this.ticking = true + + await new Promise(resolve => setTimeout(resolve)) + + this.delayedActions.forEach(action => action()) + this.delayedActions = [] + this.retries.slice().forEach(retry => retry()) + + this.ticking = false } localUrl (path) { @@ -83,12 +94,10 @@ module.exports = class DomAssembly { } eventually (fn) { - this.delayedOperations++ + this.tick() return new Promise(resolve => { - setTimeout(() => { - this.delayedOperations-- + this.delayedActions.push(() => { var result = fn() - this.retry() resolve(result) }) }) diff --git a/test/assemblies/domAssemblySpec.js b/test/assemblies/domAssemblySpec.js new file mode 100644 index 0000000..efd830a --- /dev/null +++ b/test/assemblies/domAssemblySpec.js @@ -0,0 +1,40 @@ +const DomAssembly = require('./DomAssembly') +const {expect} = require('chai') + +describe('DomAssembly', () => { + let assembly + let browser + + beforeEach(() => { + assembly = new DomAssembly() + browser = assembly.browserMonkey() + }) + + it('can insert html', async () => { + const element = assembly.insertHtml('
') + const found = await browser.find('.a').element() + expect(found).to.equal(element) + }) + + it('can eventually insert html before searching', async () => { + const element = assembly.eventuallyInsertHtml('
') + const found = await browser.find('.a').element() + expect(found).to.equal(await element) + }) + + it('can eventually insert html after searching', async () => { + const found = browser.find('.a').element() + const element = assembly.eventuallyInsertHtml('
') + expect(await found).to.equal(await element) + }) + + it('can eventually insert html before searching twice', async () => { + const element = assembly.eventuallyInsertHtml('
') + const found = await Promise.all([ + browser.find('.a').element(), + browser.find('.a').element() + ]) + expect(found[0]).to.equal(await element) + expect(found[1]).to.equal(await element) + }) +}) diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index 70d4c9d..870f36c 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -1,40 +1,47 @@ +var describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +const escapeStringRegexp = require('escape-string-regexp') var assert = require('assert') var demand = require('must') -var domTest = require('./domTest') describe('assertions', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + describe('shouldNotExist', function () { - domTest("should ensure that element eventually doesn't exists", function (browser, dom) { - dom.insert('
') - dom.insert('
') + it("should ensure that element eventually doesn't exists", function () { + assembly.insertHtml('
') + assembly.insertHtml('
') var good = browser.find('.removing').shouldNotExist() var bad = browser.find('.staying').shouldNotExist() - setTimeout(function () { - dom.el.find('.removing').remove() - }, 50) + assembly.eventuallyDeleteHtml('.removing') return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(/expected no elements/) ]) }) - domTest('allows trytryagain parameters to be used', function (browser, dom) { - dom.insert('
') + it('allows trytryagain parameters to be used', function () { + assembly.insertHtml('
') var promise = browser.find('.removing').shouldNotExist({timeout: 500, interval: 100}) - setTimeout(function () { - dom.el.find('.removing').remove() - }, 50) + assembly.eventuallyDeleteHtml('.removing') return promise }) - domTest('stack trace', function (browser, dom) { - dom.insert('
') + it('stack trace', function () { + assembly.insertHtml('
') return browser.find('div') .shouldNotExist() @@ -45,7 +52,7 @@ describe('assertions', function () { }) describe('shouldExist', function () { - domTest('stack trace', function (browser, dom) { + it('stack trace', function () { return browser.find('div') .shouldExist() .assertStackTrace(__filename) @@ -55,7 +62,7 @@ describe('assertions', function () { }) describe('shouldFind', function () { - domTest('stack trace', function (browser, dom) { + it('stack trace', function () { return browser .shouldFind('div') .assertStackTrace(__filename) @@ -65,32 +72,30 @@ describe('assertions', function () { }) describe('is', function () { - domTest('should eventually find an element if it has a class', function (browser, dom) { + it('should eventually find an element if it has a class', function () { + var element = assembly.insertHtml('
') + var good = browser.find('.element').is('.good').shouldExist() var bad = browser.find('.element').is('.bad').shouldExist() - setTimeout(function () { - var element = dom.insert('
') - - setTimeout(function () { - element.addClass('good') - }, 10) - }, 10) + assembly.eventually(function () { + element.classList.add('good') + }) - return Promise.all([good, demand(bad).reject.with.error()]) + return Promise.all([good, demand(bad).reject.with.error(/expected some elements/)]) }) }) - domTest('eventually finds an element containing text', function (browser, dom) { + it('eventually finds an element containing text', function () { var promise = browser.find('.element', {text: 'some t'}).shouldExist() - dom.eventuallyInsert('
some text
') + assembly.eventuallyInsertHtml('
some text
') return promise }) - domTest('eventually finds an element containing text as it appears on the page', function (browser, dom) { - var promise = browser.find('.element').shouldHave({text: 'This is some text that is all on one line.\nAnd some more on another line'}) + it('eventually finds an element containing text as it appears on the page', function () { + var promise = browser.find('.element').shouldHave({text: 'This is some text that is all on one line.\nAnd some more on another line.'}) /* eslint-disable no-multi-str */ - dom.eventuallyInsert('
\ + assembly.eventuallyInsertHtml('
\ This\ is\ some\ @@ -103,20 +108,20 @@ describe('assertions', function () { return promise }) - domTest('eventually finds an element containing exactText', function (browser, dom) { + it('eventually finds an element containing exactText', function () { var good = browser.find('.a', {exactText: '8'}).shouldExist() var bad = browser.find('.b', {exactText: '8'}).shouldExist() - dom.eventuallyInsert('
8
28
') + assembly.eventuallyInsertHtml('
8
28
') return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(/expected some elements/) ]) }) - domTest("treats assertion of text: '' as exact text", function (browser, dom) { - dom.eventuallyInsert('
something
') + it("treats assertion of text: '' as exact text", function () { + assembly.eventuallyInsertHtml('
something
') return browser.find('.a', {text: 'something'}).shouldExist().then(function () { return Promise.all([ @@ -127,8 +132,8 @@ describe('assertions', function () { }) describe('shouldHave', function () { - domTest('stack trace', function (browser, dom) { - dom.insert('
hello
') + it('stack trace', function () { + assembly.insertHtml('
hello
') return browser.find('div') .shouldHave({text: 'something'}) .assertStackTrace(__filename) @@ -136,72 +141,44 @@ describe('assertions', function () { mochaOnly: true }) - domTest('eventually finds an element and asserts that it has text', function (browser, dom) { + it('eventually finds an element and asserts that it has text', function () { var good = browser.find('.element').shouldHave({text: 'some t'}) var bad = browser.find('.element').shouldHave({text: 'sme t'}) - dom.eventuallyInsert('
some text
') - - return Promise.all([ - good, - demand(bad).reject.with.error() - ]) - }) - - domTest('allows trytryagain parameters to be used', function (browser, dom) { - var good = browser.find('.element').shouldHave({text: 'some t', timeout: 400, interval: 100}) - var bad = browser.find('.element').shouldHave({text: 'sme t'}) - - dom.eventuallyInsert('
some text
') + assembly.eventuallyInsertHtml('
some text
') return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(partMatch("expected [ 'some text' ] to contain [ 'sme t' ]")) ]) }) - domTest('finds duplicate text when asserting array of text', function (browser, dom) { - dom.insert('
a
') - dom.insert('
a
') + it('finds duplicate text when asserting array of text', function () { + assembly.insertHtml('
a
') + assembly.insertHtml('
a
') return browser.find('.element1').shouldHave({text: ['a', 'a']}) }) - domTest('eventually finds an element and asserts that it has value', function (browser, dom) { + it('eventually finds an element and asserts that it has value', function () { var good1 = browser.find('.element1 input').shouldHave({value: 'some t'}) - var good2 = browser.find('.element2 input').shouldHave({value: 0}) + var good2 = browser.find('.element2 input').shouldHave({value: '0'}) var bad = browser.find('.element1 input').shouldHave({value: 'sme t'}) - dom.eventuallyInsert('
') - dom.eventuallyInsert('
') + assembly.eventuallyInsertHtml('
') + assembly.eventuallyInsertHtml('
') return Promise.all([ good1, good2, - demand(bad).reject.with.error() + demand(bad).reject.with.error(partMatch("expected [ 'some text' ] to contain [ 'sme t' ]")) ]) }) - domTest('error has a tip with suggestions for how to fix it', function (browser, dom) { - dom.insert('abc') - dom.insert('bac') - dom.insert('cba') - - return browser.find('span').shouldHave({ - text: [ - 'cba', - 'abc', - 'bac' - ] - }).catch(function (error) { - demand(error.message).to.include('\nThe text was found but in a different order than specified - maybe you need some sorting?') - }) - }) - - domTest('error contains actual element text when no match found', function (browser, dom) { - dom.insert('abc') - dom.insert('bac') - dom.insert('c') + it('error contains actual element text when no match found', function () { + assembly.insertHtml('abc') + assembly.insertHtml('bac') + assembly.insertHtml('c') return browser.find('span').shouldHave({ text: [ @@ -214,35 +191,35 @@ describe('assertions', function () { }) }) - domTest('finds an element with exact value', function (browser, dom) { + it('finds an element with exact value', function () { var bad = browser.find('.element1 input').shouldHave({exactValue: 'some t'}) var good = browser.find('.element1 input').shouldHave({exactValue: 'some text'}) - dom.eventuallyInsert('
') + assembly.eventuallyInsertHtml('
') return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(partMatch("expected [ 'some text' ] to deeply equal [ 'some t' ]")) ]) }) - domTest("treats assertion of value: '' as exact value", function (browser, dom) { + it("treats assertion of value: '' as exact value", function () { var bad = browser.find('.element1 input').shouldHave({value: ''}) var good = browser.find('.element2 input').shouldHave({value: ''}) - dom.eventuallyInsert('
\n' + + assembly.eventuallyInsertHtml('
\n' + '
\n' + '
\n' + '
') return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(partMatch("expected [ 'some text' ] to contain [ '' ]")) ]) }) - domTest('treats selects with no value as empty string', function (browser, dom) { - dom.insert('') + it('treats selects with no value as empty string', function () { + assembly.insertHtml('') var select = browser.find('select') @@ -252,27 +229,8 @@ describe('assertions', function () { ]) }) - domTest('recurses through a tree of assertions', function (browser, dom) { - dom.insert('
Aug 2055LHR
') - return browser.component({ - airport: function () { - return this.find('.airport').component({ - date: function () { return this.find('.date') }, - text: function () { return this.find('.text') }, - blank: function () { return this.find('.blank') } - }) - } - }).shouldHave({ - airport: { - date: { exactText: 'Aug 2055' }, - text: { text: 'LHR' }, - blank: { text: undefined } - } - }) - }) - - domTest('verifies attributes are present', function (browser, dom) { - dom.insert('hello') + it('verifies attributes are present', function () { + assembly.insertHtml('hello') var good = browser.find('a').shouldHave({ attributes: { id: 'abc', @@ -286,12 +244,12 @@ describe('assertions', function () { }) return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(partMatch("expected [ { class: null } ] to deeply equal [ { class: 'other' } ]")) ]) }) - domTest('verifies array of attributes are present', function (browser, dom) { - dom.insert('
') + it('verifies array of attributes are present', function () { + assembly.insertHtml('
') var good = browser.find('img').shouldHave({ attributes: [ {src: '/a'}, @@ -308,172 +266,172 @@ describe('assertions', function () { }) return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(partMatch("expected [ { src: '/a' }, { src: '/b' }, { src: '/c' } ] to deeply equal [ { src: '/c' }, { src: '/a' }, { src: '/b' } ]")) ]) }) describe('exactText', function () { - domTest('eventually finds elements that have the exact array of text', function (browser, dom) { + it('eventually finds elements that have the exact array of text', function () { var promise = browser.find('.element option').shouldHave({exactText: ['', 'Mr', 'Mrs']}) - dom.eventuallyInsert('') + assembly.eventuallyInsertHtml('') return promise }) - domTest('fails to find exact text', function (browser, dom) { + it('fails to find exact text', function () { var promise = browser.find('option').shouldHave({exactText: ['', 'Mr', 'Mrs']}) - dom.eventuallyInsert('') + assembly.eventuallyInsertHtml('') - return demand(promise).reject.with.error() + return demand(promise).reject.with.error(partMatch("expected [ 'Optional', 'Mr', 'Mrs' ] to deeply equal [ '', 'Mr', 'Mrs' ]")) }) }) describe('checkboxes', function () { - domTest('eventually finds a checked checkbox', function (browser, dom) { + it('eventually finds a checked checkbox', function () { var good = browser.find('.checkbox').shouldHave({checked: true}) - var checkbox = dom.insert('') - setTimeout(function () { - checkbox.prop('checked', true) - }, 20) + var checkbox = assembly.insertHtml('') + assembly.eventually(function () { + checkbox.checked = true + }) return Promise.all([ good ]) }) - domTest('fails if only one of many checkboxes is checked', function (browser, dom) { + it('fails if we expected one checkbox, but found many', function () { var good = browser.find('.checkbox').shouldHave({checked: true}) - var checkbox = dom.insert('') - dom.insert('') - setTimeout(function () { - checkbox.prop('checked', true) - }, 20) + var checkbox = assembly.insertHtml('') + assembly.insertHtml('') + assembly.eventually(function () { + checkbox.checked = true + }) return Promise.all([ - demand(good).reject.with.error() + demand(good).reject.with.error(partMatch('expected [ true, false ] to deeply equal [ true ]')) ]) }) - domTest('ensures that each checkbox in the scope is either checked or unchecked', function (browser, dom) { + it('ensures that each checkbox in the scope is either checked or unchecked', function () { var good = browser.find('.checkbox').shouldHave({checked: [true, false]}) var bad = browser.find('.checkbox').shouldHave({checked: [false, true]}) - var checkbox = dom.insert('') - dom.insert('') - setTimeout(function () { - checkbox.prop('checked', true) - }, 20) + var checkbox = assembly.insertHtml('') + assembly.insertHtml('') + assembly.eventually(function () { + checkbox.checked = true + }) return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(partMatch('expected [ true, false ] to deeply equal [ false, true ]')) ]) }) - domTest('fails to find a checked checkbox', function (browser, dom) { + it('fails to find a checked checkbox', function () { var good = browser.find('.checkbox').shouldHave({checked: false}) var bad = browser.find('.checkbox').shouldHave({checked: true}) - dom.insert('') + assembly.insertHtml('') return Promise.all([ good, - demand(bad).reject.with.error() + demand(bad).reject.with.error(partMatch('expected [ false ] to deeply equal [ true ]')) ]) }) }) - domTest('eventually finds elements and asserts that they each have text', function (browser, dom) { + it('eventually finds elements and asserts that they each have text', function () { var good = browser.find('.element div').shouldHave({text: ['one', 2]}) var bad1 = browser.find('.element div').shouldHave({text: ['one']}) var bad2 = browser.find('.element div').shouldHave({text: ['one', 'three']}) - dom.eventuallyInsert('
\nfirst one
number 2\n
') + assembly.eventuallyInsertHtml('
\nfirst one
number 2\n
') return Promise.all([ good, - demand(bad1).reject.with.error(), - demand(bad2).reject.with.error() + demand(bad1).reject.with.error(partMatch("expected [ 'first one', 'number 2' ] to contain [ 'one' ]")), + demand(bad2).reject.with.error(partMatch("expected [ 'first one', 'number 2' ] to contain [ 'one', 'three' ]")) ]) }) - domTest('eventually finds elements and asserts that they each have value', function (browser, dom) { + it('eventually finds elements and asserts that they each have value', function () { var good = browser.find('.element input').shouldHave({value: ['one', 2, 0]}) var bad1 = browser.find('.element input').shouldHave({value: ['one']}) var bad2 = browser.find('.element input').shouldHave({value: ['one', 'three']}) - dom.eventuallyInsert('
') + assembly.eventuallyInsertHtml('
') return Promise.all([ good, - demand(bad1).reject.with.error(), - demand(bad2).reject.with.error() + demand(bad1).reject.with.error(partMatch("expected [ 'first one', 'number 2', '0' ] to contain [ 'one' ]")), + demand(bad2).reject.with.error(partMatch("expected [ 'first one', 'number 2', '0' ] to contain [ 'one', 'three' ]")) ]) }) - domTest('eventually finds an element and asserts that it has css', function (browser, dom) { + it('eventually finds an element and asserts that it has css', function () { var good = browser.find('.element').shouldHave({css: '.the-class'}) var bad1 = browser.find('.element').shouldHave({css: '.not-the-class'}) var bad2 = browser.find('.element').shouldHave({css: '.not-found'}) - dom.eventuallyInsert('
some text
') + assembly.eventuallyInsertHtml('
some text
') return Promise.all([ good, - demand(bad1).reject.with.error(), - demand(bad2).reject.with.error() + demand(bad1).reject.with.error(partMatch("expected [ 'div.element.the-class' ] to match css [ '.not-the-class' ]")), + demand(bad2).reject.with.error(partMatch("expected [ 'div.element.the-class' ] to match css [ '.not-found' ]")) ]) }) - domTest('eventually finds an element and asserts that it has n elements', function (browser, dom) { + it('eventually finds an element and asserts that it has n elements', function () { var good = browser.find('.element').shouldHave({length: 2}) var bad1 = browser.find('.element').shouldHave({length: 1}) - dom.eventuallyInsert('
') - dom.eventuallyInsert('
') + assembly.eventuallyInsertHtml('
') + assembly.eventuallyInsertHtml('
') return Promise.all([ good, - demand(bad1).reject.with.error() + demand(bad1).reject.with.error(partMatch('expected 2 to equal 1')) ]) }) - domTest('eventually finds an element and asserts that it passes an assertion', function (browser, dom, $) { + it('eventually finds an element and asserts that it passes an assertion', function () { var good1 = browser.find('.element').shouldHaveElement(function (element) { - assert.equal(element.text(), 'a') + assert.equal(element.innerText, 'a') }) var bad1 = browser.find('.multi').shouldHaveElement(function (element) { - assert.equal(element.text(), 'b') + assert.equal(element.innerText, 'b') }) var bad2 = browser.find('.element').shouldHaveElement(function (element) { - assert.equal(element.text(), 'b') + assert.equal(element.innerText, 'b') }) - var element = dom.insert('
') - dom.eventuallyInsert('
') - dom.eventuallyInsert('
b
') + var element = assembly.insertHtml('
') + assembly.eventuallyInsertHtml('
') + assembly.eventuallyInsertHtml('
b
') - setTimeout(function () { - element.text('a') - }, 30) + assembly.eventually(function () { + element.innerText = 'a' + }) return Promise.all([ good1, - demand(bad1).reject.with.error(/expected to find exactly one element/), - demand(bad2).reject.with.error("'a' == 'b'") + demand(bad1).reject.with.error(/expected one element/), + demand(bad2).reject.with.error(/'a' == 'b'/) ]) }) - domTest('eventually finds elements and asserts that they pass an assertion', function (browser, dom, $) { + it('eventually finds elements and asserts that they pass an assertion', function () { var good1 = browser.find('.element').shouldHaveElements(function (elements) { var xs = elements.map(function (element) { - return $(element).attr('data-x') + return assembly.jQuery(element).attr('data-x') }) assert.deepEqual(xs, ['one', 'two', 'three']) @@ -481,15 +439,15 @@ describe('assertions', function () { var bad1 = browser.find('.element').shouldHaveElements(function (elements) { var xs = elements.map(function (element) { - return $(element).attr('data-x') + return assembly.jQuery(element).attr('data-x') }) assert.deepEqual(xs, ['one', 'two']) }) - dom.eventuallyInsert('
') - dom.eventuallyInsert('
') - dom.eventuallyInsert('
') + assembly.eventuallyInsertHtml('
') + assembly.eventuallyInsertHtml('
') + assembly.eventuallyInsertHtml('
') return Promise.all([ good1, @@ -497,19 +455,19 @@ describe('assertions', function () { ]) }) - domTest('copies error properly', function (browser, dom, $) { + it('copies error properly', function () { var errorThrown var good1 = browser.find('.element').shouldHaveElement(function (element) { try { - assert.equal($(element).text(), 'not text') + assert.equal(assembly.jQuery(element).text(), 'not text') } catch (error) { errorThrown = error throw error } }) - dom.eventuallyInsert('
text
') + assembly.eventuallyInsertHtml('
text
') return good1.catch(function (error) { assert.equal(error, errorThrown) @@ -518,20 +476,25 @@ describe('assertions', function () { }) describe('shouldNotHave', function () { - domTest('eventually finds an element and asserts that it does not have text', function (browser, dom) { + it('eventually finds an element and asserts that it does not have text', function () { var promise = browser.find('.element').shouldNotHave({text: 'sme t'}) - dom.eventuallyInsert('
some text
') + assembly.eventuallyInsertHtml('
some text
') return promise }) - domTest('allows trytryagain parameters to be used', function (browser, dom) { + it('allows trytryagain parameters to be used', function () { var promise = browser.find('.element').shouldNotHave({text: 'sme t', timeout: 400, interval: 100}) - dom.eventuallyInsert('
some text
') + assembly.eventuallyInsertHtml('
some text
') return promise }) }) + }) }) + +function partMatch (str) { + return new RegExp(escapeStringRegexp(str)) +} diff --git a/test/domSpec.js b/test/domSpec.js index a4d8692..a129f5a 100644 --- a/test/domSpec.js +++ b/test/domSpec.js @@ -7,6 +7,7 @@ const elementMatches = require('../lib/elementMatches') const elementSubmit = require('../lib/elementSubmit') const elementTriggerEvent = require('../lib/elementTriggerEvent') const elementEnterText = require('../lib/elementEnterText') +const elementInnerText = require('../lib/elementInnerText') const {expect} = require('chai') describe('dom', () => { @@ -18,6 +19,15 @@ describe('dom', () => { assembly.div() }) + function preventDefaultSubmit (handler) { + return e => { + if (e.type === 'submit') { + e.preventDefault() + } + return handler(e) + } + } + describe('submitting', () => { it('form with submit button receives submit event when element submitted inside it', () => { var formEvents = [] @@ -29,7 +39,7 @@ describe('dom', () => { var button = assembly.find('button') ;['submit', 'keydown', 'keypress', 'click', 'keyup'].forEach(type => { - form.addEventListener(type, () => formEvents.push(type)) + form.addEventListener(type, preventDefaultSubmit(e => formEvents.push(type))) input.addEventListener(type, () => inputEvents.push(type)) button.addEventListener(type, () => buttonEvents.push(type)) }) @@ -65,7 +75,7 @@ describe('dom', () => { var input = assembly.find('input') ;['submit', 'keydown', 'keypress', 'click', 'keyup'].forEach(type => { - form.addEventListener(type, () => formEvents.push(type)) + form.addEventListener(type, preventDefaultSubmit(e => formEvents.push(type))) input.addEventListener(type, () => inputEvents.push(type)) }) @@ -88,6 +98,32 @@ describe('dom', () => { }) }) + describe('innerText', () => { + it('normalises innerText removing extra spaces at the end of lines', () => { + var element = assembly.insertHtml(` +
+ first + line +
+ second + line +
+ `) + + expect(elementInnerText(element)).to.equal('first line\nsecond line') + }) + + it('normalises innerText removing extra spaces between words', () => { + var element = assembly.insertHtml(` +
+ two words +
+ `) + + expect(elementInnerText(element)).to.equal('two words') + }) + }) + describe('clicking', () => { it('clicks an element and sends correct events', () => { var events = [] @@ -114,7 +150,7 @@ describe('dom', () => { var submit = assembly.find('#submit') ;['submit'].forEach(type => { - element.addEventListener(type, () => events.push(type)) + element.addEventListener(type, preventDefaultSubmit(() => events.push(type))) }) elementClick(submit) @@ -330,9 +366,9 @@ describe('dom', () => { eventTypes.forEach(eventType => { element.addEventListener(eventType, function (event) { console.log(element.tagName, eventType, event.target.value) - // if (eventType === 'submit') { - // event.preventDefault() - // } + if (eventType === 'submit') { + event.preventDefault() + } }) }) } diff --git a/test/selectorsSpec.js b/test/selectorsSpec.js index fdfb9ab..f560930 100644 --- a/test/selectorsSpec.js +++ b/test/selectorsSpec.js @@ -354,7 +354,7 @@ describe('selectors', () => {
`) - await expect(name).to.reject.with.error('expected one element (found: .container [1], .contact [2], .name [0])') + await expect(name).to.reject.with.error('expected one element (found: find: .container [1], find: .contact [2], find: .name [0])') }) }) }) diff --git a/yarn.lock b/yarn.lock index 9683771..aa2a1e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -854,10 +854,6 @@ backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" -balanced-match@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -956,14 +952,7 @@ boom@5.x.x: dependencies: hoek "4.x.x" -brace-expansion@^1.0.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" - dependencies: - balanced-match "^0.4.1" - concat-map "0.0.1" - -brace-expansion@^1.1.7: +brace-expansion@^1.0.0, brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" dependencies: @@ -3623,8 +3612,8 @@ lowscore@1.8.0: resolved "https://registry.yarnpkg.com/lowscore/-/lowscore-1.8.0.tgz#f27b1dfb98d84f523d8efe11766f90360949d97b" lowscore@^1.12.1: - version "1.12.1" - resolved "https://registry.yarnpkg.com/lowscore/-/lowscore-1.12.1.tgz#e6f7b41eb72e6e050eed0eafb45722c84fd0484a" + version "1.17.0" + resolved "https://registry.yarnpkg.com/lowscore/-/lowscore-1.17.0.tgz#74f34c742b011372d993ce21f0b5d97d6216cab2" lru-cache@2.2.x: version "2.2.4" @@ -3741,13 +3730,13 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3: +minimatch@^3.0.0, minimatch@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" dependencies: brace-expansion "^1.0.0" -minimatch@^3.0.4: +minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: From 4cbbef200c64bd9b8c5c070b9306c6f99065fdd8 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Wed, 14 Mar 2018 09:47:08 +0100 Subject: [PATCH 004/127] remove chai dependency --- lib/BrowserMonkeyAssertionError.js | 22 ++++ lib/actions.js | 18 +--- lib/assertions.js | 39 ++++--- lib/elementTester.js | 162 ----------------------------- lib/errorHandler.js | 6 +- lib/expectDeepEqual.js | 16 +++ lib/expectEqual.js | 15 +++ lib/expectOneElement.js | 8 -- lib/finders.js | 18 ++-- lib/options.js | 58 ----------- lib/selector.js | 25 +++-- package.json | 2 + test/assemblies/DomAssembly.js | 36 ++++--- test/assertionsSpec.js | 16 +-- test/findersSpec.js | 2 +- yarn.lock | 8 ++ 16 files changed, 147 insertions(+), 304 deletions(-) create mode 100644 lib/BrowserMonkeyAssertionError.js delete mode 100644 lib/elementTester.js create mode 100644 lib/expectDeepEqual.js create mode 100644 lib/expectEqual.js delete mode 100644 lib/expectOneElement.js delete mode 100644 lib/options.js diff --git a/lib/BrowserMonkeyAssertionError.js b/lib/BrowserMonkeyAssertionError.js new file mode 100644 index 0000000..feea96c --- /dev/null +++ b/lib/BrowserMonkeyAssertionError.js @@ -0,0 +1,22 @@ +function BrowserMonkeyAssertionError (message, options) { + this.message = message + this.showDiff = true + this.expected = options && options.expected + this.actual = options && options.actual + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, BrowserMonkeyAssertionError) + } else { + try { + throw new Error() + } catch (e) { + this.stack = e.stack + } + } +} + +BrowserMonkeyAssertionError.prototype = Object.create(Error.prototype) +BrowserMonkeyAssertionError.prototype.name = 'BrowserMonkeyAssertionError' +BrowserMonkeyAssertionError.prototype.constructor = BrowserMonkeyAssertionError + +module.exports = BrowserMonkeyAssertionError diff --git a/lib/actions.js b/lib/actions.js index d999594..e0b6e79 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -39,14 +39,10 @@ module.exports = { } }, - click: function (options) { + click: function () { var self = this - if (typeof options === 'string') { - self = this.linkOrButton(options) - } - - return self.enabled().element(options).then(function (element) { + return self.enabled().element().then(function (element) { debug('click', element) return self.handleEvent({type: 'click', element: element}).then(function () { elementClick(element) @@ -190,13 +186,3 @@ var inputsSelector = 'input[type=url], ' + 'input[type=number],' + 'textarea' - -function canTypeIntoElement (element) { - return element.is(inputsSelector) -} - -function assertCanTypeIntoElement (element) { - if (!canTypeIntoElement(element)) { - throw new Error('Cannot type into ' + element.prop('tagName')) - } -} diff --git a/lib/assertions.js b/lib/assertions.js index 19f9068..953b747 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1,9 +1,10 @@ const elementInnerText = require('./elementInnerText') const elementSelector = require('./elementSelector') var errorHandler = require('./errorHandler') -const chai = require('chai') -const inspect = require('chai/lib/chai/utils/inspect') -const expect = chai.expect +const inspect = require('object-inspect') +const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') +var expectDeepEqual = require('./expectDeepEqual') +var expectEqual = require('./expectEqual') var object = require('lowscore/object') const elementMatches = require('./elementMatches') const findOptionsToString = require('./findOptionsToString') @@ -11,13 +12,13 @@ const findOptionsToString = require('./findOptionsToString') module.exports = { shouldEqual: function (value) { return this.ensure(function (result) { - expect(result).to.equal(value) + expectEqual(result, value) }) }, shouldDeepEqual: function (value) { return this.ensure(function (result) { - expect(result).to.eql(value) + expectDeepEqual(result, value) }) }, @@ -64,10 +65,10 @@ module.exports = { let thrownError try { assertElementExpectations(elements, options) - throw (thrownError = new chai.AssertionError('expected elements not to have: ' + JSON.stringify(options))) + throw (thrownError = new BrowserMonkeyAssertionError('expected elements not to have: ' + JSON.stringify(options))) } catch (e) { // we expect assertion errors, but nothing else - if (!(e instanceof chai.AssertionError) || e === thrownError) { + if (!(e instanceof BrowserMonkeyAssertionError) || e === thrownError) { throw e } } @@ -94,7 +95,7 @@ function assertInexactValues (actual, expected) { var expectedMessage = inspect(expected) var actualMessage = inspect(actual) - throw new chai.AssertionError('expected ' + actualMessage + ' to contain ' + expectedMessage) + throw new BrowserMonkeyAssertionError('expected ' + actualMessage + ' to contain ' + expectedMessage) } } @@ -126,7 +127,9 @@ var elementExpectations = { return element.value }) - expect(actual).to.eql(expectedArray) + expectDeepEqual(actual, expectedArray, function (actual, expected) { + return 'expected ' + actual + ' to have exact values ' + expected + }) }, exactText: function (elements, expected) { @@ -136,7 +139,9 @@ var elementExpectations = { return elementInnerText(element) }) - expect(actual).to.eql(expectedArray) + expectDeepEqual(actual, expectedArray, function (actual, expected) { + return 'expected ' + actual + ' to have exact inner texts ' + expected + }) }, checked: function (elements, expected) { @@ -146,7 +151,9 @@ var elementExpectations = { return element.checked }) - expect(actual).to.eql(expectedArray) + expectDeepEqual(actual, expectedArray, function (actual, expected) { + return 'expected checked properties ' + actual + ' to equal ' + expected + }) }, css: function (elements, expected) { @@ -165,12 +172,14 @@ var elementExpectations = { var expectedMessage = inspect(expectedArray) var actualMessage = inspect(actual) - throw new chai.AssertionError('expected ' + actualMessage + ' to match css ' + expectedMessage) + throw new BrowserMonkeyAssertionError('expected ' + actualMessage + ' to match css ' + expectedMessage) } }, length: function (elements, expected) { - expect(elements.length).to.equal(expected) + expectEqual(elements.length, expected, function (actual, expected) { + return 'expected number of elements found ' + actual + ' to equal ' + expected + }) }, attributes: function (elements, expected) { @@ -186,7 +195,9 @@ var elementExpectations = { } }) - expect(actual).to.eql(expectedArray) + expectDeepEqual(actual, expectedArray, function (actual, expected) { + return 'expected ' + actual + ' to have attributes ' + expected + }) } } diff --git a/lib/elementTester.js b/lib/elementTester.js deleted file mode 100644 index c85e5e6..0000000 --- a/lib/elementTester.js +++ /dev/null @@ -1,162 +0,0 @@ -var chai = require('chai') -var expect = chai.expect -var elementsToString = require('./elementsToString') -require('array.prototype.find').shim() -const normaliseText = require('./normaliseText') - -function assertElementProperties ($, elements, expected, getProperty, exact) { - function assertion (actual, expected) { - if (exact) { - expect(actual, 'expected element to have exact text ' + JSON.stringify(expected) + ' but contained ' + JSON.stringify(actual)).to.equal(expected.toString()) - } else { - expect(actual, 'expected element to contain ' + JSON.stringify(expected) + ' but contained ' + JSON.stringify(actual)).to.contain(expected) - } - } - - if (expected instanceof Array) { - var actualTexts = elements.toArray().map(function (item) { - return getProperty($(item)) - }) - var originalActualTexts = actualTexts.slice(0) - - expect(actualTexts.length, 'expected ' + JSON.stringify(actualTexts) + ' to respectively contain ' + JSON.stringify(expected)).to.eql(expected.length) - - var comparer = exact === true - ? function (a, b) { return a === b } - : function (a, b) { return a.indexOf(b) !== -1 } - - var found = [] - expected.forEach(function (value) { - var foundValue = actualTexts.find(function (actual) { - return comparer(actual, value) - }) - if (foundValue !== undefined) { - actualTexts.splice(actualTexts.indexOf(foundValue), 1) - found.push(value) - } - }) - - if (found.length !== expected.length) { - expect(originalActualTexts).to.eql(expected) - } - - try { - expect(found).to.eql(expected) - } catch (e) { - if (found.slice().sort().toString() === expected.slice().sort().toString()) { - e.message += '\nThe text was found but in a different order than specified - maybe you need some sorting?' - } - throw e - } - } else { - var elementText = getProperty($(elements)) - assertion(elementText, expected) - } -} - -function getValue (e, property) { - var val = e.val() - // Fails with missing value attribute in VDOM without 'string' test (returns SoftSetHook{value: ''}) - return (typeof val === 'string' && val) || '' -} - -module.exports = { - css: function ($el, message, css) { - if (!$el.is(css)) { - throw new Error(message || ('expected elements ' + elementsToString($el) + ' to have css ' + css)) - } - }, - elements: function ($el, message, predicate) { - if (!predicate($el.toArray())) { - throw new Error(message || 'expected elements to pass predicate') - } - }, - text: function ($el, message, text) { - assertElementProperties(this.get('$'), $el, text, function (e) { return normaliseText(e) }, text === '') - }, - length: function ($el, message, length) { - if ($el.length !== length) { - throw new Error(message || ('expected ' + elementsToString($el) + ' to have ' + length + ' elements')) - } - }, - html: function ($el, message, html) { - assertElementProperties(this.get('$'), $el, html, function (e) { return e.html() }) - }, - checked: function ($el, message, checked) { - var $ = this.get('$') - var elements = $el.toArray() - - if (checked instanceof Array) { - var elementsChecked = elements.map(function (element) { - return !!$(element).prop('checked') - }) - expect(elementsChecked, 'expected ' + elementsToString($el) + ' to have checked states ' + JSON.stringify(checked)).to.eql(checked) - } else { - var elementsNotMatching = elements.filter(function (element) { - return $(element).prop('checked') !== checked - }) - expect(elementsNotMatching.length, 'expected ' + elementsToString($el) + ' to be ' + (checked ? 'checked' : 'unchecked')).to.equal(0) - } - }, - exactText: function ($el, message, exactText) { - assertElementProperties(this.get('$'), $el, exactText, function (e) { return normaliseText(e) }, true) - }, - value: function ($el, message, value) { - assertElementProperties(this.get('$'), $el, value, getValue, value === '') - }, - exactValue: function ($el, message, exactValue) { - assertElementProperties(this.get('$'), $el, exactValue, getValue, true) - }, - attributes: function ($el, message, attributes) { - var $ = this.get('$') - var elements = $el.toArray() - - if (attributes instanceof Array) { - expect(elements.length).to.equal(attributes.length, 'expected the matched elements to be the same length as the expected attributes') - elements.forEach(function (el, index) { - var attributesForElement = attributes[index] - Object.keys(attributesForElement).forEach(function (attributeKey) { - expect($(el).attr(attributeKey)).to.equal(attributesForElement[attributeKey]) - }) - }) - } else { - elements.forEach(function (el) { - Object.keys(attributes).forEach(function (attributeKey) { - expect($(el).attr(attributeKey)).to.equal(attributes[attributeKey]) - }) - }) - } - }, - - label: function ($el, message, label) { - var $ = this.get('$') - var links = $el.toArray().filter(function (el) { - if ($(el).is('a')) { - var anchor = $(el) - var href = anchor.attr('href') - return typeof href !== typeof undefined && - href !== false && - ( - anchor.text() === label || - anchor.attr('id') === label || - anchor.attr('title') === label || - anchor.find('img').toArray().filter(function (img) { - return $(img).attr('alt') === label - }).length > 0 - ) - } - if ($(el).is('button, input[type=submit], input[type=button], input[type=reset]')) { - var button = $(el) - return button.attr('id') === label || - button.text() === label || - (typeof button.attr('value') === 'string' && button.attr('value').indexOf(label) > -1) || - (typeof button.attr('title') === 'string' && button.attr('title').indexOf(label) > -1) || - button.find('img').toArray().filter(function (img) { - return typeof $(img).attr('alt') === 'string' && $(img).attr('alt').indexOf(label) > -1 - }).length > 0 - } - }) - - expect(links.length).to.equal(1, message) - } -} diff --git a/lib/errorHandler.js b/lib/errorHandler.js index 917ed09..5ab0fb3 100644 --- a/lib/errorHandler.js +++ b/lib/errorHandler.js @@ -1,6 +1,10 @@ +var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') + module.exports = function (error) { return function (e) { - e.stack = error.stack + if (e instanceof BrowserMonkeyAssertionError) { + e.stack = error.stack + } throw e } } diff --git a/lib/expectDeepEqual.js b/lib/expectDeepEqual.js new file mode 100644 index 0000000..6714502 --- /dev/null +++ b/lib/expectDeepEqual.js @@ -0,0 +1,16 @@ +var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') +var deepEqual = require('deep-equal') +var inspect = require('object-inspect') + +module.exports = function expectDeepEqual (actual, expected, createMessage) { + if (!deepEqual(actual, expected)) { + var message = createMessage + ? createMessage(inspect(actual), inspect(expected)) + : 'expected ' + actual + ' to deeply equal ' + expected + + throw new BrowserMonkeyAssertionError(message, { + actual: actual, + expected: expected + }) + } +} diff --git a/lib/expectEqual.js b/lib/expectEqual.js new file mode 100644 index 0000000..451ecfd --- /dev/null +++ b/lib/expectEqual.js @@ -0,0 +1,15 @@ +var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') +var inspect = require('object-inspect') + +module.exports = function expectEqual (actual, expected, createMessage) { + if (actual !== expected) { + var message = createMessage + ? createMessage(inspect(actual), inspect(expected)) + : 'expected ' + actual + ' to equal ' + expected + + throw new BrowserMonkeyAssertionError(message, { + actual: actual, + expected: expected + }) + } +} diff --git a/lib/expectOneElement.js b/lib/expectOneElement.js deleted file mode 100644 index 0ca1f53..0000000 --- a/lib/expectOneElement.js +++ /dev/null @@ -1,8 +0,0 @@ -var chai = require('chai') -var expect = chai.expect -var elementsToString = require('./elementsToString') - -module.exports = function expectOneElement (scope, elements) { - var msg = 'expected to find exactly one element: ' + scope.printFinders(scope._finders) + ', but found :' + elementsToString(elements) - expect(elements.length, msg).to.equal(1) -} diff --git a/lib/finders.js b/lib/finders.js index 0fc7cc3..a504c4f 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -1,9 +1,9 @@ var flatten = require('lowscore/flatten') -require('chai').config.truncateThreshold = 0 const elementVisible = require('./elementVisible') const elementMatches = require('./elementMatches') const elementInnerText = require('./elementInnerText') const findOptionsToString = require('./findOptionsToString') +var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') module.exports = { find: function (selector, options) { @@ -36,7 +36,11 @@ module.exports = { iframe: function () { return this.map(function (element) { if (isIframe(element)) { - return element.contentDocument.body + if (element.contentDocument.body) { + return element.contentDocument.body + } else { + throw new BrowserMonkeyAssertionError('iframe not loaded') + } } else { return element } @@ -69,7 +73,7 @@ module.exports = { if (isArrayOfHTMLElements(elements)) { return elements } else { - throw new Error('expected DOM elements') + throw new BrowserMonkeyAssertionError('expected DOM elements') } }, 'elements').some() } @@ -84,16 +88,10 @@ function isArrayOfHTMLElements (elements) { function expectElements (elements) { if (!isArrayOfHTMLElements(elements)) { - throwElementsError(elements, 'expected HTML elements') + throw new BrowserMonkeyAssertionError('expected HTML elements') } } -function throwElementsError (elements, message) { - var error = new Error(message) - error.elements = elements - throw error -} - var findOptionFilters = { text: function (element, text) { if (text === '') { diff --git a/lib/options.js b/lib/options.js deleted file mode 100644 index 4ad2251..0000000 --- a/lib/options.js +++ /dev/null @@ -1,58 +0,0 @@ -function Options (options) { - this.options = options - this.isOptionsObject = typeof options === 'object' - this.validOptions = [] -} - -Options.get = function (options, propertyName) { - if (typeof options === 'object') { - var value = options[propertyName] - return value - } -} - -Options.remove = function (options, propertyNames) { - var newOptions = {} - - if (typeof options === 'object') { - propertyNames.forEach(function (propertyName) { - newOptions[propertyName] = options[propertyName] - delete options[propertyName] - }) - } - - return newOptions -} - -Options.default = function (options, defaults) { - var newOptions = typeof options === 'object' ? options : {} - - Object.keys(defaults).forEach(function (key) { - if (!newOptions.hasOwnProperty(key)) { - newOptions[key] = defaults[key] - } - }) - - return newOptions -} - -Options.prototype.option = function (name) { - this.validOptions.push(name) - if (this.isOptionsObject) { - var value = this.options.hasOwnProperty(name) ? this.options[name] : undefined - delete this.options[name] - return value - } -} - -Options.prototype.validate = function () { - if (this.isOptionsObject) { - var keys = Object.keys(this.options) - - if (keys.length > 0) { - throw new Error('properties ' + keys.join(', ') + ' not recognised, try ' + this.validOptions.join(', ')) - } - } -} - -module.exports = Options diff --git a/lib/selector.js b/lib/selector.js index 6a674a5..9ee20d6 100644 --- a/lib/selector.js +++ b/lib/selector.js @@ -1,5 +1,6 @@ var extend = require('lowscore/extend') var retry = require('trytryagain') +var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') function Selector () { this._conditions = [] @@ -81,13 +82,17 @@ Selector.prototype.then = function () { })).then(function (accumulator) { return accumulator.value }, function (error) { - var foundMessage = accumulator.transforms.map(function (transform) { - var v = transform.value instanceof Array - ? '[' + transform.value.length + ']' - : '(' + typeof transform.value + ')' - return transform.name + ' ' + v - }).join(', ') - throw new Error(error.message + ' (found: ' + foundMessage + ')') + if (error instanceof BrowserMonkeyAssertionError) { + var foundMessage = accumulator.transforms.map(function (transform) { + var v = transform.value instanceof Array + ? '[' + transform.value.length + ']' + : '(' + typeof transform.value + ')' + return transform.name + ' ' + v + }).join(', ') + error.message = error.message + ' (found: ' + foundMessage + ')' + } + + throw error }) return promise.then.apply(promise, arguments) } @@ -122,7 +127,7 @@ Selector.prototype.zero = function () { return this.mapAll(results => { if (results instanceof Array) { if (results.length !== 0) { - throw new Error('expected no elements') + throw new BrowserMonkeyAssertionError('expected no elements') } else { return results } @@ -138,7 +143,7 @@ Selector.prototype.one = function () { if (results.length === 1) { return results[0] } else { - throw new Error('expected one element') + throw new BrowserMonkeyAssertionError('expected one element') } } else { return results @@ -151,7 +156,7 @@ Selector.prototype.some = function () { if (results instanceof Array && results.length !== 0) { return results } else { - throw new Error('expected some elements') + throw new BrowserMonkeyAssertionError('expected some elements') } }, 'some') } diff --git a/package.json b/package.json index 5486d61..d4eb953 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,14 @@ "browserify-optional": "1.0.0", "chai": "3.5.0", "debug": "^2.6.3", + "deep-equal": "1.0.1", "detect-browser": "^1.6.2", "finished-promise": "0.0.2", "global": "^4.3.1", "hobostyle": "1.0.0", "jquery": "^3.2.1", "lowscore": "^1.12.1", + "object-inspect": "1.5.0", "trytryagain": "1.2.0" }, "standard": { diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.js index 30aa649..e16a115 100644 --- a/test/assemblies/DomAssembly.js +++ b/test/assemblies/DomAssembly.js @@ -26,27 +26,31 @@ module.exports = class DomAssembly { return browserMonkey.scope(this._div).options({ retry: (retry) => { - return new Promise((resolve, reject) => { - const success = result => { - resolve(result) - const index = this.retries.indexOf(retrier) - if (index !== -1) { - this.retries.splice(index, 1) + if (this._normalRetry) { + return trytryagain(retry) + } else { + return new Promise((resolve, reject) => { + const success = result => { + resolve(result) + const index = this.retries.indexOf(retrier) + if (index !== -1) { + this.retries.splice(index, 1) + } } - } - const retrier = () => { - try { - success(retry()) - } catch (e) { - reject(e) + const retrier = () => { + try { + success(retry()) + } catch (e) { + reject(e) + } } - } - this.retries.push(retrier) + this.retries.push(retrier) - this.tick() - }) + this.tick() + }) + } } }) } diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index 870f36c..a44a49f 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -199,7 +199,7 @@ describe('assertions', function () { return Promise.all([ good, - demand(bad).reject.with.error(partMatch("expected [ 'some text' ] to deeply equal [ 'some t' ]")) + demand(bad).reject.with.error(partMatch("expected [ 'some text' ] to have exact values [ 'some t' ]")) ]) }) @@ -244,7 +244,7 @@ describe('assertions', function () { }) return Promise.all([ good, - demand(bad).reject.with.error(partMatch("expected [ { class: null } ] to deeply equal [ { class: 'other' } ]")) + demand(bad).reject.with.error(partMatch("expected [ { class: null } ] to have attributes [ { class: 'other' } ]")) ]) }) @@ -266,7 +266,7 @@ describe('assertions', function () { }) return Promise.all([ good, - demand(bad).reject.with.error(partMatch("expected [ { src: '/a' }, { src: '/b' }, { src: '/c' } ] to deeply equal [ { src: '/c' }, { src: '/a' }, { src: '/b' } ]")) + demand(bad).reject.with.error(partMatch("expected [ { src: '/a' }, { src: '/b' }, { src: '/c' } ] to have attributes [ { src: '/c' }, { src: '/a' }, { src: '/b' } ]")) ]) }) @@ -284,7 +284,7 @@ describe('assertions', function () { assembly.eventuallyInsertHtml('') - return demand(promise).reject.with.error(partMatch("expected [ 'Optional', 'Mr', 'Mrs' ] to deeply equal [ '', 'Mr', 'Mrs' ]")) + return demand(promise).reject.with.error(partMatch("expected [ 'Optional', 'Mr', 'Mrs' ] to have exact inner texts [ '', 'Mr', 'Mrs' ]")) }) }) @@ -312,7 +312,7 @@ describe('assertions', function () { }) return Promise.all([ - demand(good).reject.with.error(partMatch('expected [ true, false ] to deeply equal [ true ]')) + demand(good).reject.with.error(partMatch('expected checked properties [ true, false ] to equal [ true ]')) ]) }) @@ -328,7 +328,7 @@ describe('assertions', function () { return Promise.all([ good, - demand(bad).reject.with.error(partMatch('expected [ true, false ] to deeply equal [ false, true ]')) + demand(bad).reject.with.error(partMatch('expected checked properties [ true, false ] to equal [ false, true ]')) ]) }) @@ -340,7 +340,7 @@ describe('assertions', function () { return Promise.all([ good, - demand(bad).reject.with.error(partMatch('expected [ false ] to deeply equal [ true ]')) + demand(bad).reject.with.error(partMatch('expected checked properties [ false ] to equal [ true ]')) ]) }) }) @@ -396,7 +396,7 @@ describe('assertions', function () { return Promise.all([ good, - demand(bad1).reject.with.error(partMatch('expected 2 to equal 1')) + demand(bad1).reject.with.error(partMatch('expected number of elements found 2 to equal 1')) ]) }) diff --git a/test/findersSpec.js b/test/findersSpec.js index 667cd92..3c5c91b 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -153,7 +153,7 @@ describe('find', function () { assembly.eventuallyInsertHtml('
bad
') - return demand(promise).reject.with.error(/expected one element/) + return demand(promise).reject.with.error(/expected some elements/) }) it('errors with a usable css selector if it cant find an element containing another', function () { diff --git a/yarn.lock b/yarn.lock index aa2a1e1..12ebdfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1613,6 +1613,10 @@ deep-eql@^0.1.3: dependencies: type-detect "0.1.1" +deep-equal@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + deep-extend@~0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" @@ -3948,6 +3952,10 @@ object-component@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" +object-inspect@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.5.0.tgz#9d876c11e40f485c79215670281b767488f9bfe3" + object-keys@^1.0.10, object-keys@^1.0.8: version "1.0.11" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" From ead934eb22de1d02189f3d9b82e86e7e1d1228a5 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Thu, 15 Mar 2018 13:54:25 +0100 Subject: [PATCH 005/127] better error stack trickery, buttons --- create.js | 4 +- lib/actions.js | 23 +- lib/assertions.js | 5 - lib/button.js | 13 + lib/elementFindElements.js | 57 +++ lib/elementsFindElements.js | 10 + lib/elementsToString.js | 5 - lib/errorHandler.js | 10 - lib/finders.js | 78 +--- lib/promise.js | 16 - lib/selector.js | 2 + lib/sendkeys.js | 37 -- package.json | 2 +- test/actionsSpec.js | 768 ++++++++++++++++----------------- test/assemblies/DomAssembly.js | 25 +- test/assertionsSpec.js | 94 ++-- test/buttonSpec.js | 63 +++ test/componentSpec.js | 53 +-- test/domTest.js | 60 --- test/eventsSpec.js | 61 +-- test/findersSpec.js | 2 +- test/fuzzyFinderSpec.js | 83 ++-- test/immediateSpec.js | 65 --- test/scopeSpec.js | 38 +- yarn.lock | 14 +- 25 files changed, 710 insertions(+), 878 deletions(-) create mode 100644 lib/button.js create mode 100644 lib/elementFindElements.js create mode 100644 lib/elementsFindElements.js delete mode 100644 lib/elementsToString.js delete mode 100644 lib/errorHandler.js delete mode 100644 lib/promise.js delete mode 100644 lib/sendkeys.js create mode 100644 test/buttonSpec.js delete mode 100644 test/domTest.js delete mode 100644 test/immediateSpec.js diff --git a/create.js b/create.js index 7fdf139..3fbea97 100644 --- a/create.js +++ b/create.js @@ -1,13 +1,13 @@ var Selector = require('./lib/selector') var finders = require('./lib/finders') var actions = require('./lib/actions') +var button = require('./lib/button') var assertions = require('./lib/assertions') -var promise = require('./lib/promise') module.exports = function (rootSelector) { return new Selector(rootSelector) - .component(promise) .component(finders) .component(actions) + .component(button) .component(assertions) } diff --git a/lib/actions.js b/lib/actions.js index e0b6e79..6ad6354 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -1,6 +1,4 @@ var debug = require('debug')('browser-monkey') -var sendkeys = require('./sendkeys') -var errorHandler = require('./errorHandler') var elementSelect = require('./elementSelect') var elementClick = require('./elementClick') var elementSubmit = require('./elementSubmit') @@ -47,7 +45,7 @@ module.exports = { return self.handleEvent({type: 'click', element: element}).then(function () { elementClick(element) }) - }).catch(errorHandler(new Error())) + }) }, select: function (options) { @@ -71,7 +69,7 @@ module.exports = { }).then(function () { elementSelect(selectElement, optionElement) }) - }).catch(errorHandler(new Error())) + }) }, typeIn: function (text, options) { @@ -85,7 +83,7 @@ module.exports = { return self.handleEvent({type: 'typing', text: text, element: element}).then(function () { elementEnterText(element, text) }) - }).catch(errorHandler(new Error())) + }) }, submit: function () { @@ -96,18 +94,19 @@ module.exports = { return self.handleEvent({type: 'submit', element: element}).then(function () { elementSubmit(element) }) - }).catch(errorHandler(new Error())) + }) }, - typeInHtml: function (html, options) { + typeInHtml: function (html) { var self = this - return this.element(options).then(function (element) { - self.focus(element, options) + return this.element().then(function (element) { debug('typeInHtml', element, html) - self.handleEvent({type: 'typing html', html: html, element: element}) - return sendkeys.html(element, html) - }).catch(errorHandler(new Error())) + return self.handleEvent({type: 'typing html', html: html, element: element}).then(function () { + element.focus() + element.innerHTML = html + }) + }) }, fill: function (field) { diff --git a/lib/assertions.js b/lib/assertions.js index 953b747..a9af7a9 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1,6 +1,5 @@ const elementInnerText = require('./elementInnerText') const elementSelector = require('./elementSelector') -var errorHandler = require('./errorHandler') const inspect = require('object-inspect') const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') var expectDeepEqual = require('./expectDeepEqual') @@ -28,14 +27,12 @@ module.exports = { shouldExist: function (options) { return this.some() - .catch(errorHandler(new Error())) }, shouldHave: function (options) { return this.elements().ensure(function (elements) { assertElementExpectations(elements, options) }, findOptionsToString(options)) - .catch(errorHandler(new Error())) }, shouldFind: function (selector, findOptions) { @@ -45,7 +42,6 @@ module.exports = { shouldNotExist: function () { return this.zero() - .catch(errorHandler(new Error())) }, has: function (options) { @@ -73,7 +69,6 @@ module.exports = { } } }, findOptionsToString(options)) - .catch(errorHandler(new Error())) } } diff --git a/lib/button.js b/lib/button.js new file mode 100644 index 0000000..2516cbe --- /dev/null +++ b/lib/button.js @@ -0,0 +1,13 @@ +const elementsFindElements = require('./elementsFindElements') + +module.exports = { + button: function (name) { + var self = this + + return this.mapAll(function (elements) { + return elementsFindElements(elements, 'button', {exactText: name}, self._options) + .concat(elementsFindElements(elements, 'input[type=button]', {exactValue: name}, self._options)) + .concat(elementsFindElements(elements, 'a', {exactText: name}, self._options)) + }, 'button') + } +} diff --git a/lib/elementFindElements.js b/lib/elementFindElements.js new file mode 100644 index 0000000..e3d11d7 --- /dev/null +++ b/lib/elementFindElements.js @@ -0,0 +1,57 @@ +const elementInnerText = require('./elementInnerText') +const elementVisible = require('./elementVisible') + +module.exports = function findElements (element, selector, findOptions, selectorOptions) { + var children = Array.prototype.slice.call(element.querySelectorAll(selector)) + + children = selectorOptions.visibleOnly + ? children.filter(elementVisible) + : children + + children = findOptions + ? findByOptions(children, findOptions) + : children + + return children +} + +function findByOptions (elements, options) { + var filters = Object.keys(options).map(function (key) { + var filter = findOptionFilters[key] + if (!filter) { + throw new Error('no such filter ' + key + ', try one of: ' + Object.keys(findOptionFilters).join(', ')) + } + return { + filter: filter, + expected: options[key] + } + }) + + return elements.filter(function (element, index) { + return filters.every(function (filter) { + return filter.filter(element, filter.expected, index) + }) + }) +} + +var findOptionFilters = { + text: function (element, text) { + if (text === '') { + return elementInnerText(element) === text + } else { + return elementInnerText(element).indexOf(text) !== -1 + } + }, + + exactText: function (element, text) { + return elementInnerText(element) === text + }, + + exactValue: function (element, text) { + return element.value === text + }, + + index: function (element, expectedIndex, actualIndex) { + return expectedIndex === actualIndex + } +} diff --git a/lib/elementsFindElements.js b/lib/elementsFindElements.js new file mode 100644 index 0000000..503ec50 --- /dev/null +++ b/lib/elementsFindElements.js @@ -0,0 +1,10 @@ +var elementFindElements = require('./elementFindElements') +var flatten = require('lowscore/flatten') + +module.exports = function (elements, selector, findOptions, selectorOptions) { + return flatten( + elements.map(function (element) { + return elementFindElements(element, selector, findOptions, selectorOptions) + }) + ) +} diff --git a/lib/elementsToString.js b/lib/elementsToString.js deleted file mode 100644 index d07bd46..0000000 --- a/lib/elementsToString.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function elementsToString (els) { - return els.toArray().map(function (el) { - if (el && el.outerHTML) { return el.outerHTML.replace(el.innerHTML, '') } - }).join(', ') -} diff --git a/lib/errorHandler.js b/lib/errorHandler.js deleted file mode 100644 index 5ab0fb3..0000000 --- a/lib/errorHandler.js +++ /dev/null @@ -1,10 +0,0 @@ -var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') - -module.exports = function (error) { - return function (e) { - if (e instanceof BrowserMonkeyAssertionError) { - e.stack = error.stack - } - throw e - } -} diff --git a/lib/finders.js b/lib/finders.js index a504c4f..ba30774 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -1,22 +1,17 @@ -var flatten = require('lowscore/flatten') -const elementVisible = require('./elementVisible') const elementMatches = require('./elementMatches') -const elementInnerText = require('./elementInnerText') +const elementFindElements = require('./elementFindElements') +const elementsFindElements = require('./elementsFindElements') const findOptionsToString = require('./findOptionsToString') var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') module.exports = { - find: function (selector, options) { + find: function (selector, findOptions) { var self = this return this.mapAll(function (elements) { expectElements(elements) - return flatten( - elements.map(function (element) { - return findElements(element, selector, options, self._options) - }) - ) - }, 'find: ' + selector + (options ? ' ' + findOptionsToString(options) : '')) + return elementsFindElements(elements, selector, findOptions, self._options) + }, 'find: ' + selector + (findOptions ? ' ' + findOptionsToString(findOptions) : '')) }, is: function (selector) { @@ -29,7 +24,7 @@ module.exports = { var self = this return this.filter(function (element) { - return findElements(element, selector, options, self._options).length > 0 + return elementFindElements(element, selector, options, self._options).length > 0 }, 'containing: ' + selector + (options ? ' ' + findOptionsToString(options) : '')) }, @@ -47,13 +42,15 @@ module.exports = { }, 'iframe') }, - scope: function (scope) { - var selector = this.value([scope]) + scope: function (element) { + var selector = this.value([element]) - if (isIframe(scope)) { + if (isIframe(element)) { return selector.iframe() - } else { + } else if (isHTMLElement(element)) { return selector + } else { + throw new Error('scope() expects HTML element') } }, @@ -92,43 +89,6 @@ function expectElements (elements) { } } -var findOptionFilters = { - text: function (element, text) { - if (text === '') { - return elementInnerText(element) === text - } else { - return elementInnerText(element).indexOf(text) !== -1 - } - }, - - exactText: function (element, text) { - return elementInnerText(element) === text - }, - - index: function (element, expectedIndex, actualIndex) { - return expectedIndex === actualIndex - } -} - -function findByOptions (elements, options) { - var filters = Object.keys(options).map(function (key) { - var filter = findOptionFilters[key] - if (!filter) { - throw new Error('no such filter ' + key + ', try one of: ' + Object.keys(findOptionFilters).join(', ')) - } - return { - filter: filter, - expected: options[key] - } - }) - - return elements.filter(function (element, index) { - return filters.every(function (filter) { - return filter.filter(element, filter.expected, index) - }) - }) -} - function isHTMLElement (element, subclass) { if (element.ownerDocument && element.ownerDocument.defaultView) { // an element inside an iframe @@ -138,20 +98,6 @@ function isHTMLElement (element, subclass) { } } -function findElements (element, selector, findOptions, selectorOptions) { - var children = Array.prototype.slice.call(element.querySelectorAll(selector)) - - children = selectorOptions.visibleOnly - ? children.filter(elementVisible) - : children - - children = findOptions - ? findByOptions(children, findOptions) - : children - - return children -} - function isIframe (element) { return isHTMLElement(element, 'HTMLIFrameElement') } diff --git a/lib/promise.js b/lib/promise.js deleted file mode 100644 index 04ef42b..0000000 --- a/lib/promise.js +++ /dev/null @@ -1,16 +0,0 @@ -var FinishedPromise = require('finished-promise') -var trytryagain = require('trytryagain') - -function immediately (retryOptions, fn) { - return FinishedPromise.resolve().then(fn) -} - -module.exports = { - promise: function () { - return this.get('immediate') ? FinishedPromise : Promise - }, - - retry: function (options, fn) { - return this.get('immediate') ? immediately(options, fn) : trytryagain(options, fn) - } -} diff --git a/lib/selector.js b/lib/selector.js index 9ee20d6..74db1f7 100644 --- a/lib/selector.js +++ b/lib/selector.js @@ -63,6 +63,7 @@ Selector.prototype.then = function () { var self = this var retry = retryFromOptions(this._options) var accumulator = {} + var originalStack = new Error().stack function reducer (accumulator, mapper, index) { if (index === 0) { @@ -90,6 +91,7 @@ Selector.prototype.then = function () { return transform.name + ' ' + v }).join(', ') error.message = error.message + ' (found: ' + foundMessage + ')' + error.stack = originalStack } throw error diff --git a/lib/sendkeys.js b/lib/sendkeys.js deleted file mode 100644 index fa41405..0000000 --- a/lib/sendkeys.js +++ /dev/null @@ -1,37 +0,0 @@ -function dispatchEvent (el, type, char) { - el.trigger(type, {charCode: char}) -} - -function sendkey (el, char) { - dispatchEvent(el, 'keydown', char) - dispatchEvent(el, 'keypress', char) - dispatchEvent(el, 'input') - dispatchEvent(el, 'keyup', char) -} - -function sendkeys (el, text) { - var originalValue = el.val() - - if (text.length === 0) { - el.val('') - sendkey(el, '') - } else { - for (var n = 0; n < text.length; ++n) { - var char = text[n] - var value = text.substring(0, n + 1) - el.val(value) - sendkey(el, char) - } - } - - if (originalValue !== text) { - dispatchEvent(el, 'change') - } -}; - -sendkeys.html = function (el, html) { - el.innerHTML = html - dispatchEvent(el, 'input') -} - -module.exports = sendkeys diff --git a/package.json b/package.json index d4eb953..24dcb9d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "debug": "^2.6.3", "deep-equal": "1.0.1", "detect-browser": "^1.6.2", - "finished-promise": "0.0.2", + "error-stack-parser": "2.0.1", "global": "^4.3.1", "hobostyle": "1.0.0", "jquery": "^3.2.1", diff --git a/test/actionsSpec.js b/test/actionsSpec.js index cc9c008..a986e73 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -1,7 +1,6 @@ var describeAssemblies = require('./describeAssemblies') const DomAssembly = require('./assemblies/DomAssembly') var demand = require('must') -var domTest = require('./domTest') var retry = require('trytryagain') describe('actions', function () { @@ -14,529 +13,492 @@ describe('actions', function () { browser = assembly.browserMonkey() }) - describe('clicking', function () { - domTest('stack trace', function (browser, dom) { - return browser.find('div') - .click() - .assertStackTrace(__filename) - }, { - mochaOnly: true - }) + describe('clicking', function () { + it('should eventually click an element', function () { + var promise = browser.find('.element').click() + var clicked = false - it('should eventually click an element', function () { - var promise = browser.find('.element').click() - var clicked = false + assembly.eventuallyInsertHtml( + assembly.jQuery('
').on('click', function () { + clicked = true + }) + ) - assembly.eventuallyInsertHtml( - assembly.jQuery('
').on('click', function () { - clicked = true + return promise.then(function () { + demand(clicked).to.equal(true) }) - ) - - return promise.then(function () { - demand(clicked).to.equal(true) }) - }) - it('sends mousedown mouseup and click events', function () { - var events = [] + it('sends mousedown mouseup and click events', function () { + var events = [] - assembly.insertHtml( - assembly.jQuery('
').on('mousedown', function () { - events.push('mousedown') - }).on('mouseup', function () { - events.push('mouseup') - }).on('click', function () { - events.push('click') - }) - ) + assembly.insertHtml( + assembly.jQuery('
').on('mousedown', function () { + events.push('mousedown') + }).on('mouseup', function () { + events.push('mouseup') + }).on('click', function () { + events.push('click') + }) + ) - return browser.find('.element').click().then(function () { - demand(events).to.eql(['mousedown', 'mouseup', 'click']) + return browser.find('.element').click().then(function () { + demand(events).to.eql(['mousedown', 'mouseup', 'click']) + }) }) - }) - it('mousedown mouseup and click events bubble up to parent', function () { - var events = [] + it('mousedown mouseup and click events bubble up to parent', function () { + var events = [] - assembly.insertHtml( - assembly.jQuery('
inner
').on('mousedown', function () { - events.push('mousedown') - }).on('mouseup', function () { - events.push('mouseup') - }).on('click', function () { - events.push('click') - }) - ) + assembly.insertHtml( + assembly.jQuery('
inner
').on('mousedown', function () { + events.push('mousedown') + }).on('mouseup', function () { + events.push('mouseup') + }).on('click', function () { + events.push('click') + }) + ) - return browser.find('.inner-element').click().then(function () { - demand(events).to.eql(['mousedown', 'mouseup', 'click']) - }) - }, {vdom: false}) + return browser.find('.inner-element').click().then(function () { + demand(events).to.eql(['mousedown', 'mouseup', 'click']) + }) + }, {vdom: false}) - it('waits until checkbox is enabled before clicking', function () { - var promise = browser.find('input[type=checkbox]').click() - var clicked - var buttonState = 'disabled' + it('waits until checkbox is enabled before clicking', function () { + var promise = browser.find('input[type=checkbox]').click() + var clicked + var buttonState = 'disabled' - var button = assembly.jQuery(assembly.insertHtml('')) - button.on('click', function () { - clicked = buttonState - }) + var button = assembly.jQuery(assembly.insertHtml('')) + button.on('click', function () { + clicked = buttonState + }) - assembly.eventually(function () { - button.prop('disabled', false) - buttonState = 'enabled' - }) + assembly.eventually(function () { + button.prop('disabled', false) + buttonState = 'enabled' + }) - return promise.then(function () { - demand(clicked).to.equal('enabled') + return promise.then(function () { + demand(clicked).to.equal('enabled') + }) }) - }) - it('waits until button is enabled before clicking', function () { - var promise = browser.find('button', {text: 'a button'}).click() - var clicked - var buttonState = 'disabled' + it('waits until button is enabled before clicking', function () { + var promise = browser.find('button', {text: 'a button'}).click() + var clicked + var buttonState = 'disabled' - var button = assembly.jQuery(assembly.insertHtml('')) - button.on('click', function () { - clicked = buttonState - }) + var button = assembly.jQuery(assembly.insertHtml('')) + button.on('click', function () { + clicked = buttonState + }) - assembly.eventually(function () { - button.prop('disabled', false) - buttonState = 'enabled' - }) + assembly.eventually(function () { + button.prop('disabled', false) + buttonState = 'enabled' + }) - return promise.then(function () { - demand(clicked).to.equal('enabled') + return promise.then(function () { + demand(clicked).to.equal('enabled') + }) }) }) - }) - describe('select', function () { - domTest('stack trace', function (browser, dom) { - return browser.find('div') - .select({text: 'Text'}) - .assertStackTrace(__filename) - }, { - mochaOnly: true - }) + describe('select', function () { + describe('text', function () { + it('eventually selects an option element using the text', function () { + var promise = browser.find('.element').select({text: 'Second'}) + var selectedItem - describe('text', function () { - it('eventually selects an option element using the text', function () { - var promise = browser.find('.element').select({text: 'Second'}) - var selectedItem + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function () { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function () { - selectedItem = assembly.jQuery(this).find(':selected').text() + return promise.then(function () { + demand(selectedItem).to.equal('Second') }) - ) - - return promise.then(function () { - demand(selectedItem).to.equal('Second') }) - }) - it('eventually selects an option element using the text, when text is passed as a string', function () { - var promise = browser.find('.element').select('Second') - var selectedItem + it('eventually selects an option element using the text, when text is passed as a string', function () { + var promise = browser.find('.element').select('Second') + var selectedItem - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function () { - selectedItem = assembly.jQuery(this).find(':selected').text() - }) - ) + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function () { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) - return promise.then(function () { - demand(selectedItem).to.equal('Second') + return promise.then(function () { + demand(selectedItem).to.equal('Second') + }) }) - }) - it('should eventually select an option element using a partial match', function () { - var promise = browser.find('.element').select({text: 'Seco'}) - var selectedItem + it('should eventually select an option element using a partial match', function () { + var promise = browser.find('.element').select({text: 'Seco'}) + var selectedItem - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').text() - }) - ) + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) - return promise.then(function () { - demand(selectedItem).to.equal('Second') + return promise.then(function () { + demand(selectedItem).to.equal('Second') + }) }) - }) - it('selects the option by index names are ambiguous', function () { - var selectedItem + it('selects the option by index names are ambiguous', function () { + var selectedItem - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - var select = e.target - selectedItem = select.value - }) - ) + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + var select = e.target + selectedItem = select.value + }) + ) - return browser.find('select').select({index: 1}).then(function () { - demand(selectedItem).to.equal('2') + return browser.find('select').select({index: 1}).then(function () { + demand(selectedItem).to.equal('2') + }) }) - }) - it('selects an option that eventually appears', function () { - var promise = browser.find('.element').select({text: 'Second'}) - var selectedItem + it('selects an option that eventually appears', function () { + var promise = browser.find('.element').select({text: 'Second'}) + var selectedItem - var select = assembly.insertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').text() + var select = assembly.insertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) + + assembly.eventuallyInsertHtml('', select) + + return promise.then(function () { + demand(selectedItem).to.equal('Second') }) - ) + }) - assembly.eventuallyInsertHtml('', select) + it('errors when the specified option does not exist', function () { + var promise = browser.find('.element').select({text: 'Does not exist'}) - return promise.then(function () { - demand(selectedItem).to.equal('Second') + assembly.eventuallyInsertHtml('') + + return assembly.assertRejection(promise, 'expected some elements') }) - }) - it('errors when the specified option does not exist', function () { - var promise = browser.find('.element').select({text: 'Does not exist'}) + it('errors when the input is not a select', function () { + var promise = browser.find('.element').select({text: 'Whatevs'}) + assembly.eventuallyInsertHtml('
') + return assembly.assertRejection(promise, 'expected some elements') + }) - assembly.eventuallyInsertHtml('') + it('selects an option using text that is falsy', function () { + var promise = browser.find('.element').select({text: 0}) + var selectedItem - return demand(promise).reject.with.error() - }) + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) - it('errors when the input is not a select', function () { - var promise = browser.find('.element').select({text: 'Whatevs'}) - assembly.eventuallyInsertHtml('
') - return demand(promise).reject.with.error(/expected some elements/) + return promise.then(function () { + demand(selectedItem).to.equal('0') + }) + }) }) - it('selects an option using text that is falsy', function () { - var promise = browser.find('.element').select({text: 0}) - var selectedItem + describe('exactText', function () { + it('should select an option using exact text that would otherwise match multiple options', function () { + var promise = browser.find('.element').select({exactText: 'Mr'}) + var selectedItem - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').text() + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').text() + }) + ) + + return promise.then(function () { + demand(selectedItem).to.equal('Mr') }) - ) + }) - return promise.then(function () { - demand(selectedItem).to.equal('0') + it('should select an option using exact text that is falsy', function () { + var promise = browser.find('.element').select({exactText: ''}) + var selectedItem + + assembly.eventuallyInsertHtml( + assembly.jQuery('').on('change', function (e) { + selectedItem = assembly.jQuery(this).find(':selected').val() + }) + ) + + return promise.then(function () { + demand(selectedItem).to.equal('0') + }) }) }) }) - describe('exactText', function () { - it('should select an option using exact text that would otherwise match multiple options', function () { - var promise = browser.find('.element').select({exactText: 'Mr'}) - var selectedItem + describe('submit', function () { + it('should submit the form', function () { + var submitted = false + var promise = browser.find('input').submit() assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').text() + assembly.jQuery('
').on('submit', function (ev) { + submitted = true }) ) return promise.then(function () { - demand(selectedItem).to.equal('Mr') + demand(submitted).to.equal(true) }) }) - it('should select an option using exact text that is falsy', function () { - var promise = browser.find('.element').select({exactText: ''}) - var selectedItem + it('should submit the form when submit button is clicked', function () { + var submitted = false + var promise = browser.find('input').click() assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').val() + assembly.jQuery('
submit
').on('submit', function (ev) { + ev.preventDefault() + submitted = true }) ) return promise.then(function () { - demand(selectedItem).to.equal('0') + demand(submitted).to.equal(true) }) }) }) - }) - describe('submit', function () { - domTest('stack trace', function (browser, dom) { - return browser.find('div') - .submit() - .assertStackTrace(__filename) - }, { - mochaOnly: true - }) - - it('should submit the form', function () { - var submitted = false - var promise = browser.find('input').submit() - - assembly.eventuallyInsertHtml( - assembly.jQuery('
').on('submit', function (ev) { - submitted = true + describe('typeIn', function () { + var allowedToTypeInto = [ + '', + '', + '', + '', + '', + '', + '', + '', + '') + }) + + function canSetInputValueOfType (type, value) { + canSetInputValueOfHtml(``, value) + } + + function canSetInputValueOfHtml (html, value = 'value') { + it(`can set ${html} value`, async () => { + const events = [] + const field = assembly.insertHtml(html) + field.addEventListener('change', () => events.push('change')) + + await browser.scope(field).setValue(value) + + demand(events).to.eql(['change']) + demand(field.value).to.equal(value) + + const valueSet = await browser.scope(field).value() + demand(valueSet).to.equal(value) + }) + } + + describe('setting select values', () => { + it('can set a select value by option value', async () => { + const select = assembly.insertHtml(` + + `) + + const selectScope = browser.scope(select) + + await selectScope.setValue('value1') + const value = await selectScope.value() + demand(value).to.equal('Text One') + }) + + it('can set a select value by option text', async () => { + const select = assembly.insertHtml(` + + `) + + const selectScope = browser.scope(select) + + await selectScope.setValue('Text Two') + const value = await selectScope.value() + demand(value).to.equal('Text Two') + }) + + it('waits for option to appear before selecting it', async () => { + const select = assembly.insertHtml(` + + `) + + assembly.eventuallyAppendHtml(select, '') + + const selectScope = browser.scope(select) + + await selectScope.setValue('Text Three') + const value = await selectScope.value() + demand(value).to.equal('Text Three') + }) + }) + + describe('setting a checkbox value', () => { + it('can set a checkbox value', async () => { + const checkbox = assembly.insertHtml(` + + `) + + const selectScope = browser.scope(checkbox) + + await selectScope.setValue(true) + const trueValue = await selectScope.value() + demand(trueValue).to.equal(true) + + await selectScope.setValue(false) + const falseValue = await selectScope.value() + demand(falseValue).to.equal(false) + }) + }) + }) + + describe('shouldHave value', () => { + it.only('uses .value in shouldHave({value}) and shouldHave({exactValue})', async () => { + const custom = browser.defineFieldValue({ + get: (monkey) => monkey.mapAll(() => 'div value') + }) + + await custom.shouldHave({exactValue: 'div value'}) + }) + }) + describe('setting a field', () => { it("can set a field's value", () => { const events = [] @@ -70,9 +226,9 @@ describe('fields', function () { field.addEventListener('change', () => events.push('change')) - browser.defineField((name, monkey) => monkey.find('div.field').containing('.field-name', {exactText: name}).find('input')) + const custom = browser.defineField((monkey, name) => monkey.find('div.field').containing('.field-name', {exactText: name}).find('input')) - return browser.setField('Email', 'bob@example.com').then(function () { + return custom.setField('Email', 'bob@example.com').then(function () { demand(events).to.eql(['change']) }) }) diff --git a/test/fuzzyFinderSpec.js b/test/fuzzyFinderSpec.js deleted file mode 100644 index e79cda0..0000000 --- a/test/fuzzyFinderSpec.js +++ /dev/null @@ -1,165 +0,0 @@ -var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') -var demand = require('must') - -describe('fuzzy finders', function () { - describeAssemblies([DomAssembly], function (Assembly) { - var assembly - var browser - var clicks - - beforeEach(function () { - assembly = new Assembly() - browser = assembly.browserMonkey() - domSetup() - }) - - function domSetup () { - assembly.insertHtml( - '
' + - 'Link One ' + - 'Link Two ' + - 'Another Link ' + - 'Link Four ' + - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' + - ' ' + - ' ' + - '
' - ) - assembly.find('*').addEventListener('click', function (e) { - if (e.target.tagName !== 'DIV') { - clicks.push(e.target.id) - } - return false - }) - - return clicks - }; - - describe('.click(label)', function () { - it('clicks links or buttons with the label', function () { - var labels = ['Link One', 'button_seven', 'Button Six'] - return Promise.all(labels.map(function (label) { - return browser.click(label) - })).then(function () { - demand(clicks.sort()).to.eql(['button_seven', 'button_six', 'link_one']) - }) - }) - }) - - describe('.linkOrButton(label)', function () { - it('finds links or buttons with the label', function () { - var labels = ['Link Four', 'button_two', 'Button Eight'] - return Promise.all(labels.map(function (label) { - return browser.linkOrButton(label).click() - })).then(function () { - demand(clicks.sort()).to.eql(['button_eight', 'button_two', 'link_four']) - }) - }) - }) - - describe('.link(label)', function () { - it('finds anchors by text', function () { - return browser.link('Link One').click().then(function () { - demand(clicks).to.eql(['link_one']) - }) - }) - - it('finds anchors by id', function () { - return browser.link('link_two').click().then(function () { - demand(clicks).to.eql(['link_two']) - }) - }) - - it('finds anchors by title', function () { - return browser.link('Link Three').click().then(function () { - demand(clicks).to.eql(['link_three']) - }) - }) - - it('finds anchors by nested img alt', function () { - return browser.link('Link Four').click().then(function () { - demand(clicks).to.eql(['link_four']) - }) - }) - - it('finds anchors inside elements', function () { - return browser.find('#container').link('Fifth Link').click().then(function () { - demand(clicks).to.eql(['link_five']) - }) - }) - - it('does not find anchors by id', function () { - return browser.link('omg').shouldNotExist() - }) - - it('does not find anchors inside elements', function () { - return browser.find('#container').link('foo').shouldNotExist() - }) - }) - - describe('.button(label)', function () { - it('finds button elements by text', function () { - return browser.button('Button One').click().then(function () { - demand(clicks).to.eql(['button_one']) - }) - }) - - it('finds button elements by id', function () { - return browser.button('button_two').click().then(function () { - demand(clicks).to.eql(['button_two']) - }) - }) - - it('does not find button elements by id', function () { - return browser.button('button_666').shouldNotExist() - }) - - it('finds button elements by value substring', function () { - return browser.button('Three').click().then(function () { - demand(clicks).to.eql(['button_three']) - }) - }) - - it('does not find button elements by value substring', function () { - return browser.button('not a substring').shouldNotExist() - }) - - it('finds button elements by title substring', function () { - return browser.button('Four').click().then(function () { - demand(clicks).to.eql(['button_four']) - }) - }) - - it('finds input elements by title substring', function () { - return browser.button('Five').click().then(function () { - demand(clicks).to.eql(['button_five']) - }) - }) - - it('finds button elements by nested image alt substring', function () { - return browser.button('Six').click().then(function () { - demand(clicks).to.eql(['button_six']) - }) - }) - - it('finds input[type=reset] elements', function () { - return browser.button('Seven').click().then(function () { - demand(clicks).to.eql(['button_seven']) - }) - }) - - it('finds input[type=button] elements', function () { - return browser.button('Eight').click().then(function () { - demand(clicks).to.eql(['button_eight']) - }) - }) - }) - }) -}) diff --git a/test/selectorsSpec.js b/test/selectorsSpec.js index 18a7615..bb111cd 100644 --- a/test/selectorsSpec.js +++ b/test/selectorsSpec.js @@ -337,6 +337,43 @@ describe('selectors', () => { }) }) + describe.only('actions', () => { + it('actions are only executed once', async () => { + let actionExecuted = 0 + + assembly.insertHtml(` +
A
+ `) + + const action = browserMonkey.find('div').action(function () { + actionExecuted++ + }) + + await action + expect(actionExecuted).to.equal(1) + await action + expect(actionExecuted).to.equal(1) + }) + + it('actions return the element or elements they acted on', async () => { + const divA = assembly.insertHtml(` +
A
+ `) + const divB = assembly.insertHtml(` +
B
+ `) + + let givenElements + const action = browserMonkey.find('div').action(function (els) { + givenElements = els + }) + + const elements = await action + expect(elements).to.eql([divA, divB]) + expect(givenElements).to.eql([divA, divB]) + }) + }) + describe('errors', () => { it('shows what it was able to map', async () => { const name = browserMonkey @@ -410,10 +447,10 @@ describe('selectors', () => { const [a, b] = await promise expect(a).to.equal(undefined) - expect(b).to.eql([await bPromise]) + expect(b).to.eql(await bPromise) }) - it.only('returns descriptions of all queries used when none are successful', async () => { + it('returns descriptions of all queries used when none are successful', async () => { const promise = browserMonkey.race([ b => b.find('.a').some(), b => b.find('.b').some() @@ -421,7 +458,7 @@ describe('selectors', () => { assembly.eventuallyInsertHtml('
') - await promise + await expect(promise).to.reject.with.error('all queries failed in race (found: race between (expected some elements (found: find: .a [0]), and expected some elements (found: find: .b [0])))') }) }) }) diff --git a/yarn.lock b/yarn.lock index 9525196..d7f5033 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,8 +15,8 @@ htmlparser2 "^3.9.0" "@types/node@^8.0.24": - version "8.9.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.9.4.tgz#dfd327582a06c114eb6e0441fa3d6fab35edad48" + version "8.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.1.tgz#aac98b810c50568054486f2bb8c486d824713be8" JSONStream@^1.0.3: version "1.3.1" @@ -216,14 +216,14 @@ asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" -assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - assert@^1.4.0: version "1.4.1" resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" @@ -1342,13 +1342,13 @@ combine-source-map@~0.7.1: lodash.memoize "~3.0.3" source-map "~0.5.3" -combined-stream@1.0.6: +combined-stream@1.0.6, combined-stream@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" dependencies: delayed-stream "~1.0.0" -combined-stream@^1.0.5, combined-stream@~1.0.5: +combined-stream@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" dependencies: @@ -1473,7 +1473,7 @@ core-js@^2.2.0, core-js@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e" -core-util-is@~1.0.0: +core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1563,7 +1563,7 @@ debug-log@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" -debug@2, debug@^2.1.1, debug@^2.2.0, debug@^2.6.3: +debug@2, debug@^2.1.1, debug@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d" dependencies: @@ -1587,7 +1587,7 @@ debug@2.6.1: dependencies: ms "0.7.2" -debug@2.6.9, debug@^2.1.3: +debug@2.6.9, debug@^2.1.3, debug@^2.2.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -1618,8 +1618,8 @@ deep-equal@1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" deep-extend@~0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253" + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" deep-is@~0.1.3: version "0.1.3" @@ -1850,8 +1850,8 @@ electron-window@^0.8.0: is-electron-renderer "^2.0.0" electron@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.2.tgz#a817cd733c2972b3c7cc4f777caf6e424b88014d" + version "1.8.4" + resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.4.tgz#cca8d0e6889f238f55b414ad224f03e03b226a38" dependencies: "@types/node" "^8.0.24" electron-download "^3.0.1" @@ -2346,9 +2346,13 @@ extract-zip@^1.0.3: mkdirp "0.5.0" yauzl "2.4.1" -extsprintf@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" fast-deep-equal@^1.0.0: version "1.1.0" @@ -2589,8 +2593,8 @@ get-stdin@^5.0.1: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" getpass@^0.1.1: - version "0.1.6" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" dependencies: assert-plus "^1.0.0" @@ -2618,7 +2622,7 @@ glob@7.0.5: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.2: +glob@7.1.2, glob@^7.0.5: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -2629,7 +2633,7 @@ glob@7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.0, glob@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -2817,8 +2821,8 @@ home-path@^1.0.1: resolved "https://registry.yarnpkg.com/home-path/-/home-path-1.0.5.tgz#788b29815b12d53bacf575648476e6f9041d133f" hosted-git-info@^2.1.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + version "2.6.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" htmlescape@^1.1.0: version "1.1.1" @@ -2959,8 +2963,8 @@ inherits@2.0.1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" ini@~1.3.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" inline-source-map@~0.6.0: version "0.6.2" @@ -3226,12 +3230,6 @@ isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" -jodid25519@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" - dependencies: - jsbn "~0.1.0" - jquery@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" @@ -3320,12 +3318,13 @@ jsonpointer@^4.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" jsprim@^1.2.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" dependencies: - extsprintf "1.0.2" + assert-plus "1.0.0" + extsprintf "1.3.0" json-schema "0.2.3" - verror "1.3.6" + verror "1.10.0" jsx-ast-utils@^1.3.4: version "1.4.0" @@ -3706,18 +3705,18 @@ mime-types@2.1.11, mime-types@~2.1.11: dependencies: mime-db "~1.23.0" -mime-types@^2.1.12, mime-types@~2.1.13, mime-types@~2.1.7: - version "2.1.14" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" - dependencies: - mime-db "~1.26.0" - -mime-types@~2.1.17: +mime-types@^2.1.12, mime-types@~2.1.17: version "2.1.18" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: mime-db "~1.33.0" +mime-types@~2.1.13, mime-types@~2.1.7: + version "2.1.14" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee" + dependencies: + mime-db "~1.26.0" + mime@1.3.4, mime@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" @@ -4408,8 +4407,8 @@ raw-body@~2.2.0: unpipe "1.0.0" rc@^1.1.2: - version "1.2.5" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" + version "1.2.6" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092" dependencies: deep-extend "~0.4.0" ini "~1.3.0" @@ -4617,8 +4616,8 @@ repeating@^2.0.0: is-finite "^1.0.0" request@^2.45.0: - version "2.83.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + version "2.85.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" dependencies: aws-sign2 "~0.7.0" aws4 "^1.6.0" @@ -4702,12 +4701,18 @@ restore-cursor@^1.0.1: exit-hook "^1.0.0" onetime "^1.0.0" -rimraf@2, rimraf@^2.2.8, rimraf@^2.6.0: +rimraf@2, rimraf@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" +rimraf@^2.2.8: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + rimraf@~2.5.1, rimraf@~2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" @@ -4732,11 +4737,7 @@ rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" -safe-buffer@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" - -safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -4998,8 +4999,8 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" sshpk@^1.7.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.11.0.tgz#2d8d5ebb4a6fab28ffba37fa62a90f4a3ea59d77" + version "1.14.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.14.1.tgz#130f5975eddad963f1d56f92b9ac6c51fa9f83eb" dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -5008,7 +5009,6 @@ sshpk@^1.7.0: optionalDependencies: bcrypt-pbkdf "^1.0.0" ecc-jsbn "~0.1.1" - jodid25519 "^1.0.0" jsbn "~0.1.0" tweetnacl "~0.14.0" @@ -5454,11 +5454,13 @@ vdom-to-html@2.3.1: param-case "^1.0.1" xtend "^4.0.0" -verror@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" dependencies: - extsprintf "1.0.2" + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" virtual-dom@2.1.1, virtual-dom@^2.1.1: version "2.1.1" From 735cf6e8f1b556e547ba2e6fd5039f5d81d3fbdc Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Wed, 18 Apr 2018 08:12:41 +0100 Subject: [PATCH 008/127] wait for iframe to be ready --- electron-mocha | 1 - lib/finders.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100755 electron-mocha diff --git a/electron-mocha b/electron-mocha deleted file mode 100755 index 278a9d5..0000000 --- a/electron-mocha +++ /dev/null @@ -1 +0,0 @@ -yarn electron-mocha --renderer $@ diff --git a/lib/finders.js b/lib/finders.js index be7074f..389b3fc 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -31,7 +31,7 @@ module.exports = { iframe: function () { return this.map(function (element) { if (isIframe(element)) { - if (element.contentDocument.body) { + if (element.contentDocument && element.contentDocument.readyState === 'complete') { return element.contentDocument.body } else { throw new BrowserMonkeyAssertionError('iframe not loaded') From af94f6a0fc1e3bf740bcfb7f00c674d0892192f0 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Tue, 8 Jan 2019 10:18:54 +0100 Subject: [PATCH 009/127] better checkbox handling --- lib/BrowserMonkeyAssertionError.js | 1 + lib/actions.js | 37 +++-- lib/assertions.js | 53 ++++--- lib/button.js | 5 +- lib/elementChecked.js | 5 + lib/{elementFindElements.js => elementHas.js} | 25 +-- lib/elementQuerySelectorAll.js | 11 ++ lib/elementsFindElements.js | 10 -- lib/fields.js | 2 +- lib/finders.js | 41 +++-- lib/selector.js | 145 +++++++++++------- test/actionsSpec.js | 4 +- test/assertionsSpec.js | 13 ++ test/componentSpec.js | 132 ++++++++-------- test/fieldsSpec.js | 10 +- test/findersSpec.js | 20 ++- test/hyperdomIntegrationSpec.js | 98 ++++++------ test/optionsSpec.js | 13 +- test/page1.html | 1 + test/page2.html | 2 +- test/selectorsSpec.js | 51 +++--- 21 files changed, 393 insertions(+), 286 deletions(-) create mode 100644 lib/elementChecked.js rename lib/{elementFindElements.js => elementHas.js} (57%) create mode 100644 lib/elementQuerySelectorAll.js delete mode 100644 lib/elementsFindElements.js diff --git a/lib/BrowserMonkeyAssertionError.js b/lib/BrowserMonkeyAssertionError.js index feea96c..7590725 100644 --- a/lib/BrowserMonkeyAssertionError.js +++ b/lib/BrowserMonkeyAssertionError.js @@ -3,6 +3,7 @@ function BrowserMonkeyAssertionError (message, options) { this.showDiff = true this.expected = options && options.expected this.actual = options && options.actual + this.transforms = [] if (Error.captureStackTrace) { Error.captureStackTrace(this, BrowserMonkeyAssertionError) diff --git a/lib/actions.js b/lib/actions.js index 523ddda..e52a6d4 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -3,6 +3,7 @@ var elementSelect = require('./elementSelect') var elementClick = require('./elementClick') var elementSubmit = require('./elementSubmit') var elementEnterText = require('./elementEnterText') +var elementChecked = require('./elementChecked') const elementInnerText = require('./elementInnerText') function notSillyBlankIEObject (element) { @@ -38,14 +39,15 @@ module.exports = { }, select: function (value) { - return this.is('select').anyOf([ - function (monkey) { - return monkey.find('option', {exactText: value}) - }, - function (monkey) { - return monkey.find('option', {exactValue: value}) + var criteria = typeof value === 'string' + ? [{exactText: value}, {exactValue: value}] + : [value] + + return this.is('select').all(criteria.map(function (c) { + return function (monkey) { + return monkey.find('option', c) } - ]).element().action(function (optionElement) { + })).element().action(function (optionElement) { var selectElement = optionElement.parentNode debug('select', selectElement) elementSelect(selectElement, optionElement) @@ -54,7 +56,7 @@ module.exports = { check: function (value) { return this.is('input[type=checkbox]').one().action(function (checkbox) { - if (checkbox.checked !== value) { + if (elementChecked(checkbox) !== value) { elementClick(checkbox) } }) @@ -62,15 +64,21 @@ module.exports = { checked: function () { return this.is('input[type=checkbox]').one().mapAll(function (checkbox) { - return checkbox.checked + return elementChecked(checkbox) }) }, selectValue: function () { - return this.is('select').find('option').elements().mapAll(function (options) { - var selectElement = options[0].parentNode - var selectedOption = options[selectElement.selectedIndex] - return elementInnerText(selectedOption) + return this.is('select').one().mapAll(function (selectElement) { + return [selectElement] + }).find('option').mapAll(function (options) { + if (options.length) { + var selectElement = options[0].parentNode + var selectedOption = options[selectElement.selectedIndex] + return elementInnerText(selectedOption) + } else { + return '' + } }) }, @@ -82,6 +90,9 @@ module.exports = { typeIn: function (text) { return this.is(inputsSelector).element().action(function (element) { + if (typeof text !== 'string') { + throw new Error('expected string as argument to typeIn') + } debug('typeIn', element, text) elementEnterText(element, text) }) diff --git a/lib/assertions.js b/lib/assertions.js index c6865b8..431ab03 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1,5 +1,6 @@ const elementInnerText = require('./elementInnerText') const elementSelector = require('./elementSelector') +const elementChecked = require('./elementChecked') const inspect = require('object-inspect') const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') var expectDeepEqual = require('./expectDeepEqual') @@ -29,8 +30,10 @@ module.exports = { }, shouldHave: function (options) { + var self = this + return this.elements().ensure(function (elements) { - assertElementExpectations(elements, options) + assertElementExpectations(self, elements, options) }, inspect(options)) }, @@ -43,10 +46,6 @@ module.exports = { return this.zero() }, - has: function (options) { - return this.shouldHave(options) - }, - shouldHaveElement: function (fn) { return this.one().ensure(fn) }, @@ -56,10 +55,12 @@ module.exports = { }, shouldNotHave: function (options) { + var self = this + return this.elements().ensure(function (elements) { let thrownError try { - assertElementExpectations(elements, options) + assertElementExpectations(self, elements, options) throw (thrownError = new BrowserMonkeyAssertionError('expected elements not to have: ' + JSON.stringify(options))) } catch (e) { // we expect assertion errors, but nothing else @@ -93,8 +94,18 @@ function assertInexactValues (actual, expected) { } } +function queryValue (query, elements) { + var valueQuery = query.subQuery(function (q) { + return q.value() + }) + + return elements.map(function (element) { + return valueQuery.resolve([element]).value + }) +} + var elementExpectations = { - text: function (elements, expected) { + text: function (query, elements, expected) { var expectedArray = toArray(expected) var actual = elements.map(function (element) { @@ -104,29 +115,25 @@ var elementExpectations = { assertInexactValues(actual, expectedArray) }, - value: function (elements, expected) { + value: function (query, elements, expected) { var expectedArray = toArray(expected) - var actual = elements.map(function (element) { - return element.value - }) + var actual = queryValue(query, elements) assertInexactValues(actual, expectedArray) }, - exactValue: function (elements, expected) { + exactValue: function (query, elements, expected) { var expectedArray = toArray(expected) - var actual = elements.map(function (element) { - return element.value - }) + var actual = queryValue(query, elements) expectDeepEqual(actual, expectedArray, function (actual, expected) { return 'expected ' + actual + ' to have exact values ' + expected }) }, - exactText: function (elements, expected) { + exactText: function (query, elements, expected) { var expectedArray = toArray(expected) var actual = elements.map(function (element) { @@ -138,11 +145,11 @@ var elementExpectations = { }) }, - checked: function (elements, expected) { + checked: function (query, elements, expected) { var expectedArray = toArray(expected) var actual = elements.map(function (element) { - return element.checked + return elementChecked(element) }) expectDeepEqual(actual, expectedArray, function (actual, expected) { @@ -150,7 +157,7 @@ var elementExpectations = { }) }, - css: function (elements, expected) { + css: function (query, elements, expected) { var expectedArray = toArray(expected) var equal = elements.length === expectedArray.length && @@ -170,13 +177,13 @@ var elementExpectations = { } }, - length: function (elements, expected) { + length: function (query, elements, expected) { expectEqual(elements.length, expected, function (actual, expected) { return 'expected number of elements found ' + actual + ' to equal ' + expected }) }, - attributes: function (elements, expected) { + attributes: function (query, elements, expected) { var expectedArray = toArray(expected) var actual = elements.map(function (element, index) { @@ -195,7 +202,7 @@ var elementExpectations = { } } -function assertElementExpectations (elements, options) { +function assertElementExpectations (query, elements, options) { Object.keys(options).forEach(key => { var expectation = elementExpectations[key] @@ -203,6 +210,6 @@ function assertElementExpectations (elements, options) { throw new Error('no such expectation ' + key + ', try one of: ' + Object.keys(elementExpectations).join(', ')) } - expectation(elements, options[key]) + expectation(query, elements, options[key]) }) } diff --git a/lib/button.js b/lib/button.js index 1c42129..0cd4f19 100644 --- a/lib/button.js +++ b/lib/button.js @@ -1,6 +1,6 @@ module.exports = { button: function (name) { - return this.anyOf(this._options.definitions.button.map(function (definition) { + return this.all(this._options.definitions.button.map(function (definition) { return function (monkey) { return definition(monkey, name) } @@ -8,8 +8,9 @@ module.exports = { }, defineButton: function (definition) { + var self = this return this.clone(function (clone) { - this._options.definitions.button.push(definition) + self._options.definitions.button.push(definition) }) }, diff --git a/lib/elementChecked.js b/lib/elementChecked.js new file mode 100644 index 0000000..224192a --- /dev/null +++ b/lib/elementChecked.js @@ -0,0 +1,5 @@ +module.exports = function (element) { + return element.indeterminate + ? 'indeterminate' + : element.checked +} diff --git a/lib/elementFindElements.js b/lib/elementHas.js similarity index 57% rename from lib/elementFindElements.js rename to lib/elementHas.js index e3d11d7..d311314 100644 --- a/lib/elementFindElements.js +++ b/lib/elementHas.js @@ -1,21 +1,6 @@ const elementInnerText = require('./elementInnerText') -const elementVisible = require('./elementVisible') -module.exports = function findElements (element, selector, findOptions, selectorOptions) { - var children = Array.prototype.slice.call(element.querySelectorAll(selector)) - - children = selectorOptions.visibleOnly - ? children.filter(elementVisible) - : children - - children = findOptions - ? findByOptions(children, findOptions) - : children - - return children -} - -function findByOptions (elements, options) { +function elementHas (element, index, options) { var filters = Object.keys(options).map(function (key) { var filter = findOptionFilters[key] if (!filter) { @@ -27,10 +12,8 @@ function findByOptions (elements, options) { } }) - return elements.filter(function (element, index) { - return filters.every(function (filter) { - return filter.filter(element, filter.expected, index) - }) + return filters.every(function (filter) { + return filter.filter(element, filter.expected, index) }) } @@ -55,3 +38,5 @@ var findOptionFilters = { return expectedIndex === actualIndex } } + +module.exports = elementHas diff --git a/lib/elementQuerySelectorAll.js b/lib/elementQuerySelectorAll.js new file mode 100644 index 0000000..259db1c --- /dev/null +++ b/lib/elementQuerySelectorAll.js @@ -0,0 +1,11 @@ +const elementVisible = require('./elementVisible') + +module.exports = function elementQuerySelectorAll (element, selector, selectorOptions) { + var children = Array.prototype.slice.call(element.querySelectorAll(selector)) + + children = selectorOptions.visibleOnly + ? children.filter(elementVisible) + : children + + return children +} diff --git a/lib/elementsFindElements.js b/lib/elementsFindElements.js deleted file mode 100644 index 503ec50..0000000 --- a/lib/elementsFindElements.js +++ /dev/null @@ -1,10 +0,0 @@ -var elementFindElements = require('./elementFindElements') -var flatten = require('lowscore/flatten') - -module.exports = function (elements, selector, findOptions, selectorOptions) { - return flatten( - elements.map(function (element) { - return elementFindElements(element, selector, findOptions, selectorOptions) - }) - ) -} diff --git a/lib/fields.js b/lib/fields.js index cfb730e..ca8fa07 100644 --- a/lib/fields.js +++ b/lib/fields.js @@ -1,6 +1,6 @@ module.exports = { field: function (name) { - return this.anyOf(this._options.definitions.field.map(function (definition) { + return this.all(this._options.definitions.field.map(function (definition) { return function (monkey) { return definition(monkey, name) } diff --git a/lib/finders.js b/lib/finders.js index 389b3fc..8c5579d 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -1,6 +1,7 @@ var elementMatches = require('./elementMatches') -var elementFindElements = require('./elementFindElements') -var elementsFindElements = require('./elementsFindElements') +var elementQuerySelectorAll = require('./elementQuerySelectorAll') +var elementHas = require('./elementHas') +var flatten = require('lowscore/flatten') var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') var inspect = require('object-inspect') @@ -8,10 +9,25 @@ module.exports = { find: function (selector, findOptions) { var self = this + var findElements = this.mapAll(function (elements) { + expectElements(elements) + return flatten(elements.map(function (element) { + return elementQuerySelectorAll(element, selector, self._options) + })) + }, 'find(' + inspect(selector) + ')') + + if (findOptions) { + return findElements.has(findOptions) + } else { + return findElements + } + }, + + has: function (options) { return this.mapAll(function (elements) { expectElements(elements) - return elementsFindElements(elements, selector, findOptions, self._options) - }, 'find: ' + selector + (findOptions ? ' ' + inspect(findOptions) : '')) + return elements.filter(function (element, index) { return elementHas(element, index, options) }) + }, 'has: ' + inspect(options)) }, is: function (selector) { @@ -24,11 +40,18 @@ module.exports = { var self = this return this.filter(function (element) { - return elementFindElements(element, selector, options, self._options).length > 0 + var foundElements = elementQuerySelectorAll(element, selector, self._options) + var filteredElements = options + ? foundElements.filter(function (element, index) { + return elementHas(element, index, options) + }) + : foundElements + + return filteredElements.length > 0 }, 'containing: ' + selector + (options ? ' ' + inspect(options) : '')) }, - iframe: function () { + iframeContent: function () { return this.map(function (element) { if (isIframe(element)) { if (element.contentDocument && element.contentDocument.readyState === 'complete') { @@ -37,16 +60,16 @@ module.exports = { throw new BrowserMonkeyAssertionError('iframe not loaded') } } else { - return element + throw new BrowserMonkeyAssertionError('not iframe') } - }, 'iframe') + }, 'iframeContent') }, scope: function (element) { var selector = this.input([element]) if (isIframe(element)) { - return selector.iframe() + return selector.iframeContent() } else if (isHTMLElement(element)) { return selector } else { diff --git a/lib/selector.js b/lib/selector.js index 217f696..95ed66f 100644 --- a/lib/selector.js +++ b/lib/selector.js @@ -4,11 +4,14 @@ var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') var elementInnerText = require('./elementInnerText') var inspect = require('object-inspect') var uniq = require('lowscore/uniq') +var debug = require('debug')('browser-monkey') function Selector () { this._conditions = [] this._options = { visibleOnly: true, + timeout: 1000, + interval: 10, definitions: { button: [ function (monkey, name) { @@ -126,27 +129,17 @@ Selector.prototype.filter = function (filter, description) { }) } -function subQuery (selector, queryCreator) { - var clone = selector.clone() - clone._conditions = [] - var query = queryCreator(clone) - if (!(query instanceof Selector)) { - throw new Error('expected to create query: ' + queryCreator) - } - return query -} - -Selector.prototype.anyOf = function (queryCreators) { +Selector.prototype.all = function (queryCreators) { var self = this var queries = queryCreators.map(function (queryCreator) { - return subQuery(self, queryCreator) + return self.subQuery(queryCreator) }) return this.clone(function (clone) { clone._conditions.push(function (elements) { - var accumulators = queries.map((query) => { - return reduceTransformations(elements, query) + var accumulators = queries.map(function (query) { + return query.resolve(elements) }) var value = uniq(Array.prototype.concat.apply([], accumulators.map(function (accumulator) { @@ -154,26 +147,80 @@ Selector.prototype.anyOf = function (queryCreators) { }))) return { - anyOf: accumulators.map(function (accumulator) { return accumulator.transforms }), + all: accumulators.map(function (accumulator) { return accumulator.transforms }), value: value } }) }) } +Selector.prototype.subQuery = function (createQuery) { + var clone = this.clone() + clone._conditions = [] + var query = createQuery(clone) + if (!(query instanceof Selector)) { + throw new Error('expected to create query: ' + createQuery) + } + return query +} + +Selector.prototype.resolveSubQuery = function (createQuery, value) { + return this.subQuery(createQuery).resolve(value) +} + +Selector.prototype.resolve = function (value, options) { + var accumulator = {} + var includeScope = options && options.hasOwnProperty('includeScope') && options.includeScope !== undefined + ? options.includeScope + : false + + function reducer (accumulator, mapper, index) { + if (index === 0) { + accumulator.value = value + accumulator.transforms = [] + + if (includeScope) { + accumulator.transforms.push({ + description: 'scope', + value: value + }) + } + } + var result = mapper(accumulator.value) + accumulator.value = result.value + accumulator.transforms.push(result) + return accumulator + } + + try { + var queryResult = this._conditions.reduce(reducer, accumulator) + if (this._action && !this._actionExecuted) { + this._action(queryResult.value) + this._actionExecuted = true + } + return queryResult + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + e.transforms.unshift.apply(e.transforms, accumulator.transforms) + } + + throw e + } +} + Selector.prototype.race = function (queryCreators) { var self = this var queries = queryCreators.map(function (queryCreator) { - return subQuery(self, queryCreator) + return self.subQuery(queryCreator) }) return this.clone(function (clone) { clone._conditions.push(function (elements) { - var values = queries.map((query) => { + var values = queries.map(function (query) { try { return { - value: reduceTransformations(elements, query).value + value: query.resolve(elements).value } } catch (e) { return { @@ -204,38 +251,12 @@ Selector.prototype.race = function (queryCreators) { } Selector.prototype.options = function (options) { - return this.clone(function (clone) { - extend(clone._options, options) - }) -} - -function reduceTransformations (value, query) { - var accumulator = {} - - function reducer (accumulator, mapper, index) { - if (index === 0) { - accumulator.value = value - accumulator.transforms = [] - } - var result = mapper(accumulator.value) - accumulator.value = result.value - accumulator.transforms.push(result) - return accumulator - } - - try { - var queryResult = query._conditions.reduce(reducer, accumulator) - if (query._action && !query._actionExecuted) { - query._action(queryResult.value) - query._actionExecuted = true - } - return queryResult - } catch (e) { - if (e instanceof BrowserMonkeyAssertionError && !e.transforms) { - e.transforms = accumulator.transforms - } - - throw e + if (options) { + return this.clone(function (clone) { + extend(clone._options, options) + }) + } else { + return this._options } } @@ -245,13 +266,18 @@ Selector.prototype.then = function () { var originalStack = new Error().stack var promise = Promise.resolve(retry(function () { - return reduceTransformations(self._value, self) - })).then(function (accumulator) { - return accumulator.value - }, function (error) { + return self.resolve(self._value, {includeScope: true}).value + })).catch(function (error) { if (error instanceof BrowserMonkeyAssertionError) { error.message = browserMonkeyAssertionErrorMessage(error) error.stack = originalStack + + if (debug.enabled) { + debug('assertion error', error.message) + error.transforms.forEach(function (transform) { + debug('found: ' + transform.description, transform.value) + }) + } } throw error @@ -274,9 +300,9 @@ function browserMonkeyAssertionErrorMessage (error) { function describeAccumulatedTransforms (accumulatedTransforms) { return accumulatedTransforms.map(function (transform) { - if (transform.anyOf instanceof Array) { + if (transform.all instanceof Array) { return describeTransform({ - description: 'anyOf (' + transform.anyOf.map(describeAccumulatedTransforms).join(', or ') + ')', + description: 'all (' + transform.all.map(describeAccumulatedTransforms).join(', or ') + ')', value: transform.value }) } else if (transform.raceErrors instanceof Array) { @@ -378,7 +404,12 @@ Selector.prototype.component = function (methods) { Component.prototype = new this.constructor() Object.keys(methods).forEach(function (method) { - Component.prototype[method] = methods[method] + var descriptor = Object.getOwnPropertyDescriptor(methods, method) + if (descriptor) { + Object.defineProperty(Component.prototype, method, descriptor) + } else { + Component.prototype[method] = methods[method] + } }) Component.prototype.constructor = Component diff --git a/test/actionsSpec.js b/test/actionsSpec.js index a986e73..7f0a4bb 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -412,7 +412,7 @@ describe('actions', function () { return component.fill([ {name: 'title', action: 'select', options: {exactText: 'Mr'}}, - {name: 'name', action: 'typeIn', options: {text: 'Joe'}} + {name: 'name', action: 'typeIn', options: 'Joe'} ]).then(function () { demand(assembly.find('.title').value).to.equal('Mr') demand(assembly.find('.name').value).to.equal('Joe') @@ -437,7 +437,7 @@ describe('actions', function () { return component.fill([ {select: 'title', text: 'Mrs'}, - {typeIn: 'name', options: {text: 'Joe'}}, + {typeIn: 'name', options: 'Joe'}, {click: 'agree'} ]).then(function () { demand(assembly.find('.title').value).to.equal('Mrs') diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index 601b206..dfea739 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -265,6 +265,19 @@ describe('assertions', function () { ]) }) + it('asserts that a checkbox is indeterminate', function () { + var good = browser.find('.checkbox').shouldHave({checked: 'indeterminate'}) + + var checkbox = assembly.insertHtml('') + assembly.eventually(function () { + checkbox.indeterminate = true + }) + + return Promise.all([ + good + ]) + }) + it('fails if we expected one checkbox, but found many', function () { var bad = browser.find('.checkbox').shouldHave({checked: true}) diff --git a/test/componentSpec.js b/test/componentSpec.js index ed6de59..9565abf 100644 --- a/test/componentSpec.js +++ b/test/componentSpec.js @@ -11,88 +11,98 @@ describe('component', function () { browser = assembly.browserMonkey() }) - it('can return new selectors by extending', function () { - var user = browser.component({ - name: function () { - return this.find('.user-name') - }, - - address: function () { - return this.find('.user-address') - } - }) - - var promise = user.name().shouldExist() + it('can return new selectors by extending', function () { + var user = browser.component({ + name: function () { + return this.find('.user-name') + } + }) - assembly.eventuallyInsertHtml('
bob
bob\'s address
') + var promise = user.name().shouldExist() - return promise - }) + assembly.eventuallyInsertHtml('
bob
bob\'s address
') - it('components are independent', function () { - var user = browser.component({ - name: function () { - return this.find('.user-name') - } + return promise }) - browser.component({ - name: function () { - return this.find('.bah-name') - } + it('can define properties', function () { + var user = browser.component({ + get name () { + return this.find('.user-name') + } + }) + + var promise = user.name.shouldExist() + + assembly.eventuallyInsertHtml('
bob
bob\'s address
') + + return promise }) - var promise = user.name().shouldExist() + it('components are independent', function () { + var user = browser.component({ + name: function () { + return this.find('.user-name') + } + }) - assembly.eventuallyInsertHtml('
bob
bob\'s address
') + browser.component({ + name: function () { + return this.find('.bah-name') + } + }) - return promise - }) + var promise = user.name().shouldExist() - it('can extend another component', function () { - var user = browser.component({ - name: function () { - return this.find('.user-name') - }, + assembly.eventuallyInsertHtml('
bob
bob\'s address
') - address: function () { - return this.find('.user-address') - } + return promise }) - var bossUser = user.component({ - secondAddress: function () { - return this.find('.user-second-address') - } - }) + it('can extend another component', function () { + var user = browser.component({ + name: function () { + return this.find('.user-name') + }, - var name = bossUser.name().shouldExist() - var secondAddress = bossUser.secondAddress().shouldExist() + address: function () { + return this.find('.user-address') + } + }) - assembly.eventuallyInsertHtml('
bob
bob\'s address
bob\'s second address
') + var bossUser = user.component({ + secondAddress: function () { + return this.find('.user-second-address') + } + }) - return Promise.all([name, secondAddress]) - }) + var name = bossUser.name().shouldExist() + var secondAddress = bossUser.secondAddress().shouldExist() - it('components inherit scope', function () { - var adminArea = browser.find('.admin') + assembly.eventuallyInsertHtml('
bob
bob\'s address
bob\'s second address
') - var admin = adminArea.component({ - user: function () { - return this.find('.user') - } + return Promise.all([name, secondAddress]) }) - var promise = admin.user().shouldHave({text: ['Jane']}) + it('components inherit scope', function () { + var adminArea = browser.find('.admin') - assembly.eventuallyInsertHtml( - '
Bob
' + - '
' + - '
Jane
' + - '
' - ) + var admin = adminArea.component({ + user: function () { + return this.find('.user') + } + }) - return promise - }) + var promise = admin.user().shouldHave({text: ['Jane']}) + + assembly.eventuallyInsertHtml( + '
Bob
' + + '
' + + '
Jane
' + + '
' + ) + + return promise + }) }) }) diff --git a/test/fieldsSpec.js b/test/fieldsSpec.js index a8ac95f..ee56666 100644 --- a/test/fieldsSpec.js +++ b/test/fieldsSpec.js @@ -199,13 +199,21 @@ describe('fields', function () { }) describe('shouldHave value', () => { - it.only('uses .value in shouldHave({value}) and shouldHave({exactValue})', async () => { + it('uses .value in shouldHave({exactValue})', async () => { const custom = browser.defineFieldValue({ get: (monkey) => monkey.mapAll(() => 'div value') }) await custom.shouldHave({exactValue: 'div value'}) }) + + it('uses .value in shouldHave({value})', async () => { + const custom = browser.defineFieldValue({ + get: (monkey) => monkey.mapAll(() => 'div value') + }) + + await custom.shouldHave({value: 'value'}) + }) }) describe('setting a field', () => { diff --git a/test/findersSpec.js b/test/findersSpec.js index 17a0677..00d15bc 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -1,4 +1,3 @@ -require('babel-polyfill') var describeAssemblies = require('./describeAssemblies') const DomAssembly = require('./assemblies/DomAssembly') var demand = require('must') @@ -64,6 +63,19 @@ describe('find', function () { }) if (Assembly.hasDom()) { + describe('iframes', () => { + it('finds content after iframe navigation', async () => { + const page2Exists = browser.find('iframe').iframeContent().find('h1', {text: 'Page 2'}).shouldExist() + assembly.useNormalRetry() + + assembly.eventuallyInsertHtml(` + ` + + const addressBar = div.querySelector('.browser-monkey-address-bar') + const forwardButton = div.querySelector('.browser-monkey-forward-button') + const backButton = div.querySelector('.browser-monkey-back-button') + const addressBarText = div.querySelector('.browser-monkey-address-bar-text') + addressBarText.innerText = url + const iframe = div.querySelector('.browser-monkey-iframe') + + forwardButton.addEventListener('click', () => { + iframe.contentWindow.history.forward() + }) + + backButton.addEventListener('click', () => { + iframe.contentWindow.history.back() + }) + + iframe.src = url + iframe.addEventListener('load', () => { + console.log('loading') + addressBarText.innerText = iframe.contentWindow.location.href + const script = iframe.contentDocument.createElement('script') + script.src = 'file://' + __dirname + '/node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js' + iframe.contentDocument.head.appendChild(script) + console.log('done') + }) + + iframeResizer({ log: true, checkOrigin: false }, iframe) + + hobostyle.link(__dirname + '/iframe.css') + + return createMonkey(iframe) } diff --git a/lib/BrowserMonkeyAssertionError.js b/lib/BrowserMonkeyAssertionError.js deleted file mode 100644 index 7590725..0000000 --- a/lib/BrowserMonkeyAssertionError.js +++ /dev/null @@ -1,23 +0,0 @@ -function BrowserMonkeyAssertionError (message, options) { - this.message = message - this.showDiff = true - this.expected = options && options.expected - this.actual = options && options.actual - this.transforms = [] - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, BrowserMonkeyAssertionError) - } else { - try { - throw new Error() - } catch (e) { - this.stack = e.stack - } - } -} - -BrowserMonkeyAssertionError.prototype = Object.create(Error.prototype) -BrowserMonkeyAssertionError.prototype.name = 'BrowserMonkeyAssertionError' -BrowserMonkeyAssertionError.prototype.constructor = BrowserMonkeyAssertionError - -module.exports = BrowserMonkeyAssertionError diff --git a/lib/BrowserMonkeyAssertionError.ts b/lib/BrowserMonkeyAssertionError.ts new file mode 100644 index 0000000..b8766be --- /dev/null +++ b/lib/BrowserMonkeyAssertionError.ts @@ -0,0 +1,34 @@ +import { ExecutedTransformSequence } from './ExecutedTransformSequence' + +class BrowserMonkeyAssertionError extends Error { + private description: string + public showDiff: boolean + public expected: any + public actual: any + public executedTransforms: ExecutedTransformSequence + + public constructor (message, { + expected = undefined, + actual = undefined, + executedTransforms = new ExecutedTransformSequence(undefined) + } = {}) { + super(message) + this.description = message + this.showDiff = true + this.expected = expected + this.actual = actual + this.executedTransforms = executedTransforms + + Object.setPrototypeOf(this, BrowserMonkeyAssertionError.prototype) + } + + public rewriteMessage (): void { + this.message = this.renderError() + } + + public renderError (): string { + return `${this.description} (found: ${this.executedTransforms.renderError()})` + } +} + +export default BrowserMonkeyAssertionError diff --git a/lib/ExecutedAndTransform.ts b/lib/ExecutedAndTransform.ts new file mode 100644 index 0000000..ab8cb24 --- /dev/null +++ b/lib/ExecutedAndTransform.ts @@ -0,0 +1,14 @@ +import { ExecutedTransform } from './ExecutedTransform' + +export class ExecutedAndTransform extends ExecutedTransform { + public items: ExecutedTransform[] + + public constructor (value: any, items: ExecutedTransform[]) { + super(value) + this.items = items + } + + public renderError (): string { + return `and(${this.items.map(i => i.renderError()).join(', ')}) [${this.value.length}]` + } +} diff --git a/lib/ExecutedConcatTransform.ts b/lib/ExecutedConcatTransform.ts new file mode 100644 index 0000000..fc6e998 --- /dev/null +++ b/lib/ExecutedConcatTransform.ts @@ -0,0 +1,14 @@ +import { ExecutedTransform } from './ExecutedTransform' + +export class ExecutedConcatTransform extends ExecutedTransform { + public items: ExecutedTransform[] + + public constructor (value: any, items: ExecutedTransform[]) { + super(value) + this.items = items + } + + public renderError (): string { + return `concat (${this.items.map(i => i.renderError()).join(', ')}) [${this.value.length}]` + } +} diff --git a/lib/ExecutedOrTransform.ts b/lib/ExecutedOrTransform.ts new file mode 100644 index 0000000..16e509f --- /dev/null +++ b/lib/ExecutedOrTransform.ts @@ -0,0 +1,16 @@ +import { ExecutedTransform } from './ExecutedTransform' + +export class ExecutedOrTransform extends ExecutedTransform { + public items: ExecutedTransform[] + public index: number + + public constructor (value: any, index: number, items: ExecutedTransform[]) { + super(value) + this.index = index + this.items = items + } + + public renderError (): string { + return `or(${this.items.map(i => i.renderError()).join(', ')}) [${this.value.length}]` + } +} diff --git a/lib/ExecutedSimpleTransform.ts b/lib/ExecutedSimpleTransform.ts new file mode 100644 index 0000000..2733860 --- /dev/null +++ b/lib/ExecutedSimpleTransform.ts @@ -0,0 +1,14 @@ +import { ExecutedTransform } from './ExecutedTransform' + +export class ExecutedSimpleTransform extends ExecutedTransform { + public description: string + + public constructor (value: any, description = '') { + super(value) + this.description = description + } + + public renderError (): string { + return `${this.description} [${this.value.length}]` + } +} diff --git a/lib/ExecutedTransform.ts b/lib/ExecutedTransform.ts new file mode 100644 index 0000000..b5bb521 --- /dev/null +++ b/lib/ExecutedTransform.ts @@ -0,0 +1,15 @@ +export class ExecutedTransform { + public value: any + + public constructor (value: any) { + this.value = value + } + + public renderError (): string { + throw new Error() + } + + public print (): void { + throw new Error() + } +} diff --git a/lib/ExecutedTransformError.ts b/lib/ExecutedTransformError.ts new file mode 100644 index 0000000..ba545bb --- /dev/null +++ b/lib/ExecutedTransformError.ts @@ -0,0 +1,15 @@ +import { ExecutedTransform } from './ExecutedTransform' +import BrowserMonkeyAssertionError from './BrowserMonkeyAssertionError' + +export class ExecutedTransformError extends ExecutedTransform { + public exception: BrowserMonkeyAssertionError + + public constructor (exception: BrowserMonkeyAssertionError) { + super([]) + this.exception = exception + } + + public renderError (): string { + return this.exception.renderError() + } +} diff --git a/lib/ExecutedTransformSequence.ts b/lib/ExecutedTransformSequence.ts new file mode 100644 index 0000000..02cb43d --- /dev/null +++ b/lib/ExecutedTransformSequence.ts @@ -0,0 +1,30 @@ +import { ExecutedTransform } from './ExecutedTransform' + +export class ExecutedTransformSequence extends ExecutedTransform { + public transforms: ExecutedTransform[] + + public constructor (value: any) { + super(value) + this.transforms = [] + } + + public addTransform (transform: ExecutedTransform): void { + this.value = transform.value + this.transforms.push(transform) + } + + public prepend (transforms: ExecutedTransformSequence): void { + this.value = transforms.value + this.transforms.unshift(...transforms.transforms) + } + + public renderError (): string { + return this.transforms.map(t => t.renderError()).join(', ') + } + + public clone (): ExecutedTransformSequence { + const e = new ExecutedTransformSequence(this.value) + e.transforms = this.transforms.slice() + return e + } +} diff --git a/lib/actions.js b/lib/actions.js index b15039a..ecd9e1d 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -5,6 +5,7 @@ var elementSubmit = require('./elementSubmit') var elementEnterText = require('./elementEnterText') var elementChecked = require('./elementChecked') const elementInnerText = require('./elementInnerText') +var inputsSelector = require('./inputsSelector') function notSillyBlankIEObject (element) { return Object.keys(element).length > 0 @@ -31,6 +32,13 @@ module.exports = { } }, + enter: function (selector) { + return this.query(selector).element().action(function (element) { + debug('enter', element) + elementSubmit(element) + }) + }, + click: function () { return this.enabled().element().then(function (element) { debug('click', element) @@ -178,20 +186,3 @@ function inferField (component, field) { throw new Error('No action found for field: ' + JSON.stringify(field)) } } - -var inputsSelector = - 'input:not([type]), ' + - 'input[type=text], ' + - 'input[type=email], ' + - 'input[type=password], ' + - 'input[type=search], ' + - 'input[type=tel], ' + - 'input[type=url], ' + - 'input[type=number],' + - 'input[type=date],' + - 'input[type=datetime-local],' + - 'input[type=month],' + - 'input[type=time],' + - 'input[type=week],' + - 'input[type=range],' + - 'textarea' diff --git a/lib/assertions.js b/lib/assertions.js index f5f036f..57d9ec1 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -100,7 +100,7 @@ function queryValue (query, elements) { }) return elements.map(function (element) { - return valueQuery.resolve([element]).value + return valueQuery.execute([element]).value }) } diff --git a/lib/fields.js b/lib/fields.js index ca8fa07..b7a0a20 100644 --- a/lib/fields.js +++ b/lib/fields.js @@ -23,7 +23,7 @@ module.exports = { return this.field(name).setValue(value) }, - value: function () { + valueOld: function () { var getters = this._options.definitions.fieldValue.map(function (definition) { return definition.get }) diff --git a/lib/finders.js b/lib/finders.js index 8c5579d..c4c193c 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -4,23 +4,16 @@ var elementHas = require('./elementHas') var flatten = require('lowscore/flatten') var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') var inspect = require('object-inspect') +const { ExecutedSimpleTransform } = require('./ExecutedSimpleTransform') module.exports = { - find: function (selector, findOptions) { - var self = this - - var findElements = this.mapAll(function (elements) { + css: function (selector) { + return this.transform((elements) => { expectElements(elements) - return flatten(elements.map(function (element) { - return elementQuerySelectorAll(element, selector, self._options) - })) - }, 'find(' + inspect(selector) + ')') - - if (findOptions) { - return findElements.has(findOptions) - } else { - return findElements - } + return new ExecutedSimpleTransform(flatten(elements.map(element => { + return elementQuerySelectorAll(element, selector, this._options) + })), 'find(' + inspect(selector) + ')') + }) }, has: function (options) { diff --git a/lib/inputsSelector.js b/lib/inputsSelector.js new file mode 100644 index 0000000..3b0eb74 --- /dev/null +++ b/lib/inputsSelector.js @@ -0,0 +1,18 @@ +var inputsSelector = +'input:not([type]), ' + +'input[type=text], ' + +'input[type=email], ' + +'input[type=password], ' + +'input[type=search], ' + +'input[type=tel], ' + +'input[type=url], ' + +'input[type=number],' + +'input[type=date],' + +'input[type=datetime-local],' + +'input[type=month],' + +'input[type=time],' + +'input[type=week],' + +'input[type=range],' + +'textarea' + +module.exports = inputsSelector \ No newline at end of file diff --git a/lib/query.js b/lib/query.js deleted file mode 100644 index e108a39..0000000 --- a/lib/query.js +++ /dev/null @@ -1,419 +0,0 @@ -var extend = require('lowscore/extend') -var retry = require('trytryagain') -var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') -var elementInnerText = require('./elementInnerText') -var inspect = require('object-inspect') -var uniq = require('lowscore/uniq') -var debug = require('debug')('browser-monkey') - -function Query () { - this._conditions = [] - this._options = { - visibleOnly: true, - timeout: 1000, - interval: 10, - definitions: { - button: [ - function (monkey, name) { - return monkey.find('button', { exactText: name }) - }, - function (monkey, name) { - return monkey.find('input[type=button]', { exactValue: name }) - }, - function (monkey, name) { - return monkey.find('a', { exactText: name }) - } - ], - field: [ - function (monkey, name) { - return monkey.find('label', { exactText: name }).find('input') - }, - function (monkey, name) { - return monkey.find('input').filter(function (element) { - var labelledBy = element.getAttribute('aria-labelledby') - if (labelledBy) { - var ids = labelledBy.split(/\s+/) - var labels = ids.map(function (id) { - return element.ownerDocument.getElementById(id) - }) - var label = labels.filter(Boolean).map(function (l) { - return elementInnerText(l) - }).join(' ') - return name === label - } - }, 'aria-labelledby ' + inspect({ text: name })) - } - ], - fieldValue: [ - { - set: function (monkey, value) { - return monkey.typeIn(value) - }, - get: function (monkey) { - return monkey.inputValue() - } - }, - { - set: function (monkey, value) { - return monkey.select(value) - }, - get: function (monkey) { - return monkey.selectValue() - } - }, - { - set: function (monkey, value) { - return monkey.check(value) - }, - get: function (monkey) { - return monkey.checked() - } - } - ] - } - } - this._value = undefined -} - -Query.prototype.action = function (action) { - return this.clone(function (clone) { - if (clone._action) { - throw new Error('can only have one action') - } - clone._action = action - }) -} - -function retryFromOptions (options) { - if (options && options.retry) { - return options.retry - } else if (options && (options.timeout || options.interval)) { - return function (fn) { - return retry(fn, options) - } - } else { - return retry - } -} - -Query.prototype.mapAll = function (mapper, description) { - return this.clone(function (clone) { - clone._conditions.push(function (elements) { - return { - description: description, - value: mapper(elements) - } - }) - }) -} - -Query.prototype.map = function (mapper, description) { - return this.clone(function (clone) { - clone._conditions.push(function (elements) { - return { - description: description, - value: elements.map(mapper) - } - }) - }) -} - -Query.prototype.filter = function (filter, description) { - return this.clone(function (clone) { - clone._conditions.push(function (elements) { - return { - description: description, - value: elements.filter(filter) - } - }) - }) -} - -Query.prototype.all = function (queryCreators) { - var self = this - - var queries = queryCreators.map(function (queryCreator) { - return self.subQuery(queryCreator) - }) - - return this.clone(function (clone) { - clone._conditions.push(function (elements) { - var accumulators = queries.map(function (query) { - return query.resolve(elements) - }) - - var value = uniq(Array.prototype.concat.apply([], accumulators.map(function (accumulator) { - return accumulator.value - }))) - - return { - all: accumulators.map(function (accumulator) { return accumulator.transforms }), - value: value - } - }) - }) -} - -Query.prototype.subQuery = function (createQuery) { - var clone = this.clone() - clone._conditions = [] - var query = createQuery(clone) - if (!(query instanceof Query)) { - throw new Error('expected to create query: ' + createQuery) - } - return query -} - -Query.prototype.resolveSubQuery = function (createQuery, value) { - return this.subQuery(createQuery).resolve(value) -} - -Query.prototype.resolve = function (value, options) { - var accumulator = {} - var includeScope = options && options.hasOwnProperty('includeScope') && options.includeScope !== undefined - ? options.includeScope - : false - - function reducer (accumulator, mapper, index) { - if (index === 0) { - accumulator.value = value - accumulator.transforms = [] - - if (includeScope) { - accumulator.transforms.push({ - description: 'scope', - value: value - }) - } - } - var result = mapper(accumulator.value) - accumulator.value = result.value - accumulator.transforms.push(result) - return accumulator - } - - try { - var queryResult = this._conditions.reduce(reducer, accumulator) - if (this._action && !this._actionExecuted) { - this._action(queryResult.value) - this._actionExecuted = true - } - return queryResult - } catch (e) { - if (e instanceof BrowserMonkeyAssertionError) { - e.transforms.unshift.apply(e.transforms, accumulator.transforms) - } - - throw e - } -} - -Query.prototype.race = function (queryCreators) { - var self = this - - var queries = queryCreators.map(function (queryCreator) { - return self.subQuery(queryCreator) - }) - - return this.clone(function (clone) { - clone._conditions.push(function (elements) { - var values = queries.map(function (query) { - try { - return { - value: query.resolve(elements).value - } - } catch (e) { - return { - error: e - } - } - }) - - var firstSuccess = values.find(function (v) { - return !v.error - }) - if (firstSuccess) { - return { - name: 'race', - value: firstSuccess.value - } - } else { - var error = new BrowserMonkeyAssertionError('all queries failed in race') - error.transforms = [{ - raceErrors: values.map(function (value) { - return value.error - }) - }] - throw error - } - }) - }) -} - -Query.prototype.options = function (options) { - if (options) { - return this.clone(function (clone) { - extend(clone._options, options) - }) - } else { - return this._options - } -} - -Query.prototype.then = function () { - var self = this - var retry = retryFromOptions(this._options) - var originalStack = new Error().stack - - var promise = Promise.resolve(retry(function () { - return self.resolve(self._value, { includeScope: true }).value - })).catch(function (error) { - if (error instanceof BrowserMonkeyAssertionError) { - error.message = browserMonkeyAssertionErrorMessage(error) - error.stack = originalStack - - if (debug.enabled) { - debug('assertion error', error.message) - error.transforms.forEach(function (transform) { - debug('found: ' + transform.description, transform.value) - }) - } - } - - throw error - }) - return promise.then.apply(promise, arguments) -} - -function errorMessage (error) { - if (error instanceof BrowserMonkeyAssertionError) { - return browserMonkeyAssertionErrorMessage(error) - } else { - return error.message - } -} - -function browserMonkeyAssertionErrorMessage (error) { - var foundMessage = describeAccumulatedTransforms(error.transforms) - return error.message + ' (found: ' + foundMessage + ')' -} - -function describeAccumulatedTransforms (accumulatedTransforms) { - return accumulatedTransforms.map(function (transform) { - if (transform.all instanceof Array) { - return describeTransform({ - description: 'all (' + transform.all.map(describeAccumulatedTransforms).join(', or ') + ')', - value: transform.value - }) - } else if (transform.raceErrors instanceof Array) { - return 'race between (' + transform.raceErrors.map(errorMessage).join(', and ') + ')' - } else { - return describeTransform(transform) - } - }).join(', ') -} - -function describeTransform (transform) { - var v = transform.value instanceof Array - ? '[' + transform.value.length + ']' - : '(' + typeof transform.value + ')' - return transform.description + ' ' + v -} - -Query.prototype.catch = function (fn) { - return this.then(undefined, fn) -} - -function copySelectorFields (from, to) { - to._conditions = from._conditions.slice() - to._value = from._value - - to._options = extend({}, from._options) - to._options.definitions = {} - Object.keys(from._options.definitions).forEach(function (key) { - to._options.definitions[key] = from._options.definitions[key].slice() - }) -} - -Query.prototype.clone = function (modifier) { - var clone = new this.constructor() - copySelectorFields(this, clone) - if (modifier) { - modifier(clone) - } - return clone -} - -Query.prototype.ensure = function (assertion, name) { - return this.mapAll(function (value) { - assertion(value) - return value - }, name || 'ensure') -} - -Query.prototype.zero = function () { - return this.mapAll(function (results) { - if (results instanceof Array) { - if (results.length !== 0) { - throw new BrowserMonkeyAssertionError('expected no elements') - } else { - return results - } - } else { - return results - } - }, 'zero') -} - -Query.prototype.one = function () { - return this.mapAll(function (results) { - if (results instanceof Array) { - if (results.length === 1) { - return results[0] - } else { - throw new BrowserMonkeyAssertionError('expected one element') - } - } else { - return results - } - }, 'one') -} - -Query.prototype.some = function () { - return this.mapAll(function (results) { - if (results instanceof Array && results.length !== 0) { - return results - } else { - throw new BrowserMonkeyAssertionError('expected some elements') - } - }, 'some') -} - -Query.prototype.input = function (value) { - return this.clone(function (clone) { - clone._value = value - }) -} - -Query.prototype.component = function (methods) { - var self = this - - function Component () { - copySelectorFields(self, this) - } - - Component.prototype = new this.constructor() - Object.keys(methods).forEach(function (method) { - var descriptor = Object.getOwnPropertyDescriptor(methods, method) - if (descriptor) { - Object.defineProperty(Component.prototype, method, descriptor) - } else { - Component.prototype[method] = methods[method] - } - }) - Component.prototype.constructor = Component - - return new Component() -} - -module.exports = Query diff --git a/lib/query.ts b/lib/query.ts new file mode 100644 index 0000000..eb3779a --- /dev/null +++ b/lib/query.ts @@ -0,0 +1,510 @@ +import { ExecutedTransformSequence } from './ExecutedTransformSequence' +import { ExecutedSimpleTransform } from './ExecutedSimpleTransform' +import { ExecutedConcatTransform } from './ExecutedConcatTransform' +import { ExecutedOrTransform } from './ExecutedOrTransform' +import { ExecutedTransformError } from './ExecutedTransformError' +import BrowserMonkeyAssertionError from './BrowserMonkeyAssertionError' +import toExecutedTransform from './toExecutedTransform' +var extend = require('lowscore/extend') +var retry = require('trytryagain') +var elementInnerText = require('./elementInnerText') +var inspect = require('object-inspect') +var uniq = require('lowscore/uniq') +var debug = require('debug')('browser-monkey') +var inputsSelector = require('./inputsSelector') +const elementEnterText = require('./elementEnterText') + +type Transform = (any) => any + +class Query { + private _transforms: Transform[] + private _executedTransforms: ExecutedTransformSequence + private _options: { + visibleOnly: boolean + timeout: number + interval: number + definitions: { + setters: any[] + button: ((monkey: any, name: any) => any)[] + fields: {} + finders: {} + field: ((monkey: any, name: any) => any)[] + fieldValue: { + get: (monkey: any) => any + set: (monkey: any, value: any) => void + setter?: (monkey: any, value: any) => void + }[] + } + } + + private _value: any + private _actionExecuted: any + private _action: (value: any) => void + private _isResolved: boolean + + public constructor () { + this._isResolved = false + this._transforms = [] + this._options = { + visibleOnly: true, + timeout: 1000, + interval: 10, + definitions: { + setters: [], + button: [ + function (monkey, name) { + return monkey.find('button', { exactText: name }) + }, + function (monkey, name) { + return monkey.find('input[type=button]', { exactValue: name }) + }, + function (monkey, name) { + return monkey.find('a', { exactText: name }) + } + ], + + fields: {}, + finders: {}, + + field: [ + function (monkey, name) { + return monkey.find('label', { exactText: name }).find('input') + }, + function (monkey, name) { + return monkey.find('input').filter(function (element) { + var labelledBy = element.getAttribute('aria-labelledby') + if (labelledBy) { + var ids = labelledBy.split(/\s+/) + var labels = ids.map(function (id) { + return element.ownerDocument.getElementById(id) + }) + var label = labels.filter(Boolean).map(function (l) { + return elementInnerText(l) + }).join(' ') + return name === label + } + }, 'aria-labelledby ' + inspect({ text: name })) + } + ], + fieldValue: [ + { + set: function (monkey, value) { + return monkey.typeIn(value) + }, + setter: function (monkey) { + return monkey.is(inputsSelector).element().mapAll(function (element) { + return function (value) { + if (typeof value !== 'string') { + throw new Error('expected string as argument to typeIn') + } + debug('typeIn', element, value) + elementEnterText(element, value) + } + }) + }, + get: function (monkey) { + return monkey.inputValue() + } + }, + { + set: function (monkey, value) { + return monkey.select(value) + }, + get: function (monkey) { + return monkey.selectValue() + } + }, + { + set: function (monkey, value) { + return monkey.check(value) + }, + get: function (monkey) { + return monkey.checked() + } + } + ] + } + } + this._value = undefined + } + + public transform (transform: Transform): this { + if (this._isResolved) { + return this.clone(clone => { + const executedTransform = toExecutedTransform(transform.call(this, this._value)) + clone._executedTransforms.addTransform(executedTransform) + clone._value = executedTransform.value + }) + } else { + return this.clone(clone => clone._transforms.push(transform)) + } + } + + public assertion (assertion: (any) => void): this { + return this.transform((value) => { + assertion.call(this, value) + return value + }) + } + + public action (action: (any) => void): any { + if (this._action) { + throw new Error('can only have one action') + } + + if (this._isResolved) { + action.call(this, this._value) + } else { + return this.clone(clone => { + clone._action = action + }) + } + } + + public value (): any { + if (this._isResolved) { + return this._value + } else { + return toExecutedTransform(this.execute()).value + } + } + + public resolve (): this { + if (!this._isResolved) { + this._executedTransforms = this.execute() + this._value = this._executedTransforms.value + this._isResolved = true + return this + } else { + return this + } + } + + public map (mapper, description): this { + return this.transform((elements) => { + return new ExecutedSimpleTransform(elements.map(mapper), description) + }) + } + + public filter (filter, description): this { + return this.transform((elements) => { + return new ExecutedSimpleTransform(elements.filter(filter), description) + }) + } + + public concat (queryCreators): this { + var queries = queryCreators.map(queryCreator => { + return this.subQuery(queryCreator) + }) + + return this.transform((elements) => { + var transforms = queries.map(query => { + const transforms = new ExecutedTransformSequence(elements) + query.executeQuery(transforms) + return transforms + }) + + var value = uniq(Array.prototype.concat.apply([], transforms.map(t => t.value))) + return new ExecutedConcatTransform(value, transforms) + }) + } + + private subQuery (createQuery) { + var clone = this.clone() + clone._transforms = [] + var query = createQuery(clone) + if (!(query instanceof Query)) { + throw new Error('expected to create query: ' + createQuery) + } + return query + } + + public error (message: string, { expected, actual }): void { + if (this._isResolved) { + throw new BrowserMonkeyAssertionError(message, { expected, actual, executedTransforms: this._executedTransforms }) + } else { + throw new BrowserMonkeyAssertionError(message, { expected, actual }) + } + } + + private resolveSubQuery (createQuery, value) { + return this.subQuery(createQuery).resolve() + } + + private executeQuery (transformSequence: ExecutedTransformSequence) { + const query = this + + function reducer (transform) { + const executedTransform = toExecutedTransform(transform.call(query, transformSequence.value)) + transformSequence.addTransform(executedTransform) + } + + try { + this._transforms.forEach(reducer) + if (this._action && !this._actionExecuted) { + this._action(transformSequence.value) + this._actionExecuted = true + } + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + e.executedTransforms.prepend(transformSequence) + } + + throw e + } + } + + private execute (): ExecutedTransformSequence { + if (!this._isResolved) { + const transformSequence = new ExecutedTransformSequence(this._value) + this.executeQuery(transformSequence) + return transformSequence + } else { + return this._executedTransforms + } + } + + private race (queryCreators): this { + return this.transform(() => { + const resolved = this.clone().resolve() + + var values = queryCreators.map(query => { + try { + return { + value: query(resolved).execute() + } + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + return { + error: new ExecutedTransformError(e) + } + } else { + throw e + } + } + }) + + var firstSuccessIndex = values.findIndex(v => { + return !v.error + }) + + const firstSuccess = values[firstSuccessIndex] + + const transform = new ExecutedOrTransform((firstSuccess && firstSuccess.value.value) || [], firstSuccessIndex, values.map(v => v.error || v.value)) + + if (firstSuccess) { + return transform + } else { + var error = new BrowserMonkeyAssertionError('all queries failed in race') + error.executedTransforms.addTransform(transform) + throw error + } + }) + } + + public query (selector: string) { + const field = this._options.definitions.fields[selector] + + if (field) { + return field(this) + } else { + const [finderName, value] = selector.split(':') + if (this[finderName]) { + return this[finderName](value.trim()) + } else { + throw new Error('no such finder: ' + finderName) + } + } + } + + public options (options) { + if (options) { + return this.clone(function (clone) { + extend(clone._options, options) + }) + } else { + return this._options + } + } + + public then (...args) { + var self = this + var retry = retryFromOptions(this._options) + var originalStack = new Error().stack + + var promise = Promise.resolve(retry(function () { + return self.execute().value + })).catch(function (error) { + if (error instanceof BrowserMonkeyAssertionError) { + error.rewriteMessage() + error.stack = originalStack + + if (debug.enabled) { + debug('assertion error', error.message) + error.executedTransforms.transforms.forEach(function (transform) { + transform.print() + }) + } + } + + throw error + }) + return promise.then.apply(promise, args) + } + + public catch (fn): Promise { + return this.then(undefined, fn) + } + + public clone (modifier?: (clone: this) => void): this { + var clone = new (this.constructor as any)() + copySelectorFields(this, clone) + if (modifier) { + modifier(clone) + } + return clone + } + + public ensure (assertion, name): this { + return this.transform(function (value) { + assertion(value) + return new ExecutedSimpleTransform(value, name || 'ensure') + }) + } + + public zero (): this { + return this.transform(function (results) { + if (results instanceof Array) { + if (results.length !== 0) { + throw new BrowserMonkeyAssertionError('expected no elements') + } else { + return new ExecutedSimpleTransform(results, 'zero') + } + } else { + throw new BrowserMonkeyAssertionError('expected array of no elements') + } + }) + } + + public one (): this { + return this.transform(function (results) { + if (results instanceof Array) { + if (results.length === 1) { + return new ExecutedSimpleTransform(results[0], 'one') + } else { + throw new BrowserMonkeyAssertionError('expected one element') + } + } else { + return new ExecutedSimpleTransform(results, 'one') + } + }) + } + + public some (): this { + return this.transform(function (results) { + if (results instanceof Array && results.length !== 0) { + return new ExecutedSimpleTransform(results, 'some') + } else { + throw new BrowserMonkeyAssertionError('expected some elements') + } + }) + } + + public input (value): this { + return this.clone(function (clone) { + clone._value = value + }) + } + + public component (methods): this { + var self = this + + function Component () { + copySelectorFields(self, this) + } + + Component.prototype = new (this.constructor as any)() + Object.keys(methods).forEach(function (method) { + var descriptor = Object.getOwnPropertyDescriptor(methods, method) + if (descriptor) { + Object.defineProperty(Component.prototype, method, descriptor) + } else { + Component.prototype[method] = methods[method] + } + }) + Component.prototype.constructor = Component + + return new Component() + } +} + +function copySelectorFields (from, to) { + to._transforms = from._transforms.slice() + to._value = from._value + + to._isResolved = from._isResolved + if (from._isResolved) { + to._executedTransforms = from._executedTransforms.clone() + } + + to._options = extend({}, from._options) + to._options.definitions = {} + + Object.keys(from._options.definitions).forEach(function (key) { + if (from._options.definitions[key] instanceof Array) { + to._options.definitions[key] = from._options.definitions[key].slice() + } else { + to._options.definitions[key] = Object.assign({}, from._options.definitions[key]) + } + }) +} + +function errorMessage (error) { + if (error instanceof BrowserMonkeyAssertionError) { + return browserMonkeyAssertionErrorMessage(error) + } else { + return error.message + } +} + +function browserMonkeyAssertionErrorMessage (error: BrowserMonkeyAssertionError) { + var foundMessage = describeAccumulatedTransforms(error.executedTransforms) + return error.message + ' (found: ' + foundMessage + ')' +} + +function describeAccumulatedTransforms (accumulatedTransforms: ExecutedTransformSequence) { + return accumulatedTransforms.transforms.map(function (transform) { + return transform.renderError() + // if (transform.concat instanceof Array) { + // return describeTransform({ + // description: 'concat (' + transform.concat.map(describeAccumulatedTransforms).join(', or ') + ')', + // value: transform.value + // }) + // } else if (transform.raceErrors instanceof Array) { + // return 'race between (' + transform.raceErrors.map(errorMessage).join(', and ') + ')' + // } else { + // return describeTransform(transform) + // } + }).join(', ') +} + +function describeTransform (transform) { + var v = transform.value instanceof Array + ? '[' + transform.value.length + ']' + : '(' + typeof transform.value + ')' + return transform.description + ' ' + v +} + +function retryFromOptions (options) { + if (options && options.retry) { + return options.retry + } else if (options && (options.timeout || options.interval)) { + return function (fn) { + return retry(fn, options) + } + } else { + return retry + } +} + +module.exports = Query diff --git a/lib/set.js b/lib/set.js new file mode 100644 index 0000000..23c2088 --- /dev/null +++ b/lib/set.js @@ -0,0 +1,166 @@ +var inputsSelector = require('./inputsSelector') +var debug = require('debug')('browser-monkey') +var elementEnterText = require('./elementEnterText') +var elementInnerText = require('./elementInnerText') +const inspect = require('object-inspect') + +function assertLength (query, length) { + const actualLength = query.value().length + if (actualLength !== length) { + const error = query.error('expected ' + length + ' elements', { expected: length, actual: actualLength }) + error.executedTransforms.prepend(query._executedTransforms) + throw error + } +} + +function mapModel (query, elements, model, actions) { + function map (query, model) { + if (model instanceof Array) { + assertLength(query, model.length) + + if (model.length === 0) { + return function () {} + } else { + model.forEach((item, index) => { + map(query.index(index), item) + }) + } + } else if (model.constructor === Object) { + assertLength(query, 1) + Object.entries(model).forEach(([selector, value]) => { + map(query.query(selector), value) + }) + } else if (typeof model === 'function') { + actions.function(query, model) + } else { + assertLength(query, 1) + + actions.value(query, model) + } + } + + map(query, model) + + return actions +} + +module.exports = { + set: function (model) { + return this.action(function (elements) { + const setters = [] + + const actions = { + value: (query, model) => { + var setter = query.setter().value() + setters.push(() => setter(model)) + }, + function: (query, model) => { + setters.push(() => model(query)) + } + } + + const clone = this.clone().resolve() + mapModel(clone, elements, model, actions) + + setters.forEach(set => { + set() + }) + }) + }, + + assert: function (model) { + return this.assertion(function (elements) { + const actions = { + value: (query, model) => { + var asserter = query.asserter().value() + query.assertion(() => asserter(model)) + }, + function: (query, model) => { + model(query) + } + } + + const clone = this.clone().resolve() + mapModel(clone, elements, model, actions) + }) + }, + + index: function (index) { + return this.transform(function (elements) { + return [elements[index]] + }, 'index ' + index) + }, + + length: function (length) { + return this.assertion(function (results) { + const actualLength = results.length + if (actualLength !== length) { + this.error('expected ' + length + ' elements', { expected: length, actual: actualLength }) + } + }) + }, + + query: function (selector) { + const field = this._options.definitions.fields[selector] + + if (field) { + return field(this) + } else { + const [finderName, value] = selector.split(':') + const finder = this._options.definitions.finders[finderName] + return finder(this, value.trim()) + } + }, + + define: function (name, fieldFinder) { + this._options.definitions.fields[name] = fieldFinder + }, + + defineFinder: function (name, finder) { + this._options.definitions.finders[name] = finder + }, + + setter: function () { + return this.race(this._options.definitions.setters) + }, + + asserter: function () { + return this.race(this._options.definitions.asserters) + }, + + installSetters: function () { + this._options.definitions.setters = [ + function (query) { + return query.is(inputsSelector).element().transform(function (element) { + return function (text) { + if (typeof text !== 'string') { + throw new Error('expected string as argument to set input') + } + debug('set', element, text) + elementEnterText(element, text) + } + }) + } + ] + + this._options.definitions.asserters = [ + function (query) { + return query.element().transform(function (element) { + var self = this + return function (expected) { + const actual = elementInnerText(element) + if (actual !== expected) { + self.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) + } + } + }) + } + ] + + this._options.definitions.finders.css = function (query, css) { + return query.css(css) + } + + return this + } +} diff --git a/lib/toExecutedTransform.ts b/lib/toExecutedTransform.ts new file mode 100644 index 0000000..f060ee3 --- /dev/null +++ b/lib/toExecutedTransform.ts @@ -0,0 +1,10 @@ +import { ExecutedTransform } from './ExecutedTransform' +import { ExecutedSimpleTransform } from './ExecutedSimpleTransform' + +export default function toExecutedTransform (value: any): ExecutedTransform { + if (value instanceof ExecutedTransform) { + return value + } else { + return new ExecutedSimpleTransform(value) + } +} diff --git a/lib/transformation.ts b/lib/transformation.ts new file mode 100644 index 0000000..1c052b8 --- /dev/null +++ b/lib/transformation.ts @@ -0,0 +1,6 @@ +module.exports = class Transformation { + constructor (transform : Transform, parent? : Transformation) { + this.transform = transform + this.parent = parent + } +} \ No newline at end of file diff --git a/package.json b/package.json index 1e4aace..6ec69c0 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "description": "reliable dom testing", "main": "index.js", "scripts": { - "test": "standard && yarn test-browser-compatibility && yarn electron-mocha && yarn karma", + "test": "yarn test-browser-compatibility && yarn test-electron-mocha && yarn karma && eslint .", "karma": "karma start --single-run", - "electron-mocha": "electron-mocha -r @babel/register --renderer test/*Spec.js test/**/*Spec.js", + "test-electron-mocha": "yarn electron-mocha test/*Spec.js test/**/*Spec.js", + "electron-mocha": "electron-mocha -r test/register --renderer", "test-browser-compatibility": "browserify ./index.js > /dev/null", "update-readme-example-links": "node ./docs/update-readme-example-links.js" }, @@ -30,6 +31,7 @@ "express": "^4.16.4", "httpism": "^3.11.0", "hyperdom": "^0.13.0", + "iframe-resizer": "^4.1.1", "karma": "3.1.4", "karma-browserify": "^6.0.0", "karma-browserstack-launcher": "https://github.com/karma-runner/karma-browserstack-launcher", @@ -45,17 +47,27 @@ "react": "^16.7.0", "react-dom": "^16.7.0", "server-destroy": "^1.0.1", - "standard": "^12.0.1", + "ts-node": "^8.1.0", + "typescript": "^3.4.4", "vdom-query": "https://github.com/featurist/vdom-query", "virtual-dom": "^2.1.1", "watchify": "^3.11.0" }, "dependencies": { + "@babel/preset-typescript": "^7.3.3", + "@typescript-eslint/eslint-plugin": "^1.7.0", "array.prototype.find": "^2.0.4", "chai": "4.2.0", "debug-es5": "^4.1.0", "deep-equal": "1.0.1", "error-stack-parser": "2.0.2", + "eslint": "^5.16.0", + "eslint-config-standard": "^12.0.0", + "eslint-plugin-import": "^2.17.2", + "eslint-plugin-mocha": "^5.3.0", + "eslint-plugin-node": "^9.0.1", + "eslint-plugin-promise": "^4.1.1", + "eslint-plugin-standard": "^4.0.0", "global": "^4.3.2", "hobostyle": "1.0.0", "jquery": "^3.3.1", diff --git a/test/assertSpec.js b/test/assertSpec.js new file mode 100644 index 0000000..eef7e59 --- /dev/null +++ b/test/assertSpec.js @@ -0,0 +1,153 @@ +const describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +var BrowserMonkeyAssertionError = require('../lib/BrowserMonkeyAssertionError').default + +describe('assert', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + + describe('objects', function () { + it('passes when the fields are there and have the expected values', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + await browser.assert({ + 'css:h1': 'Title', + 'css:.content': 'The Content' + }) + }) + + it("fails when one field isn't there", async () => { + assembly.insertHtml(` +
+

Title

+
+ `) + + await assembly.assertRejection(browser.assert({ + 'css:h1': 'Title', + 'css:.content': 'The Content' + }), '.content') + }) + }) + + describe('arrays', function () { + it('passes when the fields are there and have the expected values', async () => { + assembly.insertHtml(` +
+ Result 1 +
+
+ Result 2 +
+
+ Result 3 +
+ `) + + await browser.assert({ + 'css:.result': [ + 'Result 1', + 'Result 2', + 'Result 3' + ] + }) + }) + + it('fails when the number of items is wrong', async () => { + assembly.insertHtml(` +
+ Result 1 +
+
+ Result 2 +
+
+ Result 3 +
+ `) + + await assembly.assertRejection(browser.assert({ + 'css:.result': [ + 'Result 1', + 'Result 2' + ] + }), 'expected 2 elements') + }) + + it('can assert objects as items', async () => { + assembly.insertHtml(` +
+

Title 1

+
Description 1
+
+
+

Title 2

+
Description 2
+
+ `) + + await browser.assert({ + 'css:.result': [ + { + 'css: h3': 'Title 1', + 'css: .description': 'Description 1' + }, + { + 'css: h3': 'Title 2', + 'css: .description': 'Description 2' + } + ] + }) + }) + }) + + describe('functions', () => { + it('a function can assert something specific about the element found', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + await browser.assert({ + 'css:h1': 'Title', + 'css:.content': query => { + if (!/Content/.test(query.element().value().innerText)) { + throw new BrowserMonkeyAssertionError('asdf') + } + } + }) + }) + + it('fails when query from function fails', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + await assembly.assertRejection(browser.assert({ + 'css:h1': 'Title', + 'css:.content': query => { + if (/Content/.test(query.element().value().innerText)) { + throw new BrowserMonkeyAssertionError('asdf') + } + } + }), 'asdf') + }) + }) + }) +}) diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index e69de29..0000000 diff --git a/test/querySpec.js b/test/querySpec.js index 89d1069..69f6649 100644 --- a/test/querySpec.js +++ b/test/querySpec.js @@ -1,6 +1,7 @@ var describeAssemblies = require('./describeAssemblies') const expect = require('must') const DomAssembly = require('./assemblies/DomAssembly') +const BrowserMonkeyAssertionError = require('../lib/BrowserMonkeyAssertionError').default describe('query', () => { describeAssemblies([DomAssembly], Assembly => { @@ -13,7 +14,7 @@ describe('query', () => { }) it('can find an element by DOM selector', async () => { - const selectedElementPromise = browserMonkey.find('.test').one() + const selectedElementPromise = browserMonkey.css('.test').one() const insertedElementPromise = assembly.eventuallyInsertHtml( `
` @@ -66,7 +67,7 @@ describe('query', () => { it('passes through the maps', async () => { const monkey = browserMonkey .input(1) - .mapAll(x => x + 1) + .transform(x => x + 1) .component({ aMethod: function () { return 'aMethod' } }) @@ -85,7 +86,7 @@ describe('query', () => { `) const contacts = browserMonkey - .find('.contact') + .css('.contact') .zero() assembly.eventuallyDeleteHtml('.contact') @@ -106,11 +107,10 @@ describe('query', () => { `) const contacts = browserMonkey - .find('.contact') + .css('.contact') .zero() assembly.eventuallyDeleteHtml('.message') - await expect(contacts).reject.with.error(/expected no elements/) }) }) @@ -118,7 +118,7 @@ describe('query', () => { describe('one', () => { it('when there is one element, selects it', async () => { const contacts = browserMonkey - .find('.contact') + .css('.contact') .one() assembly.eventuallyInsertHtml(` @@ -133,7 +133,7 @@ describe('query', () => { it('when there is more than one element, throws an error', async () => { const contacts = browserMonkey - .find('.contact') + .css('.contact') .one() assembly.eventuallyInsertHtml(` @@ -152,7 +152,7 @@ describe('query', () => { it('when there no elements, throws an error', async () => { const contacts = browserMonkey - .find('.contact') + .css('.contact') .one() assembly.eventuallyInsertHtml(` @@ -165,7 +165,7 @@ describe('query', () => { it('when there is already one result, no array, selects it', async () => { const contacts = browserMonkey - .find('.contact') + .css('.contact') .one() .one() @@ -183,7 +183,7 @@ describe('query', () => { describe('some', () => { it('when there are one or more elements, selects them', async () => { const contacts = browserMonkey - .find('.contact') + .css('.contact') .some() assembly.eventuallyInsertHtml(` @@ -202,7 +202,7 @@ describe('query', () => { it('when there no elements, throws an error', async () => { const contacts = browserMonkey - .find('.contact') + .css('.contact') .some() assembly.eventuallyInsertHtml(` @@ -217,7 +217,7 @@ describe('query', () => { describe('map', () => { it('can map elements', async () => { const contacts = browserMonkey - .find('.contact') + .css('.contact') .map(contact => ({ name: contact.querySelector('.name').innerText, address: contact.querySelector('.address').innerText @@ -249,10 +249,10 @@ describe('query', () => { }) describe('input', () => { - it('input sets the input used in mapAll', async () => { + it('input sets the input used in transform', async () => { const monkey = browserMonkey .input('a') - .mapAll(x => `input: ${x}`) + .transform(x => `input: ${x}`) expect(await monkey).to.eql('input: a') }) @@ -295,11 +295,11 @@ describe('query', () => { }) }) - describe('mapAll', () => { + describe('transform', () => { it('can map all the elements', async () => { const contacts = browserMonkey - .find('.name') - .mapAll(names => { + .css('.name') + .transform(names => { return names.map(contact => contact.innerText).join(', ') }) .ensure(x => expect(x).to.not.be.empty()) @@ -316,7 +316,7 @@ describe('query', () => { describe('filter', () => { it('can filter elements', async () => { const contacts = browserMonkey - .find('.contact') + .css('.contact') .filter(contact => { return contact.querySelector('.name').innerText === 'Sally' }) @@ -345,7 +345,7 @@ describe('query', () => {
A
`) - const action = browserMonkey.find('div').action(function () { + const action = browserMonkey.css('div').action(function () { actionExecuted++ }) @@ -364,7 +364,7 @@ describe('query', () => { `) let givenElements - const action = browserMonkey.find('div').action(function (els) { + const action = browserMonkey.css('div').action(function (els) { givenElements = els }) @@ -377,25 +377,26 @@ describe('query', () => { describe('errors', () => { it('shows what it was able to map', async () => { const name = browserMonkey - .find('.container') - .find('.contact') - .find('.name') + .css('.container') + .css('.contact') + .css('.name') .one() assembly.eventuallyInsertHtml(`
+ no .name
`) - await expect(name).to.reject.with.error("expected one element (found: scope [1], find('.container') [1], find('.contact') [2], find('.name') [0])") + await expect(name).to.reject.with.error("expected one element (found: find('.container') [1], find('.contact') [2], find('.name') [0])") }) }) - describe('all', () => { + describe('concat', () => { it('finds either', async () => { const a = assembly.insertHtml(`
A
@@ -407,15 +408,15 @@ describe('query', () => {
C
`) - const elementsFound = await browserMonkey.all([ - b => b.find('.a'), - b => b.find('.b') + const elementsFound = await browserMonkey.concat([ + b => b.css('.a'), + b => b.css('.b') ]) expect(elementsFound).to.eql([a, b]) }) - it('throws error with finders used in all', async () => { + it('throws error with finders used in concat', async () => { assembly.insertHtml(`
A
`) @@ -426,20 +427,20 @@ describe('query', () => {
C
`) - const promise = browserMonkey.all([ - b => b.find('.a'), - b => b.find('.b') - ]).find('.child').some() + const promise = browserMonkey.concat([ + b => b.css('.a'), + b => b.css('.b') + ]).css('.child').some() - return expect(promise).to.reject.with.error("expected some elements (found: scope [1], all (find('.a') [1], or find('.b') [1]) [2], find('.child') [0])") + return expect(promise).to.reject.with.error("expected some elements (found: concat (find('.a') [1], find('.b') [1]) [2], find('.child') [0])") }) }) describe('race', () => { it('finds the first of two or more queries', async () => { const promise = browserMonkey.race([ - b => b.find('.a').one(), - b => b.find('.b').one() + b => b.css('.a').one(), + b => b.css('.b').one() ]) const bPromise = assembly.eventuallyInsertHtml('
B
') @@ -452,12 +453,12 @@ describe('query', () => { it('returns descriptions of all queries used when none are successful', async () => { assembly.insertHtml('
content
C
') - const promise = browserMonkey.find('.content').race([ - b => b.find('.a').some(), - b => b.find('.b').some() + const promise = browserMonkey.css('.content').race([ + b => b.css('.a').some(), + b => b.css('.b').some() ]) - await expect(promise).to.reject.with.error("all queries failed in race (found: scope [1], find('.content') [1], race between (expected some elements (found: find('.a') [0]), and expected some elements (found: find('.b') [0])))") + await expect(promise).to.reject.with.error("all queries failed in race (found: find('.content') [1], or(expected some elements (found: find('.a') [0]), expected some elements (found: find('.b') [0])) [0])") }) }) }) diff --git a/test/register.js b/test/register.js new file mode 100644 index 0000000..3476328 --- /dev/null +++ b/test/register.js @@ -0,0 +1,3 @@ +require('@babel/register')({ + extensions: ['.js', '.jsx', '.ts', '.tsx'] +}) diff --git a/test/setSpec.js b/test/setSpec.js new file mode 100644 index 0000000..6ab4ea0 --- /dev/null +++ b/test/setSpec.js @@ -0,0 +1,246 @@ +const describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +const demand = require('must') + +describe('set', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + + it('can set fields by CSS', async () => { + assembly.insertHtml(` +
+ + + +
+ `) + + await browser.set({ + 'css:.address': '7 Lola St', + 'css:.phone': '123123123', + 'css:.first-name': 'Barry' + }) + + demand(assembly.find('.address').value).to.equal('7 Lola St') + demand(assembly.find('.phone').value).to.equal('123123123') + demand(assembly.find('.first-name').value).to.equal('Barry') + }) + + describe('objects', function () { + it('can set deep fields by CSS', async () => { + assembly.insertHtml(` +
+
+ + + + +
+ + +
+ `) + + await browser.set({ + 'css:.address': { + 'css:.street': '7 Lola St', + 'css:.city': 'Frisby City', + 'css:.country': 'Atlantis', + 'css:.postcode': '12345' + }, + 'css:.phone': '123123123', + 'css:.first-name': 'Barry' + }) + + demand(assembly.find('.address .street').value).to.equal('7 Lola St') + demand(assembly.find('.address .city').value).to.equal('Frisby City') + demand(assembly.find('.address .country').value).to.equal('Atlantis') + demand(assembly.find('.address .postcode').value).to.equal('12345') + demand(assembly.find('.phone').value).to.equal('123123123') + demand(assembly.find('.first-name').value).to.equal('Barry') + }) + + it('deep fields disambiguate', async () => { + assembly.insertHtml(` +
+
+ +
+ +
+ `) + + await browser.set({ + 'css:.address': { + 'css:.street': '7 Lola St' + } + }) + + demand(assembly.find('.address .street').value).to.equal('7 Lola St') + demand(assembly.find('form > .street').value).to.equal('') + }) + }) + + describe('arrays', function () { + it('arrays can set multiple matching elements', async () => { + assembly.insertHtml(` +
+
+ +
+
+ +
+
+ +
+
+ `) + + await browser.set({ + 'css:.address': [ + { 'css:.street': '1' }, + { 'css:.street': '2' }, + { 'css:.street': '3' } + ] + }) + + demand(assembly.find('.address:nth-child(1) .street').value).to.equal('1') + demand(assembly.find('.address:nth-child(2) .street').value).to.equal('2') + demand(assembly.find('.address:nth-child(3) .street').value).to.equal('3') + }) + + it('arrays must have exact number of matching elements', async () => { + assembly.insertHtml(` +
+
+ +
+
+ +
+
+ +
+
+ `) + + await assembly.assertRejection( + browser.set({ + 'css:.address': [ + { 'css:.street': '1' }, + { 'css:.street': '2' } + ] + }), + 'expected 2 elements' + ) + + demand(assembly.find('.address:nth-child(1) .street').value).to.equal('') + demand(assembly.find('.address:nth-child(2) .street').value).to.equal('') + demand(assembly.find('.address:nth-child(3) .street').value).to.equal('') + }) + + it('empty arrays assert no elements', async () => { + assembly.insertHtml(` +
+
+ +
+
+ `) + + await assembly.assertRejection( + browser.set({ + 'css:.address': [ + ] + }), + 'expected 0 elements' + ) + + demand(assembly.find('.address:nth-child(1) .street').value).to.equal('') + }) + }) + + describe('functions', () => { + it('can return query to execute', async () => { + assembly.insertHtml(` +
+ +
+ `) + + await browser.set({ + 'css: .street': streetQuery => { + streetQuery.element().value().innerText = 'hi' + } + }) + + demand(assembly.find('.street').innerText).to.equal('hi') + }) + }) + + it('setting fields is atomic', async () => { + assembly.insertHtml(` +
+ + +
+ `) + + const promise = browser.set({ + 'css:.street': '7 Lola St', + 'css:.phone': '123123123', + 'css:.first-name': 'Barry' + }) + await assembly.assertRejection(promise, 'expected 1 elements') + + demand(assembly.find('.phone').value).to.equal('') + demand(assembly.find('.first-name').value).to.equal('') + }) + + it('can define a named field', async function () { + assembly.insertHtml(` +
+ + +
+ `) + + browser.define('phone', b => b.css('.phone')) + browser.define('firstName', b => b.css('.first-name')) + + await browser.set({ + phone: '123123123', + firstName: 'Barry' + }) + + demand(assembly.find('.phone').value).to.equal('123123123') + demand(assembly.find('.first-name').value).to.equal('Barry') + }) + + it('can define a named finder', async function () { + assembly.insertHtml(` +
+ + +
+ `) + + browser.defineFinder('form', (b, name) => b.css('* [name=' + JSON.stringify(name) + ']')) + + await browser.set({ + 'form: phone': '123123123', + 'form: firstname': 'Barry' + }) + + demand(assembly.find('.phone').value).to.equal('123123123') + demand(assembly.find('.first-name').value).to.equal('Barry') + }) + }) +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1d6541a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "lib": ["es2015"], + "noImplicitAny": false, + + "emitDecoratorMetadata": true, + "module": "commonjs", + "target": "ES5", + "outDir": "lib", + "rootDir": "lib" + } +} diff --git a/yarn.lock b/yarn.lock index 66516f8..c605092 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,6 +54,17 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" + integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== + dependencies: + "@babel/types" "^7.4.4" + jsesc "^2.5.1" + lodash "^4.17.11" + source-map "^0.5.0" + trim-right "^1.0.1" + "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -86,6 +97,18 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-create-class-features-plugin@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz#fc3d690af6554cc9efc607364a82d48f58736dba" + integrity sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-member-expression-to-functions" "^7.0.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.4.4" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/helper-define-map@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c" @@ -192,6 +215,16 @@ "@babel/traverse" "^7.2.3" "@babel/types" "^7.0.0" +"@babel/helper-replace-supers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27" + integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.0.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + "@babel/helper-simple-access@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" @@ -207,6 +240,13 @@ dependencies: "@babel/types" "^7.0.0" +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + "@babel/helper-wrap-function@^7.1.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" @@ -240,6 +280,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.3.tgz#32f5df65744b70888d17872ec106b02434ba1489" integrity sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA== +"@babel/parser@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" + integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== + "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -249,6 +294,14 @@ "@babel/helper-remap-async-to-generator" "^7.1.0" "@babel/plugin-syntax-async-generators" "^7.2.0" +"@babel/plugin-proposal-class-properties@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce" + integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.4.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" @@ -317,6 +370,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-typescript@^7.2.0": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz#a7cc3f66119a9f7ebe2de5383cce193473d65991" + integrity sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-arrow-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" @@ -555,6 +615,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-typescript@^7.3.2": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.4.5.tgz#ab3351ba35307b79981993536c93ff8be050ba28" + integrity sha512-RPB/YeGr4ZrFKNwfuQRlMf2lxoCUaU01MTw39/OFE/RiL8HDjtn68BwEPft1P7JN4akyEmjGWAMNldOV7o9V2g== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-typescript" "^7.2.0" + "@babel/plugin-transform-unicode-regex@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b" @@ -630,6 +698,14 @@ "@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0" +"@babel/preset-typescript@^7.3.3": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz#88669911053fa16b2b276ea2ede2ca603b3f307a" + integrity sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-typescript" "^7.3.2" + "@babel/register@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.0.0.tgz#fa634bae1bfa429f60615b754fc1f1d745edd827" @@ -667,6 +743,21 @@ globals "^11.1.0" lodash "^4.17.10" +"@babel/traverse@^7.4.4": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216" + integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.4.5" + "@babel/types" "^7.4.4" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.11" + "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.2.tgz#44e10fc24e33af524488b716cdaee5360ea8ed1e" @@ -676,11 +767,49 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" +"@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + "@types/node@^10.12.18": version "10.12.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== +"@typescript-eslint/eslint-plugin@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.7.0.tgz#570e45dc84fb97852e363f1e00f47e604a0b8bcc" + integrity sha512-NUSz1aTlIzzTjFFVFyzrbo8oFjHg3K/M9MzYByqbMCxeFdErhLAcGITVfXzSz+Yvp5OOpMu3HkIttB0NyKl54Q== + dependencies: + "@typescript-eslint/parser" "1.7.0" + "@typescript-eslint/typescript-estree" "1.7.0" + eslint-utils "^1.3.1" + regexpp "^2.0.1" + requireindex "^1.2.0" + tsutils "^3.7.0" + +"@typescript-eslint/parser@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.7.0.tgz#c3ea0d158349ceefbb6da95b5b09924b75357851" + integrity sha512-1QFKxs2V940372srm12ovSE683afqc1jB6zF/f8iKhgLz1yoSjYeGHipasao33VXKI+0a/ob9okeogGdKGvvlg== + dependencies: + "@typescript-eslint/typescript-estree" "1.7.0" + eslint-scope "^4.0.0" + eslint-visitor-keys "^1.0.0" + +"@typescript-eslint/typescript-estree@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.7.0.tgz#59ec02f5371964da1cc679dba7b878a417bc8c60" + integrity sha512-K5uedUxVmlYrVkFbyV3htDipvLqTE3QMOUQEHYJaKtgzxj6r7c5Ca/DG1tGgFxX+fsbi9nDIrf4arq7Ib7H/Yw== + dependencies: + lodash.unescape "4.0.1" + semver "5.5.0" + JSONStream@^1.0.3: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -732,6 +861,11 @@ acorn@^6.0.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.5.tgz#81730c0815f3f3b34d8efa95cb7430965f4d887a" integrity sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg== +acorn@^6.0.7: + version "6.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" + integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -751,12 +885,7 @@ agentkeepalive@^3.3.0: dependencies: humanize-ms "^1.2.1" -ajv-keywords@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" - integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= - -ajv@^6.0.1, ajv@^6.5.0, ajv@^6.5.5: +ajv@^6.5.5: version "6.6.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.2.tgz#caceccf474bf3fc3ce3b147443711a24063cc30d" integrity sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g== @@ -766,6 +895,16 @@ ajv@^6.0.1, ajv@^6.5.0, ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.9.1: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -788,6 +927,11 @@ ansi-escapes@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -798,12 +942,12 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-styles@^3.2.1: +ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== @@ -844,6 +988,11 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arg@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" + integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -986,6 +1135,11 @@ ast-types@^0.7.0: resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.7.8.tgz#902d2e0d60d071bdcd46dc115e1809ed11c138a9" integrity sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk= +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -1024,15 +1178,6 @@ axios@^0.16.2: follow-redirects "^1.2.3" is-buffer "^1.1.5" -babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - babelify@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/babelify/-/babelify-10.0.0.tgz#fe73b1a22583f06680d8d072e25a1e0d1d1d7fb5" @@ -1519,22 +1664,15 @@ cached-path-relative@^1.0.0: resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg== -caller-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" - integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8= - dependencies: - callsites "^0.2.0" - callsite@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= -callsites@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" - integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase-keys@^2.0.0: version "2.1.0" @@ -1593,18 +1731,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1618,6 +1745,11 @@ chardet@^0.4.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -1677,11 +1809,6 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" -circular-json@^0.3.1: - version "0.3.3" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" - integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== - circular-json@^0.5.5: version "0.5.9" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" @@ -2117,11 +2244,6 @@ debug-es5@^4.1.0: dependencies: ms "^2.1.1" -debug-log@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" - integrity sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8= - debug@2.6.9, debug@^2.1.2, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2143,7 +2265,7 @@ debug@^3.0.0, debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.1.0: +debug@^4.0.1, debug@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -2221,18 +2343,6 @@ defined@^1.0.0: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= -deglob@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/deglob/-/deglob-2.1.1.tgz#d268e168727799862e8eac07042e165957c1f3be" - integrity sha512-2kjwuGGonL7gWE1XU4Fv79+vVzpoQCl0V+boMwWtOQJV2AGDabCwez++nB1Nli/8BabAfZQ/UuHPlp6AymKdWw== - dependencies: - find-root "^1.0.0" - glob "^7.0.5" - ignore "^3.0.9" - pkg-config "^1.1.0" - run-parallel "^1.1.2" - uniq "^1.0.1" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -2295,7 +2405,7 @@ di@^0.0.1: resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= -diff@3.5.0: +diff@3.5.0, diff@^3.1.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== @@ -2317,10 +2427,10 @@ doctrine@1.5.0: esutils "^2.0.2" isarray "^1.0.0" -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== dependencies: esutils "^2.0.2" @@ -2491,6 +2601,11 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.0" +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -2570,7 +2685,7 @@ err-code@^1.0.0: resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -2631,7 +2746,7 @@ escape-html@^1.0.1, escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -2647,17 +2762,12 @@ escodegen@~1.2.0: optionalDependencies: source-map "~0.1.30" -eslint-config-standard-jsx@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/eslint-config-standard-jsx/-/eslint-config-standard-jsx-6.0.2.tgz#90c9aa16ac2c4f8970c13fc7efc608bacd02da70" - integrity sha512-D+YWAoXw+2GIdbMBRAzWwr1ZtvnSf4n4yL0gKGg7ShUOGXkSOLerI17K4F6LdQMJPNMoWYqepzQD/fKY+tXNSg== - -eslint-config-standard@12.0.0: +eslint-config-standard@^12.0.0: version "12.0.0" resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-12.0.0.tgz#638b4c65db0bd5a41319f96bba1f15ddad2107d9" integrity sha512-COUz8FnXhqFitYj4DTqHzidjIL/t4mumGZto5c7DrBpvWoie+Sn3P4sLEzUGeYhRElWuFEf8K1S1EfvD1vixCQ== -eslint-import-resolver-node@^0.3.1: +eslint-import-resolver-node@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== @@ -2665,15 +2775,15 @@ eslint-import-resolver-node@^0.3.1: debug "^2.6.9" resolve "^1.5.0" -eslint-module-utils@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746" - integrity sha1-snA2LNiLGkitMIl2zn+lTphBF0Y= +eslint-module-utils@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz#8b93499e9b00eab80ccb6614e69f03678e84e09a" + integrity sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw== dependencies: debug "^2.6.8" - pkg-dir "^1.0.0" + pkg-dir "^2.0.0" -eslint-plugin-es@^1.3.1: +eslint-plugin-es@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz#475f65bb20c993fc10e8c8fe77d1d60068072da6" integrity sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw== @@ -2681,59 +2791,56 @@ eslint-plugin-es@^1.3.1: eslint-utils "^1.3.0" regexpp "^2.0.1" -eslint-plugin-import@~2.14.0: - version "2.14.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8" - integrity sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g== +eslint-plugin-import@^2.17.2: + version "2.17.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz#d227d5c6dc67eca71eb590d2bb62fb38d86e9fcb" + integrity sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g== dependencies: + array-includes "^3.0.3" contains-path "^0.1.0" - debug "^2.6.8" + debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.1" - eslint-module-utils "^2.2.0" - has "^1.0.1" - lodash "^4.17.4" - minimatch "^3.0.3" + eslint-import-resolver-node "^0.3.2" + eslint-module-utils "^2.4.0" + has "^1.0.3" + lodash "^4.17.11" + minimatch "^3.0.4" read-pkg-up "^2.0.0" - resolve "^1.6.0" + resolve "^1.10.0" + +eslint-plugin-mocha@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-mocha/-/eslint-plugin-mocha-5.3.0.tgz#cf3eb18ae0e44e433aef7159637095a7cb19b15b" + integrity sha512-3uwlJVLijjEmBeNyH60nzqgA1gacUWLUmcKV8PIGNvj1kwP/CTgAWQHn2ayyJVwziX+KETkr9opNwT1qD/RZ5A== + dependencies: + ramda "^0.26.1" -eslint-plugin-node@~7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-7.0.1.tgz#a6e054e50199b2edd85518b89b4e7b323c9f36db" - integrity sha512-lfVw3TEqThwq0j2Ba/Ckn2ABdwmL5dkOgAux1rvOk6CO7A6yGyPI2+zIxN6FyNkp1X1X/BSvKOceD6mBWSj4Yw== +eslint-plugin-node@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-9.0.1.tgz#93e44626fa62bcb6efea528cee9687663dc03b62" + integrity sha512-fljT5Uyy3lkJzuqhxrYanLSsvaILs9I7CmQ31atTtZ0DoIzRbbvInBh4cQ1CrthFHInHYBQxfPmPt6KLHXNXdw== dependencies: - eslint-plugin-es "^1.3.1" + eslint-plugin-es "^1.4.0" eslint-utils "^1.3.1" - ignore "^4.0.2" + ignore "^5.1.1" minimatch "^3.0.4" - resolve "^1.8.1" - semver "^5.5.0" - -eslint-plugin-promise@~4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2" - integrity sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg== + resolve "^1.10.1" + semver "^6.0.0" -eslint-plugin-react@~7.11.1: - version "7.11.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.11.1.tgz#c01a7af6f17519457d6116aa94fc6d2ccad5443c" - integrity sha512-cVVyMadRyW7qsIUh3FHp3u6QHNhOgVrLQYdQEB1bPWBsgbNCHdFAeNMquBMCcZJu59eNthX053L70l7gRt4SCw== - dependencies: - array-includes "^3.0.3" - doctrine "^2.1.0" - has "^1.0.3" - jsx-ast-utils "^2.0.1" - prop-types "^15.6.2" +eslint-plugin-promise@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db" + integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ== -eslint-plugin-standard@~4.0.0: +eslint-plugin-standard@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.0.tgz#f845b45109c99cd90e77796940a344546c8f6b5c" integrity sha512-OwxJkR6TQiYMmt1EsNRMe5qG3GsbjlcOhbGUBY4LtavF9DsLaTcoR+j2Tdjqi23oUwKNUqX7qcn5fPStafMdlA== -eslint-scope@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" - integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== +eslint-scope@^4.0.0, eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" @@ -2748,56 +2855,54 @@ eslint-visitor-keys@^1.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== -eslint@~5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.4.0.tgz#d068ec03006bb9e06b429dc85f7e46c1b69fac62" - integrity sha512-UIpL91XGex3qtL6qwyCQJar2j3osKxK9e3ano3OcGEIRM4oWIpCkDg9x95AXEC2wMs7PnxzOkPZ2gq+tsMS9yg== +eslint@^5.16.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" + integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== dependencies: - ajv "^6.5.0" - babel-code-frame "^6.26.0" + "@babel/code-frame" "^7.0.0" + ajv "^6.9.1" chalk "^2.1.0" cross-spawn "^6.0.5" - debug "^3.1.0" - doctrine "^2.1.0" - eslint-scope "^4.0.0" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^4.0.3" eslint-utils "^1.3.1" eslint-visitor-keys "^1.0.0" - espree "^4.0.0" + espree "^5.0.1" esquery "^1.0.1" esutils "^2.0.2" - file-entry-cache "^2.0.0" + file-entry-cache "^5.0.1" functional-red-black-tree "^1.0.1" glob "^7.1.2" globals "^11.7.0" - ignore "^4.0.2" + ignore "^4.0.6" + import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^5.2.0" - is-resolvable "^1.1.0" - js-yaml "^3.11.0" + inquirer "^6.2.2" + js-yaml "^3.13.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.3.0" - lodash "^4.17.5" + lodash "^4.17.11" minimatch "^3.0.4" mkdirp "^0.5.1" natural-compare "^1.4.0" optionator "^0.8.2" path-is-inside "^1.0.2" - pluralize "^7.0.0" progress "^2.0.0" - regexpp "^2.0.0" - require-uncached "^1.0.3" - semver "^5.5.0" + regexpp "^2.0.1" + semver "^5.5.1" strip-ansi "^4.0.0" strip-json-comments "^2.0.1" - table "^4.0.3" + table "^5.2.3" text-table "^0.2.0" -espree@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-4.1.0.tgz#728d5451e0fd156c04384a7ad89ed51ff54eb25f" - integrity sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w== +espree@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a" + integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A== dependencies: - acorn "^6.0.2" + acorn "^6.0.7" acorn-jsx "^5.0.0" eslint-visitor-keys "^1.0.0" @@ -2995,7 +3100,7 @@ extend@^3.0.0, extend@~3.0.2: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -external-editor@^2.0.4, external-editor@^2.1.0: +external-editor@^2.0.4: version "2.2.0" resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" integrity sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A== @@ -3004,6 +3109,15 @@ external-editor@^2.0.4, external-editor@^2.1.0: iconv-lite "^0.4.17" tmp "^0.0.33" +external-editor@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -3074,13 +3188,12 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" - integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== dependencies: - flat-cache "^1.2.1" - object-assign "^4.0.1" + flat-cache "^2.0.1" file-name@^0.1.0: version "0.1.0" @@ -3168,11 +3281,6 @@ find-pkg@^0.1.0: dependencies: find-file-up "^0.1.2" -find-root@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" - integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== - find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3188,15 +3296,14 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" -flat-cache@^1.2.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" - integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg== +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== dependencies: - circular-json "^0.3.1" - graceful-fs "^4.1.2" - rimraf "~2.6.2" - write "^0.2.1" + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" flatted@^2.0.0: version "2.0.0" @@ -3415,11 +3522,6 @@ get-stdin@^4.0.1: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -3503,7 +3605,7 @@ glob@7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: +glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -3548,11 +3650,16 @@ global@4.3.2, global@^4.3.0, global@^4.3.2: min-document "^2.19.0" process "~0.5.1" -globals@^11.1.0, globals@^11.7.0: +globals@^11.1.0: version "11.9.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.9.0.tgz#bde236808e987f290768a93d065060d78e6ab249" integrity sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg== +globals@^11.7.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + got@^6.7.1: version "6.7.1" resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" @@ -3593,13 +3700,6 @@ har-validator@~5.1.0: ajv "^6.5.5" har-schema "^2.0.0" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - has-binary2@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" @@ -3838,7 +3938,7 @@ iconv-lite@0.4.23: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -3866,6 +3966,11 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= +iframe-resizer@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/iframe-resizer/-/iframe-resizer-4.1.1.tgz#607c36edfb4547ecce8247226ef062e269a4eccc" + integrity sha512-+cs/M4ivpLyKYDs9AZWK0eRA1FKXut0yQr0iaEqdMKuk5yZJeF9H691q1qW1Cz5D3wHmiT+cTubWEsaSJBLUJA== + ignore-walk@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" @@ -3873,21 +3978,29 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" -ignore@^3.0.9: - version "3.3.10" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" - integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== - -ignore@^4.0.2: +ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.1.tgz#2fc6b8f518aff48fef65a7f348ed85632448e4a5" + integrity sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA== + image-size@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.6.3.tgz#e7e5c65bb534bd7cdcedd6cb5166272a85f75fb2" integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA== +import-fresh@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390" + integrity sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -3965,23 +4078,23 @@ inquirer@^3.1.1: strip-ansi "^4.0.0" through "^2.3.6" -inquirer@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726" - integrity sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ== +inquirer@^6.2.2: + version "6.3.1" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.3.1.tgz#7a413b5e7950811013a3db491c61d1f3b776e8e7" + integrity sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA== dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.0" + ansi-escapes "^3.2.0" + chalk "^2.4.2" cli-cursor "^2.1.0" cli-width "^2.0.0" - external-editor "^2.1.0" + external-editor "^3.0.3" figures "^2.0.0" - lodash "^4.3.0" + lodash "^4.17.11" mute-stream "0.0.7" run-async "^2.2.0" - rxjs "^5.5.2" + rxjs "^6.4.0" string-width "^2.1.0" - strip-ansi "^4.0.0" + strip-ansi "^5.1.0" through "^2.3.6" insert-module-globals@^7.0.0: @@ -4271,11 +4384,6 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" -is-resolvable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== - is-retry-allowed@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" @@ -4396,15 +4504,10 @@ js-string-escape@^1.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@^3.11.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" - integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== +js-yaml@^3.13.0: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -4424,7 +4527,7 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= -json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1: +json-parse-better-errors@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== @@ -4504,13 +4607,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jsx-ast-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" - integrity sha1-6AGxs5mF4g//yHtA43SAgOLcrH8= - dependencies: - array-includes "^3.0.3" - karma-browserify@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/karma-browserify/-/karma-browserify-6.0.0.tgz#423b719fe80d064ad5ec36f8eb15c399305b9aba" @@ -4702,16 +4798,6 @@ load-json-file@^2.0.0: pify "^2.0.0" strip-bom "^3.0.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -4742,6 +4828,11 @@ lodash.memoize@~3.0.3: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" integrity sha1-LcvSwofLwKVcxCMovQxzYVDVPj8= +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + "lodash.wrap@>= 3 < 4": version "3.0.1" resolved "https://registry.yarnpkg.com/lodash.wrap/-/lodash.wrap-3.0.1.tgz#3fcd8bef867b2ec8c21bac638d816180593f71ab" @@ -4749,7 +4840,7 @@ lodash.memoize@~3.0.3: dependencies: lodash._createwrapper "^3.0.0" -lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -4822,6 +4913,11 @@ make-dir@^1.0.0: dependencies: pify "^3.0.0" +make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + make-fetch-happen@^2.4.13: version "2.6.0" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-2.6.0.tgz#8474aa52198f6b1ae4f3094c04e8370d35ea8a38" @@ -5008,7 +5104,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -5612,6 +5708,13 @@ param-case@^1.0.1: dependencies: sentence-case "^1.1.2" +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + parents@^1.0.0, parents@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" @@ -5664,14 +5767,6 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -5824,30 +5919,6 @@ pirates@^4.0.0: dependencies: node-modules-regexp "^1.0.0" -pkg-conf@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-2.1.0.tgz#2126514ca6f2abfebd168596df18ba57867f0058" - integrity sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg= - dependencies: - find-up "^2.0.0" - load-json-file "^4.0.0" - -pkg-config@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pkg-config/-/pkg-config-1.1.1.tgz#557ef22d73da3c8837107766c52eadabde298fe4" - integrity sha1-VX7yLXPaPIg3EHdmxS6tq94pj+Q= - dependencies: - debug-log "^1.0.0" - find-root "^1.0.0" - xtend "^4.0.1" - -pkg-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" - integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q= - dependencies: - find-up "^1.0.0" - pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -5855,11 +5926,6 @@ pkg-dir@^2.0.0: dependencies: find-up "^2.1.0" -pluralize@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" - integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== - posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -6054,6 +6120,11 @@ querystring@0.2.0: resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= +ramda@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== + random-string@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/random-string/-/random-string-0.2.0.tgz#a46e4375352beda9a0d7b0d19ed6d321ecd1d82d" @@ -6257,7 +6328,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexpp@^2.0.0, regexpp@^2.0.1: +regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== @@ -6368,13 +6439,10 @@ request@^2.45.0: tunnel-agent "^0.6.0" uuid "^3.3.2" -require-uncached@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" - integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= - dependencies: - caller-path "^0.1.0" - resolve-from "^1.0.0" +requireindex@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" + integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== requires-port@^1.0.0: version "1.0.0" @@ -6389,10 +6457,10 @@ resolve-dir@^0.1.0: expand-tilde "^1.2.2" global-modules "^0.2.3" -resolve-from@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" - integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== resolve-url@^0.2.1: version "0.2.1" @@ -6404,13 +6472,20 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.4, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1: +resolve@^1.1.4, resolve@^1.3.2, resolve@^1.3.3, resolve@^1.4.0: version "1.9.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.9.0.tgz#a14c6fdfa8f92a7df1d996cb7105fa744658ea06" integrity sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ== dependencies: path-parse "^1.0.6" +resolve@^1.10.0, resolve@^1.10.1, resolve@^1.5.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.1.tgz#664842ac960795bbe758221cdccda61fb64b5f18" + integrity sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA== + dependencies: + path-parse "^1.0.6" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -6434,7 +6509,7 @@ rfdc@^1.1.2: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.2.tgz#e6e72d74f5dc39de8f538f65e00c36c18018e349" integrity sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA== -rimraf@2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2: +rimraf@2, rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -6456,11 +6531,6 @@ run-async@^2.2.0: dependencies: is-promise "^2.1.0" -run-parallel@^1.1.2: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== - run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -6480,12 +6550,12 @@ rx-lite@*, rx-lite@^4.0.8: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= -rxjs@^5.5.2: - version "5.5.12" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" - integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== +rxjs@^6.4.0: + version "6.5.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.1.tgz#f7a005a9386361921b8524f38f54cbf80e5d08f4" + integrity sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg== dependencies: - symbol-observable "1.0.1" + tslib "^1.9.0" safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" @@ -6529,6 +6599,21 @@ semver-diff@^2.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== +semver@5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== + +semver@^5.5.1: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +semver@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" + integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== + send@0.16.2: version "0.16.2" resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" @@ -6667,11 +6752,13 @@ single-line-log@^1.1.2: dependencies: string-width "^1.0.1" -slice-ansi@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" - integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg== +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" smart-buffer@^1.0.13: @@ -6782,6 +6869,14 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" +source-map-support@^0.5.6: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@^0.5.9: version "0.5.9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" @@ -6889,31 +6984,6 @@ stackframe@^1.0.4: resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b" integrity sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw== -standard-engine@~9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/standard-engine/-/standard-engine-9.0.0.tgz#d3a3d74c4c1b91f51a1e66362465261ca7610316" - integrity sha512-ZfNfCWZ2Xq67VNvKMPiVMKHnMdvxYzvZkf1AH8/cw2NLDBm5LRsxMqvEJpsjLI/dUosZ3Z1d6JlHDp5rAvvk2w== - dependencies: - deglob "^2.1.0" - get-stdin "^6.0.0" - minimist "^1.1.0" - pkg-conf "^2.0.0" - -standard@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/standard/-/standard-12.0.1.tgz#0fc5a8aa6c34c546c5562aae644242b24dae2e61" - integrity sha512-UqdHjh87OG2gUrNCSM4QRLF5n9h3TFPwrCNyVlkqu31Hej0L/rc8hzKqVvkb2W3x0WMq7PzZdkLfEcBhVOR6lg== - dependencies: - eslint "~5.4.0" - eslint-config-standard "12.0.0" - eslint-config-standard-jsx "6.0.2" - eslint-plugin-import "~2.14.0" - eslint-plugin-node "~7.0.1" - eslint-plugin-promise "~4.0.0" - eslint-plugin-react "~7.11.1" - eslint-plugin-standard "~4.0.0" - standard-engine "~9.0.0" - static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -7017,6 +7087,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + string_decoder@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" @@ -7050,6 +7129,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -7100,11 +7186,6 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -7112,11 +7193,6 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -symbol-observable@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" - integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ= - syntax-error@^1.1.1: version "1.4.0" resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" @@ -7124,17 +7200,15 @@ syntax-error@^1.1.1: dependencies: acorn-node "^1.2.0" -table@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" - integrity sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg== +table@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/table/-/table-5.2.3.tgz#cde0cc6eb06751c009efab27e8c820ca5b67b7f2" + integrity sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ== dependencies: - ajv "^6.0.1" - ajv-keywords "^3.0.0" - chalk "^2.1.0" - lodash "^4.17.4" - slice-ansi "1.0.0" - string-width "^2.1.1" + ajv "^6.9.1" + lodash "^4.17.11" + slice-ansi "^2.1.0" + string-width "^3.0.0" tar-fs@^1.15.3: version "1.16.3" @@ -7328,6 +7402,29 @@ trytryagain@1.2.0: resolved "https://registry.yarnpkg.com/trytryagain/-/trytryagain-1.2.0.tgz#5be62b5c9c2622a58d47de4271c120554dd09212" integrity sha1-W+YrXJwmIqWNR95CccEgVU3QkhI= +ts-node@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.1.0.tgz#8c4b37036abd448577db22a061fd7a67d47e658e" + integrity sha512-34jpuOrxDuf+O6iW1JpgTRDFynUZ1iEqtYruBqh35gICNjN8x+LpVcPAcwzLPi9VU6mdA3ym+x233nZmZp445A== + dependencies: + arg "^4.1.0" + diff "^3.1.0" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +tslib@^1.8.1, tslib@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + +tsutils@^3.7.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.10.0.tgz#6f1c95c94606e098592b0dff06590cf9659227d6" + integrity sha512-q20XSMq7jutbGB8luhKKsQldRKWvyBO2BGqni3p4yq8Ys9bEP/xQw3KepKmMRt9gJ4lvQSScrihJrcKdKoSU7Q== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" @@ -7370,6 +7467,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typescript@^3.4.4: + version "3.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.4.tgz#aac4a08abecab8091a75f10842ffa0631818f785" + integrity sha512-xt5RsIRCEaf6+j9AyOBgvVuAec0i92rgCaS3S+UVf5Z/vF2Hvtsw08wtUTJqp4djwznoAgjSxeCcU4r+CcDBJA== + ultron@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" @@ -7428,11 +7530,6 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^0.4.3" -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - unique-filename@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -7714,10 +7811,10 @@ write-file-atomic@^2.0.0: imurmurhash "^0.1.4" signal-exit "^3.0.2" -write@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" - integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== dependencies: mkdirp "^0.5.1" @@ -7793,3 +7890,8 @@ yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + +yn@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.0.tgz#fcbe2db63610361afcc5eb9e0ac91e976d046114" + integrity sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg== From 3c578efd6990a97240160d28d553f8bc72f79c7d Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Fri, 31 May 2019 12:53:54 +0200 Subject: [PATCH 016/127] more crap --- lib/transformation.ts | 6 ------ test/tsSpec.js | 5 +++++ testA.ts | 7 +++++++ tsconfig.json | 6 ++---- 4 files changed, 14 insertions(+), 10 deletions(-) delete mode 100644 lib/transformation.ts create mode 100644 test/tsSpec.js create mode 100644 testA.ts diff --git a/lib/transformation.ts b/lib/transformation.ts deleted file mode 100644 index 1c052b8..0000000 --- a/lib/transformation.ts +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = class Transformation { - constructor (transform : Transform, parent? : Transformation) { - this.transform = transform - this.parent = parent - } -} \ No newline at end of file diff --git a/test/tsSpec.js b/test/tsSpec.js new file mode 100644 index 0000000..0b981d1 --- /dev/null +++ b/test/tsSpec.js @@ -0,0 +1,5 @@ +import x from '../../browser-monkey/testA.ts' + +it('x', async () => { + console.log('x', await x()) +}) diff --git a/testA.ts b/testA.ts new file mode 100644 index 0000000..65c4088 --- /dev/null +++ b/testA.ts @@ -0,0 +1,7 @@ +async function a () { + const x: number = 0 + await new Promise(r => setTimeout(r, 100)) + return x +} + +export default a diff --git a/tsconfig.json b/tsconfig.json index 1d6541a..a43e330 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,11 +2,9 @@ "compilerOptions": { "lib": ["es2015"], "noImplicitAny": false, - - "emitDecoratorMetadata": true, "module": "commonjs", "target": "ES5", - "outDir": "lib", - "rootDir": "lib" + "outDir": "dist", + "rootDir": "." } } From 8b412ea1d4c47a5a9ed4757af3b6c6730a82f8f8 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Fri, 14 Jun 2019 17:30:49 +0200 Subject: [PATCH 017/127] continuing to rewrite internals, better assert() errors --- .eslintrc.js | 25 ++ .gitignore | 1 + create.js | 2 +- ...ansform.ts => ExecutedFirstOfTransform.ts} | 4 +- lib/ExecutedSimpleTransform.ts | 4 +- lib/ExecutedTransformSequence.ts | 20 +- lib/actions.js | 34 +-- lib/assertions.js | 28 +- lib/button.js | 2 +- lib/expectDeepEqual.js | 2 +- lib/expectEqual.js | 2 +- lib/fields.js | 8 +- lib/finders.js | 80 ++++-- lib/query.ts | 248 +++++++----------- lib/set.js | 169 ++++++++---- react.js | 6 +- test/actionsSpec.js | 6 +- test/assemblies/DomAssembly.js | 14 +- test/assertSpec.js | 107 ++++++-- test/assertionsSpec.js | 12 +- test/buttonSpec.js | 40 ++- test/fieldsSpec.js | 42 ++- test/findersSpec.js | 47 +--- test/optionsSpec.js | 9 +- test/querySpec.js | 199 ++++++-------- test/reloadButtonSpec.js | 2 +- test/scopeSpec.js | 6 +- test/setSpec.js | 60 ++--- tsconfig.json | 6 +- 29 files changed, 627 insertions(+), 558 deletions(-) create mode 100644 .eslintrc.js rename lib/{ExecutedOrTransform.ts => ExecutedFirstOfTransform.ts} (65%) diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..443549a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: ['plugin:@typescript-eslint/recommended'], + plugins: ['@typescript-eslint', 'mocha'], + rules: { + "@typescript-eslint/no-var-requires": "off", + "indent": "off", + "@typescript-eslint/indent": ["error", 2], + "@typescript-eslint/member-delimiter-style": ['error', { + "multiline": { + "delimiter": "none", + }, + "singleline": { + "delimiter": "comma", + } + }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-use-before-define': ['error', { 'functions': false, 'classes': true }], + '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], + }, + env: { + mocha: true, + node: true + } +} diff --git a/.gitignore b/.gitignore index 03bba7c..a9ce0d6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.orig /node_modules/ .DS_Store +/dist/ diff --git a/create.js b/create.js index df487a6..dc040d5 100644 --- a/create.js +++ b/create.js @@ -6,7 +6,7 @@ var fields = require('./lib/fields') var set = require('./lib/set') var assertions = require('./lib/assertions') -module.exports = function (rootSelector) { +module.exports = function (rootSelector = document.body) { return new Query() .component(finders) .component(actions) diff --git a/lib/ExecutedOrTransform.ts b/lib/ExecutedFirstOfTransform.ts similarity index 65% rename from lib/ExecutedOrTransform.ts rename to lib/ExecutedFirstOfTransform.ts index 16e509f..c037de4 100644 --- a/lib/ExecutedOrTransform.ts +++ b/lib/ExecutedFirstOfTransform.ts @@ -1,6 +1,6 @@ import { ExecutedTransform } from './ExecutedTransform' -export class ExecutedOrTransform extends ExecutedTransform { +export class ExecutedFirstOfTransform extends ExecutedTransform { public items: ExecutedTransform[] public index: number @@ -11,6 +11,6 @@ export class ExecutedOrTransform extends ExecutedTransform { } public renderError (): string { - return `or(${this.items.map(i => i.renderError()).join(', ')}) [${this.value.length}]` + return `firstOf(${this.items.map(i => i.renderError()).join(', ')}) [${this.value.length}]` } } diff --git a/lib/ExecutedSimpleTransform.ts b/lib/ExecutedSimpleTransform.ts index 2733860..c49e00a 100644 --- a/lib/ExecutedSimpleTransform.ts +++ b/lib/ExecutedSimpleTransform.ts @@ -9,6 +9,8 @@ export class ExecutedSimpleTransform extends ExecutedTransform { } public renderError (): string { - return `${this.description} [${this.value.length}]` + if (this.description) { + return `${this.description} [${this.value.length}]` + } } } diff --git a/lib/ExecutedTransformSequence.ts b/lib/ExecutedTransformSequence.ts index 02cb43d..73b89dc 100644 --- a/lib/ExecutedTransformSequence.ts +++ b/lib/ExecutedTransformSequence.ts @@ -18,8 +18,26 @@ export class ExecutedTransformSequence extends ExecutedTransform { this.transforms.unshift(...transforms.transforms) } + public clear (): void { + this.transforms = [] + } + public renderError (): string { - return this.transforms.map(t => t.renderError()).join(', ') + const transformsUpUntilAndIncludingFirstFailure = this.transforms.reduce(({ keepTaking, array }, b) => { + if (keepTaking) { + return { + keepTaking: !!b.value.length, + array: array.concat([b]) + } + } else { + return { array } + } + }, { + array: [], + keepTaking: true + }).array + + return this.transforms.map(t => t.renderError()).filter(Boolean).join(', ') } public clone (): ExecutedTransformSequence { diff --git a/lib/actions.js b/lib/actions.js index ecd9e1d..4ce186b 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -33,14 +33,14 @@ module.exports = { }, enter: function (selector) { - return this.query(selector).element().action(function (element) { + return this.query(selector).expectOneElement().action(function ([element]) { debug('enter', element) elementSubmit(element) }) }, click: function () { - return this.enabled().element().then(function (element) { + return this.enabled().expectOneElement().then(function ([element]) { debug('click', element) elementClick(element) }) @@ -51,11 +51,11 @@ module.exports = { ? [{ exactText: value }, { exactValue: value }] : [value] - return this.is('select').all(criteria.map(function (c) { + return this.is('select').concat(criteria.map(function (c) { return function (monkey) { return monkey.find('option', c) } - })).element().action(function (optionElement) { + })).expectOneElement().action(function ([optionElement]) { var selectElement = optionElement.parentNode debug('select', selectElement) elementSelect(selectElement, optionElement) @@ -63,7 +63,7 @@ module.exports = { }, check: function (value) { - return this.is('input[type=checkbox]').one().action(function (checkbox) { + return this.is('input[type=checkbox]').expectOneElement().action(function ([checkbox]) { if (elementChecked(checkbox) !== value) { elementClick(checkbox) } @@ -71,33 +71,27 @@ module.exports = { }, checked: function () { - return this.is('input[type=checkbox]').one().mapAll(function (checkbox) { + return this.is('input[type=checkbox]').expectOneElement().transform(function ([checkbox]) { return elementChecked(checkbox) }) }, selectValue: function () { - return this.is('select').one().mapAll(function (selectElement) { - return [selectElement] - }).find('option').mapAll(function (options) { - if (options.length) { - var selectElement = options[0].parentNode - var selectedOption = options[selectElement.selectedIndex] - return elementInnerText(selectedOption) - } else { - return '' - } + return this.is('select').expectOneElement('expected to be select element').find('option').expectSomeElements('expected select options elements').transform(function (options) { + var selectElement = options[0].parentNode + var selectedOption = options[selectElement.selectedIndex] + return elementInnerText(selectedOption) }) }, inputValue: function () { - return this.is(inputsSelector).element().mapAll(function (element) { + return this.is(inputsSelector).expectOneElement().transform(function ([element]) { return element.value }) }, typeIn: function (text) { - return this.is(inputsSelector).element().action(function (element) { + return this.is(inputsSelector).expectOneElement().action(function ([element]) { if (typeof text !== 'string') { throw new Error('expected string as argument to typeIn') } @@ -107,14 +101,14 @@ module.exports = { }, submit: function () { - return this.element().then(function (element) { + return this.expectOneElement().then(function ([element]) { debug('submit', element) elementSubmit(element) }) }, typeInHtml: function (html) { - return this.element().then(function (element) { + return this.expectOneElement().then(function ([element]) { debug('typeInHtml', element, html) element.focus() element.innerHTML = html diff --git a/lib/assertions.js b/lib/assertions.js index 57d9ec1..4c6d35f 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -2,7 +2,7 @@ const elementInnerText = require('./elementInnerText') const elementSelector = require('./elementSelector') const elementChecked = require('./elementChecked') const inspect = require('object-inspect') -const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') +const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default var expectDeepEqual = require('./expectDeepEqual') var expectEqual = require('./expectEqual') var object = require('lowscore/object') @@ -10,13 +10,13 @@ const elementMatches = require('./elementMatches') module.exports = { shouldEqual: function (value) { - return this.ensure(function (result) { + return this.expect(function (result) { expectEqual(result, value) }) }, shouldDeepEqual: function (value) { - return this.ensure(function (result) { + return this.expect(function (result) { expectDeepEqual(result, value) }) }, @@ -26,13 +26,13 @@ module.exports = { }, shouldExist: function (options) { - return this.some() + return this.expectSomeElements() }, shouldHave: function (options) { var self = this - return this.elements().ensure(function (elements) { + return this.expectSomeElements().expect(function (elements) { assertElementExpectations(self, elements, options) }, inspect(options)) }, @@ -43,21 +43,21 @@ module.exports = { }, shouldNotExist: function () { - return this.zero() + return this.expectNoElements() }, shouldHaveElement: function (fn) { - return this.one().ensure(fn) + return this.expectOneElement().expect(([el]) => fn(el)) }, shouldHaveElements: function (fn, options) { - return this.some().ensure(fn) + return this.expectSomeElements().expect(fn) }, shouldNotHave: function (options) { var self = this - return this.elements().ensure(function (elements) { + return this.expectSomeElements().expect(function (elements) { let thrownError try { assertElementExpectations(self, elements, options) @@ -95,12 +95,8 @@ function assertInexactValues (actual, expected) { } function queryValue (query, elements) { - var valueQuery = query.subQuery(function (q) { - return q.value() - }) - return elements.map(function (element) { - return valueQuery.execute([element]).value + return query.transform(() => [element]).value().result() }) } @@ -203,6 +199,8 @@ var elementExpectations = { } function assertElementExpectations (query, elements, options) { + const resolved = query.resolve() + Object.keys(options).forEach(function (key) { var expectation = elementExpectations[key] @@ -210,6 +208,6 @@ function assertElementExpectations (query, elements, options) { throw new Error('no such expectation ' + key + ', try one of: ' + Object.keys(elementExpectations).join(', ')) } - expectation(query, elements, options[key]) + expectation(resolved, elements, options[key]) }) } diff --git a/lib/button.js b/lib/button.js index 0cd4f19..0c63e02 100644 --- a/lib/button.js +++ b/lib/button.js @@ -1,6 +1,6 @@ module.exports = { button: function (name) { - return this.all(this._options.definitions.button.map(function (definition) { + return this.concat(this._options.definitions.button.map(function (definition) { return function (monkey) { return definition(monkey, name) } diff --git a/lib/expectDeepEqual.js b/lib/expectDeepEqual.js index 6714502..f7ebc87 100644 --- a/lib/expectDeepEqual.js +++ b/lib/expectDeepEqual.js @@ -1,4 +1,4 @@ -var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') +var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default var deepEqual = require('deep-equal') var inspect = require('object-inspect') diff --git a/lib/expectEqual.js b/lib/expectEqual.js index 451ecfd..29ad9e9 100644 --- a/lib/expectEqual.js +++ b/lib/expectEqual.js @@ -1,4 +1,4 @@ -var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') +var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default var inspect = require('object-inspect') module.exports = function expectEqual (actual, expected, createMessage) { diff --git a/lib/fields.js b/lib/fields.js index b7a0a20..4611de8 100644 --- a/lib/fields.js +++ b/lib/fields.js @@ -1,6 +1,6 @@ module.exports = { field: function (name) { - return this.all(this._options.definitions.field.map(function (definition) { + return this.concat(this._options.definitions.field.map(function (definition) { return function (monkey) { return definition(monkey, name) } @@ -23,12 +23,12 @@ module.exports = { return this.field(name).setValue(value) }, - valueOld: function () { + value: function () { var getters = this._options.definitions.fieldValue.map(function (definition) { return definition.get }) - return this.race(getters) + return this.firstOf(getters) }, setValue: function (value) { @@ -38,6 +38,6 @@ module.exports = { } }) - return this.race(setters) + return this.firstOf(setters) } } diff --git a/lib/finders.js b/lib/finders.js index c4c193c..251be7e 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -2,25 +2,36 @@ var elementMatches = require('./elementMatches') var elementQuerySelectorAll = require('./elementQuerySelectorAll') var elementHas = require('./elementHas') var flatten = require('lowscore/flatten') -var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError') +var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default var inspect = require('object-inspect') const { ExecutedSimpleTransform } = require('./ExecutedSimpleTransform') module.exports = { - css: function (selector) { - return this.transform((elements) => { + find: function (selector, findOptions) { + const findElements = this.transform(elements => { expectElements(elements) return new ExecutedSimpleTransform(flatten(elements.map(element => { return elementQuerySelectorAll(element, selector, this._options) })), 'find(' + inspect(selector) + ')') }) + + if (findOptions) { + return findElements.has(findOptions) + } else { + return findElements + } }, has: function (options) { - return this.mapAll(function (elements) { + return this.transform(function (elements) { expectElements(elements) - return elements.filter(function (element, index) { return elementHas(element, index, options) }) - }, 'has: ' + inspect(options)) + return new ExecutedSimpleTransform(elements.filter(function (element, index) { + return elementHas(element, index, options) + }), 'has: ' + inspect(options)) + }) + }, + + elementsMatching: function (pattern) { }, is: function (selector) { @@ -45,21 +56,24 @@ module.exports = { }, iframeContent: function () { - return this.map(function (element) { - if (isIframe(element)) { - if (element.contentDocument && element.contentDocument.readyState === 'complete') { - return element.contentDocument.body + return this.transform(function (elements) { + return elements.map(element => { + if (isIframe(element)) { + if (element.contentDocument && element.contentDocument.readyState === 'complete') { + return element.contentDocument.body + } else { + throw new BrowserMonkeyAssertionError('iframe not loaded') + } } else { - throw new BrowserMonkeyAssertionError('iframe not loaded') + throw new BrowserMonkeyAssertionError('not iframe') } - } else { - throw new BrowserMonkeyAssertionError('not iframe') - } + }) }, 'iframeContent') }, scope: function (element) { - var selector = this.input([element]) + var selector = this.clone() + selector.input([element]) if (isIframe(element)) { return selector.iframeContent() @@ -77,18 +91,34 @@ module.exports = { }, 'enabled') }, - element: function () { - return this.elements().one() + expectNoElements: function (message) { + return this.expect(elements => { + expectElements(elements) + + if (elements.length !== 0) { + this.error(message || 'expected no elements, found ' + elements.length) + } + }) + }, + + expectOneElement: function (message) { + return this.expect(elements => { + expectElements(elements) + + if (elements.length !== 1) { + this.error(message || 'expected just one element, found ' + elements.length) + } + }) }, - elements: function () { - return this.ensure(function (elements) { - if (isArrayOfHTMLElements(elements)) { - return elements - } else { - throw new BrowserMonkeyAssertionError('expected DOM elements') + expectSomeElements: function (message) { + return this.expect(function (elements) { + expectElements(elements) + + if (elements.length < 1) { + this.error(message || 'expected one or more elements, found ' + elements.length) } - }, 'elements').some() + }) } } @@ -101,7 +131,7 @@ function isArrayOfHTMLElements (elements) { function expectElements (elements) { if (!isArrayOfHTMLElements(elements)) { - throw new BrowserMonkeyAssertionError('expected HTML elements') + throw new BrowserMonkeyAssertionError('expected an array of HTML elements') } } diff --git a/lib/query.ts b/lib/query.ts index eb3779a..f67fe03 100644 --- a/lib/query.ts +++ b/lib/query.ts @@ -1,7 +1,7 @@ import { ExecutedTransformSequence } from './ExecutedTransformSequence' import { ExecutedSimpleTransform } from './ExecutedSimpleTransform' import { ExecutedConcatTransform } from './ExecutedConcatTransform' -import { ExecutedOrTransform } from './ExecutedOrTransform' +import { ExecutedFirstOfTransform } from './ExecutedFirstOfTransform' import { ExecutedTransformError } from './ExecutedTransformError' import BrowserMonkeyAssertionError from './BrowserMonkeyAssertionError' import toExecutedTransform from './toExecutedTransform' @@ -37,13 +37,15 @@ class Query { } } - private _value: any + private _input: any private _actionExecuted: any private _action: (value: any) => void private _isResolved: boolean + private _hasExpectation: boolean public constructor () { this._isResolved = false + this._hasExpectation = false this._transforms = [] this._options = { visibleOnly: true, @@ -125,26 +127,29 @@ class Query { ] } } - this._value = undefined + this._input = undefined } public transform (transform: Transform): this { if (this._isResolved) { return this.clone(clone => { - const executedTransform = toExecutedTransform(transform.call(this, this._value)) + const executedTransform = toExecutedTransform(transform.call(this, this._executedTransforms.value)) clone._executedTransforms.addTransform(executedTransform) - clone._value = executedTransform.value }) } else { return this.clone(clone => clone._transforms.push(transform)) } } - public assertion (assertion: (any) => void): this { - return this.transform((value) => { - assertion.call(this, value) + public expect (expectation: (any) => void): this { + const expectQuery = this.transform(function (value) { + expectation.call(this, value) return value }) + + expectQuery._hasExpectation = true + + return expectQuery } public action (action: (any) => void): any { @@ -153,7 +158,11 @@ class Query { } if (this._isResolved) { - action.call(this, this._value) + if (!this._actionExecuted) { + action.call(this, this._executedTransforms.value) + this._actionExecuted = true + } + return this } else { return this.clone(clone => { clone._action = action @@ -161,9 +170,9 @@ class Query { } } - public value (): any { + public result (): any { if (this._isResolved) { - return this._value + return this._executedTransforms.value } else { return toExecutedTransform(this.execute()).value } @@ -171,21 +180,15 @@ class Query { public resolve (): this { if (!this._isResolved) { - this._executedTransforms = this.execute() - this._value = this._executedTransforms.value - this._isResolved = true - return this + const resolved = this.clone() + resolved._executedTransforms = resolved.execute() + resolved._isResolved = true + return resolved } else { return this } } - public map (mapper, description): this { - return this.transform((elements) => { - return new ExecutedSimpleTransform(elements.map(mapper), description) - }) - } - public filter (filter, description): this { return this.transform((elements) => { return new ExecutedSimpleTransform(elements.filter(filter), description) @@ -193,33 +196,17 @@ class Query { } public concat (queryCreators): this { - var queries = queryCreators.map(queryCreator => { - return this.subQuery(queryCreator) - }) + return this.transform(() => { + const resolved = this.resolve() - return this.transform((elements) => { - var transforms = queries.map(query => { - const transforms = new ExecutedTransformSequence(elements) - query.executeQuery(transforms) - return transforms - }) + var transforms = queryCreators.map(queryCreator => runQueryCreator(queryCreator, resolved).execute()) var value = uniq(Array.prototype.concat.apply([], transforms.map(t => t.value))) return new ExecutedConcatTransform(value, transforms) }) } - private subQuery (createQuery) { - var clone = this.clone() - clone._transforms = [] - var query = createQuery(clone) - if (!(query instanceof Query)) { - throw new Error('expected to create query: ' + createQuery) - } - return query - } - - public error (message: string, { expected, actual }): void { + public error (message: string, {expected = undefined, actual = undefined} = {}): void { if (this._isResolved) { throw new BrowserMonkeyAssertionError(message, { expected, actual, executedTransforms: this._executedTransforms }) } else { @@ -227,24 +214,22 @@ class Query { } } - private resolveSubQuery (createQuery, value) { - return this.subQuery(createQuery).resolve() - } - - private executeQuery (transformSequence: ExecutedTransformSequence) { + private executeQuery (input: any): ExecutedTransformSequence { const query = this - - function reducer (transform) { - const executedTransform = toExecutedTransform(transform.call(query, transformSequence.value)) - transformSequence.addTransform(executedTransform) - } + const transformSequence = new ExecutedTransformSequence(input) try { - this._transforms.forEach(reducer) + this._transforms.forEach(transform => { + const executedTransform = toExecutedTransform(transform.call(query, transformSequence.value)) + transformSequence.addTransform(executedTransform) + }) + if (this._action && !this._actionExecuted) { this._action(transformSequence.value) this._actionExecuted = true } + + return transformSequence } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { e.executedTransforms.prepend(transformSequence) @@ -256,22 +241,23 @@ class Query { private execute (): ExecutedTransformSequence { if (!this._isResolved) { - const transformSequence = new ExecutedTransformSequence(this._value) - this.executeQuery(transformSequence) - return transformSequence + return this.executeQuery(this._input) } else { return this._executedTransforms } } - private race (queryCreators): this { - return this.transform(() => { - const resolved = this.clone().resolve() + private firstOf (queryCreators): this { + const transformed = this.transform(() => { + const resolved = this.resolve().clone() + resolved._executedTransforms.clear() var values = queryCreators.map(query => { try { + const q = runQueryCreator(query, resolved) + q.assertHasActionOrExpectation() return { - value: query(resolved).execute() + value: q.execute() } } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { @@ -290,44 +276,60 @@ class Query { const firstSuccess = values[firstSuccessIndex] - const transform = new ExecutedOrTransform((firstSuccess && firstSuccess.value.value) || [], firstSuccessIndex, values.map(v => v.error || v.value)) + const transform = new ExecutedFirstOfTransform(firstSuccess ? firstSuccess.value.value : [], firstSuccessIndex, values.map(v => v.error || v.value)) if (firstSuccess) { return transform } else { - var error = new BrowserMonkeyAssertionError('all queries failed in race') + var error = new BrowserMonkeyAssertionError('all queries failed in firstOf') error.executedTransforms.addTransform(transform) throw error } }) + + transformed._hasExpectation = true + + return transformed } - public query (selector: string) { + public query (selector: string): this { const field = this._options.definitions.fields[selector] if (field) { return field(this) } else { - const [finderName, value] = selector.split(':') - if (this[finderName]) { - return this[finderName](value.trim()) - } else { - throw new Error('no such finder: ' + finderName) + const match = /(.*?)!(.*)/.exec(selector) + + if (match) { + const [, finderName, value] = match + const finder = this._options.definitions.finders[finderName] + + if (finder) { + return finder(this, value.trim()) + } } } + + return this.find(selector) } - public options (options) { + public options (options): Options { if (options) { - return this.clone(function (clone) { - extend(clone._options, options) - }) + extend(this._options, options) } else { return this._options } } - public then (...args) { + private assertHasActionOrExpectation () { + if (!this._hasExpectation && !this._action) { + throw new Error('no expectations or actions in query, use .result(), or add an expectation or an action') + } + } + + public then (...args): Promise { + this.assertHasActionOrExpectation() + var self = this var retry = retryFromOptions(this._options) var originalStack = new Error().stack @@ -365,55 +367,12 @@ class Query { return clone } - public ensure (assertion, name): this { - return this.transform(function (value) { - assertion(value) - return new ExecutedSimpleTransform(value, name || 'ensure') - }) - } - - public zero (): this { - return this.transform(function (results) { - if (results instanceof Array) { - if (results.length !== 0) { - throw new BrowserMonkeyAssertionError('expected no elements') - } else { - return new ExecutedSimpleTransform(results, 'zero') - } - } else { - throw new BrowserMonkeyAssertionError('expected array of no elements') - } - }) - } - - public one (): this { - return this.transform(function (results) { - if (results instanceof Array) { - if (results.length === 1) { - return new ExecutedSimpleTransform(results[0], 'one') - } else { - throw new BrowserMonkeyAssertionError('expected one element') - } - } else { - return new ExecutedSimpleTransform(results, 'one') - } - }) - } - - public some (): this { - return this.transform(function (results) { - if (results instanceof Array && results.length !== 0) { - return new ExecutedSimpleTransform(results, 'some') - } else { - throw new BrowserMonkeyAssertionError('expected some elements') - } - }) - } + public input (value): void { + if (this._isResolved) { + throw new Error('cannot change input, query is already resolved') + } - public input (value): this { - return this.clone(function (clone) { - clone._value = value - }) + this._input = value } public component (methods): this { @@ -440,8 +399,9 @@ class Query { function copySelectorFields (from, to) { to._transforms = from._transforms.slice() - to._value = from._value + to._input = from._input + to._hasExpectation = from._hasExpectation to._isResolved = from._isResolved if (from._isResolved) { to._executedTransforms = from._executedTransforms.clone() @@ -459,42 +419,6 @@ function copySelectorFields (from, to) { }) } -function errorMessage (error) { - if (error instanceof BrowserMonkeyAssertionError) { - return browserMonkeyAssertionErrorMessage(error) - } else { - return error.message - } -} - -function browserMonkeyAssertionErrorMessage (error: BrowserMonkeyAssertionError) { - var foundMessage = describeAccumulatedTransforms(error.executedTransforms) - return error.message + ' (found: ' + foundMessage + ')' -} - -function describeAccumulatedTransforms (accumulatedTransforms: ExecutedTransformSequence) { - return accumulatedTransforms.transforms.map(function (transform) { - return transform.renderError() - // if (transform.concat instanceof Array) { - // return describeTransform({ - // description: 'concat (' + transform.concat.map(describeAccumulatedTransforms).join(', or ') + ')', - // value: transform.value - // }) - // } else if (transform.raceErrors instanceof Array) { - // return 'race between (' + transform.raceErrors.map(errorMessage).join(', and ') + ')' - // } else { - // return describeTransform(transform) - // } - }).join(', ') -} - -function describeTransform (transform) { - var v = transform.value instanceof Array - ? '[' + transform.value.length + ']' - : '(' + typeof transform.value + ')' - return transform.description + ' ' + v -} - function retryFromOptions (options) { if (options && options.retry) { return options.retry @@ -507,4 +431,14 @@ function retryFromOptions (options) { } } +function runQueryCreator (queryCreator, query): Query { + const q = queryCreator(query) + + if (!(q instanceof Query)) { + throw new Error(`function ${queryCreator} expected to return Query but was: ` + q) + } + + return q +} + module.exports = Query diff --git a/lib/set.js b/lib/set.js index 23c2088..77c470f 100644 --- a/lib/set.js +++ b/lib/set.js @@ -3,9 +3,12 @@ var debug = require('debug')('browser-monkey') var elementEnterText = require('./elementEnterText') var elementInnerText = require('./elementInnerText') const inspect = require('object-inspect') +const object = require('lowscore/object') +const range = require('lowscore/range') +const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default function assertLength (query, length) { - const actualLength = query.value().length + const actualLength = query.result().length if (actualLength !== length) { const error = query.error('expected ' + length + ' elements', { expected: length, actual: actualLength }) error.executedTransforms.prepend(query._executedTransforms) @@ -13,35 +16,51 @@ function assertLength (query, length) { } } -function mapModel (query, elements, model, actions) { - function map (query, model) { - if (model instanceof Array) { - assertLength(query, model.length) +const missing = {} + +function spliceModelArrayFromActual (model, query) { + const length = query.result().length - if (model.length === 0) { - return function () {} + if (length > model.length) { + return range(0, length).map((item, index) => { + const modelItem = model[index] + + if (modelItem !== undefined) { + return modelItem } else { - model.forEach((item, index) => { - map(query.index(index), item) - }) + return missing } - } else if (model.constructor === Object) { - assertLength(query, 1) - Object.entries(model).forEach(([selector, value]) => { - map(query.query(selector), value) + }) + } else { + return model.slice(0, length) + } +} + +function mapModel (query, elements, model, actions) { + function map (query, model) { + if (model === missing) { + return query.result().map(e => e.innerText).join() + } else if (model instanceof Array) { + actions.assertLength(query, model.length) + + return spliceModelArrayFromActual(model, query).map((item, index) => { + return map(query.index(index), item) }) + } else if (model.constructor === Object) { + actions.assertLength(query, 1) + return object(Object.entries(model).map(([selector, value]) => { + return [selector, map(query.query(selector), value)] + })) } else if (typeof model === 'function') { - actions.function(query, model) + return actions.function(query, model) } else { - assertLength(query, 1) + actions.assertLength(query, 1) - actions.value(query, model) + return actions.value(query, model) } } - map(query, model) - - return actions + return map(query, model) } module.exports = { @@ -50,8 +69,12 @@ module.exports = { const setters = [] const actions = { + assertLength: (query, length) => { + assertLength(query, length) + }, + value: (query, model) => { - var setter = query.setter().value() + var setter = query.setter().result() setters.push(() => setter(model)) }, function: (query, model) => { @@ -69,19 +92,70 @@ module.exports = { }, assert: function (model) { - return this.assertion(function (elements) { + return this.expect(elements => { + let isError = false + + const catchError = returnError => (model, fn) => { + try { + fn() + return model + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + isError = true + return returnError && e.actual === undefined ? e : e.actual + } else { + throw e + } + } + } + + const catchFunctionError = catchError(true) + const catchValueError = catchError(false) + const actions = { + assertLength: (query, length) => { + const actualLength = query.result().length + if (actualLength !== length) { + isError = true + } + }, + value: (query, model) => { - var asserter = query.asserter().value() - query.assertion(() => asserter(model)) + try { + var asserter = query.asserter().result() + asserter(model) + return model + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + isError = true + return e.actual + } else { + throw e + } + } }, + function: (query, model) => { - model(query) + try { + model(query) + return model + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + isError = true + return e.actual === undefined ? e : e.actual + } else { + throw e + } + } } } const clone = this.clone().resolve() - mapModel(clone, elements, model, actions) + const result = mapModel(clone, elements, model, actions) + + if (isError) { + this.error('could not match', {expected: model, actual: result}) + } }) }, @@ -92,7 +166,7 @@ module.exports = { }, length: function (length) { - return this.assertion(function (results) { + return this.expect(function (results) { const actualLength = results.length if (actualLength !== length) { this.error('expected ' + length + ' elements', { expected: length, actual: actualLength }) @@ -100,18 +174,6 @@ module.exports = { }) }, - query: function (selector) { - const field = this._options.definitions.fields[selector] - - if (field) { - return field(this) - } else { - const [finderName, value] = selector.split(':') - const finder = this._options.definitions.finders[finderName] - return finder(this, value.trim()) - } - }, - define: function (name, fieldFinder) { this._options.definitions.fields[name] = fieldFinder }, @@ -121,18 +183,18 @@ module.exports = { }, setter: function () { - return this.race(this._options.definitions.setters) + return this.firstOf(this._options.definitions.setters) }, asserter: function () { - return this.race(this._options.definitions.asserters) + return this.firstOf(this._options.definitions.asserters) }, installSetters: function () { this._options.definitions.setters = [ - function (query) { - return query.is(inputsSelector).element().transform(function (element) { - return function (text) { + query => { + return query.is(inputsSelector).expectOneElement().transform(([element]) => { + return text => { if (typeof text !== 'string') { throw new Error('expected string as argument to set input') } @@ -144,13 +206,22 @@ module.exports = { ] this._options.definitions.asserters = [ - function (query) { - return query.element().transform(function (element) { - var self = this - return function (expected) { + query => { + return query.find('input').expectOneElement().transform(([input]) => { + return expected => { + const actual = input.value + if (actual !== expected) { + this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) + } + } + }) + }, + query => { + return query.expectOneElement().transform(([element]) => { + return expected => { const actual = elementInnerText(element) if (actual !== expected) { - self.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) + this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) } } }) diff --git a/react.js b/react.js index 2108091..56924eb 100644 --- a/react.js +++ b/react.js @@ -4,13 +4,13 @@ var ReactDOM = require('react-dom') var createMonkey = require('./create') var createTestDiv = require('./lib/createTestDiv') -module.exports = function (app) { - return new Mount(app, { +module.exports = function (App, props) { + return new Mount(App, { stopApp: function () { }, startApp: function () { var div = createTestDiv() - ReactDOM.render(React.createElement(this.app.constructor, null), div) + ReactDOM.render(React.createElement(this.app, props), div) return createMonkey(document.body) } diff --git a/test/actionsSpec.js b/test/actionsSpec.js index e2e0417..c87e4d2 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -190,13 +190,13 @@ describe('actions', function () { assembly.eventuallyInsertHtml('') - return assembly.assertRejection(promise, 'expected some elements') + return assembly.assertRejection(promise, 'expected just one element') }) it('errors when the input is not a select', function () { var promise = browser.find('.element').select({ text: 'Whatevs' }) assembly.eventuallyInsertHtml('
') - return assembly.assertRejection(promise, 'expected some elements') + return assembly.assertRejection(promise, 'expected just one element') }) it('selects an option using text that is falsy', function () { @@ -316,7 +316,7 @@ describe('actions', function () { var promise = browser.find('.element').typeIn('whatevs') var inputPromise = assembly.eventuallyInsertHtml(html) return inputPromise.then(function (input) { - return assembly.assertRejection(promise, 'expected some elements') + return assembly.assertRejection(promise, 'expected just one element') }) }) }) diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.js index f2181ca..f4ad8cd 100644 --- a/test/assemblies/DomAssembly.js +++ b/test/assemblies/DomAssembly.js @@ -25,7 +25,9 @@ module.exports = class DomAssembly { this.retry = () => {} - return createBrowserMonkey(this._div).options({ + const browserMonkey = createBrowserMonkey(this._div) + + browserMonkey.options({ retry: (retry) => { if (this._normalRetry) { return trytryagain(retry) @@ -54,6 +56,8 @@ module.exports = class DomAssembly { } } }) + + return browserMonkey } async tick () { @@ -144,6 +148,14 @@ module.exports = class DomAssembly { }) } + assertExpectedActual (query, expected, actual) { + return query.assert(expected).then(() => { + throw new Error('expected rejection') + }, e => { + expect(e.actual).to.eql(actual) + }) + } + static hasDom () { return true } diff --git a/test/assertSpec.js b/test/assertSpec.js index eef7e59..cfb8e48 100644 --- a/test/assertSpec.js +++ b/test/assertSpec.js @@ -1,6 +1,7 @@ const describeAssemblies = require('./describeAssemblies') const DomAssembly = require('./assemblies/DomAssembly') var BrowserMonkeyAssertionError = require('../lib/BrowserMonkeyAssertionError').default +const {expect} = require('chai') describe('assert', function () { describeAssemblies([DomAssembly], function (Assembly) { @@ -22,8 +23,8 @@ describe('assert', function () { `) await browser.assert({ - 'css:h1': 'Title', - 'css:.content': 'The Content' + 'h1': 'Title', + '.content': 'The Content' }) }) @@ -34,10 +35,13 @@ describe('assert', function () {
`) - await assembly.assertRejection(browser.assert({ - 'css:h1': 'Title', - 'css:.content': 'The Content' - }), '.content') + await assembly.assertExpectedActual(browser, { + 'h1': 'Title', + '.content': 'The Content' + }, { + 'h1': 'Title', + '.content': undefined + }) }) }) @@ -56,7 +60,7 @@ describe('assert', function () { `) await browser.assert({ - 'css:.result': [ + '.result': [ 'Result 1', 'Result 2', 'Result 3' @@ -64,7 +68,7 @@ describe('assert', function () { }) }) - it('fails when the number of items is wrong', async () => { + it('fails when the number of items is actually greater', async () => { assembly.insertHtml(`
Result 1 @@ -77,12 +81,37 @@ describe('assert', function () {
`) - await assembly.assertRejection(browser.assert({ - 'css:.result': [ + await assembly.assertExpectedActual(browser, { + '.result': [ 'Result 1', 'Result 2' ] - }), 'expected 2 elements') + }, { + '.result': [ + 'Result 1', + 'Result 2', + 'Result 3' + ] + }) + }) + + it('fails when the number of items is actually less', async () => { + assembly.insertHtml(` +
+ Result 1 +
+ `) + + await assembly.assertExpectedActual(browser, { + '.result': [ + 'Result 1', + 'Result 2' + ] + }, { + '.result': [ + 'Result 1', + ] + }) }) it('can assert objects as items', async () => { @@ -98,14 +127,14 @@ describe('assert', function () { `) await browser.assert({ - 'css:.result': [ + '.result': [ { - 'css: h3': 'Title 1', - 'css: .description': 'Description 1' + 'h3': 'Title 1', + '.description': 'Description 1' }, { - 'css: h3': 'Title 2', - 'css: .description': 'Description 2' + 'h3': 'Title 2', + '.description': 'Description 2' } ] }) @@ -122,16 +151,16 @@ describe('assert', function () { `) await browser.assert({ - 'css:h1': 'Title', - 'css:.content': query => { - if (!/Content/.test(query.element().value().innerText)) { + 'h1': 'Title', + '.content': query => { + if (!/Content/.test(query.expectOneElement().result()[0].innerText)) { throw new BrowserMonkeyAssertionError('asdf') } } }) }) - it('fails when query from function fails', async () => { + it('fails when query from function fails with error with actual', async () => { assembly.insertHtml(`

Title

@@ -139,14 +168,40 @@ describe('assert', function () {
`) - await assembly.assertRejection(browser.assert({ - 'css:h1': 'Title', - 'css:.content': query => { - if (/Content/.test(query.element().value().innerText)) { - throw new BrowserMonkeyAssertionError('asdf') + await assembly.assertExpectedActual(browser, { + 'h1': 'Title', + '.content': query => { + if (/Content/.test(query.expectOneElement().result()[0].innerText)) { + throw new BrowserMonkeyAssertionError('asdf', {actual: 'Content'}) } } - }), 'asdf') + }, { + 'h1': 'Title', + '.content': 'Content' + }) + }) + + it('fails when query from function fails with error without actual', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + let error = new BrowserMonkeyAssertionError('asdf') + + await assembly.assertExpectedActual(browser, { + 'h1': 'Title', + '.content': query => { + if (/Content/.test(query.expectOneElement().result()[0].innerText)) { + throw error + } + } + }, { + 'h1': 'Title', + '.content': error + }) }) }) }) diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index 643ecea..e49a351 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -52,7 +52,7 @@ describe('assertions', function () { return Promise.all([ good, - assembly.assertRejection(bad, 'expected some elements') + assembly.assertRejection(bad, 'expected one or more elements') ]) }) }) @@ -87,7 +87,7 @@ describe('assertions', function () { return Promise.all([ good, - assembly.assertRejection(bad, 'expected some elements') + assembly.assertRejection(bad, 'expected one or more elements') ]) }) @@ -180,14 +180,14 @@ describe('assertions', function () { ]) }) - it('treats selects with no value as empty string', function () { + it('cannot assert against selects with no options', function () { assembly.insertHtml('') var select = browser.find('select') return Promise.all([ - select.shouldHave({ value: '' }), - select.shouldHave({ exactValue: '' }) + assembly.assertRejection(select.shouldHave({ value: '' }), 'expected select options'), + assembly.assertRejection(select.shouldHave({ exactValue: '' }), 'expected select options'), ]) }) @@ -398,7 +398,7 @@ describe('assertions', function () { return Promise.all([ good1, - assembly.assertRejection(bad1, 'expected one element'), + assembly.assertRejection(bad1, 'expected just one element'), assembly.assertRejection(bad2, '"a" must be equivalent to "b"') ]) }) diff --git a/test/buttonSpec.js b/test/buttonSpec.js index d966e9d..ca50758 100644 --- a/test/buttonSpec.js +++ b/test/buttonSpec.js @@ -15,49 +15,43 @@ describe('buttons', function () { it('can recognise a button with exact text', function () { var button = assembly.insertHtml('') - return browser.button('Login').then(function (elements) { - demand(elements).to.eql([button]) - }) + const elements = browser.button('Login').result() + demand(elements).to.eql([button]) }) it("doesn't recognise a button without exact text match", function () { assembly.insertHtml('') - return browser.button('Logi').then(function (elements) { - demand(elements).to.eql([]) - }) + const elements = browser.button('Logi').result() + demand(elements).to.eql([]) }) it('can recognise an input of type button with exact value', function () { var button = assembly.insertHtml('') - return browser.button('Login').then(function (elements) { - demand(elements).to.eql([button]) - }) + const elements = browser.button('Login').result() + demand(elements).to.eql([button]) }) it("doesn't recognise an input of type button without exact value match", function () { assembly.insertHtml('') - return browser.button('Logi').then(function (elements) { - demand(elements).to.eql([]) - }) + const elements = browser.button('Logi').result() + demand(elements).to.eql([]) }) it('can recognise a link with exact text', function () { var button = assembly.insertHtml('Login') - return browser.button('Login').then(function (elements) { - demand(elements).to.eql([button]) - }) + const elements = browser.button('Login').result() + demand(elements).to.eql([button]) }) it("doesn't recognise a link without exact text match", function () { assembly.insertHtml('Login') - return browser.button('Logi').then(function (elements) { - demand(elements).to.eql([]) - }) + const elements = browser.button('Logi').result() + demand(elements).to.eql([]) }) describe('defineButton', () => { @@ -66,9 +60,8 @@ describe('buttons', function () { browser.defineButton((monkey, name) => monkey.find('div.button', { exactText: name })) - return browser.button('Login').then(function (elements) { - demand(elements).to.eql([button]) - }) + const elements = browser.button('Login').result() + demand(elements).to.eql([button]) }) it('can define a new button and still use original button definitions', () => { @@ -76,9 +69,8 @@ describe('buttons', function () { browser.defineButton((monkey, name) => monkey.find('div.button', { exactText: name })) - return browser.button('Login').then(function (elements) { - demand(elements).to.eql([button]) - }) + const elements = browser.button('Login').result() + demand(elements).to.eql([button]) }) }) diff --git a/test/fieldsSpec.js b/test/fieldsSpec.js index 7ae890a..28f8086 100644 --- a/test/fieldsSpec.js +++ b/test/fieldsSpec.js @@ -15,18 +15,16 @@ describe('fields', function () { it('can recognise a label with input', () => { const field = assembly.insertHtml('').querySelector('input') - return browser.field('Email').then(function (elements) { - demand(elements).to.eql([field]) - }) + const elements = browser.field('Email').result() + demand(elements).to.eql([field]) }) it('can recognise an input with aria-labelledby', () => { assembly.insertHtml('') const field = assembly.insertHtml('') - return browser.field('Email').then(function (elements) { - demand(elements).to.eql([field]) - }) + const elements = browser.field('Email').result() + demand(elements).to.eql([field]) }) describe('defineField', () => { @@ -36,9 +34,8 @@ describe('fields', function () { const customFieldBrowser = browser.defineField((monkey, name) => monkey.find('div.field').containing('.field-name', { exactText: name }).find('input')) - return customFieldBrowser.field('Email').then(function (elements) { - demand(elements).to.eql([field]) - }) + const elements = customFieldBrowser.field('Email').result() + demand(elements).to.eql([field]) }) it('can define a new field and still use original field definitions', () => { @@ -46,9 +43,8 @@ describe('fields', function () { const customFieldBrowser = browser.defineField((monkey, name) => monkey.find('div.field').containing('.field-name', { exactText: name }).find('input')) - return customFieldBrowser.field('Email').then(function (elements) { - demand(elements).to.eql([field]) - }) + const elements = customFieldBrowser.field('Email').result() + demand(elements).to.eql([field]) }) }) @@ -57,11 +53,11 @@ describe('fields', function () { const fieldDiv = assembly.insertHtml('
') const custom = browser.defineFieldValue({ - set: (monkey, value) => monkey.is('div.nomatch').one().action((element) => { element.innerText = 'no-match: ' + value }), - get: (monkey) => monkey.is('div.nomatch').one().text() + set: (monkey, value) => monkey.is('div.nomatch').expectOneElement().action(([element]) => { element.innerText = 'no-match: ' + value }), + get: (monkey) => monkey.is('div.nomatch').expectOneElement().text() }).defineFieldValue({ - set: (monkey, value) => monkey.is('div.field').one().action((element) => { element.innerText = 'match: ' + value }), - get: (monkey) => monkey.is('div.field').one().text() + set: (monkey, value) => monkey.is('div.field').expectOneElement().action(([element]) => { element.innerText = 'match: ' + value }), + get: (monkey) => monkey.is('div.field').expectOneElement().text() }) await custom.find('.field').setValue('value') @@ -72,14 +68,14 @@ describe('fields', function () { assembly.insertHtml('
') const custom = browser.defineFieldValue({ - set: (monkey, value) => monkey.is('div.nomatch').one().action((element) => { element.innerText = 'no-match: ' + value }), - get: (monkey) => monkey.is('div.nomatch').one().text() + set: (monkey, value) => monkey.is('div.nomatch').expectOneElement().action(([element]) => { element.innerText = 'no-match: ' + value }), + get: (monkey) => monkey.is('div.nomatch').expectOneElement().text() }).defineFieldValue({ - set: (monkey, value) => monkey.is('div.field').one().action((element) => { element.innerText = 'match: ' + value }), - get: (monkey) => monkey.is('div.field').one().text() + set: (monkey, value) => monkey.is('div.field').expectOneElement().action(([element]) => { element.innerText = 'match: ' + value }), + get: (monkey) => monkey.is('div.field').expectOneElement().text() }) - await demand(custom.find('.non-field').setValue('value')).to.reject.with.error(/all queries failed in race/) + await demand(custom.find('.non-field').setValue('value')).to.reject.with.error(/all queries failed in firstOf/) }) }) @@ -201,7 +197,7 @@ describe('fields', function () { describe('shouldHave value', () => { it('uses .value in shouldHave({exactValue})', async () => { const custom = browser.defineFieldValue({ - get: (monkey) => monkey.mapAll(() => 'div value') + get: (monkey) => monkey.expect(() => {}).transform(() => 'div value') }) await custom.shouldHave({ exactValue: 'div value' }) @@ -209,7 +205,7 @@ describe('fields', function () { it('uses .value in shouldHave({value})', async () => { const custom = browser.defineFieldValue({ - get: (monkey) => monkey.mapAll(() => 'div value') + get: (monkey) => monkey.expect(() => {}).transform(() => 'div value') }) await custom.shouldHave({ value: 'value' }) diff --git a/test/findersSpec.js b/test/findersSpec.js index d28355d..412dcc5 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -32,29 +32,29 @@ describe('find', function () { it('should eventually find an element using a filter', async function () { var foundElement = browser.find('.element').filter(function (element) { return element.classList.contains('correct') - }, 'has class "correct"').element() + }, 'has class "correct"').expectOneElement() assembly.insertHtml('
') const correctElement = await assembly.eventuallyInsertHtml('
') - demand(await foundElement).to.equal(correctElement) + demand(await foundElement).to.eql([correctElement]) }) it('should eventually find an element with the right text', function () { - var promise = browser.find('.element', { text: 'green' }).element() + var promise = browser.find('.element', { text: 'green' }).expectOneElement() assembly.insertHtml('
') assembly.eventuallyInsertHtml('
red
blue
green
') - return promise.then(function (element) { - demand(element).to.equal(assembly.find('#green')) + return promise.then(function (elements) { + demand(elements).to.eql([assembly.find('#green')]) }) }) it('filter fails with the right message', function () { var promise = browser.find('.element').filter(function (element) { return element.classList.contains('correct') - }, 'has class "correct"').element() + }, 'has class "correct"').expectOneElement() assembly.insertHtml('
') assembly.eventuallyInsertHtml('
') @@ -103,7 +103,7 @@ describe('find', function () { assembly.useNormalRetry() - return browser.find('iframe').element().then(function (iframe) { + return browser.find('iframe').expectOneElement().then(function ([iframe]) { return browser.scope(iframe).find('h1', { text: 'Page 2' }).shouldExist() }) }, { vdom: false }) @@ -123,7 +123,8 @@ describe('find', function () { assembly.eventuallyDoNothing() - return browser.options({ visibleOnly: false }).find('.element > span').shouldExist() + browser.options({ visibleOnly: false }) + return browser.find('.element > span').shouldExist() }) it('should find elements that are visually hidden because of how html renders them', function () { @@ -148,48 +149,24 @@ describe('find', function () { }) it('element returns the outer element', function () { - var promise = browser.find('.outer').containing('.inner').element() + var promise = browser.find('.outer').containing('.inner').expectOneElement() assembly.eventuallyInsertHtml( '
bad
' + '
good
' ) - return promise.then(function (element) { + return promise.then(function ([element]) { demand(element).to.eql(assembly.find('#good')) }) }) - it('errors with a usable css selector if it cant find something', function () { - var promise = browser.find('.outer').find('.not-there').element() - - assembly.eventuallyInsertHtml('
bad
') - - return demand(promise).reject.with.error(/expected some elements/) - }) - - it('errors with a usable css selector if it cant find an element containing another', function () { - var promise = browser.find('.outer').containing('.not-there').shouldExist() - - assembly.eventuallyInsertHtml('
bad
') - - return demand(promise).reject.with.error(/expected some elements/) - }) - it("fails if it can't find an element containing another", function () { var promise = browser.find('.outer').containing('.inner').shouldExist() assembly.eventuallyInsertHtml('
bad
') - return demand(promise).reject.with.error(/expected some elements/) - }) - - it('errors with a usable css selector if it cant find an element containing another', function () { - var promise = browser.find('.outer').containing('.not-there').shouldExist() - - assembly.eventuallyInsertHtml('
bad
') - - return demand(promise).reject.with.error(/expected some elements/) + return demand(promise).reject.with.error(/expected one or more elements/) }) it("fails if it can't find an element containing another", function () { diff --git a/test/optionsSpec.js b/test/optionsSpec.js index 4c8977c..a4726d8 100644 --- a/test/optionsSpec.js +++ b/test/optionsSpec.js @@ -12,15 +12,10 @@ describe('options', function () { browser = assembly.browserMonkey() }) - it('can set an option that is inerhited by components', function () { - var parentComponent = browser.find('div').component({}) - var childComponent = parentComponent.options({ myOption: 'abc' }).find('div') - demand(childComponent._options.myOption).to.equal('abc') - }) - it('can overide default timeout', function () { var defaultTimeout = browser.options().timeout - const timeoutBrowser = browser.options({ timeout: 1 }) + const timeoutBrowser = browser.clone() + timeoutBrowser.options({ timeout: 1 }) var start = new Date() return timeoutBrowser.find('.doesnt-exist').shouldExist().catch(function () { diff --git a/test/querySpec.js b/test/querySpec.js index 69f6649..777f3cf 100644 --- a/test/querySpec.js +++ b/test/querySpec.js @@ -14,13 +14,13 @@ describe('query', () => { }) it('can find an element by DOM selector', async () => { - const selectedElementPromise = browserMonkey.css('.test').one() + const selectedElementPromise = browserMonkey.find('.test').expectOneElement() const insertedElementPromise = assembly.eventuallyInsertHtml( `
` ) - const selectedElement = await selectedElementPromise + const [selectedElement] = await selectedElementPromise const insertedElement = await insertedElementPromise expect(selectedElement).to.equal(insertedElement) @@ -28,9 +28,8 @@ describe('query', () => { describe('options', () => { it('returns a new Browser Monkey object without modifying the current one', () => { - const withOptions = browserMonkey.options({ a: 'a' }) - expect(browserMonkey._options.a).to.equal(undefined) - expect(withOptions._options.a).to.equal('a') + browserMonkey.options({ a: 'a' }) + expect(browserMonkey._options.a).to.equal('a') }) }) @@ -45,8 +44,9 @@ describe('query', () => { }) it('passes through the options', () => { + browserMonkey.options({ a: 'a' }) + const monkey = browserMonkey - .options({ a: 'a' }) .component({ aMethod: function () { return 'aMethod' } }) @@ -55,28 +55,30 @@ describe('query', () => { }) it('passes through the input', () => { + browserMonkey.input('a') + const monkey = browserMonkey - .input('a') .component({ aMethod: function () { return 'aMethod' } }) - expect(monkey._value).to.equal('a') + expect(monkey._input).to.equal('a') }) it('passes through the maps', async () => { + browserMonkey.input(1) + const monkey = browserMonkey - .input(1) .transform(x => x + 1) .component({ aMethod: function () { return 'aMethod' } }) - expect(await monkey).to.equal(2) + expect(monkey.result()).to.equal(2) }) }) - describe('zero', () => { + describe('expectNoElements', () => { it('passes when there are no elements found', async () => { assembly.insertHtml(`
@@ -86,8 +88,8 @@ describe('query', () => { `) const contacts = browserMonkey - .css('.contact') - .zero() + .find('.contact') + .expectNoElements() assembly.eventuallyDeleteHtml('.contact') @@ -107,19 +109,19 @@ describe('query', () => { `) const contacts = browserMonkey - .css('.contact') - .zero() + .find('.contact') + .expectNoElements() assembly.eventuallyDeleteHtml('.message') await expect(contacts).reject.with.error(/expected no elements/) }) }) - describe('one', () => { + describe('expectOneElement', () => { it('when there is one element, selects it', async () => { const contacts = browserMonkey - .css('.contact') - .one() + .find('.contact') + .expectOneElement() assembly.eventuallyInsertHtml(`
@@ -128,13 +130,13 @@ describe('query', () => {
`) - expect(await contacts).to.eql(assembly.find('.contact:nth-child(1)')) + expect(await contacts).to.eql([assembly.find('.contact:nth-child(1)')]) }) it('when there is more than one element, throws an error', async () => { const contacts = browserMonkey - .css('.contact') - .one() + .find('.contact') + .expectOneElement() assembly.eventuallyInsertHtml(`
@@ -147,44 +149,28 @@ describe('query', () => {
`) - await expect(contacts).reject.with.error(/expected one element/) + await expect(contacts).reject.with.error(/expected just one element, found 2/) }) it('when there no elements, throws an error', async () => { const contacts = browserMonkey - .css('.contact') - .one() + .find('.contact') + .expectOneElement() assembly.eventuallyInsertHtml(`
`) - await expect(contacts).reject.with.error(/expected one element/) - }) - - it('when there is already one result, no array, selects it', async () => { - const contacts = browserMonkey - .css('.contact') - .one() - .one() - - assembly.eventuallyInsertHtml(` -
-
Sally
-
32 Yellow Drive
-
- `) - - expect(await contacts).to.eql(assembly.find('.contact:nth-child(1)')) + await expect(contacts).reject.with.error(/expected just one element, found 0/) }) }) - describe('some', () => { + describe('expectSomeElements', () => { it('when there are one or more elements, selects them', async () => { const contacts = browserMonkey - .css('.contact') - .some() + .find('.contact') + .expectSomeElements() assembly.eventuallyInsertHtml(`
@@ -202,59 +188,26 @@ describe('query', () => { it('when there no elements, throws an error', async () => { const contacts = browserMonkey - .css('.contact') - .some() + .find('.contact') + .expectSomeElements() assembly.eventuallyInsertHtml(`
`) - await expect(contacts).reject.with.error(/expected some elements/) - }) - }) - - describe('map', () => { - it('can map elements', async () => { - const contacts = browserMonkey - .css('.contact') - .map(contact => ({ - name: contact.querySelector('.name').innerText, - address: contact.querySelector('.address').innerText - })) - .some() - - assembly.eventuallyInsertHtml(` -
-
Sally
-
32 Yellow Drive
-
-
-
Bob
-
32 Red Drive
-
- `) - - expect(await contacts).to.eql([ - { - name: 'Sally', - address: '32 Yellow Drive' - }, - { - name: 'Bob', - address: '32 Red Drive' - } - ]) + await expect(contacts).reject.with.error(/expected one or more elements, found 0/) }) }) describe('input', () => { it('input sets the input used in transform', async () => { + browserMonkey.input('a') + const monkey = browserMonkey - .input('a') .transform(x => `input: ${x}`) - expect(await monkey).to.eql('input: a') + expect(monkey.result()).to.eql('input: a') }) }) @@ -263,14 +216,14 @@ describe('query', () => { const monkey = browserMonkey .scope(document.body) - expect(monkey._value).to.eql([document.body]) + expect(monkey._input).to.eql([document.body]) }) }) - describe('ensure', () => { + describe('expect', () => { it('waits for the input to eventually pass the assertion', async () => { const hello = browserMonkey - .ensure(elements => { + .expect(elements => { expect(elements.some(element => element.innerText.includes('hello'))).to.equal(true) }) @@ -283,7 +236,7 @@ describe('query', () => { it('eventually throws the last error if it never passes', async () => { const hello = browserMonkey - .ensure(elements => { + .expect(elements => { expect(elements.some(element => element.innerText.includes('hello')), 'expected to see hello').to.equal(true) }) @@ -298,11 +251,11 @@ describe('query', () => { describe('transform', () => { it('can map all the elements', async () => { const contacts = browserMonkey - .css('.name') + .find('.name') .transform(names => { return names.map(contact => contact.innerText).join(', ') }) - .ensure(x => expect(x).to.not.be.empty()) + .expect(x => expect(x).to.not.be.empty()) assembly.eventuallyInsertHtml(`
Sally
@@ -316,11 +269,11 @@ describe('query', () => { describe('filter', () => { it('can filter elements', async () => { const contacts = browserMonkey - .css('.contact') + .find('.contact') .filter(contact => { return contact.querySelector('.name').innerText === 'Sally' }) - .one() + .expectOneElement() assembly.eventuallyInsertHtml(`
@@ -333,7 +286,7 @@ describe('query', () => {
`) - expect(await contacts).to.eql(assembly.find('.contact:nth-child(1)')) + expect(await contacts).to.eql([assembly.find('.contact:nth-child(1)')]) }) }) @@ -345,7 +298,7 @@ describe('query', () => {
A
`) - const action = browserMonkey.css('div').action(function () { + const action = browserMonkey.find('div').action(function () { actionExecuted++ }) @@ -364,7 +317,7 @@ describe('query', () => { `) let givenElements - const action = browserMonkey.css('div').action(function (els) { + const action = browserMonkey.find('div').action(function (els) { givenElements = els }) @@ -377,10 +330,10 @@ describe('query', () => { describe('errors', () => { it('shows what it was able to map', async () => { const name = browserMonkey - .css('.container') - .css('.contact') - .css('.name') - .one() + .find('.container') + .find('.contact') + .find('.name') + .expectOneElement() assembly.eventuallyInsertHtml(`
@@ -392,7 +345,7 @@ describe('query', () => {
`) - await expect(name).to.reject.with.error("expected one element (found: find('.container') [1], find('.contact') [2], find('.name') [0])") + await expect(name).to.reject.with.error("expected just one element, found 0 (found: find('.container') [1], find('.contact') [2], find('.name') [0])") }) }) @@ -408,10 +361,10 @@ describe('query', () => {
C
`) - const elementsFound = await browserMonkey.concat([ - b => b.css('.a'), - b => b.css('.b') - ]) + const elementsFound = browserMonkey.concat([ + b => b.find('.a'), + b => b.find('.b') + ]).result() expect(elementsFound).to.eql([a, b]) }) @@ -428,37 +381,49 @@ describe('query', () => { `) const promise = browserMonkey.concat([ - b => b.css('.a'), - b => b.css('.b') - ]).css('.child').some() + b => b.find('.a'), + b => b.find('.b') + ]).find('.child').expectSomeElements() - return expect(promise).to.reject.with.error("expected some elements (found: concat (find('.a') [1], find('.b') [1]) [2], find('.child') [0])") + return expect(promise).to.reject.with.error("expected one or more elements, found 0 (found: concat (find('.a') [1], find('.b') [1]) [2], find('.child') [0])") }) }) - describe('race', () => { + describe('firstOf', () => { it('finds the first of two or more queries', async () => { - const promise = browserMonkey.race([ - b => b.css('.a').one(), - b => b.css('.b').one() + const promise = browserMonkey.firstOf([ + b => b.find('.a').expectSomeElements(), + b => b.find('.b').expectSomeElements() ]) const bPromise = assembly.eventuallyInsertHtml('
B
') - const b = await promise + const foundB = await promise + const actualB = [await bPromise] - expect(b).to.eql(await bPromise) + expect(foundB).to.eql(actualB) }) it('returns descriptions of all queries used when none are successful', async () => { assembly.insertHtml('
content
C
') - const promise = browserMonkey.css('.content').race([ - b => b.css('.a').some(), - b => b.css('.b').some() + const promise = browserMonkey.find('.content').firstOf([ + b => b.find('.a').expectSomeElements(), + b => b.find('.b').expectSomeElements() + ]) + + await expect(promise).to.reject.with.error("all queries failed in firstOf (found: find('.content') [1], firstOf(expected one or more elements, found 0 (found: find('.a') [0]), expected one or more elements, found 0 (found: find('.b') [0])) [0])") + }) + + it('throws if one of the queries does not have an assertion or action', async () => { + assembly.insertHtml('
content
C
') + + const promise = browserMonkey.find('.content').firstOf([ + b => b.find('.a'), + b => b.find('.b').expectSomeElements() ]) - await expect(promise).to.reject.with.error("all queries failed in race (found: find('.content') [1], or(expected some elements (found: find('.a') [0]), expected some elements (found: find('.b') [0])) [0])") + await expect(promise).to.reject.with.error(/no expectations or actions in query/) }) }) }) diff --git a/test/reloadButtonSpec.js b/test/reloadButtonSpec.js index 31426bd..19dc19e 100644 --- a/test/reloadButtonSpec.js +++ b/test/reloadButtonSpec.js @@ -27,7 +27,7 @@ describe('reload button', function () { reloadButton() reloadButton() - return browser.find('a', { text: '⟳ reload' }).elements().then(function (elements) { + return browser.find('a', { text: '⟳ reload' }).expectSomeElements().then(function (elements) { expect(elements.length).to.eq(1) }) }, { vdom: false }) diff --git a/test/scopeSpec.js b/test/scopeSpec.js index f1e3755..54864c4 100644 --- a/test/scopeSpec.js +++ b/test/scopeSpec.js @@ -16,11 +16,11 @@ describe('scope', function () { var red = assembly.insertHtml('
red
') var blue = assembly.insertHtml('
blue
') - return browser.scope(red).find('.element').element().then(function (element) { + return browser.scope(red).find('.element').expectOneElement().then(function ([element]) { demand(element.innerText).to.equal('red') }).then(function () { - return browser.scope(blue).find('.element').element() - }).then(function (element) { + return browser.scope(blue).find('.element').expectOneElement() + }).then(function ([element]) { demand(element.innerText).to.equal('blue') }) }) diff --git a/test/setSpec.js b/test/setSpec.js index 6ab4ea0..56ba02d 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -22,9 +22,9 @@ describe('set', function () { `) await browser.set({ - 'css:.address': '7 Lola St', - 'css:.phone': '123123123', - 'css:.first-name': 'Barry' + '.address': '7 Lola St', + '.phone': '123123123', + '.first-name': 'Barry' }) demand(assembly.find('.address').value).to.equal('7 Lola St') @@ -48,14 +48,14 @@ describe('set', function () { `) await browser.set({ - 'css:.address': { - 'css:.street': '7 Lola St', - 'css:.city': 'Frisby City', - 'css:.country': 'Atlantis', - 'css:.postcode': '12345' + '.address': { + '.street': '7 Lola St', + '.city': 'Frisby City', + '.country': 'Atlantis', + '.postcode': '12345' }, - 'css:.phone': '123123123', - 'css:.first-name': 'Barry' + '.phone': '123123123', + '.first-name': 'Barry' }) demand(assembly.find('.address .street').value).to.equal('7 Lola St') @@ -77,8 +77,8 @@ describe('set', function () { `) await browser.set({ - 'css:.address': { - 'css:.street': '7 Lola St' + '.address': { + '.street': '7 Lola St' } }) @@ -104,10 +104,10 @@ describe('set', function () { `) await browser.set({ - 'css:.address': [ - { 'css:.street': '1' }, - { 'css:.street': '2' }, - { 'css:.street': '3' } + '.address': [ + { '.street': '1' }, + { '.street': '2' }, + { '.street': '3' } ] }) @@ -133,9 +133,9 @@ describe('set', function () { await assembly.assertRejection( browser.set({ - 'css:.address': [ - { 'css:.street': '1' }, - { 'css:.street': '2' } + '.address': [ + { '.street': '1' }, + { '.street': '2' } ] }), 'expected 2 elements' @@ -157,7 +157,7 @@ describe('set', function () { await assembly.assertRejection( browser.set({ - 'css:.address': [ + '.address': [ ] }), 'expected 0 elements' @@ -176,8 +176,8 @@ describe('set', function () { `) await browser.set({ - 'css: .street': streetQuery => { - streetQuery.element().value().innerText = 'hi' + '.street': streetQuery => { + streetQuery.expectOneElement().result()[0].innerText = 'hi' } }) @@ -194,9 +194,9 @@ describe('set', function () { `) const promise = browser.set({ - 'css:.street': '7 Lola St', - 'css:.phone': '123123123', - 'css:.first-name': 'Barry' + '.street': '7 Lola St', + '.phone': '123123123', + '.first-name': 'Barry' }) await assembly.assertRejection(promise, 'expected 1 elements') @@ -212,8 +212,8 @@ describe('set', function () { `) - browser.define('phone', b => b.css('.phone')) - browser.define('firstName', b => b.css('.first-name')) + browser.define('phone', b => b.find('.phone')) + browser.define('firstName', b => b.find('.first-name')) await browser.set({ phone: '123123123', @@ -232,11 +232,11 @@ describe('set', function () { `) - browser.defineFinder('form', (b, name) => b.css('* [name=' + JSON.stringify(name) + ']')) + browser.defineFinder('form', (b, name) => b.find('* [name=' + JSON.stringify(name) + ']')) await browser.set({ - 'form: phone': '123123123', - 'form: firstname': 'Barry' + 'form! phone': '123123123', + 'form! firstname': 'Barry' }) demand(assembly.find('.phone').value).to.equal('123123123') diff --git a/tsconfig.json b/tsconfig.json index a43e330..0e1fd28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,5 +6,9 @@ "target": "ES5", "outDir": "dist", "rootDir": "." - } + }, + "exclude": [ + "node_modules", + "dist" + ] } From 6e6b3b163120e26068596a8743fc002b4f98724e Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Sun, 16 Jun 2019 11:02:29 +0200 Subject: [PATCH 018/127] removed resolved mode for queries --- lib/ExecutedTransformSequence.ts | 7 +- lib/finders.js | 2 +- lib/query.ts | 132 +++----- lib/set.js | 88 +++--- package.json | 5 +- test/actionsSpec.js | 36 +-- test/assemblies/DomAssembly.js | 65 ++-- test/assertionsSpec.js | 105 ++++--- test/componentSpec.js | 12 +- test/fieldsSpec.js | 7 +- test/findersSpec.js | 28 +- test/querySpec.js | 16 +- test/setSpec.js | 35 ++- yarn.lock | 513 ++++++++++++++++++++++++------- 14 files changed, 665 insertions(+), 386 deletions(-) diff --git a/lib/ExecutedTransformSequence.ts b/lib/ExecutedTransformSequence.ts index 73b89dc..26bd095 100644 --- a/lib/ExecutedTransformSequence.ts +++ b/lib/ExecutedTransformSequence.ts @@ -13,6 +13,11 @@ export class ExecutedTransformSequence extends ExecutedTransform { this.transforms.push(transform) } + public addTransforms (transforms: ExecutedTransform[]): void { + this.value = transforms[transforms.length - 1].value + this.transforms.push(...transforms) + } + public prepend (transforms: ExecutedTransformSequence): void { this.value = transforms.value this.transforms.unshift(...transforms.transforms) @@ -37,7 +42,7 @@ export class ExecutedTransformSequence extends ExecutedTransform { keepTaking: true }).array - return this.transforms.map(t => t.renderError()).filter(Boolean).join(', ') + return transformsUpUntilAndIncludingFirstFailure.map(t => t.renderError()).filter(Boolean).join(', ') } public clone (): ExecutedTransformSequence { diff --git a/lib/finders.js b/lib/finders.js index 251be7e..77e7dee 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -7,7 +7,7 @@ var inspect = require('object-inspect') const { ExecutedSimpleTransform } = require('./ExecutedSimpleTransform') module.exports = { - find: function (selector, findOptions) { + css: function (selector, findOptions) { const findElements = this.transform(elements => { expectElements(elements) return new ExecutedSimpleTransform(flatten(elements.map(element => { diff --git a/lib/query.ts b/lib/query.ts index f67fe03..b24d36e 100644 --- a/lib/query.ts +++ b/lib/query.ts @@ -1,3 +1,4 @@ +import { ExecutedTransform } from './ExecutedTransform' import { ExecutedTransformSequence } from './ExecutedTransformSequence' import { ExecutedSimpleTransform } from './ExecutedSimpleTransform' import { ExecutedConcatTransform } from './ExecutedConcatTransform' @@ -14,11 +15,11 @@ var debug = require('debug')('browser-monkey') var inputsSelector = require('./inputsSelector') const elementEnterText = require('./elementEnterText') -type Transform = (any) => any +type Transform = (elements: any, executedTransforms: ExecutedTransform[]) => any +type Action = (elements: any, executedTransforms: ExecutedTransform[]) => void class Query { private _transforms: Transform[] - private _executedTransforms: ExecutedTransformSequence private _options: { visibleOnly: boolean timeout: number @@ -39,12 +40,10 @@ class Query { private _input: any private _actionExecuted: any - private _action: (value: any) => void - private _isResolved: boolean + private _action: Action private _hasExpectation: boolean public constructor () { - this._isResolved = false this._hasExpectation = false this._transforms = [] this._options = { @@ -131,14 +130,7 @@ class Query { } public transform (transform: Transform): this { - if (this._isResolved) { - return this.clone(clone => { - const executedTransform = toExecutedTransform(transform.call(this, this._executedTransforms.value)) - clone._executedTransforms.addTransform(executedTransform) - }) - } else { - return this.clone(clone => clone._transforms.push(transform)) - } + return this.clone(clone => clone._transforms.push(transform)) } public expect (expectation: (any) => void): this { @@ -152,52 +144,36 @@ class Query { return expectQuery } - public action (action: (any) => void): any { + public action (action: Action): any { if (this._action) { throw new Error('can only have one action') } - if (this._isResolved) { - if (!this._actionExecuted) { - action.call(this, this._executedTransforms.value) - this._actionExecuted = true - } - return this - } else { - return this.clone(clone => { - clone._action = action - }) - } + return this.clone(clone => { + clone._action = action + }) } public result (): any { - if (this._isResolved) { - return this._executedTransforms.value - } else { - return toExecutedTransform(this.execute()).value - } + return this.execute().value } - public resolve (): this { - if (!this._isResolved) { - const resolved = this.clone() - resolved._executedTransforms = resolved.execute() - resolved._isResolved = true - return resolved - } else { - return this - } + public resolve (input: any): this { + const resolved = this.clone() + resolved._input = input + resolved._transforms = [] + return resolved } - public filter (filter, description): this { + public filter (filter: (a: any) => boolean, description: string): this { return this.transform((elements) => { return new ExecutedSimpleTransform(elements.filter(filter), description) }) } - public concat (queryCreators): this { - return this.transform(() => { - const resolved = this.resolve() + public concat (queryCreators: [(q: Query) => Query]): this { + return this.transform((elements) => { + const resolved = this.resolve(elements) var transforms = queryCreators.map(queryCreator => runQueryCreator(queryCreator, resolved).execute()) @@ -207,25 +183,20 @@ class Query { } public error (message: string, {expected = undefined, actual = undefined} = {}): void { - if (this._isResolved) { - throw new BrowserMonkeyAssertionError(message, { expected, actual, executedTransforms: this._executedTransforms }) - } else { - throw new BrowserMonkeyAssertionError(message, { expected, actual }) - } + throw new BrowserMonkeyAssertionError(message, { expected, actual }) } - private executeQuery (input: any): ExecutedTransformSequence { - const query = this - const transformSequence = new ExecutedTransformSequence(input) + private execute (): ExecutedTransformSequence { + const transformSequence = new ExecutedTransformSequence(this._input) try { this._transforms.forEach(transform => { - const executedTransform = toExecutedTransform(transform.call(query, transformSequence.value)) + const executedTransform = toExecutedTransform(transform.call(this, transformSequence.value, transformSequence.transforms)) transformSequence.addTransform(executedTransform) }) if (this._action && !this._actionExecuted) { - this._action(transformSequence.value) + this._action(transformSequence.value, transformSequence.transforms) this._actionExecuted = true } @@ -239,18 +210,9 @@ class Query { } } - private execute (): ExecutedTransformSequence { - if (!this._isResolved) { - return this.executeQuery(this._input) - } else { - return this._executedTransforms - } - } - - private firstOf (queryCreators): this { - const transformed = this.transform(() => { - const resolved = this.resolve().clone() - resolved._executedTransforms.clear() + public firstOf (queryCreators: [(q: Query) => Query]): this { + const transformed = this.transform((elements) => { + const resolved = this.resolve(elements) var values = queryCreators.map(query => { try { @@ -292,25 +254,28 @@ class Query { return transformed } - public query (selector: string): this { - const field = this._options.definitions.fields[selector] + public find (selector: string, options = undefined): this { + // name(value) + const match = /^\s*(.*?)\s*(\((.*)\)\s*)?$/.exec(selector) - if (field) { - return field(this) - } else { - const match = /(.*?)!(.*)/.exec(selector) + if (match) { + const [, name,, value] = match + const finder = this._options.definitions.fields[name] - if (match) { - const [, finderName, value] = match - const finder = this._options.definitions.finders[finderName] + if (finder) { + if (options) { + throw new Error('options not used here') + } - if (finder) { + if (value !== undefined) { return finder(this, value.trim()) + } else { + return finder(this) } } } - return this.find(selector) + return this.css(selector, options) } public options (options): Options { @@ -321,7 +286,7 @@ class Query { } } - private assertHasActionOrExpectation () { + private assertHasActionOrExpectation (): void { if (!this._hasExpectation && !this._action) { throw new Error('no expectations or actions in query, use .result(), or add an expectation or an action') } @@ -351,6 +316,7 @@ class Query { throw error }) + return promise.then.apply(promise, args) } @@ -368,10 +334,6 @@ class Query { } public input (value): void { - if (this._isResolved) { - throw new Error('cannot change input, query is already resolved') - } - this._input = value } @@ -397,15 +359,11 @@ class Query { } } -function copySelectorFields (from, to) { +function copySelectorFields (from: Query, to: Query): void { to._transforms = from._transforms.slice() to._input = from._input to._hasExpectation = from._hasExpectation - to._isResolved = from._isResolved - if (from._isResolved) { - to._executedTransforms = from._executedTransforms.clone() - } to._options = extend({}, from._options) to._options.definitions = {} @@ -431,7 +389,7 @@ function retryFromOptions (options) { } } -function runQueryCreator (queryCreator, query): Query { +function runQueryCreator (queryCreator: (q: Query) => Query, query: Query): Query { const q = queryCreator(query) if (!(q instanceof Query)) { diff --git a/lib/set.js b/lib/set.js index 77c470f..46ea524 100644 --- a/lib/set.js +++ b/lib/set.js @@ -2,18 +2,22 @@ var inputsSelector = require('./inputsSelector') var debug = require('debug')('browser-monkey') var elementEnterText = require('./elementEnterText') var elementInnerText = require('./elementInnerText') +var elementSelect = require('./elementSelect') const inspect = require('object-inspect') const object = require('lowscore/object') const range = require('lowscore/range') +const pluralize = require('pluralize') const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default function assertLength (query, length) { - const actualLength = query.result().length - if (actualLength !== length) { - const error = query.error('expected ' + length + ' elements', { expected: length, actual: actualLength }) - error.executedTransforms.prepend(query._executedTransforms) - throw error - } + query.expect(elements => { + const actualLength = elements.length + if (actualLength !== length) { + const error = query.error('expected ' + length + ' ' + pluralize('elements', length) + ', found ' + actualLength, { expected: length, actual: actualLength }) + error.executedTransforms.prepend(query._executedTransforms) + throw error + } + }).result() } const missing = {} @@ -36,7 +40,7 @@ function spliceModelArrayFromActual (model, query) { } } -function mapModel (query, elements, model, actions) { +function mapModel (query, model, actions) { function map (query, model) { if (model === missing) { return query.result().map(e => e.innerText).join() @@ -49,7 +53,7 @@ function mapModel (query, elements, model, actions) { } else if (model.constructor === Object) { actions.assertLength(query, 1) return object(Object.entries(model).map(([selector, value]) => { - return [selector, map(query.query(selector), value)] + return [selector, map(query.find(selector), value)] })) } else if (typeof model === 'function') { return actions.function(query, model) @@ -74,16 +78,17 @@ module.exports = { }, value: (query, model) => { - var setter = query.setter().result() - setters.push(() => setter(model)) + var setter = query.setter(model).result() + setters.push(() => setter()) }, + function: (query, model) => { setters.push(() => model(query)) } } - const clone = this.clone().resolve() - mapModel(clone, elements, model, actions) + const clone = this.resolve(elements) + mapModel(clone, model, actions) setters.forEach(set => { set() @@ -95,23 +100,6 @@ module.exports = { return this.expect(elements => { let isError = false - const catchError = returnError => (model, fn) => { - try { - fn() - return model - } catch (e) { - if (e instanceof BrowserMonkeyAssertionError) { - isError = true - return returnError && e.actual === undefined ? e : e.actual - } else { - throw e - } - } - } - - const catchFunctionError = catchError(true) - const catchValueError = catchError(false) - const actions = { assertLength: (query, length) => { const actualLength = query.result().length @@ -150,8 +138,8 @@ module.exports = { } } - const clone = this.clone().resolve() - const result = mapModel(clone, elements, model, actions) + const clone = this.resolve(elements) + const result = mapModel(clone, model, actions) if (isError) { this.error('could not match', {expected: model, actual: result}) @@ -165,15 +153,6 @@ module.exports = { }, 'index ' + index) }, - length: function (length) { - return this.expect(function (results) { - const actualLength = results.length - if (actualLength !== length) { - this.error('expected ' + length + ' elements', { expected: length, actual: actualLength }) - } - }) - }, - define: function (name, fieldFinder) { this._options.definitions.fields[name] = fieldFinder }, @@ -182,8 +161,8 @@ module.exports = { this._options.definitions.finders[name] = finder }, - setter: function () { - return this.firstOf(this._options.definitions.setters) + setter: function (model) { + return this.firstOf(this._options.definitions.setters.map(s => s(model))) }, asserter: function () { @@ -192,16 +171,31 @@ module.exports = { installSetters: function () { this._options.definitions.setters = [ - query => { + model => query => { return query.is(inputsSelector).expectOneElement().transform(([element]) => { - return text => { - if (typeof text !== 'string') { + return () => { + if (typeof model !== 'string') { throw new Error('expected string as argument to set input') } - debug('set', element, text) - elementEnterText(element, text) + debug('set', element, model) + elementEnterText(element, model) } }) + }, + model => query => { + return query + .is('select') + .expectOneElement('expected to be select element') + .css('option') + .filter(o => o.value === model && elementInnerText(o) === model, `option with text or value ${JSON.stringify(model)}`) + .expectOneElement(`expected one option element with text or value ${JSON.stringify(model)}`) + .transform(function ([option]) { + return () => { + const selectElement = option.parentNode + debug('select', selectElement) + elementSelect(selectElement, option) + } + }) } ] diff --git a/package.json b/package.json index 6ec69c0..0125c97 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "browserify-optional": "^1.0.1", "codesandbox": "^1.3.8", "detect-node": "^2.0.4", - "electron": "^4.0.1", - "electron-mocha": "^6.0.4", + "electron": "^5.0.4", + "electron-mocha": "^8.0.2", "express": "^4.16.4", "httpism": "^3.11.0", "hyperdom": "^0.13.0", @@ -73,6 +73,7 @@ "jquery": "^3.3.1", "lowscore": "^1.12.1", "object-inspect": "1.6.0", + "pluralize": "^8.0.0", "trytryagain": "1.2.0" }, "standard": { diff --git a/test/actionsSpec.js b/test/actionsSpec.js index c87e4d2..2b4db05 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -109,7 +109,7 @@ describe('actions', function () { describe('select', function () { describe('text', function () { it('eventually selects an option element using the text', function () { - var promise = browser.find('.element').select({ text: 'Second' }) + var promise = browser.find('.element').select({ text: 'Second' }).then() var selectedItem assembly.eventuallyInsertHtml( @@ -124,7 +124,7 @@ describe('actions', function () { }) it('eventually selects an option element using the text, when text is passed as a string', function () { - var promise = browser.find('.element').select('Second') + var promise = browser.find('.element').select('Second').then() var selectedItem assembly.eventuallyInsertHtml( @@ -139,7 +139,7 @@ describe('actions', function () { }) it('should eventually select an option element using a partial match', function () { - var promise = browser.find('.element').select({ text: 'Seco' }) + var promise = browser.find('.element').select({ text: 'Seco' }).then() var selectedItem assembly.eventuallyInsertHtml( @@ -156,7 +156,7 @@ describe('actions', function () { it('selects the option by index names are ambiguous', function () { var selectedItem - assembly.eventuallyInsertHtml( + assembly.insertHtml( assembly.jQuery('').on('change', function (e) { var select = e.target selectedItem = select.value @@ -169,7 +169,7 @@ describe('actions', function () { }) it('selects an option that eventually appears', function () { - var promise = browser.find('.element').select({ text: 'Second' }) + var promise = browser.find('.element').select({ text: 'Second' }).then() var selectedItem var select = assembly.insertHtml( @@ -186,7 +186,7 @@ describe('actions', function () { }) it('errors when the specified option does not exist', async function () { - var promise = browser.find('.element').select({ text: 'Does not exist' }) + var promise = browser.find('.element').select({ text: 'Does not exist' }).then() assembly.eventuallyInsertHtml('') @@ -194,13 +194,13 @@ describe('actions', function () { }) it('errors when the input is not a select', function () { - var promise = browser.find('.element').select({ text: 'Whatevs' }) + var promise = browser.find('.element').select({ text: 'Whatevs' }).then() assembly.eventuallyInsertHtml('
') return assembly.assertRejection(promise, 'expected just one element') }) it('selects an option using text that is falsy', function () { - var promise = browser.find('.element').select({ text: 0 }) + var promise = browser.find('.element').select({ text: 0 }).then() var selectedItem assembly.eventuallyInsertHtml( @@ -217,7 +217,7 @@ describe('actions', function () { describe('exactText', function () { it('should select an option using exact text that would otherwise match multiple options', function () { - var promise = browser.find('.element').select({ exactText: 'Mr' }) + var promise = browser.find('.element').select({ exactText: 'Mr' }).then() var selectedItem assembly.eventuallyInsertHtml( @@ -232,7 +232,7 @@ describe('actions', function () { }) it('should select an option using exact text that is falsy', function () { - var promise = browser.find('.element').select({ exactText: '' }) + var promise = browser.find('.element').select({ exactText: '' }).then() var selectedItem assembly.eventuallyInsertHtml( @@ -296,7 +296,7 @@ describe('actions', function () { allowedToTypeInto.forEach(function (html) { it('eventually enters text into: ' + html, function () { - var typeInPromise = browser.find('.element').typeIn('1234') + var typeInPromise = browser.find('.element').typeIn('1234').then() var inputPromise = assembly.eventuallyInsertHtml(html) return Promise.all([typeInPromise, inputPromise]).then(function (elements) { var input = elements[1] @@ -313,7 +313,7 @@ describe('actions', function () { notAllowedToTypeInto.forEach(function (html) { it('rejects attempt to type into element: ' + html, function () { - var promise = browser.find('.element').typeIn('whatevs') + var promise = browser.find('.element').typeIn('whatevs').then() var inputPromise = assembly.eventuallyInsertHtml(html) return inputPromise.then(function (input) { return assembly.assertRejection(promise, 'expected just one element') @@ -407,8 +407,8 @@ describe('actions', function () { return this.find('.name') } }) - assembly.eventuallyInsertHtml('') - assembly.eventuallyInsertHtml('') + assembly.insertHtml('') + assembly.insertHtml('') return component.fill([ { name: 'title', action: 'select', options: { exactText: 'Mr' } }, @@ -431,9 +431,9 @@ describe('actions', function () { return this.find('.agree') } }) - assembly.eventuallyInsertHtml('') - assembly.eventuallyInsertHtml('') - assembly.eventuallyInsertHtml('') + assembly.insertHtml('') + assembly.insertHtml('') + assembly.insertHtml('') return component.fill([ { select: 'title', text: 'Mrs' }, @@ -461,7 +461,7 @@ describe('actions', function () { return this.find('.title') } }) - assembly.eventuallyInsertHtml('') + assembly.insertHtml('') return component.fill([ { myAction: 'title' } diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.js index f4ad8cd..931ed8a 100644 --- a/test/assemblies/DomAssembly.js +++ b/test/assemblies/DomAssembly.js @@ -14,6 +14,7 @@ module.exports = class DomAssembly { this.jQuery = $ this.retries = [] this.delayedActions = [] + this.queuedRetries = 0 } div () { @@ -23,8 +24,6 @@ module.exports = class DomAssembly { browserMonkey () { this.div() - this.retry = () => {} - const browserMonkey = createBrowserMonkey(this._div) browserMonkey.options({ @@ -33,25 +32,16 @@ module.exports = class DomAssembly { return trytryagain(retry) } else { return new Promise((resolve, reject) => { - const success = result => { - resolve(result) - const index = this.retries.indexOf(retrier) - if (index !== -1) { - this.retries.splice(index, 1) - } - } - - const retrier = () => { + this.retries.push(() => { try { - success(retry()) + resolve(retry()) } catch (e) { - reject(e) + if (this.queuedRetries === 0) { + reject(e) + } } - } - - this.retries.push(retrier) - - this.tick() + }) + this.eventuallyDoNothing() }) } } @@ -60,20 +50,17 @@ module.exports = class DomAssembly { return browserMonkey } - async tick () { - if (this.ticking) { + tick () { + if (this._normalRetry) { return } - this.ticking = true - - await new Promise(resolve => setTimeout(resolve)) - - this.delayedActions.forEach(action => action()) - this.delayedActions = [] - this.retries.slice().forEach(retry => retry()) - - this.ticking = false + if (this.retries.length) { + this.queuedRetries-- + this.retries.forEach(r => r()) + } else { + throw new Error('nothing retrying yet') + } } localUrl (path) { @@ -99,16 +86,22 @@ module.exports = class DomAssembly { } eventuallyDoNothing () { - return this.eventually(function () {}) + this.eventually(() => {}) } eventually (fn) { - this.tick() - return new Promise(resolve => { - this.delayedActions.push(() => { - var result = fn() + if (!this._normalRetry && !this.retries.length) { + throw new Error('nothing retrying yet, start retrying by calling .then() on a query') + } + + this.queuedRetries++ + + return new Promise((resolve) => { + setTimeout(() => { + const result =fn() + this.tick() resolve(result) - }) + }, 0) }) } @@ -140,7 +133,7 @@ module.exports = class DomAssembly { assertRejection (promise, expectedMessage) { return promise.then(() => { - throw new Error('expected rejection') + throw new Error('expected rejection ' + JSON.stringify(expectedMessage)) }, e => { if (e.message.indexOf(expectedMessage) === -1) { throw new Error('expected error message ' + inspect(e.message) + ' to include ' + inspect(expectedMessage)) diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index e49a351..26a0c2e 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -13,12 +13,12 @@ describe('assertions', function () { }) describe('shouldNotExist', function () { - it("should ensure that element eventually doesn't exists", function () { + it("should ensure that element eventually doesn't exist", function () { assembly.insertHtml('
') assembly.insertHtml('
') - var good = browser.find('.removing').shouldNotExist() - var bad = browser.find('.staying').shouldNotExist() + var good = browser.find('.removing').shouldNotExist().then() + var bad = browser.find('.staying').shouldNotExist().then() assembly.eventuallyDeleteHtml('.removing') @@ -31,7 +31,7 @@ describe('assertions', function () { it('allows trytryagain parameters to be used', function () { assembly.insertHtml('
') - var promise = browser.find('.removing').shouldNotExist({ timeout: 500, interval: 100 }) + var promise = browser.find('.removing').shouldNotExist({ timeout: 500, interval: 100 }).then() assembly.eventuallyDeleteHtml('.removing') @@ -43,8 +43,8 @@ describe('assertions', function () { it('should eventually find an element if it has a class', function () { var element = assembly.insertHtml('
') - var good = browser.find('.element').is('.good').shouldExist() - var bad = browser.find('.element').is('.bad').shouldExist() + var good = browser.find('.element').is('.good').shouldExist().then() + var bad = browser.find('.element').is('.bad').shouldExist().then() assembly.eventually(function () { element.classList.add('good') @@ -58,13 +58,13 @@ describe('assertions', function () { }) it('eventually finds an element containing text', function () { - var promise = browser.find('.element', { text: 'some t' }).shouldExist() + var promise = browser.find('.element', { text: 'some t' }).shouldExist().then() assembly.eventuallyInsertHtml('
some text
') return promise }) it('eventually finds an element containing text as it appears on the page', function () { - var promise = browser.find('.element').shouldHave({ text: 'This is some text that is all on one line.\nAnd some more on another line.' }) + var promise = browser.find('.element').shouldHave({ text: 'This is some text that is all on one line.\nAnd some more on another line.' }).then() /* eslint-disable no-multi-str */ assembly.eventuallyInsertHtml('
\ This\ @@ -80,8 +80,8 @@ describe('assertions', function () { }) it('eventually finds an element containing exactText', function () { - var good = browser.find('.a', { exactText: '8' }).shouldExist() - var bad = browser.find('.b', { exactText: '8' }).shouldExist() + var good = browser.find('.a', { exactText: '8' }).shouldExist().then() + var bad = browser.find('.b', { exactText: '8' }).shouldExist().then() assembly.eventuallyInsertHtml('
8
28
') @@ -92,9 +92,11 @@ describe('assertions', function () { }) it("treats assertion of text: '' as exact text", function () { + const promise = browser.find('.a', { text: 'something' }).shouldExist().then() + assembly.eventuallyInsertHtml('
something
') - return browser.find('.a', { text: 'something' }).shouldExist().then(function () { + return promise.then(function () { return Promise.all([ browser.find('.a', { text: '' }).shouldNotExist(), browser.find('.b', { text: '' }).shouldExist() @@ -104,8 +106,8 @@ describe('assertions', function () { describe('shouldHave', function () { it('eventually finds an element and asserts that it has text', function () { - var good = browser.find('.element').shouldHave({ text: 'some t' }) - var bad = browser.find('.element').shouldHave({ text: 'sme t' }) + var good = browser.find('.element').shouldHave({ text: 'some t' }).then() + var bad = browser.find('.element').shouldHave({ text: 'sme t' }).then() assembly.eventuallyInsertHtml('
some text
') @@ -123,9 +125,9 @@ describe('assertions', function () { }) it('eventually finds an element and asserts that it has value', function () { - var good1 = browser.find('.element1 input').shouldHave({ value: 'some t' }) - var good2 = browser.find('.element2 input').shouldHave({ value: '0' }) - var bad = browser.find('.element1 input').shouldHave({ value: 'sme t' }) + var good1 = browser.find('.element1 input').shouldHave({ value: 'some t' }).then() + var good2 = browser.find('.element2 input').shouldHave({ value: '0' }).then() + var bad = browser.find('.element1 input').shouldHave({ value: 'sme t' }).then() assembly.eventuallyInsertHtml('
') assembly.eventuallyInsertHtml('
') @@ -154,8 +156,8 @@ describe('assertions', function () { }) it('finds an element with exact value', function () { - var bad = browser.find('.element1 input').shouldHave({ exactValue: 'some t' }) - var good = browser.find('.element1 input').shouldHave({ exactValue: 'some text' }) + var bad = browser.find('.element1 input').shouldHave({ exactValue: 'some t' }).then() + var good = browser.find('.element1 input').shouldHave({ exactValue: 'some text' }).then() assembly.eventuallyInsertHtml('
') @@ -166,8 +168,8 @@ describe('assertions', function () { }) it("treats assertion of value: '' as exact value", function () { - var bad = browser.find('.element1 input').shouldHave({ value: '' }) - var good = browser.find('.element2 input').shouldHave({ value: '' }) + var bad = browser.find('.element1 input').shouldHave({ value: '' }).then() + var good = browser.find('.element2 input').shouldHave({ value: '' }).then() assembly.eventuallyInsertHtml('
\n' + '
\n' + @@ -234,7 +236,7 @@ describe('assertions', function () { describe('exactText', function () { it('eventually finds elements that have the exact array of text', function () { - var promise = browser.find('.element option').shouldHave({ exactText: ['', 'Mr', 'Mrs'] }) + var promise = browser.find('.element option').shouldHave({ exactText: ['', 'Mr', 'Mrs'] }).then() assembly.eventuallyInsertHtml('') @@ -242,7 +244,7 @@ describe('assertions', function () { }) it('fails to find exact text', function () { - var promise = browser.find('option').shouldHave({ exactText: ['', 'Mr', 'Mrs'] }) + var promise = browser.find('option').shouldHave({ exactText: ['', 'Mr', 'Mrs'] }).then() assembly.eventuallyInsertHtml('') @@ -252,7 +254,7 @@ describe('assertions', function () { describe('checkboxes', function () { it('eventually finds a checked checkbox', function () { - var good = browser.find('.checkbox').shouldHave({ checked: true }) + var good = browser.find('.checkbox').shouldHave({ checked: true }).then() var checkbox = assembly.insertHtml('') assembly.eventually(function () { @@ -265,7 +267,7 @@ describe('assertions', function () { }) it('asserts that a checkbox is indeterminate', function () { - var good = browser.find('.checkbox').shouldHave({ checked: 'indeterminate' }) + var good = browser.find('.checkbox').shouldHave({ checked: 'indeterminate' }).then() var checkbox = assembly.insertHtml('') assembly.eventually(function () { @@ -278,7 +280,7 @@ describe('assertions', function () { }) it('fails if we expected one checkbox, but found many', function () { - var bad = browser.find('.checkbox').shouldHave({ checked: true }) + var bad = browser.find('.checkbox').shouldHave({ checked: true }).then() var checkbox = assembly.insertHtml('') assembly.insertHtml('') @@ -292,8 +294,8 @@ describe('assertions', function () { }) it('ensures that each checkbox in the scope is either checked or unchecked', function () { - var good = browser.find('.checkbox').shouldHave({ checked: [true, false] }) - var bad = browser.find('.checkbox').shouldHave({ checked: [false, true] }) + var good = browser.find('.checkbox').shouldHave({ checked: [true, false] }).then() + var bad = browser.find('.checkbox').shouldHave({ checked: [false, true] }).then() var checkbox = assembly.insertHtml('') assembly.insertHtml('') @@ -321,9 +323,9 @@ describe('assertions', function () { }) it('eventually finds elements and asserts that they each have text', function () { - var good = browser.find('.element div').shouldHave({ text: ['one', 2] }) - var bad1 = browser.find('.element div').shouldHave({ text: ['one'] }) - var bad2 = browser.find('.element div').shouldHave({ text: ['one', 'three'] }) + var good = browser.find('.element div').shouldHave({ text: ['one', 2] }).then() + var bad1 = browser.find('.element div').shouldHave({ text: ['one'] }).then() + var bad2 = browser.find('.element div').shouldHave({ text: ['one', 'three'] }).then() assembly.eventuallyInsertHtml('
\nfirst one
number 2\n
') @@ -335,9 +337,9 @@ describe('assertions', function () { }) it('eventually finds elements and asserts that they each have value', function () { - var good = browser.find('.element input').shouldHave({ value: ['one', 2, 0] }) - var bad1 = browser.find('.element input').shouldHave({ value: ['one'] }) - var bad2 = browser.find('.element input').shouldHave({ value: ['one', 'three'] }) + var good = browser.find('.element input').shouldHave({ value: ['one', 2, 0] }).then() + var bad1 = browser.find('.element input').shouldHave({ value: ['one'] }).then() + var bad2 = browser.find('.element input').shouldHave({ value: ['one', 'three'] }).then() assembly.eventuallyInsertHtml('
') @@ -349,9 +351,9 @@ describe('assertions', function () { }) it('eventually finds an element and asserts that it has css', function () { - var good = browser.find('.element').shouldHave({ css: '.the-class' }) - var bad1 = browser.find('.element').shouldHave({ css: '.not-the-class' }) - var bad2 = browser.find('.element').shouldHave({ css: '.not-found' }) + var good = browser.find('.element').shouldHave({ css: '.the-class' }).then() + var bad1 = browser.find('.element').shouldHave({ css: '.not-the-class' }).then() + var bad2 = browser.find('.element').shouldHave({ css: '.not-found' }).then() assembly.eventuallyInsertHtml('
some text
') @@ -363,11 +365,10 @@ describe('assertions', function () { }) it('eventually finds an element and asserts that it has n elements', function () { - var good = browser.find('.element').shouldHave({ length: 2 }) - var bad1 = browser.find('.element').shouldHave({ length: 1 }) + var good = browser.find('.element').shouldHave({ length: 2 }).then() + var bad1 = browser.find('.element').shouldHave({ length: 1 }).then() - assembly.eventuallyInsertHtml('
') - assembly.eventuallyInsertHtml('
') + assembly.eventuallyInsertHtml('
') return Promise.all([ good, @@ -378,15 +379,15 @@ describe('assertions', function () { it('eventually finds an element and asserts that it passes an assertion', function () { var good1 = browser.find('.element').shouldHaveElement(function (element) { demand(element.innerText).to.eql('a') - }) + }).then() var bad1 = browser.find('.multi').shouldHaveElement(function (element) { demand(element.innerText).to.eql('b') - }) + }).then() var bad2 = browser.find('.element').shouldHaveElement(function (element) { demand(element.innerText).to.eql('b') - }) + }).then() var element = assembly.insertHtml('
') assembly.eventuallyInsertHtml('
') @@ -410,7 +411,7 @@ describe('assertions', function () { }) demand(xs).to.eql(['one', 'two', 'three']) - }) + }).then() var bad1 = browser.find('.element').shouldHaveElements(function (elements) { var xs = elements.map(function (element) { @@ -418,11 +419,13 @@ describe('assertions', function () { }) demand(xs).to.eql(['one', 'two']) - }) + }).then() - assembly.eventuallyInsertHtml('
') - assembly.eventuallyInsertHtml('
') - assembly.eventuallyInsertHtml('
') + assembly.eventuallyInsertHtml(` +
+
+
+ `) return Promise.all([ good1, @@ -440,7 +443,7 @@ describe('assertions', function () { errorThrown = error throw error } - }) + }).then() assembly.eventuallyInsertHtml('
text
') @@ -452,7 +455,7 @@ describe('assertions', function () { describe('shouldNotHave', function () { it('eventually finds an element and asserts that it does not have text', function () { - var promise = browser.find('.element').shouldNotHave({ text: 'sme t' }) + var promise = browser.find('.element').shouldNotHave({ text: 'sme t' }).then() assembly.eventuallyInsertHtml('
some text
') @@ -460,7 +463,7 @@ describe('assertions', function () { }) it('allows trytryagain parameters to be used', function () { - var promise = browser.find('.element').shouldNotHave({ text: 'sme t', timeout: 400, interval: 100 }) + var promise = browser.find('.element').shouldNotHave({ text: 'sme t', timeout: 400, interval: 100 }).then() assembly.eventuallyInsertHtml('
some text
') diff --git a/test/componentSpec.js b/test/componentSpec.js index 35bddec..2c886fc 100644 --- a/test/componentSpec.js +++ b/test/componentSpec.js @@ -18,7 +18,7 @@ describe('component', function () { } }) - var promise = user.name().shouldExist() + var promise = user.name().shouldExist().then() assembly.eventuallyInsertHtml('
bob
bob\'s address
') @@ -32,7 +32,7 @@ describe('component', function () { } }) - var promise = user.name.shouldExist() + var promise = user.name.shouldExist().then() assembly.eventuallyInsertHtml('
bob
bob\'s address
') @@ -52,7 +52,7 @@ describe('component', function () { } }) - var promise = user.name().shouldExist() + var promise = user.name().shouldExist().then() assembly.eventuallyInsertHtml('
bob
bob\'s address
') @@ -76,8 +76,8 @@ describe('component', function () { } }) - var name = bossUser.name().shouldExist() - var secondAddress = bossUser.secondAddress().shouldExist() + var name = bossUser.name().shouldExist().then() + var secondAddress = bossUser.secondAddress().shouldExist().then() assembly.eventuallyInsertHtml('
bob
bob\'s address
bob\'s second address
') @@ -93,7 +93,7 @@ describe('component', function () { } }) - var promise = admin.user().shouldHave({ text: ['Jane'] }) + var promise = admin.user().shouldHave({ text: ['Jane'] }).then() assembly.eventuallyInsertHtml( '
Bob
' + diff --git a/test/fieldsSpec.js b/test/fieldsSpec.js index 28f8086..de84d07 100644 --- a/test/fieldsSpec.js +++ b/test/fieldsSpec.js @@ -165,11 +165,12 @@ describe('fields', function () { `) - assembly.eventuallyAppendHtml(select, '') - const selectScope = browser.scope(select) + const setPromise = selectScope.setValue('Text Three').then() + + assembly.eventuallyAppendHtml(select, '') - await selectScope.setValue('Text Three') + await setPromise const value = await selectScope.value() demand(value).to.equal('Text Three') }) diff --git a/test/findersSpec.js b/test/findersSpec.js index 412dcc5..134ce40 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -14,7 +14,7 @@ describe('find', function () { }) it('should eventually find an element', function () { - var promise = browser.find('.element').shouldExist() + var promise = browser.find('.element').shouldExist().then() assembly.eventuallyInsertHtml('
') @@ -22,7 +22,7 @@ describe('find', function () { }) it('should eventually find an element, when collapsed into shouldFind(selector)', function () { - var promise = browser.shouldFind('.element') + var promise = browser.shouldFind('.element').then() assembly.eventuallyInsertHtml('
') @@ -32,7 +32,7 @@ describe('find', function () { it('should eventually find an element using a filter', async function () { var foundElement = browser.find('.element').filter(function (element) { return element.classList.contains('correct') - }, 'has class "correct"').expectOneElement() + }, 'has class "correct"').expectOneElement().then() assembly.insertHtml('
') const correctElement = await assembly.eventuallyInsertHtml('
') @@ -41,7 +41,7 @@ describe('find', function () { }) it('should eventually find an element with the right text', function () { - var promise = browser.find('.element', { text: 'green' }).expectOneElement() + var promise = browser.find('.element', { text: 'green' }).expectOneElement().then() assembly.insertHtml('
') assembly.eventuallyInsertHtml('
red
blue
green
') @@ -54,7 +54,7 @@ describe('find', function () { it('filter fails with the right message', function () { var promise = browser.find('.element').filter(function (element) { return element.classList.contains('correct') - }, 'has class "correct"').expectOneElement() + }, 'has class "correct"').expectOneElement().then() assembly.insertHtml('
') assembly.eventuallyInsertHtml('
') @@ -113,16 +113,12 @@ describe('find', function () { it('should not find an element that is visually hidden', function () { assembly.insertHtml('
hello world
') - assembly.eventuallyDoNothing() - return browser.find('.element > span').shouldNotExist() }, { vdom: false }) it('should find an element that is visually hidden when visibleOnly = false', function () { assembly.insertHtml('
hello world
') - assembly.eventuallyDoNothing() - browser.options({ visibleOnly: false }) return browser.find('.element > span').shouldExist() }) @@ -130,15 +126,13 @@ describe('find', function () { it('should find elements that are visually hidden because of how html renders them', function () { assembly.insertHtml('') - assembly.eventuallyDoNothing() - return browser.find('select option').shouldHave({ text: ['First', 'Second'] }) }) }) describe('containing', function () { it('eventually finds an element containing another element', function () { - var promise = browser.find('.outer').containing('.inner').shouldExist() + var promise = browser.find('.outer').containing('.inner').shouldExist().then() assembly.eventuallyInsertHtml( '
bad
' + @@ -149,7 +143,7 @@ describe('find', function () { }) it('element returns the outer element', function () { - var promise = browser.find('.outer').containing('.inner').expectOneElement() + var promise = browser.find('.outer').containing('.inner').expectOneElement().then() assembly.eventuallyInsertHtml( '
bad
' + @@ -162,7 +156,7 @@ describe('find', function () { }) it("fails if it can't find an element containing another", function () { - var promise = browser.find('.outer').containing('.inner').shouldExist() + var promise = browser.find('.outer').containing('.inner').shouldExist().then() assembly.eventuallyInsertHtml('
bad
') @@ -170,7 +164,7 @@ describe('find', function () { }) it("fails if it can't find an element containing another", function () { - var promise = browser.find('.outer').containing('.inner').shouldExist() + var promise = browser.find('.outer').containing('.inner').shouldExist().then() assembly.eventuallyInsertHtml('
bad
') @@ -181,7 +175,7 @@ describe('find', function () { describe('chains', function () { it('eventually finds the inner element, even if the outer element exists', function () { assembly.insertHtml('
') - var promise = browser.find('.outer').find('.inner').shouldExist() + var promise = browser.find('.outer').find('.inner').shouldExist().then() assembly.eventuallyInsertHtml('
good
', '.outer') @@ -189,7 +183,7 @@ describe('find', function () { }) it('fails to find the inner element if it never arrives', function () { - var promise = browser.find('.outer').find('.inner').shouldExist() + var promise = browser.find('.outer').find('.inner').shouldExist().then() assembly.eventuallyInsertHtml('
') diff --git a/test/querySpec.js b/test/querySpec.js index 777f3cf..1bf7647 100644 --- a/test/querySpec.js +++ b/test/querySpec.js @@ -14,7 +14,7 @@ describe('query', () => { }) it('can find an element by DOM selector', async () => { - const selectedElementPromise = browserMonkey.find('.test').expectOneElement() + const selectedElementPromise = browserMonkey.find('.test').expectOneElement().then() const insertedElementPromise = assembly.eventuallyInsertHtml( `
` @@ -90,6 +90,7 @@ describe('query', () => { const contacts = browserMonkey .find('.contact') .expectNoElements() + .then() assembly.eventuallyDeleteHtml('.contact') @@ -111,6 +112,7 @@ describe('query', () => { const contacts = browserMonkey .find('.contact') .expectNoElements() + .then() assembly.eventuallyDeleteHtml('.message') await expect(contacts).reject.with.error(/expected no elements/) @@ -122,6 +124,7 @@ describe('query', () => { const contacts = browserMonkey .find('.contact') .expectOneElement() + .then() assembly.eventuallyInsertHtml(`
@@ -137,6 +140,7 @@ describe('query', () => { const contacts = browserMonkey .find('.contact') .expectOneElement() + .then() assembly.eventuallyInsertHtml(`
@@ -156,6 +160,7 @@ describe('query', () => { const contacts = browserMonkey .find('.contact') .expectOneElement() + .then() assembly.eventuallyInsertHtml(`
@@ -171,6 +176,7 @@ describe('query', () => { const contacts = browserMonkey .find('.contact') .expectSomeElements() + .then() assembly.eventuallyInsertHtml(`
@@ -190,6 +196,7 @@ describe('query', () => { const contacts = browserMonkey .find('.contact') .expectSomeElements() + .then() assembly.eventuallyInsertHtml(`
@@ -226,6 +233,7 @@ describe('query', () => { .expect(elements => { expect(elements.some(element => element.innerText.includes('hello'))).to.equal(true) }) + .then() assembly.eventuallyInsertHtml(`
hello
@@ -239,6 +247,7 @@ describe('query', () => { .expect(elements => { expect(elements.some(element => element.innerText.includes('hello')), 'expected to see hello').to.equal(true) }) + .then() assembly.eventuallyInsertHtml(`
goodbye
@@ -256,6 +265,7 @@ describe('query', () => { return names.map(contact => contact.innerText).join(', ') }) .expect(x => expect(x).to.not.be.empty()) + .then() assembly.eventuallyInsertHtml(`
Sally
@@ -274,6 +284,7 @@ describe('query', () => { return contact.querySelector('.name').innerText === 'Sally' }) .expectOneElement() + .then() assembly.eventuallyInsertHtml(`
@@ -334,6 +345,7 @@ describe('query', () => { .find('.contact') .find('.name') .expectOneElement() + .then() assembly.eventuallyInsertHtml(`
@@ -394,7 +406,7 @@ describe('query', () => { const promise = browserMonkey.firstOf([ b => b.find('.a').expectSomeElements(), b => b.find('.b').expectSomeElements() - ]) + ]).then() const bPromise = assembly.eventuallyInsertHtml('
B
') diff --git a/test/setSpec.js b/test/setSpec.js index 56ba02d..a97bc0c 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -1,6 +1,7 @@ const describeAssemblies = require('./describeAssemblies') const DomAssembly = require('./assemblies/DomAssembly') const demand = require('must') +const {expect} = require('chai') describe('set', function () { describeAssemblies([DomAssembly], function (Assembly) { @@ -193,17 +194,41 @@ describe('set', function () { `) - const promise = browser.set({ + const promise = browser.find('form').set({ '.street': '7 Lola St', '.phone': '123123123', '.first-name': 'Barry' }) - await assembly.assertRejection(promise, 'expected 1 elements') + await assembly.assertRejection(promise, "expected 1 element, found 0 (found: find('form') [1], find('.street') [0])") demand(assembly.find('.phone').value).to.equal('') demand(assembly.find('.first-name').value).to.equal('') }) + describe('selecting options atomically', () => { + it('waits until the desired option appears on the page', async () => { + const select = assembly.insertHtml(` + + `) + + const promise = browser.set({ + 'select': 'Three', + }).then() + + assembly.eventually(() => { + assembly.jQuery(select).append('') + }) + + await promise + + const selectedItem = assembly.jQuery(select).find(':selected').text() + expect(selectedItem).to.equal('Three') + }) + }) + it('can define a named field', async function () { assembly.insertHtml(`
@@ -232,11 +257,11 @@ describe('set', function () {
`) - browser.defineFinder('form', (b, name) => b.find('* [name=' + JSON.stringify(name) + ']')) + browser.define('form', (b, name) => b.find('* [name=' + JSON.stringify(name) + ']')) await browser.set({ - 'form! phone': '123123123', - 'form! firstname': 'Barry' + 'form(phone)': '123123123', + 'form(firstname)': 'Barry' }) demand(assembly.find('.phone').value).to.equal('123123123') diff --git a/yarn.lock b/yarn.lock index c605092..2b1f6be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,17 +54,6 @@ source-map "^0.5.0" trim-right "^1.0.1" -"@babel/generator@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" - integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== - dependencies: - "@babel/types" "^7.4.4" - jsesc "^2.5.1" - lodash "^4.17.11" - source-map "^0.5.0" - trim-right "^1.0.1" - "@babel/helper-annotate-as-pure@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" @@ -97,18 +86,6 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-create-class-features-plugin@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.4.4.tgz#fc3d690af6554cc9efc607364a82d48f58736dba" - integrity sha512-UbBHIa2qeAGgyiNR9RszVF7bUHEdgS4JAUNT8SiqrAN6YJVxlOxeLr5pBzb5kan302dejJ9nla4RyKcR1XT6XA== - dependencies: - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-member-expression-to-functions" "^7.0.0" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.4.4" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/helper-define-map@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c" @@ -215,16 +192,6 @@ "@babel/traverse" "^7.2.3" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27" - integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.0.0" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.4.4" - "@babel/types" "^7.4.4" - "@babel/helper-simple-access@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" @@ -240,13 +207,6 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helper-split-export-declaration@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" - integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== - dependencies: - "@babel/types" "^7.4.4" - "@babel/helper-wrap-function@^7.1.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa" @@ -280,11 +240,6 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.3.tgz#32f5df65744b70888d17872ec106b02434ba1489" integrity sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA== -"@babel/parser@^7.4.5": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" - integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== - "@babel/plugin-proposal-async-generator-functions@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e" @@ -294,14 +249,6 @@ "@babel/helper-remap-async-to-generator" "^7.1.0" "@babel/plugin-syntax-async-generators" "^7.2.0" -"@babel/plugin-proposal-class-properties@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.4.4.tgz#93a6486eed86d53452ab9bab35e368e9461198ce" - integrity sha512-WjKTI8g8d5w1Bc9zgwSz2nfrsNQsXcCf9J9cdCvrJV6RF56yztwm4TmJC0MgJ9tvwO9gUA/mcYe89bLdGfiXFg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.4.4" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" @@ -743,21 +690,6 @@ globals "^11.1.0" lodash "^4.17.10" -"@babel/traverse@^7.4.4": - version "7.4.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216" - integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.4.4" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.4.5" - "@babel/types" "^7.4.4" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.11" - "@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.2.tgz#44e10fc24e33af524488b716cdaee5360ea8ed1e" @@ -767,15 +699,6 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" -"@babel/types@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" - integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== - dependencies: - esutils "^2.0.2" - lodash "^4.17.11" - to-fast-properties "^2.0.0" - "@types/node@^10.12.18": version "10.12.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" @@ -922,6 +845,16 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" +ansi-colors@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" + integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== + +ansi-colors@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + ansi-escapes@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" @@ -1692,6 +1625,11 @@ camelcase@^4.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + camelize@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" @@ -1846,6 +1784,24 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" @@ -1951,7 +1907,7 @@ commander@2.15.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== -commander@^2.15.1, commander@^2.9.0: +commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -2148,7 +2104,7 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^6.0.5: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2258,7 +2214,7 @@ debug@3.1.0, debug@=3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.0: +debug@3.2.6, debug@^3.0.0, debug@^3.1.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -2277,7 +2233,7 @@ debug@~0.7.2: resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39" integrity sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk= -decamelize@^1.1.2: +decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -2556,16 +2512,18 @@ electron-download@^4.1.0: semver "^5.4.1" sumchecker "^2.0.2" -electron-mocha@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/electron-mocha/-/electron-mocha-6.0.4.tgz#5130ff3ed1ffc2971de26881cd7d18c0c0179baf" - integrity sha512-WVJAldQRv7hC7Db65qKU1TQ3w9rwqPXzz3UzpQQDRPqIxd7FERxnwRXqb+bQKBOdO5ThnHxMtnHWVJgG+FZivw== +electron-mocha@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/electron-mocha/-/electron-mocha-8.0.2.tgz#1588a4f92998b93c5ba7c62aa577109cef06922a" + integrity sha512-IlqYToYJjo8vuqbN3SJoDPvNcPGsG1nvOth3UzHGnd3SRVl5cLuSFJWxYJK7hP5ZZQiMFwmJ1J0ldaPBlZuYZA== dependencies: - commander "^2.15.1" + ansi-colors "^3.2.4" electron-window "^0.8.0" - fs-extra "^6.0.1" - mocha "^5.2.0" + fs-extra "^7.0.1" + log-symbols "^2.2.0" + mocha "^6.1.2" which "^1.3.1" + yargs "^13.2.2" electron-to-chromium@^1.3.96: version "1.3.100" @@ -2579,10 +2537,10 @@ electron-window@^0.8.0: dependencies: is-electron-renderer "^2.0.0" -electron@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-4.0.1.tgz#c41eaee9e081c2e5e4a4a4a761b7577a77d2eb18" - integrity sha512-kBWDLn1Vq8Tm6+/HpQc8gkjX7wJyQI8v/lf2kAirfi0Q4cXh6vBjozFvV1U/9gGCbyKnIDM+m8/wpyJIjg4w7g== +electron@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/electron/-/electron-5.0.4.tgz#2e0d09055363e983f4a73317cde4821c39617b02" + integrity sha512-7QaKorvANvP+azMT7wElx33oLlqw8QxmLs7/outfH7LC5amErk4EUtWDesQ6Zgr+s5pYFbykl8ZtJ4ZGXER05g== dependencies: "@types/node" "^10.12.18" electron-download "^4.1.0" @@ -2708,7 +2666,7 @@ error@^4.3.0: string-template "~0.2.0" xtend "~4.0.0" -es-abstract@^1.7.0: +es-abstract@^1.5.1, es-abstract@^1.7.0: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== @@ -2993,6 +2951,19 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + expand-braces@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" @@ -3281,6 +3252,13 @@ find-pkg@^0.1.0: dependencies: find-file-up "^0.1.2" +find-up@3.0.0, find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3305,6 +3283,13 @@ flat-cache@^2.0.1: rimraf "2.6.3" write "1.0.3" +flat@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" + integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + dependencies: + is-buffer "~2.0.3" + flatted@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" @@ -3419,10 +3404,10 @@ fs-extra@^4.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.1.tgz#8abc128f7946e310135ddc93b98bddb410e7a34b" - integrity sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA== +fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== dependencies: graceful-fs "^4.1.2" jsonfile "^4.0.0" @@ -3512,6 +3497,16 @@ get-assigned-identifiers@^1.2.0: resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ== +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-func-name@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" @@ -3527,6 +3522,13 @@ get-stream@^3.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -3605,7 +3607,7 @@ glob@7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: +glob@7.1.3, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== @@ -3791,6 +3793,11 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4120,6 +4127,11 @@ invariant@^2.2.2: dependencies: loose-envify "^1.0.0" +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + ip@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -4161,6 +4173,11 @@ is-buffer@^1.1.0, is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-buffer@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" + integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== + is-builtin-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" @@ -4504,7 +4521,7 @@ js-string-escape@^1.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.0: +js-yaml@3.13.1, js-yaml@^3.13.0: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -4764,6 +4781,13 @@ lazy-cache@^1.0.4: resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -4806,6 +4830,14 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + lodash._createwrapper@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash._createwrapper/-/lodash._createwrapper-3.2.0.tgz#df453e664163217b895a454065af1c47a0ea3c4d" @@ -4845,7 +4877,7 @@ lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== -log-symbols@^2.1.0: +log-symbols@2.2.0, log-symbols@^2.1.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== @@ -4935,6 +4967,13 @@ make-fetch-happen@^2.4.13: socks-proxy-agent "^3.0.1" ssri "^5.0.0" +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -4971,6 +5010,15 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + meow@^3.1.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -5087,6 +5135,11 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -5188,7 +5241,7 @@ mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "0.0.8" -mocha@5.2.0, mocha@^5.2.0: +mocha@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== @@ -5205,6 +5258,35 @@ mocha@5.2.0, mocha@^5.2.0: mkdirp "0.5.1" supports-color "5.4.0" +mocha@^6.1.2: + version "6.1.4" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.1.4.tgz#e35fada242d5434a7e163d555c705f6875951640" + integrity sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg== + dependencies: + ansi-colors "3.2.3" + browser-stdout "1.3.1" + debug "3.2.6" + diff "3.5.0" + escape-string-regexp "1.0.5" + find-up "3.0.0" + glob "7.1.3" + growl "1.10.5" + he "1.2.0" + js-yaml "3.13.1" + log-symbols "2.2.0" + minimatch "3.0.4" + mkdirp "0.5.1" + ms "2.1.1" + node-environment-flags "1.0.5" + object.assign "4.1.0" + strip-json-comments "2.0.1" + supports-color "6.0.0" + which "1.3.1" + wide-align "1.1.3" + yargs "13.2.2" + yargs-parser "13.0.0" + yargs-unparser "1.5.0" + module-deps@^6.0.0: version "6.2.0" resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.0.tgz#d41a2e790245ce319171e4e7c4d8c73993ba3cd5" @@ -5248,7 +5330,7 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.0.0, ms@^2.1.1: +ms@2.1.1, ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== @@ -5334,6 +5416,14 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-environment-flags@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a" + integrity sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ== + dependencies: + object.getownpropertydescriptors "^2.0.3" + semver "^5.7.0" + node-fetch-npm@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7" @@ -5496,6 +5586,11 @@ object-inspect@1.6.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== +object-keys@^1.0.11: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + object-keys@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" @@ -5513,6 +5608,24 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" +object.assign@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -5601,6 +5714,15 @@ os-homedir@^1.0.0, os-homedir@^1.0.1: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= +os-locale@^3.0.0, os-locale@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + os-shim@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" @@ -5626,11 +5748,21 @@ outpipe@^1.1.0: dependencies: shell-quote "^1.4.2" +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -5638,6 +5770,13 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" +p-limit@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -5645,11 +5784,23 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + package-json@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" @@ -5926,6 +6077,11 @@ pkg-dir@^2.0.0: dependencies: find-up "^2.1.0" +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -6066,6 +6222,14 @@ pump@^2.0.0, pump@^2.0.1: end-of-stream "^1.1.0" once "^1.3.1" +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + pumpify@^1.3.3: version "1.5.1" resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" @@ -6439,6 +6603,21 @@ request@^2.45.0: tunnel-agent "^0.6.0" uuid "^3.3.2" +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + requireindex@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" @@ -6604,7 +6783,7 @@ semver@5.5.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== -semver@^5.5.1: +semver@^5.5.1, semver@^5.7.0: version "5.7.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== @@ -6655,7 +6834,7 @@ server-destroy@^1.0.1: resolved "https://registry.yarnpkg.com/server-destroy/-/server-destroy-1.0.1.tgz#f13bf928e42b9c3e79383e61cc3998b5d14e6cdd" integrity sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0= -set-blocking@~2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -7087,7 +7266,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0: +string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== @@ -7129,7 +7308,7 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.1.0: +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== @@ -7160,7 +7339,7 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: +strip-json-comments@2.0.1, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= @@ -7186,6 +7365,13 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" +supports-color@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" + integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== + dependencies: + has-flag "^3.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -7766,14 +7952,19 @@ watchify@^3.11.0: through2 "^2.0.0" xtend "^4.0.0" -which@^1.2.1, which@^1.2.12, which@^1.2.9, which@^1.3.1: +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@1.3.1, which@^1.2.1, which@^1.2.12, which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" -wide-align@^1.1.0: +wide-align@1.1.3, wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== @@ -7797,6 +7988,23 @@ wordwrap@~1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -7864,7 +8072,7 @@ y18n@^3.2.1: resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= -y18n@^4.0.0: +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== @@ -7879,6 +8087,91 @@ yallist@^3.0.0, yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== +yargs-parser@13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.0.0.tgz#3fc44f3e76a8bdb1cc3602e860108602e5ccde8b" + integrity sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^13.0.0, yargs-parser@^13.1.0: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-unparser@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.5.0.tgz#f2bb2a7e83cbc87bb95c8e572828a06c9add6e0d" + integrity sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw== + dependencies: + flat "^4.1.0" + lodash "^4.17.11" + yargs "^12.0.5" + +yargs@13.2.2: + version "13.2.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993" + integrity sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA== + dependencies: + cliui "^4.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.0.0" + +yargs@^12.0.5: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + dependencies: + cliui "^4.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^11.1.1" + +yargs@^13.2.2: + version "13.2.4" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" + integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.0" + yauzl@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" From ae3c549a8a96ed1b7d37becc38c5dd07035bb8c5 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Sun, 16 Jun 2019 15:34:29 +0200 Subject: [PATCH 019/127] set/assert support for checkboxes, selects --- create.js | 7 +- lib/BrowserMonkeyAssertionError.ts | 14 ++- lib/actions.js | 2 +- lib/query.ts | 14 +-- lib/set.js | 183 ++++++++++++++++++----------- test/actionsSpec.js | 2 +- test/assemblies/DomAssembly.js | 1 + test/assertSpec.js | 99 +++++++++++----- test/assertionsSpec.js | 2 +- test/buttonSpec.js | 2 +- test/componentSpec.js | 2 +- test/eventsSpec.js | 2 +- test/fieldsSpec.js | 2 +- test/findersSpec.js | 2 +- test/optionsSpec.js | 2 +- test/reloadButtonSpec.js | 2 +- test/setSpec.js | 148 +++++++++++++---------- 17 files changed, 308 insertions(+), 178 deletions(-) diff --git a/create.js b/create.js index dc040d5..dec270d 100644 --- a/create.js +++ b/create.js @@ -9,11 +9,14 @@ var assertions = require('./lib/assertions') module.exports = function (rootSelector = document.body) { return new Query() .component(finders) - .component(actions) .component(button) .component(fields) .component(set) - .component(assertions) .installSetters() .scope(rootSelector) + .component({ + browserMonkey2Compat: function () { + return this.component(actions).component(assertions) + } + }) } diff --git a/lib/BrowserMonkeyAssertionError.ts b/lib/BrowserMonkeyAssertionError.ts index b8766be..dd91a0f 100644 --- a/lib/BrowserMonkeyAssertionError.ts +++ b/lib/BrowserMonkeyAssertionError.ts @@ -1,3 +1,4 @@ +import { ExecutedTransform } from './ExecutedTransform' import { ExecutedTransformSequence } from './ExecutedTransformSequence' class BrowserMonkeyAssertionError extends Error { @@ -20,14 +21,25 @@ class BrowserMonkeyAssertionError extends Error { this.executedTransforms = executedTransforms Object.setPrototypeOf(this, BrowserMonkeyAssertionError.prototype) + this.message = this.renderError() } public rewriteMessage (): void { this.message = this.renderError() } + public addExecutedTransform (executedTransform: ExecutedTransform): void { + this.executedTransforms.addTransform(executedTransform) + this.rewriteMessage() + } + + public prependExecutedTransforms (executedTransforms: ExecutedTransformSequence): void { + this.executedTransforms.prepend(executedTransforms) + this.rewriteMessage() + } + public renderError (): string { - return `${this.description} (found: ${this.executedTransforms.renderError()})` + return `${this.description}${this.executedTransforms.transforms.length ? ` (found: ${this.executedTransforms.renderError()})`: ''}` } } diff --git a/lib/actions.js b/lib/actions.js index 4ce186b..72b82ff 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -33,7 +33,7 @@ module.exports = { }, enter: function (selector) { - return this.query(selector).expectOneElement().action(function ([element]) { + return this.find(selector).expectOneElement().action(function ([element]) { debug('enter', element) elementSubmit(element) }) diff --git a/lib/query.ts b/lib/query.ts index b24d36e..00c4ed1 100644 --- a/lib/query.ts +++ b/lib/query.ts @@ -25,10 +25,12 @@ class Query { timeout: number interval: number definitions: { - setters: any[] + fieldTypes: { + value: (query: Query) => any + settter?: (query: Query, value: any) => () => void + }[] button: ((monkey: any, name: any) => any)[] fields: {} - finders: {} field: ((monkey: any, name: any) => any)[] fieldValue: { get: (monkey: any) => any @@ -51,7 +53,7 @@ class Query { timeout: 1000, interval: 10, definitions: { - setters: [], + fieldTypes: [], button: [ function (monkey, name) { return monkey.find('button', { exactText: name }) @@ -65,7 +67,6 @@ class Query { ], fields: {}, - finders: {}, field: [ function (monkey, name) { @@ -203,7 +204,7 @@ class Query { return transformSequence } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { - e.executedTransforms.prepend(transformSequence) + e.prependExecutedTransforms(transformSequence) } throw e @@ -244,7 +245,7 @@ class Query { return transform } else { var error = new BrowserMonkeyAssertionError('all queries failed in firstOf') - error.executedTransforms.addTransform(transform) + error.addExecutedTransform(transform) throw error } }) @@ -303,7 +304,6 @@ class Query { return self.execute().value })).catch(function (error) { if (error instanceof BrowserMonkeyAssertionError) { - error.rewriteMessage() error.stack = originalStack if (debug.enabled) { diff --git a/lib/set.js b/lib/set.js index 46ea524..3d40496 100644 --- a/lib/set.js +++ b/lib/set.js @@ -1,5 +1,7 @@ var inputsSelector = require('./inputsSelector') var debug = require('debug')('browser-monkey') +var elementClick = require('./elementClick') +var elementChecked = require('./elementChecked') var elementEnterText = require('./elementEnterText') var elementInnerText = require('./elementInnerText') var elementSelect = require('./elementSelect') @@ -9,23 +11,22 @@ const range = require('lowscore/range') const pluralize = require('pluralize') const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default -function assertLength (query, length) { - query.expect(elements => { +function expectLength (query, length) { + return query.expect(elements => { const actualLength = elements.length if (actualLength !== length) { - const error = query.error('expected ' + length + ' ' + pluralize('elements', length) + ', found ' + actualLength, { expected: length, actual: actualLength }) - error.executedTransforms.prepend(query._executedTransforms) - throw error + query.error('expected ' + length + ' ' + pluralize('elements', length) + ', found ' + actualLength) } - }).result() + }) } const missing = {} -function spliceModelArrayFromActual (model, query) { +function spliceModelArrayFromActual (model, query, actions) { const length = query.result().length if (length > model.length) { + actions.arrayLengthError(query, length, model.length) return range(0, length).map((item, index) => { const modelItem = model[index] @@ -35,8 +36,11 @@ function spliceModelArrayFromActual (model, query) { return missing } }) - } else { + } else if (length < model.length) { + actions.arrayLengthError(query, length, model.length) return model.slice(0, length) + } else { + return model } } @@ -45,22 +49,17 @@ function mapModel (query, model, actions) { if (model === missing) { return query.result().map(e => e.innerText).join() } else if (model instanceof Array) { - actions.assertLength(query, model.length) - - return spliceModelArrayFromActual(model, query).map((item, index) => { + return spliceModelArrayFromActual(model, query, actions).map((item, index) => { return map(query.index(index), item) }) } else if (model.constructor === Object) { - actions.assertLength(query, 1) return object(Object.entries(model).map(([selector, value]) => { - return [selector, map(query.find(selector), value)] + return [selector, map(expectLength(query, 1).find(selector), value)] })) } else if (typeof model === 'function') { return actions.function(query, model) } else { - actions.assertLength(query, 1) - - return actions.value(query, model) + return actions.value(expectLength(query, 1), model) } } @@ -73,8 +72,8 @@ module.exports = { const setters = [] const actions = { - assertLength: (query, length) => { - assertLength(query, length) + arrayLengthError: (query, actualLength, expectedLength) => { + query.error('expected ' + expectedLength + ' ' + pluralize('elements', expectedLength) + ', found ' + actualLength) }, value: (query, model) => { @@ -101,26 +100,36 @@ module.exports = { let isError = false const actions = { - assertLength: (query, length) => { - const actualLength = query.result().length - if (actualLength !== length) { - isError = true - } + arrayLengthError: () => { + isError = true }, value: (query, model) => { + let asserter + try { - var asserter = query.asserter().result() - asserter(model) - return model + asserter = query.asserter(model).result() } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { isError = true - return e.actual + return 'Error: ' + e.message } else { throw e } } + + try { + asserter() + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + isError = true + return e.actual === undefined ? 'Error: ' + e.message : e.actual + } else { + throw e + } + } + + return model }, function: (query, model) => { @@ -130,7 +139,7 @@ module.exports = { } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { isError = true - return e.actual === undefined ? e : e.actual + return 'Error: ' + e.message } else { throw e } @@ -157,38 +166,58 @@ module.exports = { this._options.definitions.fields[name] = fieldFinder }, - defineFinder: function (name, finder) { - this._options.definitions.finders[name] = finder + setter: function (model) { + return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.setter).map(def => { + return query => def.setter(query, model) + })) }, - setter: function (model) { - return this.firstOf(this._options.definitions.setters.map(s => s(model))) + asserter: function (expected) { + return this.firstOf(this._options.definitions.fieldTypes.map(def => { + return query => def.value(query).transform(actual => { + return () => { + if (actual !== expected) { + this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) + } + } + }) + })) }, - asserter: function () { - return this.firstOf(this._options.definitions.asserters) + defineFieldType: function (fieldTypeDefinition) { + this._options.definitions.fieldTypes.push(fieldTypeDefinition) }, installSetters: function () { - this._options.definitions.setters = [ - model => query => { - return query.is(inputsSelector).expectOneElement().transform(([element]) => { - return () => { - if (typeof model !== 'string') { + this.defineFieldType({ + setter: (query, value) => { + return query + .is(inputsSelector) + .expectOneElement() + .transform(([element]) => { + if (typeof value !== 'string') { throw new Error('expected string as argument to set input') } - debug('set', element, model) - elementEnterText(element, model) - } - }) + return () => { + debug('set', element, value) + elementEnterText(element, value) + } + }) }, - model => query => { + value: (query) => { + return query.is(inputsSelector).expectOneElement().transform(([input]) => { + return input.value + }) + } + }) + this.defineFieldType({ + setter: (query, value) => { return query .is('select') .expectOneElement('expected to be select element') .css('option') - .filter(o => o.value === model && elementInnerText(o) === model, `option with text or value ${JSON.stringify(model)}`) - .expectOneElement(`expected one option element with text or value ${JSON.stringify(model)}`) + .filter(o => o.value === value && elementInnerText(o) === value, `option with text or value ${JSON.stringify(value)}`) + .expectOneElement(`expected one option element with text or value ${JSON.stringify(value)}`) .transform(function ([option]) { return () => { const selectElement = option.parentNode @@ -196,35 +225,51 @@ module.exports = { elementSelect(selectElement, option) } }) + }, + value: (query) => { + return query + .is('select') + .expectOneElement('expected to be select element') + .transform(([select]) => { + const selectedOption = select.options[select.selectedIndex] + return selectedOption && elementInnerText(selectedOption) + }) } - ] - - this._options.definitions.asserters = [ - query => { - return query.find('input').expectOneElement().transform(([input]) => { - return expected => { - const actual = input.value - if (actual !== expected) { - this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) + }) + this.defineFieldType({ + setter: (query, value) => { + return query + .is('input[type=checkbox]') + .expectOneElement() + .transform(([checkbox]) => { + if (typeof value !== 'boolean') { + throw new Error('expected boolean as argument to set checkbox') } - } - }) + return () => { + if (elementChecked(checkbox) !== value) { + debug('checkbox', checkbox, value) + elementClick(checkbox) + } + } + }) }, - query => { + value: (query) => { + return query + .is('input[type=checkbox]') + .expectOneElement() + .transform(([checkbox]) => { + return elementChecked(checkbox) + }) + } + }) + + this.defineFieldType({ + value: (query) => { return query.expectOneElement().transform(([element]) => { - return expected => { - const actual = elementInnerText(element) - if (actual !== expected) { - this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) - } - } + return elementInnerText(element) }) } - ] - - this._options.definitions.finders.css = function (query, css) { - return query.css(css) - } + }) return this } diff --git a/test/actionsSpec.js b/test/actionsSpec.js index 2b4db05..817b125 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -10,7 +10,7 @@ describe('actions', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().browserMonkey2Compat() }) describe('clicking', function () { diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.js index 931ed8a..21a9570 100644 --- a/test/assemblies/DomAssembly.js +++ b/test/assemblies/DomAssembly.js @@ -145,6 +145,7 @@ module.exports = class DomAssembly { return query.assert(expected).then(() => { throw new Error('expected rejection') }, e => { + expect(e.message).to.contain('could not match') expect(e.actual).to.eql(actual) }) } diff --git a/test/assertSpec.js b/test/assertSpec.js index cfb8e48..8b2f4ac 100644 --- a/test/assertSpec.js +++ b/test/assertSpec.js @@ -13,6 +13,74 @@ describe('assert', function () { browser = assembly.browserMonkey() }) + describe('input types', () => { + it('can assert html elements', async () => { + assembly.insertHtml(` + 12 Hapless Boulevard + `) + + await browser.assert({ + '.address': '12 Hapless Boulevard', + }) + }) + + it('can assert text inputs', async () => { + assembly.insertHtml(` + + `) + + await browser.assert({ + '.address': '12 Hapless Boulevard', + }) + }) + + it('can reject text inputs', async () => { + assembly.insertHtml(` + + `) + + await assembly.assertExpectedActual(browser, { + '.address': '12 Hapless Boulevard', + }, { + '.address': '12 apless Boulevard', + }) + }) + + it('can reject text inputs when the element is not found at all', async () => { + assembly.insertHtml(` + `) + + await assembly.assertExpectedActual(browser, { + '.address': '12 Hapless Boulevard', + }, { + '.address': "Error: expected 1 element, found 0 (found: find('.address') [0])", + }) + }) + + it('can assert select options', async () => { + assembly.insertHtml(` + + `) + + await browser.assert({ + 'select': 'Two', + }) + }) + + it('can assert checkbox', async () => { + assembly.insertHtml(` + + `) + + await browser.assert({ + '.check': true, + }) + }) + }) + describe('objects', function () { it('passes when the fields are there and have the expected values', async () => { assembly.insertHtml(` @@ -40,7 +108,7 @@ describe('assert', function () { '.content': 'The Content' }, { 'h1': 'Title', - '.content': undefined + '.content': "Error: expected 1 element, found 0 (found: find('.content') [0])" }) }) }) @@ -160,28 +228,7 @@ describe('assert', function () { }) }) - it('fails when query from function fails with error with actual', async () => { - assembly.insertHtml(` -
-

Title

-
The Content
-
- `) - - await assembly.assertExpectedActual(browser, { - 'h1': 'Title', - '.content': query => { - if (/Content/.test(query.expectOneElement().result()[0].innerText)) { - throw new BrowserMonkeyAssertionError('asdf', {actual: 'Content'}) - } - } - }, { - 'h1': 'Title', - '.content': 'Content' - }) - }) - - it('fails when query from function fails with error without actual', async () => { + it('fails when query from function fails with error', async () => { assembly.insertHtml(`

Title

@@ -189,18 +236,16 @@ describe('assert', function () {
`) - let error = new BrowserMonkeyAssertionError('asdf') - await assembly.assertExpectedActual(browser, { 'h1': 'Title', '.content': query => { if (/Content/.test(query.expectOneElement().result()[0].innerText)) { - throw error + throw new BrowserMonkeyAssertionError('asdf') } } }, { 'h1': 'Title', - '.content': error + '.content': 'Error: asdf' }) }) }) diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index 26a0c2e..9a14793 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -9,7 +9,7 @@ describe('assertions', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().browserMonkey2Compat() }) describe('shouldNotExist', function () { diff --git a/test/buttonSpec.js b/test/buttonSpec.js index ca50758..ea848e2 100644 --- a/test/buttonSpec.js +++ b/test/buttonSpec.js @@ -9,7 +9,7 @@ describe('buttons', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().browserMonkey2Compat() }) it('can recognise a button with exact text', function () { diff --git a/test/componentSpec.js b/test/componentSpec.js index 2c886fc..7af7730 100644 --- a/test/componentSpec.js +++ b/test/componentSpec.js @@ -8,7 +8,7 @@ describe('component', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().browserMonkey2Compat() }) it('can return new selectors by extending', function () { diff --git a/test/eventsSpec.js b/test/eventsSpec.js index 09ba49e..00f3764 100644 --- a/test/eventsSpec.js +++ b/test/eventsSpec.js @@ -10,7 +10,7 @@ describe('events', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().browserMonkey2Compat() }) it('typeIn element should fire change', function () { diff --git a/test/fieldsSpec.js b/test/fieldsSpec.js index de84d07..ccdf6f8 100644 --- a/test/fieldsSpec.js +++ b/test/fieldsSpec.js @@ -9,7 +9,7 @@ describe('fields', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().browserMonkey2Compat() }) it('can recognise a label with input', () => { diff --git a/test/findersSpec.js b/test/findersSpec.js index 134ce40..e648f0b 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -10,7 +10,7 @@ describe('find', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().browserMonkey2Compat() }) it('should eventually find an element', function () { diff --git a/test/optionsSpec.js b/test/optionsSpec.js index a4726d8..b999ee6 100644 --- a/test/optionsSpec.js +++ b/test/optionsSpec.js @@ -9,7 +9,7 @@ describe('options', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().browserMonkey2Compat() }) it('can overide default timeout', function () { diff --git a/test/reloadButtonSpec.js b/test/reloadButtonSpec.js index 19dc19e..d69a759 100644 --- a/test/reloadButtonSpec.js +++ b/test/reloadButtonSpec.js @@ -10,7 +10,7 @@ describe('reload button', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().scope(document.body) + browser = assembly.browserMonkey().scope(document.body).browserMonkey2Compat() }) it('adds reload button that reloads to the initial url', function () { diff --git a/test/setSpec.js b/test/setSpec.js index a97bc0c..9b9f91b 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -13,24 +13,46 @@ describe('set', function () { browser = assembly.browserMonkey() }) - it('can set fields by CSS', async () => { - assembly.insertHtml(` -
+ describe('field types', () => { + it('can set text fields', async () => { + assembly.insertHtml(` - - -
- `) - - await browser.set({ - '.address': '7 Lola St', - '.phone': '123123123', - '.first-name': 'Barry' + `) + + await browser.set({ + '.address': '7 Lola St', + }) + + demand(assembly.find('.address').value).to.equal('7 Lola St') + }) + + it('can set select fields', async () => { + assembly.insertHtml(` + + `) + + await browser.set({ + 'select': 'Two', + }) + + const selectedItem = assembly.jQuery(assembly.find('select')).find(':selected').text() + expect(selectedItem).to.equal('Two') }) - demand(assembly.find('.address').value).to.equal('7 Lola St') - demand(assembly.find('.phone').value).to.equal('123123123') - demand(assembly.find('.first-name').value).to.equal('Barry') + it('can set checkbox fields', async () => { + const checkbox = assembly.insertHtml(` + + `) + + await browser.set({ + '.check': true, + }) + + expect(checkbox.checked).to.equal(true) + }) }) describe('objects', function () { @@ -186,26 +208,26 @@ describe('set', function () { }) }) - it('setting fields is atomic', async () => { - assembly.insertHtml(` -
- - -
- `) - - const promise = browser.find('form').set({ - '.street': '7 Lola St', - '.phone': '123123123', - '.first-name': 'Barry' - }) - await assembly.assertRejection(promise, "expected 1 element, found 0 (found: find('form') [1], find('.street') [0])") + describe('selecting options atomically', () => { + it('setting fields is atomic', async () => { + assembly.insertHtml(` +
+ + +
+ `) - demand(assembly.find('.phone').value).to.equal('') - demand(assembly.find('.first-name').value).to.equal('') - }) + const promise = browser.find('form').set({ + '.street': '7 Lola St', + '.phone': '123123123', + '.first-name': 'Barry' + }) + await assembly.assertRejection(promise, "expected 1 element, found 0 (found: find('form') [1], find('.street') [0])") + + demand(assembly.find('.phone').value).to.equal('') + demand(assembly.find('.first-name').value).to.equal('') + }) - describe('selecting options atomically', () => { it('waits until the desired option appears on the page', async () => { const select = assembly.insertHtml(` - - - `) + describe('defining fields', () => { + it('can define a named field', async function () { + assembly.insertHtml(` +
+ + +
+ `) + + browser.define('phone', b => b.find('.phone')) + browser.define('firstName', b => b.find('.first-name')) - browser.define('phone', b => b.find('.phone')) - browser.define('firstName', b => b.find('.first-name')) + await browser.set({ + phone: '123123123', + firstName: 'Barry' + }) - await browser.set({ - phone: '123123123', - firstName: 'Barry' + demand(assembly.find('.phone').value).to.equal('123123123') + demand(assembly.find('.first-name').value).to.equal('Barry') }) - demand(assembly.find('.phone').value).to.equal('123123123') - demand(assembly.find('.first-name').value).to.equal('Barry') - }) + it('can define a named field with parameter', async function () { + assembly.insertHtml(` +
+ + +
+ `) - it('can define a named finder', async function () { - assembly.insertHtml(` -
- - -
- `) + browser.define('form', (b, name) => b.find('* [name=' + JSON.stringify(name) + ']')) - browser.define('form', (b, name) => b.find('* [name=' + JSON.stringify(name) + ']')) + await browser.set({ + 'form(phone)': '123123123', + 'form(firstname)': 'Barry' + }) - await browser.set({ - 'form(phone)': '123123123', - 'form(firstname)': 'Barry' + demand(assembly.find('.phone').value).to.equal('123123123') + demand(assembly.find('.first-name').value).to.equal('Barry') }) - - demand(assembly.find('.phone').value).to.equal('123123123') - demand(assembly.find('.first-name').value).to.equal('Barry') }) }) }) From dc46003ab1cac80be9f52337f42d4cefc6b7be22 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 17 Jun 2019 12:03:19 +0200 Subject: [PATCH 020/127] click, containing specs --- create.js | 9 +- lib/actions.js | 14 --- lib/assertions.js | 10 +- lib/fields.js | 43 ------- lib/finders.js | 3 - lib/query.ts | 104 ++++++----------- lib/set.js | 77 +++++++++++-- package.json | 2 +- test/actionsSpec.js | 114 +----------------- test/assertSpec.js | 80 +++++++++++++ test/assertionsSpec.js | 6 +- test/buttonSpec.js | 16 ++- test/componentSpec.js | 2 +- test/containingSpec.js | 70 +++++++++++ test/eventsSpec.js | 2 +- test/fieldsSpec.js | 242 --------------------------------------- test/findersSpec.js | 44 +------ test/optionsSpec.js | 2 +- test/reloadButtonSpec.js | 2 +- test/setSpec.js | 63 +++++++++- 20 files changed, 347 insertions(+), 558 deletions(-) delete mode 100644 lib/fields.js create mode 100644 test/containingSpec.js delete mode 100644 test/fieldsSpec.js diff --git a/create.js b/create.js index dec270d..59f67b9 100644 --- a/create.js +++ b/create.js @@ -2,7 +2,6 @@ var Query = require('./lib/query') var finders = require('./lib/finders') var actions = require('./lib/actions') var button = require('./lib/button') -var fields = require('./lib/fields') var set = require('./lib/set') var assertions = require('./lib/assertions') @@ -10,13 +9,9 @@ module.exports = function (rootSelector = document.body) { return new Query() .component(finders) .component(button) - .component(fields) .component(set) .installSetters() .scope(rootSelector) - .component({ - browserMonkey2Compat: function () { - return this.component(actions).component(assertions) - } - }) + .component(actions) + .component(assertions) } diff --git a/lib/actions.js b/lib/actions.js index 72b82ff..c462199 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -32,20 +32,6 @@ module.exports = { } }, - enter: function (selector) { - return this.find(selector).expectOneElement().action(function ([element]) { - debug('enter', element) - elementSubmit(element) - }) - }, - - click: function () { - return this.enabled().expectOneElement().then(function ([element]) { - debug('click', element) - elementClick(element) - }) - }, - select: function (value) { var criteria = typeof value === 'string' ? [{ exactText: value }, { exactValue: value }] diff --git a/lib/assertions.js b/lib/assertions.js index 4c6d35f..77c4fbb 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -76,10 +76,12 @@ function toArray (i) { return i instanceof Array ? i : [i] } -function assertInexactValues (actual, expected) { +function assertContainsValues (actual, expected) { var equal = actual.length === expected.length && actual.every(function (a, index) { - if (expected[index] === '') { + if (a === undefined) { + return expected[index] === undefined + } else if (expected[index] === '') { return a === '' } else { return a.indexOf(expected[index]) !== -1 @@ -108,7 +110,7 @@ var elementExpectations = { return elementInnerText(element) }) - assertInexactValues(actual, expectedArray) + assertContainsValues(actual, expectedArray) }, value: function (query, elements, expected) { @@ -116,7 +118,7 @@ var elementExpectations = { var actual = queryValue(query, elements) - assertInexactValues(actual, expectedArray) + assertContainsValues(actual, expectedArray) }, exactValue: function (query, elements, expected) { diff --git a/lib/fields.js b/lib/fields.js deleted file mode 100644 index 4611de8..0000000 --- a/lib/fields.js +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = { - field: function (name) { - return this.concat(this._options.definitions.field.map(function (definition) { - return function (monkey) { - return definition(monkey, name) - } - })) - }, - - defineField: function (definition) { - return this.clone(function (clone) { - clone._options.definitions.field.push(definition) - }) - }, - - defineFieldValue: function (definition) { - return this.clone(function (clone) { - clone._options.definitions.fieldValue.push(definition) - }) - }, - - setField: function (name, value) { - return this.field(name).setValue(value) - }, - - value: function () { - var getters = this._options.definitions.fieldValue.map(function (definition) { - return definition.get - }) - - return this.firstOf(getters) - }, - - setValue: function (value) { - var setters = this._options.definitions.fieldValue.map(function (definition) { - return function (monkey) { - return definition.set(monkey, value) - } - }) - - return this.firstOf(setters) - } -} diff --git a/lib/finders.js b/lib/finders.js index 77e7dee..97e6852 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -31,9 +31,6 @@ module.exports = { }) }, - elementsMatching: function (pattern) { - }, - is: function (selector) { return this.filter(function (element) { return elementMatches(element, selector) diff --git a/lib/query.ts b/lib/query.ts index 00c4ed1..6e175f9 100644 --- a/lib/query.ts +++ b/lib/query.ts @@ -14,6 +14,8 @@ var uniq = require('lowscore/uniq') var debug = require('debug')('browser-monkey') var inputsSelector = require('./inputsSelector') const elementEnterText = require('./elementEnterText') +var elementClick = require('./elementClick') +var elementSubmit = require('./elementSubmit') type Transform = (elements: any, executedTransforms: ExecutedTransform[]) => any type Action = (elements: any, executedTransforms: ExecutedTransform[]) => void @@ -31,12 +33,6 @@ class Query { }[] button: ((monkey: any, name: any) => any)[] fields: {} - field: ((monkey: any, name: any) => any)[] - fieldValue: { - get: (monkey: any) => any - set: (monkey: any, value: any) => void - setter?: (monkey: any, value: any) => void - }[] } } @@ -56,75 +52,17 @@ class Query { fieldTypes: [], button: [ function (monkey, name) { - return monkey.find('button', { exactText: name }) + return monkey.css('button', { exactText: name }) }, function (monkey, name) { - return monkey.find('input[type=button]', { exactValue: name }) + return monkey.css('input[type=button]', { exactValue: name }) }, function (monkey, name) { - return monkey.find('a', { exactText: name }) + return monkey.css('a', { exactText: name }) } ], fields: {}, - - field: [ - function (monkey, name) { - return monkey.find('label', { exactText: name }).find('input') - }, - function (monkey, name) { - return monkey.find('input').filter(function (element) { - var labelledBy = element.getAttribute('aria-labelledby') - if (labelledBy) { - var ids = labelledBy.split(/\s+/) - var labels = ids.map(function (id) { - return element.ownerDocument.getElementById(id) - }) - var label = labels.filter(Boolean).map(function (l) { - return elementInnerText(l) - }).join(' ') - return name === label - } - }, 'aria-labelledby ' + inspect({ text: name })) - } - ], - fieldValue: [ - { - set: function (monkey, value) { - return monkey.typeIn(value) - }, - setter: function (monkey) { - return monkey.is(inputsSelector).element().mapAll(function (element) { - return function (value) { - if (typeof value !== 'string') { - throw new Error('expected string as argument to typeIn') - } - debug('typeIn', element, value) - elementEnterText(element, value) - } - }) - }, - get: function (monkey) { - return monkey.inputValue() - } - }, - { - set: function (monkey, value) { - return monkey.select(value) - }, - get: function (monkey) { - return monkey.selectValue() - } - }, - { - set: function (monkey, value) { - return monkey.check(value) - }, - get: function (monkey) { - return monkey.checked() - } - } - ] } } this._input = undefined @@ -155,6 +93,18 @@ class Query { }) } + public button (name: string): this { + return this.concat(this._options.definitions.button.map(definition => { + return (q: Query): Query => { + return definition(q, name) + } + })) + } + + public defineButton (definition: (q: Query, name?: string) => Query): void { + this._options.definitions.button.push(definition) + } + public result (): any { return this.execute().value } @@ -172,7 +122,7 @@ class Query { }) } - public concat (queryCreators: [(q: Query) => Query]): this { + public concat (queryCreators: ((q: Query) => Query)[]): this { return this.transform((elements) => { const resolved = this.resolve(elements) @@ -279,6 +229,24 @@ class Query { return this.css(selector, options) } + public submit (): void { + return this.expectOneElement().action(function ([element]) { + debug('enter', element) + elementSubmit(element) + }) + } + + public click (name?: string): void { + const query = name === undefined + ? this + : this.button(name) + + return query.enabled().expectOneElement().action(function ([element]) { + debug('click', element) + elementClick(element) + }) + } + public options (options): Options { if (options) { extend(this._options, options) diff --git a/lib/set.js b/lib/set.js index 3d40496..b03b557 100644 --- a/lib/set.js +++ b/lib/set.js @@ -10,6 +10,7 @@ const object = require('lowscore/object') const range = require('lowscore/range') const pluralize = require('pluralize') const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default +const { ExecutedSimpleTransform } = require('./ExecutedSimpleTransform') function expectLength (query, length) { return query.expect(elements => { @@ -54,7 +55,7 @@ function mapModel (query, model, actions) { }) } else if (model.constructor === Object) { return object(Object.entries(model).map(([selector, value]) => { - return [selector, map(expectLength(query, 1).find(selector), value)] + return [selector, map(query.find(selector), value)] })) } else if (typeof model === 'function') { return actions.function(query, model) @@ -67,7 +68,13 @@ function mapModel (query, model, actions) { } module.exports = { - set: function (model) { + set: function (selector, value) { + const model = value === undefined + ? selector + : { + [selector]: value + } + return this.action(function (elements) { const setters = [] @@ -82,7 +89,7 @@ module.exports = { }, function: (query, model) => { - setters.push(() => model(query)) + setters.push(() => runModelFunction(model, query)) } } @@ -134,7 +141,7 @@ module.exports = { function: (query, model) => { try { - model(query) + runModelFunction(model, query) return model } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { @@ -173,22 +180,61 @@ module.exports = { }, asserter: function (expected) { - return this.firstOf(this._options.definitions.fieldTypes.map(def => { - return query => def.value(query).transform(actual => { - return () => { - if (actual !== expected) { - this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) - } + return this.value().transform(actual => { + return () => { + if (expected instanceof RegExp ? !expected.test(actual) : actual !== expected) { + this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) } - }) - })) + } + }) }, defineFieldType: function (fieldTypeDefinition) { this._options.definitions.fieldTypes.push(fieldTypeDefinition) }, + containing: function (model) { + return this.transform(elements => { + const actions = { + arrayLengthError: (query, actualLength, expectedLength) => { + query.error('expected ' + expectedLength + ' ' + pluralize('elements', expectedLength) + ', found ' + actualLength) + }, + + value: (query, value) => { + const asserter = query.asserter(value).result() + asserter() + }, + + function: (query, fn) => { + runModelFunction(fn, query) + } + } + + return new ExecutedSimpleTransform(elements.filter(element => { + try { + const clone = this.resolve([element]) + mapModel(clone, model, actions) + return true + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + return false + } else { + throw e + } + } + }), `containing(${JSON.stringify(model)})`) + }) + }, + + value: function () { + return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.value).map(def => { + return query => def.value(query) + })) + }, + installSetters: function () { + this.define('css', (query, css) => query.css(css)) + this.defineFieldType({ setter: (query, value) => { return query @@ -274,3 +320,10 @@ module.exports = { return this } } + +function runModelFunction(fn, query) { + const result = fn(query) + if (result && typeof result.then === 'function') { + query.error('model functions must not be asynchronous') + } +} diff --git a/package.json b/package.json index 0125c97..7344c1b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "yarn test-browser-compatibility && yarn test-electron-mocha && yarn karma && eslint .", "karma": "karma start --single-run", "test-electron-mocha": "yarn electron-mocha test/*Spec.js test/**/*Spec.js", - "electron-mocha": "electron-mocha -r test/register --renderer", + "electron-mocha": "electron-mocha --disable-site-isolation-trials -r test/register --renderer", "test-browser-compatibility": "browserify ./index.js > /dev/null", "update-readme-example-links": "node ./docs/update-readme-example-links.js" }, diff --git a/test/actionsSpec.js b/test/actionsSpec.js index 817b125..1c5ee1f 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -10,12 +10,12 @@ describe('actions', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().browserMonkey2Compat() + browser = assembly.browserMonkey() }) describe('clicking', function () { it('should eventually click an element', function () { - var promise = browser.find('.element').click() + var promise = browser.find('.element').click().then() var clicked = false assembly.eventuallyInsertHtml( @@ -66,7 +66,7 @@ describe('actions', function () { }, { vdom: false }) it('waits until checkbox is enabled before clicking', function () { - var promise = browser.find('input[type=checkbox]').click() + var promise = browser.find('input[type=checkbox]').click().then() var clicked var buttonState = 'disabled' @@ -86,7 +86,7 @@ describe('actions', function () { }) it('waits until button is enabled before clicking', function () { - var promise = browser.find('button', { text: 'a button' }).click() + var promise = browser.find('button', { text: 'a button' }).click().then() var clicked var buttonState = 'disabled' @@ -266,7 +266,7 @@ describe('actions', function () { it('should submit the form when submit button is clicked', function () { var submitted = false - var promise = browser.find('input').click() + var promise = browser.find('input').click().then() assembly.eventuallyInsertHtml( assembly.jQuery('
submit
').on('submit', function (ev) { @@ -396,109 +396,5 @@ describe('actions', function () { }) }) }) - - describe('fill', function () { - it('fills a component with the supplied values', function () { - var component = browser.component({ - title: function () { - return this.find('.title') - }, - name: function () { - return this.find('.name') - } - }) - assembly.insertHtml('') - assembly.insertHtml('') - - return component.fill([ - { name: 'title', action: 'select', options: { exactText: 'Mr' } }, - { name: 'name', action: 'typeIn', options: 'Joe' } - ]).then(function () { - demand(assembly.find('.title').value).to.equal('Mr') - demand(assembly.find('.name').value).to.equal('Joe') - }) - }) - - it('can fill using shortcut syntax', function () { - var component = browser.component({ - title: function () { - return this.find('.title') - }, - name: function () { - return this.find('.name') - }, - agree: function () { - return this.find('.agree') - } - }) - assembly.insertHtml('') - assembly.insertHtml('') - assembly.insertHtml('') - - return component.fill([ - { select: 'title', text: 'Mrs' }, - { typeIn: 'name', options: 'Joe' }, - { click: 'agree' } - ]).then(function () { - demand(assembly.find('.title').value).to.equal('Mrs') - demand(assembly.find('.name').value).to.equal('Joe') - demand(assembly.find('.agree input').checked).to.equal(true) - }) - }) - - it('can execute actions on a component', function () { - var myActionRan = false - var component = browser.component({ - myAction: function () { - myActionRan = true - - return new Promise(function (resolve) { - resolve() - }) - } - }).component({ - title: function () { - return this.find('.title') - } - }) - assembly.insertHtml('') - - return component.fill([ - { myAction: 'title' } - ]).then(function () { - demand(myActionRan).to.equal(true) - }) - }) - - it('throws an error if the action cannot be found', function () { - var component = browser.component({}) - - var promise = component.fill([ - { actionDoesNotExist: 'name' } - ]) - - return assembly.assertRejection(promise, 'actionDoesNotExist') - }) - - it('throws an error when trying to call an action on a field which does not exist', function () { - var component = browser.component({}) - - var promise = component.fill([ - { typeIn: 'name' } - ]) - - return assembly.assertRejection(promise, "Field 'name' does not exist") - }) - - it('throws an error if the field does not exist', function () { - var component = browser.component({}) - - var promise = component.fill( - { name: 'address', action: 'blah' } - ) - - return assembly.assertRejection(promise, "No field 'address' exists on this component") - }) - }) }) }) diff --git a/test/assertSpec.js b/test/assertSpec.js index 8b2f4ac..7431e0e 100644 --- a/test/assertSpec.js +++ b/test/assertSpec.js @@ -81,6 +81,68 @@ describe('assert', function () { }) }) + describe('values', () => { + it('matches if the exact text of the element is the same', async () => { + assembly.insertHtml(` +
+
+ +
+
+ `) + + await browser.find('.address .street').assert('7 Lola St') + }) + + it('single argument, with scope asserts value', async () => { + assembly.insertHtml(` +
+
+ +
+
+ `) + + await assembly.assertExpectedActual(browser.find('.address .street'), '8 Lola St', '7 Lola St') + }) + + it('fails even if it contains the right text', async () => { + assembly.insertHtml(` +
+
+ +
+
+ `) + + await assembly.assertExpectedActual(browser.find('.address .street'), '8 Rue de Paris', '8 Rue de Paris, St Etienne') + }) + + it('can match a resulgar expression', async () => { + assembly.insertHtml(` +
+
+ +
+
+ `) + + await browser.find('.address .street').assert(/\d+ Lola St/) + }) + + it('fails if the regular expression does not match', async () => { + assembly.insertHtml(` +
+
+ +
+
+ `) + + await assembly.assertExpectedActual(browser.find('.address .street'), /\d+ High St/, '7 Lola St') + }) + }) + describe('objects', function () { it('passes when the fields are there and have the expected values', async () => { assembly.insertHtml(` @@ -248,6 +310,24 @@ describe('assert', function () { '.content': 'Error: asdf' }) }) + + it('throws if the function is asynchronous', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + await assembly.assertExpectedActual(browser, { + 'h1': 'Title', + '.content': async () => { + } + }, { + 'h1': 'Title', + '.content': 'Error: model functions must not be asynchronous' + }) + }) }) }) }) diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index 9a14793..2dc7281 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -9,7 +9,7 @@ describe('assertions', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().browserMonkey2Compat() + browser = assembly.browserMonkey() }) describe('shouldNotExist', function () { @@ -188,8 +188,8 @@ describe('assertions', function () { var select = browser.find('select') return Promise.all([ - assembly.assertRejection(select.shouldHave({ value: '' }), 'expected select options'), - assembly.assertRejection(select.shouldHave({ exactValue: '' }), 'expected select options'), + select.shouldHave({ value: undefined }), + select.shouldHave({ exactValue: undefined }), ]) }) diff --git a/test/buttonSpec.js b/test/buttonSpec.js index ea848e2..b9f822f 100644 --- a/test/buttonSpec.js +++ b/test/buttonSpec.js @@ -9,7 +9,7 @@ describe('buttons', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().browserMonkey2Compat() + browser = assembly.browserMonkey() }) it('can recognise a button with exact text', function () { @@ -80,7 +80,17 @@ describe('buttons', function () { const button = assembly.insertHtml('') button.addEventListener('click', () => events.push('click')) - return browser.clickButton('Login').then(function () { + return browser.click('Login').then(function () { + demand(events).to.eql(['click']) + }) + }) + + it('can find and click a button', () => { + const events = [] + const button = assembly.insertHtml('') + button.addEventListener('click', () => events.push('click')) + + return browser.find('button').click().then(function () { demand(events).to.eql(['click']) }) }) @@ -93,7 +103,7 @@ describe('buttons', function () { browser.defineButton((monkey, name) => monkey.find('div.button', { exactText: name })) - return browser.clickButton('Login').then(function () { + return browser.click('Login').then(function () { demand(events).to.eql(['click']) }) }) diff --git a/test/componentSpec.js b/test/componentSpec.js index 7af7730..2c886fc 100644 --- a/test/componentSpec.js +++ b/test/componentSpec.js @@ -8,7 +8,7 @@ describe('component', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().browserMonkey2Compat() + browser = assembly.browserMonkey() }) it('can return new selectors by extending', function () { diff --git a/test/containingSpec.js b/test/containingSpec.js new file mode 100644 index 0000000..af09089 --- /dev/null +++ b/test/containingSpec.js @@ -0,0 +1,70 @@ +const describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +const {expect} = require('chai') + +describe('containing', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + + describe('values', () => { + it('filters elements that have the exact text', () => { + assembly.insertHtml(` + car + car + horse + cart + `) + + const transports = browser.find('span').containing('car').result() + const cars = assembly.findAll('.car') + expect(transports).to.eql(cars) + }) + }) + + describe('objects', () => { + it('must have all the fields in the same element', () => { + assembly.insertHtml(` +
+
Title
+
+
+
Title
+
Body
+
+
+
Body
+
+ `) + + const results = browser.find('.result').containing({'.title': 'Title', '.body': 'Body'}).result() + const expected = assembly.findAll('.correct') + expect(results).to.eql(expected) + }) + + it("shows why it couldn't find the element", () => { + assembly.insertHtml(` +
+
Title
+
+
+
Title
+
Body
+
+
+
Body
+
+ `) + + expect(() => + browser.find('.result').containing({'.title': 'Title', '.body': 'None'}).expectOneElement().result() + ).to.throw(`expected just one element, found 0 (found: find('.result') [3], containing({".title":"Title",".body":"None"}) [0])`) + }) + }) + }) +}) diff --git a/test/eventsSpec.js b/test/eventsSpec.js index 00f3764..09ba49e 100644 --- a/test/eventsSpec.js +++ b/test/eventsSpec.js @@ -10,7 +10,7 @@ describe('events', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().browserMonkey2Compat() + browser = assembly.browserMonkey() }) it('typeIn element should fire change', function () { diff --git a/test/fieldsSpec.js b/test/fieldsSpec.js deleted file mode 100644 index ccdf6f8..0000000 --- a/test/fieldsSpec.js +++ /dev/null @@ -1,242 +0,0 @@ -var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') -var demand = require('must') - -describe('fields', function () { - describeAssemblies([DomAssembly], function (Assembly) { - var assembly - var browser - - beforeEach(function () { - assembly = new Assembly() - browser = assembly.browserMonkey().browserMonkey2Compat() - }) - - it('can recognise a label with input', () => { - const field = assembly.insertHtml('').querySelector('input') - - const elements = browser.field('Email').result() - demand(elements).to.eql([field]) - }) - - it('can recognise an input with aria-labelledby', () => { - assembly.insertHtml('') - const field = assembly.insertHtml('') - - const elements = browser.field('Email').result() - demand(elements).to.eql([field]) - }) - - describe('defineField', () => { - it('can define a new field', () => { - const fieldDiv = assembly.insertHtml('
Email
') - const field = fieldDiv.querySelector('input') - - const customFieldBrowser = browser.defineField((monkey, name) => monkey.find('div.field').containing('.field-name', { exactText: name }).find('input')) - - const elements = customFieldBrowser.field('Email').result() - demand(elements).to.eql([field]) - }) - - it('can define a new field and still use original field definitions', () => { - const field = assembly.insertHtml('').querySelector('input') - - const customFieldBrowser = browser.defineField((monkey, name) => monkey.find('div.field').containing('.field-name', { exactText: name }).find('input')) - - const elements = customFieldBrowser.field('Email').result() - demand(elements).to.eql([field]) - }) - }) - - describe('defineFieldValue', () => { - it('matches the right field type', async () => { - const fieldDiv = assembly.insertHtml('
') - - const custom = browser.defineFieldValue({ - set: (monkey, value) => monkey.is('div.nomatch').expectOneElement().action(([element]) => { element.innerText = 'no-match: ' + value }), - get: (monkey) => monkey.is('div.nomatch').expectOneElement().text() - }).defineFieldValue({ - set: (monkey, value) => monkey.is('div.field').expectOneElement().action(([element]) => { element.innerText = 'match: ' + value }), - get: (monkey) => monkey.is('div.field').expectOneElement().text() - }) - - await custom.find('.field').setValue('value') - demand(fieldDiv.innerText).to.equal('match: value') - }) - - it('throws if no match can be found', async () => { - assembly.insertHtml('
') - - const custom = browser.defineFieldValue({ - set: (monkey, value) => monkey.is('div.nomatch').expectOneElement().action(([element]) => { element.innerText = 'no-match: ' + value }), - get: (monkey) => monkey.is('div.nomatch').expectOneElement().text() - }).defineFieldValue({ - set: (monkey, value) => monkey.is('div.field').expectOneElement().action(([element]) => { element.innerText = 'match: ' + value }), - get: (monkey) => monkey.is('div.field').expectOneElement().text() - }) - - await demand(custom.find('.non-field').setValue('value')).to.reject.with.error(/all queries failed in firstOf/) - }) - }) - - describe('set value', () => { - describe('setting input values', () => { - [ - ['date', '1970-01-01'], - ['datetime-local', '1970-01-01T09:30'], - 'email', - ['month', '1970-01'], - ['number', '1'], - 'password', - 'search', - 'tel', - 'text', - ['time', '09:30'], - 'url', - ['week', '1970-W01'], - ['range', '50'] - ].forEach(type => { - if (type instanceof Array) { - canSetInputValueOfType.apply(undefined, type) - } else { - canSetInputValueOfType(type) - } - }) - - canSetInputValueOfHtml('') - }) - - function canSetInputValueOfType (type, value) { - canSetInputValueOfHtml(``, value) - } - - function canSetInputValueOfHtml (html, value = 'value') { - it(`can set ${html} value`, async () => { - const events = [] - const field = assembly.insertHtml(html) - field.addEventListener('change', () => events.push('change')) - - await browser.scope(field).setValue(value) - - demand(events).to.eql(['change']) - demand(field.value).to.equal(value) - - const valueSet = await browser.scope(field).value() - demand(valueSet).to.equal(value) - }) - } - - describe('setting select values', () => { - it('can set a select value by option value', async () => { - const select = assembly.insertHtml(` - - `) - - const selectScope = browser.scope(select) - - await selectScope.setValue('value1') - const value = await selectScope.value() - demand(value).to.equal('Text One') - }) - - it('can set a select value by option text', async () => { - const select = assembly.insertHtml(` - - `) - - const selectScope = browser.scope(select) - - await selectScope.setValue('Text Two') - const value = await selectScope.value() - demand(value).to.equal('Text Two') - }) - - it('waits for option to appear before selecting it', async () => { - const select = assembly.insertHtml(` - - `) - - const selectScope = browser.scope(select) - const setPromise = selectScope.setValue('Text Three').then() - - assembly.eventuallyAppendHtml(select, '') - - await setPromise - const value = await selectScope.value() - demand(value).to.equal('Text Three') - }) - }) - - describe('setting a checkbox value', () => { - it('can set a checkbox value', async () => { - const checkbox = assembly.insertHtml(` - - `) - - const selectScope = browser.scope(checkbox) - - await selectScope.setValue(true) - const trueValue = await selectScope.value() - demand(trueValue).to.equal(true) - - await selectScope.setValue(false) - const falseValue = await selectScope.value() - demand(falseValue).to.equal(false) - }) - }) - }) - - describe('shouldHave value', () => { - it('uses .value in shouldHave({exactValue})', async () => { - const custom = browser.defineFieldValue({ - get: (monkey) => monkey.expect(() => {}).transform(() => 'div value') - }) - - await custom.shouldHave({ exactValue: 'div value' }) - }) - - it('uses .value in shouldHave({value})', async () => { - const custom = browser.defineFieldValue({ - get: (monkey) => monkey.expect(() => {}).transform(() => 'div value') - }) - - await custom.shouldHave({ value: 'value' }) - }) - }) - - describe('setting a field', () => { - it("can set a field's value", () => { - const events = [] - const field = assembly.insertHtml('') - field.addEventListener('change', () => events.push('change')) - - return browser.setField('Email', 'bob@example.com').then(function () { - demand(events).to.eql(['change']) - }) - }) - - it("can set a defined field's value", () => { - const events = [] - const fieldDiv = assembly.insertHtml('
Email
') - const field = fieldDiv.querySelector('input') - - field.addEventListener('change', () => events.push('change')) - - const custom = browser.defineField((monkey, name) => monkey.find('div.field').containing('.field-name', { exactText: name }).find('input')) - - return custom.setField('Email', 'bob@example.com').then(function () { - demand(events).to.eql(['change']) - }) - }) - }) - }) -}) diff --git a/test/findersSpec.js b/test/findersSpec.js index e648f0b..8d24e63 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -10,7 +10,7 @@ describe('find', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().browserMonkey2Compat() + browser = assembly.browserMonkey() }) it('should eventually find an element', function () { @@ -130,48 +130,6 @@ describe('find', function () { }) }) - describe('containing', function () { - it('eventually finds an element containing another element', function () { - var promise = browser.find('.outer').containing('.inner').shouldExist().then() - - assembly.eventuallyInsertHtml( - '
bad
' + - '
good
' - ) - - return promise - }) - - it('element returns the outer element', function () { - var promise = browser.find('.outer').containing('.inner').expectOneElement().then() - - assembly.eventuallyInsertHtml( - '
bad
' + - '
good
' - ) - - return promise.then(function ([element]) { - demand(element).to.eql(assembly.find('#good')) - }) - }) - - it("fails if it can't find an element containing another", function () { - var promise = browser.find('.outer').containing('.inner').shouldExist().then() - - assembly.eventuallyInsertHtml('
bad
') - - return demand(promise).reject.with.error(/expected one or more elements/) - }) - - it("fails if it can't find an element containing another", function () { - var promise = browser.find('.outer').containing('.inner').shouldExist().then() - - assembly.eventuallyInsertHtml('
bad
') - - return demand(promise).reject.with.error() - }) - }) - describe('chains', function () { it('eventually finds the inner element, even if the outer element exists', function () { assembly.insertHtml('
') diff --git a/test/optionsSpec.js b/test/optionsSpec.js index b999ee6..a4726d8 100644 --- a/test/optionsSpec.js +++ b/test/optionsSpec.js @@ -9,7 +9,7 @@ describe('options', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().browserMonkey2Compat() + browser = assembly.browserMonkey() }) it('can overide default timeout', function () { diff --git a/test/reloadButtonSpec.js b/test/reloadButtonSpec.js index d69a759..19dc19e 100644 --- a/test/reloadButtonSpec.js +++ b/test/reloadButtonSpec.js @@ -10,7 +10,7 @@ describe('reload button', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().scope(document.body).browserMonkey2Compat() + browser = assembly.browserMonkey().scope(document.body) }) it('adds reload button that reloads to the initial url', function () { diff --git a/test/setSpec.js b/test/setSpec.js index 9b9f91b..908dab5 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -55,6 +55,22 @@ describe('set', function () { }) }) + describe('values', () => { + it('single argument, with scope sets value', async () => { + assembly.insertHtml(` +
+
+ +
+
+ `) + + await browser.find('.address .street').set('7 Lola St') + + demand(assembly.find('.address .street').value).to.equal('7 Lola St') + }) + }) + describe('objects', function () { it('can set deep fields by CSS', async () => { assembly.insertHtml(` @@ -89,6 +105,50 @@ describe('set', function () { demand(assembly.find('.first-name').value).to.equal('Barry') }) + it("an object doesn't assert the number of matching elements", async () => { + assembly.insertHtml(` +
+
+ +
+
+ +
+
+ `) + + await browser.set({ + '.address': { + '.street': '7 Lola St', + '.city': 'Frisby City', + }, + }) + + demand(assembly.find('.address .street').value).to.equal('7 Lola St') + demand(assembly.find('.address .city').value).to.equal('Frisby City') + }) + + it('a value does assert that there is only one matching element', async () => { + assembly.insertHtml(` +
+
+ +
+
+ + +
+
+ `) + + await assembly.assertRejection(browser.set({ + '.address': { + '.street': '7 Lola St', + '.city': 'Frisby City', + }, + }), "expected 1 element, found 2 (found: find('.address') [2], find('.street') [2])") + }) + it('deep fields disambiguate', async () => { assembly.insertHtml(`
@@ -180,8 +240,7 @@ describe('set', function () { await assembly.assertRejection( browser.set({ - '.address': [ - ] + '.address': [] }), 'expected 0 elements' ) From c4cdb58e194b792875e4b4596f2edf4550a2e802 Mon Sep 17 00:00:00 2001 From: artemave Date: Mon, 17 Jun 2019 12:22:29 +0200 Subject: [PATCH 021/127] remove unused code --- test/tsSpec.js | 5 ----- testA.ts | 7 ------- 2 files changed, 12 deletions(-) delete mode 100644 test/tsSpec.js delete mode 100644 testA.ts diff --git a/test/tsSpec.js b/test/tsSpec.js deleted file mode 100644 index 0b981d1..0000000 --- a/test/tsSpec.js +++ /dev/null @@ -1,5 +0,0 @@ -import x from '../../browser-monkey/testA.ts' - -it('x', async () => { - console.log('x', await x()) -}) diff --git a/testA.ts b/testA.ts deleted file mode 100644 index 65c4088..0000000 --- a/testA.ts +++ /dev/null @@ -1,7 +0,0 @@ -async function a () { - const x: number = 0 - await new Promise(r => setTimeout(r, 100)) - return x -} - -export default a From e9696f913801fa670034e64f5bbb0d977b4d5ca9 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 17 Jun 2019 12:43:30 +0200 Subject: [PATCH 022/127] all tests passing --- .babelrc | 1 + iframe.js | 99 ++++++++++++++++-------------- lib/mount.js | 4 +- package.json | 2 +- react.js | 2 +- test/assemblies/domAssemblySpec.js | 22 ++++--- test/mountSpec.js | 2 +- 7 files changed, 73 insertions(+), 59 deletions(-) diff --git a/.babelrc b/.babelrc index 0fa1885..464e1c7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,7 @@ { "presets": [ "@babel/preset-typescript", + "@babel/preset-react", [ "@babel/preset-env", { diff --git a/iframe.js b/iframe.js index a95976e..92adc81 100644 --- a/iframe.js +++ b/iframe.js @@ -1,52 +1,61 @@ var debug = require('debug')('browser-monkey:iframe') +var Mount = require('./lib/mount') var createMonkey = require('./create') var hobostyle = require('hobostyle') var createTestDiv = require('./lib/createTestDiv') var { iframeResizer } = require('iframe-resizer') -module.exports = function (url) { - debug('Mounting iframe: ' + url) - - var div = createTestDiv() - div.className = 'browser-monkey-browser' - - div.innerHTML = ` -
- - -
-
- - ` - - const addressBar = div.querySelector('.browser-monkey-address-bar') - const forwardButton = div.querySelector('.browser-monkey-forward-button') - const backButton = div.querySelector('.browser-monkey-back-button') - const addressBarText = div.querySelector('.browser-monkey-address-bar-text') - addressBarText.innerText = url - const iframe = div.querySelector('.browser-monkey-iframe') - - forwardButton.addEventListener('click', () => { - iframe.contentWindow.history.forward() - }) - - backButton.addEventListener('click', () => { - iframe.contentWindow.history.back() - }) - - iframe.src = url - iframe.addEventListener('load', () => { - console.log('loading') - addressBarText.innerText = iframe.contentWindow.location.href - const script = iframe.contentDocument.createElement('script') - script.src = 'file://' + __dirname + '/node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js' - iframe.contentDocument.head.appendChild(script) - console.log('done') - }) - - iframeResizer({ log: true, checkOrigin: false }, iframe) - - hobostyle.link(__dirname + '/iframe.css') - - return createMonkey(iframe) +module.exports = function (url, {resize = false} = {}) { + return new Mount(url, { + stopApp: function () {}, + startApp: function () { + debug('Mounting iframe: ' + url) + + var div = createTestDiv() + div.className = 'browser-monkey-browser' + + div.innerHTML = ` +
+ + +
+
+ + ` + + const addressBar = div.querySelector('.browser-monkey-address-bar') + const forwardButton = div.querySelector('.browser-monkey-forward-button') + const backButton = div.querySelector('.browser-monkey-back-button') + const addressBarText = div.querySelector('.browser-monkey-address-bar-text') + addressBarText.innerText = url + const iframe = div.querySelector('.browser-monkey-iframe') + + forwardButton.addEventListener('click', () => { + iframe.contentWindow.history.forward() + }) + + backButton.addEventListener('click', () => { + iframe.contentWindow.history.back() + }) + + iframe.src = url + iframe.addEventListener('load', () => { + addressBarText.innerText = iframe.contentWindow.location.href + + if (resize) { + const script = iframe.contentDocument.createElement('script') + script.src = 'file://' + __dirname + '/node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js' + iframe.contentDocument.head.appendChild(script) + } + }) + + if (resize) { + iframeResizer({ log: true, checkOrigin: false }, iframe) + } + + hobostyle.link(__dirname + '/iframe.css') + + return createMonkey(iframe) + } + }).start() } diff --git a/lib/mount.js b/lib/mount.js index 020ede8..cdfb840 100644 --- a/lib/mount.js +++ b/lib/mount.js @@ -10,10 +10,12 @@ function Mount (app, options) { Mount.prototype.start = function () { var monkey = this.startApp() - return monkey.options({ + monkey.options({ app: this.app, mount: this }) + + return monkey } Mount.prototype.stop = function () { diff --git a/package.json b/package.json index 7344c1b..16955cb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "yarn test-browser-compatibility && yarn test-electron-mocha && yarn karma && eslint .", "karma": "karma start --single-run", "test-electron-mocha": "yarn electron-mocha test/*Spec.js test/**/*Spec.js", - "electron-mocha": "electron-mocha --disable-site-isolation-trials -r test/register --renderer", + "electron-mocha": "electron-mocha --disable-site-isolation-trials --full-trace -r test/register --renderer", "test-browser-compatibility": "browserify ./index.js > /dev/null", "update-readme-example-links": "node ./docs/update-readme-example-links.js" }, diff --git a/react.js b/react.js index 56924eb..6b1d325 100644 --- a/react.js +++ b/react.js @@ -10,7 +10,7 @@ module.exports = function (App, props) { }, startApp: function () { var div = createTestDiv() - ReactDOM.render(React.createElement(this.app, props), div) + ReactDOM.render(React.createElement(App, props), div) return createMonkey(document.body) } diff --git a/test/assemblies/domAssemblySpec.js b/test/assemblies/domAssemblySpec.js index a23cfbc..6531f99 100644 --- a/test/assemblies/domAssemblySpec.js +++ b/test/assemblies/domAssemblySpec.js @@ -12,29 +12,31 @@ describe('DomAssembly', () => { it('can insert html', async () => { const element = assembly.insertHtml('
') - const found = await browser.find('.a').element() + const [found] = await browser.find('.a').expectOneElement() expect(found).to.equal(element) }) it('can eventually insert html before searching', async () => { + const promise = browser.find('.a').expectOneElement().then() const element = assembly.eventuallyInsertHtml('
') - const found = await browser.find('.a').element() + const [found] = await promise expect(found).to.equal(await element) }) it('can eventually insert html after searching', async () => { - const found = browser.find('.a').element() + const found = browser.find('.a').expectOneElement().then() const element = assembly.eventuallyInsertHtml('
') - expect(await found).to.equal(await element) + expect((await found)[0]).to.equal(await element) }) it('can eventually insert html before searching twice', async () => { - const element = assembly.eventuallyInsertHtml('
') - const found = await Promise.all([ - browser.find('.a').element(), - browser.find('.a').element() + const promise = Promise.all([ + browser.find('.a').expectOneElement().then(), + browser.find('.a').expectOneElement().then() ]) - expect(found[0]).to.equal(await element) - expect(found[1]).to.equal(await element) + const element = assembly.eventuallyInsertHtml('
') + const [[found1], [found2]] = await promise + expect(found1).to.equal(await element) + expect(found2).to.equal(await element) }) }) diff --git a/test/mountSpec.js b/test/mountSpec.js index 81ae997..f4405ab 100644 --- a/test/mountSpec.js +++ b/test/mountSpec.js @@ -8,7 +8,7 @@ describeAssemblies([DomAssembly], (Assembly) => { if (Assembly.hasDom()) { testMount('angular', require('./app/angular'), require('../angular')) testMount('hyperdom', new (require('./app/hyperdom'))(), require('../hyperdom')) - testMount('react', new (require('./app/react'))(), require('../react')) + testMount('react', require('./app/react'), require('../react')) testMount('iframe', assembly.localUrl(pathUtils.join(__dirname, 'iframe-mount-test.html')), require('../iframe')) } }) From ef951c23425c28fbf312f3da9031fb1ee4d01da9 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 17 Jun 2019 13:15:48 +0200 Subject: [PATCH 023/127] fixed react onChange event for inputs --- lib/elementEnterText.js | 15 +++++++++++++-- test/app/angular.js | 1 + test/app/hyperdom.jsx | 1 + test/app/react.jsx | 1 + test/iframe-mount-test.html | 13 ++++++++----- test/mountSpec.js | 19 ++++++++++++------- 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/lib/elementEnterText.js b/lib/elementEnterText.js index 46fc62b..22472e2 100644 --- a/lib/elementEnterText.js +++ b/lib/elementEnterText.js @@ -6,7 +6,12 @@ module.exports = function elementEnterText (element, text) { if (element.value !== '') { elementTriggerEvent(element, 'keydown') elementTriggerEvent(element, 'keypress') - element.value = '' + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value').set + if (nativeInputValueSetter) { + nativeInputValueSetter.call(element, '') + } else { + element.value = '' + } elementTriggerEvent(element, 'input') elementTriggerEvent(element, 'keyup') } @@ -14,7 +19,13 @@ module.exports = function elementEnterText (element, text) { text.split('').forEach(function (key, index) { elementTriggerEvent(element, 'keydown', key) elementTriggerEvent(element, 'keypress', key) - element.value = text.slice(0, index + 1) + const value = text.slice(0, index + 1) + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value').set + if (nativeInputValueSetter) { + nativeInputValueSetter.call(element, value) + } else { + element.value = value + } elementTriggerEvent(element, 'input') elementTriggerEvent(element, 'keyup', key) }) diff --git a/test/app/angular.js b/test/app/angular.js index db5aa83..731c9c8 100644 --- a/test/app/angular.js +++ b/test/app/angular.js @@ -8,6 +8,7 @@ angular controller: 'FrameworksController', template: `
+
{{message}}
` } diff --git a/test/app/hyperdom.jsx b/test/app/hyperdom.jsx index 56efbe2..134bbc4 100644 --- a/test/app/hyperdom.jsx +++ b/test/app/hyperdom.jsx @@ -13,6 +13,7 @@ module.exports = class WebApp { render () { return
+
{this.message}
} diff --git a/test/app/react.jsx b/test/app/react.jsx index abb1a85..01bcd4c 100644 --- a/test/app/react.jsx +++ b/test/app/react.jsx @@ -15,6 +15,7 @@ class WebApp extends React.Component { render () { return
+ this.setState({message: e.target.value})}/>
{this.state.message}
} diff --git a/test/iframe-mount-test.html b/test/iframe-mount-test.html index d63ddd6..671a86a 100644 --- a/test/iframe-mount-test.html +++ b/test/iframe-mount-test.html @@ -8,12 +8,15 @@
default
+ diff --git a/test/mountSpec.js b/test/mountSpec.js index f4405ab..20235f8 100644 --- a/test/mountSpec.js +++ b/test/mountSpec.js @@ -15,24 +15,29 @@ describeAssemblies([DomAssembly], (Assembly) => { function testMount (appType, app, monkeyBuilder) { describe(`mount ${appType}`, () => { - var monkey + var page beforeEach(() => { - monkey = monkeyBuilder(app) + page = monkeyBuilder(app) }) - afterEach(() => monkey.options().mount.stop()) + afterEach(() => page.options().mount.stop()) it('loads some data', () => { - return monkey.find('.message').shouldHave({ text: 'default' }).then(() => { - return monkey.find('button').click() + return page.find('.message').shouldHave({ text: 'default' }).then(() => { + return page.find('button').click() }).then(() => { - return monkey.find('.message').shouldHave({ text: 'hello browser-monkey' }) + return page.find('.message').shouldHave({ text: 'hello browser-monkey' }) }) }) + it('can enter form fields', async () => { + await page.set({'input': 'hi'}) + await page.assert({'.message': 'hi'}) + }) + it('exposes the app', () => { - expect(monkey.options().app).to.equal(app) + expect(page.options().app).to.equal(app) }) }) } From fe03cfedb9ad9776ee310d8ee1a3445ae1a6c1f1 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 17 Jun 2019 15:17:27 +0200 Subject: [PATCH 024/127] typescript compilation on git install --- lib/actions.js | 13 ++++++- lib/finders.js | 24 ++++++++++++ lib/query.ts | 99 +++++++++++++++++--------------------------------- package.json | 5 ++- tsconfig.json | 5 ++- 5 files changed, 75 insertions(+), 71 deletions(-) diff --git a/lib/actions.js b/lib/actions.js index c462199..61c556d 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -86,8 +86,19 @@ module.exports = { }) }, + click: function (name) { + const query = name === undefined + ? this + : this.button(name) + + return query.enabled().expectOneElement().action(function ([element]) { + debug('click', element) + elementClick(element) + }) + }, + submit: function () { - return this.expectOneElement().then(function ([element]) { + return this.expectOneElement().action(function ([element]) { debug('submit', element) elementSubmit(element) }) diff --git a/lib/finders.js b/lib/finders.js index 97e6852..543a5a6 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -22,6 +22,30 @@ module.exports = { } }, + find: function (selector, options) { + // name(value) + const match = /^\s*(.*?)\s*(\((.*)\)\s*)?$/.exec(selector) + + if (match) { + const [, name,, value] = match + const finder = this._options.definitions.fields[name] + + if (finder) { + if (options) { + throw new Error('options not used here') + } + + if (value !== undefined) { + return finder(this, value.trim()) + } else { + return finder(this) + } + } + } + + return this.css(selector, options) + }, + has: function (options) { return this.transform(function (elements) { expectElements(elements) diff --git a/lib/query.ts b/lib/query.ts index 6e175f9..a64cf44 100644 --- a/lib/query.ts +++ b/lib/query.ts @@ -19,6 +19,14 @@ var elementSubmit = require('./elementSubmit') type Transform = (elements: any, executedTransforms: ExecutedTransform[]) => any type Action = (elements: any, executedTransforms: ExecutedTransform[]) => void +interface Definitions { + fieldTypes: { + value: (query: Query) => any + settter?: (query: Query, value: any) => () => void + }[] + button: ((monkey: any, name: any) => any)[] + fields: {} +} class Query { private _transforms: Transform[] @@ -26,14 +34,7 @@ class Query { visibleOnly: boolean timeout: number interval: number - definitions: { - fieldTypes: { - value: (query: Query) => any - settter?: (query: Query, value: any) => () => void - }[] - button: ((monkey: any, name: any) => any)[] - fields: {} - } + definitions: Definitions } private _input: any @@ -205,49 +206,7 @@ class Query { return transformed } - public find (selector: string, options = undefined): this { - // name(value) - const match = /^\s*(.*?)\s*(\((.*)\)\s*)?$/.exec(selector) - - if (match) { - const [, name,, value] = match - const finder = this._options.definitions.fields[name] - - if (finder) { - if (options) { - throw new Error('options not used here') - } - - if (value !== undefined) { - return finder(this, value.trim()) - } else { - return finder(this) - } - } - } - - return this.css(selector, options) - } - - public submit (): void { - return this.expectOneElement().action(function ([element]) { - debug('enter', element) - elementSubmit(element) - }) - } - - public click (name?: string): void { - const query = name === undefined - ? this - : this.button(name) - - return query.enabled().expectOneElement().action(function ([element]) { - debug('click', element) - elementClick(element) - }) - } - - public options (options): Options { + public options (options): any { if (options) { extend(this._options, options) } else { @@ -294,7 +253,7 @@ class Query { public clone (modifier?: (clone: this) => void): this { var clone = new (this.constructor as any)() - copySelectorFields(this, clone) + clone.copyQueryFields(this) if (modifier) { modifier(clone) } @@ -308,8 +267,8 @@ class Query { public component (methods): this { var self = this - function Component () { - copySelectorFields(self, this) + function Component (): void { + this.copyQueryFields(self) } Component.prototype = new (this.constructor as any)() @@ -325,27 +284,35 @@ class Query { return new Component() } -} -function copySelectorFields (from: Query, to: Query): void { - to._transforms = from._transforms.slice() - to._input = from._input + private copyQueryFields (from: Query): void { + this._transforms = from._transforms.slice() + this._input = from._input + + this._hasExpectation = from._hasExpectation - to._hasExpectation = from._hasExpectation + this._options = extend({}, from._options) + this._options.definitions = cloneDefinitions(from._options.definitions) + } +} - to._options = extend({}, from._options) - to._options.definitions = {} +function cloneDefinitions(definitions: Definitions): Definitions { + const result = {} - Object.keys(from._options.definitions).forEach(function (key) { - if (from._options.definitions[key] instanceof Array) { - to._options.definitions[key] = from._options.definitions[key].slice() + Object.keys(definitions).forEach(function (key) { + if (definitions[key] instanceof Array) { + result[key] = definitions[key].slice() } else { - to._options.definitions[key] = Object.assign({}, from._options.definitions[key]) + result[key] = Object.assign({}, definitions[key]) } }) + + return result as Definitions } -function retryFromOptions (options) { +type Retry = (fn: () => any) => Promise + +function retryFromOptions (options: {retry?: Retry, timeout?: number, interval?: number}): Retry { if (options && options.retry) { return options.retry } else if (options && (options.timeout || options.interval)) { diff --git a/package.json b/package.json index 16955cb..cb68fe7 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,15 @@ "name": "browser-monkey", "version": "2.7.2", "description": "reliable dom testing", - "main": "index.js", + "main": "dist/index.js", "scripts": { "test": "yarn test-browser-compatibility && yarn test-electron-mocha && yarn karma && eslint .", "karma": "karma start --single-run", "test-electron-mocha": "yarn electron-mocha test/*Spec.js test/**/*Spec.js", "electron-mocha": "electron-mocha --disable-site-isolation-trials --full-trace -r test/register --renderer", + "update-readme-example-links": "node ./docs/update-readme-example-links.js", "test-browser-compatibility": "browserify ./index.js > /dev/null", - "update-readme-example-links": "node ./docs/update-readme-example-links.js" + "prepare": "tcs" }, "author": "Tim Macfarlane ", "license": "MIT", diff --git a/tsconfig.json b/tsconfig.json index 0e1fd28..c3529c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,12 @@ { "compilerOptions": { - "lib": ["es2015"], + "lib": ["es2015", "dom"], "noImplicitAny": false, "module": "commonjs", "target": "ES5", "outDir": "dist", - "rootDir": "." + "rootDir": ".", + "allowJs": true }, "exclude": [ "node_modules", From 790702dcbec2a40e6068fafdc47fd6d43ac1682b Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Sat, 30 Nov 2019 22:09:21 +0100 Subject: [PATCH 025/127] define objects --- lib/set.js | 14 ++++++-- package.json | 2 +- test/assertSpec.js | 83 ++++++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 8 ++--- 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/lib/set.js b/lib/set.js index b03b557..34d776a 100644 --- a/lib/set.js +++ b/lib/set.js @@ -169,8 +169,16 @@ module.exports = { }, 'index ' + index) }, - define: function (name, fieldFinder) { - this._options.definitions.fields[name] = fieldFinder + define: function (name, finder) { + if (typeof finder === 'function') { + this._options.definitions.fields[name] = finder + } else if (typeof finder === 'string') { + this._options.definitions.fields[name] = q => q.find(finder) + } else if (name.constructor === Object && finder === undefined) { + Object.entries(name).forEach(([name, finder]) => this.define(name, finder)) + } + + return this }, setter: function (model) { @@ -246,7 +254,7 @@ module.exports = { } return () => { debug('set', element, value) - elementEnterText(element, value) + elementEnterText(element, value, {incremental: false}) } }) }, diff --git a/package.json b/package.json index cb68fe7..5703588 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "react-dom": "^16.7.0", "server-destroy": "^1.0.1", "ts-node": "^8.1.0", - "typescript": "^3.4.4", + "typescript": "^3.5.2", "vdom-query": "https://github.com/featurist/vdom-query", "virtual-dom": "^2.1.1", "watchify": "^3.11.0" diff --git a/test/assertSpec.js b/test/assertSpec.js index 7431e0e..8eb88c5 100644 --- a/test/assertSpec.js +++ b/test/assertSpec.js @@ -329,5 +329,88 @@ describe('assert', function () { }) }) }) + + describe('define', () => { + it('can define a field using a finder', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + browser.define('header', q => q.find('h1')) + + await browser.assert({ + header: 'Title', + '.content': 'The Content' + }) + }) + + it('can define a field using css', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + browser.define('header', 'h1') + + await browser.assert({ + header: 'Title', + '.content': 'The Content' + }) + }) + + it('can define a several fields using an object', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + browser.define({ + header: 'h1', + content: q => q.find('.content'), + }) + + await browser.assert({ + header: 'Title', + content: 'The Content' + }) + }) + + it('can define a several hierarchical fields using finders', async () => { + assembly.insertHtml(` +
+

Title

+
+
title
+
+ body +
+
+
+ `) + + browser.define({ + title: 'h1', + content: q => q.find('.content').define({ + title: '.title', + body: '.body' + }), + }) + + await browser.assert({ + title: 'Title', + content: { + title: 'title', + body: 'body' + } + }) + }) + }) }) }) diff --git a/yarn.lock b/yarn.lock index 2b1f6be..1c62ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7653,10 +7653,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.4.4: - version "3.4.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.4.tgz#aac4a08abecab8091a75f10842ffa0631818f785" - integrity sha512-xt5RsIRCEaf6+j9AyOBgvVuAec0i92rgCaS3S+UVf5Z/vF2Hvtsw08wtUTJqp4djwznoAgjSxeCcU4r+CcDBJA== +typescript@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.2.tgz#a09e1dc69bc9551cadf17dba10ee42cf55e5d56c" + integrity sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA== ultron@~1.1.0: version "1.1.1" From 66a0de7857ee6ba51818ea6da75b7f350828406b Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Sat, 30 Nov 2019 22:09:42 +0100 Subject: [PATCH 026/127] non-incremental text input --- lib/elementEnterText.js | 52 ++++++++++++++++++++++------------------- test/actionsSpec.js | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/lib/elementEnterText.js b/lib/elementEnterText.js index 22472e2..6d5bacf 100644 --- a/lib/elementEnterText.js +++ b/lib/elementEnterText.js @@ -1,33 +1,37 @@ var elementTriggerEvent = require('./elementTriggerEvent') -module.exports = function elementEnterText (element, text) { +function setValue (element, value) { + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value').set + if (nativeInputValueSetter) { + nativeInputValueSetter.call(element, value) + } else { + element.value = value + } + elementTriggerEvent(element, 'input') +} + +function typeKey (element, value, key) { + elementTriggerEvent(element, 'keydown', key) + elementTriggerEvent(element, 'keypress', key) + setValue(element, value) + elementTriggerEvent(element, 'keyup', key) +} + +module.exports = function elementEnterText (element, text, {incremental = true} = {}) { element.focus() - if (element.value !== '') { - elementTriggerEvent(element, 'keydown') - elementTriggerEvent(element, 'keypress') - const nativeInputValueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value').set - if (nativeInputValueSetter) { - nativeInputValueSetter.call(element, '') - } else { - element.value = '' + if (incremental) { + if (element.value !== '') { + typeKey(element, '', undefined) } - elementTriggerEvent(element, 'input') - elementTriggerEvent(element, 'keyup') + + text.split('').forEach(function (key, index) { + const value = text.slice(0, index + 1) + typeKey(element, value, key) + }) + } else { + setValue(element, text) } - text.split('').forEach(function (key, index) { - elementTriggerEvent(element, 'keydown', key) - elementTriggerEvent(element, 'keypress', key) - const value = text.slice(0, index + 1) - const nativeInputValueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value').set - if (nativeInputValueSetter) { - nativeInputValueSetter.call(element, value) - } else { - element.value = value - } - elementTriggerEvent(element, 'input') - elementTriggerEvent(element, 'keyup', key) - }) elementTriggerEvent(element, 'change') } diff --git a/test/actionsSpec.js b/test/actionsSpec.js index 1c5ee1f..67d4189 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -251,7 +251,7 @@ describe('actions', function () { describe('submit', function () { it('should submit the form', function () { var submitted = false - var promise = browser.find('input').submit() + var promise = browser.find('input').submit().then() assembly.eventuallyInsertHtml( assembly.jQuery('').on('submit', function (ev) { From e2cd23df9e2dfc9b49e531d6312bfcf462735ad7 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Sun, 1 Dec 2019 23:04:28 +0100 Subject: [PATCH 027/127] refactored DOM manipulation into Dom class --- lib/Dom.ts | 164 +++++++++++++++++++++++++++++++++ lib/actions.js | 57 +++++------- lib/assertions.js | 52 +++++------ lib/elementChecked.js | 5 - lib/elementClick.js | 8 -- lib/elementEnterText.js | 37 -------- lib/elementMatches.js | 15 --- lib/elementQuerySelectorAll.js | 11 --- lib/elementSelect.js | 9 -- lib/elementSelector.js | 9 -- lib/elementSubmit.js | 14 --- lib/elementTriggerEvent.js | 57 ------------ lib/elementVisible.js | 7 -- lib/finders.js | 26 +++--- lib/query.ts | 7 +- lib/set.js | 21 ++--- test/assemblies/DomAssembly.js | 2 + test/domSpec.js | 64 ++++++------- 18 files changed, 265 insertions(+), 300 deletions(-) create mode 100644 lib/Dom.ts delete mode 100644 lib/elementChecked.js delete mode 100644 lib/elementClick.js delete mode 100644 lib/elementEnterText.js delete mode 100644 lib/elementMatches.js delete mode 100644 lib/elementQuerySelectorAll.js delete mode 100644 lib/elementSelect.js delete mode 100644 lib/elementSelector.js delete mode 100644 lib/elementSubmit.js delete mode 100644 lib/elementTriggerEvent.js delete mode 100644 lib/elementVisible.js diff --git a/lib/Dom.ts b/lib/Dom.ts new file mode 100644 index 0000000..71c73e1 --- /dev/null +++ b/lib/Dom.ts @@ -0,0 +1,164 @@ +var MouseEvent = require('./polyfills').MouseEvent +var KeyboardEvent = require('./polyfills').KeyboardEvent +const normaliseText = require('./normaliseText') + +export default class Dom { + enterText (element: HTMLInputElement, text: string, {incremental = true} = {}): void { + element.focus() + + if (incremental) { + if (element.value !== '') { + this.typeKey(element, '', undefined) + } + + text.split('').forEach((key, index) => { + const value = text.slice(0, index + 1) + this.typeKey(element, value, key) + }) + } else { + this.setInputValue(element, text) + } + + this.triggerEvent(element, 'change') + } + + elementInnerText (element: HTMLElement, selector): void { + return normaliseText(element.innerText) + } + + click (element: HTMLElement): void { + this.triggerEvent(element, 'mousedown') + this.triggerEvent(element, 'mouseup') + element.click() + element.focus() + } + + querySelectorAll (element: HTMLElement, selector: string, {visibleOnly = true} = {}): HTMLElement[] { + var children = Array.prototype.slice.call(element.querySelectorAll(selector)) + + children = visibleOnly + ? children.filter(c => this.elementVisible(c)) + : children + + return children + } + + elementVisible (element: HTMLElement): boolean { + if (element.tagName === 'OPTION' && element.parentNode) { + return this.elementVisible(element.parentNode as HTMLElement) + } else { + return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length) + } + } + + elementMatches (element: HTMLElement, selector: string): boolean { + return element.matches(selector) + } + + selectOption (selectElement: HTMLSelectElement, optionElement: HTMLOptionElement): void { + this.click(selectElement) + selectElement.selectedIndex = optionElement.index + this.triggerEvent(selectElement, 'input') + this.triggerEvent(selectElement, 'change') + } + + triggerEvent (element: HTMLElement, eventType, value?) { + var creator = eventCreatorsByType[eventType] + + if (!creator) { + throw new Error('event type ' + JSON.stringify(eventType) + ' not recognised') + } + + var event = creator(value) + + element.dispatchEvent(event) + } + + setInputValue (element: HTMLInputElement, value: string): void { + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value').set + if (nativeInputValueSetter) { + nativeInputValueSetter.call(element, value) + } else { + element.value = value + } + this.triggerEvent(element, 'input') + } + + typeKey (element: HTMLInputElement, value: string, key: string): void { + this.triggerEvent(element, 'keydown', key) + this.triggerEvent(element, 'keypress', key) + this.setInputValue(element, value) + this.triggerEvent(element, 'keyup', key) + } + + checked (element: HTMLInputElement): 'indeterminate'|boolean { + return element.indeterminate + ? 'indeterminate' + : element.checked + } + + selector (element: HTMLElement): string { + var id = element.id + var classes = element.className.split(/ +/g).filter(Boolean).sort() + var tag = element.tagName.toLowerCase() + + return (id || '') + + tag + + (classes.length ? '.' + classes.join('.') : '') + } + + submit (element: HTMLInputElement) { + element.focus() + this.triggerEvent(element, 'keydown') + this.triggerEvent(element, 'keypress') + var submitButton = element.form.querySelector('input[type="submit"], button[type="submit"]') + if (submitButton) { + (submitButton as HTMLElement).click() + } else { + this.triggerEvent(element.form, 'submit') + } + this.triggerEvent(element, 'keyup') + } +} + +function createMouseEvent (type) { + return new MouseEvent(type, { bubbles: true, cancelable: true }) +} + +function createEvent (type, params = {bubbles: true, cancelable: false}) { + // IE compatible old school way of creating events. + var event = document.createEvent('Event') + event.initEvent(type, params.bubbles, params.cancelable) + return event +} + +function createKeyboardEvent (type, key) { + return new KeyboardEvent(type, { bubbles: true, cancelable: true, key: key }) +} + +var eventCreatorsByType = { + mousedown: function () { + return createMouseEvent('mousedown') + }, + mouseup: function () { + return createMouseEvent('mouseup') + }, + change: function () { + return createEvent('change') + }, + input: function () { + return createEvent('input') + }, + keydown: function (key) { + return createKeyboardEvent('keydown', key) + }, + keyup: function (key) { + return createKeyboardEvent('keyup', key) + }, + keypress: function (key) { + return createKeyboardEvent('keypress', key) + }, + submit: function () { + return createEvent('submit', { bubbles: true, cancelable: true }) + } +} diff --git a/lib/actions.js b/lib/actions.js index 61c556d..4efb2d1 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -1,10 +1,4 @@ var debug = require('debug')('browser-monkey') -var elementSelect = require('./elementSelect') -var elementClick = require('./elementClick') -var elementSubmit = require('./elementSubmit') -var elementEnterText = require('./elementEnterText') -var elementChecked = require('./elementChecked') -const elementInnerText = require('./elementInnerText') var inputsSelector = require('./inputsSelector') function notSillyBlankIEObject (element) { @@ -37,52 +31,52 @@ module.exports = { ? [{ exactText: value }, { exactValue: value }] : [value] - return this.is('select').concat(criteria.map(function (c) { - return function (monkey) { + return this.is('select').concat(criteria.map(c => { + return monkey => { return monkey.find('option', c) } - })).expectOneElement().action(function ([optionElement]) { + })).expectOneElement().action(([optionElement]) => { var selectElement = optionElement.parentNode debug('select', selectElement) - elementSelect(selectElement, optionElement) + this._dom.selectOption(selectElement, optionElement) }) }, check: function (value) { - return this.is('input[type=checkbox]').expectOneElement().action(function ([checkbox]) { - if (elementChecked(checkbox) !== value) { - elementClick(checkbox) + return this.is('input[type=checkbox]').expectOneElement().action(([checkbox]) => { + if (this._dom.checked(checkbox) !== value) { + this._dom.click(checkbox) } }) }, checked: function () { - return this.is('input[type=checkbox]').expectOneElement().transform(function ([checkbox]) { - return elementChecked(checkbox) + return this.is('input[type=checkbox]').expectOneElement().transform(([checkbox]) => { + return this._dom.checked(checkbox) }) }, selectValue: function () { - return this.is('select').expectOneElement('expected to be select element').find('option').expectSomeElements('expected select options elements').transform(function (options) { + return this.is('select').expectOneElement('expected to be select element').find('option').expectSomeElements('expected select options elements').transform(options => { var selectElement = options[0].parentNode var selectedOption = options[selectElement.selectedIndex] - return elementInnerText(selectedOption) + return this._dom.elementInnerText(selectedOption) }) }, inputValue: function () { - return this.is(inputsSelector).expectOneElement().transform(function ([element]) { + return this.is(inputsSelector).expectOneElement().transform(([element]) => { return element.value }) }, typeIn: function (text) { - return this.is(inputsSelector).expectOneElement().action(function ([element]) { + return this.is(inputsSelector).expectOneElement().action(([element]) => { if (typeof text !== 'string') { throw new Error('expected string as argument to typeIn') } debug('typeIn', element, text) - elementEnterText(element, text) + this._dom.enterText(element, text) }) }, @@ -91,21 +85,21 @@ module.exports = { ? this : this.button(name) - return query.enabled().expectOneElement().action(function ([element]) { + return query.enabled().expectOneElement().action(([element]) => { debug('click', element) - elementClick(element) + this._dom.click(element) }) }, submit: function () { - return this.expectOneElement().action(function ([element]) { + return this.expectOneElement().action(([element]) => { debug('submit', element) - elementSubmit(element) + this._dom.submit(element) }) }, typeInHtml: function (html) { - return this.expectOneElement().then(function ([element]) { + return this.expectOneElement().then(([element]) => { debug('typeInHtml', element, html) element.focus() element.innerHTML = html @@ -114,24 +108,23 @@ module.exports = { fill: function (field) { var isArray = Object.prototype.toString.call(field) === '[object Array]' - var component = this - return new Promise(function (resolve, reject) { + return new Promise((resolve, reject) => { if (isArray) { - fillField(component, field) + fillField(this, field) .then(resolve) .catch(reject) } else { if (!field.name) { try { - field = inferField(component, field) + field = inferField(this, field) } catch (e) { reject(e) return } } - if (typeof component[field.name] === 'function') { - resolve(component[field.name]()[field.action](field.options)) + if (typeof this[field.name] === 'function') { + resolve(this[field.name]()[field.action](field.options)) } else { reject(new Error("No field '" + field.name + "' exists on this component")) } @@ -143,7 +136,7 @@ module.exports = { function fillField (component, fields) { var field = fields.shift() if (field) { - return component.fill(field).then(function () { + return component.fill(field).then(() => { return fillField(component, fields) }) } else { diff --git a/lib/assertions.js b/lib/assertions.js index 77c4fbb..18db821 100644 --- a/lib/assertions.js +++ b/lib/assertions.js @@ -1,22 +1,18 @@ -const elementInnerText = require('./elementInnerText') -const elementSelector = require('./elementSelector') -const elementChecked = require('./elementChecked') const inspect = require('object-inspect') const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default var expectDeepEqual = require('./expectDeepEqual') var expectEqual = require('./expectEqual') var object = require('lowscore/object') -const elementMatches = require('./elementMatches') module.exports = { shouldEqual: function (value) { - return this.expect(function (result) { + return this.expect(result => { expectEqual(result, value) }) }, shouldDeepEqual: function (value) { - return this.expect(function (result) { + return this.expect(result => { expectDeepEqual(result, value) }) }, @@ -32,7 +28,7 @@ module.exports = { shouldHave: function (options) { var self = this - return this.expectSomeElements().expect(function (elements) { + return this.expectSomeElements().expect(elements => { assertElementExpectations(self, elements, options) }, inspect(options)) }, @@ -57,7 +53,7 @@ module.exports = { shouldNotHave: function (options) { var self = this - return this.expectSomeElements().expect(function (elements) { + return this.expectSomeElements().expect(elements => { let thrownError try { assertElementExpectations(self, elements, options) @@ -78,7 +74,7 @@ function toArray (i) { function assertContainsValues (actual, expected) { var equal = actual.length === expected.length && - actual.every(function (a, index) { + actual.every((a, index) => { if (a === undefined) { return expected[index] === undefined } else if (expected[index] === '') { @@ -97,7 +93,7 @@ function assertContainsValues (actual, expected) { } function queryValue (query, elements) { - return elements.map(function (element) { + return elements.map(element => { return query.transform(() => [element]).value().result() }) } @@ -106,8 +102,8 @@ var elementExpectations = { text: function (query, elements, expected) { var expectedArray = toArray(expected) - var actual = elements.map(function (element) { - return elementInnerText(element) + var actual = elements.map(element => { + return query._dom.elementInnerText(element) }) assertContainsValues(actual, expectedArray) @@ -126,7 +122,7 @@ var elementExpectations = { var actual = queryValue(query, elements) - expectDeepEqual(actual, expectedArray, function (actual, expected) { + expectDeepEqual(actual, expectedArray, (actual, expected) => { return 'expected ' + actual + ' to have exact values ' + expected }) }, @@ -134,11 +130,11 @@ var elementExpectations = { exactText: function (query, elements, expected) { var expectedArray = toArray(expected) - var actual = elements.map(function (element) { - return elementInnerText(element) + var actual = elements.map(element => { + return query._dom.elementInnerText(element) }) - expectDeepEqual(actual, expectedArray, function (actual, expected) { + expectDeepEqual(actual, expectedArray, (actual, expected) => { return 'expected ' + actual + ' to have exact inner texts ' + expected }) }, @@ -146,11 +142,11 @@ var elementExpectations = { checked: function (query, elements, expected) { var expectedArray = toArray(expected) - var actual = elements.map(function (element) { - return elementChecked(element) + var actual = elements.map(element => { + return query._dom.checked(element) }) - expectDeepEqual(actual, expectedArray, function (actual, expected) { + expectDeepEqual(actual, expectedArray, (actual, expected) => { return 'expected checked properties ' + actual + ' to equal ' + expected }) }, @@ -159,13 +155,13 @@ var elementExpectations = { var expectedArray = toArray(expected) var equal = elements.length === expectedArray.length && - elements.every(function (element, index) { - return elementMatches(element, expectedArray[index]) + elements.every((element, index) => { + return query._dom.elementMatches(element, expectedArray[index]) }) if (!equal) { - var actual = elements.map(function (element) { - return elementSelector(element) + var actual = elements.map((element) => { + return query._dom.selector(element) }) var expectedMessage = inspect(expectedArray) @@ -176,7 +172,7 @@ var elementExpectations = { }, length: function (query, elements, expected) { - expectEqual(elements.length, expected, function (actual, expected) { + expectEqual(elements.length, expected, (actual, expected) => { return 'expected number of elements found ' + actual + ' to equal ' + expected }) }, @@ -184,17 +180,17 @@ var elementExpectations = { attributes: function (query, elements, expected) { var expectedArray = toArray(expected) - var actual = elements.map(function (element, index) { + var actual = elements.map((element, index) => { var expected = expectedArray[index] if (expected) { - return object(Object.keys(expected).map(function (name) { + return object(Object.keys(expected).map(name => { return [name, element.getAttribute(name)] })) } }) - expectDeepEqual(actual, expectedArray, function (actual, expected) { + expectDeepEqual(actual, expectedArray, (actual, expected) => { return 'expected ' + actual + ' to have attributes ' + expected }) } @@ -203,7 +199,7 @@ var elementExpectations = { function assertElementExpectations (query, elements, options) { const resolved = query.resolve() - Object.keys(options).forEach(function (key) { + Object.keys(options).forEach(key => { var expectation = elementExpectations[key] if (!expectation) { diff --git a/lib/elementChecked.js b/lib/elementChecked.js deleted file mode 100644 index 224192a..0000000 --- a/lib/elementChecked.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function (element) { - return element.indeterminate - ? 'indeterminate' - : element.checked -} diff --git a/lib/elementClick.js b/lib/elementClick.js deleted file mode 100644 index 1c34d13..0000000 --- a/lib/elementClick.js +++ /dev/null @@ -1,8 +0,0 @@ -var elementTriggerEvent = require('./elementTriggerEvent') - -module.exports = function elementClick (element) { - elementTriggerEvent(element, 'mousedown') - elementTriggerEvent(element, 'mouseup') - element.click() - element.focus() -} diff --git a/lib/elementEnterText.js b/lib/elementEnterText.js deleted file mode 100644 index 6d5bacf..0000000 --- a/lib/elementEnterText.js +++ /dev/null @@ -1,37 +0,0 @@ -var elementTriggerEvent = require('./elementTriggerEvent') - -function setValue (element, value) { - const nativeInputValueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value').set - if (nativeInputValueSetter) { - nativeInputValueSetter.call(element, value) - } else { - element.value = value - } - elementTriggerEvent(element, 'input') -} - -function typeKey (element, value, key) { - elementTriggerEvent(element, 'keydown', key) - elementTriggerEvent(element, 'keypress', key) - setValue(element, value) - elementTriggerEvent(element, 'keyup', key) -} - -module.exports = function elementEnterText (element, text, {incremental = true} = {}) { - element.focus() - - if (incremental) { - if (element.value !== '') { - typeKey(element, '', undefined) - } - - text.split('').forEach(function (key, index) { - const value = text.slice(0, index + 1) - typeKey(element, value, key) - }) - } else { - setValue(element, text) - } - - elementTriggerEvent(element, 'change') -} diff --git a/lib/elementMatches.js b/lib/elementMatches.js deleted file mode 100644 index 46bd6d9..0000000 --- a/lib/elementMatches.js +++ /dev/null @@ -1,15 +0,0 @@ -function matchesPolyfill (s) { - var matches = (this.document || this.ownerDocument).querySelectorAll(s) - - var i = matches.length - while (--i >= 0 && matches.item(i) !== this) {} - return i > -1 -} - -module.exports = function elementMatches (element, selector) { - if (element.matches) { - return element.matches(selector) - } else { - return matchesPolyfill.call(element, selector) - } -} diff --git a/lib/elementQuerySelectorAll.js b/lib/elementQuerySelectorAll.js deleted file mode 100644 index 259db1c..0000000 --- a/lib/elementQuerySelectorAll.js +++ /dev/null @@ -1,11 +0,0 @@ -const elementVisible = require('./elementVisible') - -module.exports = function elementQuerySelectorAll (element, selector, selectorOptions) { - var children = Array.prototype.slice.call(element.querySelectorAll(selector)) - - children = selectorOptions.visibleOnly - ? children.filter(elementVisible) - : children - - return children -} diff --git a/lib/elementSelect.js b/lib/elementSelect.js deleted file mode 100644 index 0591ae1..0000000 --- a/lib/elementSelect.js +++ /dev/null @@ -1,9 +0,0 @@ -var elementClick = require('./elementClick') -var elementTriggerEvent = require('./elementTriggerEvent') - -module.exports = function elementSelect (selectElement, optionElement) { - elementClick(selectElement) - selectElement.selectedIndex = optionElement.index - elementTriggerEvent(selectElement, 'input') - elementTriggerEvent(selectElement, 'change') -} diff --git a/lib/elementSelector.js b/lib/elementSelector.js deleted file mode 100644 index fe942f3..0000000 --- a/lib/elementSelector.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function elementSelector (element) { - var id = element.id - var classes = element.className.split(/ +/g).filter(Boolean).sort() - var tag = element.tagName.toLowerCase() - - return (id || '') + - tag + - (classes.length ? '.' + classes.join('.') : '') -} diff --git a/lib/elementSubmit.js b/lib/elementSubmit.js deleted file mode 100644 index de460f1..0000000 --- a/lib/elementSubmit.js +++ /dev/null @@ -1,14 +0,0 @@ -var elementTriggerEvent = require('./elementTriggerEvent') - -module.exports = function elementSubmit (element) { - element.focus() - elementTriggerEvent(element, 'keydown') - elementTriggerEvent(element, 'keypress') - var submitButton = element.form.querySelector('input[type="submit"], button[type="submit"]') - if (submitButton) { - submitButton.click() - } else { - elementTriggerEvent(element.form, 'submit') - } - elementTriggerEvent(element, 'keyup') -} diff --git a/lib/elementTriggerEvent.js b/lib/elementTriggerEvent.js deleted file mode 100644 index dc91605..0000000 --- a/lib/elementTriggerEvent.js +++ /dev/null @@ -1,57 +0,0 @@ -var MouseEvent = require('./polyfills').MouseEvent -var KeyboardEvent = require('./polyfills').KeyboardEvent - -function createMouseEvent (type) { - return new MouseEvent(type, { bubbles: true, cancelable: true }) -} - -function createEvent (type, params) { - params = params || { bubbles: true, cancelable: false } - // IE compatible old school way of creating events. - var event = document.createEvent('Event') - event.initEvent(type, params.bubbles, params.cancelable) - return event -} - -function createKeyboardEvent (type, key) { - return new KeyboardEvent(type, { bubbles: true, cancelable: true, key: key }) -} - -var eventCreatorsByType = { - mousedown: function () { - return createMouseEvent('mousedown') - }, - mouseup: function () { - return createMouseEvent('mouseup') - }, - change: function () { - return createEvent('change') - }, - input: function () { - return createEvent('input') - }, - keydown: function (key) { - return createKeyboardEvent('keydown', key) - }, - keyup: function (key) { - return createKeyboardEvent('keyup', key) - }, - keypress: function (key) { - return createKeyboardEvent('keypress', key) - }, - submit: function () { - return createEvent('submit', { bubbles: true, cancelable: true }) - } -} - -module.exports = function (element, eventType, value) { - var creator = eventCreatorsByType[eventType] - - if (!creator) { - throw new Error('event type ' + JSON.stringify(eventType) + ' not recognised') - } - - var event = creator(value) - - element.dispatchEvent(event) -} diff --git a/lib/elementVisible.js b/lib/elementVisible.js deleted file mode 100644 index a1e8517..0000000 --- a/lib/elementVisible.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = function elementVisible (element) { - if (element.tagName === 'OPTION' && element.parentNode) { - return elementVisible(element.parentNode) - } else { - return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length) - } -} diff --git a/lib/finders.js b/lib/finders.js index 543a5a6..c117037 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -1,5 +1,3 @@ -var elementMatches = require('./elementMatches') -var elementQuerySelectorAll = require('./elementQuerySelectorAll') var elementHas = require('./elementHas') var flatten = require('lowscore/flatten') var BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default @@ -11,7 +9,7 @@ module.exports = { const findElements = this.transform(elements => { expectElements(elements) return new ExecutedSimpleTransform(flatten(elements.map(element => { - return elementQuerySelectorAll(element, selector, this._options) + return this._dom.querySelectorAll(element, selector, this._options) })), 'find(' + inspect(selector) + ')') }) @@ -47,27 +45,25 @@ module.exports = { }, has: function (options) { - return this.transform(function (elements) { + return this.transform(elements => { expectElements(elements) - return new ExecutedSimpleTransform(elements.filter(function (element, index) { + return new ExecutedSimpleTransform(elements.filter((element, index) => { return elementHas(element, index, options) }), 'has: ' + inspect(options)) }) }, is: function (selector) { - return this.filter(function (element) { - return elementMatches(element, selector) + return this.filter(element => { + return this._dom.elementMatches(element, selector) }, 'is: ' + selector) }, containing: function (selector, options) { - var self = this - - return this.filter(function (element) { - var foundElements = elementQuerySelectorAll(element, selector, self._options) + return this.filter((element) => { + var foundElements = this._dom.querySelectorAll(element, selector, this._options) var filteredElements = options - ? foundElements.filter(function (element, index) { + ? foundElements.filter((element, index) => { return elementHas(element, index, options) }) : foundElements @@ -77,7 +73,7 @@ module.exports = { }, iframeContent: function () { - return this.transform(function (elements) { + return this.transform(elements => { return elements.map(element => { if (isIframe(element)) { if (element.contentDocument && element.contentDocument.readyState === 'complete') { @@ -106,7 +102,7 @@ module.exports = { }, enabled: function () { - return this.filter(function (element) { + return this.filter(element => { var tagName = element.tagName return !((tagName === 'BUTTON' || tagName === 'INPUT') && element.disabled) }, 'enabled') @@ -145,7 +141,7 @@ module.exports = { function isArrayOfHTMLElements (elements) { return elements instanceof Array && - elements.every(function (element) { + elements.every(element => { return isHTMLElement(element) }) } diff --git a/lib/query.ts b/lib/query.ts index a64cf44..d949995 100644 --- a/lib/query.ts +++ b/lib/query.ts @@ -4,18 +4,15 @@ import { ExecutedSimpleTransform } from './ExecutedSimpleTransform' import { ExecutedConcatTransform } from './ExecutedConcatTransform' import { ExecutedFirstOfTransform } from './ExecutedFirstOfTransform' import { ExecutedTransformError } from './ExecutedTransformError' +import Dom from './Dom' import BrowserMonkeyAssertionError from './BrowserMonkeyAssertionError' import toExecutedTransform from './toExecutedTransform' var extend = require('lowscore/extend') var retry = require('trytryagain') -var elementInnerText = require('./elementInnerText') var inspect = require('object-inspect') var uniq = require('lowscore/uniq') var debug = require('debug')('browser-monkey') var inputsSelector = require('./inputsSelector') -const elementEnterText = require('./elementEnterText') -var elementClick = require('./elementClick') -var elementSubmit = require('./elementSubmit') type Transform = (elements: any, executedTransforms: ExecutedTransform[]) => any type Action = (elements: any, executedTransforms: ExecutedTransform[]) => void @@ -41,6 +38,7 @@ class Query { private _actionExecuted: any private _action: Action private _hasExpectation: boolean + private _dom: Dom public constructor () { this._hasExpectation = false @@ -66,6 +64,7 @@ class Query { fields: {}, } } + this._dom = new Dom() this._input = undefined } diff --git a/lib/set.js b/lib/set.js index 34d776a..b870423 100644 --- a/lib/set.js +++ b/lib/set.js @@ -1,10 +1,5 @@ var inputsSelector = require('./inputsSelector') var debug = require('debug')('browser-monkey') -var elementClick = require('./elementClick') -var elementChecked = require('./elementChecked') -var elementEnterText = require('./elementEnterText') -var elementInnerText = require('./elementInnerText') -var elementSelect = require('./elementSelect') const inspect = require('object-inspect') const object = require('lowscore/object') const range = require('lowscore/range') @@ -254,7 +249,7 @@ module.exports = { } return () => { debug('set', element, value) - elementEnterText(element, value, {incremental: false}) + query._dom.enterText(element, value, {incremental: false}) } }) }, @@ -270,13 +265,13 @@ module.exports = { .is('select') .expectOneElement('expected to be select element') .css('option') - .filter(o => o.value === value && elementInnerText(o) === value, `option with text or value ${JSON.stringify(value)}`) + .filter(o => o.value === value && query._dom.elementInnerText(o) === value, `option with text or value ${JSON.stringify(value)}`) .expectOneElement(`expected one option element with text or value ${JSON.stringify(value)}`) .transform(function ([option]) { return () => { const selectElement = option.parentNode debug('select', selectElement) - elementSelect(selectElement, option) + query._dom.selectOption(selectElement, option) } }) }, @@ -286,7 +281,7 @@ module.exports = { .expectOneElement('expected to be select element') .transform(([select]) => { const selectedOption = select.options[select.selectedIndex] - return selectedOption && elementInnerText(selectedOption) + return selectedOption && query._dom.elementInnerText(selectedOption) }) } }) @@ -300,9 +295,9 @@ module.exports = { throw new Error('expected boolean as argument to set checkbox') } return () => { - if (elementChecked(checkbox) !== value) { + if (query._dom.checked(checkbox) !== value) { debug('checkbox', checkbox, value) - elementClick(checkbox) + query._dom.click(checkbox) } } }) @@ -312,7 +307,7 @@ module.exports = { .is('input[type=checkbox]') .expectOneElement() .transform(([checkbox]) => { - return elementChecked(checkbox) + return query._dom.checked(checkbox) }) } }) @@ -320,7 +315,7 @@ module.exports = { this.defineFieldType({ value: (query) => { return query.expectOneElement().transform(([element]) => { - return elementInnerText(element) + return query._dom.elementInnerText(element) }) } }) diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.js index 21a9570..b226ce7 100644 --- a/test/assemblies/DomAssembly.js +++ b/test/assemblies/DomAssembly.js @@ -7,6 +7,7 @@ const pathUtils = require('path') const trytryagain = require('trytryagain') const { expect } = require('chai') const inspect = require('object-inspect') +const Dom = require('../../lib/Dom').default module.exports = class DomAssembly { constructor () { @@ -15,6 +16,7 @@ module.exports = class DomAssembly { this.retries = [] this.delayedActions = [] this.queuedRetries = 0 + this.dom = new Dom() } div () { diff --git a/test/domSpec.js b/test/domSpec.js index a246470..afcb2a9 100644 --- a/test/domSpec.js +++ b/test/domSpec.js @@ -1,13 +1,5 @@ var describeAssemblies = require('./describeAssemblies') const DomAssembly = require('./assemblies/DomAssembly') -const elementVisible = require('../lib/elementVisible') -const elementClick = require('../lib/elementClick') -const elementSelect = require('../lib/elementSelect') -const elementMatches = require('../lib/elementMatches') -const elementSubmit = require('../lib/elementSubmit') -const elementTriggerEvent = require('../lib/elementTriggerEvent') -const elementEnterText = require('../lib/elementEnterText') -const elementInnerText = require('../lib/elementInnerText') const { expect } = require('chai') describe('dom', () => { @@ -44,7 +36,7 @@ describe('dom', () => { button.addEventListener(type, () => buttonEvents.push(type)) }) - elementSubmit(input) + assembly.dom.submit(input) expect(formEvents).to.eql([ 'keydown', @@ -79,7 +71,7 @@ describe('dom', () => { input.addEventListener(type, () => inputEvents.push(type)) }) - elementSubmit(input) + assembly.dom.submit(input) expect(formEvents).to.eql([ 'keydown', @@ -110,7 +102,7 @@ describe('dom', () => {
`) - expect(elementInnerText(element)).to.equal('first line\nsecond line') + expect(assembly.dom.elementInnerText(element)).to.equal('first line\nsecond line') }) it('normalises innerText removing extra spaces between words', () => { @@ -120,7 +112,7 @@ describe('dom', () => {
`) - expect(elementInnerText(element)).to.equal('two words') + expect(assembly.dom.elementInnerText(element)).to.equal('two words') }) }) @@ -133,7 +125,7 @@ describe('dom', () => { element.addEventListener(type, () => events.push(type)) }) - elementClick(element) + assembly.dom.click(element) expect(events).to.eql([ 'mousedown', @@ -153,7 +145,7 @@ describe('dom', () => { element.addEventListener(type, preventDefaultSubmit(() => events.push(type))) }) - elementClick(submit) + assembly.dom.click(submit) expect(events).to.eql([ 'submit' @@ -168,7 +160,7 @@ describe('dom', () => { checkbox.addEventListener(type, () => events.push(type)) }) - elementClick(checkbox) + assembly.dom.click(checkbox) expect(events).to.eql([ 'mousedown', @@ -177,7 +169,7 @@ describe('dom', () => { ]) expect(checkbox.checked).to.equal(true) - elementClick(checkbox) + assembly.dom.click(checkbox) expect(checkbox.checked).to.equal(false) }) @@ -191,7 +183,7 @@ describe('dom', () => { checkbox.addEventListener(type, () => events.push('checkbox:' + type)) }) - elementClick(checkbox) + assembly.dom.click(checkbox) expect(events).to.eql([ 'checkbox:mousedown', @@ -203,7 +195,7 @@ describe('dom', () => { ]) expect(checkbox.checked).to.equal(true) - elementClick(checkbox) + assembly.dom.click(checkbox) expect(checkbox.checked).to.equal(false) }) }) @@ -225,7 +217,7 @@ describe('dom', () => { selectElement.addEventListener(type, () => selectEvents.push(type)) }) - elementSelect(selectElement, one) + assembly.dom.selectOption(selectElement, one) expect(selectEvents).to.eql([ 'mousedown', @@ -242,7 +234,7 @@ describe('dom', () => { expect(two.selected).to.equal(false) assembly.assertElementIsFocussed(selectElement) - elementSelect(selectElement, two) + assembly.dom.selectOption(selectElement, two) expect(selectElement.selectedIndex).to.equal(1) expect(selectElement.value).to.equal('two') @@ -261,7 +253,7 @@ describe('dom', () => { input.addEventListener(type, e => events.push(type + ':' + e.target.value)) }) - elementEnterText(input, 'abc') + assembly.dom.enterText(input, 'abc') expect(events).to.eql([ 'keydown:', @@ -292,7 +284,7 @@ describe('dom', () => { input.addEventListener(type, e => events.push(type + ':' + e.target.value)) }) - elementEnterText(input, 'abc') + assembly.dom.enterText(input, 'abc') expect(events).to.eql([ 'keydown:existing', @@ -327,7 +319,7 @@ describe('dom', () => { input.addEventListener(type, e => events.push(type + ':' + e.target.value)) }) - elementEnterText(input, '') + assembly.dom.enterText(input, '') expect(events).to.eql([ 'keydown:existing', @@ -357,7 +349,7 @@ describe('dom', () => { it('throws error if event type is not known', () => { var element = assembly.insertHtml('
') - expect(() => elementTriggerEvent(element, 'asdf')).to.throw('event type "asdf" not recognised') + expect(() => assembly.dom.triggerEvent(element, 'asdf')).to.throw('event type "asdf" not recognised') }) function watchEvents (element) { @@ -415,7 +407,7 @@ describe('dom', () => { }) }) - elementTriggerEvent(element, eventName) + assembly.dom.triggerEvent(element, eventName) return eventReceived }) @@ -431,7 +423,7 @@ describe('dom', () => { var child = assembly.find('#child') - elementTriggerEvent(child, eventName) + assembly.dom.triggerEvent(child, eventName) return eventReceived }) @@ -443,19 +435,19 @@ describe('dom', () => { it('when element is visible it returns true', () => { var element = assembly.insertHtml('
hi
') - expect(elementVisible(element)).to.equal(true) + expect(assembly.dom.elementVisible(element)).to.equal(true) }) it('when element is visible but empty returns true', () => { var element = assembly.insertHtml('
') - expect(elementVisible(element)).to.equal(true) + expect(assembly.dom.elementVisible(element)).to.equal(true) }) it('when element is display:none it returns false', () => { var element = assembly.insertHtml('
hi
') - expect(elementVisible(element)).to.equal(false) + expect(assembly.dom.elementVisible(element)).to.equal(false) }) it('when element is visible but parent is not', () => { @@ -463,7 +455,7 @@ describe('dom', () => { var element = assembly.find('#child') - expect(elementVisible(element)).to.equal(false) + expect(assembly.dom.elementVisible(element)).to.equal(false) }) it('when element is option and select is visible, returns true', () => { @@ -471,7 +463,7 @@ describe('dom', () => { var element = assembly.find('#option') - expect(elementVisible(element)).to.equal(true) + expect(assembly.dom.elementVisible(element)).to.equal(true) }) it('when element is option and select is not visible, returns false', () => { @@ -479,7 +471,7 @@ describe('dom', () => { var element = assembly.find('#option') - expect(elementVisible(element)).to.equal(false) + expect(assembly.dom.elementVisible(element)).to.equal(false) }) }) @@ -487,24 +479,24 @@ describe('dom', () => { describe('tagName', function () { it('returns true when tagName matches', () => { var element = assembly.insertHtml('
') - expect(elementMatches(element, 'div')).to.equal(true) + expect(assembly.dom.elementMatches(element, 'div')).to.equal(true) }) it('returns false when tagName doesnt match', () => { var element = assembly.insertHtml('
') - expect(elementMatches(element, 'select')).to.equal(false) + expect(assembly.dom.elementMatches(element, 'select')).to.equal(false) }) }) describe('class', function () { it('returns true when class matches', () => { var element = assembly.insertHtml('
') - expect(elementMatches(element, '.hobo')).to.equal(true) + expect(assembly.dom.elementMatches(element, '.hobo')).to.equal(true) }) it('returns false when class doesnt match', () => { var element = assembly.insertHtml('
') - expect(elementMatches(element, '.hobo')).to.equal(false) + expect(assembly.dom.elementMatches(element, '.hobo')).to.equal(false) }) }) }) From c76aa6c79bf9acb84aca20c72780a90be3927319 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 2 Dec 2019 10:41:24 +0100 Subject: [PATCH 028/127] add Query.detect() --- create.js | 2 +- lib/ExecutedTransformSequence.ts | 2 +- lib/{query.ts => Query.ts} | 57 ++++++++++++++++++++++++++++++++ test/querySpec.js | 40 ++++++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) rename lib/{query.ts => Query.ts} (85%) diff --git a/create.js b/create.js index 59f67b9..ec011cb 100644 --- a/create.js +++ b/create.js @@ -1,4 +1,4 @@ -var Query = require('./lib/query') +var Query = require('./lib/Query') var finders = require('./lib/finders') var actions = require('./lib/actions') var button = require('./lib/button') diff --git a/lib/ExecutedTransformSequence.ts b/lib/ExecutedTransformSequence.ts index 26bd095..50fa433 100644 --- a/lib/ExecutedTransformSequence.ts +++ b/lib/ExecutedTransformSequence.ts @@ -31,7 +31,7 @@ export class ExecutedTransformSequence extends ExecutedTransform { const transformsUpUntilAndIncludingFirstFailure = this.transforms.reduce(({ keepTaking, array }, b) => { if (keepTaking) { return { - keepTaking: !!b.value.length, + keepTaking: !!(b.value && b.value.length), array: array.concat([b]) } } else { diff --git a/lib/query.ts b/lib/Query.ts similarity index 85% rename from lib/query.ts rename to lib/Query.ts index d949995..c529bc9 100644 --- a/lib/query.ts +++ b/lib/Query.ts @@ -3,6 +3,7 @@ import { ExecutedTransformSequence } from './ExecutedTransformSequence' import { ExecutedSimpleTransform } from './ExecutedSimpleTransform' import { ExecutedConcatTransform } from './ExecutedConcatTransform' import { ExecutedFirstOfTransform } from './ExecutedFirstOfTransform' +import ExecutedDetectTransform from './ExecutedDetectTransform' import { ExecutedTransformError } from './ExecutedTransformError' import Dom from './Dom' import BrowserMonkeyAssertionError from './BrowserMonkeyAssertionError' @@ -205,6 +206,62 @@ class Query { return transformed } + public detect (queryCreators: {[key: string]: (q: Query) => Query}): this { + const transformed = this.transform((elements) => { + const resolved = this.resolve(elements) + + var entries = Object.keys(queryCreators).map(key => { + const queryCreator = queryCreators[key] + + try { + const q = runQueryCreator(queryCreator, resolved) + q.assertHasActionOrExpectation() + return { + key, + value: q.execute() + } + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + return { + key, + error: new ExecutedTransformError(e) + } + } else { + throw e + } + } + }) + + var firstSuccessIndex = entries.findIndex(v => { + return !v.error + }) + + const firstSuccess = entries[firstSuccessIndex] + + const transform = new ExecutedDetectTransform( + firstSuccess + ? { + key: firstSuccess.key, + value: firstSuccess.value.value, + } + : undefined, + entries.map(v => ({key: v.key, transform: v.error || v.value})) + ) + + if (firstSuccess) { + return transform + } else { + var error = new BrowserMonkeyAssertionError('all queries failed in detect') + error.addExecutedTransform(transform) + throw error + } + }) + + transformed._hasExpectation = true + + return transformed + } + public options (options): any { if (options) { extend(this._options, options) diff --git a/test/querySpec.js b/test/querySpec.js index 1bf7647..9e6677c 100644 --- a/test/querySpec.js +++ b/test/querySpec.js @@ -438,5 +438,45 @@ describe('query', () => { await expect(promise).to.reject.with.error(/no expectations or actions in query/) }) }) + + describe('detect', () => { + it('finds the first of two or more queries', async () => { + const promise = browserMonkey.detect({ + a: q => q.find('.a').expectSomeElements(), + b: q => q.find('.b').expectSomeElements() + }).then() + + const bPromise = assembly.eventuallyInsertHtml('
B
') + const actualB = [await bPromise] + + const {key, value} = await promise + await bPromise + + expect(key).to.eql('b') + expect(value).to.eql(actualB) + }) + + it('returns descriptions of all queries used when none are successful', async () => { + assembly.insertHtml('
content
C
') + + const promise = browserMonkey.find('.content').detect({ + a: b => b.find('.a').expectSomeElements(), + b: b => b.find('.b').expectSomeElements() + }) + + await expect(promise).to.reject.with.error("all queries failed in detect (found: find('.content') [1], detect(a: expected one or more elements, found 0 (found: find('.a') [0]), b: expected one or more elements, found 0 (found: find('.b') [0])) [0])") + }) + + it('throws if one of the queries does not have an assertion or action', async () => { + assembly.insertHtml('
content
C
') + + const promise = browserMonkey.find('.content').detect({ + a: b => b.find('.a'), + b: b => b.find('.b').expectSomeElements() + }) + + await expect(promise).to.reject.with.error(/no expectations or actions in query/) + }) + }) }) }) From 2ce1a48943fa6a98f71fac0734d6b84d686997ab Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 2 Dec 2019 13:14:10 +0100 Subject: [PATCH 029/127] move new interface into Query.ts, deprecate v2 interface --- create.js | 22 +- lib/Dom.ts | 2 +- lib/ExecutedDetectTransform.ts | 14 + lib/Query.ts | 528 ++++++++++++++++++- lib/actions.js | 6 +- lib/button.js | 20 - lib/finders.js | 78 +-- lib/{inputsSelector.js => inputSelectors.js} | 7 +- lib/set.js | 332 ------------ test/actionsSpec.js | 2 +- test/assemblies/DomAssembly.js | 2 +- test/assertSpec.js | 28 +- test/assertionsSpec.js | 2 +- test/buttonSpec.js | 4 +- test/componentSpec.js | 2 +- test/containingSpec.js | 32 +- test/eventsSpec.js | 2 +- test/findersSpec.js | 2 +- test/hyperdomIntegrationSpec.js | 18 +- test/mountSpec.js | 6 +- test/querySpec.js | 14 + test/reloadButtonSpec.js | 6 +- test/setSpec.js | 2 +- 23 files changed, 622 insertions(+), 509 deletions(-) create mode 100644 lib/ExecutedDetectTransform.ts delete mode 100644 lib/button.js rename lib/{inputsSelector.js => inputSelectors.js} (81%) delete mode 100644 lib/set.js diff --git a/create.js b/create.js index ec011cb..1e99694 100644 --- a/create.js +++ b/create.js @@ -1,17 +1,19 @@ var Query = require('./lib/Query') -var finders = require('./lib/finders') -var actions = require('./lib/actions') -var button = require('./lib/button') -var set = require('./lib/set') -var assertions = require('./lib/assertions') module.exports = function (rootSelector = document.body) { return new Query() - .component(finders) - .component(button) - .component(set) .installSetters() .scope(rootSelector) - .component(actions) - .component(assertions) + .component({ + v2: function () { + var finders = require('./lib/finders') + var actions = require('./lib/actions') + var assertions = require('./lib/assertions') + + return this + .component(finders) + .component(actions) + .component(assertions) + }, + }) } diff --git a/lib/Dom.ts b/lib/Dom.ts index 71c73e1..24af6d8 100644 --- a/lib/Dom.ts +++ b/lib/Dom.ts @@ -22,7 +22,7 @@ export default class Dom { this.triggerEvent(element, 'change') } - elementInnerText (element: HTMLElement, selector): void { + elementInnerText (element: HTMLElement): void { return normaliseText(element.innerText) } diff --git a/lib/ExecutedDetectTransform.ts b/lib/ExecutedDetectTransform.ts new file mode 100644 index 0000000..34e96bd --- /dev/null +++ b/lib/ExecutedDetectTransform.ts @@ -0,0 +1,14 @@ +import { ExecutedTransform } from './ExecutedTransform' + +export default class ExecutedDetectTransform extends ExecutedTransform { + public items: {key: string, transform: ExecutedTransform}[] + + public constructor (value: {key: string, value: any}, items: {key: string, transform: ExecutedTransform}[]) { + super(value) + this.items = items + } + + public renderError (): string { + return `detect(${this.items.map(i => `${i.key}: ${i.transform.renderError()}`).join(', ')}) [${this.value ? this.value.length : 0}]` + } +} diff --git a/lib/Query.ts b/lib/Query.ts index c529bc9..6201a51 100644 --- a/lib/Query.ts +++ b/lib/Query.ts @@ -8,33 +8,33 @@ import { ExecutedTransformError } from './ExecutedTransformError' import Dom from './Dom' import BrowserMonkeyAssertionError from './BrowserMonkeyAssertionError' import toExecutedTransform from './toExecutedTransform' +const pluralize = require('pluralize') var extend = require('lowscore/extend') var retry = require('trytryagain') var inspect = require('object-inspect') var uniq = require('lowscore/uniq') var debug = require('debug')('browser-monkey') -var inputsSelector = require('./inputsSelector') +var inputSelectors = require('./inputSelectors') +const object = require('lowscore/object') +const range = require('lowscore/range') +var flatten = require('lowscore/flatten') type Transform = (elements: any, executedTransforms: ExecutedTransform[]) => any type Action = (elements: any, executedTransforms: ExecutedTransform[]) => void +type FieldType = { + value: (query: Query) => Query + setter?: (query: Query, value: any) => Query +} + interface Definitions { - fieldTypes: { - value: (query: Query) => any - settter?: (query: Query, value: any) => () => void - }[] - button: ((monkey: any, name: any) => any)[] + fieldTypes: FieldType[] + button: ((query: Query, name: string) => Query)[] fields: {} } class Query { private _transforms: Transform[] - private _options: { - visibleOnly: boolean - timeout: number - interval: number - definitions: Definitions - } - + private _options: Options private _input: any private _actionExecuted: any private _action: Action @@ -51,15 +51,9 @@ class Query { definitions: { fieldTypes: [], button: [ - function (monkey, name) { - return monkey.css('button', { exactText: name }) - }, - function (monkey, name) { - return monkey.css('input[type=button]', { exactValue: name }) + (query: Query, name: string): Query => { + return query.css('button, input[type=button], a').containing(name) }, - function (monkey, name) { - return monkey.css('a', { exactText: name }) - } ], fields: {}, @@ -162,7 +156,7 @@ class Query { } } - public firstOf (queryCreators: [(q: Query) => Query]): this { + public firstOf (queryCreators: ((q: Query) => Query)[]): this { const transformed = this.transform((elements) => { const resolved = this.resolve(elements) @@ -262,7 +256,7 @@ class Query { return transformed } - public options (options): any { + public options (options: Options): any { if (options) { extend(this._options, options) } else { @@ -350,6 +344,405 @@ class Query { this._options = extend({}, from._options) this._options.definitions = cloneDefinitions(from._options.definitions) } + + expectNoElements (message?: string) { + return this.expect(elements => { + expectElements(elements) + + if (elements.length !== 0) { + this.error(message || 'expected no elements, found ' + elements.length) + } + }) + } + + expectOneElement (message?: string) { + return this.expect(elements => { + expectElements(elements) + + if (elements.length !== 1) { + this.error(message || 'expected just one element, found ' + elements.length) + } + }) + } + + element (): HTMLElement { + return this.expectOneElement().result()[0] + } + + expectSomeElements (message?: string) { + return this.expect(function (elements) { + expectElements(elements) + + if (elements.length < 1) { + this.error(message || 'expected one or more elements, found ' + elements.length) + } + }) + } + + click () { + return this.expectOneElement().action(([element]) => { + debug('click', element) + this._dom.click(element) + }) + } + + submit () { + return this.expectOneElement().action(([element]) => { + debug('submit', element) + this._dom.submit(element) + }) + } + + scope (element: HTMLElement) { + var selector = this.clone() + selector.input([element]) + + if (isIframe(element)) { + return selector.iframeContent() + } else if (isHTMLElement(element)) { + return selector + } else { + throw new Error('scope() expects HTML element') + } + } + + iframeContent () { + return this.transform(elements => { + return elements.map(element => { + if (isIframe(element)) { + if (element.contentDocument && element.contentDocument.readyState === 'complete') { + return element.contentDocument.body + } else { + throw new BrowserMonkeyAssertionError('iframe not loaded') + } + } else { + throw new BrowserMonkeyAssertionError('not iframe') + } + }) + }) + } + + enabled () { + return this.filter(element => { + var tagName = element.tagName + return !((tagName === 'BUTTON' || tagName === 'INPUT') && element.disabled) + }, 'enabled') + } + + clickButton (name) { + return this.button(name).click() + } + + set (selector, value) { + const model = value === undefined + ? selector + : { + [selector]: value + } + + return this.action(function (elements) { + const setters = [] + + const actions = { + arrayLengthError: (query, actualLength, expectedLength) => { + query.error('expected ' + expectedLength + ' ' + pluralize('elements', expectedLength) + ', found ' + actualLength) + }, + + value: (query, model) => { + var setter = query.setter(model).result() + setters.push(() => setter()) + }, + + function: (query, model) => { + setters.push(() => runModelFunction(model, query)) + } + } + + const clone = this.resolve(elements) + mapModel(clone, model, actions) + + setters.forEach(set => { + set() + }) + }) + } + + shouldExist (): this { + return this.expectSomeElements() + } + + shouldContain (model): this { + return this.expect(elements => { + let isError = false + + const actions = { + arrayLengthError: () => { + isError = true + }, + + value: (query, model) => { + let asserter + + try { + asserter = query.asserter(model).result() + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + isError = true + return 'Error: ' + e.message + } else { + throw e + } + } + + try { + asserter() + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + isError = true + return e.actual === undefined ? 'Error: ' + e.message : e.actual + } else { + throw e + } + } + + return model + }, + + function: (query, model) => { + try { + runModelFunction(model, query) + return model + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + isError = true + return 'Error: ' + e.message + } else { + throw e + } + } + } + } + + const clone = this.resolve(elements) + const result = mapModel(clone, model, actions) + + if (isError) { + this.error('could not match', {expected: model, actual: result}) + } + }) + } + + index (index) { + return this.transform(function (elements) { + return new ExecutedSimpleTransform([elements[index]], 'index ' + index) + }) + } + + define (name, finder) { + if (typeof finder === 'function') { + this._options.definitions.fields[name] = finder + } else if (typeof finder === 'string') { + this._options.definitions.fields[name] = q => q.find(finder) + } else if (name.constructor === Object && finder === undefined) { + Object.entries(name).forEach(([name, finder]) => this.define(name, finder)) + } + + return this + } + + setter (model) { + return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.setter).map(def => { + return query => def.setter(query, model) + })) + } + + asserter (expected) { + return this.value().transform(actual => { + return () => { + if (expected instanceof RegExp ? !expected.test(actual) : actual !== expected) { + this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) + } + } + }) + } + + defineFieldType (fieldTypeDefinition: FieldType) { + this._options.definitions.fieldTypes.unshift(fieldTypeDefinition) + } + + containing (model: any): this { + return this.transform(elements => { + const actions = { + arrayLengthError: (query, actualLength, expectedLength) => { + query.error('expected ' + expectedLength + ' ' + pluralize('elements', expectedLength) + ', found ' + actualLength) + }, + + value: (query, value) => { + const asserter = query.asserter(value).result() + asserter() + }, + + function: (query, fn) => { + runModelFunction(fn, query) + } + } + + return new ExecutedSimpleTransform(elements.filter(element => { + try { + const clone = this.resolve([element]) + mapModel(clone, model, actions) + return true + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + return false + } else { + throw e + } + } + }), `containing(${JSON.stringify(model)})`) + }) + } + + value (): this { + return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.value).map(def => { + return query => def.value(query) + })) + } + + installSetters (): this { + this.define('css', (query, css) => query.css(css)) + + this.defineFieldType({ + value: (query) => { + return query.expectOneElement().transform(([element]) => { + return query._dom.elementInnerText(element) + }) + } + }) + + this.defineFieldType({ + setter: (query, value) => { + return query + .is(inputSelectors.settable) + .expectOneElement() + .transform(([element]) => { + if (typeof value !== 'string') { + throw new Error('expected string as argument to set input') + } + return () => { + debug('set', element, value) + query._dom.enterText(element, value, {incremental: false}) + } + }) + }, + value: (query) => { + return query.is(inputSelectors.gettable).expectOneElement().transform(([input]) => { + return input.value + }) + } + }) + + this.defineFieldType({ + setter: (query, value) => { + return query + .is('select') + .expectOneElement('expected to be select element') + .css('option') + .filter(o => { + return o.value === value || query._dom.elementInnerText(o) === value + }, `option with text or value ${JSON.stringify(value)}`) + .expectOneElement(`expected one option element with text or value ${JSON.stringify(value)}`) + .transform(function ([option]) { + return () => { + const selectElement = option.parentNode + debug('select', selectElement) + query._dom.selectOption(selectElement, option) + } + }) + }, + value: (query) => { + return query + .is('select') + .expectOneElement('expected to be select element') + .transform(([select]) => { + const selectedOption = select.options[select.selectedIndex] + return selectedOption && query._dom.elementInnerText(selectedOption) + }) + } + }) + + this.defineFieldType({ + setter: (query, value) => { + return query + .is('input[type=checkbox]') + .expectOneElement() + .transform(([checkbox]) => { + if (typeof value !== 'boolean') { + throw new Error('expected boolean as argument to set checkbox') + } + return () => { + if (query._dom.checked(checkbox) !== value) { + debug('checkbox', checkbox, value) + query._dom.click(checkbox) + } + } + }) + }, + value: (query) => { + return query + .is('input[type=checkbox]') + .expectOneElement() + .transform(([checkbox]) => { + return query._dom.checked(checkbox) + }) + } + }) + + return this + } + + css (selector: string): this { + const findElements = this.transform(elements => { + expectElements(elements) + return new ExecutedSimpleTransform(flatten(elements.map(element => { + return this._dom.querySelectorAll(element, selector, this._options) + })), 'find(' + inspect(selector) + ')') + }) + + return findElements + } + + find (selector: string): this { + // name(value) + const match = /^\s*(.*?)\s*(\((.*)\)\s*)?$/.exec(selector) + + if (match) { + const [, name,, value] = match + const finder = this._options.definitions.fields[name] + + if (finder) { + if (value !== undefined) { + return finder(this, value.trim()) + } else { + return finder(this) + } + } + } + + return this.css(selector) + } + + is (selector: string): this { + return this.filter(element => { + return this._dom.elementMatches(element, selector) + }, 'is: ' + selector) + } +} + +function expectElements (elements) { + if (!isArrayOfHTMLElements(elements)) { + throw new BrowserMonkeyAssertionError('expected an array of HTML elements') + } } function cloneDefinitions(definitions: Definitions): Definitions { @@ -366,6 +759,26 @@ function cloneDefinitions(definitions: Definitions): Definitions { return result as Definitions } +function isIframe (element) { + return isHTMLElement(element, 'HTMLIFrameElement') +} + +function isHTMLElement (element, subclass: string = 'HTMLElement') { + if (element.ownerDocument && element.ownerDocument.defaultView) { + // an element inside an iframe + return element instanceof element.ownerDocument.defaultView[subclass] + } else { + return false + } +} + +function isArrayOfHTMLElements (elements) { + return elements instanceof Array && + elements.every(element => { + return isHTMLElement(element) + }) +} + type Retry = (fn: () => any) => Promise function retryFromOptions (options: {retry?: Retry, timeout?: number, interval?: number}): Retry { @@ -390,4 +803,73 @@ function runQueryCreator (queryCreator: (q: Query) => Query, query: Query): Quer return q } +function runModelFunction(fn, query) { + const result = fn(query) + if (result && typeof result.then === 'function') { + query.error('model functions must not be asynchronous') + } +} + +const missing = {} + +function expectLength (query, length) { + return query.expect(elements => { + const actualLength = elements.length + if (actualLength !== length) { + query.error('expected ' + length + ' ' + pluralize('elements', length) + ', found ' + actualLength) + } + }) +} + +function spliceModelArrayFromActual (model, query, actions) { + const length = query.result().length + + if (length > model.length) { + actions.arrayLengthError(query, length, model.length) + return range(0, length).map((item, index) => { + const modelItem = model[index] + + if (modelItem !== undefined) { + return modelItem + } else { + return missing + } + }) + } else if (length < model.length) { + actions.arrayLengthError(query, length, model.length) + return model.slice(0, length) + } else { + return model + } +} + +function mapModel (query, model, actions) { + function map (query, model) { + if (model === missing) { + return query.result().map(e => e.innerText).join() + } else if (model instanceof Array) { + return spliceModelArrayFromActual(model, query, actions).map((item, index) => { + return map(query.index(index), item) + }) + } else if (model.constructor === Object) { + return object(Object.entries(model).map(([selector, value]) => { + return [selector, map(query.find(selector), value)] + })) + } else if (typeof model === 'function') { + return actions.function(query, model) + } else { + return actions.value(expectLength(query, 1), model) + } + } + + return map(query, model) +} + +class Options { + visibleOnly: boolean + timeout: number + interval: number + definitions: Definitions +} + module.exports = Query diff --git a/lib/actions.js b/lib/actions.js index 4efb2d1..3fe9e91 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -1,5 +1,5 @@ var debug = require('debug')('browser-monkey') -var inputsSelector = require('./inputsSelector') +var inputSelectors = require('./inputSelectors') function notSillyBlankIEObject (element) { return Object.keys(element).length > 0 @@ -65,13 +65,13 @@ module.exports = { }, inputValue: function () { - return this.is(inputsSelector).expectOneElement().transform(([element]) => { + return this.is(inputSelectors.settable).expectOneElement().transform(([element]) => { return element.value }) }, typeIn: function (text) { - return this.is(inputsSelector).expectOneElement().action(([element]) => { + return this.is(inputSelectors.settable).expectOneElement().action(([element]) => { if (typeof text !== 'string') { throw new Error('expected string as argument to typeIn') } diff --git a/lib/button.js b/lib/button.js deleted file mode 100644 index 0c63e02..0000000 --- a/lib/button.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - button: function (name) { - return this.concat(this._options.definitions.button.map(function (definition) { - return function (monkey) { - return definition(monkey, name) - } - })) - }, - - defineButton: function (definition) { - var self = this - return this.clone(function (clone) { - self._options.definitions.button.push(definition) - }) - }, - - clickButton: function (name) { - return this.button(name).click() - } -} diff --git a/lib/finders.js b/lib/finders.js index c117037..17e8eeb 100644 --- a/lib/finders.js +++ b/lib/finders.js @@ -53,13 +53,7 @@ module.exports = { }) }, - is: function (selector) { - return this.filter(element => { - return this._dom.elementMatches(element, selector) - }, 'is: ' + selector) - }, - - containing: function (selector, options) { + containingV2: function (selector, options) { return this.filter((element) => { var foundElements = this._dom.querySelectorAll(element, selector, this._options) var filteredElements = options @@ -71,72 +65,6 @@ module.exports = { return filteredElements.length > 0 }, 'containing: ' + selector + (options ? ' ' + inspect(options) : '')) }, - - iframeContent: function () { - return this.transform(elements => { - return elements.map(element => { - if (isIframe(element)) { - if (element.contentDocument && element.contentDocument.readyState === 'complete') { - return element.contentDocument.body - } else { - throw new BrowserMonkeyAssertionError('iframe not loaded') - } - } else { - throw new BrowserMonkeyAssertionError('not iframe') - } - }) - }, 'iframeContent') - }, - - scope: function (element) { - var selector = this.clone() - selector.input([element]) - - if (isIframe(element)) { - return selector.iframeContent() - } else if (isHTMLElement(element)) { - return selector - } else { - throw new Error('scope() expects HTML element') - } - }, - - enabled: function () { - return this.filter(element => { - var tagName = element.tagName - return !((tagName === 'BUTTON' || tagName === 'INPUT') && element.disabled) - }, 'enabled') - }, - - expectNoElements: function (message) { - return this.expect(elements => { - expectElements(elements) - - if (elements.length !== 0) { - this.error(message || 'expected no elements, found ' + elements.length) - } - }) - }, - - expectOneElement: function (message) { - return this.expect(elements => { - expectElements(elements) - - if (elements.length !== 1) { - this.error(message || 'expected just one element, found ' + elements.length) - } - }) - }, - - expectSomeElements: function (message) { - return this.expect(function (elements) { - expectElements(elements) - - if (elements.length < 1) { - this.error(message || 'expected one or more elements, found ' + elements.length) - } - }) - } } function isArrayOfHTMLElements (elements) { @@ -160,7 +88,3 @@ function isHTMLElement (element, subclass) { return false } } - -function isIframe (element) { - return isHTMLElement(element, 'HTMLIFrameElement') -} diff --git a/lib/inputsSelector.js b/lib/inputSelectors.js similarity index 81% rename from lib/inputsSelector.js rename to lib/inputSelectors.js index 3b0eb74..04757bf 100644 --- a/lib/inputsSelector.js +++ b/lib/inputSelectors.js @@ -1,4 +1,4 @@ -var inputsSelector = +var settable = 'input:not([type]), ' + 'input[type=text], ' + 'input[type=email], ' + @@ -15,4 +15,7 @@ var inputsSelector = 'input[type=range],' + 'textarea' -module.exports = inputsSelector \ No newline at end of file +module.exports = { + settable, + gettable: 'input,textarea', +} diff --git a/lib/set.js b/lib/set.js deleted file mode 100644 index b870423..0000000 --- a/lib/set.js +++ /dev/null @@ -1,332 +0,0 @@ -var inputsSelector = require('./inputsSelector') -var debug = require('debug')('browser-monkey') -const inspect = require('object-inspect') -const object = require('lowscore/object') -const range = require('lowscore/range') -const pluralize = require('pluralize') -const BrowserMonkeyAssertionError = require('./BrowserMonkeyAssertionError').default -const { ExecutedSimpleTransform } = require('./ExecutedSimpleTransform') - -function expectLength (query, length) { - return query.expect(elements => { - const actualLength = elements.length - if (actualLength !== length) { - query.error('expected ' + length + ' ' + pluralize('elements', length) + ', found ' + actualLength) - } - }) -} - -const missing = {} - -function spliceModelArrayFromActual (model, query, actions) { - const length = query.result().length - - if (length > model.length) { - actions.arrayLengthError(query, length, model.length) - return range(0, length).map((item, index) => { - const modelItem = model[index] - - if (modelItem !== undefined) { - return modelItem - } else { - return missing - } - }) - } else if (length < model.length) { - actions.arrayLengthError(query, length, model.length) - return model.slice(0, length) - } else { - return model - } -} - -function mapModel (query, model, actions) { - function map (query, model) { - if (model === missing) { - return query.result().map(e => e.innerText).join() - } else if (model instanceof Array) { - return spliceModelArrayFromActual(model, query, actions).map((item, index) => { - return map(query.index(index), item) - }) - } else if (model.constructor === Object) { - return object(Object.entries(model).map(([selector, value]) => { - return [selector, map(query.find(selector), value)] - })) - } else if (typeof model === 'function') { - return actions.function(query, model) - } else { - return actions.value(expectLength(query, 1), model) - } - } - - return map(query, model) -} - -module.exports = { - set: function (selector, value) { - const model = value === undefined - ? selector - : { - [selector]: value - } - - return this.action(function (elements) { - const setters = [] - - const actions = { - arrayLengthError: (query, actualLength, expectedLength) => { - query.error('expected ' + expectedLength + ' ' + pluralize('elements', expectedLength) + ', found ' + actualLength) - }, - - value: (query, model) => { - var setter = query.setter(model).result() - setters.push(() => setter()) - }, - - function: (query, model) => { - setters.push(() => runModelFunction(model, query)) - } - } - - const clone = this.resolve(elements) - mapModel(clone, model, actions) - - setters.forEach(set => { - set() - }) - }) - }, - - assert: function (model) { - return this.expect(elements => { - let isError = false - - const actions = { - arrayLengthError: () => { - isError = true - }, - - value: (query, model) => { - let asserter - - try { - asserter = query.asserter(model).result() - } catch (e) { - if (e instanceof BrowserMonkeyAssertionError) { - isError = true - return 'Error: ' + e.message - } else { - throw e - } - } - - try { - asserter() - } catch (e) { - if (e instanceof BrowserMonkeyAssertionError) { - isError = true - return e.actual === undefined ? 'Error: ' + e.message : e.actual - } else { - throw e - } - } - - return model - }, - - function: (query, model) => { - try { - runModelFunction(model, query) - return model - } catch (e) { - if (e instanceof BrowserMonkeyAssertionError) { - isError = true - return 'Error: ' + e.message - } else { - throw e - } - } - } - } - - const clone = this.resolve(elements) - const result = mapModel(clone, model, actions) - - if (isError) { - this.error('could not match', {expected: model, actual: result}) - } - }) - }, - - index: function (index) { - return this.transform(function (elements) { - return [elements[index]] - }, 'index ' + index) - }, - - define: function (name, finder) { - if (typeof finder === 'function') { - this._options.definitions.fields[name] = finder - } else if (typeof finder === 'string') { - this._options.definitions.fields[name] = q => q.find(finder) - } else if (name.constructor === Object && finder === undefined) { - Object.entries(name).forEach(([name, finder]) => this.define(name, finder)) - } - - return this - }, - - setter: function (model) { - return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.setter).map(def => { - return query => def.setter(query, model) - })) - }, - - asserter: function (expected) { - return this.value().transform(actual => { - return () => { - if (expected instanceof RegExp ? !expected.test(actual) : actual !== expected) { - this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) - } - } - }) - }, - - defineFieldType: function (fieldTypeDefinition) { - this._options.definitions.fieldTypes.push(fieldTypeDefinition) - }, - - containing: function (model) { - return this.transform(elements => { - const actions = { - arrayLengthError: (query, actualLength, expectedLength) => { - query.error('expected ' + expectedLength + ' ' + pluralize('elements', expectedLength) + ', found ' + actualLength) - }, - - value: (query, value) => { - const asserter = query.asserter(value).result() - asserter() - }, - - function: (query, fn) => { - runModelFunction(fn, query) - } - } - - return new ExecutedSimpleTransform(elements.filter(element => { - try { - const clone = this.resolve([element]) - mapModel(clone, model, actions) - return true - } catch (e) { - if (e instanceof BrowserMonkeyAssertionError) { - return false - } else { - throw e - } - } - }), `containing(${JSON.stringify(model)})`) - }) - }, - - value: function () { - return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.value).map(def => { - return query => def.value(query) - })) - }, - - installSetters: function () { - this.define('css', (query, css) => query.css(css)) - - this.defineFieldType({ - setter: (query, value) => { - return query - .is(inputsSelector) - .expectOneElement() - .transform(([element]) => { - if (typeof value !== 'string') { - throw new Error('expected string as argument to set input') - } - return () => { - debug('set', element, value) - query._dom.enterText(element, value, {incremental: false}) - } - }) - }, - value: (query) => { - return query.is(inputsSelector).expectOneElement().transform(([input]) => { - return input.value - }) - } - }) - this.defineFieldType({ - setter: (query, value) => { - return query - .is('select') - .expectOneElement('expected to be select element') - .css('option') - .filter(o => o.value === value && query._dom.elementInnerText(o) === value, `option with text or value ${JSON.stringify(value)}`) - .expectOneElement(`expected one option element with text or value ${JSON.stringify(value)}`) - .transform(function ([option]) { - return () => { - const selectElement = option.parentNode - debug('select', selectElement) - query._dom.selectOption(selectElement, option) - } - }) - }, - value: (query) => { - return query - .is('select') - .expectOneElement('expected to be select element') - .transform(([select]) => { - const selectedOption = select.options[select.selectedIndex] - return selectedOption && query._dom.elementInnerText(selectedOption) - }) - } - }) - this.defineFieldType({ - setter: (query, value) => { - return query - .is('input[type=checkbox]') - .expectOneElement() - .transform(([checkbox]) => { - if (typeof value !== 'boolean') { - throw new Error('expected boolean as argument to set checkbox') - } - return () => { - if (query._dom.checked(checkbox) !== value) { - debug('checkbox', checkbox, value) - query._dom.click(checkbox) - } - } - }) - }, - value: (query) => { - return query - .is('input[type=checkbox]') - .expectOneElement() - .transform(([checkbox]) => { - return query._dom.checked(checkbox) - }) - } - }) - - this.defineFieldType({ - value: (query) => { - return query.expectOneElement().transform(([element]) => { - return query._dom.elementInnerText(element) - }) - } - }) - - return this - } -} - -function runModelFunction(fn, query) { - const result = fn(query) - if (result && typeof result.then === 'function') { - query.error('model functions must not be asynchronous') - } -} diff --git a/test/actionsSpec.js b/test/actionsSpec.js index 67d4189..07ed6b2 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -10,7 +10,7 @@ describe('actions', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().v2() }) describe('clicking', function () { diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.js index b226ce7..37bf393 100644 --- a/test/assemblies/DomAssembly.js +++ b/test/assemblies/DomAssembly.js @@ -144,7 +144,7 @@ module.exports = class DomAssembly { } assertExpectedActual (query, expected, actual) { - return query.assert(expected).then(() => { + return query.shouldContain(expected).then(() => { throw new Error('expected rejection') }, e => { expect(e.message).to.contain('could not match') diff --git a/test/assertSpec.js b/test/assertSpec.js index 8eb88c5..9204f00 100644 --- a/test/assertSpec.js +++ b/test/assertSpec.js @@ -19,7 +19,7 @@ describe('assert', function () { 12 Hapless Boulevard `) - await browser.assert({ + await browser.shouldContain({ '.address': '12 Hapless Boulevard', }) }) @@ -29,7 +29,7 @@ describe('assert', function () { `) - await browser.assert({ + await browser.shouldContain({ '.address': '12 Hapless Boulevard', }) }) @@ -65,7 +65,7 @@ describe('assert', function () { `) - await browser.assert({ + await browser.shouldContain({ 'select': 'Two', }) }) @@ -75,7 +75,7 @@ describe('assert', function () { `) - await browser.assert({ + await browser.shouldContain({ '.check': true, }) }) @@ -91,7 +91,7 @@ describe('assert', function () { `) - await browser.find('.address .street').assert('7 Lola St') + await browser.find('.address .street').shouldContain('7 Lola St') }) it('single argument, with scope asserts value', async () => { @@ -127,7 +127,7 @@ describe('assert', function () { `) - await browser.find('.address .street').assert(/\d+ Lola St/) + await browser.find('.address .street').shouldContain(/\d+ Lola St/) }) it('fails if the regular expression does not match', async () => { @@ -152,7 +152,7 @@ describe('assert', function () {
`) - await browser.assert({ + await browser.shouldContain({ 'h1': 'Title', '.content': 'The Content' }) @@ -189,7 +189,7 @@ describe('assert', function () {
`) - await browser.assert({ + await browser.shouldContain({ '.result': [ 'Result 1', 'Result 2', @@ -256,7 +256,7 @@ describe('assert', function () {
`) - await browser.assert({ + await browser.shouldContain({ '.result': [ { 'h3': 'Title 1', @@ -280,7 +280,7 @@ describe('assert', function () {
`) - await browser.assert({ + await browser.shouldContain({ 'h1': 'Title', '.content': query => { if (!/Content/.test(query.expectOneElement().result()[0].innerText)) { @@ -341,7 +341,7 @@ describe('assert', function () { browser.define('header', q => q.find('h1')) - await browser.assert({ + await browser.shouldContain({ header: 'Title', '.content': 'The Content' }) @@ -357,7 +357,7 @@ describe('assert', function () { browser.define('header', 'h1') - await browser.assert({ + await browser.shouldContain({ header: 'Title', '.content': 'The Content' }) @@ -376,7 +376,7 @@ describe('assert', function () { content: q => q.find('.content'), }) - await browser.assert({ + await browser.shouldContain({ header: 'Title', content: 'The Content' }) @@ -403,7 +403,7 @@ describe('assert', function () { }), }) - await browser.assert({ + await browser.shouldContain({ title: 'Title', content: { title: 'title', diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index 2dc7281..b150701 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -9,7 +9,7 @@ describe('assertions', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().v2() }) describe('shouldNotExist', function () { diff --git a/test/buttonSpec.js b/test/buttonSpec.js index b9f822f..0cb997d 100644 --- a/test/buttonSpec.js +++ b/test/buttonSpec.js @@ -80,7 +80,7 @@ describe('buttons', function () { const button = assembly.insertHtml('') button.addEventListener('click', () => events.push('click')) - return browser.click('Login').then(function () { + return browser.clickButton('Login').then(function () { demand(events).to.eql(['click']) }) }) @@ -103,7 +103,7 @@ describe('buttons', function () { browser.defineButton((monkey, name) => monkey.find('div.button', { exactText: name })) - return browser.click('Login').then(function () { + return browser.clickButton('Login').then(function () { demand(events).to.eql(['click']) }) }) diff --git a/test/componentSpec.js b/test/componentSpec.js index 2c886fc..71bbdb8 100644 --- a/test/componentSpec.js +++ b/test/componentSpec.js @@ -8,7 +8,7 @@ describe('component', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().v2() }) it('can return new selectors by extending', function () { diff --git a/test/containingSpec.js b/test/containingSpec.js index af09089..3b5c53f 100644 --- a/test/containingSpec.js +++ b/test/containingSpec.js @@ -15,14 +15,40 @@ describe('containing', function () { describe('values', () => { it('filters elements that have the exact text', () => { assembly.insertHtml(` - car - car + car + car horse cart `) const transports = browser.find('span').containing('car').result() - const cars = assembly.findAll('.car') + const cars = assembly.findAll('.found') + expect(transports).to.eql(cars) + }) + + it('filters input elements that have the exact text', () => { + assembly.insertHtml(` + + + + + `) + + const transports = browser.find('input').containing('car').result() + const cars = assembly.findAll('.found') + expect(transports).to.eql(cars) + }) + + it('filters input button elements that have the exact text', () => { + assembly.insertHtml(` + + + + + `) + + const transports = browser.find('input').containing('car').result() + const cars = assembly.findAll('.found') expect(transports).to.eql(cars) }) }) diff --git a/test/eventsSpec.js b/test/eventsSpec.js index 09ba49e..39b3e48 100644 --- a/test/eventsSpec.js +++ b/test/eventsSpec.js @@ -10,7 +10,7 @@ describe('events', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().v2() }) it('typeIn element should fire change', function () { diff --git a/test/findersSpec.js b/test/findersSpec.js index 8d24e63..d3dfdd5 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -10,7 +10,7 @@ describe('find', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey() + browser = assembly.browserMonkey().v2() }) it('should eventually find an element', function () { diff --git a/test/hyperdomIntegrationSpec.js b/test/hyperdomIntegrationSpec.js index 28494c7..4c2c97d 100644 --- a/test/hyperdomIntegrationSpec.js +++ b/test/hyperdomIntegrationSpec.js @@ -60,17 +60,19 @@ describe('hyperdom integration', function () { hyperdom.append(assembly._div, new App(model), { requestRender: setTimeout }) await browserMonkey.find('.toggle').click() - await browserMonkey.find('span', { text: 'hello' }).shouldExist() - await browserMonkey.find('input').typeIn('monkey') + await browserMonkey.shouldContain({span: 'hello'}) + await browserMonkey.set({input: 'monkey'}) await retry(function () { demand(model.name).to.equal('monkey') }) - await browserMonkey.find('select').select({ text: 'Female' }) - await browserMonkey.find('.name').shouldHave({ text: 'monkey' }) - await browserMonkey.find('.gender').shouldHave({ text: 'fml' }) - await browserMonkey.find('input').shouldHave({ value: 'monkey' }) - await browserMonkey.find('select').select({ text: 'Other' }) - await browserMonkey.find('option:checked').shouldHave({ text: 'Other' }) + await browserMonkey.set({select: 'Female'}) + return + await browserMonkey.shouldContain({ + '.name': 'monkey', + '.gender': 'fml', + 'input': 'monkey', + 'select': 'Other', + }) }) }) }) diff --git a/test/mountSpec.js b/test/mountSpec.js index 20235f8..c8f08ab 100644 --- a/test/mountSpec.js +++ b/test/mountSpec.js @@ -24,16 +24,16 @@ function testMount (appType, app, monkeyBuilder) { afterEach(() => page.options().mount.stop()) it('loads some data', () => { - return page.find('.message').shouldHave({ text: 'default' }).then(() => { + return page.find('.message').shouldContain('default').then(() => { return page.find('button').click() }).then(() => { - return page.find('.message').shouldHave({ text: 'hello browser-monkey' }) + return page.find('.message').shouldContain('hello browser-monkey') }) }) it('can enter form fields', async () => { await page.set({'input': 'hi'}) - await page.assert({'.message': 'hi'}) + await page.shouldContain({'.message': 'hi'}) }) it('exposes the app', () => { diff --git a/test/querySpec.js b/test/querySpec.js index 9e6677c..c39ec84 100644 --- a/test/querySpec.js +++ b/test/querySpec.js @@ -171,6 +171,20 @@ describe('query', () => { }) }) + describe('element', () => { + it('expects one html element and returns it', () => { + const expected = assembly.insertHtml(` +
+ `) + + const actual = browserMonkey + .find('.title') + .element() + + expect(actual).to.equal(expected) + }) + }) + describe('expectSomeElements', () => { it('when there are one or more elements, selects them', async () => { const contacts = browserMonkey diff --git a/test/reloadButtonSpec.js b/test/reloadButtonSpec.js index 19dc19e..d486cac 100644 --- a/test/reloadButtonSpec.js +++ b/test/reloadButtonSpec.js @@ -15,10 +15,8 @@ describe('reload button', function () { it('adds reload button that reloads to the initial url', function () { reloadButton() - return browser.find('a', { text: '⟳ reload' }).shouldHave({ - attributes: { - href: window.location.href - } + return browser.find('a').containing('⟳ reload').shouldContain(q => { + expect(q.element().getAttribute('href')).to.equal(window.location.href) }) }, { vdom: false }) diff --git a/test/setSpec.js b/test/setSpec.js index 908dab5..5633f8e 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -35,7 +35,7 @@ describe('set', function () { `) await browser.set({ - 'select': 'Two', + select: 'Two', }) const selectedItem = assembly.jQuery(assembly.find('select')).find(':selected').text() From a9f9fa313e98c57a8c2c6cacfc19cc6178b9d40f Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 2 Dec 2019 15:22:49 +0100 Subject: [PATCH 030/127] select/option can assert value or text --- lib/Query.ts | 63 +++++++++++++++----- test/{assertSpec.js => shouldContainSpec.js} | 33 +++++++--- 2 files changed, 73 insertions(+), 23 deletions(-) rename test/{assertSpec.js => shouldContainSpec.js} (93%) diff --git a/lib/Query.ts b/lib/Query.ts index 6201a51..29501f8 100644 --- a/lib/Query.ts +++ b/lib/Query.ts @@ -22,8 +22,9 @@ var flatten = require('lowscore/flatten') type Transform = (elements: any, executedTransforms: ExecutedTransform[]) => any type Action = (elements: any, executedTransforms: ExecutedTransform[]) => void type FieldType = { - value: (query: Query) => Query + value?: (query: Query) => Query setter?: (query: Query, value: any) => Query + valueAsserter?: (query: Query, expected: any) => Query } interface Definitions { @@ -481,10 +482,10 @@ class Query { }, value: (query, model) => { - let asserter + let valueAsserter try { - asserter = query.asserter(model).result() + valueAsserter = query.valueAsserter(model).result() } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { isError = true @@ -495,7 +496,7 @@ class Query { } try { - asserter() + valueAsserter() } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { isError = true @@ -556,14 +557,18 @@ class Query { })) } - asserter (expected) { - return this.value().transform(actual => { - return () => { - if (expected instanceof RegExp ? !expected.test(actual) : actual !== expected) { - this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) - } - } - }) + valueAsserter (expected) { + return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.value).map(def => { + return query => def.valueAsserter + ? def.valueAsserter(query, expected) + : def.value(query).transform(actual => { + return () => { + if (!testEqual(actual, expected)) { + this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) + } + } + }) + })) } defineFieldType (fieldTypeDefinition: FieldType) { @@ -578,8 +583,8 @@ class Query { }, value: (query, value) => { - const asserter = query.asserter(value).result() - asserter() + const valueAsserter = query.valueAsserter(value).result() + valueAsserter() }, function: (query, fn) => { @@ -660,6 +665,32 @@ class Query { } }) }, + valueAsserter: (query, expected) => { + return query + .is('select') + .expectOneElement('expected to be select element') + .transform(([select]) => { + return () => { + const value = select.value + + if (testEqual(value, expected)) { + return + } + + const selectedOption = select.options[select.selectedIndex] + if (selectedOption) { + const actual = query._dom.elementInnerText(selectedOption) + if (testEqual(actual, expected)) { + return + } else { + this.error('expected ' + inspect(actual) + ' or ' + inspect(value) + ' to equal ' + inspect(expected), { actual, expected }) + } + } + + this.error('expected ' + inspect(value) + ' to equal ' + inspect(expected), { actual: value, expected }) + } + }) + }, value: (query) => { return query .is('select') @@ -865,6 +896,10 @@ function mapModel (query, model, actions) { return map(query, model) } +const testEqual = (actual, expected) => { + return expected instanceof RegExp ? expected.test(actual) : actual === expected +} + class Options { visibleOnly: boolean timeout: number diff --git a/test/assertSpec.js b/test/shouldContainSpec.js similarity index 93% rename from test/assertSpec.js rename to test/shouldContainSpec.js index 9204f00..2160bb5 100644 --- a/test/assertSpec.js +++ b/test/shouldContainSpec.js @@ -57,16 +57,31 @@ describe('assert', function () { }) }) - it('can assert select options', async () => { - assembly.insertHtml(` - - `) + describe('select', () => { + it('can assert select options by text', async () => { + assembly.insertHtml(` + + `) + + await browser.shouldContain({ + 'select': 'Two', + }) + }) - await browser.shouldContain({ - 'select': 'Two', + it('can assert select options by value', async () => { + assembly.insertHtml(` + + `) + + await browser.shouldContain({ + 'select': 'two', + }) }) }) From 1b56392bd37b79685e9a7e0651223d8edeb9e5d0 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 2 Dec 2019 18:09:16 +0100 Subject: [PATCH 031/127] stricter object assertions --- lib/Query.ts | 127 +++++++++++++++++++++++++------------- test/setSpec.js | 13 ++-- test/shouldContainSpec.js | 92 ++++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 54 deletions(-) diff --git a/lib/Query.ts b/lib/Query.ts index 29501f8..539fc78 100644 --- a/lib/Query.ts +++ b/lib/Query.ts @@ -235,11 +235,11 @@ class Query { const transform = new ExecutedDetectTransform( firstSuccess - ? { - key: firstSuccess.key, - value: firstSuccess.value.value, - } - : undefined, + ? { + key: firstSuccess.key, + value: firstSuccess.value.value, + } + : undefined, entries.map(v => ({key: v.key, transform: v.error || v.value})) ) @@ -346,7 +346,7 @@ class Query { this._options.definitions = cloneDefinitions(from._options.definitions) } - expectNoElements (message?: string) { + public expectNoElements (message?: string): this { return this.expect(elements => { expectElements(elements) @@ -356,7 +356,7 @@ class Query { }) } - expectOneElement (message?: string) { + public expectOneElement (message?: string): this { return this.expect(elements => { expectElements(elements) @@ -366,11 +366,11 @@ class Query { }) } - element (): HTMLElement { + public element (): HTMLElement { return this.expectOneElement().result()[0] } - expectSomeElements (message?: string) { + public expectSomeElements (message?: string): this { return this.expect(function (elements) { expectElements(elements) @@ -380,21 +380,21 @@ class Query { }) } - click () { + public click (): this { return this.expectOneElement().action(([element]) => { debug('click', element) this._dom.click(element) }) } - submit () { + public submit (): this { return this.expectOneElement().action(([element]) => { debug('submit', element) this._dom.submit(element) }) } - scope (element: HTMLElement) { + public scope (element: HTMLElement): this { var selector = this.clone() selector.input([element]) @@ -407,7 +407,7 @@ class Query { } } - iframeContent () { + public iframeContent (): this { return this.transform(elements => { return elements.map(element => { if (isIframe(element)) { @@ -423,18 +423,18 @@ class Query { }) } - enabled () { + public enabled (): this { return this.filter(element => { var tagName = element.tagName return !((tagName === 'BUTTON' || tagName === 'INPUT') && element.disabled) }, 'enabled') } - clickButton (name) { + public clickButton (name: string): this { return this.button(name).click() } - set (selector, value) { + public set (selector, value): this { const model = value === undefined ? selector : { @@ -454,6 +454,10 @@ class Query { setters.push(() => setter()) }, + expectOne: (query) => { + expectLength(query, 1).result() + }, + function: (query, model) => { setters.push(() => runModelFunction(model, query)) } @@ -468,11 +472,15 @@ class Query { }) } - shouldExist (): this { + public shouldExist (): this { return this.expectSomeElements() } - shouldContain (model): this { + public shouldNotExist (): this { + return this.expectNoElements() + } + + public shouldContain (model): this { return this.expect(elements => { let isError = false @@ -481,6 +489,20 @@ class Query { isError = true }, + expectOne: (query) => { + try { + expectLength(query, 1).result() + return {} + } catch (e) { + if (e instanceof BrowserMonkeyAssertionError) { + isError = true + return 'Error: ' + e.message + } else { + throw e + } + } + }, + value: (query, model) => { let valueAsserter @@ -533,13 +555,13 @@ class Query { }) } - index (index) { + public index (index: number): this { return this.transform(function (elements) { return new ExecutedSimpleTransform([elements[index]], 'index ' + index) }) } - define (name, finder) { + public define (name, finder): this { if (typeof finder === 'function') { this._options.definitions.fields[name] = finder } else if (typeof finder === 'string') { @@ -551,13 +573,13 @@ class Query { return this } - setter (model) { + public setter (model): this { return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.setter).map(def => { return query => def.setter(query, model) })) } - valueAsserter (expected) { + public valueAsserter (expected: any): this { return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.value).map(def => { return query => def.valueAsserter ? def.valueAsserter(query, expected) @@ -571,11 +593,11 @@ class Query { })) } - defineFieldType (fieldTypeDefinition: FieldType) { + public defineFieldType (fieldTypeDefinition: FieldType): void { this._options.definitions.fieldTypes.unshift(fieldTypeDefinition) } - containing (model: any): this { + public containing (model: any): this { return this.transform(elements => { const actions = { arrayLengthError: (query, actualLength, expectedLength) => { @@ -587,6 +609,10 @@ class Query { valueAsserter() }, + expectOne: (query) => { + query.expectOne().result() + }, + function: (query, fn) => { runModelFunction(fn, query) } @@ -608,13 +634,13 @@ class Query { }) } - value (): this { + public value (): this { return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.value).map(def => { return query => def.value(query) })) } - installSetters (): this { + public installSetters (): this { this.define('css', (query, css) => query.css(css)) this.defineFieldType({ @@ -732,7 +758,7 @@ class Query { return this } - css (selector: string): this { + public css (selector: string): this { const findElements = this.transform(elements => { expectElements(elements) return new ExecutedSimpleTransform(flatten(elements.map(element => { @@ -743,7 +769,7 @@ class Query { return findElements } - find (selector: string): this { + public find (selector: string): this { // name(value) const match = /^\s*(.*?)\s*(\((.*)\)\s*)?$/.exec(selector) @@ -763,14 +789,14 @@ class Query { return this.css(selector) } - is (selector: string): this { + public is (selector: string): this { return this.filter(element => { return this._dom.elementMatches(element, selector) }, 'is: ' + selector) } } -function expectElements (elements) { +function expectElements (elements): void { if (!isArrayOfHTMLElements(elements)) { throw new BrowserMonkeyAssertionError('expected an array of HTML elements') } @@ -790,11 +816,11 @@ function cloneDefinitions(definitions: Definitions): Definitions { return result as Definitions } -function isIframe (element) { +function isIframe (element): boolean { return isHTMLElement(element, 'HTMLIFrameElement') } -function isHTMLElement (element, subclass: string = 'HTMLElement') { +function isHTMLElement (element, subclass: string = 'HTMLElement'): boolean { if (element.ownerDocument && element.ownerDocument.defaultView) { // an element inside an iframe return element instanceof element.ownerDocument.defaultView[subclass] @@ -803,7 +829,7 @@ function isHTMLElement (element, subclass: string = 'HTMLElement') { } } -function isArrayOfHTMLElements (elements) { +function isArrayOfHTMLElements (elements): boolean { return elements instanceof Array && elements.every(element => { return isHTMLElement(element) @@ -834,7 +860,7 @@ function runQueryCreator (queryCreator: (q: Query) => Query, query: Query): Quer return q } -function runModelFunction(fn, query) { +function runModelFunction(fn: (Query) => Query, query: Query): void { const result = fn(query) if (result && typeof result.then === 'function') { query.error('model functions must not be asynchronous') @@ -843,7 +869,7 @@ function runModelFunction(fn, query) { const missing = {} -function expectLength (query, length) { +function expectLength (query, length): Query { return query.expect(elements => { const actualLength = elements.length if (actualLength !== length) { @@ -852,7 +878,14 @@ function expectLength (query, length) { }) } -function spliceModelArrayFromActual (model, query, actions) { +interface Actions { + arrayLengthError (q: Query, actual: number, expected: number): void + function (q: Query, model: any): any + value (q: Query, model: any): any + expectOne (q: Query): any +} + +function spliceModelArrayFromActual (model, query: Query, actions: Actions): any[] { const length = query.result().length if (length > model.length) { @@ -874,8 +907,8 @@ function spliceModelArrayFromActual (model, query, actions) { } } -function mapModel (query, model, actions) { - function map (query, model) { +function mapModel (query: Query, model: any, actions: Actions): any { + function map (query: Query, model: any): any { if (model === missing) { return query.result().map(e => e.innerText).join() } else if (model instanceof Array) { @@ -883,9 +916,17 @@ function mapModel (query, model, actions) { return map(query.index(index), item) }) } else if (model.constructor === Object) { - return object(Object.entries(model).map(([selector, value]) => { - return [selector, map(query.find(selector), value)] - })) + const lengthQuery = expectLength(query, 1) + + const entries = Object.entries(model) + + if (entries.length) { + return object(entries.map(([selector, value]) => { + return [selector, map(lengthQuery.find(selector), value)] + })) + } else { + return actions.expectOne(query) + } } else if (typeof model === 'function') { return actions.function(query, model) } else { @@ -896,11 +937,11 @@ function mapModel (query, model, actions) { return map(query, model) } -const testEqual = (actual, expected) => { +function testEqual (actual: any, expected: any): boolean { return expected instanceof RegExp ? expected.test(actual) : actual === expected } -class Options { +interface Options { visibleOnly: boolean timeout: number interval: number diff --git a/test/setSpec.js b/test/setSpec.js index 5633f8e..4750215 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -105,7 +105,7 @@ describe('set', function () { demand(assembly.find('.first-name').value).to.equal('Barry') }) - it("an object doesn't assert the number of matching elements", async () => { + it("an object asserts the number of matching elements", async () => { assembly.insertHtml(`
@@ -117,15 +117,12 @@ describe('set', function () { `) - await browser.set({ + await assembly.assertRejection(browser.set({ '.address': { '.street': '7 Lola St', '.city': 'Frisby City', }, - }) - - demand(assembly.find('.address .street').value).to.equal('7 Lola St') - demand(assembly.find('.address .city').value).to.equal('Frisby City') + }), "expected 1 element, found 2 (found: find('.address') [2])") }) it('a value does assert that there is only one matching element', async () => { @@ -133,8 +130,6 @@ describe('set', function () {
-
-
@@ -146,7 +141,7 @@ describe('set', function () { '.street': '7 Lola St', '.city': 'Frisby City', }, - }), "expected 1 element, found 2 (found: find('.address') [2], find('.street') [2])") + }), "expected 1 element, found 2 (found: find('.address') [1], find('.street') [2])") }) it('deep fields disambiguate', async () => { diff --git a/test/shouldContainSpec.js b/test/shouldContainSpec.js index 2160bb5..01a113a 100644 --- a/test/shouldContainSpec.js +++ b/test/shouldContainSpec.js @@ -188,6 +188,64 @@ describe('assert', function () { '.content': "Error: expected 1 element, found 0 (found: find('.content') [0])" }) }) + + it("fails when contents are spread over two containers", async () => { + assembly.insertHtml(` +
+

Title

+
+
+
The Content
+
+ `) + + await assembly.assertExpectedActual(browser, { + '.result': [{ + 'h1': 'Title', + '.content': 'The Content' + }] + }, { + '.result': [ + { + 'h1': 'Title', + '.content': "Error: expected 1 element, found 0 (found: find('.result') [2], index 0 [1], find('.content') [0])" + }, + 'The Content' + ] + }) + }) + + describe('empty objects', () => { + it("can assert that an element exists, without testing it's contents", async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + await browser.shouldContain({ + 'h1': 'Title', + '.content': {} + }) + }) + + it("fails when an element doesn't exist", async () => { + assembly.insertHtml(` +
+

Title

+
+ `) + + await assembly.assertExpectedActual(browser, { + 'h1': 'Title', + '.content': {} + }, { + 'h1': 'Title', + '.content': "Error: expected 1 element, found 0 (found: find('.content') [0])" + }) + }) + }) }) describe('arrays', function () { @@ -284,6 +342,36 @@ describe('assert', function () { ] }) }) + + describe('empty arrays', () => { + it('asserts that no elements were found', async () => { + assembly.insertHtml(` +
+ Result 1 +
+ `) + + await browser.shouldContain({ + '.notfound': [] + }) + }) + + it('fails when elements are found', async () => { + assembly.insertHtml(` +
+ Result 1 +
+ `) + + await assembly.assertExpectedActual(browser, { + '.result': [] + }, { + '.result': [ + 'Result 1', + ] + }) + }) + }) }) describe('functions', () => { @@ -298,7 +386,7 @@ describe('assert', function () { await browser.shouldContain({ 'h1': 'Title', '.content': query => { - if (!/Content/.test(query.expectOneElement().result()[0].innerText)) { + if (!/Content/.test(query.element().innerText)) { throw new BrowserMonkeyAssertionError('asdf') } } @@ -316,7 +404,7 @@ describe('assert', function () { await assembly.assertExpectedActual(browser, { 'h1': 'Title', '.content': query => { - if (/Content/.test(query.expectOneElement().result()[0].innerText)) { + if (/Content/.test(query.element().innerText)) { throw new BrowserMonkeyAssertionError('asdf') } } From c6c3d633b018e99e347c7ce475315f7b047b4d0e Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Mon, 16 Dec 2019 16:28:00 +0100 Subject: [PATCH 032/127] labels, improved selector syntax --- lib/BrowserMonkeyAssertionError.ts | 8 +- lib/Dom.ts | 86 +++++----- lib/ExecutedConcatTransform.ts | 2 +- lib/ExecutedContainingTransform.ts | 14 ++ lib/ExecutedDetectTransform.ts | 2 +- ...rmSequence.ts => ExecutedTransformPath.ts} | 29 ++-- lib/Query.ts | 151 +++++++++++++----- package.json | 2 +- test/buttonSpec.js | 45 +++--- test/containingSpec.js | 55 ++++++- test/querySpec.js | 96 +++++++++-- test/setSpec.js | 10 +- test/shouldContainSpec.js | 80 +++++++--- tsconfig.json | 4 +- 14 files changed, 407 insertions(+), 177 deletions(-) create mode 100644 lib/ExecutedContainingTransform.ts rename lib/{ExecutedTransformSequence.ts => ExecutedTransformPath.ts} (51%) diff --git a/lib/BrowserMonkeyAssertionError.ts b/lib/BrowserMonkeyAssertionError.ts index dd91a0f..984b6a6 100644 --- a/lib/BrowserMonkeyAssertionError.ts +++ b/lib/BrowserMonkeyAssertionError.ts @@ -1,17 +1,17 @@ import { ExecutedTransform } from './ExecutedTransform' -import { ExecutedTransformSequence } from './ExecutedTransformSequence' +import { ExecutedTransformPath } from './ExecutedTransformPath' class BrowserMonkeyAssertionError extends Error { private description: string public showDiff: boolean public expected: any public actual: any - public executedTransforms: ExecutedTransformSequence + public executedTransforms: ExecutedTransformPath public constructor (message, { expected = undefined, actual = undefined, - executedTransforms = new ExecutedTransformSequence(undefined) + executedTransforms = new ExecutedTransformPath(undefined) } = {}) { super(message) this.description = message @@ -33,7 +33,7 @@ class BrowserMonkeyAssertionError extends Error { this.rewriteMessage() } - public prependExecutedTransforms (executedTransforms: ExecutedTransformSequence): void { + public prependExecutedTransforms (executedTransforms: ExecutedTransformPath): void { this.executedTransforms.prepend(executedTransforms) this.rewriteMessage() } diff --git a/lib/Dom.ts b/lib/Dom.ts index 24af6d8..f12bfe2 100644 --- a/lib/Dom.ts +++ b/lib/Dom.ts @@ -2,8 +2,35 @@ var MouseEvent = require('./polyfills').MouseEvent var KeyboardEvent = require('./polyfills').KeyboardEvent const normaliseText = require('./normaliseText') +var eventCreatorsByType = { + mousedown: function () { + return createMouseEvent('mousedown') + }, + mouseup: function () { + return createMouseEvent('mouseup') + }, + change: function () { + return createEvent('change') + }, + input: function () { + return createEvent('input') + }, + keydown: function (key) { + return createKeyboardEvent('keydown', key) + }, + keyup: function (key) { + return createKeyboardEvent('keyup', key) + }, + keypress: function (key) { + return createKeyboardEvent('keypress', key) + }, + submit: function () { + return createEvent('submit', { bubbles: true, cancelable: true }) + } +} + export default class Dom { - enterText (element: HTMLInputElement, text: string, {incremental = true} = {}): void { + public enterText (element: HTMLInputElement, text: string, {incremental = true} = {}): void { element.focus() if (incremental) { @@ -22,18 +49,18 @@ export default class Dom { this.triggerEvent(element, 'change') } - elementInnerText (element: HTMLElement): void { + public elementInnerText (element: HTMLElement): string { return normaliseText(element.innerText) } - click (element: HTMLElement): void { + public click (element: HTMLElement): void { this.triggerEvent(element, 'mousedown') this.triggerEvent(element, 'mouseup') element.click() element.focus() } - querySelectorAll (element: HTMLElement, selector: string, {visibleOnly = true} = {}): HTMLElement[] { + public querySelectorAll (element: HTMLElement, selector: string, {visibleOnly = true} = {}): HTMLElement[] { var children = Array.prototype.slice.call(element.querySelectorAll(selector)) children = visibleOnly @@ -43,7 +70,7 @@ export default class Dom { return children } - elementVisible (element: HTMLElement): boolean { + public elementVisible (element: HTMLElement): boolean { if (element.tagName === 'OPTION' && element.parentNode) { return this.elementVisible(element.parentNode as HTMLElement) } else { @@ -51,18 +78,18 @@ export default class Dom { } } - elementMatches (element: HTMLElement, selector: string): boolean { + public elementMatches (element: HTMLElement, selector: string): boolean { return element.matches(selector) } - selectOption (selectElement: HTMLSelectElement, optionElement: HTMLOptionElement): void { + public selectOption (selectElement: HTMLSelectElement, optionElement: HTMLOptionElement): void { this.click(selectElement) selectElement.selectedIndex = optionElement.index this.triggerEvent(selectElement, 'input') this.triggerEvent(selectElement, 'change') } - triggerEvent (element: HTMLElement, eventType, value?) { + private triggerEvent (element: HTMLElement, eventType, value?): void { var creator = eventCreatorsByType[eventType] if (!creator) { @@ -74,7 +101,7 @@ export default class Dom { element.dispatchEvent(event) } - setInputValue (element: HTMLInputElement, value: string): void { + public setInputValue (element: HTMLInputElement, value: string): void { const nativeInputValueSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value').set if (nativeInputValueSetter) { nativeInputValueSetter.call(element, value) @@ -84,20 +111,20 @@ export default class Dom { this.triggerEvent(element, 'input') } - typeKey (element: HTMLInputElement, value: string, key: string): void { + public typeKey (element: HTMLInputElement, value: string, key: string): void { this.triggerEvent(element, 'keydown', key) this.triggerEvent(element, 'keypress', key) this.setInputValue(element, value) this.triggerEvent(element, 'keyup', key) } - checked (element: HTMLInputElement): 'indeterminate'|boolean { + public checked (element: HTMLInputElement): 'indeterminate'|boolean { return element.indeterminate ? 'indeterminate' : element.checked } - selector (element: HTMLElement): string { + public selector (element: HTMLElement): string { var id = element.id var classes = element.className.split(/ +/g).filter(Boolean).sort() var tag = element.tagName.toLowerCase() @@ -107,7 +134,7 @@ export default class Dom { (classes.length ? '.' + classes.join('.') : '') } - submit (element: HTMLInputElement) { + public submit (element: HTMLInputElement): void { element.focus() this.triggerEvent(element, 'keydown') this.triggerEvent(element, 'keypress') @@ -121,44 +148,17 @@ export default class Dom { } } -function createMouseEvent (type) { +function createMouseEvent (type): MouseEvent { return new MouseEvent(type, { bubbles: true, cancelable: true }) } -function createEvent (type, params = {bubbles: true, cancelable: false}) { +function createEvent (type, params = {bubbles: true, cancelable: false}): Event { // IE compatible old school way of creating events. var event = document.createEvent('Event') event.initEvent(type, params.bubbles, params.cancelable) return event } -function createKeyboardEvent (type, key) { +function createKeyboardEvent (type, key): KeyboardEvent { return new KeyboardEvent(type, { bubbles: true, cancelable: true, key: key }) } - -var eventCreatorsByType = { - mousedown: function () { - return createMouseEvent('mousedown') - }, - mouseup: function () { - return createMouseEvent('mouseup') - }, - change: function () { - return createEvent('change') - }, - input: function () { - return createEvent('input') - }, - keydown: function (key) { - return createKeyboardEvent('keydown', key) - }, - keyup: function (key) { - return createKeyboardEvent('keyup', key) - }, - keypress: function (key) { - return createKeyboardEvent('keypress', key) - }, - submit: function () { - return createEvent('submit', { bubbles: true, cancelable: true }) - } -} diff --git a/lib/ExecutedConcatTransform.ts b/lib/ExecutedConcatTransform.ts index fc6e998..901c0ce 100644 --- a/lib/ExecutedConcatTransform.ts +++ b/lib/ExecutedConcatTransform.ts @@ -9,6 +9,6 @@ export class ExecutedConcatTransform extends ExecutedTransform { } public renderError (): string { - return `concat (${this.items.map(i => i.renderError()).join(', ')}) [${this.value.length}]` + return `concat(${this.items.map(i => i.renderError()).join(', ')}) [${this.value.length}]` } } diff --git a/lib/ExecutedContainingTransform.ts b/lib/ExecutedContainingTransform.ts new file mode 100644 index 0000000..825018e --- /dev/null +++ b/lib/ExecutedContainingTransform.ts @@ -0,0 +1,14 @@ +import { ExecutedTransform } from './ExecutedTransform' + +export class ExecutedContainingTransform extends ExecutedTransform { + public model: any + + public constructor (value: any, model: any) { + super(value) + this.model = model + } + + public renderError (): string { + return `containing(${this.model ? '...' + this.model.renderError() : ''}) [${this.value.length}]` + } +} diff --git a/lib/ExecutedDetectTransform.ts b/lib/ExecutedDetectTransform.ts index 34e96bd..76dab4e 100644 --- a/lib/ExecutedDetectTransform.ts +++ b/lib/ExecutedDetectTransform.ts @@ -1,6 +1,6 @@ import { ExecutedTransform } from './ExecutedTransform' -export default class ExecutedDetectTransform extends ExecutedTransform { +export class ExecutedDetectTransform extends ExecutedTransform { public items: {key: string, transform: ExecutedTransform}[] public constructor (value: {key: string, value: any}, items: {key: string, transform: ExecutedTransform}[]) { diff --git a/lib/ExecutedTransformSequence.ts b/lib/ExecutedTransformPath.ts similarity index 51% rename from lib/ExecutedTransformSequence.ts rename to lib/ExecutedTransformPath.ts index 50fa433..6d6000d 100644 --- a/lib/ExecutedTransformSequence.ts +++ b/lib/ExecutedTransformPath.ts @@ -1,6 +1,6 @@ import { ExecutedTransform } from './ExecutedTransform' -export class ExecutedTransformSequence extends ExecutedTransform { +export class ExecutedTransformPath extends ExecutedTransform { public transforms: ExecutedTransform[] public constructor (value: any) { @@ -18,7 +18,7 @@ export class ExecutedTransformSequence extends ExecutedTransform { this.transforms.push(...transforms) } - public prepend (transforms: ExecutedTransformSequence): void { + public prepend (transforms: ExecutedTransformPath): void { this.value = transforms.value this.transforms.unshift(...transforms.transforms) } @@ -28,25 +28,16 @@ export class ExecutedTransformSequence extends ExecutedTransform { } public renderError (): string { - const transformsUpUntilAndIncludingFirstFailure = this.transforms.reduce(({ keepTaking, array }, b) => { - if (keepTaking) { - return { - keepTaking: !!(b.value && b.value.length), - array: array.concat([b]) - } - } else { - return { array } - } - }, { - array: [], - keepTaking: true - }).array - - return transformsUpUntilAndIncludingFirstFailure.map(t => t.renderError()).filter(Boolean).join(', ') + const errors = this.transforms.map(t => t.renderError()).filter(Boolean).join(', ') + if (this.transforms.length > 1) { + return `path(${errors})` + } else { + return errors + } } - public clone (): ExecutedTransformSequence { - const e = new ExecutedTransformSequence(this.value) + public clone (): ExecutedTransformPath { + const e = new ExecutedTransformPath(this.value) e.transforms = this.transforms.slice() return e } diff --git a/lib/Query.ts b/lib/Query.ts index 539fc78..8e19404 100644 --- a/lib/Query.ts +++ b/lib/Query.ts @@ -1,9 +1,10 @@ import { ExecutedTransform } from './ExecutedTransform' -import { ExecutedTransformSequence } from './ExecutedTransformSequence' +import { ExecutedTransformPath } from './ExecutedTransformPath' import { ExecutedSimpleTransform } from './ExecutedSimpleTransform' import { ExecutedConcatTransform } from './ExecutedConcatTransform' +import { ExecutedContainingTransform } from './ExecutedContainingTransform' import { ExecutedFirstOfTransform } from './ExecutedFirstOfTransform' -import ExecutedDetectTransform from './ExecutedDetectTransform' +import { ExecutedDetectTransform } from './ExecutedDetectTransform' import { ExecutedTransformError } from './ExecutedTransformError' import Dom from './Dom' import BrowserMonkeyAssertionError from './BrowserMonkeyAssertionError' @@ -21,7 +22,7 @@ var flatten = require('lowscore/flatten') type Transform = (elements: any, executedTransforms: ExecutedTransform[]) => any type Action = (elements: any, executedTransforms: ExecutedTransform[]) => void -type FieldType = { +interface FieldType { value?: (query: Query) => Query setter?: (query: Query, value: any) => Query valueAsserter?: (query: Query, expected: any) => Query @@ -30,6 +31,7 @@ type FieldType = { interface Definitions { fieldTypes: FieldType[] button: ((query: Query, name: string) => Query)[] + label: ((query: Query, name: string) => Query)[] fields: {} } @@ -56,8 +58,36 @@ class Query { return query.css('button, input[type=button], a').containing(name) }, ], - - fields: {}, + label: [ + (query, name) => { + return query.find('label').containing(name).find('input') + }, + (query, name) => { + return query.find('label[for]').containing(name).map(label => { + const id = label.getAttribute('for') + return label.ownerDocument.getElementById(id) + }, 'for attribute').filter(Boolean) + }, + (query, name) => { + return query.find('[aria-label]').filter(element => { + const label = element.getAttribute('aria-label') + return label === name + }, 'aria-label') + }, + (query, name) => { + return query.find('[aria-labelledby]').filter(element => { + const id = element.getAttribute('aria-labelledby') + const labelElement = element.ownerDocument.getElementById(id) + if (labelElement) { + return query._dom.elementInnerText(labelElement) === name + } + }, 'aria-label') + }, + ], + fields: { + Label: (q, value) => q.label(value), + Button: (q, value) => q.button(value), + }, } } this._dom = new Dom() @@ -97,10 +127,22 @@ class Query { })) } + public label (name: string): this { + return this.concat(this._options.definitions.label.map(definition => { + return (q: Query): Query => { + return definition(q, name) + } + })) + } + public defineButton (definition: (q: Query, name?: string) => Query): void { this._options.definitions.button.push(definition) } + public defineLabel (definition: (q: Query, name?: string) => Query): void { + this._options.definitions.label.push(definition) + } + public result (): any { return this.execute().value } @@ -112,7 +154,13 @@ class Query { return resolved } - public filter (filter: (a: any) => boolean, description: string): this { + public map (map: (a: any) => any, description?: string): this { + return this.transform((elements) => { + return new ExecutedSimpleTransform(elements.map(map), description) + }) + } + + public filter (filter: (a: any) => boolean, description?: string): this { return this.transform((elements) => { return new ExecutedSimpleTransform(elements.filter(filter), description) }) @@ -133,24 +181,24 @@ class Query { throw new BrowserMonkeyAssertionError(message, { expected, actual }) } - private execute (): ExecutedTransformSequence { - const transformSequence = new ExecutedTransformSequence(this._input) + private execute (): ExecutedTransformPath { + const transformPath = new ExecutedTransformPath(this._input) try { this._transforms.forEach(transform => { - const executedTransform = toExecutedTransform(transform.call(this, transformSequence.value, transformSequence.transforms)) - transformSequence.addTransform(executedTransform) + const executedTransform = toExecutedTransform(transform.call(this, transformPath.value, transformPath.transforms)) + transformPath.addTransform(executedTransform) }) if (this._action && !this._actionExecuted) { - this._action(transformSequence.value, transformSequence.transforms) + this._action(transformPath.value, transformPath.transforms) this._actionExecuted = true } - return transformSequence + return transformPath } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { - e.prependExecutedTransforms(transformSequence) + e.prependExecutedTransforms(transformPath) } throw e @@ -370,6 +418,10 @@ class Query { return this.expectOneElement().result()[0] } + public elements (): HTMLElement { + return this.expectSomeElements().result()[0] + } + public expectSomeElements (message?: string): this { return this.expect(function (elements) { expectElements(elements) @@ -380,8 +432,8 @@ class Query { }) } - public click (): this { - return this.expectOneElement().action(([element]) => { + public click (selector?: string): this { + return (selector ? this.find(selector) : this).expectOneElement().action(([element]) => { debug('click', element) this._dom.click(element) }) @@ -395,13 +447,13 @@ class Query { } public scope (element: HTMLElement): this { - var selector = this.clone() - selector.input([element]) + var query = this.clone() + query.input([element]) if (isIframe(element)) { - return selector.iframeContent() + return query.iframeContent() } else if (isHTMLElement(element)) { - return selector + return query } else { throw new Error('scope() expects HTML element') } @@ -459,7 +511,7 @@ class Query { }, function: (query, model) => { - setters.push(() => runModelFunction(model, query)) + setters.push(() => this.runModelFunction(model, query)) } } @@ -533,7 +585,7 @@ class Query { function: (query, model) => { try { - runModelFunction(model, query) + this.runModelFunction(model, query) return model } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { @@ -567,7 +619,7 @@ class Query { } else if (typeof finder === 'string') { this._options.definitions.fields[name] = q => q.find(finder) } else if (name.constructor === Object && finder === undefined) { - Object.entries(name).forEach(([name, finder]) => this.define(name, finder)) + Object.keys(name).forEach(key => this.define(key, name[key])) } return this @@ -610,27 +662,33 @@ class Query { }, expectOne: (query) => { - query.expectOne().result() + query.expectOne().execute() }, function: (query, fn) => { - runModelFunction(fn, query) + return this.runModelFunction(fn, query) } } - return new ExecutedSimpleTransform(elements.filter(element => { + let lastError + let lastSuccess + + const matchingElements = elements.filter(element => { try { const clone = this.resolve([element]) - mapModel(clone, model, actions) + lastSuccess = mapModel(clone, model, actions) return true } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { + lastError = new ExecutedTransformError(e) return false } else { throw e } } - }), `containing(${JSON.stringify(model)})`) + }) + + return new ExecutedContainingTransform(matchingElements, lastSuccess ? lastSuccess : lastError) }) } @@ -770,19 +828,23 @@ class Query { } public find (selector: string): this { - // name(value) - const match = /^\s*(.*?)\s*(\((.*)\)\s*)?$/.exec(selector) + // name(arg1, arg2, ...) + const match = /^\s*([$a-z_][0-9a-z_$]*)\s*(\((.*)\)\s*)?$/i.exec(selector) if (match) { const [, name,, value] = match const finder = this._options.definitions.fields[name] - if (finder) { - if (value !== undefined) { - return finder(this, value.trim()) + if (value !== undefined) { + if (finder) { + const func = new Function(`return [${value}]`) + const args = func() + return finder(this, ...args) } else { - return finder(this) + throw new Error('no such definition ' + name) } + } else if (finder) { + return finder(this) } } @@ -794,6 +856,15 @@ class Query { return this._dom.elementMatches(element, selector) }, 'is: ' + selector) } + + private runModelFunction(fn: (Query) => any, query: Query): ExecutedTransform | undefined { + const result = fn(query) + if (result instanceof Query) { + return result.execute() + } else if (result && typeof result.then === 'function') { + query.error('model functions must not be asynchronous') + } + } } function expectElements (elements): void { @@ -860,13 +931,6 @@ function runQueryCreator (queryCreator: (q: Query) => Query, query: Query): Quer return q } -function runModelFunction(fn: (Query) => Query, query: Query): void { - const result = fn(query) - if (result && typeof result.then === 'function') { - query.error('model functions must not be asynchronous') - } -} - const missing = {} function expectLength (query, length): Query { @@ -918,10 +982,11 @@ function mapModel (query: Query, model: any, actions: Actions): any { } else if (model.constructor === Object) { const lengthQuery = expectLength(query, 1) - const entries = Object.entries(model) + const keys = Object.keys(model) - if (entries.length) { - return object(entries.map(([selector, value]) => { + if (keys.length) { + return object(keys.map(selector => { + const value = model[selector] return [selector, map(lengthQuery.find(selector), value)] })) } else { diff --git a/package.json b/package.json index 5703588..46b9a9e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "electron-mocha": "electron-mocha --disable-site-isolation-trials --full-trace -r test/register --renderer", "update-readme-example-links": "node ./docs/update-readme-example-links.js", "test-browser-compatibility": "browserify ./index.js > /dev/null", - "prepare": "tcs" + "prepare": "tsc" }, "author": "Tim Macfarlane ", "license": "MIT", diff --git a/test/buttonSpec.js b/test/buttonSpec.js index 0cb997d..e21affd 100644 --- a/test/buttonSpec.js +++ b/test/buttonSpec.js @@ -1,6 +1,7 @@ var describeAssemblies = require('./describeAssemblies') const DomAssembly = require('./assemblies/DomAssembly') var demand = require('must') +const {expect} = require('chai') describe('buttons', function () { describeAssemblies([DomAssembly], function (Assembly) { @@ -75,37 +76,37 @@ describe('buttons', function () { }) describe('clicking a button', () => { - it('can click a button', () => { + async function assertCanClickButton(html, action) { const events = [] - const button = assembly.insertHtml('') + assembly.insertHtml(html) + const button = assembly.find('.target') button.addEventListener('click', () => events.push('click')) - return browser.clickButton('Login').then(function () { - demand(events).to.eql(['click']) - }) - }) + await action() + expect(events).to.eql(['click']) + } - it('can find and click a button', () => { - const events = [] - const button = assembly.insertHtml('') - button.addEventListener('click', () => events.push('click')) - - return browser.find('button').click().then(function () { - demand(events).to.eql(['click']) - }) + it('can click a button', async () => { + await assertCanClickButton('', () => browser.clickButton('Login')) }) - it('can click a defined button', () => { - const events = [] - const button = assembly.insertHtml('
Login
') + it('can find and click a button', async () => { + await assertCanClickButton('', () => browser.find('Button("Login")').click()) + }) - button.addEventListener('click', () => events.push('click')) + it('can click a button with selector', async () => { + await assertCanClickButton('', () => browser.click('Button("Login")')) + }) - browser.defineButton((monkey, name) => monkey.find('div.button', { exactText: name })) + it('can click a defined button', async () => { + browser.defineButton((monkey, name) => monkey.find('div.button').containing(name)) + await assertCanClickButton('
Login
', () => browser.click('Button("Login")')) + }) - return browser.clickButton('Login').then(function () { - demand(events).to.eql(['click']) - }) + it('throws if the button cannot be found to click', async () => { + browser.defineButton((monkey, name) => monkey.find('div.button').containing(name)) + assembly.insertHtml('') + await assembly.assertRejection(browser.clickButton('Logout'), "expected just one element, found 0 (found: concat(path(find('button, input[type=button], a') [1], containing(...expected 'Login' to equal 'Logout') [0]), path(find('div.button') [0], containing() [0])) [0])") }) }) }) diff --git a/test/containingSpec.js b/test/containingSpec.js index 3b5c53f..ef18cc8 100644 --- a/test/containingSpec.js +++ b/test/containingSpec.js @@ -89,7 +89,60 @@ describe('containing', function () { expect(() => browser.find('.result').containing({'.title': 'Title', '.body': 'None'}).expectOneElement().result() - ).to.throw(`expected just one element, found 0 (found: find('.result') [3], containing({".title":"Title",".body":"None"}) [0])`) + ).to.throw(`expected just one element, found 0 (found: path(find('.result') [3], containing(...expected 1 element, found 0 (found: path(find('.title') [0]))) [0]))`) + }) + }) + + describe('functions', () => { + it('function can assert existence', () => { + assembly.insertHtml(` +
+
Title
+
+
+
Body
+
+ `) + + const results = browser.find('.result').containing(result => result.find('.body').shouldExist()).result() + const expected = assembly.findAll('.correct') + expect(results).to.eql(expected) + }) + + it('shows what it found', () => { + assembly.insertHtml(` +
+
Title
+
+
+
Body
+
+
+
Body
+
+ `) + + expect(() => + browser.find('.result').containing(result => result.find('.body').shouldExist()).find('.title').expectOneElement().result() + ).to.throw(`expected just one element, found 0 (found: path(find('.result') [3], containing(...path(find('.body') [1])) [2], find('.title') [0]))`) + }) + + it("shows why it couldn't find the element", () => { + assembly.insertHtml(` +
+
Title
+
+
+
Title
+
+
+
Title
+
+ `) + + expect(() => + browser.find('.result').containing(result => result.find('.body').shouldExist()).find('.title').expectOneElement().result() + ).to.throw(`expected just one element, found 0 (found: path(find('.result') [3], containing(...expected one or more elements, found 0 (found: find('.body') [0])) [0], find('.title') [0])`) }) }) }) diff --git a/test/querySpec.js b/test/querySpec.js index c39ec84..5d60fec 100644 --- a/test/querySpec.js +++ b/test/querySpec.js @@ -1,7 +1,6 @@ var describeAssemblies = require('./describeAssemblies') -const expect = require('must') +const {expect} = require('chai') const DomAssembly = require('./assemblies/DomAssembly') -const BrowserMonkeyAssertionError = require('../lib/BrowserMonkeyAssertionError').default describe('query', () => { describeAssemblies([DomAssembly], Assembly => { @@ -115,7 +114,7 @@ describe('query', () => { .then() assembly.eventuallyDeleteHtml('.message') - await expect(contacts).reject.with.error(/expected no elements/) + await assembly.assertRejection(contacts, 'expected no elements') }) }) @@ -153,7 +152,7 @@ describe('query', () => {
`) - await expect(contacts).reject.with.error(/expected just one element, found 2/) + await assembly.assertRejection(contacts, 'expected just one element, found 2') }) it('when there no elements, throws an error', async () => { @@ -167,7 +166,7 @@ describe('query', () => {
`) - await expect(contacts).reject.with.error(/expected just one element, found 0/) + await assembly.assertRejection(contacts, 'expected just one element, found 0') }) }) @@ -183,6 +182,50 @@ describe('query', () => { expect(actual).to.equal(expected) }) + + it('fails if there are more than one element', () => { + assembly.insertHtml(` +
+
+ `) + + expect(() => browserMonkey + .find('.title') + .element()).to.throw('expected just one element, found 2') + }) + + it('fails if there are no elements', () => { + assembly.insertHtml(` + `) + + expect(() => browserMonkey + .find('.title') + .element()).to.throw('expected just one element, found 0') + }) + }) + + describe('elements', () => { + it('expects one or more html elements and returns them', () => { + const expected = assembly.insertHtml(` +
+
+ `) + + const actual = browserMonkey + .find('.title') + .elements() + + expect(actual).to.equal(expected) + }) + + it('fails if there are no elements', () => { + assembly.insertHtml(` + `) + + expect(() => browserMonkey + .find('.title') + .elements()).to.throw('expected one or more elements, found 0') + }) }) describe('expectSomeElements', () => { @@ -217,7 +260,7 @@ describe('query', () => {
`) - await expect(contacts).reject.with.error(/expected one or more elements, found 0/) + await assembly.assertRejection(contacts, 'expected one or more elements, found 0') }) }) @@ -267,7 +310,7 @@ describe('query', () => {
goodbye
`) - await expect(hello).to.reject.with.error(/expected to see hello/) + await assembly.assertRejection(hello, 'expected to see hello') }) }) @@ -278,7 +321,7 @@ describe('query', () => { .transform(names => { return names.map(contact => contact.innerText).join(', ') }) - .expect(x => expect(x).to.not.be.empty()) + .expect(x => expect(x).to.not.be.empty) .then() assembly.eventuallyInsertHtml(` @@ -315,6 +358,31 @@ describe('query', () => { }) }) + describe('map', () => { + it('can map elements', async () => { + const contacts = browserMonkey + .find('.contact') + .map(contact => { + return contact.querySelector('.name') + }) + .expectSomeElements() + .then() + + assembly.eventuallyInsertHtml(` +
+
Sally
+
32 Yellow Drive
+
+
+
Bob
+
32 Red Drive
+
+ `) + + expect(await contacts).to.eql(assembly.findAll('.name')) + }) + }) + describe('actions', () => { it('actions are only executed once', async () => { let actionExecuted = 0 @@ -371,7 +439,7 @@ describe('query', () => {
`) - await expect(name).to.reject.with.error("expected just one element, found 0 (found: find('.container') [1], find('.contact') [2], find('.name') [0])") + await assembly.assertRejection(name, "expected just one element, found 0 (found: path(find('.container') [1], find('.contact') [2], find('.name') [0]))") }) }) @@ -411,7 +479,7 @@ describe('query', () => { b => b.find('.b') ]).find('.child').expectSomeElements() - return expect(promise).to.reject.with.error("expected one or more elements, found 0 (found: concat (find('.a') [1], find('.b') [1]) [2], find('.child') [0])") + return assembly.assertRejection(promise, "expected one or more elements, found 0 (found: path(concat(find('.a') [1], find('.b') [1]) [2], find('.child') [0])") }) }) @@ -438,7 +506,7 @@ describe('query', () => { b => b.find('.b').expectSomeElements() ]) - await expect(promise).to.reject.with.error("all queries failed in firstOf (found: find('.content') [1], firstOf(expected one or more elements, found 0 (found: find('.a') [0]), expected one or more elements, found 0 (found: find('.b') [0])) [0])") + await assembly.assertRejection(promise, "all queries failed in firstOf (found: path(find('.content') [1], firstOf(expected one or more elements, found 0 (found: find('.a') [0]), expected one or more elements, found 0 (found: find('.b') [0])) [0]))") }) it('throws if one of the queries does not have an assertion or action', async () => { @@ -449,7 +517,7 @@ describe('query', () => { b => b.find('.b').expectSomeElements() ]) - await expect(promise).to.reject.with.error(/no expectations or actions in query/) + await assembly.assertRejection(promise, 'no expectations or actions in query') }) }) @@ -478,7 +546,7 @@ describe('query', () => { b: b => b.find('.b').expectSomeElements() }) - await expect(promise).to.reject.with.error("all queries failed in detect (found: find('.content') [1], detect(a: expected one or more elements, found 0 (found: find('.a') [0]), b: expected one or more elements, found 0 (found: find('.b') [0])) [0])") + await assembly.assertRejection(promise, "all queries failed in detect (found: path(find('.content') [1], detect(a: expected one or more elements, found 0 (found: find('.a') [0]), b: expected one or more elements, found 0 (found: find('.b') [0])) [0]))") }) it('throws if one of the queries does not have an assertion or action', async () => { @@ -489,7 +557,7 @@ describe('query', () => { b: b => b.find('.b').expectSomeElements() }) - await expect(promise).to.reject.with.error(/no expectations or actions in query/) + await assembly.assertRejection(promise, 'no expectations or actions in query') }) }) }) diff --git a/test/setSpec.js b/test/setSpec.js index 4750215..733a3c7 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -122,7 +122,7 @@ describe('set', function () { '.street': '7 Lola St', '.city': 'Frisby City', }, - }), "expected 1 element, found 2 (found: find('.address') [2])") + }), "expected 1 element, found 2 (found: path(find('.address') [2]))") }) it('a value does assert that there is only one matching element', async () => { @@ -141,7 +141,7 @@ describe('set', function () { '.street': '7 Lola St', '.city': 'Frisby City', }, - }), "expected 1 element, found 2 (found: find('.address') [1], find('.street') [2])") + }), "expected 1 element, found 2 (found: path(find('.address') [1], find('.street') [2]))") }) it('deep fields disambiguate', async () => { @@ -276,7 +276,7 @@ describe('set', function () { '.phone': '123123123', '.first-name': 'Barry' }) - await assembly.assertRejection(promise, "expected 1 element, found 0 (found: find('form') [1], find('.street') [0])") + await assembly.assertRejection(promise, "expected 1 element, found 0 (found: path(find('form') [1], find('.street') [0]))") demand(assembly.find('.phone').value).to.equal('') demand(assembly.find('.first-name').value).to.equal('') @@ -337,8 +337,8 @@ describe('set', function () { browser.define('form', (b, name) => b.find('* [name=' + JSON.stringify(name) + ']')) await browser.set({ - 'form(phone)': '123123123', - 'form(firstname)': 'Barry' + 'form("phone")': '123123123', + 'form("firstname")': 'Barry' }) demand(assembly.find('.phone').value).to.equal('123123123') diff --git a/test/shouldContainSpec.js b/test/shouldContainSpec.js index 01a113a..712c2c9 100644 --- a/test/shouldContainSpec.js +++ b/test/shouldContainSpec.js @@ -1,9 +1,8 @@ const describeAssemblies = require('./describeAssemblies') const DomAssembly = require('./assemblies/DomAssembly') var BrowserMonkeyAssertionError = require('../lib/BrowserMonkeyAssertionError').default -const {expect} = require('chai') -describe('assert', function () { +describe('shouldContain', function () { describeAssemblies([DomAssembly], function (Assembly) { var assembly var browser @@ -53,7 +52,7 @@ describe('assert', function () { await assembly.assertExpectedActual(browser, { '.address': '12 Hapless Boulevard', }, { - '.address': "Error: expected 1 element, found 0 (found: find('.address') [0])", + '.address': "Error: expected 1 element, found 0 (found: path(find('.address') [0]))", }) }) @@ -185,7 +184,7 @@ describe('assert', function () { '.content': 'The Content' }, { 'h1': 'Title', - '.content': "Error: expected 1 element, found 0 (found: find('.content') [0])" + '.content': "Error: expected 1 element, found 0 (found: path(find('.content') [0]))" }) }) @@ -208,7 +207,7 @@ describe('assert', function () { '.result': [ { 'h1': 'Title', - '.content': "Error: expected 1 element, found 0 (found: find('.result') [2], index 0 [1], find('.content') [0])" + '.content': "Error: expected 1 element, found 0 (found: path(find('.result') [2], index 0 [1], find('.content') [0]))" }, 'The Content' ] @@ -242,7 +241,7 @@ describe('assert', function () { '.content': {} }, { 'h1': 'Title', - '.content': "Error: expected 1 element, found 0 (found: find('.content') [0])" + '.content': "Error: expected 1 element, found 0 (found: path(find('.content') [0]))" }) }) }) @@ -431,6 +430,20 @@ describe('assert', function () { '.content': 'Error: model functions must not be asynchronous' }) }) + + it('does not throw if the function returns a query', async () => { + assembly.insertHtml(` +
+

Title

+
The Content
+
+ `) + + await browser.shouldContain({ + 'h1': 'Title', + '.content': q => q.shouldExist() + }) + }) }) describe('define', () => { @@ -442,10 +455,10 @@ describe('assert', function () {
`) - browser.define('header', q => q.find('h1')) + browser.define('Header', q => q.find('h1')) await browser.shouldContain({ - header: 'Title', + Header: 'Title', '.content': 'The Content' }) }) @@ -458,10 +471,10 @@ describe('assert', function () {
`) - browser.define('header', 'h1') + browser.define('Header', 'h1') await browser.shouldContain({ - header: 'Title', + Header: 'Title', '.content': 'The Content' }) }) @@ -475,13 +488,36 @@ describe('assert', function () { `) browser.define({ - header: 'h1', - content: q => q.find('.content'), + Header: 'h1', + Content: q => q.find('.content'), }) await browser.shouldContain({ - header: 'Title', - content: 'The Content' + Header: 'Title', + Content: 'The Content' + }) + }) + + it('can define a section by its contents', async () => { + assembly.insertHtml(` +
+

Section A

+
Section A Content
+
+
+

Section B

+
Section B Content
+
+ `) + + browser.define('Section', (query, value) => { + return query.find('section').containing(section => section.find('h1').containing(value).shouldExist()) + }) + + await browser.shouldContain({ + 'Section(/S.* A/)': { + '.content': 'Section A Content' + } }) }) @@ -499,18 +535,18 @@ describe('assert', function () { `) browser.define({ - title: 'h1', - content: q => q.find('.content').define({ - title: '.title', - body: '.body' + Title: 'h1', + Content: q => q.find('.content').define({ + Title: '.title', + Body: '.body' }), }) await browser.shouldContain({ - title: 'Title', - content: { - title: 'title', - body: 'body' + Title: 'Title', + Content: { + Title: 'title', + Body: 'body' } }) }) diff --git a/tsconfig.json b/tsconfig.json index c3529c0..02bb0b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,11 +2,13 @@ "compilerOptions": { "lib": ["es2015", "dom"], "noImplicitAny": false, + "esModuleInterop": true, "module": "commonjs", "target": "ES5", "outDir": "dist", "rootDir": ".", - "allowJs": true + "allowJs": true, + "jsx": "react" }, "exclude": [ "node_modules", From b3dd6be586d2e99a669e1a95e94154ceaa7ef123 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Tue, 17 Dec 2019 11:28:34 +0100 Subject: [PATCH 033/127] renamed element and elements to elementResult and elementsResult * seems we need to have a convention for things that return queries and things that return results --- create.js | 1 - lib/Query.ts | 304 +++++++++++++++++++------------------- test/containingSpec.js | 2 +- test/defineSpec.js | 43 ++++++ test/labelSpec.js | 154 +++++++++++++++++++ test/querySpec.js | 20 +-- test/reloadButtonSpec.js | 2 +- test/setSpec.js | 6 +- test/shouldAppearSpec.js | 87 +++++++++++ test/shouldContainSpec.js | 14 +- test/submitSpec.js | 63 ++++++++ test/valueSpec.js | 66 +++++++++ 12 files changed, 591 insertions(+), 171 deletions(-) create mode 100644 test/defineSpec.js create mode 100644 test/labelSpec.js create mode 100644 test/shouldAppearSpec.js create mode 100644 test/submitSpec.js create mode 100644 test/valueSpec.js diff --git a/create.js b/create.js index 1e99694..6806b74 100644 --- a/create.js +++ b/create.js @@ -2,7 +2,6 @@ var Query = require('./lib/Query') module.exports = function (rootSelector = document.body) { return new Query() - .installSetters() .scope(rootSelector) .component({ v2: function () { diff --git a/lib/Query.ts b/lib/Query.ts index 8e19404..8ed7fbf 100644 --- a/lib/Query.ts +++ b/lib/Query.ts @@ -35,7 +35,7 @@ interface Definitions { fields: {} } -class Query { +class Query implements PromiseLike { private _transforms: Transform[] private _options: Options private _input: any @@ -52,7 +52,116 @@ class Query { timeout: 1000, interval: 10, definitions: { - fieldTypes: [], + fieldTypes: [ + { + setter: (query, value) => { + return query + .is('input[type=checkbox]') + .expectOneElement() + .transform(([checkbox]) => { + if (typeof value !== 'boolean') { + throw new Error('expected boolean as argument to set checkbox') + } + return () => { + if (query._dom.checked(checkbox) !== value) { + debug('checkbox', checkbox, value) + query._dom.click(checkbox) + } + } + }) + }, + value: (query) => { + return query + .is('input[type=checkbox]') + .expectOneElement() + .transform(([checkbox]) => { + return query._dom.checked(checkbox) + }) + } + }, + { + setter: (query, value) => { + return query + .is('select') + .expectOneElement('expected to be select element') + .css('option') + .filter(o => { + return o.value === value || query._dom.elementInnerText(o) === value + }, `option with text or value ${JSON.stringify(value)}`) + .expectOneElement(`expected one option element with text or value ${JSON.stringify(value)}`) + .transform(function ([option]) { + return () => { + const selectElement = option.parentNode + debug('select', selectElement) + query._dom.selectOption(selectElement, option) + } + }) + }, + valueAsserter: (query, expected) => { + return query + .is('select') + .expectOneElement('expected to be select element') + .transform(([select]) => { + return () => { + const value = select.value + + if (testEqual(value, expected)) { + return + } + + const selectedOption = select.options[select.selectedIndex] + if (selectedOption) { + const actual = query._dom.elementInnerText(selectedOption) + if (testEqual(actual, expected)) { + return + } else { + this.error('expected ' + inspect(actual) + ' or ' + inspect(value) + ' to equal ' + inspect(expected), { actual, expected }) + } + } + + this.error('expected ' + inspect(value) + ' to equal ' + inspect(expected), { actual: value, expected }) + } + }) + }, + value: (query) => { + return query + .is('select') + .expectOneElement('expected to be select element') + .transform(([select]) => { + const selectedOption = select.options[select.selectedIndex] + return selectedOption && query._dom.elementInnerText(selectedOption) + }) + } + }, + { + setter: (query, value) => { + return query + .is(inputSelectors.settable) + .expectOneElement() + .transform(([element]) => { + if (typeof value !== 'string') { + throw new Error('expected string as argument to set input') + } + return () => { + debug('set', element, value) + query._dom.enterText(element, value, {incremental: false}) + } + }) + }, + value: (query) => { + return query.is(inputSelectors.gettable).expectOneElement().transform(([input]) => { + return input.value + }) + } + }, + { + value: (query) => { + return query.expectOneElement().transform(([element]) => { + return query._dom.elementInnerText(element) + }) + } + }, + ], button: [ (query: Query, name: string): Query => { return query.css('button, input[type=button], a').containing(name) @@ -87,6 +196,7 @@ class Query { fields: { Label: (q, value) => q.label(value), Button: (q, value) => q.button(value), + Css: (q, value) => q.css(value), }, } } @@ -94,6 +204,10 @@ class Query { this._input = undefined } + public get [Symbol.toStringTag](): string { + return 'Query'; + } + public transform (transform: Transform): this { return this.clone(clone => clone._transforms.push(transform)) } @@ -319,7 +433,7 @@ class Query { } } - public then (...args): Promise { + public then (resolve: (r) => any, reject: (e) => any): Promise { this.assertHasActionOrExpectation() var self = this @@ -343,7 +457,7 @@ class Query { throw error }) - return promise.then.apply(promise, args) + return promise.then.call(promise, resolve, reject) } public catch (fn): Promise { @@ -414,12 +528,12 @@ class Query { }) } - public element (): HTMLElement { + public elementResult (): HTMLElement { return this.expectOneElement().result()[0] } - public elements (): HTMLElement { - return this.expectSomeElements().result()[0] + public elementsResult (): HTMLElement { + return this.expectSomeElements().result() } public expectSomeElements (message?: string): this { @@ -439,11 +553,22 @@ class Query { }) } - public submit (): this { - return this.expectOneElement().action(([element]) => { - debug('submit', element) - this._dom.submit(element) - }) + public clickButton (name: string): this { + return this.button(name).click() + } + + public submit (selector?: string): this { + return (selector ? this.find(selector) : this) + .expectOneElement() + .expect(([element]) => { + if (!element.form) { + throw new BrowserMonkeyAssertionError('expected element to be inside a form for submit') + } + }) + .action(([element]) => { + debug('submit', element) + this._dom.submit(element) + }) } public scope (element: HTMLElement): this { @@ -482,10 +607,6 @@ class Query { }, 'enabled') } - public clickButton (name: string): this { - return this.button(name).click() - } - public set (selector, value): this { const model = value === undefined ? selector @@ -507,7 +628,7 @@ class Query { }, expectOne: (query) => { - expectLength(query, 1).result() + query.expectOneElement().result() }, function: (query, model) => { @@ -532,6 +653,18 @@ class Query { return this.expectNoElements() } + public async shouldAppearAfter (action: () => void): Promise { + await this.shouldNotExist() + await action() + await this.shouldExist() + } + + public async shouldDisappearAfter (action: () => void): Promise { + await this.shouldExist() + await action() + await this.shouldNotExist() + } + public shouldContain (model): this { return this.expect(elements => { let isError = false @@ -543,7 +676,7 @@ class Query { expectOne: (query) => { try { - expectLength(query, 1).result() + query.expectOneElement().result() return {} } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { @@ -625,13 +758,13 @@ class Query { return this } - public setter (model): this { + private setter (model): this { return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.setter).map(def => { return query => def.setter(query, model) })) } - public valueAsserter (expected: any): this { + private valueAsserter (expected: any): this { return this.firstOf(this._options.definitions.fieldTypes.filter(def => def.value).map(def => { return query => def.valueAsserter ? def.valueAsserter(query, expected) @@ -698,124 +831,6 @@ class Query { })) } - public installSetters (): this { - this.define('css', (query, css) => query.css(css)) - - this.defineFieldType({ - value: (query) => { - return query.expectOneElement().transform(([element]) => { - return query._dom.elementInnerText(element) - }) - } - }) - - this.defineFieldType({ - setter: (query, value) => { - return query - .is(inputSelectors.settable) - .expectOneElement() - .transform(([element]) => { - if (typeof value !== 'string') { - throw new Error('expected string as argument to set input') - } - return () => { - debug('set', element, value) - query._dom.enterText(element, value, {incremental: false}) - } - }) - }, - value: (query) => { - return query.is(inputSelectors.gettable).expectOneElement().transform(([input]) => { - return input.value - }) - } - }) - - this.defineFieldType({ - setter: (query, value) => { - return query - .is('select') - .expectOneElement('expected to be select element') - .css('option') - .filter(o => { - return o.value === value || query._dom.elementInnerText(o) === value - }, `option with text or value ${JSON.stringify(value)}`) - .expectOneElement(`expected one option element with text or value ${JSON.stringify(value)}`) - .transform(function ([option]) { - return () => { - const selectElement = option.parentNode - debug('select', selectElement) - query._dom.selectOption(selectElement, option) - } - }) - }, - valueAsserter: (query, expected) => { - return query - .is('select') - .expectOneElement('expected to be select element') - .transform(([select]) => { - return () => { - const value = select.value - - if (testEqual(value, expected)) { - return - } - - const selectedOption = select.options[select.selectedIndex] - if (selectedOption) { - const actual = query._dom.elementInnerText(selectedOption) - if (testEqual(actual, expected)) { - return - } else { - this.error('expected ' + inspect(actual) + ' or ' + inspect(value) + ' to equal ' + inspect(expected), { actual, expected }) - } - } - - this.error('expected ' + inspect(value) + ' to equal ' + inspect(expected), { actual: value, expected }) - } - }) - }, - value: (query) => { - return query - .is('select') - .expectOneElement('expected to be select element') - .transform(([select]) => { - const selectedOption = select.options[select.selectedIndex] - return selectedOption && query._dom.elementInnerText(selectedOption) - }) - } - }) - - this.defineFieldType({ - setter: (query, value) => { - return query - .is('input[type=checkbox]') - .expectOneElement() - .transform(([checkbox]) => { - if (typeof value !== 'boolean') { - throw new Error('expected boolean as argument to set checkbox') - } - return () => { - if (query._dom.checked(checkbox) !== value) { - debug('checkbox', checkbox, value) - query._dom.click(checkbox) - } - } - }) - }, - value: (query) => { - return query - .is('input[type=checkbox]') - .expectOneElement() - .transform(([checkbox]) => { - return query._dom.checked(checkbox) - }) - } - }) - - return this - } - public css (selector: string): this { const findElements = this.transform(elements => { expectElements(elements) @@ -933,15 +948,6 @@ function runQueryCreator (queryCreator: (q: Query) => Query, query: Query): Quer const missing = {} -function expectLength (query, length): Query { - return query.expect(elements => { - const actualLength = elements.length - if (actualLength !== length) { - query.error('expected ' + length + ' ' + pluralize('elements', length) + ', found ' + actualLength) - } - }) -} - interface Actions { arrayLengthError (q: Query, actual: number, expected: number): void function (q: Query, model: any): any @@ -980,7 +986,7 @@ function mapModel (query: Query, model: any, actions: Actions): any { return map(query.index(index), item) }) } else if (model.constructor === Object) { - const lengthQuery = expectLength(query, 1) + const lengthQuery = query.expectOneElement() const keys = Object.keys(model) @@ -995,7 +1001,7 @@ function mapModel (query: Query, model: any, actions: Actions): any { } else if (typeof model === 'function') { return actions.function(query, model) } else { - return actions.value(expectLength(query, 1), model) + return actions.value(query.expectOneElement(), model) } } diff --git a/test/containingSpec.js b/test/containingSpec.js index ef18cc8..3425bd4 100644 --- a/test/containingSpec.js +++ b/test/containingSpec.js @@ -89,7 +89,7 @@ describe('containing', function () { expect(() => browser.find('.result').containing({'.title': 'Title', '.body': 'None'}).expectOneElement().result() - ).to.throw(`expected just one element, found 0 (found: path(find('.result') [3], containing(...expected 1 element, found 0 (found: path(find('.title') [0]))) [0]))`) + ).to.throw(`expected just one element, found 0 (found: path(find('.result') [3], containing(...expected just one element, found 0 (found: path(find('.title') [0]))) [0]))`) }) }) diff --git a/test/defineSpec.js b/test/defineSpec.js new file mode 100644 index 0000000..bdfef7e --- /dev/null +++ b/test/defineSpec.js @@ -0,0 +1,43 @@ +const describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +const {expect} = require('chai') + +describe('define', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + + it('can define a field', () => { + browser.define('Hello', q => q.transform(() => 'hello')) + + expect(browser.find('Hello').result()).to.equal('hello') + }) + + it('can define a field with arguments', () => { + let args + + browser.define('Hello', (q, arg1, arg2) => q.transform(() => { + args = [arg1, arg2] + return `hello ${arg1} ${arg2}` + })) + + expect(browser.find('Hello("one", 2)').result()).to.equal('hello one 2') + expect(args).to.eql(['one', 2]) + }) + + describe('definition not found', () => { + it('with arguments: throws an error if the definition is not found', () => { + expect(() => browser.find('Hello("one", 2)')).to.throw('no such definition Hello') + }) + + it('without arguments: assumes that it is CSS', () => { + expect(() => browser.find('Hello').shouldExist().result()).to.throw("expected one or more elements, found 0 (found: find('Hello') [0])") + }) + }) + }) +}) diff --git a/test/labelSpec.js b/test/labelSpec.js new file mode 100644 index 0000000..2c8d2d0 --- /dev/null +++ b/test/labelSpec.js @@ -0,0 +1,154 @@ +const describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +const {expect} = require('chai') + +describe('labels', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + + function assertFoundElementByLabel(html, label) { + assembly.insertHtml(html) + + const expected = assembly.find('.result') + expect(expected, 'expected to HTML to have element with class="result"').to.not.eql(null) + + const [found] = browser.find(label).result() + + expect(found).to.equal(expected) + } + + function assertNothingFound(html, label) { + assembly.insertHtml(html) + + browser.find(label).expectNoElements().result() + } + + describe('finding elements by their label', () => { + it('checkbox with label as parent', async () => { + assertFoundElementByLabel( + ` + + `, + 'Label("Feature Enabled")' + ) + }) + + it('checkbox pointed to by label with for attribute', async () => { + assertFoundElementByLabel( + ` + + + `, + 'Label("Feature Enabled")' + ) + }) + + it('nothing found if the id is wrong', async () => { + assertNothingFound( + ` + + + `, + 'Label("Feature Enabled")' + ) + }) + }) + + describe('finding things by their aria labeling', () => { + it('can find an element by its aria-label', () => { + assertFoundElementByLabel( + ` + + `, + 'Label("Search")' + ) + }) + + it('finds nothing if the label does not match', () => { + assertNothingFound( + ` + + `, + 'Label("Search")' + ) + }) + + it('can find an element by its aria-labelledby', () => { + assertFoundElementByLabel( + ` + + + `, + 'Label("Search")' + ) + }) + + it('nothing found if aria-labelledby does not resolve', () => { + assertNothingFound( + ` + + + `, + 'Label("Search")' + ) + }) + + it('nothing found if label pointed to by aria-labelledby does not match', () => { + assertNothingFound( + ` + + + `, + 'Label("Search")' + ) + }) + }) + + it('finds multiple elements', () => { + assembly.insertHtml( + ` + + + ` + ) + + const expected = assembly.findAll('.result') + + const found = browser.find('Label("Feature Enabled")').result() + + expect(found).to.eql(expected) + }) + + describe('defineLabel', () => { + it('can define a new way of finding labels', () => { + browser.defineLabel((query, label) => query.css(`[data-label=${JSON.stringify(label)}]`)) + + assertFoundElementByLabel( + ` +
+ `, + 'Label("Search")' + ) + }) + }) + }) +}) diff --git a/test/querySpec.js b/test/querySpec.js index 5d60fec..3f64bcb 100644 --- a/test/querySpec.js +++ b/test/querySpec.js @@ -170,7 +170,7 @@ describe('query', () => { }) }) - describe('element', () => { + describe('elementResult', () => { it('expects one html element and returns it', () => { const expected = assembly.insertHtml(`
@@ -178,7 +178,7 @@ describe('query', () => { const actual = browserMonkey .find('.title') - .element() + .elementResult() expect(actual).to.equal(expected) }) @@ -191,7 +191,7 @@ describe('query', () => { expect(() => browserMonkey .find('.title') - .element()).to.throw('expected just one element, found 2') + .elementResult()).to.throw('expected just one element, found 2') }) it('fails if there are no elements', () => { @@ -200,22 +200,24 @@ describe('query', () => { expect(() => browserMonkey .find('.title') - .element()).to.throw('expected just one element, found 0') + .elementResult()).to.throw('expected just one element, found 0') }) }) - describe('elements', () => { + describe('elementsResult', () => { it('expects one or more html elements and returns them', () => { - const expected = assembly.insertHtml(` + const div1 = assembly.insertHtml(`
+ `) + const div2 = assembly.insertHtml(`
`) const actual = browserMonkey .find('.title') - .elements() + .elementsResult() - expect(actual).to.equal(expected) + expect(actual).to.eql([div1, div2]) }) it('fails if there are no elements', () => { @@ -224,7 +226,7 @@ describe('query', () => { expect(() => browserMonkey .find('.title') - .elements()).to.throw('expected one or more elements, found 0') + .elementsResult()).to.throw('expected one or more elements, found 0') }) }) diff --git a/test/reloadButtonSpec.js b/test/reloadButtonSpec.js index d486cac..d6bfe12 100644 --- a/test/reloadButtonSpec.js +++ b/test/reloadButtonSpec.js @@ -16,7 +16,7 @@ describe('reload button', function () { it('adds reload button that reloads to the initial url', function () { reloadButton() return browser.find('a').containing('⟳ reload').shouldContain(q => { - expect(q.element().getAttribute('href')).to.equal(window.location.href) + expect(q.elementResult().getAttribute('href')).to.equal(window.location.href) }) }, { vdom: false }) diff --git a/test/setSpec.js b/test/setSpec.js index 733a3c7..c17f3be 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -122,7 +122,7 @@ describe('set', function () { '.street': '7 Lola St', '.city': 'Frisby City', }, - }), "expected 1 element, found 2 (found: path(find('.address') [2]))") + }), "expected just one element, found 2 (found: path(find('.address') [2]))") }) it('a value does assert that there is only one matching element', async () => { @@ -141,7 +141,7 @@ describe('set', function () { '.street': '7 Lola St', '.city': 'Frisby City', }, - }), "expected 1 element, found 2 (found: path(find('.address') [1], find('.street') [2]))") + }), "expected just one element, found 2 (found: path(find('.address') [1], find('.street') [2]))") }) it('deep fields disambiguate', async () => { @@ -276,7 +276,7 @@ describe('set', function () { '.phone': '123123123', '.first-name': 'Barry' }) - await assembly.assertRejection(promise, "expected 1 element, found 0 (found: path(find('form') [1], find('.street') [0]))") + await assembly.assertRejection(promise, "expected just one element, found 0 (found: path(find('form') [1], find('.street') [0]))") demand(assembly.find('.phone').value).to.equal('') demand(assembly.find('.first-name').value).to.equal('') diff --git a/test/shouldAppearSpec.js b/test/shouldAppearSpec.js new file mode 100644 index 0000000..ec6cfce --- /dev/null +++ b/test/shouldAppearSpec.js @@ -0,0 +1,87 @@ +var describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +var demand = require('must') +const {expect} = require('chai') + +describe('buttons', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + + describe('shouldAppearAfter', () => { + it("passes if an element doesn't exist, but after the action, it does", async () => { + const button = assembly.insertHtml(` + + `) + + button.addEventListener('click', () => assembly.insertHtml('
')) + + await browser.find('.element').shouldAppearAfter(() => browser.clickButton('Show')) + }) + + it('fails if the element fails to appear', async () => { + assembly.insertHtml(` + + `) + + await assembly.assertRejection( + browser.find('.element').shouldAppearAfter(() => browser.clickButton('Show')), + 'expected one or more elements, found 0' + ) + }) + + it('fails if the element exists before the action', async () => { + assembly.insertHtml(` +
+ `) + + await assembly.assertRejection( + browser.find('.element').shouldAppearAfter(() => browser.clickButton('Show')), + 'expected no elements' + ) + }) + }) + + describe('shouldDisappearAfter', () => { + it("passes if an element does exist, but after the action, it doesn't", async () => { + const button = assembly.insertHtml(` + + `) + const element = assembly.insertHtml(` +
+ `) + + button.addEventListener('click', () => element.parentNode.removeChild(element)) + + await browser.find('.element').shouldDisappearAfter(() => browser.clickButton('Show')) + }) + + it('fails if the element is still there after the action', async () => { + assembly.insertHtml(` + +
+ `) + + await assembly.assertRejection( + browser.find('.element').shouldDisappearAfter(() => browser.clickButton('Show')), + 'expected no elements, found 1' + ) + }) + + it("fails if the element doesn't exist before the action", async () => { + assembly.insertHtml(` + `) + + await assembly.assertRejection( + browser.find('.element').shouldDisappearAfter(() => browser.clickButton('Show')), + 'expected one or more elements, found 0' + ) + }) + }) + }) +}) diff --git a/test/shouldContainSpec.js b/test/shouldContainSpec.js index 712c2c9..8981796 100644 --- a/test/shouldContainSpec.js +++ b/test/shouldContainSpec.js @@ -52,7 +52,7 @@ describe('shouldContain', function () { await assembly.assertExpectedActual(browser, { '.address': '12 Hapless Boulevard', }, { - '.address': "Error: expected 1 element, found 0 (found: path(find('.address') [0]))", + '.address': "Error: expected just one element, found 0 (found: path(find('.address') [0]))", }) }) @@ -184,7 +184,7 @@ describe('shouldContain', function () { '.content': 'The Content' }, { 'h1': 'Title', - '.content': "Error: expected 1 element, found 0 (found: path(find('.content') [0]))" + '.content': "Error: expected just one element, found 0 (found: path(find('.content') [0]))" }) }) @@ -207,7 +207,7 @@ describe('shouldContain', function () { '.result': [ { 'h1': 'Title', - '.content': "Error: expected 1 element, found 0 (found: path(find('.result') [2], index 0 [1], find('.content') [0]))" + '.content': "Error: expected just one element, found 0 (found: path(find('.result') [2], index 0 [1], find('.content') [0]))" }, 'The Content' ] @@ -241,7 +241,7 @@ describe('shouldContain', function () { '.content': {} }, { 'h1': 'Title', - '.content': "Error: expected 1 element, found 0 (found: path(find('.content') [0]))" + '.content': "Error: expected just one element, found 0 (found: path(find('.content') [0]))" }) }) }) @@ -385,7 +385,7 @@ describe('shouldContain', function () { await browser.shouldContain({ 'h1': 'Title', '.content': query => { - if (!/Content/.test(query.element().innerText)) { + if (!/Content/.test(query.elementResult().innerText)) { throw new BrowserMonkeyAssertionError('asdf') } } @@ -403,7 +403,7 @@ describe('shouldContain', function () { await assembly.assertExpectedActual(browser, { 'h1': 'Title', '.content': query => { - if (/Content/.test(query.element().innerText)) { + if (/Content/.test(query.elementResult().innerText)) { throw new BrowserMonkeyAssertionError('asdf') } } @@ -511,7 +511,7 @@ describe('shouldContain', function () { `) browser.define('Section', (query, value) => { - return query.find('section').containing(section => section.find('h1').containing(value).shouldExist()) + return query.find('section').containing({h1: value}) }) await browser.shouldContain({ diff --git a/test/submitSpec.js b/test/submitSpec.js new file mode 100644 index 0000000..46229d4 --- /dev/null +++ b/test/submitSpec.js @@ -0,0 +1,63 @@ +var describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') +var demand = require('must') +const {expect} = require('chai') + +describe('submit', function () { + describeAssemblies([DomAssembly], function (Assembly) { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey() + }) + + async function assertCanSubmitAForm (html, action) { + const events = [] + assembly.insertHtml(html) + const button = assembly.find('.target') + button.addEventListener('submit', () => events.push('submit')) + + await action() + expect(events).to.eql(['submit']) + } + + async function assertSubmitFailure (html, action, rejection) { + assembly.insertHtml(html) + await assembly.assertRejection(action(), rejection) + } + + it('can submit a form', async () => { + await assertCanSubmitAForm( + ` + + + + `, + () => browser.find('input').submit() + ) + }) + + it('can submit a form with a selector', async () => { + await assertCanSubmitAForm( + ` +
+ +
+ `, + () => browser.submit('input') + ) + }) + + it('fails if the element is not in a form', async () => { + await assertSubmitFailure( + ` + + `, + () => browser.submit('input'), + 'expected element to be inside a form for submit' + ) + }) + }) +}) diff --git a/test/valueSpec.js b/test/valueSpec.js new file mode 100644 index 0000000..de0c9c4 --- /dev/null +++ b/test/valueSpec.js @@ -0,0 +1,66 @@ +var expect = require('chai').expect +var describeAssemblies = require('./describeAssemblies') +const DomAssembly = require('./assemblies/DomAssembly') + +describe('value', function () { + describeAssemblies([DomAssembly], Assembly => { + var assembly + var browser + + beforeEach(function () { + assembly = new Assembly() + browser = assembly.browserMonkey().scope(document.body) + }) + + function assertValue (html, expectedValue) { + assembly.insertHtml(html) + + const value = browser.find('.element').expectOneElement().value().result() + expect(value).to.equal(expectedValue) + } + + describe('text inputs', () => { + it('can get value of input', () => { + assertValue('', 'the value') + }) + }) + + describe('HTML elements', () => { + it('returns the innerHTML', () => { + assertValue('
this is the inner text
', 'this is the inner text') + }) + }) + + describe('checkboxes', () => { + it('when checked, returns true', () => { + assertValue('', true) + }) + + it('when unchecked, returns false', () => { + assertValue('', false) + }) + + it("when indeterminate, returns 'indeterminate'", () => { + const checkbox = assembly.insertHtml('') + checkbox.indeterminate = true + + const value = browser.find('.element').expectOneElement().value().result() + expect(value).to.equal('indeterminate') + }) + }) + + describe('selects', () => { + it('returns the text of the option that is selected', () => { + assertValue( + ` + + `, + 'two' + ) + }) + }) + }) +}) From ab9b34787ca3b9ec3506d0ff51ef00b35b14a0f9 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Tue, 24 Dec 2019 12:02:56 +0000 Subject: [PATCH 034/127] can assert HTML attributes * some internal refactoring on allowing actual and expected from AssertionExceptions to be returned from shouldContain --- create.js | 2 +- lib/Query.ts | 296 +++++++++++------- package.json | 6 +- test/actionsSpec.js | 2 +- .../{DomAssembly.js => DomAssembly.ts} | 94 ++++-- test/assemblies/domAssemblySpec.js | 2 +- test/assertionsSpec.js | 2 +- test/buttonSpec.js | 28 +- test/componentSpec.js | 2 +- test/containingSpec.js | 25 +- test/defineSpec.js | 2 +- test/domSpec.js | 2 +- test/eventsSpec.js | 2 +- test/findersSpec.js | 2 +- test/hyperdomIntegrationSpec.js | 2 +- test/{labelSpec.js => labelSpec.ts} | 52 ++- test/mountSpec.js | 2 +- test/optionsSpec.js | 2 +- test/querySpec.js | 2 +- test/reloadButtonSpec.js | 2 +- test/scopeSpec.js | 2 +- test/setSpec.js | 2 +- test/shouldAppearSpec.js | 2 +- test/shouldContainSpec.js | 100 +++++- test/submitSpec.js | 2 +- test/valueSpec.js | 2 +- 26 files changed, 447 insertions(+), 192 deletions(-) rename test/assemblies/{DomAssembly.js => DomAssembly.ts} (53%) rename test/{labelSpec.js => labelSpec.ts} (72%) diff --git a/create.js b/create.js index 6806b74..b48d6b7 100644 --- a/create.js +++ b/create.js @@ -1,4 +1,4 @@ -var Query = require('./lib/Query') +var {Query} = require('./lib/Query') module.exports = function (rootSelector = document.body) { return new Query() diff --git a/lib/Query.ts b/lib/Query.ts index 8ed7fbf..fa3d56c 100644 --- a/lib/Query.ts +++ b/lib/Query.ts @@ -19,6 +19,7 @@ var inputSelectors = require('./inputSelectors') const object = require('lowscore/object') const range = require('lowscore/range') var flatten = require('lowscore/flatten') +import {match} from './match' type Transform = (elements: any, executedTransforms: ExecutedTransform[]) => any type Action = (elements: any, executedTransforms: ExecutedTransform[]) => void @@ -28,14 +29,20 @@ interface FieldType { valueAsserter?: (query: Query, expected: any) => Query } +type LabelName = string | RegExp +type LabelDefinition = (query: Q, name: LabelName) => Q +type FieldDefinition = (query: Q, ...any) => Q + interface Definitions { fieldTypes: FieldType[] - button: ((query: Query, name: string) => Query)[] - label: ((query: Query, name: string) => Query)[] - fields: {} + button: LabelDefinition[] + label: ({name: string, definition: LabelDefinition})[] + fields: { + [key: string]: FieldDefinition + } } -class Query implements PromiseLike { +export class Query implements PromiseLike { private _transforms: Transform[] private _options: Options private _input: any @@ -84,12 +91,12 @@ class Query implements PromiseLike { return query .is('select') .expectOneElement('expected to be select element') - .css('option') + .findCss('option') .filter(o => { return o.value === value || query._dom.elementInnerText(o) === value }, `option with text or value ${JSON.stringify(value)}`) .expectOneElement(`expected one option element with text or value ${JSON.stringify(value)}`) - .transform(function ([option]) { + .transform(([option]) => { return () => { const selectElement = option.parentNode debug('select', selectElement) @@ -105,14 +112,14 @@ class Query implements PromiseLike { return () => { const value = select.value - if (testEqual(value, expected)) { + if (match(expected, value).isMatch) { return } const selectedOption = select.options[select.selectedIndex] if (selectedOption) { const actual = query._dom.elementInnerText(selectedOption) - if (testEqual(actual, expected)) { + if (match(expected, actual).isMatch) { return } else { this.error('expected ' + inspect(actual) + ' or ' + inspect(value) + ' to equal ' + inspect(expected), { actual, expected }) @@ -163,40 +170,52 @@ class Query implements PromiseLike { }, ], button: [ - (query: Query, name: string): Query => { - return query.css('button, input[type=button], a').containing(name) + (query, name) => { + return query.findCss('button, input[type=button], a').containing(name) }, ], label: [ - (query, name) => { - return query.find('label').containing(name).find('input') + { + name: 'label', + definition: (query, name) => { + return query.find('label').containing(name).find('input') + }, }, - (query, name) => { - return query.find('label[for]').containing(name).map(label => { - const id = label.getAttribute('for') - return label.ownerDocument.getElementById(id) - }, 'for attribute').filter(Boolean) + { + name: 'label-for', + definition: (query, name) => { + return query.find('label[for]').containing(name).map(label => { + const id = label.getAttribute('for') + return label.ownerDocument.getElementById(id) + }, 'for attribute').filter(Boolean) + }, }, - (query, name) => { - return query.find('[aria-label]').filter(element => { - const label = element.getAttribute('aria-label') - return label === name - }, 'aria-label') + { + name: 'aria-label', + definition: (query, name) => { + return query.find('[aria-label]').filter(element => { + const label = element.getAttribute('aria-label') + return label === name + }, 'aria-label') + }, }, - (query, name) => { - return query.find('[aria-labelledby]').filter(element => { - const id = element.getAttribute('aria-labelledby') - const labelElement = element.ownerDocument.getElementById(id) - if (labelElement) { - return query._dom.elementInnerText(labelElement) === name - } - }, 'aria-label') + { + name: 'aria-labelledby', + definition: (query, name) => { + return query.find('[aria-labelledby]').filter(element => { + const id = element.getAttribute('aria-labelledby') + const labelElement = element.ownerDocument.getElementById(id) + if (labelElement) { + return query._dom.elementInnerText(labelElement) === name + } + }, 'aria-label') + }, }, ], fields: { - Label: (q, value) => q.label(value), - Button: (q, value) => q.button(value), - Css: (q, value) => q.css(value), + Label: (q, value) => q.findLabel(value), + Button: (q, value) => q.findButton(value), + Css: (q, value) => q.findCss(value), }, } } @@ -233,7 +252,7 @@ class Query implements PromiseLike { }) } - public button (name: string): this { + public findButton (name: LabelName): this { return this.concat(this._options.definitions.button.map(definition => { return (q: Query): Query => { return definition(q, name) @@ -241,20 +260,32 @@ class Query implements PromiseLike { })) } - public label (name: string): this { - return this.concat(this._options.definitions.label.map(definition => { + public findLabel (name: string): this { + return this.concat(this._options.definitions.label.map(({definition}) => { return (q: Query): Query => { return definition(q, name) } })) } - public defineButton (definition: (q: Query, name?: string) => Query): void { + public addButtonDefinition (definition: LabelDefinition): void { this._options.definitions.button.push(definition) } - public defineLabel (definition: (q: Query, name?: string) => Query): void { - this._options.definitions.label.push(definition) + public addLabelDefinition (name: string | LabelDefinition, definition?: LabelDefinition): void { + if (!definition) { + definition = name as LabelDefinition + name = undefined + } + + this._options.definitions.label.push({name: name as string, definition}) + } + + public removeLabelDefinition (name: string): void { + const index = this._options.definitions.label.findIndex(def => def.name === name) + if (index >= 0) { + this._options.definitions.label.splice(index, 1) + } } public result (): any { @@ -440,15 +471,15 @@ class Query implements PromiseLike { var retry = retryFromOptions(this._options) var originalStack = new Error().stack - var promise = Promise.resolve(retry(function () { + var promise = Promise.resolve(retry(() => { return self.execute().value - })).catch(function (error) { + })).catch(error => { if (error instanceof BrowserMonkeyAssertionError) { error.stack = originalStack if (debug.enabled) { debug('assertion error', error.message) - error.executedTransforms.transforms.forEach(function (transform) { + error.executedTransforms.transforms.forEach(transform => { transform.print() }) } @@ -485,7 +516,7 @@ class Query implements PromiseLike { } Component.prototype = new (this.constructor as any)() - Object.keys(methods).forEach(function (method) { + Object.keys(methods).forEach(method => { var descriptor = Object.getOwnPropertyDescriptor(methods, method) if (descriptor) { Object.defineProperty(Component.prototype, method, descriptor) @@ -537,7 +568,7 @@ class Query implements PromiseLike { } public expectSomeElements (message?: string): this { - return this.expect(function (elements) { + return this.expect(elements => { expectElements(elements) if (elements.length < 1) { @@ -553,8 +584,8 @@ class Query implements PromiseLike { }) } - public clickButton (name: string): this { - return this.button(name).click() + public clickButton (name: LabelName): this { + return this.findButton(name).click() } public submit (selector?: string): this { @@ -607,32 +638,38 @@ class Query implements PromiseLike { }, 'enabled') } - public set (selector, value): this { - const model = value === undefined - ? selector - : { - [selector]: value - } - - return this.action(function (elements) { + public set (model: any): this { + return this.action(elements => { const setters = [] const actions = { - arrayLengthError: (query, actualLength, expectedLength) => { + arrayLengthError: (query, actualLength, expectedLength): void => { query.error('expected ' + expectedLength + ' ' + pluralize('elements', expectedLength) + ', found ' + actualLength) }, - value: (query, model) => { + value: (query, model): ActualExpected => { var setter = query.setter(model).result() setters.push(() => setter()) + return { + actual: undefined, + expected: undefined, + } }, - expectOne: (query) => { + expectOne: (query): ActualExpected => { query.expectOneElement().result() + return { + actual: {}, + expected: {}, + } }, - function: (query, model) => { + function: (query, model): ActualExpected => { setters.push(() => this.runModelFunction(model, query)) + return { + actual: undefined, + expected: undefined, + } } } @@ -670,25 +707,28 @@ class Query implements PromiseLike { let isError = false const actions = { - arrayLengthError: () => { + arrayLengthError: (): void => { isError = true }, - expectOne: (query) => { + expectOne: (query): ActualExpected => { try { query.expectOneElement().result() - return {} + return {actual: {}, expected: {}} } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { isError = true - return 'Error: ' + e.message + return { + actual: 'Error: ' + e.message, + expected: {}, + } } else { throw e } } }, - value: (query, model) => { + value: (query, model): ActualExpected => { let valueAsserter try { @@ -696,7 +736,10 @@ class Query implements PromiseLike { } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { isError = true - return 'Error: ' + e.message + return { + actual: e, + expected: model, + } } else { throw e } @@ -707,23 +750,35 @@ class Query implements PromiseLike { } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { isError = true - return e.actual === undefined ? 'Error: ' + e.message : e.actual + return { + actual: e.actual, + expected: e.expected, + } } else { throw e } } - return model + return { + actual: model, + expected: model, + } }, - function: (query, model) => { + function: (query, model): ActualExpected => { try { this.runModelFunction(model, query) - return model + return { + actual: model, + expected: model, + } } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { isError = true - return 'Error: ' + e.message + return { + actual: e.actual, + expected: e.expected, + } } else { throw e } @@ -735,23 +790,23 @@ class Query implements PromiseLike { const result = mapModel(clone, model, actions) if (isError) { - this.error('could not match', {expected: model, actual: result}) + this.error('could not match', {expected: result.expected, actual: result.actual}) } }) } public index (index: number): this { - return this.transform(function (elements) { + return this.transform(elements => { return new ExecutedSimpleTransform([elements[index]], 'index ' + index) }) } - public define (name, finder): this { - if (typeof finder === 'function') { - this._options.definitions.fields[name] = finder - } else if (typeof finder === 'string') { - this._options.definitions.fields[name] = q => q.find(finder) - } else if (name.constructor === Object && finder === undefined) { + public define (name: string, fieldDefinition): this { + if (typeof fieldDefinition === 'function') { + this._options.definitions.fields[name] = fieldDefinition + } else if (typeof fieldDefinition === 'string') { + this._options.definitions.fields[name] = q => q.find(fieldDefinition) + } else if (name.constructor === Object && fieldDefinition === undefined) { Object.keys(name).forEach(key => this.define(key, name[key])) } @@ -770,7 +825,7 @@ class Query implements PromiseLike { ? def.valueAsserter(query, expected) : def.value(query).transform(actual => { return () => { - if (!testEqual(actual, expected)) { + if (!match(actual, expected).isMatch) { this.error('expected ' + inspect(actual) + ' to equal ' + inspect(expected), { actual, expected }) } } @@ -785,21 +840,31 @@ class Query implements PromiseLike { public containing (model: any): this { return this.transform(elements => { const actions = { - arrayLengthError: (query, actualLength, expectedLength) => { + arrayLengthError: (query, actualLength, expectedLength): void => { query.error('expected ' + expectedLength + ' ' + pluralize('elements', expectedLength) + ', found ' + actualLength) }, - value: (query, value) => { + value: (query, value): ActualExpected => { const valueAsserter = query.valueAsserter(value).result() valueAsserter() + + return { + actual: value, + expected: value + } }, - expectOne: (query) => { + expectOne: (query): ActualExpected => { query.expectOne().execute() + return { + actual: {}, + expected: {}, + } }, - function: (query, fn) => { - return this.runModelFunction(fn, query) + function: (query, fn): ActualExpected => { + this.runModelFunction(fn, query) + return {actual: fn, expected: fn} } } @@ -809,7 +874,7 @@ class Query implements PromiseLike { const matchingElements = elements.filter(element => { try { const clone = this.resolve([element]) - lastSuccess = mapModel(clone, model, actions) + lastSuccess = new ExecutedSimpleTransform(mapModel(clone, model, actions)) return true } catch (e) { if (e instanceof BrowserMonkeyAssertionError) { @@ -831,7 +896,7 @@ class Query implements PromiseLike { })) } - public css (selector: string): this { + public findCss (selector: string): this { const findElements = this.transform(elements => { expectElements(elements) return new ExecutedSimpleTransform(flatten(elements.map(element => { @@ -863,7 +928,7 @@ class Query implements PromiseLike { } } - return this.css(selector) + return this.findCss(selector) } public is (selector: string): this { @@ -877,7 +942,7 @@ class Query implements PromiseLike { if (result instanceof Query) { return result.execute() } else if (result && typeof result.then === 'function') { - query.error('model functions must not be asynchronous') + throw new Error('model functions must not be asynchronous') } } } @@ -948,11 +1013,13 @@ function runQueryCreator (queryCreator: (q: Query) => Query, query: Query): Quer const missing = {} +interface ActualExpected {actual: any, expected: any} + interface Actions { arrayLengthError (q: Query, actual: number, expected: number): void - function (q: Query, model: any): any - value (q: Query, model: any): any - expectOne (q: Query): any + function (q: Query, model: any): ActualExpected + value (q: Query, model: any): ActualExpected + expectOne (q: Query): ActualExpected } function spliceModelArrayFromActual (model, query: Query, actions: Actions): any[] { @@ -977,24 +1044,49 @@ function spliceModelArrayFromActual (model, query: Query, actions: Actions): any } } +function arrayAssign (a: any[], b: any[]): any[] { + return a.map((itemA, index) => { + return index < b.length ? b[index] : itemA + }) +} + function mapModel (query: Query, model: any, actions: Actions): any { function map (query: Query, model: any): any { if (model === missing) { - return query.result().map(e => e.innerText).join() + return { + actual: query.result().map(e => e.innerText).join(), + } } else if (model instanceof Array) { - return spliceModelArrayFromActual(model, query, actions).map((item, index) => { + const items = spliceModelArrayFromActual(model, query, actions).map((item, index) => { return map(query.index(index), item) }) + + return { + actual: items.map(i => i.actual), + expected: arrayAssign(model, items.map(i => i.expected)), + } } else if (model.constructor === Object) { const lengthQuery = query.expectOneElement() const keys = Object.keys(model) if (keys.length) { - return object(keys.map(selector => { + const properties = keys.map(selector => { const value = model[selector] - return [selector, map(lengthQuery.find(selector), value)] - })) + + const {actual, expected} = map(lengthQuery.find(selector), value) + + return [ + selector, + actual, + expected, + ] + }) + + return { + actual: object(properties.map(([key, actual]) => [key, actual])), + expected: object(properties.map(([key, , expected]) => [key, expected])), + } } else { return actions.expectOne(query) } @@ -1008,15 +1100,9 @@ function mapModel (query: Query, model: any, actions: Actions): any { return map(query, model) } -function testEqual (actual: any, expected: any): boolean { - return expected instanceof RegExp ? expected.test(actual) : actual === expected -} - interface Options { - visibleOnly: boolean - timeout: number - interval: number - definitions: Definitions + visibleOnly?: boolean + timeout?: number + interval?: number + definitions?: Definitions } - -module.exports = Query diff --git a/package.json b/package.json index 46b9a9e..e79e518 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,10 @@ "test": "yarn test-browser-compatibility && yarn test-electron-mocha && yarn karma && eslint .", "karma": "karma start --single-run", "test-electron-mocha": "yarn electron-mocha test/*Spec.js test/**/*Spec.js", - "electron-mocha": "electron-mocha --disable-site-isolation-trials --full-trace -r test/register --renderer", + "electron-mocha": "electron-mocha --disable-site-isolation-trials --full-trace --main electron/foreignIframe.js -r test/register.js --renderer", + "mocha": "mocha -r test/register.js", "update-readme-example-links": "node ./docs/update-readme-example-links.js", - "test-browser-compatibility": "browserify ./index.js > /dev/null", - "prepare": "tsc" + "test-browser-compatibility": "browserify ./index.js > /dev/null" }, "author": "Tim Macfarlane ", "license": "MIT", diff --git a/test/actionsSpec.js b/test/actionsSpec.js index 07ed6b2..588d7f9 100644 --- a/test/actionsSpec.js +++ b/test/actionsSpec.js @@ -1,5 +1,5 @@ var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') var demand = require('must') var retry = require('trytryagain') diff --git a/test/assemblies/DomAssembly.js b/test/assemblies/DomAssembly.ts similarity index 53% rename from test/assemblies/DomAssembly.js rename to test/assemblies/DomAssembly.ts index 37bf393..1a2f42a 100644 --- a/test/assemblies/DomAssembly.js +++ b/test/assemblies/DomAssembly.ts @@ -7,36 +7,46 @@ const pathUtils = require('path') const trytryagain = require('trytryagain') const { expect } = require('chai') const inspect = require('object-inspect') -const Dom = require('../../lib/Dom').default - -module.exports = class DomAssembly { - constructor () { +import Dom from '../../lib/Dom' +import {Query} from '../../lib/Query' +const object = require('lowscore/object') + +export class DomAssembly { + private delayedOperations: number + private jQuery: JQuery + private retries: (() => void)[] + private queuedRetries: number + private dom: Dom + private _div: HTMLElement + private _normalRetry: boolean + + public constructor () { this.delayedOperations = 0 this.jQuery = $ this.retries = [] - this.delayedActions = [] this.queuedRetries = 0 this.dom = new Dom() + this._normalRetry = false } - div () { + private div (): void { this._div = createTestDiv() } - browserMonkey () { + public browserMonkey (): Query { this.div() const browserMonkey = createBrowserMonkey(this._div) browserMonkey.options({ - retry: (retry) => { + retry: (fn) => { if (this._normalRetry) { - return trytryagain(retry) + return trytryagain(fn) } else { return new Promise((resolve, reject) => { this.retries.push(() => { try { - resolve(retry()) + resolve(fn()) } catch (e) { if (this.queuedRetries === 0) { reject(e) @@ -52,7 +62,7 @@ module.exports = class DomAssembly { return browserMonkey } - tick () { + public tick (): void { if (this._normalRetry) { return } @@ -65,33 +75,33 @@ module.exports = class DomAssembly { } } - localUrl (path) { + public localUrl (path): string { return location.protocol === 'file:' ? 'file://' + path : '/base/' + pathUtils.relative(pathUtils.join(__dirname, '../..'), path) } - useNormalRetry () { + public useNormalRetry (): void { this._normalRetry = true } - findAll (css) { + public findAll (css): HTMLElement[] { return Array.prototype.slice.call(this._div.querySelectorAll(css)) } - find (css) { + public find (css): HTMLElement { return this._div.querySelector(css) } - insertHtml (html) { + public insertHtml (html): void { return $(html).appendTo(this._div).get(0) } - eventuallyDoNothing () { + public eventuallyDoNothing (): void { this.eventually(() => {}) } - eventually (fn) { + public eventually (fn: () => T): Promise { if (!this._normalRetry && !this.retries.length) { throw new Error('nothing retrying yet, start retrying by calling .then() on a query') } @@ -100,21 +110,21 @@ module.exports = class DomAssembly { return new Promise((resolve) => { setTimeout(() => { - const result =fn() + const result = fn() this.tick() resolve(result) }, 0) }) } - eventuallyDeleteHtml (selector) { - return this.eventually(() => { + public async eventuallyDeleteHtml (selector: string): Promise { + await this.eventually(() => { $(selector).remove() }) } - eventuallyInsertHtml (html, selector) { - return this.eventually(() => { + public async eventuallyInsertHtml (html: string, selector: string): Promise { + return await this.eventually(() => { var div = selector ? $(this._div).find(selector).get(0) : this._div @@ -123,17 +133,17 @@ module.exports = class DomAssembly { }) } - eventuallyAppendHtml (element, html) { - return this.eventually(() => { + public async eventuallyAppendHtml (element, html): Promise { + return await this.eventually(() => { return $(html).appendTo(element).get(0) }) } - assertElementIsFocussed (element) { + public assertElementIsFocussed (element): void { expect(document.activeElement).to.equal(element) } - assertRejection (promise, expectedMessage) { + public assertRejection (promise: Promise, expectedMessage): Promise { return promise.then(() => { throw new Error('expected rejection ' + JSON.stringify(expectedMessage)) }, e => { @@ -143,16 +153,38 @@ module.exports = class DomAssembly { }) } - assertExpectedActual (query, expected, actual) { - return query.shouldContain(expected).then(() => { + public async assertExpectedActual (query: Query, expected: any, actual: any, finalExpected?: any): Promise { + await query.shouldContain(expected).then(() => { throw new Error('expected rejection') }, e => { expect(e.message).to.contain('could not match') - expect(e.actual).to.eql(actual) + const actualWithErrorsAsStrings = deepMap(e.actual, value => { + if (value instanceof Error) { + return value.toString() + } else { + return value + } + }) + expect(actualWithErrorsAsStrings).to.eql(actual) + expect(e.expected).to.eql(finalExpected || expected) }) } - static hasDom () { + public static hasDom (): boolean { return true } } + +function deepMap (obj: any, mapper: (any, index?: number | string) => any): any { + function map (obj: any, index?: number | string): any { + if (obj instanceof Array) { + return obj.map((item, index) => map(item, index)) + } else if (obj && obj.constructor === Object) { + return object(Object.keys(obj).map(key => [key, map(obj[key], key)])) + } else { + return mapper(obj, index) + } + } + + return map(obj) +} diff --git a/test/assemblies/domAssemblySpec.js b/test/assemblies/domAssemblySpec.js index 6531f99..3b861f0 100644 --- a/test/assemblies/domAssemblySpec.js +++ b/test/assemblies/domAssemblySpec.js @@ -1,4 +1,4 @@ -const DomAssembly = require('./DomAssembly') +const {DomAssembly} = require('./DomAssembly') const { expect } = require('chai') describe('DomAssembly', () => { diff --git a/test/assertionsSpec.js b/test/assertionsSpec.js index b150701..ce45958 100644 --- a/test/assertionsSpec.js +++ b/test/assertionsSpec.js @@ -1,5 +1,5 @@ const describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') const demand = require('must') describe('assertions', function () { diff --git a/test/buttonSpec.js b/test/buttonSpec.js index e21affd..6cc91e8 100644 --- a/test/buttonSpec.js +++ b/test/buttonSpec.js @@ -1,5 +1,5 @@ var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') var demand = require('must') const {expect} = require('chai') @@ -16,61 +16,61 @@ describe('buttons', function () { it('can recognise a button with exact text', function () { var button = assembly.insertHtml('') - const elements = browser.button('Login').result() + const elements = browser.findButton('Login').result() demand(elements).to.eql([button]) }) it("doesn't recognise a button without exact text match", function () { assembly.insertHtml('') - const elements = browser.button('Logi').result() + const elements = browser.findButton('Logi').result() demand(elements).to.eql([]) }) it('can recognise an input of type button with exact value', function () { var button = assembly.insertHtml('') - const elements = browser.button('Login').result() + const elements = browser.findButton('Login').result() demand(elements).to.eql([button]) }) it("doesn't recognise an input of type button without exact value match", function () { assembly.insertHtml('') - const elements = browser.button('Logi').result() + const elements = browser.findButton('Logi').result() demand(elements).to.eql([]) }) it('can recognise a link with exact text', function () { var button = assembly.insertHtml('Login') - const elements = browser.button('Login').result() + const elements = browser.findButton('Login').result() demand(elements).to.eql([button]) }) it("doesn't recognise a link without exact text match", function () { assembly.insertHtml('Login') - const elements = browser.button('Logi').result() + const elements = browser.findButton('Logi').result() demand(elements).to.eql([]) }) - describe('defineButton', () => { + describe('defining buttons', () => { it('can define a new button', () => { const button = assembly.insertHtml('
Login
') - browser.defineButton((monkey, name) => monkey.find('div.button', { exactText: name })) + browser.addButtonDefinition((monkey, name) => monkey.find('div.button', { exactText: name })) - const elements = browser.button('Login').result() + const elements = browser.findButton('Login').result() demand(elements).to.eql([button]) }) it('can define a new button and still use original button definitions', () => { const button = assembly.insertHtml('') - browser.defineButton((monkey, name) => monkey.find('div.button', { exactText: name })) + browser.addButtonDefinition((monkey, name) => monkey.find('div.button', { exactText: name })) - const elements = browser.button('Login').result() + const elements = browser.findButton('Login').result() demand(elements).to.eql([button]) }) }) @@ -99,12 +99,12 @@ describe('buttons', function () { }) it('can click a defined button', async () => { - browser.defineButton((monkey, name) => monkey.find('div.button').containing(name)) + browser.addButtonDefinition((monkey, name) => monkey.find('div.button').containing(name)) await assertCanClickButton('
Login
', () => browser.click('Button("Login")')) }) it('throws if the button cannot be found to click', async () => { - browser.defineButton((monkey, name) => monkey.find('div.button').containing(name)) + browser.addButtonDefinition((monkey, name) => monkey.find('div.button').containing(name)) assembly.insertHtml('') await assembly.assertRejection(browser.clickButton('Logout'), "expected just one element, found 0 (found: concat(path(find('button, input[type=button], a') [1], containing(...expected 'Login' to equal 'Logout') [0]), path(find('div.button') [0], containing() [0])) [0])") }) diff --git a/test/componentSpec.js b/test/componentSpec.js index 71bbdb8..08385dc 100644 --- a/test/componentSpec.js +++ b/test/componentSpec.js @@ -1,5 +1,5 @@ var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') describe('component', function () { describeAssemblies([DomAssembly], function (Assembly) { diff --git a/test/containingSpec.js b/test/containingSpec.js index 3425bd4..6fbad54 100644 --- a/test/containingSpec.js +++ b/test/containingSpec.js @@ -1,6 +1,7 @@ const describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') const {expect} = require('chai') +const {elementAttributes} = require('../matchers') describe('containing', function () { describeAssemblies([DomAssembly], function (Assembly) { @@ -93,6 +94,23 @@ describe('containing', function () { }) }) + describe('attributes', () => { + it('function can assert existence', () => { + assembly.insertHtml(` +
+
Title
+
+
+
Body
+
+ `) + + const results = browser.find('.result').containing({'.title': elementAttributes({style: {color: 'red'}})}).result() + const expected = assembly.findAll('.correct') + expect(results).to.eql(expected) + }) + }) + describe('functions', () => { it('function can assert existence', () => { assembly.insertHtml(` @@ -109,7 +127,7 @@ describe('containing', function () { expect(results).to.eql(expected) }) - it('shows what it found', () => { + xit('shows what it found', () => { assembly.insertHtml(`
Title
@@ -122,12 +140,13 @@ describe('containing', function () {
`) + browser.find('.result').containing(result => result.find('.body').shouldExist()).find('.title').expectOneElement().result() expect(() => browser.find('.result').containing(result => result.find('.body').shouldExist()).find('.title').expectOneElement().result() ).to.throw(`expected just one element, found 0 (found: path(find('.result') [3], containing(...path(find('.body') [1])) [2], find('.title') [0]))`) }) - it("shows why it couldn't find the element", () => { + xit("shows why it couldn't find the element", () => { assembly.insertHtml(`
Title
diff --git a/test/defineSpec.js b/test/defineSpec.js index bdfef7e..ceb9db3 100644 --- a/test/defineSpec.js +++ b/test/defineSpec.js @@ -1,5 +1,5 @@ const describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') const {expect} = require('chai') describe('define', function () { diff --git a/test/domSpec.js b/test/domSpec.js index afcb2a9..205853d 100644 --- a/test/domSpec.js +++ b/test/domSpec.js @@ -1,5 +1,5 @@ var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') const { expect } = require('chai') describe('dom', () => { diff --git a/test/eventsSpec.js b/test/eventsSpec.js index 39b3e48..e18e88b 100644 --- a/test/eventsSpec.js +++ b/test/eventsSpec.js @@ -1,5 +1,5 @@ var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') var demand = require('must') var retry = require('trytryagain') diff --git a/test/findersSpec.js b/test/findersSpec.js index d3dfdd5..db76f77 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -1,5 +1,5 @@ var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') var demand = require('must') const pathUtils = require('path') diff --git a/test/hyperdomIntegrationSpec.js b/test/hyperdomIntegrationSpec.js index 4c2c97d..47201de 100644 --- a/test/hyperdomIntegrationSpec.js +++ b/test/hyperdomIntegrationSpec.js @@ -3,7 +3,7 @@ var retry = require('trytryagain') var hyperdom = require('hyperdom') var h = hyperdom.html var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') describe('hyperdom integration', function () { describeAssemblies([DomAssembly], Assembly => { diff --git a/test/labelSpec.js b/test/labelSpec.ts similarity index 72% rename from test/labelSpec.js rename to test/labelSpec.ts index 2c8d2d0..0a7eeb9 100644 --- a/test/labelSpec.js +++ b/test/labelSpec.ts @@ -1,18 +1,19 @@ const describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') const {expect} = require('chai') +import {Query} from '../lib/Query' describe('labels', function () { describeAssemblies([DomAssembly], function (Assembly) { var assembly - var browser + var browser: Query beforeEach(function () { assembly = new Assembly() browser = assembly.browserMonkey() }) - function assertFoundElementByLabel(html, label) { + function assertFoundElementByLabel(html, label): void { assembly.insertHtml(html) const expected = assembly.find('.result') @@ -23,7 +24,7 @@ describe('labels', function () { expect(found).to.equal(expected) } - function assertNothingFound(html, label) { + function assertNothingFound(html, label): void { assembly.insertHtml(html) browser.find(label).expectNoElements().result() @@ -65,6 +66,30 @@ describe('labels', function () { 'Label("Feature Enabled")' ) }) + + it('fails if not exact name', async () => { + assertNothingFound( + ` + + + `, + 'Label("Feature")' + ) + }) + + it('can be found by regex', async () => { + assertFoundElementByLabel( + ` + + + `, + 'Label(/Feature/)' + ) + }) }) describe('finding things by their aria labeling', () => { @@ -138,9 +163,9 @@ describe('labels', function () { expect(found).to.eql(expected) }) - describe('defineLabel', () => { + describe('label definitions', () => { it('can define a new way of finding labels', () => { - browser.defineLabel((query, label) => query.css(`[data-label=${JSON.stringify(label)}]`)) + browser.addLabelDefinition((query, label) => query.findCss(`[data-label=${JSON.stringify(label)}]`)) assertFoundElementByLabel( ` @@ -149,6 +174,21 @@ describe('labels', function () { 'Label("Search")' ) }) + + it('can add and remove a label definition by name', async () => { + browser.addLabelDefinition('data-label', (query, label) => query.findCss(`[data-label=${JSON.stringify(label)}]`)) + + assertFoundElementByLabel( + ` +
+ `, + 'Label("Search")' + ) + + browser.removeLabelDefinition('data-label') + + await browser.find('Label("Search")').expectNoElements().result() + }) }) }) }) diff --git a/test/mountSpec.js b/test/mountSpec.js index c8f08ab..47a9e37 100644 --- a/test/mountSpec.js +++ b/test/mountSpec.js @@ -1,7 +1,7 @@ const { expect } = require('chai') const pathUtils = require('path') const describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') describeAssemblies([DomAssembly], (Assembly) => { const assembly = new Assembly() diff --git a/test/optionsSpec.js b/test/optionsSpec.js index a4726d8..27ddc1d 100644 --- a/test/optionsSpec.js +++ b/test/optionsSpec.js @@ -1,6 +1,6 @@ var demand = require('must') const describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') describe('options', function () { describeAssemblies([DomAssembly], function (Assembly) { diff --git a/test/querySpec.js b/test/querySpec.js index 3f64bcb..89d652a 100644 --- a/test/querySpec.js +++ b/test/querySpec.js @@ -1,6 +1,6 @@ var describeAssemblies = require('./describeAssemblies') const {expect} = require('chai') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') describe('query', () => { describeAssemblies([DomAssembly], Assembly => { diff --git a/test/reloadButtonSpec.js b/test/reloadButtonSpec.js index d6bfe12..84a1678 100644 --- a/test/reloadButtonSpec.js +++ b/test/reloadButtonSpec.js @@ -1,7 +1,7 @@ var expect = require('chai').expect var reloadButton = require('../lib/reloadButton') var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') describe('reload button', function () { describeAssemblies([DomAssembly], Assembly => { diff --git a/test/scopeSpec.js b/test/scopeSpec.js index 54864c4..997577f 100644 --- a/test/scopeSpec.js +++ b/test/scopeSpec.js @@ -1,6 +1,6 @@ var demand = require('must') var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') describe('scope', function () { describeAssemblies([DomAssembly], Assembly => { diff --git a/test/setSpec.js b/test/setSpec.js index c17f3be..842c5f9 100644 --- a/test/setSpec.js +++ b/test/setSpec.js @@ -1,5 +1,5 @@ const describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') const demand = require('must') const {expect} = require('chai') diff --git a/test/shouldAppearSpec.js b/test/shouldAppearSpec.js index ec6cfce..c0e5001 100644 --- a/test/shouldAppearSpec.js +++ b/test/shouldAppearSpec.js @@ -1,5 +1,5 @@ var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') var demand = require('must') const {expect} = require('chai') diff --git a/test/shouldContainSpec.js b/test/shouldContainSpec.js index 8981796..efba260 100644 --- a/test/shouldContainSpec.js +++ b/test/shouldContainSpec.js @@ -1,6 +1,7 @@ const describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') var BrowserMonkeyAssertionError = require('../lib/BrowserMonkeyAssertionError').default +const {elementAttributes} = require('../matchers') describe('shouldContain', function () { describeAssemblies([DomAssembly], function (Assembly) { @@ -404,12 +405,15 @@ describe('shouldContain', function () { 'h1': 'Title', '.content': query => { if (/Content/.test(query.elementResult().innerText)) { - throw new BrowserMonkeyAssertionError('asdf') + throw new BrowserMonkeyAssertionError('asdf', {actual: 'error actual', expected: 'error expected'}) } } }, { 'h1': 'Title', - '.content': 'Error: asdf' + '.content': 'error actual' + }, { + 'h1': 'Title', + '.content': 'error expected' }) }) @@ -421,14 +425,14 @@ describe('shouldContain', function () {
`) - await assembly.assertExpectedActual(browser, { - 'h1': 'Title', - '.content': async () => { - } - }, { - 'h1': 'Title', - '.content': 'Error: model functions must not be asynchronous' - }) + await assembly.assertRejection( + browser.shouldContain({ + 'h1': 'Title', + '.content': async () => { + } + }), + 'model functions must not be asynchronous' + ) }) it('does not throw if the function returns a query', async () => { @@ -446,6 +450,80 @@ describe('shouldContain', function () { }) }) + describe('attributes', () => { + it('can assert attributes', async () => { + assembly.insertHtml(` + + `) + + await browser.shouldContain({ + input: elementAttributes({ + placeholder: 'Username' + }), + }) + }) + + it("fails when the attributes don't match", async () => { + assembly.insertHtml(` + + `) + + await assembly.assertExpectedActual(browser, + { + input: elementAttributes({ + placeholder: 'User' + }) + }, + { + input: { + placeholder: 'Username', + }, + }, + { + input: { + placeholder: 'User', + }, + } + ) + }) + + it("can use regexp", async () => { + assembly.insertHtml(` + + `) + + await browser.shouldContain({ + input: elementAttributes({ + placeholder: /User/ + }) + }) + }) + + it("can assert boolean", async () => { + assembly.insertHtml(` + + `) + + await browser.shouldContain({ + input: elementAttributes({ + checked: true + }) + }) + }) + + it("can assert integer", async () => { + assembly.insertHtml(` + + `) + + await browser.shouldContain({ + input: elementAttributes({ + tabIndex: 0 + }) + }) + }) + }) + describe('define', () => { it('can define a field using a finder', async () => { assembly.insertHtml(` diff --git a/test/submitSpec.js b/test/submitSpec.js index 46229d4..2ebb9fe 100644 --- a/test/submitSpec.js +++ b/test/submitSpec.js @@ -1,5 +1,5 @@ var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') var demand = require('must') const {expect} = require('chai') diff --git a/test/valueSpec.js b/test/valueSpec.js index de0c9c4..f778f57 100644 --- a/test/valueSpec.js +++ b/test/valueSpec.js @@ -1,6 +1,6 @@ var expect = require('chai').expect var describeAssemblies = require('./describeAssemblies') -const DomAssembly = require('./assemblies/DomAssembly') +const {DomAssembly} = require('./assemblies/DomAssembly') describe('value', function () { describeAssemblies([DomAssembly], Assembly => { From e0c01f06fc09f63b5aaecbcd9f196083d756e7e8 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Wed, 15 Jan 2020 15:29:55 +0100 Subject: [PATCH 035/127] options and scope are immutable --- create.js | 2 +- lib/Query.ts | 36 +++++++++++++--------- lib/mount.js | 4 +-- test/assemblies/DomAssembly.ts | 6 ++-- test/findersSpec.js | 7 ++--- test/optionsSpec.js | 3 +- test/{querySpec.js => querySpec.ts} | 46 ++++++++++++++--------------- test/reloadButtonSpec.js | 2 +- test/scopeSpec.js | 4 +-- test/valueSpec.js | 2 +- 10 files changed, 57 insertions(+), 55 deletions(-) rename test/{querySpec.js => querySpec.ts} (95%) diff --git a/create.js b/create.js index b48d6b7..d88fc7f 100644 --- a/create.js +++ b/create.js @@ -2,7 +2,7 @@ var {Query} = require('./lib/Query') module.exports = function (rootSelector = document.body) { return new Query() - .scope(rootSelector) + .withScope(rootSelector) .component({ v2: function () { var finders = require('./lib/finders') diff --git a/lib/Query.ts b/lib/Query.ts index fa3d56c..033de57 100644 --- a/lib/Query.ts +++ b/lib/Query.ts @@ -51,7 +51,7 @@ export class Query implements PromiseLike { private _hasExpectation: boolean private _dom: Dom - public constructor () { + public constructor (scope?: HTMLElement) { this._hasExpectation = false this._transforms = [] this._options = { @@ -220,7 +220,12 @@ export class Query implements PromiseLike { } } this._dom = new Dom() - this._input = undefined + + if (scope) { + return this.withScope(scope) + } else { + this._input = undefined + } } public get [Symbol.toStringTag](): string { @@ -450,12 +455,12 @@ export class Query implements PromiseLike { return transformed } - public options (options: Options): any { - if (options) { - extend(this._options, options) - } else { - return this._options - } + public withOptions (options: Options): any { + return this.clone(q => extend(q._options, options)) + } + + public options (): any { + return this._options } private assertHasActionOrExpectation (): void { @@ -464,7 +469,7 @@ export class Query implements PromiseLike { } } - public then (resolve: (r) => any, reject: (e) => any): Promise { + public then (resolve?: (r) => any, reject?: (e) => any): Promise { this.assertHasActionOrExpectation() var self = this @@ -504,8 +509,12 @@ export class Query implements PromiseLike { return clone } - public input (value): void { - this._input = value + public withInput (value): this { + return this.clone(q => q._input = value) + } + + public input (): this { + return this._input } public component (methods): this { @@ -602,9 +611,8 @@ export class Query implements PromiseLike { }) } - public scope (element: HTMLElement): this { - var query = this.clone() - query.input([element]) + public withScope (element: HTMLElement): this { + var query = this.withInput([element]) if (isIframe(element)) { return query.iframeContent() diff --git a/lib/mount.js b/lib/mount.js index cdfb840..70274c5 100644 --- a/lib/mount.js +++ b/lib/mount.js @@ -10,12 +10,10 @@ function Mount (app, options) { Mount.prototype.start = function () { var monkey = this.startApp() - monkey.options({ + return monkey.withOptions({ app: this.app, mount: this }) - - return monkey } Mount.prototype.stop = function () { diff --git a/test/assemblies/DomAssembly.ts b/test/assemblies/DomAssembly.ts index 1a2f42a..a57fe24 100644 --- a/test/assemblies/DomAssembly.ts +++ b/test/assemblies/DomAssembly.ts @@ -38,7 +38,7 @@ export class DomAssembly { const browserMonkey = createBrowserMonkey(this._div) - browserMonkey.options({ + return browserMonkey.withOptions({ retry: (fn) => { if (this._normalRetry) { return trytryagain(fn) @@ -58,8 +58,6 @@ export class DomAssembly { } } }) - - return browserMonkey } public tick (): void { @@ -123,7 +121,7 @@ export class DomAssembly { }) } - public async eventuallyInsertHtml (html: string, selector: string): Promise { + public async eventuallyInsertHtml (html: string, selector?: string): Promise { return await this.eventually(() => { var div = selector ? $(this._div).find(selector).get(0) diff --git a/test/findersSpec.js b/test/findersSpec.js index db76f77..628d4a8 100644 --- a/test/findersSpec.js +++ b/test/findersSpec.js @@ -82,7 +82,7 @@ describe('find', function () { iframe.width = 700 iframe.height = 1000 assembly.insertHtml(iframe) - var iframeScope = browser.scope(iframe) + var iframeScope = browser.withScope(iframe) assembly.useNormalRetry() @@ -104,7 +104,7 @@ describe('find', function () { assembly.useNormalRetry() return browser.find('iframe').expectOneElement().then(function ([iframe]) { - return browser.scope(iframe).find('h1', { text: 'Page 2' }).shouldExist() + return browser.withScope(iframe).find('h1', { text: 'Page 2' }).shouldExist() }) }, { vdom: false }) } @@ -119,8 +119,7 @@ describe('find', function () { it('should find an element that is visually hidden when visibleOnly = false', function () { assembly.insertHtml('
hello world
') - browser.options({ visibleOnly: false }) - return browser.find('.element > span').shouldExist() + return browser.withOptions({ visibleOnly: false }).find('.element > span').shouldExist() }) it('should find elements that are visually hidden because of how html renders them', function () { diff --git a/test/optionsSpec.js b/test/optionsSpec.js index 27ddc1d..7272736 100644 --- a/test/optionsSpec.js +++ b/test/optionsSpec.js @@ -14,8 +14,7 @@ describe('options', function () { it('can overide default timeout', function () { var defaultTimeout = browser.options().timeout - const timeoutBrowser = browser.clone() - timeoutBrowser.options({ timeout: 1 }) + const timeoutBrowser = browser.withOptions({ timeout: 1 }) var start = new Date() return timeoutBrowser.find('.doesnt-exist').shouldExist().catch(function () { diff --git a/test/querySpec.js b/test/querySpec.ts similarity index 95% rename from test/querySpec.js rename to test/querySpec.ts index 89d652a..fd5d7b1 100644 --- a/test/querySpec.js +++ b/test/querySpec.ts @@ -1,11 +1,12 @@ var describeAssemblies = require('./describeAssemblies') const {expect} = require('chai') -const {DomAssembly} = require('./assemblies/DomAssembly') +import {DomAssembly} from './assemblies/DomAssembly' +import {Query} from '../lib/Query' describe('query', () => { describeAssemblies([DomAssembly], Assembly => { - let assembly - let browserMonkey + let assembly: DomAssembly + let browserMonkey: Query beforeEach(() => { assembly = new Assembly() @@ -27,8 +28,7 @@ describe('query', () => { describe('options', () => { it('returns a new Browser Monkey object without modifying the current one', () => { - browserMonkey.options({ a: 'a' }) - expect(browserMonkey._options.a).to.equal('a') + expect(browserMonkey.withOptions({a: 'a'}).options().a).to.equal('a') }) }) @@ -43,31 +43,26 @@ describe('query', () => { }) it('passes through the options', () => { - browserMonkey.options({ a: 'a' }) - const monkey = browserMonkey - .component({ - aMethod: function () { return 'aMethod' } - }) + .withOptions({ a: 'a' }) + .find('asdf') - expect(monkey._options.a).to.equal('a') + expect(monkey.options().a).to.equal('a') }) it('passes through the input', () => { - browserMonkey.input('a') - const monkey = browserMonkey + .withInput('a') .component({ aMethod: function () { return 'aMethod' } }) - expect(monkey._input).to.equal('a') + expect(monkey.input()).to.equal('a') }) it('passes through the maps', async () => { - browserMonkey.input(1) - const monkey = browserMonkey + .withInput(1) .transform(x => x + 1) .component({ aMethod: function () { return 'aMethod' } @@ -268,21 +263,26 @@ describe('query', () => { describe('input', () => { it('input sets the input used in transform', async () => { - browserMonkey.input('a') - - const monkey = browserMonkey + const query = browserMonkey + .withInput('a') .transform(x => `input: ${x}`) - expect(monkey.result()).to.eql('input: a') + expect(query.result()).to.eql('input: a') }) }) describe('scope', () => { it('when scope is one element, sets the input to an array of one', () => { - const monkey = browserMonkey - .scope(document.body) + const query = browserMonkey + .withScope(document.body) + + expect(query.input()).to.eql([document.body]) + }) + + it('scope can be passed in to constructor', () => { + const query = new Query(document.body) - expect(monkey._input).to.eql([document.body]) + expect(query.input()).to.eql([document.body]) }) }) diff --git a/test/reloadButtonSpec.js b/test/reloadButtonSpec.js index 84a1678..674dfab 100644 --- a/test/reloadButtonSpec.js +++ b/test/reloadButtonSpec.js @@ -10,7 +10,7 @@ describe('reload button', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().scope(document.body) + browser = assembly.browserMonkey().withScope(document.body) }) it('adds reload button that reloads to the initial url', function () { diff --git a/test/scopeSpec.js b/test/scopeSpec.js index 997577f..ec4f841 100644 --- a/test/scopeSpec.js +++ b/test/scopeSpec.js @@ -16,10 +16,10 @@ describe('scope', function () { var red = assembly.insertHtml('
red
') var blue = assembly.insertHtml('
blue
') - return browser.scope(red).find('.element').expectOneElement().then(function ([element]) { + return browser.withScope(red).find('.element').expectOneElement().then(function ([element]) { demand(element.innerText).to.equal('red') }).then(function () { - return browser.scope(blue).find('.element').expectOneElement() + return browser.withScope(blue).find('.element').expectOneElement() }).then(function ([element]) { demand(element.innerText).to.equal('blue') }) diff --git a/test/valueSpec.js b/test/valueSpec.js index f778f57..849b6cb 100644 --- a/test/valueSpec.js +++ b/test/valueSpec.js @@ -9,7 +9,7 @@ describe('value', function () { beforeEach(function () { assembly = new Assembly() - browser = assembly.browserMonkey().scope(document.body) + browser = assembly.browserMonkey().withScope(document.body) }) function assertValue (html, expectedValue) { From 5631511ae682b09a53a652dd4a24bd8e22fe5078 Mon Sep 17 00:00:00 2001 From: Tim Macfarlane Date: Thu, 16 Jan 2020 15:52:51 +0100 Subject: [PATCH 036/127] submit changes, less mutating methods --- angular.js | 4 +- create.js | 18 -- hyperdom.js | 6 +- iframe.js | 4 +- index.js | 4 +- lib/Query.ts | 68 +++-- lib/mount.js | 2 +- react.js | 4 +- test/actionsSpec.js | 400 ------------------------------ test/assemblies/DomAssembly.ts | 5 +- test/assertionsSpec.js | 439 ++------------------------------- test/buttonSpec.js | 20 +- test/componentSpec.js | 108 -------- test/eventsSpec.js | 28 +-- test/findSpec.ts | 98 ++++++++ test/findersSpec.js | 151 ------------ test/labelSpec.ts | 42 +++- test/mountSpec.js | 4 +- test/optionsSpec.js | 6 +- test/querySpec.ts | 20 +- test/reloadButtonSpec.js | 2 +- test/scopeSpec.js | 4 +- test/submitSpec.js | 36 ++- test/valueSpec.js | 2 +- 24 files changed, 268 insertions(+), 1207 deletions(-) delete mode 100644 create.js delete mode 100644 test/actionsSpec.js delete mode 100644 test/componentSpec.js create mode 100644 test/findSpec.ts delete mode 100644 test/findersSpec.js diff --git a/angular.js b/angular.js index b7c06ee..59d928a 100644 --- a/angular.js +++ b/angular.js @@ -1,6 +1,6 @@ var debug = require('debug')('browser-monkey:angular') var Mount = require('./lib/mount') -var createMonkey = require('./create') +var {Query} = require('./lib/Query') var createTestDiv = require('./lib/createTestDiv') var angular = require('angular') @@ -13,7 +13,7 @@ module.exports = function (app) { div.setAttribute(app.directiveName, '') angular.bootstrap(div, [app.moduleName]) - return createMonkey(document.body) + return new Query(document.body) } }).start() } diff --git a/create.js b/create.js deleted file mode 100644 index d88fc7f..0000000 --- a/create.js +++ /dev/null @@ -1,18 +0,0 @@ -var {Query} = require('./lib/Query') - -module.exports = function (rootSelector = document.body) { - return new Query() - .withScope(rootSelector) - .component({ - v2: function () { - var finders = require('./lib/finders') - var actions = require('./lib/actions') - var assertions = require('./lib/assertions') - - return this - .component(finders) - .component(actions) - .component(assertions) - }, - }) -} diff --git a/hyperdom.js b/hyperdom.js index 80ea1a4..4cb0b53 100644 --- a/hyperdom.js +++ b/hyperdom.js @@ -1,6 +1,6 @@ var Mount = require('./lib/mount') var hyperdom = require('hyperdom') -var createMonkey = require('./create') +var {Query} = require('./lib/Query') var window = require('global') var createTestDiv = require('./lib/createTestDiv') var extend = require('lowscore/extend') @@ -23,7 +23,7 @@ module.exports = function (app, options) { } var vdom = hyperdom.html('body') - var monkey = createMonkey(vdom) + var monkey = new Query(vdom) monkey.set({ $: vquery, visibleOnly: false, document: {} }) hyperdom.appendVDom(vdom, app, extend({ requestRender: setTimeout, window: window }, options)) @@ -34,7 +34,7 @@ module.exports = function (app, options) { options.router.push(options.url || options.hash) } hyperdom.append(testDiv, app, extend({ requestRender: setTimeout }, options)) - return createMonkey(testDiv) + return new Query(testDiv) } } }).start() diff --git a/iframe.js b/iframe.js index 92adc81..2480963 100644 --- a/iframe.js +++ b/iframe.js @@ -1,6 +1,6 @@ var debug = require('debug')('browser-monkey:iframe') var Mount = require('./lib/mount') -var createMonkey = require('./create') +var {Query} = require('./lib/Query') var hobostyle = require('hobostyle') var createTestDiv = require('./lib/createTestDiv') var { iframeResizer } = require('iframe-resizer') @@ -55,7 +55,7 @@ module.exports = function (url, {resize = false} = {}) { hobostyle.link(__dirname + '/iframe.css') - return createMonkey(iframe) + return new Query(iframe).iframe() } }).start() } diff --git a/index.js b/index.js index 9838084..f1353cf 100644 --- a/index.js +++ b/index.js @@ -1 +1,3 @@ -module.exports = require('./create')() +import {Query} from './lib/Query' + +module.exports = Query diff --git a/lib/Query.ts b/lib/Query.ts index 033de57..83bdca9 100644 --- a/lib/Query.ts +++ b/lib/Query.ts @@ -51,7 +51,7 @@ export class Query implements PromiseLike { private _hasExpectation: boolean private _dom: Dom - public constructor (scope?: HTMLElement) { + public constructor (input: HTMLElement = document.body) { this._hasExpectation = false this._transforms = [] this._options = { @@ -171,7 +171,7 @@ export class Query implements PromiseLike { ], button: [ (query, name) => { - return query.findCss('button, input[type=button], a').containing(name) + return query.findCss('button, input[type=button], input[type=submit], input[type=reset], a').containing(name) }, ], label: [ @@ -221,11 +221,7 @@ export class Query implements PromiseLike { } this._dom = new Dom() - if (scope) { - return this.withScope(scope) - } else { - this._input = undefined - } + this._input = [input] } public get [Symbol.toStringTag](): string { @@ -273,24 +269,28 @@ export class Query implements PromiseLike { })) } - public addButtonDefinition (definition: LabelDefinition): void { - this._options.definitions.button.push(definition) + public defineButtonType (definition: LabelDefinition): this { + return this.clone(q => q._options.definitions.button.push(definition)) } - public addLabelDefinition (name: string | LabelDefinition, definition?: LabelDefinition): void { + public defineLabelType (name: string | LabelDefinition, definition?: LabelDefinition): this { if (!definition) { definition = name as LabelDefinition name = undefined } - this._options.definitions.label.push({name: name as string, definition}) + return this.clone(q => q._options.definitions.label.push({name: name as string, definition})) } - public removeLabelDefinition (name: string): void { - const index = this._options.definitions.label.findIndex(def => def.name === name) - if (index >= 0) { - this._options.definitions.label.splice(index, 1) - } + public undefineLabelType (name: string): this { + return this.clone(q => { + const index = q._options.definitions.label.findIndex(def => def.name === name) + if (index >= 0) { + q._options.definitions.label.splice(index, 1) + } else { + throw new Error(`label definition ${JSON.stringify(name)} doesn't exist`) + } + }) } public result (): any { @@ -455,11 +455,11 @@ export class Query implements PromiseLike { return transformed } - public withOptions (options: Options): any { + public options (options: Options): any { return this.clone(q => extend(q._options, options)) } - public options (): any { + public getOptions (): any { return this._options } @@ -509,11 +509,11 @@ export class Query implements PromiseLike { return clone } - public withInput (value): this { + public input (value): this { return this.clone(q => q._input = value) } - public input (): this { + public getInput (): this { return this._input } @@ -587,7 +587,7 @@ export class Query implements PromiseLike { } public click (selector?: string): this { - return (selector ? this.find(selector) : this).expectOneElement().action(([element]) => { + return this.optionalSelector(selector).expectOneElement().action(([element]) => { debug('click', element) this._dom.click(element) }) @@ -598,7 +598,7 @@ export class Query implements PromiseLike { } public submit (selector?: string): this { - return (selector ? this.find(selector) : this) + return this.optionalSelector(selector) .expectOneElement() .expect(([element]) => { if (!element.form) { @@ -611,21 +611,13 @@ export class Query implements PromiseLike { }) } - public withScope (element: HTMLElement): this { - var query = this.withInput([element]) - - if (isIframe(element)) { - return query.iframeContent() - } else if (isHTMLElement(element)) { - return query - } else { - throw new Error('scope() expects HTML element') - } + public scope (element: HTMLElement): this { + return this.input([element]) } - public iframeContent (): this { - return this.transform(elements => { - return elements.map(element => { + public iframe (selector?: string): this { + return this.optionalSelector(selector).transform(elements => { + return new ExecutedSimpleTransform(elements.map(element => { if (isIframe(element)) { if (element.contentDocument && element.contentDocument.readyState === 'complete') { return element.contentDocument.body @@ -635,7 +627,7 @@ export class Query implements PromiseLike { } else { throw new BrowserMonkeyAssertionError('not iframe') } - }) + }), 'iframe.contentDocument') }) } @@ -953,6 +945,10 @@ export class Query implements PromiseLike { throw new Error('model functions must not be asynchronous') } } + + private optionalSelector (selector?: string): this { + return selector ? this.find(selector) : this + } } function expectElements (elements): void { diff --git a/lib/mount.js b/lib/mount.js index 70274c5..020ede8 100644 --- a/lib/mount.js +++ b/lib/mount.js @@ -10,7 +10,7 @@ function Mount (app, options) { Mount.prototype.start = function () { var monkey = this.startApp() - return monkey.withOptions({ + return monkey.options({ app: this.app, mount: this }) diff --git a/react.js b/react.js index 6b1d325..4aa1c22 100644 --- a/react.js +++ b/react.js @@ -1,7 +1,7 @@ var Mount = require('./lib/mount') var React = require('react') var ReactDOM = require('react-dom') -var createMonkey = require('./create') +var {Query} = require('./lib/Query') var createTestDiv = require('./lib/createTestDiv') module.exports = function (App, props) { @@ -12,7 +12,7 @@ module.exports = function (App, props) { var div = createTestDiv() ReactDOM.render(React.createElement(App, props), div) - return createMonkey(document.body) + return new Query(document.body) } }).start() } diff --git a/test/actionsSpec.js b/test/actionsSpec.js deleted file mode 100644 index 588d7f9..0000000 --- a/test/actionsSpec.js +++ /dev/null @@ -1,400 +0,0 @@ -var describeAssemblies = require('./describeAssemblies') -const {DomAssembly} = require('./assemblies/DomAssembly') -var demand = require('must') -var retry = require('trytryagain') - -describe('actions', function () { - describeAssemblies([DomAssembly], function (Assembly) { - var assembly - var browser - - beforeEach(function () { - assembly = new Assembly() - browser = assembly.browserMonkey().v2() - }) - - describe('clicking', function () { - it('should eventually click an element', function () { - var promise = browser.find('.element').click().then() - var clicked = false - - assembly.eventuallyInsertHtml( - assembly.jQuery('
').on('click', function () { - clicked = true - }) - ) - - return promise.then(function () { - demand(clicked).to.equal(true) - }) - }) - - it('sends mousedown mouseup and click events', function () { - var events = [] - - assembly.insertHtml( - assembly.jQuery('
').on('mousedown', function () { - events.push('mousedown') - }).on('mouseup', function () { - events.push('mouseup') - }).on('click', function () { - events.push('click') - }) - ) - - return browser.find('.element').click().then(function () { - demand(events).to.eql(['mousedown', 'mouseup', 'click']) - }) - }) - - it('mousedown mouseup and click events bubble up to parent', function () { - var events = [] - - assembly.insertHtml( - assembly.jQuery('
inner
').on('mousedown', function () { - events.push('mousedown') - }).on('mouseup', function () { - events.push('mouseup') - }).on('click', function () { - events.push('click') - }) - ) - - return browser.find('.inner-element').click().then(function () { - demand(events).to.eql(['mousedown', 'mouseup', 'click']) - }) - }, { vdom: false }) - - it('waits until checkbox is enabled before clicking', function () { - var promise = browser.find('input[type=checkbox]').click().then() - var clicked - var buttonState = 'disabled' - - var button = assembly.jQuery(assembly.insertHtml('')) - button.on('click', function () { - clicked = buttonState - }) - - assembly.eventually(function () { - button.prop('disabled', false) - buttonState = 'enabled' - }) - - return promise.then(function () { - demand(clicked).to.equal('enabled') - }) - }) - - it('waits until button is enabled before clicking', function () { - var promise = browser.find('button', { text: 'a button' }).click().then() - var clicked - var buttonState = 'disabled' - - var button = assembly.jQuery(assembly.insertHtml('')) - button.on('click', function () { - clicked = buttonState - }) - - assembly.eventually(function () { - button.prop('disabled', false) - buttonState = 'enabled' - }) - - return promise.then(function () { - demand(clicked).to.equal('enabled') - }) - }) - }) - - describe('select', function () { - describe('text', function () { - it('eventually selects an option element using the text', function () { - var promise = browser.find('.element').select({ text: 'Second' }).then() - var selectedItem - - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function () { - selectedItem = assembly.jQuery(this).find(':selected').text() - }) - ) - - return promise.then(function () { - demand(selectedItem).to.equal('Second') - }) - }) - - it('eventually selects an option element using the text, when text is passed as a string', function () { - var promise = browser.find('.element').select('Second').then() - var selectedItem - - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function () { - selectedItem = assembly.jQuery(this).find(':selected').text() - }) - ) - - return promise.then(function () { - demand(selectedItem).to.equal('Second') - }) - }) - - it('should eventually select an option element using a partial match', function () { - var promise = browser.find('.element').select({ text: 'Seco' }).then() - var selectedItem - - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').text() - }) - ) - - return promise.then(function () { - demand(selectedItem).to.equal('Second') - }) - }) - - it('selects the option by index names are ambiguous', function () { - var selectedItem - - assembly.insertHtml( - assembly.jQuery('').on('change', function (e) { - var select = e.target - selectedItem = select.value - }) - ) - - return browser.find('select').select({ index: 1 }).then(function () { - demand(selectedItem).to.equal('2') - }) - }) - - it('selects an option that eventually appears', function () { - var promise = browser.find('.element').select({ text: 'Second' }).then() - var selectedItem - - var select = assembly.insertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').text() - }) - ) - - assembly.eventuallyInsertHtml('', select) - - return promise.then(function () { - demand(selectedItem).to.equal('Second') - }) - }) - - it('errors when the specified option does not exist', async function () { - var promise = browser.find('.element').select({ text: 'Does not exist' }).then() - - assembly.eventuallyInsertHtml('') - - return assembly.assertRejection(promise, 'expected just one element') - }) - - it('errors when the input is not a select', function () { - var promise = browser.find('.element').select({ text: 'Whatevs' }).then() - assembly.eventuallyInsertHtml('
') - return assembly.assertRejection(promise, 'expected just one element') - }) - - it('selects an option using text that is falsy', function () { - var promise = browser.find('.element').select({ text: 0 }).then() - var selectedItem - - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').text() - }) - ) - - return promise.then(function () { - demand(selectedItem).to.equal('0') - }) - }) - }) - - describe('exactText', function () { - it('should select an option using exact text that would otherwise match multiple options', function () { - var promise = browser.find('.element').select({ exactText: 'Mr' }).then() - var selectedItem - - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').text() - }) - ) - - return promise.then(function () { - demand(selectedItem).to.equal('Mr') - }) - }) - - it('should select an option using exact text that is falsy', function () { - var promise = browser.find('.element').select({ exactText: '' }).then() - var selectedItem - - assembly.eventuallyInsertHtml( - assembly.jQuery('').on('change', function (e) { - selectedItem = assembly.jQuery(this).find(':selected').val() - }) - ) - - return promise.then(function () { - demand(selectedItem).to.equal('0') - }) - }) - }) - }) - - describe('submit', function () { - it('should submit the form', function () { - var submitted = false - var promise = browser.find('input').submit().then() - - assembly.eventuallyInsertHtml( - assembly.jQuery('
').on('submit', function (ev) { - submitted = true - }) - ) - - return promise.then(function () { - demand(submitted).to.equal(true) - }) - }) - - it('should submit the form when submit button is clicked', function () { - var submitted = false - var promise = browser.find('input').click().then() - - assembly.eventuallyInsertHtml( - assembly.jQuery('
submit
').on('submit', function (ev) { - ev.preventDefault() - submitted = true - }) - ) - - return promise.then(function () { - demand(submitted).to.equal(true) - }) - }) - }) - - describe('typeIn', function () { - var allowedToTypeInto = [ - '', - '', - '', - '', - '', - '', - '', - '', - '