diff --git a/ui-v2/GNUmakefile b/ui-v2/GNUmakefile index e87521b9928f..682077582b1e 100644 --- a/ui-v2/GNUmakefile +++ b/ui-v2/GNUmakefile @@ -25,6 +25,9 @@ lint: deps format: deps yarn run format:js +steps: + yarn run steps:list + node_modules: yarn.lock package.json yarn install diff --git a/ui-v2/lib/commands/index.js b/ui-v2/lib/commands/index.js new file mode 100644 index 000000000000..c8c558db5410 --- /dev/null +++ b/ui-v2/lib/commands/index.js @@ -0,0 +1,19 @@ +/* eslint no-console: "off" */ +/* eslint-env node */ +'use strict'; +module.exports = { + name: 'commands', + includedCommands: function() { + return { + 'steps:list': { + name: 'steps:list', + run: function(config, args) { + require('./lib/list.js')(`${process.cwd()}/tests/steps.js`); + }, + }, + }; + }, + isDevelopingAddon() { + return true; + }, +}; diff --git a/ui-v2/lib/commands/lib/list.js b/ui-v2/lib/commands/lib/list.js new file mode 100644 index 000000000000..db868141d64f --- /dev/null +++ b/ui-v2/lib/commands/lib/list.js @@ -0,0 +1,67 @@ +/* eslint no-console: "off" */ +/* eslint-env node */ +'use strict'; +const babel = require('@babel/core'); +const read = require('fs').readFileSync; +const path = require('path'); +const vm = require('vm'); +const color = require('chalk'); + +const out = function(prefix, step, desc) { + if (!Array.isArray(step)) { + step = [step]; + } + step.forEach(function(item) { + const str = + prefix + + item.replace('\n', ' | ').replace(/\$\w+/g, function(match) { + return color.cyan(match); + }); + console.log(color.green(str)); + }); +}; +const library = { + given: function(step, cb, desc) { + out('Given ', step, desc); + return this; + }, + desc: function(desc) { + console.log(color.yellow(`- ${desc.trim()}`)); + }, + section: function() { + console.log(color.yellow(`##`)); + }, + then: function(step, cb, desc) { + out('Then ', step, desc); + return this; + }, + when: function(step, cb, desc) { + out('When ', step, desc); + return this; + }, +}; +const exec = function(filename) { + const js = read(filename); + const code = babel.transform(js.toString(), { + filename: filename, + presets: [require('babel-preset-env')], + }).code; + const exports = {}; + vm.runInNewContext( + code, + { + exports: exports, + require: function(str) { + return exec(path.resolve(`${process.cwd()}/tests`, `${str}.js`)).default; + }, + }, + { + filename: filename, + } + ); + return exports; +}; + +module.exports = function(filename) { + exec(filename).default(function() {}, library, {}, {}, {}, function() {}); +}; diff --git a/ui-v2/lib/commands/package.json b/ui-v2/lib/commands/package.json new file mode 100644 index 000000000000..722b9500ec85 --- /dev/null +++ b/ui-v2/lib/commands/package.json @@ -0,0 +1,6 @@ +{ + "name": "commands", + "keywords": [ + "ember-addon" + ] +} diff --git a/ui-v2/package.json b/ui-v2/package.json index ccdf44f5d17b..507717e5ee89 100644 --- a/ui-v2/package.json +++ b/ui-v2/package.json @@ -20,7 +20,8 @@ "test-parallel": "EMBER_EXAM_PARALLEL=true ember exam --split=4 --parallel", "test:view": "ember test --server --test-port=${EMBER_TEST_PORT:-7357}", "test:coverage": "COVERAGE=true ember test --test-port=${EMBER_TEST_PORT:-7357}", - "test:view:coverage": "COVERAGE=true ember test --server --test-port=${EMBER_TEST_PORT:-7357}" + "test:view:coverage": "COVERAGE=true ember test --server --test-port=${EMBER_TEST_PORT:-7357}", + "steps:list": "node ./lib/commands/bin/list.js" }, "husky": { "hooks": { @@ -38,11 +39,13 @@ ] }, "devDependencies": { + "@babel/core": "^7.2.2", "@hashicorp/consul-api-double": "^2.0.1", "@hashicorp/ember-cli-api-double": "^1.3.0", "babel-plugin-transform-object-rest-spread": "^6.26.0", "base64-js": "^1.3.0", "broccoli-asset-rev": "^2.4.5", + "chalk": "^2.4.2", "dart-sass": "^1.14.1", "ember-ajax": "^3.0.0", "ember-browserify": "^1.2.2", @@ -104,7 +107,8 @@ "ember-addon": { "paths": [ "lib/startup", - "lib/block-slots" + "lib/block-slots", + "lib/commands" ] } } diff --git a/ui-v2/tests/acceptance/steps/steps.js b/ui-v2/tests/acceptance/steps/steps.js index df45d3f2843b..c8da3b0a2b98 100644 --- a/ui-v2/tests/acceptance/steps/steps.js +++ b/ui-v2/tests/acceptance/steps/steps.js @@ -1,2 +1,83 @@ +/* eslint no-console: "off" */ +import Inflector from 'ember-inflector'; +import utils from '@ember/test-helpers'; +import getDictionary from '@hashicorp/ember-cli-api-double/dictionary'; + +import yadda from 'consul-ui/tests/helpers/yadda'; +import pages from 'consul-ui/tests/pages'; +import api from 'consul-ui/tests/helpers/api'; + import steps from 'consul-ui/tests/steps'; -export default steps; + +const pluralize = function(str) { + return Inflector.inflector.pluralize(str); +}; +export default function(assert) { + const library = yadda.localisation.English.library( + getDictionary(function(model, cb) { + switch (model) { + case 'datacenter': + case 'datacenters': + case 'dcs': + model = 'dc'; + break; + case 'services': + model = 'service'; + break; + case 'nodes': + model = 'node'; + break; + case 'kvs': + model = 'kv'; + break; + case 'acls': + model = 'acl'; + break; + case 'sessions': + model = 'session'; + break; + case 'intentions': + model = 'intention'; + break; + } + cb(null, model); + }, yadda) + ); + const create = function(number, name, value) { + // don't return a promise here as + // I don't need it to wait + api.server.createList(name, number, value); + }; + const respondWith = function(url, data) { + api.server.respondWith(url.split('?')[0], data); + }; + const setCookie = function(key, value) { + api.server.setCookie(key, value); + }; + const getLastNthRequest = function(arr) { + return function(n, method) { + let requests = arr.slice(0).reverse(); + if (method) { + requests = requests.filter(function(item) { + return item.method === method; + }); + } + if (n == null) { + return requests; + } + return requests[n]; + }; + }; + return steps(assert, library, pages, { + pluralize: pluralize, + triggerKeyEvent: utils.triggerKeyEvent, + currentURL: utils.currentURL, + click: utils.click, + fillIn: utils.fillIn, + find: utils.find, + lastNthRequest: getLastNthRequest(api.server.history), + respondWith: respondWith, + create: create, + set: setCookie, + }); +} diff --git a/ui-v2/tests/steps.js b/ui-v2/tests/steps.js index d0adc96f5921..cad75cceef76 100644 --- a/ui-v2/tests/steps.js +++ b/ui-v2/tests/steps.js @@ -1,521 +1,38 @@ -/* eslint no-console: "off" */ -import Inflector from 'ember-inflector'; -import yadda from './helpers/yadda'; -import { currentURL, click, triggerKeyEvent, fillIn, find } from '@ember/test-helpers'; -import getDictionary from '@hashicorp/ember-cli-api-double/dictionary'; -import pages from 'consul-ui/tests/pages'; -import api from 'consul-ui/tests/helpers/api'; -// const dont = `( don't| shouldn't| can't)?`; -const pluralize = function(str) { - return Inflector.inflector.pluralize(str); -}; -const create = function(number, name, value) { - // don't return a promise here as - // I don't need it to wait - api.server.createList(name, number, value); -}; -const lastRequest = function(method) { - return api.server.history - .slice(0) - .reverse() - .find(function(item) { - return item.method === method; - }); -}; -const fillInElement = function(page, name, value) { - const cm = document.querySelector(`textarea[name="${name}"] + .CodeMirror`); - if (cm) { - cm.CodeMirror.setValue(value); - return page; - } else { - return page.fillIn(name, value); - } -}; -var currentPage; -export default function(assert) { - return ( - yadda.localisation.English.library( - getDictionary(function(model, cb) { - switch (model) { - case 'datacenter': - case 'datacenters': - case 'dcs': - model = 'dc'; - break; - case 'services': - model = 'service'; - break; - case 'nodes': - model = 'node'; - break; - case 'kvs': - model = 'kv'; - break; - case 'acls': - model = 'acl'; - break; - case 'sessions': - model = 'session'; - break; - case 'intentions': - model = 'intention'; - break; - } - cb(null, model); - }, yadda) - ) - // doubles - .given(['an external edit results in $number $model model[s]?'], function(number, model) { - return create(number, model); - }) - .given(['$number $model model[s]?'], function(number, model) { - return create(number, model); - }) - .given(['$number $model model[s]? with the value "$value"'], function(number, model, value) { - return create(number, model, value); - }) - .given( - ['$number $model model[s]? from yaml\n$yaml', '$number $model model[s]? from json\n$json'], - function(number, model, data) { - return create(number, model, data); - } - ) - .given(['settings from yaml\n$yaml'], function(data) { - return Object.keys(data).forEach(function(key) { - window.localStorage[key] = JSON.stringify(data[key]); - }); - }) - .given('a network latency of $number', function(number) { - api.server.setCookie('CONSUL_LATENCY', number); - }) - .given(["I'm using a legacy token"], function() { - window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' }); - }) - // TODO: Abstract this away from HTTP - .given(['the url "$url" responds with a $status status'], function(url, status) { - return api.server.respondWithStatus(url.split('?')[0], parseInt(status)); - }) - .given(['the url "$url" responds with from yaml\n$yaml'], function(url, data) { - api.server.respondWith(url.split('?')[0], data); - }) - // interactions - .when('I visit the $name page', function(name) { - currentPage = pages[name]; - return currentPage.visit(); - }) - .when('I visit the $name page for the "$id" $model', function(name, id, model) { - currentPage = pages[name]; - return currentPage.visit({ - [model]: id, - }); - }) - .when( - ['I visit the $name page for yaml\n$yaml', 'I visit the $name page for json\n$json'], - function(name, data) { - currentPage = pages[name]; - // TODO: Consider putting an assertion here for testing the current url - // do I absolutely definitely need that all the time? - return pages[name].visit(data); - } - ) - .when('I click "$selector"', function(selector) { - return click(selector); - }) - // TODO: Probably nicer to think of better vocab than having the 'without " rule' - .when('I click (?!")$property(?!")', function(property) { - try { - return currentPage[property](); - } catch (e) { - console.error(e); - throw new Error(`The '${property}' property on the page object doesn't exist`); - } - }) - .when('I click $prop on the $component', function(prop, component) { - // Collection - var obj; - if (typeof currentPage[component].objectAt === 'function') { - obj = currentPage[component].objectAt(0); - } else { - obj = currentPage[component]; - } - const func = obj[prop].bind(obj); - try { - return func(); - } catch (e) { - throw new Error( - `The '${prop}' property on the '${component}' page object doesn't exist.\n${e.message}` - ); - } - }) - .when('I submit', function(selector) { - return currentPage.submit(); - }) - .then('I fill in "$name" with "$value"', function(name, value) { - return currentPage.fillIn(name, value); - }) - .then(['I fill in with yaml\n$yaml', 'I fill in with json\n$json'], function(data) { - return Object.keys(data).reduce(function(prev, item, i, arr) { - return fillInElement(prev, item, data[item]); - }, currentPage); - }) - .then( - ['I fill in the $form form with yaml\n$yaml', 'I fill in the $form with json\n$json'], - function(form, data) { - return Object.keys(data).reduce(function(prev, item, i, arr) { - const name = `${form}[${item}]`; - return fillInElement(prev, name, data[item]); - }, currentPage); - } - ) - .then(['I type "$text" into "$selector"'], function(text, selector) { - return fillIn(selector, text); - }) - .then(['I type with yaml\n$yaml'], function(data) { - const keys = Object.keys(data); - return keys - .reduce(function(prev, item, i, arr) { - return prev.fillIn(item, data[item]); - }, currentPage) - .then(function() { - return Promise.all( - keys.map(function(item) { - return triggerKeyEvent(`[name="${item}"]`, 'keyup', 83); // TODO: This is 's', be more generic - }) - ); - }); - }) - // debugging helpers - .then('print the current url', function(url) { - console.log(currentURL()); - return Promise.resolve(); - }) - .then('log the "$text"', function(text) { - console.log(text); - return Promise.resolve(); - }) - .then('pause for $milliseconds', function(milliseconds) { - return new Promise(function(resolve) { - setTimeout(resolve, milliseconds); - }); - }) - // assertions - .then('pause until I see $number $model model[s]?', function(num, model) { - return new Promise(function(resolve) { - let count = 0; - const interval = setInterval(function() { - if (++count >= 50) { - clearInterval(interval); - assert.ok(false); - resolve(); - } - const len = currentPage[`${pluralize(model)}`].filter(function(item) { - return item.isVisible; - }).length; - if (len === num) { - clearInterval(interval); - assert.equal(len, num, `Expected ${num} ${model}s, saw ${len}`); - resolve(); - } - }, 100); - }); - }) - .then('a $method request is made to "$url" with the body from yaml\n$yaml', function( - method, - url, - data - ) { - const request = api.server.history[api.server.history.length - 2]; - assert.equal( - request.method, - method, - `Expected the request method to be ${method}, was ${request.method}` - ); - assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`); - const body = JSON.parse(request.requestBody); - Object.keys(data).forEach(function(key, i, arr) { - assert.deepEqual( - body[key], - data[key], - `Expected the payload to contain ${key} equaling ${data[key]}, ${key} was ${body[key]}` - ); - }); - }) - // TODO: This one can replace the above one, it covers more use cases - // also DRY it out a bit - .then('a $method request is made to "$url" from yaml\n$yaml', function(method, url, yaml) { - const request = api.server.history[api.server.history.length - 2]; - assert.equal( - request.method, - method, - `Expected the request method to be ${method}, was ${request.method}` - ); - assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`); - let data = yaml.body || {}; - const body = JSON.parse(request.requestBody); - Object.keys(data).forEach(function(key, i, arr) { - assert.equal( - body[key], - data[key], - `Expected the payload to contain ${key} to equal ${body[key]}, ${key} was ${data[key]}` - ); - }); - data = yaml.headers || {}; - const headers = request.requestHeaders; - Object.keys(data).forEach(function(key, i, arr) { - assert.equal( - headers[key], - data[key], - `Expected the payload to contain ${key} to equal ${headers[key]}, ${key} was ${ - data[key] - }` - ); - }); - }) - .then('a $method request is made to "$url" with the body "$body"', function( - method, - url, - data - ) { - const request = api.server.history[api.server.history.length - 2]; - assert.equal( - request.method, - method, - `Expected the request method to be ${method}, was ${request.method}` - ); - assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`); - const body = request.requestBody; - assert.equal(body, data, `Expected the request body to be ${data}, was ${body}`); - }) - .then('a $method request is made to "$url" with no body', function(method, url) { - const request = api.server.history[api.server.history.length - 2]; - assert.equal( - request.method, - method, - `Expected the request method to be ${method}, was ${request.method}` - ); - assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`); - const body = request.requestBody; - assert.equal(body, null, `Expected the request body to be null, was ${body}`); - }) +import models from './steps/doubles/model'; +import http from './steps/doubles/http'; +import visit from './steps/interactions/visit'; +import click from './steps/interactions/click'; +import form from './steps/interactions/form'; +import debug from './steps/debug/index'; +import assertHttp from './steps/assertions/http'; +import assertModel from './steps/assertions/model'; +import assertPage from './steps/assertions/page'; +import assertDom from './steps/assertions/dom'; - .then('a $method request is made to "$url"', function(method, url) { - const request = api.server.history[api.server.history.length - 2]; - assert.equal( - request.method, - method, - `Expected the request method to be ${method}, was ${request.method}` - ); - assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`); - }) - .then('the last $method request was made to "$url"', function(method, url) { - const request = lastRequest(method); - assert.equal( - request.method, - method, - `Expected the request method to be ${method}, was ${request.method}` - ); - assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`); - }) - .then('the last $method request was made to "$url" with the body from yaml\n$yaml', function( - method, - url, - data - ) { - const request = lastRequest(method); - assert.ok(request, `Expected a ${method} request`); - assert.equal( - request.method, - method, - `Expected the request method to be ${method}, was ${request.method}` - ); - assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`); - const body = JSON.parse(request.requestBody); - Object.keys(data).forEach(function(key, i, arr) { - assert.deepEqual( - body[key], - data[key], - `Expected the payload to contain ${key} equaling ${data[key]}, ${key} was ${body[key]}` - ); - }); - }) - .then('the last $method requests were like yaml\n$yaml', function(method, data) { - const requests = api.server.history.reverse().filter(function(item) { - return item.method === method; - }); - data.reverse().forEach(function(item, i, arr) { - assert.equal( - requests[i].url, - item, - `Expected the request url to be ${item}, was ${requests[i].url}` - ); - }); - }) - .then('the url should be $url', function(url) { - // TODO: nice! $url should be wrapped in "" - if (url === "''") { - url = ''; - } - const current = currentURL() || ''; - assert.equal(current, url, `Expected the url to be ${url} was ${current}`); - }) - .then(['I see $num $model', 'I see $num $model model', 'I see $num $model models'], function( - num, - model - ) { - const len = currentPage[pluralize(model)].filter(function(item) { - return item.isVisible; - }).length; +// const dont = `( don't| shouldn't| can't)?`; - assert.equal(len, num, `Expected ${num} ${pluralize(model)}, saw ${len}`); - }) - // TODO: I${ dont } see - .then([`I see $num $model model[s]? with the $property "$value"`], function( - // negate, - num, - model, - property, - value - ) { - const len = currentPage[pluralize(model)].filter(function(item) { - return item.isVisible && item[property] == value; - }).length; - assert.equal( - len, - num, - `Expected ${num} ${pluralize(model)} with ${property} set to "${value}", saw ${len}` - ); - }) - // TODO: Make this accept a 'contains' word so you can search for text containing also - .then('I have settings like yaml\n$yaml', function(data) { - // TODO: Inject this - const settings = window.localStorage; - // TODO: this and the setup should probably use consul: - // as we are talking about 'settings' here not localStorage - // so the prefix should be hidden - Object.keys(data).forEach(function(prop) { - const actual = settings.getItem(prop); - const expected = data[prop]; - assert.strictEqual(actual, expected, `Expected settings to be ${expected} was ${actual}`); - }); - }) - .then('I see $property on the $component like yaml\n$yaml', function( - property, - component, - yaml - ) { - const _component = currentPage[component]; - const iterator = new Array(_component.length).fill(true); - // this will catch if we get aren't managing to select a component - assert.ok(iterator.length > 0); - iterator.forEach(function(item, i, arr) { - const actual = - typeof _component.objectAt(i)[property] === 'undefined' - ? null - : _component.objectAt(i)[property]; +export default function(assert, library, pages, utils) { + var currentPage; + const getCurrentPage = function() { + return currentPage; + }; + const setCurrentPage = function(page) { + currentPage = page; + return page; + }; - // anything coming from the DOM is going to be text/strings - // if the yaml has numbers, cast them to strings - // TODO: This would get problematic for deeper objects - // will have to look to do this recursively - const expected = typeof yaml[i] === 'number' ? yaml[i].toString() : yaml[i]; + models(library, utils.create); + http(library, utils.respondWith, utils.set); + visit(library, pages, setCurrentPage); + click(library, utils.click, getCurrentPage); + form(library, utils.fillIn, utils.triggerKeyEvent, getCurrentPage); + debug(library, assert, utils.currentURL); + assertHttp(library, assert, utils.lastNthRequest); + assertModel(library, assert, getCurrentPage, utils.pluralize); + assertPage(library, assert, getCurrentPage); + assertDom(library, assert, utils.find, utils.currentURL); - assert.deepEqual( - actual, - expected, - `Expected to see ${property} on ${component}[${i}] as ${JSON.stringify( - expected - )}, was ${JSON.stringify(actual)}` - ); - }); - }) - .then(['I see $property on the $component'], function(property, component) { - // TODO: Time to work on repetition - // Collection - var obj; - if (typeof currentPage[component].objectAt === 'function') { - obj = currentPage[component].objectAt(0); - } else { - obj = currentPage[component]; - } - let _component; - if (typeof obj === 'function') { - const func = obj[property].bind(obj); - try { - _component = func(); - } catch (e) { - console.error(e); - throw new Error( - `The '${property}' property on the '${component}' page object doesn't exist` - ); - } - } else { - _component = obj; - } - assert.ok(_component[property], `Expected to see ${property} on ${component}`); - }) - .then(["I don't see $property on the $component"], function(property, component) { - // Collection - var obj; - if (typeof currentPage[component].objectAt === 'function') { - obj = currentPage[component].objectAt(0); - } else { - obj = currentPage[component]; - } - const func = obj[property].bind(obj); - assert.throws( - function() { - func(); - }, - function(e) { - return e.toString().indexOf('Element not found') !== -1; - }, - `Expected to not see ${property} on ${component}` - ); - }) - .then(["I don't see $property"], function(property) { - assert.throws( - function() { - currentPage[property](); - }, - function(e) { - return e.toString().indexOf('Element not found') !== -1; - }, - `Expected to not see ${property}` - ); - }) - .then(['I see $property'], function(property) { - assert.ok(currentPage[property], `Expected to see ${property}`); - }) - .then(['I see $property like "$value"'], function(property, value) { - assert.equal( - currentPage[property], - value, - `Expected to see ${property}, was ${currentPage[property]}` - ); - }) - .then(['I see the text "$text" in "$selector"'], function(text, selector) { - assert.ok( - find(selector).textContent.indexOf(text) !== -1, - `Expected to see "${text}" in "${selector}"` - ); - }) - // TODO: Think of better language - // TODO: These should be mergeable - .then(['"$selector" has the "$class" class'], function(selector, cls) { - // because `find` doesn't work, guessing its sandboxed to ember's container - assert.ok( - document.querySelector(selector).classList.contains(cls), - `Expected [class] to contain ${cls} on ${selector}` - ); - }) - .then(['"$selector" doesn\'t have the "$class" class'], function(selector, cls) { - assert.ok( - !document.querySelector(selector).classList.contains(cls), - `Expected [class] not to contain ${cls} on ${selector}` - ); - }) - .then('ok', function() { - assert.ok(true); - }) - ); + return library.given(["I'm using a legacy token"], function(number, model, data) { + window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' }); + }); } diff --git a/ui-v2/tests/steps/assertions/dom.js b/ui-v2/tests/steps/assertions/dom.js new file mode 100644 index 000000000000..ec26e165d230 --- /dev/null +++ b/ui-v2/tests/steps/assertions/dom.js @@ -0,0 +1,45 @@ +export default function(scenario, assert, find, currentURL) { + scenario + .then(['I see the text "$text" in "$selector"'], function(text, selector) { + assert.ok( + find(selector).textContent.indexOf(text) !== -1, + `Expected to see "${text}" in "${selector}"` + ); + }) + // TODO: Think of better language + // TODO: These should be mergeable + .then(['"$selector" has the "$class" class'], function(selector, cls) { + // because `find` doesn't work, guessing its sandboxed to ember's container + assert.ok( + document.querySelector(selector).classList.contains(cls), + `Expected [class] to contain ${cls} on ${selector}` + ); + }) + .then(['"$selector" doesn\'t have the "$class" class'], function(selector, cls) { + assert.ok( + !document.querySelector(selector).classList.contains(cls), + `Expected [class] not to contain ${cls} on ${selector}` + ); + }) + // TODO: Make this accept a 'contains' word so you can search for text containing also + .then('I have settings like yaml\n$yaml', function(data) { + // TODO: Inject this + const settings = window.localStorage; + // TODO: this and the setup should probably use consul: + // as we are talking about 'settings' here not localStorage + // so the prefix should be hidden + Object.keys(data).forEach(function(prop) { + const actual = settings.getItem(prop); + const expected = data[prop]; + assert.strictEqual(actual, expected, `Expected settings to be ${expected} was ${actual}`); + }); + }) + .then('the url should be $url', function(url) { + // TODO: nice! $url should be wrapped in "" + if (url === "''") { + url = ''; + } + const current = currentURL() || ''; + assert.equal(current, url, `Expected the url to be ${url} was ${current}`); + }); +} diff --git a/ui-v2/tests/steps/assertions/http.js b/ui-v2/tests/steps/assertions/http.js new file mode 100644 index 000000000000..b6dc7042f5b7 --- /dev/null +++ b/ui-v2/tests/steps/assertions/http.js @@ -0,0 +1,111 @@ +export default function(scenario, assert, lastNthRequest) { + // lastNthRequest should return a + // { + // method: '', + // requestBody: '', + // requestHeaders: '' + // } + const assertRequest = function(request, method, url) { + assert.equal( + request.method, + method, + `Expected the request method to be ${method}, was ${request.method}` + ); + assert.equal(request.url, url, `Expected the request url to be ${url}, was ${request.url}`); + }; + scenario + .then('a $method request is made to "$url" with the body from yaml\n$yaml', function( + method, + url, + data + ) { + const request = lastNthRequest(1); + assertRequest(request, method, url); + const body = JSON.parse(request.requestBody); + Object.keys(data).forEach(function(key, i, arr) { + assert.deepEqual( + body[key], + data[key], + `Expected the payload to contain ${key} equaling ${data[key]}, ${key} was ${body[key]}` + ); + }); + }) + // TODO: This one can replace the above one, it covers more use cases + // also DRY it out a bit + .then('a $method request is made to "$url" from yaml\n$yaml', function(method, url, yaml) { + const request = lastNthRequest(1); + assertRequest(request, method, url); + let data = yaml.body || {}; + const body = JSON.parse(request.requestBody); + Object.keys(data).forEach(function(key, i, arr) { + assert.equal( + body[key], + data[key], + `Expected the payload to contain ${key} to equal ${body[key]}, ${key} was ${data[key]}` + ); + }); + data = yaml.headers || {}; + const headers = request.requestHeaders; + Object.keys(data).forEach(function(key, i, arr) { + assert.equal( + headers[key], + data[key], + `Expected the payload to contain ${key} to equal ${headers[key]}, ${key} was ${data[key]}` + ); + }); + }) + .then('a $method request is made to "$url" with the body "$body"', function(method, url, data) { + const request = lastNthRequest(1); + assertRequest(request, method, url); + assert.equal( + request.requestBody, + data, + `Expected the request body to be ${data}, was ${request.requestBody}` + ); + }) + .then('a $method request is made to "$url" with no body', function(method, url) { + const request = lastNthRequest(1); + assertRequest(request, method, url); + assert.equal( + request.requestBody, + null, + `Expected the request body to be null, was ${request.requestBody}` + ); + }) + + .then('a $method request is made to "$url"', function(method, url) { + const request = lastNthRequest(1); + assertRequest(request, method, url); + }) + .then('the last $method request was made to "$url"', function(method, url) { + const request = lastNthRequest(0, method); + assertRequest(request, method, url); + }) + .then('the last $method request was made to "$url" with the body from yaml\n$yaml', function( + method, + url, + data + ) { + const request = lastNthRequest(0, method); + const body = JSON.parse(request.requestBody); + assert.ok(request, `Expected a ${method} request`); + assertRequest(request, method, url); + Object.keys(data).forEach(function(key, i, arr) { + assert.deepEqual( + body[key], + data[key], + `Expected the payload to contain ${key} equaling ${data[key]}, ${key} was ${body[key]}` + ); + }); + }) + .then('the last $method requests were like yaml\n$yaml', function(method, data) { + const requests = lastNthRequest(null, method); + data.reverse().forEach(function(item, i, arr) { + assert.equal( + requests[i].url, + item, + `Expected the request url to be ${item}, was ${requests[i].url}` + ); + }); + }); +} diff --git a/ui-v2/tests/steps/assertions/model.js b/ui-v2/tests/steps/assertions/model.js new file mode 100644 index 000000000000..82e11f647066 --- /dev/null +++ b/ui-v2/tests/steps/assertions/model.js @@ -0,0 +1,50 @@ +export default function(scenario, assert, currentPage, pluralize) { + scenario + .then('pause until I see $number $model model[s]?', function(num, model) { + return new Promise(function(resolve) { + let count = 0; + const interval = setInterval(function() { + if (++count >= 50) { + clearInterval(interval); + assert.ok(false); + resolve(); + } + const len = currentPage()[pluralize(model)].filter(function(item) { + return item.isVisible; + }).length; + if (len === num) { + clearInterval(interval); + assert.equal(len, num, `Expected ${num} ${model}s, saw ${len}`); + resolve(); + } + }, 100); + }); + }) + .then(['I see $num $model', 'I see $num $model model', 'I see $num $model models'], function( + num, + model + ) { + const len = currentPage()[pluralize(model)].filter(function(item) { + return item.isVisible; + }).length; + + assert.equal(len, num, `Expected ${num} ${pluralize(model)}, saw ${len}`); + }) + // TODO: I${ dont } see + .then([`I see $num $model model[s]? with the $property "$value"`], function( + // negate, + num, + model, + property, + value + ) { + const len = currentPage()[pluralize(model)].filter(function(item) { + return item.isVisible && item[property] == value; + }).length; + assert.equal( + len, + num, + `Expected ${num} ${pluralize(model)} with ${property} set to "${value}", saw ${len}` + ); + }); +} diff --git a/ui-v2/tests/steps/assertions/page.js b/ui-v2/tests/steps/assertions/page.js new file mode 100644 index 000000000000..043a6f4bb9c0 --- /dev/null +++ b/ui-v2/tests/steps/assertions/page.js @@ -0,0 +1,99 @@ +/* eslint no-console: "off" */ +export default function(scenario, assert, currentPage) { + scenario + .then('I see $property on the $component like yaml\n$yaml', function( + property, + component, + yaml + ) { + const _component = currentPage()[component]; + const iterator = new Array(_component.length).fill(true); + // this will catch if we get aren't managing to select a component + assert.ok(iterator.length > 0); + iterator.forEach(function(item, i, arr) { + const actual = + typeof _component.objectAt(i)[property] === 'undefined' + ? null + : _component.objectAt(i)[property]; + + // anything coming from the DOM is going to be text/strings + // if the yaml has numbers, cast them to strings + // TODO: This would get problematic for deeper objects + // will have to look to do this recursively + const expected = typeof yaml[i] === 'number' ? yaml[i].toString() : yaml[i]; + + assert.deepEqual( + actual, + expected, + `Expected to see ${property} on ${component}[${i}] as ${JSON.stringify( + expected + )}, was ${JSON.stringify(actual)}` + ); + }); + }) + .then(['I see $property on the $component'], function(property, component) { + // TODO: Time to work on repetition + // Collection + var obj; + if (typeof currentPage()[component].objectAt === 'function') { + obj = currentPage()[component].objectAt(0); + } else { + obj = currentPage()[component]; + } + let _component; + if (typeof obj === 'function') { + const func = obj[property].bind(obj); + try { + _component = func(); + } catch (e) { + console.error(e); + throw new Error( + `The '${property}' property on the '${component}' page object doesn't exist` + ); + } + } else { + _component = obj; + } + assert.ok(_component[property], `Expected to see ${property} on ${component}`); + }) + .then(["I don't see $property on the $component"], function(property, component) { + // Collection + var obj; + if (typeof currentPage()[component].objectAt === 'function') { + obj = currentPage()[component].objectAt(0); + } else { + obj = currentPage()[component]; + } + const func = obj[property].bind(obj); + assert.throws( + function() { + func(); + }, + function(e) { + return e.toString().indexOf('Element not found') !== -1; + }, + `Expected to not see ${property} on ${component}` + ); + }) + .then(["I don't see $property"], function(property) { + assert.throws( + function() { + currentPage()[property](); + }, + function(e) { + return e.toString().indexOf('Element not found') !== -1; + }, + `Expected to not see ${property}` + ); + }) + .then(['I see $property'], function(property) { + assert.ok(currentPage()[property], `Expected to see ${property}`); + }) + .then(['I see $property like "$value"'], function(property, value) { + assert.equal( + currentPage()[property], + value, + `Expected to see ${property}, was ${currentPage()[property]}` + ); + }); +} diff --git a/ui-v2/tests/steps/debug/index.js b/ui-v2/tests/steps/debug/index.js new file mode 100644 index 000000000000..79622ad922ce --- /dev/null +++ b/ui-v2/tests/steps/debug/index.js @@ -0,0 +1,20 @@ +/* eslint no-console: "off" */ +export default function(scenario, assert, currentURL) { + scenario + .then('print the current url', function(url) { + console.log(currentURL()); + return Promise.resolve(); + }) + .then('log the "$text"', function(text) { + console.log(text); + return Promise.resolve(); + }) + .then('pause for $milliseconds', function(milliseconds) { + return new Promise(function(resolve) { + setTimeout(resolve, milliseconds); + }); + }) + .then('ok', function() { + assert.ok(true); + }); +} diff --git a/ui-v2/tests/steps/doubles/http.js b/ui-v2/tests/steps/doubles/http.js new file mode 100644 index 000000000000..67a725b5359b --- /dev/null +++ b/ui-v2/tests/steps/doubles/http.js @@ -0,0 +1,15 @@ +export default function(scenario, respondWith, set) { + // respondWith should set the url to return a certain response shape + scenario + .given(['the url "$url" responds with a $status status'], function(url, status) { + respondWith(url, { + status: parseInt(status), + }); + }) + .given(['the url "$url" responds with from yaml\n$yaml'], function(url, data) { + respondWith(url, data); + }) + .given('a network latency of $number', function(number) { + set('CONSUL_LATENCY', number); + }); +} diff --git a/ui-v2/tests/steps/doubles/model.js b/ui-v2/tests/steps/doubles/model.js new file mode 100644 index 000000000000..f02bdbf50a76 --- /dev/null +++ b/ui-v2/tests/steps/doubles/model.js @@ -0,0 +1,22 @@ +export default function(scenario, create) { + scenario + .given(['an external edit results in $number $model model[s]?'], function(number, model) { + return create(number, model); + }) + .given(['$number $model model[s]?'], function(number, model) { + return create(number, model); + }) + .given(['$number $model model[s]? with the value "$value"'], function(number, model, value) { + return create(number, model, value); + }) + .given( + ['$number $model model[s]? from yaml\n$yaml', '$number $model model[s]? from json\n$json'], + function(number, model, data) { + return create(number, model, data); + } + ).given(['settings from yaml\n$yaml'], function(data) { + return Object.keys(data).forEach(function(key) { + window.localStorage[key] = JSON.stringify(data[key]); + }); + }); +} diff --git a/ui-v2/tests/steps/interactions/click.js b/ui-v2/tests/steps/interactions/click.js new file mode 100644 index 000000000000..aa71d1d31e47 --- /dev/null +++ b/ui-v2/tests/steps/interactions/click.js @@ -0,0 +1,33 @@ +/* eslint no-console: "off" */ +export default function(scenario, click, currentPage) { + scenario + .when('I click "$selector"', function(selector) { + return click(selector); + }) + // TODO: Probably nicer to think of better vocab than having the 'without " rule' + .when('I click (?!")$property(?!")', function(property) { + try { + return currentPage()[property](); + } catch (e) { + console.error(e); + throw new Error(`The '${property}' property on the page object doesn't exist`); + } + }) + .when('I click $prop on the $component', function(prop, component) { + // Collection + var obj; + if (typeof currentPage()[component].objectAt === 'function') { + obj = currentPage()[component].objectAt(0); + } else { + obj = currentPage()[component]; + } + const func = obj[prop].bind(obj); + try { + return func(); + } catch (e) { + throw new Error( + `The '${prop}' property on the '${component}' page object doesn't exist.\n${e.message}` + ); + } + }); +} diff --git a/ui-v2/tests/steps/interactions/form.js b/ui-v2/tests/steps/interactions/form.js new file mode 100644 index 000000000000..8ef2812f3b9b --- /dev/null +++ b/ui-v2/tests/steps/interactions/form.js @@ -0,0 +1,49 @@ +export default function(scenario, fillIn, triggerKeyEvent, currentPage) { + const fillInElement = function(page, name, value) { + const cm = document.querySelector(`textarea[name="${name}"] + .CodeMirror`); + if (cm) { + cm.CodeMirror.setValue(value); + return page; + } else { + return page.fillIn(name, value); + } + }; + scenario + .when('I submit', function(selector) { + return currentPage().submit(); + }) + .then('I fill in "$name" with "$value"', function(name, value) { + return currentPage().fillIn(name, value); + }) + .then(['I fill in with yaml\n$yaml', 'I fill in with json\n$json'], function(data) { + return Object.keys(data).reduce(function(prev, item, i, arr) { + return fillInElement(prev, item, data[item]); + }, currentPage()); + }) + .then( + ['I fill in the $form form with yaml\n$yaml', 'I fill in the $form with json\n$json'], + function(form, data) { + return Object.keys(data).reduce(function(prev, item, i, arr) { + const name = `${form}[${item}]`; + return fillInElement(prev, name, data[item]); + }, currentPage()); + } + ) + .then(['I type "$text" into "$selector"'], function(text, selector) { + return fillIn(selector, text); + }) + .then(['I type with yaml\n$yaml'], function(data) { + const keys = Object.keys(data); + return keys + .reduce(function(prev, item, i, arr) { + return prev.fillIn(item, data[item]); + }, currentPage()) + .then(function() { + return Promise.all( + keys.map(function(item) { + return triggerKeyEvent(`[name="${item}"]`, 'keyup', 83); // TODO: This is 's', be more generic + }) + ); + }); + }); +} diff --git a/ui-v2/tests/steps/interactions/visit.js b/ui-v2/tests/steps/interactions/visit.js new file mode 100644 index 000000000000..6a2ff76dc12e --- /dev/null +++ b/ui-v2/tests/steps/interactions/visit.js @@ -0,0 +1,19 @@ +export default function(scenario, pages, set) { + scenario + .when('I visit the $name page', function(name) { + return set(pages[name]).visit(); + }) + .when('I visit the $name page for the "$id" $model', function(name, id, model) { + return set(pages[name]).visit({ + [model]: id, + }); + }) + .when( + ['I visit the $name page for yaml\n$yaml', 'I visit the $name page for json\n$json'], + function(name, data) { + // TODO: Consider putting an assertion here for testing the current url + // do I absolutely definitely need that all the time? + return set(pages[name]).visit(data); + } + ); +} diff --git a/ui-v2/yarn.lock b/ui-v2/yarn.lock index 819d09391838..705593f05716 100644 --- a/ui-v2/yarn.lock +++ b/ui-v2/yarn.lock @@ -27,6 +27,26 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.2.2": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.3.tgz#d090d157b7c5060d05a05acaebc048bd2b037947" + integrity sha512-w445QGI2qd0E0GlSnq6huRZWPMmQGCp5gd5ZWS4hagn0EiwzxD5QMFkpchyusAyVC1n27OKXzQ0/88aVU9n4xQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.3.3" + "@babel/helpers" "^7.2.0" + "@babel/parser" "^7.3.3" + "@babel/template" "^7.2.2" + "@babel/traverse" "^7.2.2" + "@babel/types" "^7.3.3" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.11" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.0.0", "@babel/generator@^7.1.2": version "7.1.2" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.1.2.tgz#fde75c072575ce7abbd97322e8fef5bae67e4630" @@ -37,6 +57,17 @@ source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.2.2", "@babel/generator@^7.3.3": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.3.tgz#185962ade59a52e00ca2bdfcfd1d58e528d4e39e" + integrity sha512-aEADYwRRZjJyMnKN7llGIlircxTCofm3dtV5pmY6ob18MSIuipHpA2yZWkPlycwu5HJcx/pADS3zssd8eY7/6A== + dependencies: + "@babel/types" "^7.3.3" + 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" @@ -181,6 +212,15 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.1.2" +"@babel/helpers@^7.2.0": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9" + integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA== + dependencies: + "@babel/template" "^7.1.2" + "@babel/traverse" "^7.1.5" + "@babel/types" "^7.3.0" + "@babel/highlight@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" @@ -193,6 +233,11 @@ version "7.1.2" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.2.tgz#85c5c47af6d244fab77bce6b9bd830e38c978409" +"@babel/parser@^7.2.2", "@babel/parser@^7.2.3", "@babel/parser@^7.3.3": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.3.tgz#092d450db02bdb6ccb1ca8ffd47d8774a91aef87" + integrity sha512-xsH1CJoln2r74hR+y7cg2B5JCPaTh+Hd+EbBRk9nWGSNspuo6krjhX0Om6RnRQuIvFq8wVXCLKH3kwKDYhanSg== + "@babel/plugin-proposal-async-generator-functions@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.1.0.tgz#41c1a702e10081456e23a7b74d891922dd1bb6ce" @@ -503,6 +548,15 @@ "@babel/parser" "^7.1.2" "@babel/types" "^7.1.2" +"@babel/template@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" + integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.2.2" + "@babel/types" "^7.2.2" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0": version "7.1.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.1.0.tgz#503ec6669387efd182c3888c4eec07bcc45d91b2" @@ -517,6 +571,21 @@ globals "^11.1.0" lodash "^4.17.10" +"@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" + integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.2.2" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/parser" "^7.2.3" + "@babel/types" "^7.2.2" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.10" + "@babel/types@^7.0.0", "@babel/types@^7.1.2": version "7.1.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.1.2.tgz#183e7952cf6691628afdc2e2b90d03240bac80c0" @@ -525,6 +594,15 @@ lodash "^4.17.10" to-fast-properties "^2.0.0" +"@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.3.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.3.tgz#6c44d1cdac2a7625b624216657d5bc6c107ab436" + integrity sha512-2tACZ80Wg09UnPg5uGAOUvvInaqLk3l/IAhQzlxLQOIXacr6bMsra5SH6AWw/hIDRCSbCdHP2KzSOD+cT7TzMQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + "@ember/ordered-set@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@ember/ordered-set/-/ordered-set-1.0.0.tgz#cf9ab5fd7510bcad370370ebcded705f6d1c542b" @@ -1231,6 +1309,7 @@ babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: babel-core@^6.14.0, babel-core@^6.26.0, babel-core@^6.26.3: version "6.26.3" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" + integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== dependencies: babel-code-frame "^6.26.0" babel-generator "^6.26.0" @@ -2821,6 +2900,15 @@ chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +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== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@~0.4.0: version "0.4.0" resolved "http://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" @@ -6989,6 +7077,13 @@ json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +json5@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" + integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + dependencies: + minimist "^1.2.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -7594,9 +7689,10 @@ lodash@^4.14.0, lodash@^4.17.4: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" -lodash@^4.17.10, lodash@^4.17.5, lodash@~4.17.10: +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@~4.17.10: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== log-symbols@^1.0.2: version "1.0.2"