From 20295f197fb2e5ec8a467a7eb1f6cd9ee5897188 Mon Sep 17 00:00:00 2001 From: Jan Wirth Date: Tue, 13 Apr 2021 12:07:46 +0200 Subject: [PATCH] GH-50: Open-source e2e-testing --- README.md | 12 +- package-lock.json | 514 ++++++++++++++++++++++++- package.json | 9 +- review/elm.json | 2 +- tests/e2e.mjs | 127 +++++++ tests/e2e/features/design.feature | 60 +++ tests/e2e/features/navigation.feature | 31 ++ tests/e2e/library.mjs | 520 ++++++++++++++++++++++++++ 8 files changed, 1264 insertions(+), 11 deletions(-) create mode 100644 tests/e2e.mjs create mode 100644 tests/e2e/features/design.feature create mode 100644 tests/e2e/features/navigation.feature create mode 100644 tests/e2e/library.mjs diff --git a/README.md b/README.md index 37fe96e..b94b2b4 100644 --- a/README.md +++ b/README.md @@ -141,13 +141,21 @@ This project runs on node. We develop funk using node version 15. git clone git@github.com:funk-team/funkLang.git cd funkLang npm install +``` -# run development environment +### run development environment npm start -# run unit tests +### test-driven-development +Start test-driven unit testing environment: +```sh npm run tdd-elm +``` +Start test-driven e2e testing environment: +Th +```sh +npm run tdd-e2e ``` ## 5. License diff --git a/package-lock.json b/package-lock.json index ed6bf3d..4ee7fe9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "funk", "version": "0.0.1", "hasInstallScript": true, - "license": "MIT", + "license": "GPL-3.0-only", "dependencies": { "@babel/plugin-syntax-dynamic-import": "^7.8.0", "@babel/polyfill": "^7.11.5", @@ -21,6 +21,7 @@ "elm-format": "^0.8.1", "elm-impfix": "^1.0.8", "elm-verify-examples": "^5.0.0", + "es-main": "^1.0.2", "fluture": "^13.0.1", "fluture-sanctuary-types": "^7.0.0", "git-root-dir": "^1.0.2", @@ -39,12 +40,14 @@ "prettier": "^2.2.1", "prettier-plugin-elm": "^0.7.0", "pretty-quick": "^3.1.0", + "puppeteer": "^8.0.0", "readline": "^1.3.0", "sanctuary": "^3.1.0", "sanctuary-def": "^0.22.0", "slugify": "^1.4.5", "socket.io": "^2.3.0", - "socket.io-client": "^2.3.0" + "socket.io-client": "^2.3.0", + "yadda": "^2.1.0" }, "bin": { "funk": "cli-src/index.mjs" @@ -2062,6 +2065,15 @@ "node": ">=0.10.0" } }, + "node_modules/@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -2270,6 +2282,17 @@ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2952,7 +2975,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -2963,7 +2985,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -3326,6 +3347,14 @@ "isarray": "^1.0.0" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -5222,6 +5251,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/devtools-protocol": { + "version": "0.0.854822", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.854822.tgz", + "integrity": "sha512-xd4D8kHQtB0KtWW0c9xBZD5LVtm9chkMOfs/3Yn01RhT/sFIsVtzTtypfKoFfWBaL+7xCYLxjOLkhwPXaX/Kcg==" + }, "node_modules/diff": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", @@ -6750,6 +6784,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-main": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-main/-/es-main-1.0.2.tgz", + "integrity": "sha512-LLgW8Cby/FiyQygrI23q2EswulHiDKoyjWlDRgTGXjQ3iRim2R26VfoehpxI5oKRXSNams3L/80KtggoUdxdDQ==" + }, "node_modules/es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -7260,6 +7299,26 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -7533,6 +7592,14 @@ "node": ">=0.8.0" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -7964,6 +8031,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", @@ -8801,6 +8873,18 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -10634,6 +10718,11 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -12010,6 +12099,11 @@ "node": ">=0.12" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -14260,6 +14354,14 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", @@ -14322,6 +14424,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -14407,6 +14514,116 @@ "node": ">=8" } }, + "node_modules/puppeteer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-8.0.0.tgz", + "integrity": "sha512-D0RzSWlepeWkxPPdK3xhTcefj8rjah1791GE82Pdjsri49sy11ci/JQsAO8K2NRukqvwEtcI+ImP5F4ZiMvtIQ==", + "hasInstallScript": true, + "dependencies": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.854822", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.1.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + }, + "engines": { + "node": ">=10.18.1" + } + }, + "node_modules/puppeteer/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/puppeteer/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/puppeteer/node_modules/ws": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -17091,6 +17308,32 @@ "node": ">= 10" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -17816,6 +18059,38 @@ "which-boxed-primitive": "^1.0.1" } }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -20243,6 +20518,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" }, + "node_modules/yadda": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/yadda/-/yadda-2.1.0.tgz", + "integrity": "sha512-jWEhhPemaU2OBNjOmwtdbNm7dHS43bNFexsV4aEDhlkz/vuGvYYHsySdSCM0YWdGLzlyqEJqLkXva+vFRTOblg==" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -20340,6 +20620,15 @@ "node": ">=6" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", @@ -22055,6 +22344,15 @@ } } }, + "@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -22251,6 +22549,14 @@ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -22796,7 +23102,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -22807,7 +23112,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -23110,6 +23414,11 @@ } } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -24622,6 +24931,11 @@ } } }, + "devtools-protocol": { + "version": "0.0.854822", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.854822.tgz", + "integrity": "sha512-xd4D8kHQtB0KtWW0c9xBZD5LVtm9chkMOfs/3Yn01RhT/sFIsVtzTtypfKoFfWBaL+7xCYLxjOLkhwPXaX/Kcg==" + }, "diff": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.3.tgz", @@ -25862,6 +26176,11 @@ "unbox-primitive": "^1.0.0" } }, + "es-main": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-main/-/es-main-1.0.2.tgz", + "integrity": "sha512-LLgW8Cby/FiyQygrI23q2EswulHiDKoyjWlDRgTGXjQ3iRim2R26VfoehpxI5oKRXSNams3L/80KtggoUdxdDQ==" + }, "es-to-primitive": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", @@ -26269,6 +26588,17 @@ } } }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -26493,6 +26823,14 @@ "websocket-driver": ">=0.5.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "requires": { + "pend": "~1.2.0" + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -26849,6 +27187,11 @@ } } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-extra": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", @@ -27527,6 +27870,15 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -28915,6 +29267,11 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -29990,6 +30347,11 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -31623,6 +31985,11 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, "promise": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", @@ -31675,6 +32042,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -31758,6 +32130,79 @@ "escape-goat": "^2.0.0" } }, + "puppeteer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-8.0.0.tgz", + "integrity": "sha512-D0RzSWlepeWkxPPdK3xhTcefj8rjah1791GE82Pdjsri49sy11ci/JQsAO8K2NRukqvwEtcI+ImP5F4ZiMvtIQ==", + "requires": { + "debug": "^4.1.0", + "devtools-protocol": "0.0.854822", + "extract-zip": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.1", + "pkg-dir": "^4.2.0", + "progress": "^2.0.1", + "proxy-from-env": "^1.1.0", + "rimraf": "^3.0.2", + "tar-fs": "^2.0.0", + "unbzip2-stream": "^1.3.3", + "ws": "^7.2.3" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "ws": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "requires": {} + } + } + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -33982,6 +34427,29 @@ } } }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "temp": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.0.tgz", @@ -34544,6 +35012,26 @@ "which-boxed-primitive": "^1.0.1" } }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } + } + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -36533,6 +37021,11 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" }, + "yadda": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/yadda/-/yadda-2.1.0.tgz", + "integrity": "sha512-jWEhhPemaU2OBNjOmwtdbNm7dHS43bNFexsV4aEDhlkz/vuGvYYHsySdSCM0YWdGLzlyqEJqLkXva+vFRTOblg==" + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -36614,6 +37107,15 @@ "decamelize": "^1.2.0" } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", diff --git a/package.json b/package.json index fb2b5cf..d59e0da 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "test-all": "npm run test-elm && npm run test-js", "test-elm": "elm-verify-examples && elm-app test && npm run test-elm-review", "test-elm-review": "elm-review", - "test-js": "rm -rf tests/unit/assets/temp; node tests/unit.mjs | tap-diff; rm -rf assets/temp" + "test-js": "rm -rf tests/unit/assets/temp; node tests/unit.mjs | tap-diff; rm -rf assets/temp", + "tdd-e2e": "npx nodemon tests/e2e.mjs --watch tests/e2e.mjs --watch tests/e2e --ext 'mjs,feature'", + "test-e2e": "node tests/e2e.mjs" }, "dependencies": { "@babel/plugin-syntax-dynamic-import": "^7.8.0", @@ -42,6 +44,7 @@ "elm-format": "^0.8.1", "elm-impfix": "^1.0.8", "elm-verify-examples": "^5.0.0", + "es-main": "^1.0.2", "fluture": "^13.0.1", "fluture-sanctuary-types": "^7.0.0", "git-root-dir": "^1.0.2", @@ -60,12 +63,14 @@ "prettier": "^2.2.1", "prettier-plugin-elm": "^0.7.0", "pretty-quick": "^3.1.0", + "puppeteer": "^8.0.0", "readline": "^1.3.0", "sanctuary": "^3.1.0", "sanctuary-def": "^0.22.0", "slugify": "^1.4.5", "socket.io": "^2.3.0", - "socket.io-client": "^2.3.0" + "socket.io-client": "^2.3.0", + "yadda": "^2.1.0" }, "repository": { "type": "git", diff --git a/review/elm.json b/review/elm.json index 2cb39c5..4f3822f 100644 --- a/review/elm.json +++ b/review/elm.json @@ -6,7 +6,7 @@ "direct": { "elm/core": "1.0.5", "elm/project-metadata-utils": "1.0.1", - "jfmengels/elm-review": "2.3.11", + "jfmengels/elm-review": "2.4.0", "jfmengels/elm-review-unused": "1.1.8", "stil4m/elm-syntax": "7.2.2" }, diff --git a/tests/e2e.mjs b/tests/e2e.mjs new file mode 100644 index 0000000..df64a9b --- /dev/null +++ b/tests/e2e.mjs @@ -0,0 +1,127 @@ +import puppeteer from 'puppeteer' +import fs from 'fs' +import Yadda from 'yadda' + +import PrettyError from 'pretty-error' +import library_ from './e2e/library.mjs' +import esMain from 'es-main' + +export const library = library_ + +/* + +// script for the browser to delete all projects +const deleteAllProjects = () => { + document.querySelector('[role="button"] svg').parentElement.click(); + + setTimeout(() => + { + document.querySelector('[class*="fc-231"][role="button"]').click(); + setTimeout(deleteAllProjects, 500) + } , 100) + } + +// helper for debugging xpaths + +function getElementByXpath(path, origin) { + return document.evaluate(path, origin || document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; +} + +*/ + +// instantiate PrettyError, which can then be used to render error objects +var pe = new PrettyError() + +// White text on white ground is not so readable +pe.appendStyle({ + 'pretty-error > header > message': { color: 'black' }, +}) +pe.start() + +export const run = async library => { + const featureParser = new Yadda.parsers.FeatureParser() + const parse = file => + fs.promises + .readFile(file, 'utf8') + .then(text => featureParser.parse(text)) + const features = await Promise.all( + new Yadda.FeatureFileSearch('./tests/e2e/features/').list().map(parse) + ) + const activeFeatures = filterFocus(features) + for (const feature of activeFeatures) { + await runFeature(feature) + } +} + +const filterFocus = items => { + const oneFocused = items.some(item => item.annotations.focus) + return items.filter(item => { + const isEffectivelyFocused = oneFocused ? item.annotations.focus : true + const isSkipped = item.annotations.skip + const isKept = !isSkipped && isEffectivelyFocused + if (!isKept) { + console.log('skipping', item.title) + } + return isKept + }) +} + +const runFeature = async feature => { + const yadda = Yadda.createInstance([library]) + + // run one scenario + const runOne = scenario => + new Promise((resolve, reject) => { + // define ctx here because the callback needs access to it + const ctx = {} + // yadda usese callbacks, we translate this into a promise + const callback = async (err, res) => { + if (err) { + try { + await ctx.page.screenshot({ + path: './tests/e2e/failure.png', + format: 'A4', + }) + } catch (e) { + console.log('could not take screenshot') + } + reject(err) + } else { + console.log('SCENARIO PASS') + await ctx.browser.close() + resolve() + } + } + const { title, steps } = scenario + console.log() + console.log('RUNNING ', title) + yadda.run(steps, { ctx }, callback) + }) + + // filter active scenarios + const activeScenarios = filterFocus(feature.scenarios) + const runAllScenarios = async activeScenarios => { + for (const scenario of activeScenarios) { + try { + await runOne(scenario) + console.log('SUCCESS ', scenario.title) + console.log() + } catch (err) { + console.log(err) + return false + } + } + return true + } + const success = await runAllScenarios(activeScenarios) + + if (success) { + console.log(`${feature.title}: ALL SCENARIOS PASS`) + } else { + throw new Error(`${feature.title}: FAIL`) + } +} + +if (esMain(import.meta)) { + run(library) +} diff --git a/tests/e2e/features/design.feature b/tests/e2e/features/design.feature new file mode 100644 index 0000000..319712b --- /dev/null +++ b/tests/e2e/features/design.feature @@ -0,0 +1,60 @@ +Feature: Design applications + + As a funk user + I want to add icons to my elements + In order to improve visual interest and grab the user's attention + + +Scenario: Use a google font + Given that I open the app + Then I see the editor + + # draw screens and add content + Then I use the shortcut Shift-E + And I draw a screen with an element + + And add the text screen1 to the currently selected element + + + Then I click the Style tab + And I add a custom typography style + + Then I set the font family to Poppins + And I expect the font family to be honored + + +Scenario: Add icon from GitHub and select the element + Given that I open the app + Then I see the editor + + # draw screens and add content + Then I use the shortcut Shift-E + And I draw a screen with an element + + When I go to Design mode + And go to icons + + # select an icon that covers the element in order to test the regression in + # https://github.com/funk-team/funk/issues/627 + And select an icon that covers the center of its viewbox + Then I wait for the icon to show up in my custom icon set + + Then I add the icon to the currently selected element + And I draw another screen with an element + + When I click the element with the icon + Then I expect the element to be selected + +Scenario: Draw and move elements around + Given that I open the app + Then I see the editor + + # draw screens and add content + Then I use the shortcut Shift-E + And I draw a screen with an element + + And I select the select and transform tool + When I click the screen + And I click and drag the screens's center handle + Then I expect the screen to still be selected + And I expect the screen to be in a different location diff --git a/tests/e2e/features/navigation.feature b/tests/e2e/features/navigation.feature new file mode 100644 index 0000000..f9d71a0 --- /dev/null +++ b/tests/e2e/features/navigation.feature @@ -0,0 +1,31 @@ +Feature: Navigation + As a funk user + I want to define navigation + In order to structure the information architecture of my application + + Regression test for https://github.com/funk-team/funk/issues/618 + +Scenario: Basic in-app navigation + + # log in & create project + Given that I open the app + Then I see the editor + + # draw screens and add content + When I use the shortcut Shift-E + And I draw a screen with an element + And add the text screen1 to the currently selected element + + And I draw another screen with an element + And add the text screen2 to the currently selected element + + # define navigation + Then I select the element that says screen1 + And tell the element to navigate to the second screen + + # test + When I click the first preview button + Then I expect to see screen1 + + When I click the link that says screen1 + Then I expect to see screen2 diff --git a/tests/e2e/library.mjs b/tests/e2e/library.mjs new file mode 100644 index 0000000..87ff59c --- /dev/null +++ b/tests/e2e/library.mjs @@ -0,0 +1,520 @@ +/** + * Step library for connecting feature files to e2e tests + * TODOS: + * factor out low-level and funk-specific + * automatically log steps performed + */ +import Yadda from 'yadda' +import puppeteer from 'puppeteer' +import S from 'sanctuary' + +// GLOBALS +// INFO: You will have to create the user first +const DEFAULT_TIMEOUT = 15000 +const elementInset = 20 + +export default Yadda.localisation.English.library() + + .given('that I open the app', async function () { + // init the context / the browser here because the step parsing does not happen before execution + // otherwise we would wait for the browser to launch every time we want to see if the library matches the feature file + this.ctx = Object.assign(this.ctx, await initContext()) + console.log('that I open the app') + }) + .then('I see the editor', async function () { + console.log('I see the editor') + await seeEditor(this.ctx.page) + }) + .when('I use the shortcut Shift-E', async function () { + console.log('I use the shortcut Shift-E') + await selectDrawTool(this.ctx.page) + }) + .then('I use the shortcut Shift-E', async function () { + console.log('I use the shortcut Shift-E') + await selectDrawTool(this.ctx.page) + }) + .when('I draw a screen with an element', async function () { + console.log('I draw a screen with an element') + await drawScreenWithElement(this.ctx.page)(0) + await validateScreenAndElementStyles(this.ctx.page) + }) + + .when('I go to $modeName mode', async function (modeName) { + await goToMode(this.ctx.page)(modeName) + }) + .then('I go to $modeName mode', async function (modeName) { + await goToMode(this.ctx.page)(modeName) + }) + // NAVIGATION + + .then( + 'add the text $text to the currently selected element', + async function (text) { + await addTextContent(this.ctx.page)(text) + } + ) + .then('I draw another screen with an element', async function () { + await drawScreenWithElement(this.ctx.page)(1) + }) + .then('I select the element that says $text', async function (text) { + await selectSelectAndTransformTool(this.ctx.page) + await clickCenterOfElementByText(this.ctx.page)(text) + }) + .then('I select the select and transform tool', async function () { + await selectSelectAndTransformTool(this.ctx.page) + }) + .then( + 'tell the element to navigate to the second screen', + async function () { + await setNavigationToSecondScreen(this.ctx.page) + } + ) + .when('I click the first preview button', async function () { + const previewPage = keepNewTab(this.ctx.browser) + await clickLinkByContent(this.ctx.page)('Preview') + this.ctx.deployedPage = await previewPage + }) + .then('I expect to see $text', async function (text) { + await expectToSee(this.ctx.deployedPage)(text) + }) + .when('I click the link that says $text', async function (text) { + await clickLinkByContent(this.ctx.deployedPage)(text) + }) + .when('go to icons', async function (text) { + await clickUiElementByText(this.ctx.page)('ICONS') + // make sure the tab is loaded + }) + .when( + 'select an icon that covers the center of its viewbox', + async function () { + await selectCircleFillIcon(this.ctx.page) + } + ) + .then( + 'I wait for the icon to show up in my custom icon set', + async function (text) { + await this.ctx.page.waitForSelector('svg.jam-circle-f') + } + ) + .then( + 'I add the icon to the currently selected element', + async function (text) { + await goToMode(this.ctx.page)('Canvas') + await clickAttributesPanelTab(this.ctx.page)('Content') + await clickUiElementByText(this.ctx.page)('Icons') + await this.ctx.page.waitForSelector('svg.jam-circle-f') + const icon = await this.ctx.page.$('svg.jam-circle-f') + await icon.click() + } + ) + .when('I click the element with the icon', async function (text) { + await this.ctx.page.waitForSelector('svg.jam-circle-f') + // select the element by content + const elementIcon = await this.ctx.page.$('[id^="el-"] svg.jam') + // https://stackoverflow.com/a/40333166/3934396 + const containingFunkElementXpath = + 'ancestor::*[starts-with(@id,"el-")][position()=1]' + const [element] = await elementIcon.$x(containingFunkElementXpath) + await selectSelectAndTransformTool(this.ctx.page) + await clickCenterOfElement(this.ctx.page)(element) + this.ctx.clickedElement = element + }) + .then('I expect the element to be selected', async function (text) { + await verifyElementIsSelected(this.ctx.page)(this.ctx.clickedElement) + }) + // MOVE ELEMENT WITH HANDLES + + .when('I click the screen', async function () { + const screen = await this.ctx.page.$('.funk-canvas-screen') + const { left, top } = await findElementPosition(this.ctx.page)(screen) + this.ctx.screen = screen + await this.ctx.page.mouse.move( + left + elementInset / 2, + top + elementInset / 2 + ) + await this.ctx.page.mouse.down() + await this.ctx.page.mouse.up() + await this.ctx.page.waitForTimeout(100) + await verifyElementIsSelected(this.ctx.page)(this.ctx.screen) + }) + .then("I click and drag the screens's center handle", async function () { + this.ctx.oldScreenPosition = await findElementPosition(this.ctx.page)( + this.ctx.screen + ) + await moveCurrentlySelectedElementUsingCenterHandle(this.ctx.page)( + this.ctx.screen + ) + }) + .then('I expect the screen to still be selected', async function () { + await verifyElementIsSelected(this.ctx.page)(this.ctx.screen) + }) + .then( + 'I expect the screen to be in a different location', + async function () { + // find new position of element + const currentPosition = await findElementPosition(this.ctx.page)( + this.ctx.screen + ) + if (S.equals(this.ctx.oldScreenPosition)(currentPosition)) { + throw new Error('screen has not moved') + } + } + ) + .then('I click the $tabName tab', async function (tabName) { + await clickAttributesPanelTab(this.ctx.page)(tabName) + }) + .when('I add a custom typography style', async function () { + await this.ctx.page.waitForSelector( + "[aria-label='Customize typography']" + ) + const addTypoButton = await this.ctx.page.$( + "[aria-label='Customize typography']" + ) + await addTypoButton.click() + }) + .then('I set the font family to $family', async function (family) { + await useDropdown(this.ctx.page)('Roboto')(family) + }) + .when('I expect the font family to be honored', async function () { + // get selected element + const selectedElement = await this.ctx.page.$(".outline[id^='el-']") + await ensureFontFaceIsLoadedForElement(this.ctx.page)(selectedElement) + }) + +export const keepNewTab = async browser => + new Promise(resolve => + browser.once('targetcreated', async target => { + const page = await target.page() + page.setDefaultTimeout(DEFAULT_TIMEOUT) + resolve(page) + }) + ) + +export const seeEditor = async page => { + await page.waitForSelector('.sidebar') +} +const selectDrawTool = async page => { + // enter sticky keyboard mode + await page.keyboard.down('Shift', { delay: 250 }) + await page.keyboard.press('E', { delay: 250 }) + await page.keyboard.up('Shift', { delay: 250 }) +} +const selectCircleFillIcon = async page => { + // sometimes the search field is not typed into, try to fix with timeout + // waiting for "New Icon set" does not work. + await page.waitForSelector('input[aria-label="Search icon"]') + await page.type('input[aria-label="Search icon"]', 'circle-f') + // select the fill circle from jam icons + const blackCircle = 'img[src*="/circle-f.svg"]' + await page.waitForSelector(blackCircle) + const icon = await page.$(blackCircle) + // make sure the CDN responds correctly + await waitForImageToLoad(icon) + await icon.click() +} + +/** + * Make sure screens have a white background and elements have a grey shade + * When fixing #623 the screen backgrounds broke, that's why we need this check + */ +const validateScreenAndElementStyles = async page => { + await page.waitForSelector(".funk-canvas-screen div[id^='el-']") + const screen = await page.$('.funk-canvas-screen') + const element = await page.$(".funk-canvas-screen div[id^='el-']") + const screenStyles = await getStyles(page)(screen) + const elementStyles = await getStyles(page)(element) + if (screenStyles['background-color'] !== 'rgb(255, 255, 255)') { + throw new Error( + `Screen background is not white, it is ${screenStyles['background-color']}` + ) + } + if (elementStyles['background-color'] !== 'rgba(0, 0, 0, 0.05)') { + throw new Error( + `Element background is not the transparent grey, it is ${elementStyles['background-color']}` + ) + } + if ( + elementStyles['box-shadow'] + .replace(/\s+/g, ' ') + .indexOf('rgba(100, 100, 100, 0.15)') < 0 + ) { + throw new Error( + `Element outline is not present, the shadow is ${elementStyles['box-shadow']}` + ) + } +} + +const getStyles = page => async element => { + const styles = await page.evaluate(element => { + const computed = window.getComputedStyle(element) + // the CSSStyleDeclaration is not serializing well + var serializeable = {} + Object.entries(computed).forEach( + ([_, key]) => (serializeable[key] = computed[key]) + ) + return serializeable + }, element) + return styles +} + +const verifyElementIsSelected = page => async element => { + await page.waitForTimeout(50) + const classNameHandle = await element.getProperty('className') + const className = await classNameHandle.jsonValue() + if (className.indexOf('outline') > -1) { + return + } else { + throw new Error( + `element has no outline and hence does not seem to be selected, the className is ${className}` + ) + } +} + +const ensureFontFaceIsLoadedForElement = page => async element => { + const styles = await getStyles(page)(element) + const wantedFontFace = { + family: styles['font-family'], + weight: styles['font-weight'], + style: styles['font-style'], + } + await page.waitForTimeout(2000) + await checkFontIsLoaded(page)(wantedFontFace)(0) +} + +const checkFontIsLoaded = page => wantedFontFace => async retries => { + const maxRetries = 5 + const interval = 300 + const loadedFontFaces = await page.evaluate(async () => { + var loadedFontFaces = [] + document.fonts.forEach( + ({ status, family, weight, style }) => + status === 'loaded' && + loadedFontFaces.push({ family, weight, style }) + ) + return loadedFontFaces + }) + + const fontLoaded = S.any(S.equals(wantedFontFace))(loadedFontFaces) + + if (!fontLoaded) { + if (retries > maxRetries) { + throw new Error( + `Font face '${wantedFontFace.family} ${wantedFontFace.style} ${wantedFontFace.weight}' not loaded.` + ) + } else { + // recurse to retry + await checkFontIsLoaded(page)(wantedFontFace)(retries + 1) + } + } +} + +const waitForImageToLoad = page => img => + page.evaluate(async () => { + await Promise.all( + selectors.map(img => { + if (img.complete) return + return new Promise((resolve, reject) => { + img.addEventListener('load', resolve) + img.addEventListener('error', reject) + }) + }) + ) + }, img) + +// START THE HEADLESS BROWSER + +const initContext = async () => { + const browser = await puppeteer.launch({ headless: false }) + const page = await browser.newPage() + page.setDefaultTimeout(DEFAULT_TIMEOUT) + await page.goto('http://localhost:3000', { + waitUntil: 'networkidle2', + }) + const uniqueProjectName = null + const deployedPage = null + + await page.setViewport({ width: 1280, height: 900 }) + return { page, browser, uniqueProjectName, deployedPage } +} +const selectSelectAndTransformTool = async page => { + // enter sticky keyboard mode + const button = await page.$("[title*='Select & Transform']") + await button.click() + + await page.waitForTimeout(100) +} + +const drawScreenWithElement = page => async offset => { + const screenWidth = 250 + const screenHeight = 350 + + // determine position of screen based on size + const screenPositionX = 400 + offset * (screenWidth + 50) // offset additional screens + const screenPositionY = 100 + + // element is square + const elementWidth = screenWidth - 2 * elementInset + const elementHeight = screenWidth - 2 * elementInset + + // draw screen + await page.mouse.move(screenPositionX, screenPositionY) + await page.mouse.down() + await page.mouse.move( + screenPositionX + screenWidth, + screenPositionY + screenHeight + ) + await page.mouse.up() + + // draw smaller element + await page.mouse.move( + screenPositionX + elementInset, + screenPositionY + elementInset + ) + await page.mouse.down() + await page.mouse.move( + screenPositionX + elementInset + elementWidth, + screenPositionY + elementInset + elementHeight + ) + await page.mouse.up() + return +} + +const clickAttributesPanelTab = page => async tabName => { + const TAB_XPATH = `//div[contains(., '${tabName}') and @role='button']` + await page.waitForXPath(TAB_XPATH) + const [tab] = await page.$x(TAB_XPATH) + if (tab) { + await tab.click() + return + } + console.error(`${tabName} tab not found`) + throw `${tabName} tab now found` +} + +export const addTextContent = page => async content => { + await clickAttributesPanelTab(page)('Content') + await page.waitForSelector('textarea') + await page.type('textarea', content) +} + +const clickUiElementByText = page => async text => { + const xpath = `//*[contains(text(), "${text}")]` + await page.waitForXPath(xpath) + const [element] = await page.$x(xpath) + await element.click() +} + +// TOOLS FOR SELECTING FUNK ELEMENTS +const clickCenterOfElementByText = page => async text => { + const xpath = `//*[funk-contenteditable[contains(., "${text}")]]` + const [element] = await page.$x(xpath) + await clickCenterOfElement(page)(element) +} + +const findElementPosition = page => async element => { + const rect = await page.evaluate(element => { + // destructuring and rebuilding because this will get converted to JSON + // and JSON can not handle circulars + const { top, left, bottom, right } = element.getBoundingClientRect() + return { top, left, bottom, right } + }, element) + return rect +} + +const moveCurrentlySelectedElementUsingCenterHandle = page => async element => { + const rect = await findElementPosition(page)(element) + const [centerX, centerY] = findCenter(rect) + await page.mouse.move(centerX, centerY) + await page.mouse.down() + await page.waitForTimeout(50) + await page.mouse.move(centerX + 100, centerY + 100) + await page.mouse.up() +} + +const findCenter = rect => { + const centerX = (rect.left + rect.right) / 2 + const centerY = (rect.top + rect.bottom) / 2 + return [centerX, centerY] +} + +const clickCenterOfElement = page => async element => { + const rect = await findElementPosition(page)(element) + const [centerX, centerY] = findCenter(rect) + await page.mouse.move(centerX, centerY) + await page.mouse.down() + await page.mouse.up() +} + +// DEPLOY SPECIFIC + +const goToMode = page => async modeName => { + const MODE_XPATH = `//a[.//*[contains(., '${modeName}')]]` + const [tab] = await page.$x(MODE_XPATH) + if (tab) { + await tab.click() + } else { + throw `${modeName} mode tab now found` + } +} +export const clickLinkByContent = page => async content => { + const [link] = await page.$x(`//a[contains(., '${content}')]`) + try { + await link.click() + } catch { + throw new Error(`Could not find link with text ${content}.`) + } +} + +export const expectToSee = page => async content => { + await page.waitForXPath(`//*[contains(., '${content}')]`) + return page +} + +const setNavigationToSecondScreen = async page => { + await clickAttributesPanelTab(page)('Actions') + // add action + await page.waitForXPath(`//div[contains(text(), "+")]`) + const [plusButton] = await page.$x(`//div[contains(text(), "+")]`) + await plusButton.click() + + // set action to navigate + await page.waitForXPath(`//div[contains(text(), "Pick action")]`) + const [dropdownButton] = await page.$x( + `//div[contains(text(), "Pick action")]` + ) + await dropdownButton.click() + + await page.waitForXPath(`//div[contains(text(), "Navigate")]`) + const [dropdownOption] = await page.$x( + `//div[contains(text(), "Navigate")]` + ) + await dropdownOption.click() + + // set action to navigate + await page.waitForXPath(`//div[contains(text(), "None selected")]`) + const [dropdown2Button] = await page.$x(`//div[contains(text(), "None")]`) + await dropdown2Button.click() + console.log('clicked button - is the dropdown showing?') + + const targetScreenXpath = `//*[contains(@class, 'dropdown')]//div[contains(text(), "Screen 2")]` + await page.waitForXPath(targetScreenXpath) + const [dropdown2Option] = await page.$x(targetScreenXpath) + + await dropdown2Option.click() +} + +const useDropdown = page => currentLabel => async optionToClick => { + const buttonXPath = `//div[contains(@class, 'dropdown-button') and ./*[contains(text(), "${currentLabel}")]]` + await page.waitForXPath(buttonXPath) + const [button] = await page.$x(buttonXPath) + await button.click() + + const optionXPath = `//div[contains(@class, 'dropdown-contents')]//*[contains(text(), "${optionToClick}")]` + await page.waitForXPath(optionXPath) + const [option] = await page.$x(optionXPath) + await option.click() + + // expect the label to show + const buttonAfterXPath = `//div[contains(@class, 'dropdown-button') and ./*[contains(text(), "${optionToClick}")]]` + await page.waitForXPath(buttonAfterXPath) +}