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..2a8622c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6414,6 +6414,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" @@ -9034,7 +9042,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== @@ -11598,10 +11606,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" @@ -12250,9 +12258,9 @@ replace-ext@1.0.0: integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= replicator@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/replicator/-/replicator-1.0.3.tgz#c0b3ea31e749015bae5d52273f2ae35d541b87ef" - integrity sha512-WsKsraaM0x0QHy5CtzdgFXUxyowoBhyNkmPqmZShW6h+rOWnyT6Od3zRdTX9r616rAA6kDC9MKQGnSM/CJKfVQ== + version "1.0.5" + resolved "https://registry.yarnpkg.com/replicator/-/replicator-1.0.5.tgz#f1e56df7e276a62afe80c2248b8ac03896f4708f" + integrity sha512-saxS4y7NFkLMa92BR4bPHR41GD+f/qoDAwD2xZmN+MpDXgibkxwLO2qk7dCHYtskSkd/bWS8Jy6kC5MZUkg1tw== request-promise-core@1.1.2: version "1.1.2" @@ -14916,7 +14924,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 +14954,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"