diff --git a/.babelrc b/.babelrc deleted file mode 100755 index d55826a4..00000000 --- a/.babelrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "presets": [["@babel/preset-env"]], - "plugins": [ - "@babel/plugin-proposal-class-properties", - [ - "@babel/transform-runtime", - { - "regenerator": true - } - ], - "dynamic-import-webpack" - ], - "env": { - "test": { - "plugins": ["istanbul"], - "sourceMaps": "inline", - "retainLines": true - } - } -} diff --git a/.nycrc-browser-api-local.yml b/.nycrc-browser-api-local.yml index a6f61e66..f5ccc668 100644 --- a/.nycrc-browser-api-local.yml +++ b/.nycrc-browser-api-local.yml @@ -6,6 +6,7 @@ exclude: - 'dist/**/*.js' - 'scripts/**/*.js' - 'coverage/**/*.js' + - 'src/test/clients/waychaser-direct.js' - 'src/test/clients/waychaser-via-webdriver-remote.js' check-coverage: false diff --git a/.nycrc-browser-api-remote.yml b/.nycrc-browser-api-remote.yml index a836cbfe..41ef6351 100644 --- a/.nycrc-browser-api-remote.yml +++ b/.nycrc-browser-api-remote.yml @@ -6,7 +6,7 @@ exclude: - 'dist/**/*.js' - 'scripts/**/*.js' - 'coverage/**/*.js' - - 'src/test/clients/waychaser-via-webdriver-local-*.js' + - 'src/test/clients/waychaser-direct.js' - 'src/test/clients/waychaser-via-webdriver-local.js' check-coverage: false diff --git a/.nycrc-node-api.yml b/.nycrc-node-api.yml index a6451974..e1daaaa2 100644 --- a/.nycrc-node-api.yml +++ b/.nycrc-node-api.yml @@ -6,7 +6,8 @@ exclude: - 'dist/**/*.js' - 'scripts/**/*.js' - 'coverage/**/*.js' - - 'src/test/clients/*.js' + - 'src/test/clients/waychaser-via-webdriver-remote.js' + - 'src/test/clients/waychaser-via-webdriver-local.js' check-coverage: false reporter: - text-summary diff --git a/.vscode/settings.json b/.vscode/settings.json index 7bcca923..55f1f3b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "eslint.alwaysShowStatus": true, "editor.formatOnSave": true, "cucumberautocomplete.onTypeFormat": true, - "cucumberautocomplete.steps": ["src/test/**/*.js"], + "cucumberautocomplete.steps": ["src/test/*.steps.js"], "cucumberautocomplete.skipDocStringsFormat": true, "cucumberautocomplete.syncfeatures": "src/test/**/*.feature", "editor.codeActionsOnSave": { diff --git a/README.md b/README.md index 81c6d9b1..4e0b22d6 100644 --- a/README.md +++ b/README.md @@ -149,11 +149,39 @@ returns an API Resource Object (ARO) ## Get list of operations - `ARO.operations` ✅ -- `ARO.op` ✅ (shorthand) +- `ARO.ops` ✅ (shorthand) - `ARO.operations()` - `ARO.getOperations()` -returns a map of API operation objects (AOO) + +so the plan was to return a map of API operation objects (AOO), with the key being the rel. However it's perfectly fine to have +multiple operations with the same rel. So, we either need to: + - return a map of arrays, + - return a map with each value either being a AOO or an Array of AOO. + +The former sucks from a refencing point of view. e.g., `ARO.ops[rel][0]()` +The later sucks because we need to test if AOO or array. e.g. `Array.isArray(ARO.ops[rel]) ? ARO.ops[rel][0]() : ARO.ops[rel]()` +The later sucks even more because most of the time, users would just do `ARO.ops[rel]()`, which will be fine until the day that +resource returns multiple links with the same rel. + +For instance if you have a ARO that is a list, you can have multiple links with `rel=item` with different `anchor`s, which tells +you where to get each item in the data. + +You could also have and ARO that has links with the same rel, that take a different number of parameters. + +It's worth noting that the `http-link-header` library's `link.get(attr, value)` method returns an array. + +Maybe if the ARO is a list, we can treat each item as a ARO. e.g. `ARO.data[9].operations` will return the operations just for that item. For more complexe structures `ARO.data.foo.bar.operations` will do the same. Doesn't feel ideal, because the data structre is not just a JSON. Also, things got to crap if the JSON has a field called `operations` + +Instead, mayb we can use the anchor structure for the operations. e.g. for a list, `ARO.operations[9][rel]` would get the link with the rel for the 9th item in the list. Whereas `ARO.operations.foo.bar[rel]` would get the link with the rel for the item at `foo.bar`. Again this will fail if the data has a field with the same name as a rel. 😭 + +We probably need to be explicit about the anchor. Something like `ARO.operations[anchor][rel]`.e.g., For root links it might look like `ARO.operations['#'][rel]`. + +What of `operations` returns a function? Then we could go `ARO.operations(rel)` for root and `ARO.operations(rel, {anchor="foo.bar})` or whatever other attribute we want to get it by. + +If we do that, what should happen if `ARO.operations(rel, {anchor="foo.bar})` still finds multiple links? We'd still have the same problem that we started with, so we're going to have to always return an array, in order to not break clients when the server adds an extra link with the same rel. + +So let's go with `ARO.operations(rel)` and `ARO.operations(rel, {anchor="foo.bar})` and `ARO.operations({anchor="foo.bar})` as all valid options. And they all return a set of operations, so `ARO.operations(rel, {anchor="foo.bar})[0]` is the same as `ARO.operations(rel)({anchor="foo.bar})[0]` ## Get specific operation @@ -161,7 +189,7 @@ returns a map of API operation objects (AOO) - `ARO[rel]` - `ARO.operation(rel)` - `ARO.operations[rel]` ✅ -- `ARO.op[rel]` ✅ +- `ARO.ops[rel]` ✅ returns an API operation or an array of API operations diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..0a321c62 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,48 @@ +module.exports = { + presets: [ + [ + "@babel/preset-env", + { + corejs: { + version: "3", + proposals: true, + }, + useBuiltIns: "entry", + targets: { + ie: "11", + browsers: [ + "ie >= 11", + "last 2 versions", + "> 0.2%", + "maintained node versions", + ], + }, + }, + ], + ], + plugins: [ + "add-module-exports", + "@babel/plugin-transform-arrow-functions", + "@babel/plugin-proposal-class-properties", + [ + "@babel/transform-runtime", + { + regenerator: true, + }, + ], + "dynamic-import-webpack", + [ + "@babel/plugin-transform-modules-commonjs", + { + allowTopLevelThis: true, + }, + ], + ], + env: { + test: { + plugins: ["istanbul"], + sourceMaps: "inline", + retainLines: true, + }, + }, +}; diff --git a/cucumber.js b/cucumber.js index 209d9f63..971c21a4 100644 --- a/cucumber.js +++ b/cucumber.js @@ -6,6 +6,7 @@ const FAIL_FAST = process.env.FAIL_FAST || "--fail-fast"; const NO_STRICT = process.env.NO_STRICT || ""; const outputDirectory = "test-results"; +fs.mkdirSync(outputDirectory, { recursive: true }); function getFeatureGlob(RERUN, profile) { /* istanbul ignore next: RERUN is not set for full test runs */ @@ -22,7 +23,7 @@ function generateConfig() { const resultsDirectory = `${outputDirectory}/${profile}`; fs.mkdirSync(resultsDirectory, { recursive: true }); - const RERUN = `@cucumber-${profile}.rerun`; + const RERUN = `${outputDirectory}/@cucumber-${profile}.rerun`; const FEATURE_GLOB = getFeatureGlob(RERUN, profile); const FORMAT_OPTIONS = { snippetInterface: "async-await", diff --git a/package-lock.json b/package-lock.json index a2828aa6..9d7a4e27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1026,6 +1026,14 @@ "requires": { "core-js": "^2.6.5", "regenerator-runtime": "^0.13.4" + }, + "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "dev": true + } } }, "@babel/preset-env": { @@ -1780,12 +1788,6 @@ "integrity": "sha512-XezsTPK1mG4/dEG8IQuhbFdW1CRYPCAO11gLaMGxQ5kGPynmmR9ResO7LJHpNgW26x64nGCutKUfKYIA02zMVw==", "dev": true }, - "@windyroad/quick-containers-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@windyroad/quick-containers-js/-/quick-containers-js-2.0.0.tgz", - "integrity": "sha512-kNNilBqSy10+tLPKIvXrEiN3xuCz99EyzebJrvVRqfW6nR7h74Z6QM12BM4+uYXVHm75tu/HeLsWx/koYDvyEQ==", - "dev": true - }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -1826,6 +1828,12 @@ "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true }, + "acorn-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.0.0.tgz", + "integrity": "sha512-oZRad/3SMOI/pxbbmqyurIx7jHw1wZDcR9G44L8pUVFEomX/0dH89SrM1KaDXuv1NpzAXz6Op/Xu/Qd5XXzdEA==", + "dev": true + }, "adm-zip": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", @@ -2513,6 +2521,12 @@ "babel-runtime": "^6.22.0" } }, + "babel-plugin-add-module-exports": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/babel-plugin-add-module-exports/-/babel-plugin-add-module-exports-1.0.4.tgz", + "integrity": "sha512-g+8yxHUZ60RcyaUpfNzy56OtWW+x9cyEe9j+CranqLiqbju2yf/Cy6ZtYK40EZxtrdHllzlVZgLmcOUCTlJ7Jg==", + "dev": true + }, "babel-plugin-check-es2015-constants": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", @@ -2911,6 +2925,12 @@ "regenerator-runtime": "^0.10.5" }, "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "dev": true + }, "regenerator-runtime": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", @@ -2979,6 +2999,12 @@ "regenerator-runtime": "^0.11.0" }, "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "dev": true + }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", @@ -3888,6 +3914,15 @@ "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", "dev": true }, + "bufferutil": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.2.tgz", + "integrity": "sha512-AtnG3W6M8B2n4xDQ5R+70EXvOpnXsFYg/AK2yTZd+HQ/oxAdz+GI+DvjmhBw3L0ole+LJ0ngqY4JMbDzkfNzhA==", + "dev": true, + "requires": { + "node-gyp-build": "^4.2.0" + } + }, "builtin-modules": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", @@ -5056,9 +5091,9 @@ "dev": true }, "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.7.0.tgz", + "integrity": "sha512-NwS7fI5M5B85EwpWuIwJN4i/fbisQUwLwiSNUWeXlkAZ0sbBjLEvLvFLf1uzAUV66PcEPt4xCGCmOZSxVf3xzA==", "dev": true }, "core-js-compat": { @@ -6284,6 +6319,15 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-dldq3ZfFtgVTJMLjOe+/3sROTzALlL9E34V4/sDtUd/KlBSS0s6U1/+WPE1B4sj9CXHJpL1M6rhNJnc9Wbal9w==", + "dev": true, + "requires": { + "jake": "^10.6.1" + } + }, "electron-to-chromium": { "version": "1.3.582", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.582.tgz", @@ -7948,6 +7992,15 @@ "dev": true, "optional": true }, + "filelist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", + "integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, "filename-reserved-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", @@ -7965,6 +8018,12 @@ "trim-repeated": "^1.0.0" } }, + "filesize": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.1.0.tgz", + "integrity": "sha512-LpCHtPQ3sFx67z+uh2HnSyWSLLu5Jxo21795uRDuar/EOuYWXib5EmPaGIBuSnRqH2IODiKA2k5re/K9OnN/Yg==", + "dev": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -8602,6 +8661,16 @@ "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -10356,6 +10425,26 @@ "is-object": "^1.0.1" } }, + "jake": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", + "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "dev": true, + "requires": { + "async": "0.9.x", + "chalk": "^2.4.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + } + } + }, "joi": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/joi/-/joi-17.2.1.tgz", @@ -11296,6 +11385,11 @@ "integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==", "dev": true }, + "lokijs": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/lokijs/-/lokijs-1.5.11.tgz", + "integrity": "sha512-YYyuBPxMn/oS0tFznQDbIX5XL1ltMcwFqCboDr8voYE4VCDzR5vAsrvQDhlnua4lBeqMqHmLvUXRTmRUzUKH1Q==" + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -12412,6 +12506,12 @@ "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", "dev": true }, + "node-gyp-build": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", + "dev": true + }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", @@ -13936,6 +14036,12 @@ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "dev": true }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -18783,11 +18889,6 @@ "prepend-http": "^1.0.1" } }, - "url-polyfill": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/url-polyfill/-/url-polyfill-1.1.12.tgz", - "integrity": "sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==" - }, "url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", @@ -18800,6 +18901,15 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "utf-8-validate": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.3.tgz", + "integrity": "sha512-jtJM6fpGv8C1SoH4PtG22pGto6x+Y8uPprW0tw3//gGFhDDTiuksgradgFN6yRayDP4SyZZa6ZMGHLIa17+M8A==", + "dev": true, + "requires": { + "node-gyp-build": "^4.2.0" + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", @@ -19098,6 +19208,101 @@ } } }, + "webpack-bundle-analyzer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.1.0.tgz", + "integrity": "sha512-R3oQaPn7KGJGqnOyuAbdNlH4Nm+w+gvoXQZWqYjgaMnR+vY4Ga8VD5ntfkKa00GarO7LQfOlePvtGvr254Z4Ag==", + "dev": true, + "requires": { + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^6.2.0", + "ejs": "^3.1.5", + "express": "^4.17.1", + "filesize": "^6.1.0", + "gzip-size": "^5.1.1", + "lodash": "^4.17.20", + "mkdirp": "^1.0.4", + "opener": "^1.5.2", + "ws": "^7.3.1" + }, + "dependencies": { + "acorn": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.4.tgz", + "integrity": "sha512-XNP0PqF1XD19ZlLKvB7cMmnZswW4C/03pRHgirB30uSJTaS3A3V1/P4sS3HPvFmjoriPCJQs+JDSbm4bL1TxGQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==", + "dev": true + } + } + }, "webpack-cli": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.2.0.tgz", @@ -19290,11 +19495,27 @@ "webpack-log": "^2.0.0" }, "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, "mime": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", "dev": true + }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } } } }, @@ -19339,6 +19560,12 @@ "yargs": "^13.3.2" }, "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -19453,6 +19680,16 @@ "has-flag": "^3.0.0" } }, + "webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + } + }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", @@ -19505,24 +19742,6 @@ } } }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - } - } - }, "webpack-merge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", diff --git a/package.json b/package.json index 29a17bdf..9ea907f7 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "env": "env", "build": "npm-run-all --parallel build:*", "build:main": "babel --delete-dir-on-start --source-maps true --verbose --ignore src/test --out-dir dist src", - "build:umd": "webpack --env OUTPUT_FILENAME=waychaser.umd.js", + "build:umd": "webpack --mode development --env OUTPUT_FILENAME=waychaser.umd.js", "build:umd.min": "webpack --mode production --env OUTPUT_FILENAME=waychaser.umd.min.js --env NODE_ENV=production", "prepack": "npm run build", "webpack:stats": "NODE_ENV=production webpack --mode production --env --profile --json > stats.json && open https://chrisbateman.github.io/webpack-visualizer/ && open http://webpack.github.io/analyse/", @@ -48,13 +48,15 @@ "watch:server:dev": "nodemon -V --config $(echo ${npm_lifecycle_event} | sed 's/watch:\\(.*\\):.*/\\1/').nodemon.json -x npm run ${npm_lifecycle_event#watch:}", "browser:base": "webpack serve --mode development --devtool inline-source-map", "browser:dev": "npm run browser:base -- --port ${npm_package_config_DEV_BROWSER_PORT} --env API_PORT=${npm_package_config_DEV_API_PORT} --hot", - "browser:test": "npm run browser:base -- --port ${npm_package_config_TEST_BROWSER_PORT} --env API_PORT=${npm_package_config_TEST_API_PORT} --open=false", + "browser:test": "npm run browser:base -- --no-hot --port ${npm_package_config_TEST_BROWSER_PORT} --env API_PORT=${npm_package_config_TEST_API_PORT} --open=false", "start:dev": "concurrently \"npm run watch:server:${npm_lifecycle_event#start:}\" \"npm run browser:${npm_lifecycle_event#start:}\"", + "start-no-hot:dev": "concurrently \"npm run server:${npm_lifecycle_event#start-no-hot:}\" \"npm run browser:${npm_lifecycle_event#start-no-hot:} -- --no-hot\"", "lint:sh": "shellcheck **/*.sh", "lint:js": "eslint .", "lint:js:fix": "npm run ${npm_lifecycle_event%:fix} -- --fix", "lint": "npm-run-all --sequential ${npm_lifecycle_event}:*", "test:node-api": "scripts/test-node.sh", + "headless:test:browser-api:chrome:local": "CI=1 npm run ${npm_lifecycle_event#headless:}", "test:browser-api:chrome:local": "scripts/test-browser.sh", "test:browser-api:chrome:remote": "scripts/test-browser.sh", "test:browser-api:firefox:local": "scripts/test-browser.sh", @@ -68,6 +70,7 @@ "test:browser-api": "scripts/for-all-browsers.sh", "test": "npm-run-all --sequential ${npm_lifecycle_event}:*", "watch:test:node-api": "nodemon -V -x npm -- run ${npm_lifecycle_event#watch:}", + "watch:headless:test:browser-api:chrome:local": "nodemon -V -x npm -- run ${npm_lifecycle_event#watch:}", "watch:test:browser-api:chrome:local": "nodemon -V -x npm -- run ${npm_lifecycle_event#watch:}", "watch:test:browser-api:firefox:local": "nodemon -V -x npm -- run ${npm_lifecycle_event#watch:}", "watch:test:browser-api": "nodemon -V -x npm -- run ${npm_lifecycle_event#watch:}", @@ -99,39 +102,42 @@ "/dist/*.js.map" ], "dependencies": { - "debug": "^4.1.1", + "debug": "^4.2.0", "es6-promise": "^4.2.8", "http-link-header": "^1.0.2", "isomorphic-fetch": "^3.0.0", - "url-polyfill": "^1.1.7" + "lokijs": "^1.5.11" }, "devDependencies": { "@babel/cli": "^7.6.2", "@babel/core": "^7.12.3", "@babel/node": "^7.6.2", "@babel/plugin-proposal-class-properties": "^7.5.5", + "@babel/plugin-transform-arrow-functions": "^7.12.1", + "@babel/plugin-transform-modules-commonjs": "^7.12.1", "@babel/plugin-transform-runtime": "^7.6.2", - "@babel/polyfill": "^7.4.4", "@babel/preset-env": "^7.6.2", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.7.0", "@babel/runtime": "^7.5.0", "@istanbuljs/nyc-config-babel": "^3.0.0", "@windyroad/cucumber-js-throwables": "^1.0.4", - "@windyroad/quick-containers-js": "^2.0.0", "babel-eslint": "^10.0.2", "babel-loader": "^8.0.0", + "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-dynamic-import-webpack": "^1.1.0", "babel-plugin-istanbul": "^6.0.0", "babel-plugin-transform-imports": "^2.0.0", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.7.0", "browserstack-local": "^1.4.8", + "bufferutil": "^4.0.2", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "chromedriver": "^86.0.0", "clean-webpack-plugin": "^3.0.0", "concurrently": "^5.3.0", + "core-js": "^3.7.0", "cucumber": "^5.1.0", "cucumber-junit-formatter": "^0.2.2", "dateformat": "^3.0.3", @@ -176,8 +182,10 @@ "selenium-webdriver": "^4.0.0-alpha.5", "shellcheck": "^0.4.4", "start-server-and-test": "^1.10.6", + "utf-8-validate": "^5.0.3", "webpack": "^4.5.0", - "webpack-cli": "^4.1.0", + "webpack-bundle-analyzer": "^4.1.0", + "webpack-cli": "^4.2.0", "webpack-dev-server": "^3.11.0" }, "lint-staged": { diff --git a/public/index.html b/public/index.html index 34a94de2..fe4ed963 100755 --- a/public/index.html +++ b/public/index.html @@ -5,9 +5,14 @@ Waychaser +

Waychaser

+

Waychaser status: Loading

+

Test: init

diff --git a/src/test/clients/waychaser-direct.js b/src/test/clients/waychaser-direct.js new file mode 100644 index 00000000..8dc338a9 --- /dev/null +++ b/src/test/clients/waychaser-direct.js @@ -0,0 +1,71 @@ +import logger from "../../util/logger"; +import logging from "selenium-webdriver/lib/logging"; +import { BROWSER_PORT, BROWSER_HOST } from "../config"; +import { utils } from "istanbul"; +import { waychaser } from "../../waychaser"; + +class WaychaserDirect { + async load(url) { + try { + const resource = await waychaser.load(url); + return { success: true, resource }; + } catch (error) { + return { success: false, error }; + } + } + + async getOperationsCount(result) { + return result.resource.operations.count(); + } + + async getOpsCount(result) { + return result.resource.ops.count(); + } + + async findOneOperationByRel(result, relationship) { + return result.resource.operations.findOneByRel(relationship); + } + + async findOneOpByRel(result, relationship) { + return result.resource.ops.findOneByRel(relationship); + } + + async invokeOperationByRel(result, relationship) { + try { + const resource = await result.resource.operations.invokeByRel( + relationship + ); + logger.debug({ resource }); + return { success: true, resource }; + } catch (error) { + return { success: false, error }; + } + } + + async invokeOpByRel(result, relationship) { + try { + logger.debug("invokeOpByRel", result.resource, relationship); + const resource = await result.resource.ops.invokeByRel(relationship); + logger.debug({ resource }); + return { success: true, resource }; + } catch (error) { + return { success: false, error }; + } + } + + async invokeByRel(result, relationship) { + try { + const resource = await result.resource.invokeByRel(relationship); + return { success: true, resource }; + } catch (error) { + return { success: false, error }; + } + } + + async getUrl(result) { + return result.resource.url; + } +} +const instance = new WaychaserDirect(); + +export { instance as waychaserDirect }; diff --git a/src/test/clients/waychaser-via-webdriver-local.js b/src/test/clients/waychaser-via-webdriver-local.js index 73c83a49..3e02e92a 100644 --- a/src/test/clients/waychaser-via-webdriver-local.js +++ b/src/test/clients/waychaser-via-webdriver-local.js @@ -19,8 +19,8 @@ class WaychaserViaWebdriverLocal extends WaychaserViaWebdriver { scenario.result.status === "failed" || scenario.result.status === "pending" ) { - logger.debug("waiting for browser debugging to complete..."); - await this.driver.allowDebug(600000); + // logger.debug("waiting for browser debugging to complete..."); + // await this.allowDebug(600000); } } diff --git a/src/test/clients/waychaser-via-webdriver.js b/src/test/clients/waychaser-via-webdriver.js index ee930ef4..4b12c409 100644 --- a/src/test/clients/waychaser-via-webdriver.js +++ b/src/test/clients/waychaser-via-webdriver.js @@ -2,6 +2,7 @@ import logger from "../../util/logger"; import logging from "selenium-webdriver/lib/logging"; import { BROWSER_PORT, BROWSER_HOST } from "../config"; import { utils } from "istanbul"; +import { waychaser } from "../../waychaser"; /* global __coverage__ */ // based on https://github.com/gotwarlost/istanbul-middleware/blob/master/lib/core.js#L217 @@ -15,69 +16,233 @@ function mergeClientCoverage(object) { ); }); } + class WaychaserViaWebdriver { - async load(url, options) { - logger.debug("loading url...: %s", url); - try { - const result = await this.driver.executeAsyncScript( - /* istanbul ignore next: won't work in browser otherwise */ - function () { - /* global window */ - console.log("starting load"); - const callback = arguments[arguments.length - 1]; - try { - window.waychaser - .load(arguments[0]) - .then(function (success) { - console.log("finished load", success); - const rval = { success, result: "success" }; - callback(rval); - }) - .catch(function (error) { - console.log("finished load", error); - const rval = { - error, - errorJson: JSON.stringify(error, undefined, 2), - errorString: error.toString(), - result: "error", - position: "inner", - }; - callback(rval); - }); - } catch (error) { - console.log("finished load", error); - const rval = { - error, - errorJson: JSON.stringify(error, undefined, 2), - errorString: error.toString(), - result: "error", - position: "outer", - }; - callback(rval); - } - }, - url, - options - ); - logger.debug("after load"); + async load(url) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + console.log("in method"); + console.log(window); + window.waychaser + .load(arguments[0]) + .then(function (resource) { + const id = uuidv4(); + window[id] = resource; + callback({ success: true, id }); + }) + .catch(function (error) { + const id = uuidv4(); + window[id] = error; + console.log(window); + console.log(window.error); + callback({ success: false, id }); + }); + }, + url + ); + } - await this.getBrowserLogs(); + async getOperationsCount(result) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + const id = arguments[0]; + callback(window[id.toString()].operations.count()); + }, + result.id + ); + } - logger.debug("result:", result); - if (result.success) { - return result.success; - } - throw new Error(result.error); - } catch (error) { - console.log("error", error); - throw error; - } + async getOpsCount(result) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + const id = arguments[0]; + callback(window[id.toString()].ops.count()); + }, + result.id + ); + } + + async findOneOperationByRel(result, relationship) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + const id = arguments[0]; + const relationship = arguments[1]; + console.log(relationship); + console.log(relationship === "self"); + console.log(window[id.toString()].operations.count()); + console.log( + "collection", + JSON.stringify(window[id.toString()].operations) + ); + console.log( + "findOne", + JSON.stringify(window[id.toString()].operations.findOne()) + ); + console.log( + "findOneByRel", + JSON.stringify( + window[id.toString()].operations.findOneByRel(relationship) + ) + ); + callback(window[id.toString()].operations.findOneByRel(relationship)); + }, + result.id, + relationship + ); + } + + async findOneOpByRel(result, relationship) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + const id = arguments[0]; + const relationship = arguments[1]; + console.log(relationship); + console.log(relationship === "self"); + console.log(window[id.toString()].ops.count()); + console.log("collection", JSON.stringify(window[id.toString()].ops)); + console.log( + "findOne", + JSON.stringify(window[id.toString()].ops.findOne()) + ); + console.log( + "findOneByRel", + JSON.stringify(window[id.toString()].ops.findOneByRel(relationship)) + ); + callback(window[id.toString()].ops.findOneByRel(relationship)); + }, + result.id, + relationship + ); + } + + async invokeOperationByRel(result, relationship) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + const id = arguments[0]; + const relationship = arguments[1]; + window[id.toString()].operations + .invokeByRel(relationship) + .then(function (resource) { + const id = uuidv4(); + window[id] = resource; + callback({ success: true, id }); + }) + .catch(function (error) { + const id = uuidv4(); + window[id] = error; + console.log(window); + console.log(window.error); + callback({ success: false, id }); + }); + }, + result.id, + relationship + ); + } + + async invokeOpByRel(result, relationship) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + const id = arguments[0]; + const relationship = arguments[1]; + window[id.toString()].ops + .invokeByRel(relationship) + .then(function (resource) { + const id = uuidv4(); + window[id] = resource; + callback({ success: true, id }); + }) + .catch(function (error) { + const id = uuidv4(); + window[id] = error; + console.log(window); + console.log(window.error); + callback({ success: false, id }); + }); + }, + result.id, + relationship + ); + } + + async invokeByRel(result, relationship) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + const id = arguments[0]; + const relationship = arguments[1]; + window[id.toString()] + .invokeByRel(relationship) + .then(function (resource) { + const id = uuidv4(); + window[id] = resource; + callback({ success: true, id }); + }) + .catch(function (error) { + const id = uuidv4(); + window[id] = error; + console.log(window); + console.log(window.error); + callback({ success: false, id }); + }); + }, + result.id, + relationship + ); + } + + async getUrl(result) { + return this.driver.executeAsyncScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + const callback = arguments[arguments.length - 1]; + const id = arguments[0]; + callback(window[id.toString()].url); + }, + result.id + ); } async loadWaychaserTestPage() { - logger.debug("...loading page"); + logger.debug("loading page..."); await this.driver.get(`http://${BROWSER_HOST}:${BROWSER_PORT}`); logger.debug("...page loaded"); + + logger.debug("setting uo uuid function..."); + await this.driver.executeScript( + /* istanbul ignore next: won't work in browser otherwise */ + function () { + window.uuidv4 = function () { + /* global crypto */ + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace( + /[018]/g, + function (c) { + return ( + c ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))) + ).toString(16); + } + ); + }; + } + ); + + logger.debug("waiting for waychaser..."); await this.driver.wait(() => { return this.driver.executeScript( /* istanbul ignore next: won't work in browser otherwise */ diff --git a/src/test/get-operations.feature b/src/test/get-operations.feature new file mode 100644 index 00000000..2b624cae --- /dev/null +++ b/src/test/get-operations.feature @@ -0,0 +1,18 @@ + +Feature: Get Operations + + So that I can understand what I can do with a resource + As a developer + I want to be able to get operations + + Scenario: Get operations - empty + Given a resource with no operations + When waychaser successfully loads that resource + Then the loaded resource will have no operations + + Scenario: Get operations - single + Given a resource with a "self" operation + When waychaser successfully loads that resource + Then the loaded resource will have 1 operation + And the loaded resource will have "self" operation + But it won't have an "item" operation \ No newline at end of file diff --git a/src/test/invoke-operation.feature b/src/test/invoke-operation.feature new file mode 100644 index 00000000..958926af --- /dev/null +++ b/src/test/invoke-operation.feature @@ -0,0 +1,18 @@ + +Feature: Invoke Operation + + So that I can perform actions on a resource + As a developer + I want to be able to invoke operations + + Scenario: Invoke operation + Given a resource with a "self" operation that returns itself + When waychaser successfully loads that resource + And we successfully invoke the "self" operation + Then the same resource will be returned + + Scenario: Invoke operation error + Given a resource with a "error" operation that returns an error + When waychaser successfully loads that resource + When we invoke the "error" operation + Then it will NOT have loaded successfully \ No newline at end of file diff --git a/src/test/load-api.feature b/src/test/load-api.feature deleted file mode 100644 index 9e4478e7..00000000 --- a/src/test/load-api.feature +++ /dev/null @@ -1,16 +0,0 @@ - -Feature: Poll Releases - - So that I can start to interact with an API - As a deveroper - I want to be able to load the API - - Scenario: Load API - Given a API returning 200 - When we try to load that API - Then the API will load successfully - - Scenario: Load API error - Given a API returning 500 - When we try to load that API - Then the API will NOT load successfully diff --git a/src/test/load-api.js b/src/test/load-api.js deleted file mode 100644 index 262bbe94..00000000 --- a/src/test/load-api.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Given, When, Then } from "cucumber"; -import logger from "../util/logger"; -// eslint-disable-next-line no-unused-vars -import { API_ACCESS_PORT, API_ACCESS_HOST } from "./config"; - -Given("a API returning {int}", async function (status) { - await this.router.route("/api").get(async (request, response) => { - response.status(status).send({ status }); - }); - logger.debug("/api route setup"); -}); - -When("we try to load that API", async function () { - this.attempt = this.waychaser.load( - `http://${API_ACCESS_HOST}:${API_ACCESS_PORT}/api` - ); -}); - -Then("the API will load successfully", { timeout: 40000 }, async function () { - await expect(this.attempt).to.not.be.rejectedWith(Error); -}); - -Then("the API will NOT load successfully", async function () { - await expect(this.attempt).to.be.rejectedWith(Error); -}); diff --git a/src/test/load-resource.feature b/src/test/load-resource.feature new file mode 100644 index 00000000..b3539a05 --- /dev/null +++ b/src/test/load-resource.feature @@ -0,0 +1,20 @@ + +Feature: Load Resource + + So that I can start to interact with an resource + As a developer + I want to be able to load the resource + + Scenario: Load API + Given a resource returning status code 200 + When waychaser loads that resource + Then it will have loaded successfully + + Scenario: Load API error response + Given a resource returning status code 500 + When waychaser loads that resource + Then it will NOT have loaded successfully + + Scenario: Load API error cannot connect + When waychaser loads a resource that's not available + Then it will NOT have loaded successfully diff --git a/src/test/load-resource.steps.js b/src/test/load-resource.steps.js new file mode 100644 index 00000000..c3f369cd --- /dev/null +++ b/src/test/load-resource.steps.js @@ -0,0 +1,32 @@ +import { expect } from "chai"; +import { Given, When, Then } from "cucumber"; +import logger from "../util/logger"; +// eslint-disable-next-line no-unused-vars +import { API_ACCESS_PORT, API_ACCESS_HOST } from "./config"; + +When("waychaser loads that resource", async function () { + this.result = await this.waychaserProxy.load( + `http://${API_ACCESS_HOST}:${API_ACCESS_PORT}/api` + ); +}); + +When("waychaser loads a resource that's not available", async function () { + this.result = await this.waychaserProxy.load( + `http://${API_ACCESS_HOST}:0/api` + ); +}); + +Then("it will have loaded successfully", async function () { + expect(this.result.success).to.be.true; +}); + +Then("it will NOT have loaded successfully", async function () { + expect(this.result.success).to.be.false; +}); + +When("waychaser successfully loads that resource", async function () { + this.result = await this.waychaserProxy.load( + `http://${API_ACCESS_HOST}:${API_ACCESS_PORT}/api` + ); + expect(this.result.success).to.be.true; +}); diff --git a/src/test/operations.steps.js b/src/test/operations.steps.js new file mode 100644 index 00000000..e33189f3 --- /dev/null +++ b/src/test/operations.steps.js @@ -0,0 +1,112 @@ +import { expect } from "chai"; +import { Given, When, Then } from "cucumber"; +import logger from "../util/logger"; + +Then("the loaded resource will have no operations", async function () { + const operationsCount = await this.waychaserProxy.getOperationsCount( + this.result + ); + expect(operationsCount).to.equal(0); + const opsCount = await this.waychaserProxy.getOpsCount(this.result); + expect(opsCount).to.equal(0); +}); + +Then("the loaded resource will have {int} operation(s)", async function ( + expected +) { + const operationsCount = await this.waychaserProxy.getOperationsCount( + this.result + ); + expect(operationsCount).to.equal(expected); + const opsCount = await this.waychaserProxy.getOpsCount(this.result); + expect(opsCount).to.equal(expected); +}); + +Then("the loaded resource will have {string} operation", async function ( + relationship +) { + const foundOperation = await this.waychaserProxy.findOneOperationByRel( + this.result, + relationship + ); + expect(foundOperation).to.be.not.null; + const foundOp = await this.waychaserProxy.findOneOpByRel( + this.result, + relationship + ); + expect(foundOp).to.be.not.null; +}); + +Then("it won't have a(n) {string} operation", async function (relationship) { + logger.debug({ relationship }); + const foundOperation = await this.waychaserProxy.findOneOperationByRel( + this.result, + relationship + ); + logger.debug({ foundOperation }); + expect(foundOperation).to.be.null; + const foundOp = await this.waychaserProxy.findOneOpByRel( + this.result, + relationship + ); + expect(foundOp).to.be.null; +}); + +When("we successfully invoke the {string} operation", async function ( + relationship +) { + this.previousResult = this.result; + + this.operationResult = await this.waychaserProxy.invokeOperationByRel( + this.result, + relationship + ); + expect(this.operationResult.success).to.be.true; + + this.opResult = await this.waychaserProxy.invokeOpByRel( + this.result, + relationship + ); + logger.debug("this.opResult", this.opResult); + expect(this.opResult.success).to.be.true; + + this.result = await this.waychaserProxy.invokeByRel( + this.result, + relationship + ); + expect(this.result.success).to.be.true; +}); + +When("we invoke the {string} operation", async function (relationship) { + this.previousResult = this.result; + + this.operationResult = await this.waychaserProxy.invokeOperationByRel( + this.result, + relationship + ); + + this.opResult = await this.waychaserProxy.invokeOpByRel( + this.result, + relationship + ); + + this.result = await this.waychaserProxy.invokeByRel( + this.result, + relationship + ); +}); + +Then("the same resource will be returned", async function () { + const operationResultUrl = await this.waychaserProxy.getUrl( + this.operationResult + ); + logger.debug({ opResult: this.opResult }); + const opResultUrl = await this.waychaserProxy.getUrl(this.opResult); + const resultUrl = await this.waychaserProxy.getUrl(this.result); + const previousResultUrl = await this.waychaserProxy.getUrl( + this.previousResult + ); + expect(operationResultUrl).to.deep.equal(previousResultUrl); + expect(opResultUrl).to.deep.equal(previousResultUrl); + expect(resultUrl).to.deep.equal(previousResultUrl); +}); diff --git a/src/test/resource.steps.js b/src/test/resource.steps.js new file mode 100644 index 00000000..6ad205c9 --- /dev/null +++ b/src/test/resource.steps.js @@ -0,0 +1,61 @@ +import { Given, When, Then } from "cucumber"; +import logger from "../util/logger"; +import LinkHeader from "http-link-header"; +import { API_ACCESS_PORT, API_ACCESS_HOST } from "./config"; + +Given("a resource returning status code {int}", async function (status) { + await this.router.route("/api").get(async (request, response) => { + response.status(status).send({ status }); + }); + logger.debug("/api route setup"); +}); + +Given("a resource with no operations", async function () { + await this.router.route("/api").get(async (request, response) => { + response.status(200).send({ status: 200 }); + }); +}); + +Given("a resource with a {string} operation", async function (relationship) { + await this.router.route("/api").get(async (request, response) => { + const links = new LinkHeader(); + links.set({ + rel: relationship, + }); + response.header("link", links.toString()).status(200).send({ status: 200 }); + }); +}); + +Given( + "a resource with a {string} operation that returns itself", + async function (relationship) { + await this.router.route("/api").get(async (request, response) => { + const links = new LinkHeader(); + links.set({ + rel: relationship, + uri: "/api", + }); + response + .header("link", links.toString()) + .status(200) + .send({ status: 200 }); + }); + } +); + +Given( + "a resource with a {string} operation that returns an error", + async function (relationship) { + await this.router.route("/api").get(async (request, response) => { + const links = new LinkHeader(); + links.set({ + rel: relationship, + uri: `http://${API_ACCESS_HOST}:0/api`, + }); + response + .header("link", links.toString()) + .status(200) + .send({ status: 200 }); + }); + } +); diff --git a/src/test/world.js b/src/test/world.js index 9d8b1b09..4b1e62da 100644 --- a/src/test/world.js +++ b/src/test/world.js @@ -14,8 +14,8 @@ import { import chai from "chai"; import logger from "../util/logger"; import chaiAsPromised from "chai-as-promised"; -import { waychaser as waychaserDirect } from "../waychaser"; +import { waychaserDirect } from "./clients/waychaser-direct"; import { waychaserViaWebdriverLocal } from "./clients/waychaser-via-webdriver-local"; import { waychaserViaWebdriverRemote } from "./clients/waychaser-via-webdriver-remote"; @@ -30,7 +30,7 @@ const profile = process.env.npm_lifecycle_event .replace("test:", "") .replace(/:/g, "-"); -let waychaser, webdriver; +let waychaserProxy, webdriver; // if testing via browser, setup web-driver if (profile.startsWith("browser-api")) { @@ -39,18 +39,18 @@ if (profile.startsWith("browser-api")) { local: waychaserViaWebdriverLocal, remote: waychaserViaWebdriverRemote, }; - waychaser = clients[mode.toString()]; + waychaserProxy = clients[mode.toString()]; /* istanbul ignore next: only get's executed when there are test config issues */ - if (waychaser === undefined) { + if (waychaserProxy === undefined) { throw new Error(`unknown mode: ${mode}`); } - webdriver = waychaser; + webdriver = waychaserProxy; webdriver.browser = profile.replace(/browser-api-(.*)-.*/, "$1"); } else { // otherwise, direct - waychaser = waychaserDirect; + waychaserProxy = waychaserDirect; } BeforeAll({ timeout: 240000 }, async function () { @@ -78,7 +78,7 @@ function world({ attach }) { Before({ timeout: 240000 }, async function (scenario) { logger.debug("BEGIN Before"); this.router = getNewRouter(); - this.waychaser = waychaser; + this.waychaserProxy = waychaserProxy; if (webdriver) { await webdriver.beforeTest(scenario); } diff --git a/src/util/logger.js b/src/util/logger.js index b1391ff7..c360b450 100644 --- a/src/util/logger.js +++ b/src/util/logger.js @@ -5,6 +5,7 @@ const logger = { error: debug("error"), browser: debug("browser"), remote: debug("remote"), + waychaser: debug("waychaser"), }; logger.debug.log = console.log.bind(console); @@ -12,7 +13,21 @@ logger.info.log = console.log.bind(console); logger.error.log = console.log.bind(console); logger.browser.log = console.log.bind(console); logger.remote.log = console.log.bind(console); +logger.waychaser.log = console.log.bind(console); -debug.enable("debug,info,error,browser,remote"); +debug.enable("debug,info,error,browser,remote,waychaser"); export default logger; + +/* +import getLogger from "webpack-log"; +const logger = { + debug: getLogger({ name: "debug", timestamp: true, level: "debug" }).debug, + info: getLogger({ name: "info", level: "debug" }).info, + error: getLogger({ name: "error", level: "debug" }).error, + browser: getLogger({ name: "browser", level: "debug" }).debug, + remote: getLogger({ name: "remote", level: "debug" }).debug, + waychaser: getLogger({ name: "waychaser", level: "debug" }).debug, +}; + +export default logger; */ diff --git a/src/waychaser.js b/src/waychaser.js index f2b1c1be..0d63d3d3 100644 --- a/src/waychaser.js +++ b/src/waychaser.js @@ -1,11 +1,47 @@ import fetch from "isomorphic-fetch"; -require("es6-promise").polyfill(); +import { polyfill } from "es6-promise"; +import LinkHeader from "http-link-header"; +import Loki from "lokijs"; +import logger from "./util/logger"; +polyfill(); -class ApiResourceObject { - constructor(response) { - this.response = response; +/** + * @param url + * @param options + */ +function loadResource(url, options) { + return fetch(url, options).then((response) => { + if (!response.ok) { + throw new Error("Bad response from server", response); + } + return new waychaser.ApiResourceObject(response); + }); +} + +class Operation { + constructor(callingContext) { + this.callingContext = callingContext; + } + + async invoke(context, options) { + logger.waychaser(this, context, options); + const contextUrl = this.callingContext.url; + const invokeUrl = new URL(this.uri, contextUrl); + logger.waychaser({ invokeUrl }); + return loadResource(invokeUrl, options); } } +Loki.Collection.prototype.findOneByRel = function (relationship) { + return this.findOne({ rel: relationship }); +}; + +Loki.Collection.prototype.invokeByRel = async function ( + relationship, + context, + options +) { + return this.findOneByRel(relationship).invoke(context, options); +}; /** @namespace */ const waychaser = { @@ -20,14 +56,41 @@ const waychaser = { * @throws {Error} If the server returns with a status >= 400 */ load: async function (url, options) { - return fetch(url, options).then((response) => { - console.log("waychaser:response", response); - if (response.status >= 400) { - throw new Error("Bad response from server", response); + return loadResource(url, options); + }, + + ApiResourceObject: class { + constructor(response) { + logger.waychaser("creating ARO", response); + this.response = response; + const linkHeader = this.response.headers.get("link"); + const linkDatabase = new Loki(); + this.operations = linkDatabase.addCollection(); + if (linkHeader) { + const links = LinkHeader.parse(linkHeader); + + this.operations.insert( + links.refs.map((reference) => { + logger.waychaser({ reference }); + logger.waychaser("creating operation", this.response, reference); + const operation = Object.assign( + new Operation(this.response), + reference + ); + logger.waychaser(JSON.stringify({ operation })); + return operation; + }) + ); } + } + + get ops() { + return this.operations; + } - return new ApiResourceObject(response); - }); + async invokeByRel(relationship) { + return this.operations.invokeByRel(relationship); + } }, }; diff --git a/webpack.config.js b/webpack.config.js index b82556fa..d2857f05 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,11 +13,21 @@ module.exports = (environment) => ({ optimization: { runtimeChunk: true, }, + node: { + fs: "empty", + }, module: { rules: [ { - test: /\.js$/, - exclude: /node_modules/, + test: [/\.(js)$/], + exclude: [ + /coverage/, + /docs/, + /out/, + /scripts/, + /test-results/, + /cucumber\.js/, + ], use: ["babel-loader"], }, ], @@ -28,6 +38,7 @@ module.exports = (environment) => ({ devServer: { port: environment.BROWSER_PORT, disableHostCheck: true, + liveReload: false, open: true, proxy: { "/api": {