diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 49d7a05..94fc594 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,6 +26,7 @@ e2e_tests: - yarn build - yarn e2e artifacts: + when: on_failure paths: - testcafe/screenshots/latest - testcafe/screenshots/diffs diff --git a/README.md b/README.md index 15928a2..6f877e0 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,6 @@ For now, I would ask to please consider the technical aspects involved, like: * [Re-ducks](https://github.com/alexnm/re-ducks) modular architecture * CSS Grid * Styles created using [SASS](https://sass-lang.com/) with the [BEM methodology](http://getbem.com/); -* [Typescript](https://www.typescriptlang.org) \ No newline at end of file +* [Typescript](https://www.typescriptlang.org) +* Testing with [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) +* End-to-end tests using [TestCafe](https://devexpress.github.io/testcafe/documentation/getting-started/) and [gherkin-testcafe](https://github.com/Dbuggerx/gherkin-testcafe) \ No newline at end of file diff --git a/package.json b/package.json index cb90040..9bfc73a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "path-to-regexp": "^3.0.0", "react": "^16.8.6", "react-dom": "^16.8.6", - "react-hot-loader": "^4.11.1", + "react-hot-loader": "^4.12.0", "react-redux": "^7.1.0", "react-router": "^5.0.1", "react-router-dom": "^5.0.1", @@ -73,6 +73,7 @@ "eslint-plugin-jest": "^22.7.1", "eslint-plugin-react": "^7.14.2", "eslint-plugin-react-hooks": "^1.6.1", + "file-loader": "^4.0.0", "gherkin-testcafe": "https://github.com/Dbuggerx/gherkin-testcafe.git", "html-webpack-plugin": "^3.2.0", "identity-obj-proxy": "^3.0.0", @@ -96,7 +97,7 @@ "stylelint-webpack-plugin": "^0.10.5", "testcafe": "^1.2.1", "typescript": "^3.5.2", - "webpack": "^4.35.0", + "webpack": "^4.35.2", "webpack-bundle-analyzer": "^3.3.2", "webpack-cli": "^3.3.5", "webpack-dev-server": "^3.7.2", diff --git a/src/components/BookCard/BookCard.scss b/src/components/BookCard/BookCard.scss index 411ff9a..5bf6891 100644 --- a/src/components/BookCard/BookCard.scss +++ b/src/components/BookCard/BookCard.scss @@ -4,7 +4,7 @@ .book-card { $card-border: solid 1px get($colors, border); - font-family: 'Roboto', Arial, Verdana, Tahoma, sans-serif; + font-family: $default-font-family; border: $card-border; border-radius: get($spaces, border-radius); box-shadow: 0 2px 4px get($colors, greys, 2000); diff --git a/src/components/Dropdown/Dropdown.scss b/src/components/Dropdown/Dropdown.scss index 8a0abee..e5af8db 100644 --- a/src/components/Dropdown/Dropdown.scss +++ b/src/components/Dropdown/Dropdown.scss @@ -9,7 +9,7 @@ vertical-align: middle; position: relative; width: 100%; - font-family: 'Roboto', Arial, Verdana, Tahoma, sans-serif; + font-family: $default-font-family; &__button { @extend %input-color; diff --git a/src/components/Info/Info.scss b/src/components/Info/Info.scss new file mode 100644 index 0000000..b712ec1 --- /dev/null +++ b/src/components/Info/Info.scss @@ -0,0 +1,6 @@ +@import '../scss/variables'; + +/* @define info */ +.info { + font-family: $default-font-family; +} diff --git a/src/components/Info/index.tsx b/src/components/Info/index.tsx new file mode 100644 index 0000000..47611e9 --- /dev/null +++ b/src/components/Info/index.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import './Info.scss'; + +const Info = () => ( +
+

This is still a work in progress

+ For now, I would ask to please consider the technical aspects involved, like: + +
+); + +Info.displayName = 'Info'; + +export default Info; diff --git a/src/components/MainLayout/MainLayout.scss b/src/components/MainLayout/MainLayout.scss index bc8c301..268788a 100644 --- a/src/components/MainLayout/MainLayout.scss +++ b/src/components/MainLayout/MainLayout.scss @@ -16,7 +16,7 @@ body, 'search .' 'books info' 'pagination info'; - font-family: Arial, Helvetica, sans-serif; + font-family: $default-font-family; height: 100vh; width: 100vw; diff --git a/src/components/MainLayout/index.tsx b/src/components/MainLayout/index.tsx index a1307e1..f77febc 100644 --- a/src/components/MainLayout/index.tsx +++ b/src/components/MainLayout/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import './MainLayout.scss'; +import Info from '../Info'; type Props = { loadingBooks: boolean; @@ -19,103 +20,7 @@ const MainLayout = (props: Props) => (
{props.searchForm}
{props.pagination}
-

This is still a work in progress

- For now, I would ask to please consider the technical aspects involved, like: - +
); diff --git a/src/components/SearchForm/SearchForm.scss b/src/components/SearchForm/SearchForm.scss index 7c8e0b5..f164c64 100644 --- a/src/components/SearchForm/SearchForm.scss +++ b/src/components/SearchForm/SearchForm.scss @@ -34,7 +34,7 @@ border: solid 1px get($colors, greys, 2000); padding: get($spaces, 500) get($spaces, 1000); outline: 0; - font-family: 'Roboto', Arial, Verdana, Tahoma, sans-serif; + font-family: $default-font-family; font-size: 1em; &::placeholder { diff --git a/src/components/SearchForm/index.tsx b/src/components/SearchForm/index.tsx index b398da5..4fddf54 100644 --- a/src/components/SearchForm/index.tsx +++ b/src/components/SearchForm/index.tsx @@ -36,6 +36,8 @@ const SearchForm = (props: Props) => { props.search(category, genre, query); }; + const renderDropdownItem = useCallback((item: SearchParam) => ({ id: item.id, el: item.label }), []); + return (
{(props.selectedCategory || props.selectedGenre || props.selectedQuery) && ( @@ -76,7 +78,7 @@ const SearchForm = (props: Props) => {
items={props.availableCategories || []} - renderItem={i => ({ id: i.id, el: i.label })} + renderItem={renderDropdownItem} onSelect={handleCategorySelected} placeholder="Category" /> @@ -84,7 +86,7 @@ const SearchForm = (props: Props) => {
items={props.availableGenres || []} - renderItem={i => ({ id: i.id, el: i.label })} + renderItem={renderDropdownItem} onSelect={handleGenreSelected} placeholder="Genre" /> @@ -113,4 +115,4 @@ const SearchForm = (props: Props) => { SearchForm.displayName = 'SearchForm'; -export default SearchForm; +export default React.memo(SearchForm); diff --git a/src/components/scss/_base.scss b/src/components/scss/_base.scss index 83ef851..fc544da 100644 --- a/src/components/scss/_base.scss +++ b/src/components/scss/_base.scss @@ -1,5 +1,5 @@ @import 'variables'; body { - font-family: 'Roboto', Arial, Verdana, Tahoma, sans-serif; + font-family: $default-font-family; } diff --git a/src/components/scss/_variables.scss b/src/components/scss/_variables.scss index d375a13..303732e 100644 --- a/src/components/scss/_variables.scss +++ b/src/components/scss/_variables.scss @@ -52,3 +52,5 @@ $times: ( @return $map; } + +$default-font-family: 'Roboto', Arial, Verdana, Tahoma, sans-serif; diff --git a/testcafe/compare-imgs.js b/testcafe/compare-imgs.js index d3e57d5..1b38c1b 100644 --- a/testcafe/compare-imgs.js +++ b/testcafe/compare-imgs.js @@ -55,12 +55,9 @@ function getScreenshotsPaths() { ); } -async function compareImgs(imgPath1, imgPath2) { - // eslint-disable-next-line no-console - console.log(`Comparing images: "${path.basename(imgPath1)}"`); +async function getMistachedPixels(imgPath1, imgPath2) { const imgs = await Promise.all([getPngData(imgPath1), getPngData(imgPath2)]); const diff = new PNG({ width: imgs[0].width, height: imgs[0].height }); - const mismatchedPixels = pixelmatch( imgs[0].data, imgs[1].data, @@ -71,23 +68,50 @@ async function compareImgs(imgPath1, imgPath2) { threshold: 0.2 } ); + return { mismatchedPixels, diff }; +} - if (mismatchedPixels === 0) return; - - const imgFileName = path.basename(imgPath1); - const browserDir = path.basename(path.dirname(imgPath1)); - +function handleMismatchError(imgPath, diff, mismatchedPixels) { + const imgFileName = path.basename(imgPath); + const browserDir = path.basename(path.dirname(imgPath)); if (!fs.existsSync(path.join(diffsPath, browserDir))) fs.mkdirSync(path.join(diffsPath, browserDir)); - diff.pack().pipe(fs.createWriteStream(path.join(diffsPath, browserDir, imgFileName))); - throw new Error(`${mismatchedPixels} mismatched pixels for "${imgFileName}"`); + return new Promise(resolve => { + const writableStream = fs.createWriteStream( + path.join(diffsPath, browserDir, imgFileName) + ); + + diff.pack().pipe(writableStream); + + writableStream.once('close', () => + resolve({ + imgFileName, + mismatchedPixels + }) + ); + }); +} + +async function compareImgs(imgPath1, imgPath2) { + // eslint-disable-next-line no-console + console.log(`Comparing images: "${path.basename(imgPath1)}"`); + const { mismatchedPixels, diff } = await getMistachedPixels(imgPath1, imgPath2); + + if (mismatchedPixels === 0) return null; + + return handleMismatchError(imgPath1, diff, mismatchedPixels); } async function diffScreenshots() { if (fs.existsSync(diffsPath)) clearDir(diffsPath); else fs.mkdirSync(diffsPath); - await Promise.all(getScreenshotsPaths().map(r => compareImgs(...r))); + const results = await Promise.all(getScreenshotsPaths().map(r => compareImgs(...r))); + if (results.some(r => !!r)) { + // eslint-disable-next-line no-console + console.table(results); + throw new Error('Visual regression errors were found'); + } } module.exports = function compareScreenshots() { diff --git a/testcafe/pages/home.js b/testcafe/pages/home.js index 321c1e7..d916f16 100644 --- a/testcafe/pages/home.js +++ b/testcafe/pages/home.js @@ -56,4 +56,9 @@ module.exports = class HomePage extends BasePage { get booksContainer() { return Selector('.main-layout__books').with({ boundTestRun: this.t }); } + + async takeScreenshotOfBooksContainer() { + await this.prepareForScreenshot(); + await this.t.takeScreenshot(); + } }; diff --git a/testcafe/screenshots/base/HeadlessChrome/Feature Home route-Scenario I see book cards-1.png b/testcafe/screenshots/base/HeadlessChrome/Feature Home route-Scenario I see book cards-1.png index e27fb4f..cdf4c18 100644 Binary files a/testcafe/screenshots/base/HeadlessChrome/Feature Home route-Scenario I see book cards-1.png and b/testcafe/screenshots/base/HeadlessChrome/Feature Home route-Scenario I see book cards-1.png differ diff --git a/testcafe/steps/home-route.js b/testcafe/steps/home-route.js index 28a4c00..b563dc0 100644 --- a/testcafe/steps/home-route.js +++ b/testcafe/steps/home-route.js @@ -9,8 +9,6 @@ Then( async (t, [bookCardCount]) => { const bookCards = await t.ctx.page.getBookCards(); await t.expect(bookCards.length).eql(bookCardCount); - - await t.ctx.page.prepareForScreenshot(); - await t.takeElementScreenshot(t.ctx.page.booksContainer); + await t.ctx.page.takeScreenshotOfBooksContainer(); } ); diff --git a/yarn.lock b/yarn.lock index c9376e1..f7e6e0b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4127,6 +4127,14 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + call-me-maybe@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" @@ -6414,6 +6422,14 @@ file-loader@^3.0.1: loader-utils "^1.0.2" schema-utils "^1.0.0" +file-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-4.0.0.tgz#c3570783fefb6e1bc0978a856f4bf5825b966c2a" + integrity sha512-roAbL6IdSGczwfXxhMi6Zq+jD4IfUpL0jWHD7fvmjdOVb7xBfdRUHe4LpBgO23VtVK5AW1OlWZo0p34Jvx3iWg== + dependencies: + loader-utils "^1.2.2" + schema-utils "^1.0.0" + file-system-cache@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f" @@ -6801,6 +6817,15 @@ get-func-name@^2.0.0: resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= +get-intrinsic@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" + integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" @@ -7175,6 +7200,11 @@ has-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -9034,7 +9064,7 @@ loader-runner@^2.3.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.0.3, loader-utils@^1.1.0, loader-utils@^1.2.3: +loader-utils@1.2.3, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.0.3, loader-utils@^1.1.0, loader-utils@^1.2.2, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -10152,6 +10182,11 @@ object-hash@^1.1.4: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== +object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + object-keys@^1.0.11, object-keys@^1.0.12: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -11388,15 +11423,22 @@ qrcode-terminal@^0.10.0: resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.10.0.tgz#a76a48e2610a18f97fa3a2bd532b682acff86c53" integrity sha1-p2pI4mEKGPl/o6K9UytoKs/4bFM= -qs@6.7.0, qs@^6.6.0: +qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.6.0: + version "6.11.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f" + integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== query-string@^4.1.0, query-string@^4.3.2: version "4.3.4" @@ -11598,10 +11640,10 @@ react-helmet-async@^1.0.2: react-fast-compare "2.0.4" shallowequal "1.1.0" -react-hot-loader@^4.11.1: - version "4.11.1" - resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.11.1.tgz#2cabbd0f1c8a44c28837b86d6ce28521e6d9a8ac" - integrity sha512-HAC0UedYzM3mD+ZaQHesntFO0yi2ftOV4ZMMRTj43E4GvW5sQqYTPvur+6J7EaH3MDr/RqjDKXyCqKepV8+y7w== +react-hot-loader@^4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.0.tgz#cb93d78db8b97fd7c1f5ab688a7eeaa4ad47df3f" + integrity sha512-N+8ct1euiQnwqqDyX+SrxsgZ13ax4e8JiHbSAPf7xAshPxF3iTqVJQUxOLH90RXOFaT8LLynq0VPz3rCK7AW1A== dependencies: fast-levenshtein "^2.0.6" global "^4.3.0" @@ -12780,6 +12822,15 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -14916,7 +14967,7 @@ webpack-sources@^1.1.0, webpack-sources@^1.3.0: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.33.0, webpack@^4.35.0: +webpack@^4.33.0: version "4.35.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.0.tgz#ad3f0f8190876328806ccb7a36f3ce6e764b8378" integrity sha512-M5hL3qpVvtr8d4YaJANbAQBc4uT01G33eDpl/psRTBCfjxFTihdhin1NtAKB1ruDwzeVdcsHHV3NX+QsAgOosw== @@ -14946,6 +14997,36 @@ webpack@^4.33.0, webpack@^4.35.0: watchpack "^1.5.0" webpack-sources "^1.3.0" +webpack@^4.35.2: + version "4.35.2" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.35.2.tgz#5c8b8a66602cbbd6ec65c6e6747914a61c1449b1" + integrity sha512-TZAmorNymV4q66gAM/h90cEjG+N3627Q2MnkSgKlX/z3DlNVKUtqy57lz1WmZU2+FUZwzM+qm7cGaO95PyrX5A== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + acorn "^6.0.5" + acorn-dynamic-import "^4.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" + chrome-trace-event "^1.0.0" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.0" + json-parse-better-errors "^1.0.2" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + micromatch "^3.1.8" + mkdirp "~0.5.0" + neo-async "^2.5.0" + node-libs-browser "^2.0.0" + schema-utils "^1.0.0" + tapable "^1.1.0" + terser-webpack-plugin "^1.1.0" + watchpack "^1.5.0" + webpack-sources "^1.3.0" + websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9"