From 2d7307f1a8e2cd0b7a26fb1bc600f6f852315ee2 Mon Sep 17 00:00:00 2001 From: Daniel Bartholomae Date: Sun, 2 Dec 2018 16:36:11 +0100 Subject: [PATCH 01/11] refactor: rename dockerUrl to storybookUrl It is the URL where storybook is running, not where docker is running --- src/targets/chrome/docker.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/targets/chrome/docker.js b/src/targets/chrome/docker.js index 55927188..3457d59f 100644 --- a/src/targets/chrome/docker.js +++ b/src/targets/chrome/docker.js @@ -76,7 +76,7 @@ function createChromeDockerTarget({ let port; let dockerId; let host; - let dockerUrl = baseUrl; + let storybookUrl = baseUrl; const dockerPath = 'docker'; const runArgs = ['run', '--rm', '-d', '-P']; @@ -91,13 +91,13 @@ function createChromeDockerTarget({ 'Unable to detect local IP address, try passing --host argument' ); } - dockerUrl = baseUrl.replace('localhost', ip); + storybookUrl = baseUrl.replace('localhost', ip); } else if (baseUrl.indexOf('file:') === 0) { const staticPath = baseUrl.substr('file:'.length); const staticMountPath = '/var/loki'; runArgs.push('-v'); runArgs.push(`${staticPath}:${staticMountPath}`); - dockerUrl = `file://${staticMountPath}`; + storybookUrl = `file://${staticMountPath}`; } async function getIsImageDownloaded(imageName) { @@ -188,7 +188,7 @@ function createChromeDockerTarget({ start, stop, createNewDebuggerInstance, - dockerUrl, + storybookUrl, ensureImageDownloaded ); } From 8aa19a17f47df5901dee5067cf8b6853ba51b740 Mon Sep 17 00:00:00 2001 From: Daniel Bartholomae Date: Sun, 2 Dec 2018 16:37:41 +0100 Subject: [PATCH 02/11] refactor: move out docker helper functions They will be needed for the new existing-docker-target. This also makes the code more readable. --- src/targets/chrome/docker.js | 35 ++----------------- .../chrome/helpers/get-local-ip-address.js | 17 +++++++++ .../chrome/helpers/wait-on-CDP-available.js | 27 ++++++++++++++ 3 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 src/targets/chrome/helpers/get-local-ip-address.js create mode 100644 src/targets/chrome/helpers/wait-on-CDP-available.js diff --git a/src/targets/chrome/docker.js b/src/targets/chrome/docker.js index 3457d59f..15678492 100644 --- a/src/targets/chrome/docker.js +++ b/src/targets/chrome/docker.js @@ -1,45 +1,14 @@ const debug = require('debug')('loki:chrome:docker'); -const os = require('os'); const { execSync } = require('child_process'); const execa = require('execa'); -const waitOn = require('wait-on'); +const getLocalIPAddress = require('./helpers/get-local-ip-address'); +const waitOnCDPAvailable = require('./helpers/wait-on-CDP-available'); const CDP = require('chrome-remote-interface'); const fs = require('fs-extra'); const getRandomPort = require('get-port'); const { ensureDependencyAvailable } = require('../../dependency-detection'); const createChromeTarget = require('./create-chrome-target'); -const getLocalIPAddress = () => { - const interfaces = os.networkInterfaces(); - const ips = Object.keys(interfaces) - .map(key => - interfaces[key] - .filter(({ family, internal }) => family === 'IPv4' && !internal) - .map(({ address }) => address) - ) - .reduce((acc, current) => acc.concat(current), []); - return ips[0]; -}; - -const waitOnCDPAvailable = (host, port) => - new Promise((resolve, reject) => { - waitOn( - { - resources: [`tcp:${host}:${port}`], - delay: 50, - interval: 100, - timeout: 5000, - }, - err => { - if (err) { - reject(err); - } else { - resolve(); - } - } - ); - }); - const getNetworkHost = async dockerId => { let host = '127.0.0.1'; diff --git a/src/targets/chrome/helpers/get-local-ip-address.js b/src/targets/chrome/helpers/get-local-ip-address.js new file mode 100644 index 00000000..bdd021c7 --- /dev/null +++ b/src/targets/chrome/helpers/get-local-ip-address.js @@ -0,0 +1,17 @@ +const os = require('os'); + +/** + * Returns the first external IPv4 address this machine can be reached at. + * @returns {String} The IPv4 address + */ +module.exports = function getLocalIPAddress () { + const interfaces = os.networkInterfaces(); + const ips = Object.keys(interfaces) + .map(key => + interfaces[key] + .filter(({ family, internal }) => family === 'IPv4' && !internal) + .map(({ address }) => address) + ) + .reduce((acc, current) => acc.concat(current), []); + return ips[0]; +}; diff --git a/src/targets/chrome/helpers/wait-on-CDP-available.js b/src/targets/chrome/helpers/wait-on-CDP-available.js new file mode 100644 index 00000000..d4f563e3 --- /dev/null +++ b/src/targets/chrome/helpers/wait-on-CDP-available.js @@ -0,0 +1,27 @@ +const waitOn = require('wait-on'); + +/** + * Waits for the Chrome DevTools Protocol to be available. It will timeout after 5 seconds. + * @param host The host where to look for CDP + * @param port THe port where to look for CDP + * @returns {Promise} Will resolve without content when CDP is available or error out otherwise + */ +module.exports = function waitOnCDPAvailable (host, port) { + return new Promise((resolve, reject) => { + waitOn( + { + resources: [`tcp:${host}:${port}`], + delay: 50, + interval: 100, + timeout: 5000, + }, + err => { + if (err) { + reject(err); + } else { + resolve(); + } + } + ); + }); +} From 4dbcd403792fe247215bc34f058c4e14af31e87b Mon Sep 17 00:00:00 2001 From: Daniel Bartholomae Date: Sun, 2 Dec 2018 22:38:16 +0100 Subject: [PATCH 03/11] chore: add debugging line to loading stories --- src/targets/chrome/create-chrome-target.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/targets/chrome/create-chrome-target.js b/src/targets/chrome/create-chrome-target.js index 6b3d33fb..9cb1a08c 100644 --- a/src/targets/chrome/create-chrome-target.js +++ b/src/targets/chrome/create-chrome-target.js @@ -241,6 +241,7 @@ function createChromeTarget( } const selector = configuration.chromeSelector || options.chromeSelector; const url = getStoryUrl(kind, story); + debug(`Loading story from ${url}`) const tab = await launchNewTab(tabOptions); let screenshot; From 0cca0d7cf5898687d5be7dd8663866423264c689 Mon Sep 17 00:00:00 2001 From: Daniel Bartholomae Date: Sun, 2 Dec 2018 13:51:00 +0100 Subject: [PATCH 04/11] docs: add docs for using an existing headless chrome docker container --- docs/command-line-arguments.md | 3 +++ docs/continuous-integration.md | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/docs/command-line-arguments.md b/docs/command-line-arguments.md index afe37887..59781c95 100644 --- a/docs/command-line-arguments.md +++ b/docs/command-line-arguments.md @@ -22,6 +22,9 @@ yarn loki test -- --port 9009 | **`--difference`** | Path to image diff folder | `./.loki/difference` | | **`--diffingEngine`** | What diffing engine to use, currently supported are `looks-same` and `gm` | `gm` if available | | **`--chromeConcurrency`** | How many stories to test in parallel when using chrome | `4` | +| **`--chromeDockerUseExisting`**| Whether to use an already running container wiht headless chrome or spin up a new one. See also `--chromeDockerHost` and `--chromeDockerHost` | `false` | +| **`--chromeDockerHost`** | At which host to look for a docker container running headless chrome. Only relevant if chromeDockerUseExisting is set | `localhost` | +| **`--chromeDockerPort`** | At what port to look for a docker container running headless chrome. Only relevant if chromeDockerUseExisting is set | `9222` | | **`--chromeDockerImage`** | What docker image to use when running chrome | `yukinying/chrome-headless:63.0.3230.2` | | **`--chromeEnableAnimations`** | Enable CSS transitions and animations. | `false` | | **`--chromeFlags`** | Custom chrome flags. | `--headless --disable-gpu --hide-scrollbars` | diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 91c579b2..277e16fb 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -9,3 +9,8 @@ build-storybook && loki --requireReference --reactUri file:./storybook-static ``` See the [loki react example project](https://github.com/oblador/loki/tree/master/examples/react) for a reference implementation of this approach. + +## Using Docker + +Some CIs like e. g. [CircleCI](http://circleci.com/) run the code in a Docker container already, so setting up a Chrome headless docker container inside the container isn't a viable solution. Instead it is possible to spin up a headless-chrome container in the CI and connect it to the test. +To do this you can run loki with `--chromeDockerUseExisting`. This way loki will look for an already running container at `localhost:9222` (or any other location you specify via `--chromeDockerHost` and `--chromeDockerHost`). We recommend to use the exact same image for headless chrome on your CI that you use for local testing to avoid discrepancies in rendering. From 5c997369d61a1a17cf95fad041e496da2c2f8cd1 Mon Sep 17 00:00:00 2001 From: Daniel Bartholomae Date: Sun, 2 Dec 2018 16:46:27 +0100 Subject: [PATCH 05/11] feat: allow to run loki with existing headless chrome docker container This adds a new target for running storybook in chrome on an existing docker container. The target isn't exported directly, but as a variant of the chromeDockerTarget to make it possible to let Loki run docker on a local machine while enabling the use of an existing container with a flag on the CI. This way the same target config can be used on local and on the CI. The biggest challenge with this change was to provide access to local files as we cannot assume that the external headless chrome docker container has access to those. Instead Loki will run a small static server to serve the file and direct the headless chrome towards this page. Two new packages are added: bluebird for better promise handling, especially timeouts, and serve-handler to serve the static directory. All changes are behind the chromeDockerUseExisting flag which is false by default, so there should be no breaking changes. --- package.json | 2 + src/commands/test/default-options.json | 2 + src/commands/test/parse-options.js | 5 +- src/commands/test/run-tests.js | 3 + .../chrome/create-existing-docker-target.js | 126 ++++++++++++++++++ src/targets/chrome/docker.js | 10 ++ yarn.lock | 64 ++++++++- 7 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 src/targets/chrome/create-existing-docker-target.js diff --git a/package.json b/package.json index bcc41bbe..b33429ed 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ }, "license": "MIT", "dependencies": { + "bluebird": "^3.5.3", "chalk": "^2.0.1", "chrome-remote-interface": "^0.23.3", "ci-info": "^1.1.3", @@ -58,6 +59,7 @@ "minimist": "^1.2.0", "osnap": "^1.1.0", "ramda": "^0.24.1", + "serve-handler": "^5.0.7", "shelljs": "^0.7.8", "transliteration": "^1.6.2", "wait-on": "^2.0.2", diff --git a/src/commands/test/default-options.json b/src/commands/test/default-options.json index 78e037bb..f259a323 100644 --- a/src/commands/test/default-options.json +++ b/src/commands/test/default-options.json @@ -1,5 +1,7 @@ { "chromeConcurrency": "4", + "chromeDockerHost": "localhost", + "chromeDockerPort": "9222", "chromeDockerImage": "yukinying/chrome-headless:63.0.3230.2", "chromeFlags": "--headless --disable-gpu --hide-scrollbars", "chromeLoadTimeout": "60000", diff --git a/src/commands/test/parse-options.js b/src/commands/test/parse-options.js index 615fe115..7663be88 100644 --- a/src/commands/test/parse-options.js +++ b/src/commands/test/parse-options.js @@ -7,7 +7,7 @@ const getAbsoluteURL = require('./get-absolute-url'); function parseOptions(args, config) { const argv = minimist(args, { - boolean: ['requireReference', 'chromeEnableAnimations', 'verboseRenderer'], + boolean: ['requireReference', 'chromeEnableAnimations', 'chromeDockerUseExisting', 'verboseRenderer'], }); const $ = key => argv[key] || config[key] || defaults[key]; @@ -21,6 +21,9 @@ function parseOptions(args, config) { `http://${$('host')}:${argv.port || $('reactPort')}`, reactNativeUri: `ws://${$('host')}:${argv.port || $('reactNativePort')}`, chromeConcurrency: parseInt($('chromeConcurrency'), 10), + chromeDockerUseExisting: $('chromeDockerUseExisting'), + chromeDockerHost: $('chromeDockerHost'), + chromeDockerPort: $('chromeDockerPort'), chromeDockerImage: $('chromeDockerImage'), chromeEnableAnimations: $('chromeEnableAnimations'), chromeFlags: $('chromeFlags').split(' '), diff --git a/src/commands/test/run-tests.js b/src/commands/test/run-tests.js index d7c4d0b2..74e14062 100644 --- a/src/commands/test/run-tests.js +++ b/src/commands/test/run-tests.js @@ -179,6 +179,9 @@ async function runTests(flatConfigurations, options) { createChromeDockerTarget({ baseUrl: options.reactUri, chromeDockerImage: options.chromeDockerImage, + chromeDockerUseExisting: options.chromeDockerUseExisting, + chromeDockerHost: options.chromeDockerHost, + chromeDockerPort: options.chromeDockerPort, chromeFlags: options.chromeFlags, }), configurations, diff --git a/src/targets/chrome/create-existing-docker-target.js b/src/targets/chrome/create-existing-docker-target.js new file mode 100644 index 00000000..9827530f --- /dev/null +++ b/src/targets/chrome/create-existing-docker-target.js @@ -0,0 +1,126 @@ +const debug = require('debug')('loki:chrome:existing'); +const CDP = require('chrome-remote-interface'); +const serveHandler = require('serve-handler'); +const http = require('http'); +const Promise = require('bluebird'); +const waitOnCDPAvailable = require('./helpers/wait-on-CDP-available'); +const getLocalIPAddress = require('./helpers/get-local-ip-address'); +const createChromeTarget = require('./create-chrome-target'); + +const SERVER_STARTUP_TIMEOUT = 10000 /* ms */; + +function createChromeDockerTarget({ + baseUrl = 'http://localhost:6006', + chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'], + chromeDockerHost = 'localhost', + chromeDockerPort = '9222', +}) { + let storybookUrl = baseUrl; + + const RUN_MODES = { + local: 'local', + remote: 'remote', + file: 'file' + } + + let runMode = RUN_MODES.remote + if (baseUrl.indexOf('http://localhost') === 0) { + runMode = RUN_MODES.local + } else if (baseUrl.indexOf('file:') === 0) { + runMode = RUN_MODES.file + } + debug(`Running in ${runMode} mode`); + + let server; + let storybookPort; + let storybookHost; + if (runMode === RUN_MODES.file) { + const staticPath = baseUrl.substr('file:'.length); + storybookPort = 8080; + const ip = getLocalIPAddress(); + if (!ip) { + throw new Error( + 'Unable to detect local IP address, try passing --host argument' + ); + } + storybookUrl = `http://${ip}:${storybookPort}`; + storybookHost = ip; + server = http.createServer((req, res) => serveHandler(req, res, {public: staticPath})) + debug(`Serving files from ${staticPath}`); + } + + if (runMode === RUN_MODES.local) { + const ip = getLocalIPAddress(); + if (!ip) { + throw new Error( + 'Unable to detect local IP address, try passing --host argument' + ); + } + storybookUrl = baseUrl.replace('localhost', ip); + } + + debug(`Looking for storybook at ${storybookUrl}`); + + let dockerHost = chromeDockerHost + if (chromeDockerHost.indexOf('localhost') === 0) { + const ip = getLocalIPAddress(); + if (!ip) { + throw new Error( + 'Unable to detect local IP address, try passing --host argument' + ); + } + dockerHost = chromeDockerHost.replace('localhost', ip); + } + + async function start() { + debug(`Trying to connect to Chrome at http://${dockerHost}:${chromeDockerPort}`) + if (server) { + debug(`Serving storybook at http://${storybookHost}:${storybookPort}`) + server.listen({host: storybookHost, port: storybookPort}) + await Promise.all([ + waitOnCDPAvailable(dockerHost, chromeDockerPort), + new Promise((resolve, reject) => { + server.addListener('listening', resolve); + server.addListener('error', reject); + }).timeout(SERVER_STARTUP_TIMEOUT) + ]); + debug('Set up complete') + } else { + await waitOnCDPAvailable(dockerHost, chromeDockerPort); + } + } + + async function stop() { + if (server) { + const serverClosed = new Promise((resolve, reject) => { + server.on('close', resolve); + server.on('error', reject); + }); + server.close(); + await serverClosed; + } + } + + async function createNewDebuggerInstance() { + debug(`Launching new tab with debugger at port ${dockerHost}:${chromeDockerPort}`); + const target = await CDP.New({ host: dockerHost, port: chromeDockerPort }); + debug(`Launched with target id ${target.id}`); + const client = await CDP({ host: dockerHost, port: chromeDockerPort, target }); + + client.close = () => { + debug('New closing tab'); + return CDP.Close({ host: dockerHost, port: chromeDockerPort, id: target.id }); + }; + + return client; + } + + return createChromeTarget( + start, + stop, + createNewDebuggerInstance, + storybookUrl + ); +} + +module.exports = createChromeDockerTarget; diff --git a/src/targets/chrome/docker.js b/src/targets/chrome/docker.js index 15678492..e9a93d87 100644 --- a/src/targets/chrome/docker.js +++ b/src/targets/chrome/docker.js @@ -8,6 +8,7 @@ const fs = require('fs-extra'); const getRandomPort = require('get-port'); const { ensureDependencyAvailable } = require('../../dependency-detection'); const createChromeTarget = require('./create-chrome-target'); +const createExistingDockerTarget = require('./create-existing-docker-target'); const getNetworkHost = async dockerId => { let host = '127.0.0.1'; @@ -41,7 +42,16 @@ function createChromeDockerTarget({ baseUrl = 'http://localhost:6006', chromeDockerImage = 'yukinying/chrome-headless', chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'], + chromeDockerUseExisting = false, + chromeDockerHost = 'localhost', + chromeDockerPort = '9222', }) { + if (chromeDockerUseExisting) { + return createExistingDockerTarget({ + baseUrl, chromeFlags, chromeDockerHost, chromeDockerPort + }) + } + let port; let dockerId; let host; diff --git a/yarn.lock b/yarn.lock index 33ba80eb..ee2334f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -676,6 +676,11 @@ bluebird@^3.5.0, bluebird@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" +bluebird@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" + integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== + boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -764,6 +769,11 @@ builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + cacache@^9.2.9, cacache@~9.2.9: version "9.2.9" resolved "https://registry.yarnpkg.com/cacache/-/cacache-9.2.9.tgz#f9d7ffe039851ec94c28290662afa4dd4bb9e8dd" @@ -1146,6 +1156,11 @@ contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= + convert-source-map@^1.1.0, convert-source-map@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" @@ -1943,6 +1958,13 @@ fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" +fast-url-parser@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" + integrity sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0= + dependencies: + punycode "^1.3.2" + fb-watchman@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" @@ -3977,10 +3999,22 @@ mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" + integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== + mime-db@~1.35.0: version "1.35.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.35.0.tgz#0569d657466491283709663ad379a99b90d9ab47" +mime-types@2.1.18: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" + integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== + dependencies: + mime-db "~1.33.0" + mime-types@^2.1.11: version "2.1.17" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" @@ -4003,7 +4037,7 @@ mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.3: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -4825,7 +4859,7 @@ path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" -path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.0, path-is-inside@~1.0.2: +path-is-inside@1.0.2, path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.0, path-is-inside@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" @@ -4837,6 +4871,11 @@ path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" +path-to-regexp@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" + integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -5028,7 +5067,7 @@ pumpify@^1.3.3: inherits "^2.0.1" pump "^1.0.0" -punycode@^1.4.1: +punycode@^1.3.2, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -5063,6 +5102,11 @@ randomatic@^1.1.3: is-number "^3.0.0" kind-of "^4.0.0" +range-parser@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= + rc@^1.0.1, rc@^1.1.6: version "1.2.1" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" @@ -5598,6 +5642,20 @@ semver@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" +serve-handler@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-5.0.7.tgz#317877420925913e99e4dc228e67f6e5774e5387" + integrity sha512-PuLoJHAO2jj3p1fYWfXVHsEqNesx1+h+6qj0FIWrCe526ZtpDqeYuKA4knE5pjK9xoOVShoB+qGOP93EY46xEw== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + fast-url-parser "1.1.3" + mime-types "2.1.18" + minimatch "3.0.4" + path-is-inside "1.0.2" + path-to-regexp "2.2.1" + range-parser "1.2.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" From 397add4b6f34cbeab3f3a7c9aa2b58b36e16f168 Mon Sep 17 00:00:00 2001 From: Maciej Jaroszewski Date: Thu, 17 Jan 2019 11:33:34 +0100 Subject: [PATCH 06/11] Test CI version of repo --- package.json | 32 ++++---- src/commands/test/index.js | 8 +- src/commands/test/run-tests.js | 46 ++++++------ src/commands/test/test-story.js | 2 +- src/targets/chrome/app.js | 6 +- src/targets/chrome/create-chrome-target.js | 74 ++++++++++--------- .../chrome/create-existing-docker-target.js | 48 ++++++++---- src/targets/decorate-storybook.js | 6 +- 8 files changed, 123 insertions(+), 99 deletions(-) diff --git a/package.json b/package.json index b33429ed..62f181f6 100644 --- a/package.json +++ b/package.json @@ -45,25 +45,27 @@ "license": "MIT", "dependencies": { "bluebird": "^3.5.3", - "chalk": "^2.0.1", - "chrome-remote-interface": "^0.23.3", - "ci-info": "^1.1.3", - "debug": "^2.6.8", - "execa": "^0.7.0", - "fs-extra": "^3.0.1", - "get-port": "^3.2.0", - "gm": "^1.23.0", + "chalk": "^2.4.1", + "chrome-launcher": "^0.10.5", + "chrome-remote-interface": "^0.27.0", + "ci-info": "^1.6.0", + "debug": "^4.1.0", + "execa": "^1.0.0", + "express": "^4.16.4", + "fs-extra": "^7.0.1", + "get-port": "^4.0.0", + "gm": "^1.23.1", "lighthouse": "^2.2.1", - "listr": "^0.12.0", - "looks-same": "^3.2.1", + "listr": "^0.14.3", + "looks-same": "^4.0.0", "minimist": "^1.2.0", "osnap": "^1.1.0", - "ramda": "^0.24.1", + "ramda": "^0.26.1", "serve-handler": "^5.0.7", - "shelljs": "^0.7.8", - "transliteration": "^1.6.2", - "wait-on": "^2.0.2", - "ws": "^3.0.0" + "shelljs": "^0.8.3", + "transliteration": "^1.6.6", + "wait-on": "^3.2.0", + "ws": "^6.1.2" }, "devDependencies": { "babel-eslint": "^10.0.1", diff --git a/src/commands/test/index.js b/src/commands/test/index.js index 2948f53a..5d751ec0 100644 --- a/src/commands/test/index.js +++ b/src/commands/test/index.js @@ -9,18 +9,18 @@ const { ensureDependencyAvailable } = require('../../dependency-detection'); const { ReferenceImageError } = require('../../errors'); const buildCommand = require('../../build-command'); -const escapeRegExp = str => str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); +const escapeRegExp = (str) => str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); const getUpdateCommand = (errors, args) => { const argv = minimist(args); - const stories = uniq(errors.map(e => `${e.kind} ${e.story}`)); + const stories = uniq(errors.map((e) => `${e.kind} ${e.story}`)); const storiesFilter = `^${stories.map(escapeRegExp).join('|')}$`; const tooManyToFilter = stories.length > 10; const argObject = Object.assign( { configurationFilter: argv._[1], - storiesFilter: !tooManyToFilter && storiesFilter, + storiesFilter: !tooManyToFilter && storiesFilter }, pickBy((value, key) => { switch (key) { @@ -67,7 +67,7 @@ async function test(args) { } catch (err) { if (err.name === 'ListrError') { const imageErrors = err.errors.filter( - e => e instanceof ReferenceImageError + (e) => e instanceof ReferenceImageError ); if (imageErrors.length !== 0) { error('Visual tests failed'); diff --git a/src/commands/test/run-tests.js b/src/commands/test/run-tests.js index 74e14062..ea67506b 100644 --- a/src/commands/test/run-tests.js +++ b/src/commands/test/run-tests.js @@ -11,16 +11,16 @@ const { createChromeAppTarget, createChromeDockerTarget, createIOSSimulatorTarget, - createAndroidEmulatorTarget, + createAndroidEmulatorTarget } = require('../../targets'); const testStory = require('./test-story'); async function placeGitignore(pathsToIgnore) { const parentDir = path.dirname(pathsToIgnore[0]); const gitignorePath = `${parentDir}/.gitignore`; - if (!(await fs.pathExists(gitignorePath))) { - const relativeToParent = p => path.relative(parentDir, p); - const isDecendant = p => p.indexOf('..') !== 0; + if (!await fs.pathExists(gitignorePath)) { + const relativeToParent = (p) => path.relative(parentDir, p); + const isDecendant = (p) => p.indexOf('..') !== 0; const gitignore = pathsToIgnore .map(relativeToParent) .filter(isDecendant) @@ -30,14 +30,14 @@ async function placeGitignore(pathsToIgnore) { } } -const groupByTarget = configurations => +const groupByTarget = (configurations) => mapObjIndexed( fromPairs, groupBy(([, { target }]) => target, toPairs(configurations)) ); const filterStorybook = (storybook, excludePattern, includePattern) => { - const filterStory = kind => story => { + const filterStory = (kind) => (story) => { const fullStoryName = `${kind} ${story}`; const exclude = excludePattern && new RegExp(excludePattern, 'i').test(fullStoryName); @@ -49,12 +49,12 @@ const filterStorybook = (storybook, excludePattern, includePattern) => { return storybook .map(({ kind, stories }) => ({ kind, - stories: stories.filter(filterStory(kind)), + stories: stories.filter(filterStory(kind)) })) .filter(({ stories }) => stories.length !== 0); }; -const getListr = options => (tasks, listrOptions = {}) => { +const getListr = (options) => (tasks, listrOptions = {}) => { const newOptions = listrOptions; if (options.verboseRenderer) { newOptions.renderer = 'verbose'; @@ -89,14 +89,14 @@ async function runTests(flatConfigurations, options) { task: async () => { await target.prepare(); }, - enabled: () => !!target.prepare, + enabled: () => !!target.prepare }, { title: 'Start', task: async ({ activeTargets }) => { await target.start(); activeTargets.push(target); - }, + } }, { title: 'Fetch list of stories', @@ -105,7 +105,7 @@ async function runTests(flatConfigurations, options) { if (storybook.length === 0) { throw new Error('Error: No stories were found.'); } - }, + } }, ...Object.values( mapObjIndexed( @@ -121,7 +121,7 @@ async function runTests(flatConfigurations, options) { title: kind, task: () => getListr(options)( - stories.map(story => ({ + stories.map((story) => ({ title: options.verboseRenderer ? `${kind}: ${story}` : story, @@ -134,12 +134,12 @@ async function runTests(flatConfigurations, options) { configurationName, kind, story - ), + ) })) - ), + ) })), { concurrent: concurrency, exitOnError: false } - ), + ) }), configurations ) @@ -152,13 +152,13 @@ async function runTests(flatConfigurations, options) { if (index !== -1) { activeTargets.splice(index, 1); } - }, - }, - ]), + } + } + ]) }; }; - const createTargetTask = configurations => { + const createTargetTask = (configurations) => { const { target } = configurations[Object.keys(configurations)[0]]; switch (target) { case 'chrome.app': { @@ -166,7 +166,7 @@ async function runTests(flatConfigurations, options) { 'Chrome (app)', createChromeAppTarget({ baseUrl: options.reactUri, - chromeFlags: options.chromeFlags, + chromeFlags: options.chromeFlags }), configurations, options.chromeConcurrency, @@ -182,7 +182,7 @@ async function runTests(flatConfigurations, options) { chromeDockerUseExisting: options.chromeDockerUseExisting, chromeDockerHost: options.chromeDockerHost, chromeDockerPort: options.chromeDockerPort, - chromeFlags: options.chromeFlags, + chromeFlags: options.chromeFlags }), configurations, options.chromeConcurrency, @@ -217,8 +217,10 @@ async function runTests(flatConfigurations, options) { try { await tasks.run(context); } catch (err) { - await Promise.all(context.activeTargets.map(target => target.stop())); + await Promise.all(context.activeTargets.map((target) => target.stop())); throw err; + } finally { + process.exit(0); } } diff --git a/src/commands/test/test-story.js b/src/commands/test/test-story.js index 5a0e5b52..4a9d850d 100644 --- a/src/commands/test/test-story.js +++ b/src/commands/test/test-story.js @@ -6,7 +6,7 @@ const { getImageDiffer } = require('../../diffing'); const SLUGIFY_OPTIONS = { lowercase: false, - separator: '_', + separator: '_' }; const getBaseName = (configurationName, kind, story) => diff --git a/src/targets/chrome/app.js b/src/targets/chrome/app.js index 34be5f75..f5de114b 100644 --- a/src/targets/chrome/app.js +++ b/src/targets/chrome/app.js @@ -1,11 +1,11 @@ const debug = require('debug')('loki:chrome:local'); -const chromeLauncher = require('lighthouse/chrome-launcher/chrome-launcher'); +const chromeLauncher = require('chrome-launcher'); const CDP = require('chrome-remote-interface'); const createChromeTarget = require('./create-chrome-target'); function createChromeAppTarget({ baseUrl = 'http://localhost:6006', - chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'], + chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'] }) { let instance; @@ -13,7 +13,7 @@ function createChromeAppTarget({ const launchOptions = Object.assign( { chromeFlags, - logLevel: 'silent', + logLevel: 'silent' }, options ); diff --git a/src/targets/chrome/create-chrome-target.js b/src/targets/chrome/create-chrome-target.js index 9cb1a08c..af92292b 100644 --- a/src/targets/chrome/create-chrome-target.js +++ b/src/targets/chrome/create-chrome-target.js @@ -8,13 +8,13 @@ const awaitLokiReady = require('./await-loki-ready'); const { withTimeout, TimeoutError, - withRetries, + withRetries } = require('../../failure-handling'); const { FetchingURLsError, ServerError } = require('../../errors'); const LOADING_STORIES_TIMEOUT = 60000; const CAPTURING_SCREENSHOT_TIMEOUT = 30000; -const REQUEST_STABILIZATION_TIMEOUT = 100; +const REQUEST_STABILIZATION_TIMEOUT = 1000; function createChromeTarget( start, @@ -29,7 +29,7 @@ function createChromeTarget( height: options.height, deviceScaleFactor: options.deviceScaleFactor || 1, mobile: options.mobile || false, - fitWindow: options.fitWindow || false, + fitWindow: options.fitWindow || false }; } @@ -45,7 +45,7 @@ function createChromeTarget( await Page.enable(); if (options.userAgent) { await Network.setUserAgentOverride({ - userAgent: options.userAgent, + userAgent: options.userAgent }); } if (options.clearBrowserCookies) { @@ -80,17 +80,18 @@ function createChromeTarget( } }; - const requestEnded = requestId => { + const requestEnded = (requestId) => { delete pendingRequestURLMap[requestId]; maybeFulfillPromise(); }; - const requestFailed = requestId => { + const requestFailed = (requestId) => { failedURLs.push(pendingRequestURLMap[requestId]); requestEnded(requestId); }; Network.requestWillBeSent(({ requestId, request }) => { + console.info('!!!!', request.url); pendingRequestURLMap[requestId] = request.url; }); @@ -111,7 +112,7 @@ function createChromeTarget( maybeFulfillPromise(); }); - const evaluateOnNewDocument = scriptSource => { + const evaluateOnNewDocument = (scriptSource) => { if (Page.addScriptToEvaluateOnLoad) { // For backwards support return Page.addScriptToEvaluateOnLoad({ scriptSource }); @@ -126,7 +127,7 @@ function createChromeTarget( const expression = `(() => Promise.resolve((${functionToExecute})(${stringifiedArgs})).then(JSON.stringify))()`; const { result } = await Runtime.evaluate({ expression, - awaitPromise: true, + awaitPromise: true }); if (result.subtype === 'error') { throw new Error( @@ -138,20 +139,22 @@ function createChromeTarget( client.executeFunctionWithWindow = executeFunctionWithWindow; - client.loadUrl = async url => { + client.loadUrl = async (url) => { if (!options.chromeEnableAnimations) { debug('Disabling animations'); await evaluateOnNewDocument(disableAnimations.asString); } debug(`Navigating to ${url}`); - await Promise.all([Page.navigate({ url }), awaitRequestsFinished()]); - + await Promise.all([ + Page.navigate({ url, transitionType: 'auto_subframe' }), + awaitRequestsFinished() + ]); debug('Awaiting runtime setup'); await executeFunctionWithWindow(awaitLokiReady); }; - const getPositionInViewport = async selector => { + const getPositionInViewport = async (selector) => { try { return await executeFunctionWithWindow(getSelectorBoxSize, selector); } catch (error) { @@ -165,28 +168,28 @@ function createChromeTarget( }; client.captureScreenshot = withRetries(options.chromeRetries)( - withTimeout(CAPTURING_SCREENSHOT_TIMEOUT, 'captureScreenshot')( - async (selector = 'body') => { - debug(`Getting viewport position of "${selector}"`); - const position = await getPositionInViewport(selector); - - if (position.width === 0 || position.height === 0) { - throw new Error( - `Selector "${selector} has zero width or height. Can't capture screenshot.` - ); - } + withTimeout( + CAPTURING_SCREENSHOT_TIMEOUT, + 'captureScreenshot' + )(async (selector = 'body') => { + debug(`Getting viewport position of "${selector}"`); + const position = await getPositionInViewport(selector); + if (position.width === 0 || position.height === 0) { + throw new Error( + `Selector "${selector} has zero width or height. Can't capture screenshot.` + ); + } - debug('Capturing screenshot'); - const clip = Object.assign({ scale: 1 }, position); - const screenshot = await Page.captureScreenshot({ - format: 'png', - clip, - }); - const buffer = Buffer.from(screenshot.data, 'base64'); + debug('Capturing screenshot'); + const clip = Object.assign({ scale: 1 }, position); + const screenshot = await Page.captureScreenshot({ + format: 'png', + clip + }); + const buffer = Buffer.from(screenshot.data, 'base64'); - return buffer; - } - ) + return buffer; + }) ); return client; @@ -202,7 +205,7 @@ function createChromeTarget( width: 100, height: 100, chromeEnableAnimations: true, - clearBrowserCookies: false, + clearBrowserCookies: false }); const url = `${baseUrl}/iframe.html`; try { @@ -241,8 +244,7 @@ function createChromeTarget( } const selector = configuration.chromeSelector || options.chromeSelector; const url = getStoryUrl(kind, story); - debug(`Loading story from ${url}`) - + debug(`Loading story from ${url}`); const tab = await launchNewTab(tabOptions); let screenshot; try { @@ -268,7 +270,7 @@ function createChromeTarget( prepare, getStorybook, launchNewTab, - captureScreenshotForStory, + captureScreenshotForStory }; } diff --git a/src/targets/chrome/create-existing-docker-target.js b/src/targets/chrome/create-existing-docker-target.js index 9827530f..704a77ea 100644 --- a/src/targets/chrome/create-existing-docker-target.js +++ b/src/targets/chrome/create-existing-docker-target.js @@ -7,13 +7,13 @@ const waitOnCDPAvailable = require('./helpers/wait-on-CDP-available'); const getLocalIPAddress = require('./helpers/get-local-ip-address'); const createChromeTarget = require('./create-chrome-target'); -const SERVER_STARTUP_TIMEOUT = 10000 /* ms */; +const SERVER_STARTUP_TIMEOUT = 10000; /* ms */ function createChromeDockerTarget({ baseUrl = 'http://localhost:6006', chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'], chromeDockerHost = 'localhost', - chromeDockerPort = '9222', + chromeDockerPort = '9222' }) { let storybookUrl = baseUrl; @@ -21,13 +21,13 @@ function createChromeDockerTarget({ local: 'local', remote: 'remote', file: 'file' - } + }; - let runMode = RUN_MODES.remote + let runMode = RUN_MODES.remote; if (baseUrl.indexOf('http://localhost') === 0) { - runMode = RUN_MODES.local + runMode = RUN_MODES.local; } else if (baseUrl.indexOf('file:') === 0) { - runMode = RUN_MODES.file + runMode = RUN_MODES.file; } debug(`Running in ${runMode} mode`); @@ -45,7 +45,13 @@ function createChromeDockerTarget({ } storybookUrl = `http://${ip}:${storybookPort}`; storybookHost = ip; - server = http.createServer((req, res) => serveHandler(req, res, {public: staticPath})) + server = http.createServer((req, res) => + serveHandler(req, res, { + public: staticPath, + cleanUrls: false, + renderSingle: true + }) + ); debug(`Serving files from ${staticPath}`); } @@ -61,7 +67,7 @@ function createChromeDockerTarget({ debug(`Looking for storybook at ${storybookUrl}`); - let dockerHost = chromeDockerHost + let dockerHost = chromeDockerHost; if (chromeDockerHost.indexOf('localhost') === 0) { const ip = getLocalIPAddress(); if (!ip) { @@ -73,10 +79,12 @@ function createChromeDockerTarget({ } async function start() { - debug(`Trying to connect to Chrome at http://${dockerHost}:${chromeDockerPort}`) + debug( + `Trying to connect to Chrome at http://${dockerHost}:${chromeDockerPort}` + ); if (server) { - debug(`Serving storybook at http://${storybookHost}:${storybookPort}`) - server.listen({host: storybookHost, port: storybookPort}) + debug(`Serving storybook at http://${storybookHost}:${storybookPort}`); + server.listen({ host: storybookHost, port: storybookPort }); await Promise.all([ waitOnCDPAvailable(dockerHost, chromeDockerPort), new Promise((resolve, reject) => { @@ -84,7 +92,7 @@ function createChromeDockerTarget({ server.addListener('error', reject); }).timeout(SERVER_STARTUP_TIMEOUT) ]); - debug('Set up complete') + debug('Set up complete'); } else { await waitOnCDPAvailable(dockerHost, chromeDockerPort); } @@ -102,14 +110,24 @@ function createChromeDockerTarget({ } async function createNewDebuggerInstance() { - debug(`Launching new tab with debugger at port ${dockerHost}:${chromeDockerPort}`); + debug( + `Launching new tab with debugger at port ${dockerHost}:${chromeDockerPort}` + ); const target = await CDP.New({ host: dockerHost, port: chromeDockerPort }); debug(`Launched with target id ${target.id}`); - const client = await CDP({ host: dockerHost, port: chromeDockerPort, target }); + const client = await CDP({ + host: dockerHost, + port: chromeDockerPort, + target + }); client.close = () => { debug('New closing tab'); - return CDP.Close({ host: dockerHost, port: chromeDockerPort, id: target.id }); + return CDP.Close({ + host: dockerHost, + port: chromeDockerPort, + id: target.id + }); }; return client; diff --git a/src/targets/decorate-storybook.js b/src/targets/decorate-storybook.js index 16dbe1eb..fb1ad73b 100644 --- a/src/targets/decorate-storybook.js +++ b/src/targets/decorate-storybook.js @@ -80,7 +80,7 @@ function decorateStorybook(storybook) { enumerable: true, get: function() { return storiesOf; - }, + } }); } @@ -96,7 +96,7 @@ function decorateStorybook(storybook) { wrapWithAsyncStory(this.add.bind(this)), this.kind )(...args); - }, + } }); function getStorybook() { @@ -110,7 +110,7 @@ function decorateStorybook(storybook) { skipped: skipped, stories: stories.filter(function(story) { return skipped.indexOf(story.name) === -1; - }), + }) }; } From dda6ff1291c6bec7505fff240080e44cb7b7ab3c Mon Sep 17 00:00:00 2001 From: Maciej Jaroszewski Date: Thu, 17 Jan 2019 12:40:35 +0100 Subject: [PATCH 07/11] Fix finally process exit --- src/commands/test/run-tests.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/commands/test/run-tests.js b/src/commands/test/run-tests.js index ea67506b..38ffbe19 100644 --- a/src/commands/test/run-tests.js +++ b/src/commands/test/run-tests.js @@ -219,8 +219,6 @@ async function runTests(flatConfigurations, options) { } catch (err) { await Promise.all(context.activeTargets.map((target) => target.stop())); throw err; - } finally { - process.exit(0); } } From 6769d4fd2a099ec4cf70e5722fc7be47abf047b1 Mon Sep 17 00:00:00 2001 From: Maciej Jaroszewski Date: Thu, 17 Jan 2019 13:12:54 +0100 Subject: [PATCH 08/11] Add process exit on success --- src/commands/test/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/test/index.js b/src/commands/test/index.js index 5d751ec0..d5cb85f9 100644 --- a/src/commands/test/index.js +++ b/src/commands/test/index.js @@ -63,7 +63,11 @@ async function test(args) { } try { - await runTests(configurations, options); + await runTests(configurations, options) + .then(() => process.exit(0)) + .catch((err) => { + throw err; + }); } catch (err) { if (err.name === 'ListrError') { const imageErrors = err.errors.filter( From ffcd45df3d797fe8331ac769b012bdf8861e43ed Mon Sep 17 00:00:00 2001 From: Maciej Jaroszewski Date: Thu, 17 Jan 2019 13:13:47 +0100 Subject: [PATCH 09/11] Remove console.info debugging --- src/targets/chrome/create-chrome-target.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/targets/chrome/create-chrome-target.js b/src/targets/chrome/create-chrome-target.js index af92292b..2a3ae00c 100644 --- a/src/targets/chrome/create-chrome-target.js +++ b/src/targets/chrome/create-chrome-target.js @@ -91,7 +91,6 @@ function createChromeTarget( }; Network.requestWillBeSent(({ requestId, request }) => { - console.info('!!!!', request.url); pendingRequestURLMap[requestId] = request.url; }); From e91e4d4feec273205646ec9efa1e1cc75b54ad84 Mon Sep 17 00:00:00 2001 From: Maciej Jaroszewski Date: Fri, 18 Jan 2019 10:33:26 +0100 Subject: [PATCH 10/11] Sync with current branch of dbartholomae/loki --- src/commands/test/index.js | 8 +-- src/commands/test/run-tests.js | 42 +++++++-------- src/commands/test/test-story.js | 2 +- src/targets/chrome/app.js | 4 +- src/targets/chrome/create-chrome-target.js | 61 +++++++++++----------- src/targets/decorate-storybook.js | 6 +-- 6 files changed, 61 insertions(+), 62 deletions(-) diff --git a/src/commands/test/index.js b/src/commands/test/index.js index d5cb85f9..c3698b50 100644 --- a/src/commands/test/index.js +++ b/src/commands/test/index.js @@ -9,18 +9,18 @@ const { ensureDependencyAvailable } = require('../../dependency-detection'); const { ReferenceImageError } = require('../../errors'); const buildCommand = require('../../build-command'); -const escapeRegExp = (str) => str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); +const escapeRegExp = str => str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); const getUpdateCommand = (errors, args) => { const argv = minimist(args); - const stories = uniq(errors.map((e) => `${e.kind} ${e.story}`)); + const stories = uniq(errors.map(e => `${e.kind} ${e.story}`)); const storiesFilter = `^${stories.map(escapeRegExp).join('|')}$`; const tooManyToFilter = stories.length > 10; const argObject = Object.assign( { configurationFilter: argv._[1], - storiesFilter: !tooManyToFilter && storiesFilter + storiesFilter: !tooManyToFilter && storiesFilter, }, pickBy((value, key) => { switch (key) { @@ -71,7 +71,7 @@ async function test(args) { } catch (err) { if (err.name === 'ListrError') { const imageErrors = err.errors.filter( - (e) => e instanceof ReferenceImageError + e => e instanceof ReferenceImageError ); if (imageErrors.length !== 0) { error('Visual tests failed'); diff --git a/src/commands/test/run-tests.js b/src/commands/test/run-tests.js index 38ffbe19..bd154f49 100644 --- a/src/commands/test/run-tests.js +++ b/src/commands/test/run-tests.js @@ -11,16 +11,16 @@ const { createChromeAppTarget, createChromeDockerTarget, createIOSSimulatorTarget, - createAndroidEmulatorTarget + createAndroidEmulatorTarget, } = require('../../targets'); const testStory = require('./test-story'); async function placeGitignore(pathsToIgnore) { const parentDir = path.dirname(pathsToIgnore[0]); const gitignorePath = `${parentDir}/.gitignore`; - if (!await fs.pathExists(gitignorePath)) { - const relativeToParent = (p) => path.relative(parentDir, p); - const isDecendant = (p) => p.indexOf('..') !== 0; + if (!(await fs.pathExists(gitignorePath))) { + const relativeToParent = p => path.relative(parentDir, p); + const isDecendant = p => p.indexOf('..') !== 0; const gitignore = pathsToIgnore .map(relativeToParent) .filter(isDecendant) @@ -30,14 +30,14 @@ async function placeGitignore(pathsToIgnore) { } } -const groupByTarget = (configurations) => +const groupByTarget = configurations => mapObjIndexed( fromPairs, groupBy(([, { target }]) => target, toPairs(configurations)) ); const filterStorybook = (storybook, excludePattern, includePattern) => { - const filterStory = (kind) => (story) => { + const filterStory = kind => story => { const fullStoryName = `${kind} ${story}`; const exclude = excludePattern && new RegExp(excludePattern, 'i').test(fullStoryName); @@ -49,12 +49,12 @@ const filterStorybook = (storybook, excludePattern, includePattern) => { return storybook .map(({ kind, stories }) => ({ kind, - stories: stories.filter(filterStory(kind)) + stories: stories.filter(filterStory(kind)), })) .filter(({ stories }) => stories.length !== 0); }; -const getListr = (options) => (tasks, listrOptions = {}) => { +const getListr = options => (tasks, listrOptions = {}) => { const newOptions = listrOptions; if (options.verboseRenderer) { newOptions.renderer = 'verbose'; @@ -89,14 +89,14 @@ async function runTests(flatConfigurations, options) { task: async () => { await target.prepare(); }, - enabled: () => !!target.prepare + enabled: () => !!target.prepare, }, { title: 'Start', task: async ({ activeTargets }) => { await target.start(); activeTargets.push(target); - } + }, }, { title: 'Fetch list of stories', @@ -105,7 +105,7 @@ async function runTests(flatConfigurations, options) { if (storybook.length === 0) { throw new Error('Error: No stories were found.'); } - } + }, }, ...Object.values( mapObjIndexed( @@ -121,7 +121,7 @@ async function runTests(flatConfigurations, options) { title: kind, task: () => getListr(options)( - stories.map((story) => ({ + stories.map(story => ({ title: options.verboseRenderer ? `${kind}: ${story}` : story, @@ -134,12 +134,12 @@ async function runTests(flatConfigurations, options) { configurationName, kind, story - ) + ), })) - ) + ), })), { concurrent: concurrency, exitOnError: false } - ) + ), }), configurations ) @@ -152,13 +152,13 @@ async function runTests(flatConfigurations, options) { if (index !== -1) { activeTargets.splice(index, 1); } - } - } - ]) + }, + }, + ]), }; }; - const createTargetTask = (configurations) => { + const createTargetTask = configurations => { const { target } = configurations[Object.keys(configurations)[0]]; switch (target) { case 'chrome.app': { @@ -166,7 +166,7 @@ async function runTests(flatConfigurations, options) { 'Chrome (app)', createChromeAppTarget({ baseUrl: options.reactUri, - chromeFlags: options.chromeFlags + chromeFlags: options.chromeFlags, }), configurations, options.chromeConcurrency, @@ -217,7 +217,7 @@ async function runTests(flatConfigurations, options) { try { await tasks.run(context); } catch (err) { - await Promise.all(context.activeTargets.map((target) => target.stop())); + await Promise.all(context.activeTargets.map(target => target.stop())); throw err; } } diff --git a/src/commands/test/test-story.js b/src/commands/test/test-story.js index 4a9d850d..5a0e5b52 100644 --- a/src/commands/test/test-story.js +++ b/src/commands/test/test-story.js @@ -6,7 +6,7 @@ const { getImageDiffer } = require('../../diffing'); const SLUGIFY_OPTIONS = { lowercase: false, - separator: '_' + separator: '_', }; const getBaseName = (configurationName, kind, story) => diff --git a/src/targets/chrome/app.js b/src/targets/chrome/app.js index f5de114b..3c0ee77a 100644 --- a/src/targets/chrome/app.js +++ b/src/targets/chrome/app.js @@ -5,7 +5,7 @@ const createChromeTarget = require('./create-chrome-target'); function createChromeAppTarget({ baseUrl = 'http://localhost:6006', - chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'] + chromeFlags = ['--headless', '--disable-gpu', '--hide-scrollbars'], }) { let instance; @@ -13,7 +13,7 @@ function createChromeAppTarget({ const launchOptions = Object.assign( { chromeFlags, - logLevel: 'silent' + logLevel: 'silent', }, options ); diff --git a/src/targets/chrome/create-chrome-target.js b/src/targets/chrome/create-chrome-target.js index 2a3ae00c..60fab50a 100644 --- a/src/targets/chrome/create-chrome-target.js +++ b/src/targets/chrome/create-chrome-target.js @@ -8,7 +8,7 @@ const awaitLokiReady = require('./await-loki-ready'); const { withTimeout, TimeoutError, - withRetries + withRetries, } = require('../../failure-handling'); const { FetchingURLsError, ServerError } = require('../../errors'); @@ -29,7 +29,7 @@ function createChromeTarget( height: options.height, deviceScaleFactor: options.deviceScaleFactor || 1, mobile: options.mobile || false, - fitWindow: options.fitWindow || false + fitWindow: options.fitWindow || false, }; } @@ -45,7 +45,7 @@ function createChromeTarget( await Page.enable(); if (options.userAgent) { await Network.setUserAgentOverride({ - userAgent: options.userAgent + userAgent: options.userAgent, }); } if (options.clearBrowserCookies) { @@ -80,7 +80,7 @@ function createChromeTarget( } }; - const requestEnded = (requestId) => { + const requestEnded = requestId => { delete pendingRequestURLMap[requestId]; maybeFulfillPromise(); }; @@ -111,7 +111,7 @@ function createChromeTarget( maybeFulfillPromise(); }); - const evaluateOnNewDocument = (scriptSource) => { + const evaluateOnNewDocument = scriptSource => { if (Page.addScriptToEvaluateOnLoad) { // For backwards support return Page.addScriptToEvaluateOnLoad({ scriptSource }); @@ -126,7 +126,7 @@ function createChromeTarget( const expression = `(() => Promise.resolve((${functionToExecute})(${stringifiedArgs})).then(JSON.stringify))()`; const { result } = await Runtime.evaluate({ expression, - awaitPromise: true + awaitPromise: true, }); if (result.subtype === 'error') { throw new Error( @@ -138,7 +138,7 @@ function createChromeTarget( client.executeFunctionWithWindow = executeFunctionWithWindow; - client.loadUrl = async (url) => { + client.loadUrl = async url => { if (!options.chromeEnableAnimations) { debug('Disabling animations'); await evaluateOnNewDocument(disableAnimations.asString); @@ -153,7 +153,7 @@ function createChromeTarget( await executeFunctionWithWindow(awaitLokiReady); }; - const getPositionInViewport = async (selector) => { + const getPositionInViewport = async selector => { try { return await executeFunctionWithWindow(getSelectorBoxSize, selector); } catch (error) { @@ -167,28 +167,27 @@ function createChromeTarget( }; client.captureScreenshot = withRetries(options.chromeRetries)( - withTimeout( - CAPTURING_SCREENSHOT_TIMEOUT, - 'captureScreenshot' - )(async (selector = 'body') => { - debug(`Getting viewport position of "${selector}"`); - const position = await getPositionInViewport(selector); - if (position.width === 0 || position.height === 0) { - throw new Error( - `Selector "${selector} has zero width or height. Can't capture screenshot.` - ); - } + withTimeout(CAPTURING_SCREENSHOT_TIMEOUT, 'captureScreenshot')( + async (selector = 'body') => { + debug(`Getting viewport position of "${selector}"`); + const position = await getPositionInViewport(selector); + if (position.width === 0 || position.height === 0) { + throw new Error( + `Selector "${selector} has zero width or height. Can't capture screenshot.` + ); + } - debug('Capturing screenshot'); - const clip = Object.assign({ scale: 1 }, position); - const screenshot = await Page.captureScreenshot({ - format: 'png', - clip - }); - const buffer = Buffer.from(screenshot.data, 'base64'); + debug('Capturing screenshot'); + const clip = Object.assign({ scale: 1 }, position); + const screenshot = await Page.captureScreenshot({ + format: 'png', + clip, + }); + const buffer = Buffer.from(screenshot.data, 'base64'); - return buffer; - }) + return buffer; + } + ) ); return client; @@ -204,7 +203,7 @@ function createChromeTarget( width: 100, height: 100, chromeEnableAnimations: true, - clearBrowserCookies: false + clearBrowserCookies: false, }); const url = `${baseUrl}/iframe.html`; try { @@ -243,7 +242,7 @@ function createChromeTarget( } const selector = configuration.chromeSelector || options.chromeSelector; const url = getStoryUrl(kind, story); - debug(`Loading story from ${url}`); + const tab = await launchNewTab(tabOptions); let screenshot; try { @@ -269,7 +268,7 @@ function createChromeTarget( prepare, getStorybook, launchNewTab, - captureScreenshotForStory + captureScreenshotForStory, }; } diff --git a/src/targets/decorate-storybook.js b/src/targets/decorate-storybook.js index fb1ad73b..16dbe1eb 100644 --- a/src/targets/decorate-storybook.js +++ b/src/targets/decorate-storybook.js @@ -80,7 +80,7 @@ function decorateStorybook(storybook) { enumerable: true, get: function() { return storiesOf; - } + }, }); } @@ -96,7 +96,7 @@ function decorateStorybook(storybook) { wrapWithAsyncStory(this.add.bind(this)), this.kind )(...args); - } + }, }); function getStorybook() { @@ -110,7 +110,7 @@ function decorateStorybook(storybook) { skipped: skipped, stories: stories.filter(function(story) { return skipped.indexOf(story.name) === -1; - }) + }), }; } From d32e063bb65e719022226da9d71613259936a166 Mon Sep 17 00:00:00 2001 From: Maciej Jaroszewski Date: Fri, 18 Jan 2019 10:43:44 +0100 Subject: [PATCH 11/11] Remove unnecessary parenthesis in function arg --- src/targets/chrome/create-chrome-target.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/targets/chrome/create-chrome-target.js b/src/targets/chrome/create-chrome-target.js index 60fab50a..1334ebe7 100644 --- a/src/targets/chrome/create-chrome-target.js +++ b/src/targets/chrome/create-chrome-target.js @@ -85,7 +85,7 @@ function createChromeTarget( maybeFulfillPromise(); }; - const requestFailed = (requestId) => { + const requestFailed = requestId => { failedURLs.push(pendingRequestURLMap[requestId]); requestEnded(requestId); }; @@ -171,6 +171,7 @@ function createChromeTarget( async (selector = 'body') => { debug(`Getting viewport position of "${selector}"`); const position = await getPositionInViewport(selector); + if (position.width === 0 || position.height === 0) { throw new Error( `Selector "${selector} has zero width or height. Can't capture screenshot.`