diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..aa8f7a5 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,14 @@ +{ + "env": { + "jest": true + }, + "globals": { + "document": true + }, + "extends": ["airbnb-base", "prettier"], + "plugins": ["prettier"], + "parser": "babel-eslint", + "rules": { + "class-methods-use-this": "off" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f80f9..6f1e5af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.3.0 (2019-06-09) + +### Added + +- Ability to show user's gold and exp points on the board bar. Ability to receive notifications when gained/lose exp and gold. ([2633ace](https://github.com/alexktzk/trello-habitica/commit/2633ace)) +- Ability to show to-do difficulty on a card badge with icons from Habitica. ([0467373](https://github.com/alexktzk/trello-habitica/commit/0467373)) + # 1.2.0 (2019-05-16) ### Added diff --git a/README.md b/README.md index 432a9c2..9535117 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,7 @@ Now it's time to fill in the form. 2. To work properly this Power-Up requires some permissions. Make sure you've checked all of the following: - board-buttons -- callback - card-badges -- card-buttons - card-detail-badges - list-actions diff --git a/package-lock.json b/package-lock.json index 1ef57dc..a05049a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1428,6 +1428,12 @@ } } }, + "acorn-jsx": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", + "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", + "dev": true + }, "acorn-walk": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", @@ -1560,6 +1566,16 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + } + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -1696,6 +1712,91 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-eslint": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz", + "integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@babel/types": "^7.0.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + } + } + }, "babel-jest": { "version": "24.5.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.5.0.tgz", @@ -2155,6 +2256,23 @@ "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", "dev": true }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + }, + "dependencies": { + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + } + } + }, "callsites": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz", @@ -2228,6 +2346,12 @@ "supports-color": "^5.3.0" } }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, "chokidar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz", @@ -2287,6 +2411,12 @@ "safe-buffer": "^5.0.1" } }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -2327,6 +2457,21 @@ } } }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -2551,6 +2696,12 @@ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", "dev": true }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, "content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", @@ -3085,6 +3236,15 @@ "buffer-indexof": "^1.0.0" } }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -3253,80 +3413,491 @@ "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", "dev": true, "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", + "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "dev": true, + "requires": { + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, + "eslint": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.3.0.tgz", + "integrity": "sha512-N/tCqlMKkyNvAvLu+zI9AqDasnSLt00K+Hu8kdsERliC9jYEc8ck12XtjvOXrBKu8fK6RrBcN9bat6Xk++9jAg==", + "dev": true, + "requires": { + "ajv": "^6.5.0", + "babel-code-frame": "^6.26.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^3.1.0", + "doctrine": "^2.1.0", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", + "eslint-visitor-keys": "^1.0.0", + "espree": "^4.0.0", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "functional-red-black-tree": "^1.0.1", + "glob": "^7.1.2", + "globals": "^11.7.0", + "ignore": "^4.0.2", + "imurmurhash": "^0.1.4", + "inquirer": "^5.2.0", + "is-resolvable": "^1.1.0", + "js-yaml": "^3.11.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.2", + "pluralize": "^7.0.0", + "progress": "^2.0.0", + "regexpp": "^2.0.0", + "require-uncached": "^1.0.3", + "semver": "^5.5.0", + "string.prototype.matchall": "^2.0.0", + "strip-ansi": "^4.0.0", + "strip-json-comments": "^2.0.1", + "table": "^4.0.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "eslint-config-airbnb": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-17.1.0.tgz", + "integrity": "sha512-R9jw28hFfEQnpPau01NO5K/JWMGLi6aymiF6RsnMURjTk+MqZKllCqGK/0tOvHkPi/NWSSOU2Ced/GX++YxLnw==", + "dev": true, + "requires": { + "eslint-config-airbnb-base": "^13.1.0", + "object.assign": "^4.1.0", + "object.entries": "^1.0.4" + } + }, + "eslint-config-airbnb-base": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz", + "integrity": "sha512-XWwQtf3U3zIoKO1BbHh6aUhJZQweOwSt4c2JrPDg9FP3Ltv3+YfEv7jIDB8275tVnO/qOHbfuYg3kzw6Je7uWw==", + "dev": true, + "requires": { + "eslint-restricted-globals": "^0.1.1", + "object.assign": "^4.1.0", + "object.entries": "^1.0.4" + } + }, + "eslint-config-prettier": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-4.3.0.tgz", + "integrity": "sha512-sZwhSTHVVz78+kYD3t5pCWSYEdVSBR0PXnwjDRsUs8ytIrK8PLXw+6FKp8r3Z7rx4ZszdetWlXYKOHoUrrwPlA==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz", + "integrity": "sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + } + } + }, + "eslint-plugin-import": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.17.2.tgz", + "integrity": "sha512-m+cSVxM7oLsIpmwNn2WXTJoReOF9f/CtLMo7qOVmKd1KntBy0hEcuNZ3erTmWjx+DxRO0Zcrm5KwAvI9wHcV5g==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.0", + "has": "^1.0.3", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", + "read-pkg-up": "^2.0.0", + "resolve": "^1.10.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + } + } + }, + "eslint-plugin-prettier": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.0.tgz", + "integrity": "sha512-XWX2yVuwVNLOUhQijAkXz+rMPPoCr7WFiAl8ig6I7Xn+pPVhDhzg4DxHpmbeb0iqjO9UronEA3Tb09ChnFVHHA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" } }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "eslint-restricted-globals": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", + "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", + "dev": true + }, + "eslint-scope": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.2.tgz", + "integrity": "sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg==", "dev": true, "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" } }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", "dev": true }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "espree": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-4.1.0.tgz", + "integrity": "sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==", "dev": true, "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" + "acorn": "^6.0.2", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" }, "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true } } }, - "eslint-scope": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.2.tgz", - "integrity": "sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, "esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", @@ -3818,6 +4389,17 @@ } } }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -3895,6 +4477,12 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-glob": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.6.tgz", @@ -3965,6 +4553,25 @@ "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", "dev": true }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, "fileset": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", @@ -4041,6 +4648,18 @@ } } }, + "flat-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", + "write": "^0.2.1" + } + }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -4685,12 +5304,24 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -4873,6 +5504,23 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -5227,6 +5875,44 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "internal-ip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.2.0.tgz", @@ -5469,6 +6155,12 @@ "isobject": "^3.0.1" } }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -5478,6 +6170,12 @@ "has": "^1.0.1" } }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -6146,6 +6844,12 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -6740,6 +7444,12 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "dev": true }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, "nan": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", @@ -6997,6 +7707,30 @@ "isobject": "^3.0.0" } }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.entries": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", + "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", @@ -7044,6 +7778,15 @@ "wrappy": "1" } }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, "opn": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz", @@ -7111,6 +7854,12 @@ "mem": "^4.0.0" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -7362,6 +8111,12 @@ "find-up": "^3.0.0" } }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -7414,6 +8169,15 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", @@ -7452,6 +8216,12 @@ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -7716,6 +8486,21 @@ "integrity": "sha512-nUmxvfJyAODw+0B13hj8CFVAxhe7fDEAgJgaotBu3nnR+IgGgZq59YedJP5VYTlkEfqjuK6TuRpnymKdatLZfQ==", "dev": true }, + "regexp.prototype.flags": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", + "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2" + } + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "regexpu-core": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", @@ -7885,6 +8670,24 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + } + } + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -7931,6 +8734,16 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -7962,6 +8775,15 @@ "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", "dev": true }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -7971,6 +8793,15 @@ "aproba": "^1.1.1" } }, + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -8184,6 +9015,15 @@ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0" + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -8643,6 +9483,19 @@ } } }, + "string.prototype.matchall": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-2.0.0.tgz", + "integrity": "sha512-WoZ+B2ypng1dp4iFLF2kmZlwwlE19gmjgKuhL1FJfDgCREWb3ye3SDVHSzLH6bxfnvYmkCxbzkmWcQZHA4P//Q==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.10.0", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "regexp.prototype.flags": "^1.2.0" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -8679,6 +9532,12 @@ "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", "dev": true }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -8688,12 +9547,32 @@ "has-flag": "^3.0.0" } }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", "dev": true }, + "table": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz", + "integrity": "sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==", + "dev": true, + "requires": { + "ajv": "^6.0.1", + "ajv-keywords": "^3.0.0", + "chalk": "^2.1.0", + "lodash": "^4.17.4", + "slice-ansi": "1.0.0", + "string-width": "^2.1.1" + } + }, "tapable": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", @@ -8755,12 +9634,24 @@ "require-main-filename": "^1.0.1" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "throat": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", "dev": true }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -8786,6 +9677,15 @@ "setimmediate": "^1.0.4" } }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -9602,6 +10502,15 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, "write-file-atomic": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", diff --git a/package.json b/package.json index 3f6696a..2abcf5c 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,16 @@ "@babel/plugin-transform-runtime": "^7.3.4", "@babel/preset-env": "^7.3.4", "@babel/runtime": "^7.3.4", + "babel-eslint": "^10.0.1", "babel-jest": "^24.5.0", "babel-loader": "^8.0.5", "cpy-cli": "^2.0.0", + "eslint": "^5.3.0", + "eslint-config-airbnb": "^17.1.0", + "eslint-config-airbnb-base": "^13.1.0", + "eslint-config-prettier": "^4.3.0", + "eslint-plugin-import": "^2.17.2", + "eslint-plugin-prettier": "^3.1.0", "fetch-mock": "^7.3.0", "html-webpack-plugin": "^3.2.0", "jest": "^24.1.0", diff --git a/src/js/constants.js b/src/js/constants.js index c323a77..959da7a 100644 --- a/src/js/constants.js +++ b/src/js/constants.js @@ -1,4 +1,4 @@ -export const API = 'https://habitica.com/api/v3'; +export const BASE_URL = 'https://habitica.com/api/v3'; export const LIST_TYPES = { DONE: 'done', @@ -11,15 +11,28 @@ export const CARD_SCOPES = { }; export const ICONS = { - HABITICA: + TRELLO_LOGO: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAACmAAAApgHdff84AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAACRQTFRFAGbMA3a9A3e8Ane9A3e9GYPCR5rMa63Ud7PXvNfm4Onu7O/xNljIFwAAAAN0Uk5TBWHLjO1H6wAAAGRJREFUGNNjYFQ2hgIjAQZhYzgwZFBGcIwYjM1X794ZbDp7965iYwZjq927dxebA4nFQI41kG62ABKbacMxAxKTYRyTjI72YBgH4jiiOZZATjKMY1rR0eYM4yABVG+jBAhyUAEAulltP7yEQqgAAAAASUVORK5CYII=', + HABITICA_LOGO: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAC0UExURUdwTAAAAEEjdUEjdUIzeDQvaQAA/0EidUEjdVMiiEQndkEjdUAidUEjdUIidkEjdUEidUAidUEidEEjdUEidUEjdkAjdUMic0EjdUEjdUEjdUEidUEjdUEjdUAjdUIkdEEidEAjdUEjdUEjdUAidUMidEEjd0EidUEidUEjdUEjdUIidkEkdUEjdEEjdT4ndkEjdUIidEIjdkEjdUQlekUlfEMkeEYnfUcngEopg0UnfEwqh08GzoYAAAAzdFJOUwABn9ADBQH9/gIKwq/7QJbLj1VD33ZwD/iKslvZ9UkvUL25pE0iKoPw5Zo2HHFnGMZ8ps6c3PwAAAF2SURBVBgZZcBVluMwEEDR57hsyaCQw8zQMFSSA73/fY3TZ/7mEvEBKf8T3npIxj/SSDKAhOFjCAkQiaR8ywChqn0HMskAqs5o3nkng5R8UIe4DYzbq5m/Bf/oQgTC8L709bYfUTxuxjkXBgkpJBHF3TgfFhWbQe100j+mQArkf5yvbR36FMGpHkgjoL8GqtF5oeqqrndWw5QM2PXyjHIQT9TVZqJWjb0dEBI6BWzu3lk1qlZVXZghJJz9DobeG+eM1YapY4GE3telt2bVre83tapq1bTJEOaxfh2A9Xx6UauqJvxAQDheL2WUkJ9nS7Wq6kKJAJGM096I8eIR1GrDPd8QXoTPIb/vS2O1Ya2vEICEapuzDU5VXcNPyQCE9+4HaeyNWhPCU+s2AqTkv9YIsTdW/W4zWoYVAgibFrlQBOd8D+jfViQRZJy6Y2Drl3rlZTDnW8bnotXpPkO4+SLejafhWhY/ASKOo86+9VLOWqf9fl+Wp78TOCsCfttvbQAAAABJRU5ErkJggg==', - TASK_DOING: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAC0UExURUdwTAAAAEEjdUEjdUIzeDQvaQAA/0EidUEjdVMiiEQndkEjdUAidUEjdUIidkEjdUEidUAidUEidEEjdUEidUEjdkAjdUMic0EjdUEjdUEjdUEidUEjdUEjdUAjdUIkdEEidEAjdUEjdUEjdUAidUMidEEjd0EidUEidUEjdUEjdUIidkEkdUEjdEEjdT4ndkEjdUIidEIjdkEjdUQlekUlfEMkeEYnfUcngEopg0UnfEwqh08GzoYAAAAzdFJOUwABn9ADBQH9/gIKwq/7QJbLj1VD33ZwD/iKslvZ9UkvUL25pE0iKoPw5Zo2HHFnGMZ8ps6c3PwAAAF2SURBVBgZZcBVluMwEEDR57hsyaCQw8zQMFSSA73/fY3TZ/7mEvEBKf8T3npIxj/SSDKAhOFjCAkQiaR8ywChqn0HMskAqs5o3nkng5R8UIe4DYzbq5m/Bf/oQgTC8L709bYfUTxuxjkXBgkpJBHF3TgfFhWbQe100j+mQArkf5yvbR36FMGpHkgjoL8GqtF5oeqqrndWw5QM2PXyjHIQT9TVZqJWjb0dEBI6BWzu3lk1qlZVXZghJJz9DobeG+eM1YapY4GE3telt2bVre83tapq1bTJEOaxfh2A9Xx6UauqJvxAQDheL2WUkJ9nS7Wq6kKJAJGM096I8eIR1GrDPd8QXoTPIb/vS2O1Ya2vEICEapuzDU5VXcNPyQCE9+4HaeyNWhPCU+s2AqTkv9YIsTdW/W4zWoYVAgibFrlQBOd8D+jfViQRZJy6Y2Drl3rlZTDnW8bnotXpPkO4+SLejafhWhY/ASKOo86+9VLOWqf9fl+Wp78TOCsCfttvbQAAAABJRU5ErkJggg==', - TASK_DONE: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAACZUExURUdwTJnNMpnMM4y6J5nMM2p9AJnMM5bfPpnMM5vPM5nMM5nNMpzQNpnMM5nMM5nLMpjLMpjNM5nNMpfLLpnMM5nNMpjMMpnNM5fKMpnMMpnMMpnMM5nMM5nMM5vMM5jKMprMM5jMM5rMM5nLNJfLNJrLNJjMM5jKM5nMM6TbN6PaNprOM6HXNp/UNaDVNZzQNJ3SNKbeN6jgOA6jikkAAAAodFJOUwD99gb+Af0C+gR5mx7s18sno0YOscVO0VZw5+KB9zstiHKrwUBilW/wrNXJAAABCklEQVQoz2WR2ZqDIAxGU9SAu1Vrp/vsA4ja9v0fbojaWTAX5PNEzw8C4FQQ2IVzWBSHj6yk5lQI73jDHSx5su/YYGLuevhGC4l4rx0OsRJSypOuHdHO4EpKYbJq3CF/8LId0PIru8wCPvEi7awI2+uTfSDoUQ/By22w9IU+Ag+Ir7MCQupjsFCvs+JTqf3ZfhBp9Cl4W5EA4KiR9ayBZmgpuEuLiX91EqXoT9GhH4OHZuKQ3RiJW0Pvr1BHFEx1Tg1FIvlt8Pr3x14OWlg4cp17s4jOkGwVTcbg5IfTwao3JZCC2/IPp0lYK5uB5uUfpwnE2mcqXtyonTz39423vGkLojxxRI+J4/kGZm0YuxibMCYAAAAASUVORK5CYII=', - NOTIFICATIONS: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAXCAQAAABKIxwrAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfjAhUOEh7JZlf7AAAA5UlEQVQ4y43RvWoCQRSG4ZfFf7ELxCrEQm0kjeg1GKIQMFcg3kRyQwqBXIliGxAbLYUtJGgQ3M9mwcWZnZ3TzDDnGfg4B8zKM+ePGTm86gMhxNhsBRb+GJ91H16kGd+aFLKCjNjFUYTYMnThKVECC3FhkoZfON9hIc507PzHgoX4tuEy/yn8RMmcTINiSsgSzyavOkZQc63JUQE+H/MmenDwJ5N3HbxthtqkjFGI1T1/d2Ah3pK4wG8GXye38pWBhfi85T548AMB5ICIFpXMDR2JbM999oSEhOzp+Wz6lSVHFgzM1hX87JwFnhPN4gAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOS0wMi0yMVQxMzoxODozMCswMTowMB5daKIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTktMDItMjFUMTM6MTg6MzArMDE6MDBvANAeAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg==' + TRIVIAL: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAANlBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAR2LVAAAAEXRSTlMAFKIxCKvcF5kPOYV7ziAEc3trLr0AAACgSURBVBgZ7cFZisMwFEXBq1nykOTsf7ONMaE7E9hPkJ9WlYZhGL5mUp9GU5eZWT0iENVhBhbZRTZRZgubRVaRXZTRjd1NNhfuLjIoiV+p6KTieeSdTnCeV9XpoFZ5rzYdca18Uq86pFXeqU2HOc8z73RKSfyVik4ribtUZLKyW2UU2AVZZTZZZoFNkF0GsjpMQFCPTFaXiUl9Vg3DMPwbP7XEEQha8GZ+AAAAAElFTkSuQmCC', + EASY: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAARVBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADZo62CAAAAFnRSTlMAr8sx0UTtxksKeVG4ciDmKls5bOMQ1FHhSgAAAOJJREFUWMPtlcsSgjAQBEEgEMS39v9/qgexRElmrPJkVfq6PVubB6SqCoVCIcn4o3Lm7PJamZhcA6k0QKPzWpmA2g0glAbcCFqpwY0glSMPjvm8Vuq5WrsBMsqJJ6dcXinblhftkIorZdjwzmbVQindZw2g75ZxpXQ9aV4tpLLPFaHfP/JOyfRfrsEpw3qFn7volKF9q6XO0SmLU2636XvklMNcPOSvslYuc/WSb6CVGAAI4ms2yg6AnfofaCUGIMTKjZBXRjeAU2IwA1hlxD5NWonX6BoY5ebfxi+UQqFQ+FPuW6MsGdDOH+AAAAAASUVORK5CYII=', + MEDIUM: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAKlBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHrpZrAAAADXRSTlMAIKDgYEC/gBDwkM9Qsgnf8QAAAVxJREFUWMPdldkWgyAMBcWExC78/++2ttiDLBchb/W5mY6omWX5+8t7I0DEKBCCTUFCEKOATUF2gBgFLAryBYhRYFrBPQ7Aw82MryG5VmcaH0aU40OI+vgAojn/JtgUBo6hhhh8EDlidaYXYXVTr6Ie8zr5LWy37/xtm/2a1CZwKMwLRAU1rLRdwSLwUbAI7Ao2gbfCoEDxxm1b9yfpxRcWsKAVS/0F7NGW5wsNELTlqd8Aj0LDFzIkKDTUz5BHreMLJRTUOuqX8KilAAGoIO3cphuQuHGP1NyS+Q6uIdLxDFGrQI7IxxNEq0MpojZ+ILRdQoqSjtq/0TY9uclmr6NlFXE+5ioiuckCUbasQGTHfELUU3hCVB70PT+7CuL3L3dQwsBw44BaxgdKC9x5oJZRgeHWhbnWrkBUUBRjxgCGudauwEdBUYy5B2CYa6V+mQjVcnv2AU9jrv/wegGERSd0TQVf/QAAAABJRU5ErkJggg==', + HARD: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAIVBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt9G3DAAAACnRSTlMAEPBAwIB/IKJwAyvMcQAAAYdJREFUWMPtV8uWhSAME1oE+/8fPL5wUGnKub2LWQzb0CRHoaTT1K6wTOZaAgBTDFZ9iAmBkiyCJEAkiVgWVg1dZAMtC6uGLrKBhoVdQxM5QGxh19BEDhBaODX6IhVEFk6NrkgoUlHS6qlqSHkyhPnC1sVdCuJmS5yDWt6nuJXfKd7lb4pX+S9Fv/xO0S2vFItWvzLkoz6zuiWu1zekaBhQLcR0fIYexfMrdihqeY+i9x8fFG35k4KVk9RQPMt3+Dpl+lG+GAihWSeoIozQgq4zAwMVzYiAgIETLbilMTBwoBkTEDCwo8Vq6wwMbGi2CAgYWNFiP21MsG3bBHn6X399ZecWYudBYiHXUSYRdl0mFrEswOtMIqYF2FBYxLIAWxqJmBZgU2URywJs69fDwh89LG1MGHna5gAi0tjj2lD0QsrI835SaBlnJGBsFO6IoyWcsZC1x4Q+xUjMu1LGm2IkaN5Cyp1iJOq+Mo4zbH8h7vsHDv/I4x66/GOfe/D0j77u4fuT8f8HqE0oB/XJcWkAAAAASUVORK5CYII=', + CHECKED: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAQAAAAAYLlVAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfjBR4BLB+R2WA8AAACvklEQVRo3u2Yv09TURTHP632vTJojSZ1gKSUSIwO6mSMRhsXjcgkgwwEdyf9AyRgCaQGHFj4MWAcjXGRCGliYmICYWgEHdiMUBSQHwE1WITS61CEdx8FWuAdBt9ZX+79fN9955zvPQ/ccMON/z08B8D0cpKLlLLMCP2HDwBfShV3OcMSA2Tk8WU8YgyFQvGHl7J4DyHqSK7hFatMyuJLqLfgFWmGJPFBony14DPM8kBOQIBmJjT8PF34pPAmLUxZ8IoFujGl8H7amdPwM7RhyOE7mSFjwU/RSkAWv2rBfyNGUKoHm7Tb8OM0USKD9xCghTnt8JM0EpbCB2m2Zf4YUU5J4YuJanWvGOMx5TJ4LyHqGdfwSaKU45XBhzXLyaZeo9Th64b7r/CapFLPS3gTfoqYXOGFqLPhZ2iVajseim1+n2GBNgJydR/VMj/DPN1ylpPL75/JGe4e/N7Lfd7zgRind/2t/HTY/H6ap/nedh4yxwppUvQR2ZUEP13MapYzwROO5rt8hPTashRxbhTcLPfs99MW7Yu84WZBp2DSYcMniRbWdl6xbFn+kx5u5bncw7FNfj9KA6HCPuRlhjQJP3hNRR5bZP3+u5Z6X6inrNA88nGHBEtaAfVwe4dcyO33DbtzPINqBklpp9BLxTYS9t3vDWoY4Ldlu1/0bVkRjvi9SS39moRF4lzPIcExvze5Z5OQ4i1XbNs66vcmtQxoubDCOy6w8RfFcb83qWFQqwhFnPNrliri9wbVJLS+oIhzDkPO7w2qGF73hw0JJ3L4vUMDtkElI5oARS+9tq43T6dztx0fET7ZJChb6jk83/u5to0Ekfm+iKtbSBCa7z0U5TwFwfke/EQsNybh+T4bPioZXu8LDsz3h3Z4nmGUSY5wHMVHnvOCz6j9FJDP2xhc4iwGoySY3F+8G2644Qb8BSHeLEPgAmSrAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE5LTA1LTI5VDIzOjQ0OjMxKzAyOjAwhLix6wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOS0wNS0yOVQyMzo0NDozMSswMjowMPXlCVcAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAAElFTkSuQmCC', + NOTIFICATIONS: { + BLACK: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAPFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHr/7WAAAAE3RSTlMA8KDPPzBgQMC/EJBwgLDgIFB/TeX9tgAAAQ9JREFUWMPtl1GzgiAYRBcIIUCt9v//1x6kshIHPm8vdziP6h7BWQYEADht2IzRDgthopApAEAwFGMCgIkHiIDjIRw0SdIn10jyeQiGJD0EeJI0IEkmiSCRJJFnIsB1waYgniuIOwJfUz7/vwXRVxB7E38r6E38A4Gt2RFsb+KuwJ4qsL2JvxX0Jh4QDBLBsAgUSWqJQJPknI/Kt9AaD7flwJ4H8n2GLrzxkwGYC9e3D6YfzACsqjJsjlRZ1Bp28kAYVflmOa/G1ZdfbwhX9WXIeXXd2B6KFVsZHpO0bR1bfqlW+aG1pdkgyL8ZRHlgzEVJKedH0VJ5oYWL7UD+zSDKP79DYXHWcImGNPGy98wd4OV99UW+D+wAAAAASUVORK5CYII=', + WHITE: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAQMAAACQp+OdAAAABlBMVEVHcEz///+flKJDAAAAAXRSTlMAQObYZgAAAH1JREFUKM/N0bENwyAUBuGzKFwyAqMwGh7NozACJQXiUoDkIlVSRHnV190vPaKqNvLCoCzMB+57MAAgf4tTtX6KP0n8YPwbXBAdkJXihLLeBDrJeh06SHoH7QStp95AarEDEHtqAJwjVwDCLDcAh167s3uUuZH3FFLfiA14AVTO7CnEdrzkAAAAAElFTkSuQmCC' + }, + GOLD: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAFcUExURUdwTPujIv+mIv+mI/+AAP+nAP2kIv+nI/+aIv+nI/+mIvmjIv+mIv+mIv+hG/+mIv+pIvehIf+mI/+mIv+mIP+lIv+kIv+nI/+mI/+mIv+mIv+lI/+mI/+lIP+dJ/+kIv+mI/+iH/+lIv+nI/+lJP+mI/ehIf+mIv+nI/+mI/+kI/+mIv+mI//Tkd6RH//Ukv+nI/+oI/6lI/+wJeWWH/+qJP/SkN2RH/+tJP7Rjv+nI/WfIfegIe6bIf+zJt2MAN+TJP/WlO2aIOOUHv/Ylt6OAN6SIt+SH+CTH96QGd6PEdyQHuCUIdOKG82HGdOYQP7Qi/O6bf/Ukc6SNOSZLPGdIf+mJv+jAM2FDPS9cOuZIP+9ZP+8Ys2OLO2mQ+WcNu2vWv2jFdWUNs6UOcmKI+SwZM6HHduhTuusVeOvYvG0Yf+nHu+rTd2QH9WMHNWcSduVLu69du6+dvG0ZaaQZ4IAAAAsdFJOUwD9lfICAf/7A/ru/uoeCc8O/oiQH8o+Tdv25eGCGAY49xSzJEfH/cKzoHNyipTLOAAAApZJREFUOMtVU2dz4jAUFBB6CyUQSO/JnZGrYnuMIbaxqckRIL0nF3JpV///zEk2dYfxB3afnt7TLgA2vAEAZrN78YVcbiG+l50FIOAFE5jxgqVkHvESYlkk8SifXALemTGPy7+GeMT4XQlFSbj8DOJDX+y/h/zaOo/8YVVoHWC0BDXsR/z62lARAJk873fpLaveKGM06lZNd/n5fMZRBEA0JAWLhlU3e7f/jo/fbntm3TKKQSmUJgoviM1JQQVa5t1Fu6NVKlqnfdEzIVSC0lwM0wGQLAWL8MC8b2t0YR+jQGvtj4YFi8HSIjki7WF9hmW+afR+YQBZ69w2LMPHeqL4Dm7er1vmvVYY8YUCff2uQlH38268vwgKtxp3Mqmnadr+0NdnFCfAWhhFZkEWMaplXmikXpZl+3N9dsoxChRVBmVJB6Hea5NS+Vf3udN5fnp8OKUoijNEgfTYklyt8g+aNJa/fX+tVF7PDznMU8xls+aStsA8ShyUjyv7tuDm5c/fF0fAMSfNagLNgxyrjAXd7s3Ref/QZ59wIkKFzQHPlODppt+fEnimW3R/q2rzyBaMWuxMXdJAyDganGBfcgekJsZ8PBcoyuj/tKdwxkyBzcGi5IJ29cBhivM5QziL2hyvmr46O2WoEUardh6rfK9hfkxTjD58LBAlzw3hxzvFjarxjBAOnjsAFolhqg2VYwhFfgynw+rQMF6w7FgOCgrH2OAUATqWW8Y0MW2EmFbEh16eYFzihiIxbSQ6tPUKtrVPr4li81MUP5uiWNN92NQr42Ck4yWGBKdWhbBaE1QcrlI8PRmtmHvVjt62omzb0Vt1x8a8ncKNVJ7leYllJZ5nF1IbYJJ34h/LJHfjHk98N5mJjeP/H2u0q0e5TrInAAAAAElFTkSuQmCC', + EXP: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAD8UExURUdwTP+lHv+/QP+mI/+mIv+mI/+mI/+pAP/yAP+mIv+mIf+mIP+mIv+mIv+mI/+kG/+jDv+mIv+lE/+mIv+lHv+kF/+mI/+nIv+sJP+uJf+rJP+9Wv+sJf+4Tv+sNf+mJP+8WP/UlP/Tkf/Rjf/cqe6cJvekK//ox//erP+qMeCTIv+/Yv+kFP+6U//Hev+uJ/+2TP++XvujIfWfIP/Da//itv/CafymKv20S//Tlv/Wmu+cIP+yRt2PGPfIh//fsffOkO6hLf+uPP+zTP+xRv/HeOObOe6aFuiXIueWHeq4dvGkOu6YAPDJjvbQmO/JkN6MAPvWn/S/d+CWKhATkNUAAAAYdFJOUwD9BL1Kv/0CAUj9/8rBRv79w/xq/P3GaXKR1V0AAAGcSURBVDjLhZPpUsIwFIVTaVNatoJVm6RLTMu+i+DKDu677/8upliUAQL3R2Yy58x3k7sAsBoqkGV+iCMOkkl+CEMFaYzTOxBxELOsmBgRAiDcgYgDBZkmEiJUoNlHs0lGiOCAZm5OcyLEAZBtY+q3yxLW+GWLDlJNo8HaLRMpYMOhcughNqbMJ9TN4BOeUF2T5Sz2ag122qOO7uGsvGJZyCkbSbVb5nevHHKR8JCVWlpCWVO4bLoOOb8ulnqFfOcMQoQVLbKkFYwyuXKLUqcaFIeD8cv9TefM1BGOpbksJ7EFzQltt0nvqlTqfo5GX883+XzhAkILJ7XIMJv7zD/tFovDt1LAqnVCL8t6aJDDFDGMjnLTBmP+dRC8D3p1ShxXX6aIHtk0DG45rwav32PSck0J2X+PjL7ZNGq3pM6ePiqFRIbLG5XI2l7NIdW7/kNirVD/pZZcUq/0O95GqZfNQqZDK49wW7OidksurXTg9nb/Tpx+mQ+7HReNHNbLBZ0DVPHQ6lA8tAvEzrHfuzj7Vy9c3uOd283Xfx3wA4kALaPyZ1lJAAAAAElFTkSuQmCC' }; - -export const TRELLO_ICON = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAACmAAAApgHdff84AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAACRQTFRFAGbMA3a9A3e8Ane9A3e9GYPCR5rMa63Ud7PXvNfm4Onu7O/xNljIFwAAAAN0Uk5TBWHLjO1H6wAAAGRJREFUGNNjYFQ2hgIjAQZhYzgwZFBGcIwYjM1X794ZbDp7965iYwZjq927dxebA4nFQI41kG62ABKbacMxAxKTYRyTjI72YBgH4jiiOZZATjKMY1rR0eYM4yABVG+jBAhyUAEAulltP7yEQqgAAAAASUVORK5CYII='; diff --git a/src/js/edit-task.js b/src/js/edit-task.js index aaab2de..e769d73 100644 --- a/src/js/edit-task.js +++ b/src/js/edit-task.js @@ -1,5 +1,6 @@ import TaskForm from './task-form'; +// eslint-disable-next-line no-undef const trello = TrelloPowerUp.iframe(); const form = new TaskForm(trello); form.initialize(); diff --git a/src/js/habitica-api.js b/src/js/habitica-api.js index 873c707..ad81cf3 100644 --- a/src/js/habitica-api.js +++ b/src/js/habitica-api.js @@ -1,23 +1,22 @@ -import { API } from './constants' -import Storage from './storage' +import { BASE_URL } from './constants'; +import Storage from './storage'; export default class HabiticaApi { - constructor( - trello, - storage = new Storage(trello) - ) { - this.t = trello - this.storage = storage + constructor(trello, storage = new Storage(trello)) { + this.t = trello; + this.storage = storage; } - async request(url, userParams = {}) { - let defaultParams = { + async request(path, userParams = {}) { + const url = BASE_URL + path; + const defaultParams = { headers: await this.authHeaders() - } + }; + + const params = Object.assign({}, defaultParams, userParams); - let params = Object.assign({}, defaultParams, userParams) - return fetch(url, params) - .then(res => this.handleResponse(res)) + // eslint-disable-next-line no-undef + return fetch(url, params).then(res => this.handleResponse(res)); } async authHeaders() { @@ -25,68 +24,74 @@ export default class HabiticaApi { 'x-api-user': await this.t.loadSecret('userId'), 'x-api-key': await this.t.loadSecret('apiToken'), 'Content-Type': 'application/json' - } + }; } handleResponse(res) { - if (res.ok) { - return res.json() - } else { - return this.handleError(res) - } + if (res.ok) return res.json(); + + return this.handleError(res); } async handleError(error) { - if (error.status == 401) { - await this.storage.removeUser().then(() => ( - this.notify( - "Wrong User ID or API Token. Try to login again.", - 'error' - ) - )) - } else if (error.status == 404) { - await this.storage.removeTask() + if (error.status === 401) { + await this.storage + .removeUser() + .then(() => + this.notify( + 'Wrong User ID or API Token. Try to login again.', + 'error' + ) + ); + } else if (error.status === 404) { + await this.storage.removeTask(); } - return error + return error; } - + addTask(params) { - return this.request(API + '/tasks/user', { + return this.request('/tasks/user', { method: 'POST', - body: JSON.stringify(params), - }) + body: JSON.stringify(params) + }); } updateTask(id, params) { - return this.request(API + `/tasks/${id}`, { + return this.request(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(params) - }) + }); } doTask(id) { - return this.request(API + `/tasks/${id}/score/up`, { - method: 'POST', - }) + return this.request(`/tasks/${id}/score/up`, { + method: 'POST' + }); } undoTask(id) { - return this.request(API + `/tasks/${id}/score/down`, { - method: 'POST', - }) + return this.request(`/tasks/${id}/score/down`, { + method: 'POST' + }); } removeTask(id) { - return this.request(API + `/tasks/${id}`, { - method: 'DELETE', - }) + return this.request(`/tasks/${id}`, { + method: 'DELETE' + }); } getUserProfile() { - return this.request(API + '/user?userFields=profile', { + return this.request('/user?userFields=profile', { + method: 'GET' + }); + } + + getUserStats() { + return this.request('/user?userFields=stats', { method: 'GET' - }) + }); } notify(message, display = 'info') { @@ -94,6 +99,6 @@ export default class HabiticaApi { message, display, duration: 10 - }) + }); } } diff --git a/src/js/index.js b/src/js/index.js index ecf3397..c0475ee 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -3,10 +3,10 @@ import Storage from './storage'; import Sync from './sync'; import List from './list'; -let syncing = {}; +const syncing = {}; const syncWithHabitica = async t => { - let id = t.getContext().card; + const id = t.getContext().card; // prevent duplicated tries to sync while previous sync in progress if (syncing[id]) return syncing[id]; @@ -18,68 +18,109 @@ const syncWithHabitica = async t => { return syncing[id]; }; +const priorityIcon = { + 0.1: ICONS.TRIVIAL, + 1: ICONS.EASY, + 1.5: ICONS.MEDIUM, + 2: ICONS.HARD +}; + +// eslint-disable-next-line no-undef TrelloPowerUp.initialize({ 'board-buttons': async t => { - let currentUser = await new Storage(t).getUser(); + let buttons; + const storage = new Storage(t); + const currentUser = await storage.getUser(); + const settings = await storage.getSettings(); - let settingsPage = t => - t.popup({ - title: 'Habitica settings', - url: './settings.html', - height: 394 - }); - - let loginPage = t => - t.popup({ - title: 'Log in Habitica', - url: './login.html', - height: 340 - }); + if (currentUser.loggedIn) { + buttons = [ + { + condition: 'always', + icon: { + dark: ICONS.HABITICA_LOGO, + light: ICONS.HABITICA_LOGO + }, + text: currentUser.name, + callback: tt => + tt.popup({ + title: 'Settings', + url: './settings.html', + height: 270 + }) + } + ]; - return [ - { - icon: ICONS.HABITICA, - text: currentUser.loggedIn ? currentUser.name : 'Login', - callback: currentUser.loggedIn ? settingsPage : loginPage - }, - { - icon: ICONS.NOTIFICATIONS, - text: ' ', - callback: t => - t.popup({ - title: "What's new?", - url: './changelog/', - height: 525 - }) + if (settings.showStats) { + buttons.unshift( + { + condition: 'always', + icon: { + dark: ICONS.EXP, + light: ICONS.EXP + }, + text: currentUser.exp + ? `${currentUser.exp} / ${currentUser.expToNextLevel}` + : '?' + }, + { + condition: 'always', + icon: { + dark: ICONS.GOLD, + light: ICONS.GOLD + }, + text: currentUser.gold ? currentUser.gold.toFixed(2) : '?' + } + ); } - ]; + } else { + buttons = [ + { + condition: 'always', + icon: { + dark: ICONS.HABITICA_LOGO, + light: ICONS.HABITICA_LOGO + }, + text: 'Login', + callback: tt => + tt.popup({ + title: 'Log in Habitica', + url: './login.html', + height: 340 + }) + } + ]; + } + + return buttons; }, 'card-badges': async t => { - let storage = new Storage(t); - let currentUser = await storage.getUser(); + const storage = new Storage(t); + const currentUser = await storage.getUser(); if (!currentUser.loggedIn) return []; return syncWithHabitica(t).then(async () => { - let settings = await storage.getSettings(); + const settings = await storage.getSettings(); if (!settings.showBadges) return []; - let taskData = await storage.getTask(); + const taskData = await storage.getTask(); + return [ - { icon: taskData.id ? ICONS.TASK_DOING : null }, - { icon: taskData.done ? ICONS.TASK_DONE : null } + { icon: taskData.id ? priorityIcon[taskData.priority] : null }, + { icon: taskData.done ? ICONS.CHECKED : null } ]; }); }, 'card-detail-badges': async t => { - let taskData = await new Storage(t).getTask(); + const taskData = await new Storage(t).getTask(); if (!taskData.id) return []; return [ { title: 'To-Do', text: 'Edit', - callback: t => - t.popup({ + callback: tt => + tt.popup({ title: 'Edit To-Do', url: './edit-task.html', height: 240 @@ -88,39 +129,40 @@ TrelloPowerUp.initialize({ ]; }, 'list-actions': async t => { - let storage = new Storage(t); - let currentUser = await storage.getUser(); + const storage = new Storage(t); + const currentUser = await storage.getUser(); if (!currentUser.loggedIn) return []; return storage.getLists().then(lists => { return t.list('id').then(list => { const listType = lists[list.id]; - if (listType === LIST_TYPES.DOING) { - return [ - { - text: 'Unmark list as "Doing"', - callback: t => new List(t).unmark() - } - ]; - } else if (listType === LIST_TYPES.DONE) { - return [ - { - text: 'Unmark list as "Done"', - callback: t => new List(t).unmark() - } - ]; - } else { - return [ - { - text: 'Mark list as "Doing"', - callback: t => new List(t).markAsDoing() - }, - { - text: 'Mark list as "Done"', - callback: t => new List(t).markAsDone() - } - ]; + switch (listType) { + case LIST_TYPES.DOING: + return [ + { + text: 'Unmark list as "Doing"', + callback: tt => new List(tt).unmark() + } + ]; + case LIST_TYPES.DONE: + return [ + { + text: 'Unmark list as "Done"', + callback: tt => new List(tt).unmark() + } + ]; + default: + return [ + { + text: 'Mark list as "Doing"', + callback: tt => new List(tt).markAsDoing() + }, + { + text: 'Mark list as "Done"', + callback: tt => new List(tt).markAsDone() + } + ]; } }); }); diff --git a/src/js/list.js b/src/js/list.js index 3f89bea..0472da0 100644 --- a/src/js/list.js +++ b/src/js/list.js @@ -1,50 +1,47 @@ -import Storage from './storage' -import { LIST_TYPES } from './constants' +import Storage from './storage'; +import { LIST_TYPES } from './constants'; export default class List { - constructor( - trello, - storage = new Storage(trello) - ) { - this.t = trello - this.storage = storage + constructor(trello, storage = new Storage(trello)) { + this.t = trello; + this.storage = storage; } markAsDone() { - return this.mark(LIST_TYPES.DONE) + return this.mark(LIST_TYPES.DONE); } markAsDoing() { - return this.mark(LIST_TYPES.DOING) + return this.mark(LIST_TYPES.DOING); } async mark(listType) { - const lists = await this.storage.getLists() - const list = await this.t.list('id', 'name') + const lists = await this.storage.getLists(); + const list = await this.t.list('id', 'name'); - lists[list.id] = listType + lists[list.id] = listType; - this.t.closePopup() - this.notify(`List "${list.name}" was successfully marked`, 'success') - return this.storage.setLists(lists) + this.t.closePopup(); + this.notify(`List "${list.name}" was successfully marked`, 'success'); + return this.storage.setLists(lists); } async unmark() { - const lists = await this.storage.getLists() - const list = await this.t.list('id', 'name') + const lists = await this.storage.getLists(); + const list = await this.t.list('id', 'name'); - delete lists[list.id] + delete lists[list.id]; - this.t.closePopup() - this.notify(`List "${list.name}" was successfully unmarked`, 'success') - return this.storage.setLists(lists) + this.t.closePopup(); + this.notify(`List "${list.name}" was successfully unmarked`, 'success'); + return this.storage.setLists(lists); } notify(message, display = 'info') { this.t.alert({ message, display, - duration: 3 - }) + duration: 5 // min is 5 + }); } } diff --git a/src/js/login.js b/src/js/login.js index ea60377..86db0a3 100644 --- a/src/js/login.js +++ b/src/js/login.js @@ -1,5 +1,6 @@ import LoginForm from './login-form'; +// eslint-disable-next-line no-undef const trello = TrelloPowerUp.iframe(); const form = new LoginForm(trello); form.initialize(); diff --git a/src/js/settings-form.js b/src/js/settings-form.js index 8827de3..ae90d3c 100644 --- a/src/js/settings-form.js +++ b/src/js/settings-form.js @@ -1,3 +1,5 @@ +/* eslint-disable class-methods-use-this */ + import Storage from './storage'; export default class SettingsForm { @@ -7,52 +9,34 @@ export default class SettingsForm { } initialize() { - this.storage.getSettings().then(settings => { - this.initializeElements(); - - this.setScope(settings.scope); - this.setPriority(settings.priority); - this.setShowBadges(settings.showBadges); - this.setPrependIcon(settings.prependIcon); - - this.listenToSubmit(); - this.listenToLogout(); - }); + this.initializeElements(); + this.assignValues(); + this.listenToSubmit(); + this.listenToLogout(); } initializeElements() { this.$scope = document.getElementById('scope'); this.$priority = document.getElementById('priority'); + this.$prependIcon = document.getElementById('prepend-icon'); + this.$showBadges = document.getElementById('show-badges'); + this.$showStats = document.getElementById('show-stats'); this.$submitButton = document.getElementById('submit-btn'); this.$logoutButton = document.getElementById('logout-btn'); + this.$showStatsNotifications = document.getElementById( + 'show-stats-notifications' + ); } - setScope(val) { - this.$scope.value = val; - } - - setPriority(val) { - this.$priority.value = val; - } - - setShowBadges(val) { - const el = document.querySelector(`#show-badges-${val}`); - if (el) el.checked = true; - } - - getShowBadges() { - const el = document.querySelector('input[name="show-badges"]:checked'); - return el ? JSON.parse(el.value) : true; - } - - setPrependIcon(val) { - const el = document.querySelector(`#prepend-icon-${val}`); - if (el) el.checked = true; - } - - getPrependIcon() { - const el = document.querySelector('input[name="prepend-icon"]:checked'); - return el ? JSON.parse(el.value) : false; + assignValues() { + return this.storage.getSettings().then(async settings => { + this.$scope.value = settings.scope; + this.$priority.value = settings.priority; + this.$prependIcon.checked = settings.prependIcon; + this.$showBadges.checked = settings.showBadges; + this.$showStats.checked = settings.showStats; + this.$showStatsNotifications.checked = settings.showStatsNotifications; + }); } listenToSubmit() { @@ -70,8 +54,10 @@ export default class SettingsForm { .setSettings({ scope: this.$scope.value, priority: this.$priority.value, - showBadges: this.getShowBadges(), - prependIcon: this.getPrependIcon() + prependIcon: this.$prependIcon.checked, + showBadges: this.$showBadges.checked, + showStats: this.$showStats.checked, + showStatsNotifications: this.$showStatsNotifications.checked }) .then(() => this.t.closePopup()); } diff --git a/src/js/settings.js b/src/js/settings.js index a008d89..5567f63 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -1,5 +1,6 @@ import SettingsForm from './settings-form'; +// eslint-disable-next-line no-undef const trello = TrelloPowerUp.iframe(); const form = new SettingsForm(trello); form.initialize(); diff --git a/src/js/storage.js b/src/js/storage.js index 9999de5..99d142f 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -1,8 +1,8 @@ export default class Storage { constructor(trello) { - this.t = trello + this.t = trello; } - + getAll() { return this.t.getAll(); } @@ -21,7 +21,12 @@ export default class Storage { setUser(obj) { return this.t.get('board', 'private', 'user', {}).then(current => { - return this.t.set('board', 'private', 'user', Object.assign({}, current, obj)); + return this.t.set( + 'board', + 'private', + 'user', + Object.assign({}, current, obj) + ); }); } @@ -35,7 +40,12 @@ export default class Storage { setTask(obj) { return this.t.get('card', 'private', 'task', {}).then(current => { - return this.t.set('card', 'private', 'task', Object.assign({}, current, obj)); + return this.t.set( + 'card', + 'private', + 'task', + Object.assign({}, current, obj) + ); }); } @@ -44,20 +54,27 @@ export default class Storage { } getSettings() { - let defaultSettings = { + const defaultSettings = { scope: 'me', - priority: '1', + priority: 1, + prependIcon: true, showBadges: true, - prependIcon: false + showStats: true, + showStatsNotifications: true }; return this.t.get('board', 'private', 'settings', {}).then(settings => { - return Object.assign({}, defaultSettings, settings) + return Object.assign({}, defaultSettings, settings); }); } setSettings(obj) { return this.t.get('board', 'private', 'settings', {}).then(current => { - return this.t.set('board', 'private', 'settings', Object.assign({}, current, obj)); + return this.t.set( + 'board', + 'private', + 'settings', + Object.assign({}, current, obj) + ); }); } diff --git a/src/js/sync.js b/src/js/sync.js index d429b6b..4add706 100644 --- a/src/js/sync.js +++ b/src/js/sync.js @@ -1,75 +1,82 @@ -import { CARD_SCOPES, LIST_TYPES } from './constants' -import Storage from './storage' -import Task from './task' +/* eslint-disable consistent-return */ +/* eslint-disable no-else-return */ +/* eslint-disable no-lonely-if */ + +import { CARD_SCOPES, LIST_TYPES } from './constants'; +import Storage from './storage'; +import Task from './task'; export default class Sync { - constructor( - trello, - storage = new Storage(trello) - ) { - this.t = trello - this.storage = storage - this.task = undefined + constructor(trello, storage = new Storage(trello)) { + this.t = trello; + this.storage = storage; + this.task = undefined; } - getTask() { - this.task = this.task || new Task(this.t, this.storage) - return this.task + currentTask() { + this.task = this.task || new Task(this.t, this.storage); + return this.task; } async start() { - let card = await this.t.card('id', 'idList', 'members') - let settings = await this.storage.getSettings() - let taskData = await this.storage.getTask() + const card = await this.t.card('id', 'idList', 'members'); + const settings = await this.storage.getSettings(); + const taskData = await this.storage.getTask(); // Skips the cards that are not assigned to user. - // Remove the badge if card was already synced. - if (settings.scope == CARD_SCOPES.ASSIGNED_TO_ME) { - let me = this.t.getContext().member - if (!card.members.some(member => member.id == me)) { - return this.handleScoped(taskData) + // Removes the badge if card was already synced. + if (settings.scope === CARD_SCOPES.ASSIGNED_TO_ME) { + const me = this.t.getContext().member; + if (!card.members.some(member => member.id === me)) { + return this.handleScoped(taskData); } } - let lists = await this.storage.getLists() - let listType = lists[card.idList] + const lists = await this.storage.getLists(); + const listType = lists[card.idList]; - return this.handle(taskData, listType) + return this.handle(taskData, listType); } handleScoped(taskData) { if (taskData.id) { if (taskData.done) { - return this.getTask().handleUndo().then(() => this.getTask().handleRemove()) + return this.currentTask() + .handleUndo() + .then(() => this.currentTask().handleRemove()); } else { - return this.getTask().handleRemove() + return this.currentTask().handleRemove(); } } } handle(taskData, listType) { - if (listType == LIST_TYPES.DOING) { + if (listType === LIST_TYPES.DOING) { if (taskData.id) { if (taskData.done) { - return this.getTask().handleUndo() + return this.currentTask().handleUndo(); } } else { - return this.getTask().handleAdd() + return this.currentTask().handleAdd(); } - } else if (listType == LIST_TYPES.DONE) { + } else if (listType === LIST_TYPES.DONE) { if (taskData.id) { if (!taskData.done) { - return this.getTask().handleDo() + return this.currentTask().handleDo(); } } else { - return this.getTask().handleAdd().then(() => this.getTask().handleDo()) + return this.currentTask() + .handleAdd() + .then(() => this.currentTask().handleDo()); } } else { if (taskData.id) { if (taskData.done) { - return this.getTask().handleUndo().then(() => this.getTask().handleRemove()) + return this.currentTask() + .handleUndo() + .then(() => this.currentTask().handleRemove()); } else { - return this.getTask().handleRemove() + return this.currentTask().handleRemove(); } } } diff --git a/src/js/task-form.js b/src/js/task-form.js index 09e7d22..f6a9886 100644 --- a/src/js/task-form.js +++ b/src/js/task-form.js @@ -1,49 +1,46 @@ -import Storage from './storage' -import Task from './task' +import Storage from './storage'; +import Task from './task'; export default class TaskForm { - constructor( - trello, - storage = new Storage(trello) - ) { - this.t = trello - this.storage = storage + constructor(trello, storage = new Storage(trello)) { + this.t = trello; + this.storage = storage; } initialize() { this.storage.getTask().then(task => { - this.initializeElements() + this.initializeElements(); - this.setPriority(task.priority) - - this.listenToSubmit() - }) + this.setPriority(task.priority); + + this.listenToSubmit(); + }); } initializeElements() { - this.$priority = document.getElementById('priority') - this.$submitButton = document.getElementById('submit-btn') + this.$priority = document.getElementById('priority'); + this.$submitButton = document.getElementById('submit-btn'); } setPriority(val) { - this.$priority.value = val + this.$priority.value = val; } listenToSubmit() { - this.$submitButton.addEventListener('click', () => this.handleSubmit()) + this.$submitButton.addEventListener('click', () => this.handleSubmit()); } - + updatePriority(val) { - return new Task(this.t).handleUpdate({ priority: val }) + return new Task(this.t).handleUpdate({ priority: val }); } handleSubmit() { - this.$submitButton.disabled = true - - this.storage.getTask().then(task => { - if (task.priority == this.$priority.value) return + this.$submitButton.disabled = true; + const priority = Number(this.$priority.value); - return this.updatePriority(this.$priority.value) - }).then(() => this.t.closePopup()) + this.storage + .getTask() + .then(task => task.priority !== priority && this.updatePriority(priority)) + .then(() => this.t.closePopup()); } } diff --git a/src/js/task.js b/src/js/task.js index 112888d..b3bd0c6 100644 --- a/src/js/task.js +++ b/src/js/task.js @@ -1,75 +1,81 @@ -import { TRELLO_ICON } from './constants' -import Storage from './storage' -import HabiticaApi from './habitica-api' +import { ICONS } from './constants'; +import Storage from './storage'; +import HabiticaApi from './habitica-api'; +import User from './user'; export default class Task { constructor( trello, storage = new Storage(trello), - API = new HabiticaApi(trello, storage) + API = new HabiticaApi(trello, storage), + user = new User(trello, storage) ) { - this.t = trello - this.storage = storage - this.API = API + this.t = trello; + this.storage = storage; + this.API = API; + this.currentUser = user; } async getTemplate(card) { - let cardUrl = `https://trello.com/c/${card.shortLink}` - let settings = await this.storage.getSettings() - let icon = settings.prependIcon ? `![](${TRELLO_ICON}) ` : '' + const cardUrl = `https://trello.com/c/${card.shortLink}`; + const settings = await this.storage.getSettings(); + const icon = settings.prependIcon ? `![](${ICONS.TRELLO_LOGO}) ` : ''; return { type: 'todo', priority: settings.priority, text: `### ${icon}${card.name}`, notes: `[Open in Trello](${cardUrl})` - } + }; } async handleAdd() { - let card = await this.t.card('name', 'shortLink') - let params = await this.getTemplate(card) + const card = await this.t.card('name', 'shortLink'); + const params = await this.getTemplate(card); - return this.API.addTask(params) - .then(res => ( - this.storage.setTask({ - id: res.data.id, - text: res.data.text, - notes: res.data.notes, - priority: res.data.priority - }) - )) - } + return this.API.addTask(params).then(res => + this.storage.setTask({ + id: res.data.id, + text: res.data.text, + notes: res.data.notes, + priority: res.data.priority + }) + ); + } async handleRemove() { - let task = await this.storage.getTask() + const task = await this.storage.getTask(); - return this.API.removeTask(task.id) - .then(_ => this.storage.removeTask()) + return this.API.removeTask(task.id).then(() => this.storage.removeTask()); } async handleDo() { - let task = await this.storage.getTask() + const task = await this.storage.getTask(); - return this.API.doTask(task.id) - .then(_ => this.storage.setTask({ done: true })) + return this.API.doTask(task.id).then(res => + this.storage + .setTask({ done: true }) + .then(() => this.currentUser.updateStats(res.data)) + ); } async handleUndo() { - let task = await this.storage.getTask() + const task = await this.storage.getTask(); - return this.API.undoTask(task.id) - .then(_ => this.storage.setTask({ done: false })) + return this.API.undoTask(task.id).then(res => + this.storage + .setTask({ done: false }) + .then(() => this.currentUser.updateStats(res.data)) + ); } - async handleUpdate(params) { - let task = await this.storage.getTask() + async handleUpdate({ priority }) { + const task = await this.storage.getTask(); - return this.API.updateTask(task.id, params) - .then(res => ( - this.storage.setTask({ - priority: res.data.priority - }) - )) + return this.API.updateTask(task.id, { priority }).then(res => + this.storage.setTask({ + priority: res.data.priority + }) + ); } } diff --git a/src/js/user.js b/src/js/user.js new file mode 100644 index 0000000..aa33d7b --- /dev/null +++ b/src/js/user.js @@ -0,0 +1,48 @@ +import Storage from './storage'; + +export default class Task { + constructor(trello, storage = new Storage(trello)) { + this.t = trello; + this.storage = storage; + } + + // In some cases user stats response is missing the toNextLevel field + // so it's better to calculate it manually with this formula: + // https://habitica.fandom.com/wiki/Experience_Level_Chart + async calculateExpToNextLevel(lvl) { + const currentUser = await this.storage.getUser(); + + if (currentUser.lvl === lvl && currentUser.expToNextLevel) + return currentUser.expToNextLevel; + + const exp = 0.25 * lvl ** 2 + 10 * lvl + 139.75; + const remainder = exp % 10; + return exp - remainder + Math.round(remainder / 10) * 10; // rounded to the closest 10 + } + + async updateStats({ gp, exp, lvl }) { + const gold = gp; + + this.notifyAboutStats({ gold, exp }); + const expToNextLevel = await this.calculateExpToNextLevel(lvl); + return this.storage.setUser({ lvl, gold, exp, expToNextLevel }); + } + + async notifyAboutStats({ exp, gold }) { + const settings = await this.storage.getSettings(); + if (!settings.showStatsNotifications) return undefined; + + const currentUser = await this.storage.getUser(); + const expDiff = currentUser.exp ? exp - currentUser.exp : exp; + const goldDiff = currentUser.gold ? gold - currentUser.gold : gold; + + const outcome = goldDiff > 0 ? 'gained' : 'lost'; + const display = goldDiff > 0 ? 'success' : 'error'; + + return this.t.alert({ + message: `You ${outcome} ${expDiff} exp and ${goldDiff.toFixed(2)} gold.`, + display, + duration: 5 // min is 5 + }); + } +} diff --git a/src/settings.html b/src/settings.html index 6431a54..6d14bef 100644 --- a/src/settings.html +++ b/src/settings.html @@ -3,33 +3,54 @@ + -

Preferences

- - What cards to sync: + What cards to sync:
- Default difficulty for todos: + Default difficulty for to-dos:
+ + + + + + + + - Prepend trello icon to to-dos:
-   Yes
-   No

-
- +
+
+   + +
\ No newline at end of file diff --git a/test/habitica-api.test.js b/test/habitica-api.test.js index a138567..96de30e 100644 --- a/test/habitica-api.test.js +++ b/test/habitica-api.test.js @@ -1,402 +1,432 @@ -import Storage from '../src/js/storage' -import HabiticaApi from '../src/js/habitica-api' -import fetchMock from 'fetch-mock' +import fetchMock from 'fetch-mock'; +import HabiticaApi from '../src/js/habitica-api'; +import { BASE_URL } from '../src/js/constants'; describe('HabiticaApi class', () => { - describe('constructor', () => { - let t = {}, storage = {}, API + const t = {}; + const storage = {}; + let API; beforeEach(() => { - API = new HabiticaApi(t, storage) - }) + API = new HabiticaApi(t, storage); + }); it('assigns passed trello instance to local t variable', () => { - expect(API.t).toBeDefined() - expect(API.t).toBe(t) - }) + expect(API.t).toBeDefined(); + expect(API.t).toBe(t); + }); it('assigns passed storage to local storage variable', () => { - expect(API.storage).toBeDefined() - expect(API.storage).toBe(storage) - }) - }) + expect(API.storage).toBeDefined(); + expect(API.storage).toBe(storage); + }); + }); describe('.request()', () => { - let t = {} , storage = {}, API, params, url, response, emptyMock - + const t = {}; + const storage = {}; + let API; + let params; + let path; + let url; + let response; + let emptyMock; + beforeAll(() => { - params = { foo: 'bar' } - url = "https://fake.com" - response = { status: 200, body: 'it works' } - emptyMock = jest.fn(async () => ({}) ) - }) + params = { foo: 'bar' }; + path = '/user'; + url = BASE_URL + path; + response = { status: 200, body: 'it works' }; + emptyMock = jest.fn(async () => ({})); + }); beforeEach(() => { - API = new HabiticaApi(t, storage) - API.authHeaders = emptyMock - API.handleResponse = emptyMock - fetchMock.mock(url, response) - }) + API = new HabiticaApi(t, storage); + API.authHeaders = emptyMock; + API.handleResponse = emptyMock; + fetchMock.mock(url, response); + }); afterEach(() => { - fetchMock.restore() - }) - + fetchMock.restore(); + }); + it('loads auth headers', async () => { - await API.request(url, params) - expect(API.authHeaders).toBeCalled() - }) + await API.request(path, params); + expect(API.authHeaders).toBeCalled(); + }); it('fetches url', async () => { - await API.request(url, params) - expect(fetchMock.lastUrl()).toEqual( - expect.stringContaining(url) - ) - }) - - it('fetches url with auth headers', async ()=> { - await API.request(url, params) + await API.request(path, params); + expect(fetchMock.lastUrl()).toEqual(expect.stringContaining(path)); + }); + + it('fetches url with auth headers', async () => { + await API.request(path, params); expect(fetchMock.lastOptions()).toEqual( expect.objectContaining({ headers: expect.any(Object) }) - ) - }) + ); + }); - it('fetches url with passed params', async ()=> { - await API.request(url, params) - expect(fetchMock.lastOptions()).toEqual( - expect.objectContaining(params) - ) - }) + it('fetches url with passed params', async () => { + await API.request(path, params); + expect(fetchMock.lastOptions()).toEqual(expect.objectContaining(params)); + }); it('handles response', async () => { - await API.request(url, params) + await API.request(path, params); expect(API.handleResponse).toBeCalledWith( expect.objectContaining(response) - ) - }) - }) + ); + }); + }); describe('.handleResponse()', () => { - let t = {}, storage = {}, API, res = {} + const t = {}; + const storage = {}; + const res = {}; + let API; describe('when request is succeeded', () => { beforeAll(() => { - res.ok = true - res.json = jest.fn() - }) + res.ok = true; + res.json = jest.fn(); + }); - beforeEach(() => { - API = new HabiticaApi(t, storage) - }) + beforeEach(() => { + API = new HabiticaApi(t, storage); + }); it('returns json response', () => { - API.handleResponse(res) - expect(res.json).toBeCalled() - }) - }) + API.handleResponse(res); + expect(res.json).toBeCalled(); + }); + }); describe('when request is failed', () => { beforeAll(() => { - res.ok = false - }) + res.ok = false; + }); - beforeEach(() => { - API = new HabiticaApi(t, storage) - API.handleError = jest.fn() - }) + beforeEach(() => { + API = new HabiticaApi(t, storage); + API.handleError = jest.fn(); + }); it('handles the error', () => { - API.handleResponse(res) - expect(API.handleError).toBeCalledWith(res) - }) - }) - }) + API.handleResponse(res); + expect(API.handleError).toBeCalledWith(res); + }); + }); + }); describe('.handleError()', () => { - let t = {}, storage, API, error = {} - + const t = {}; + const storage = {}; + const error = {}; + let API; + beforeAll(() => { - storage = { - removeUser: jest.fn(async () => {}), - removeTask: jest.fn(async () => {}) - } - }) + storage.removeUser = jest.fn(async () => {}); + storage.removeTask = jest.fn(async () => {}); + }); beforeEach(() => { - API = new HabiticaApi(t, storage) - API.notify = jest.fn() - }) + API = new HabiticaApi(t, storage); + API.notify = jest.fn(); + }); describe('when 401 Unauthorized', () => { beforeAll(() => { - error.status = 401 - }) + error.status = 401; + }); it('removes user data from the storage', async () => { - await API.handleError(error) - expect(API.storage.removeUser).toBeCalled() - }) + await API.handleError(error); + expect(API.storage.removeUser).toBeCalled(); + }); it('notifies user about wrong credentials', async () => { - await API.handleError(error) + await API.handleError(error); expect(API.notify).toBeCalledWith( expect.stringContaining('User ID or API Token'), 'error' - ) - }) - }) + ); + }); + }); describe('when 404 Not Found', () => { beforeAll(() => { - error.status = 404 - }) + error.status = 404; + }); it('removes task data from the storage', async () => { - await API.handleError(error) - expect(API.storage.removeTask).toBeCalled() - }) - }) - }) + await API.handleError(error); + expect(API.storage.removeTask).toBeCalled(); + }); + }); + }); describe('.authHeaders()', () => { - let t, storage = {}, API, secrets + const t = {}; + const storage = {}; + let API; + let secrets; beforeAll(() => { secrets = { userId: 'qwer', apiToken: 'asdf' - } - t = { - loadSecret: jest.fn(async (key) => secrets[key]) - } - }) + }; + t.loadSecret = jest.fn(async key => secrets[key]); + }); beforeEach(() => { - API = new HabiticaApi(t, storage) - }) + API = new HabiticaApi(t, storage); + }); it('loads user id from local storage', async () => { - await API.authHeaders() - expect(API.t.loadSecret).toBeCalledWith('userId') - }) + await API.authHeaders(); + expect(API.t.loadSecret).toBeCalledWith('userId'); + }); it('loads api token from local storage', async () => { - await API.authHeaders() - expect(API.t.loadSecret).toBeCalledWith('apiToken') - }) + await API.authHeaders(); + expect(API.t.loadSecret).toBeCalledWith('apiToken'); + }); it('returns proper headers', async () => { - let headers = await API.authHeaders() - expect(headers).toEqual(expect.objectContaining({ - 'x-api-user': secrets.userId, - 'x-api-key': secrets.apiToken - })) - }) - }) + const headers = await API.authHeaders(); + expect(headers).toEqual( + expect.objectContaining({ + 'x-api-user': secrets.userId, + 'x-api-key': secrets.apiToken + }) + ); + }); + }); describe('.addTask()', () => { - let t = {}, storage = {}, API, params + const t = {}; + const storage = {}; + let API; + let params; beforeAll(() => { - params = { type: 'todo', text: 'Todo text', priority: 1 } - }) + params = { type: 'todo', text: 'Todo text', priority: 1 }; + }); beforeEach(() => { - API = new HabiticaApi(t, storage) - API.request = jest.fn() - }) + API = new HabiticaApi(t, storage); + API.request = jest.fn(); + }); it('starts request to proper url', () => { - API.addTask(params) + API.addTask(params); expect(API.request).toBeCalledWith( expect.stringContaining(`/tasks/user`), expect.anything() - ) - }) + ); + }); it('starts request with proper method type', () => { - API.addTask(params) + API.addTask(params); expect(API.request).toBeCalledWith( expect.anything(), - expect.objectContaining({ - method: 'POST', + expect.objectContaining({ + method: 'POST' }) - ) - }) + ); + }); it('starts request with proper body', () => { - API.addTask(params) + API.addTask(params); expect(API.request).toBeCalledWith( expect.anything(), - expect.objectContaining({ + expect.objectContaining({ body: JSON.stringify(params) }) - ) - }) - }) + ); + }); + }); describe('.updateTask()', () => { - let t = {}, storage = {}, API, id, params + const t = {}; + const storage = {}; + let API; + let id; + let params; beforeAll(() => { - id = 123 - params = { priority: 2 } - }) + id = 123; + params = { priority: 2 }; + }); beforeEach(() => { - API = new HabiticaApi(t, storage) - API.request = jest.fn() - }) + API = new HabiticaApi(t, storage); + API.request = jest.fn(); + }); it('starts request to proper url', () => { - API.updateTask(id, params) + API.updateTask(id, params); expect(API.request).toBeCalledWith( expect.stringContaining(`/tasks/${id}`), expect.anything() - ) - }) + ); + }); it('starts request with proper method type', () => { - API.updateTask(id, params) + API.updateTask(id, params); expect(API.request).toBeCalledWith( expect.anything(), - expect.objectContaining({ - method: 'PUT', + expect.objectContaining({ + method: 'PUT' }) - ) - }) + ); + }); it('starts request with proper body', () => { - API.updateTask(id, params) + API.updateTask(id, params); expect(API.request).toBeCalledWith( expect.anything(), - expect.objectContaining({ + expect.objectContaining({ body: JSON.stringify(params) }) - ) - }) - }) + ); + }); + }); describe('.doTask()', () => { - let t = {}, storage = {}, API, id + const t = {}; + const storage = {}; + let API; + let id; beforeAll(() => { - id = 123 - }) + id = 123; + }); beforeEach(() => { - API = new HabiticaApi(t, storage) - API.request = jest.fn() - }) + API = new HabiticaApi(t, storage); + API.request = jest.fn(); + }); it('starts request to proper url', () => { - API.doTask(id) + API.doTask(id); expect(API.request).toBeCalledWith( expect.stringContaining(`/tasks/${id}/score/up`), expect.anything() - ) - }) + ); + }); it('starts request with proper method type', () => { - API.doTask(id) + API.doTask(id); expect(API.request).toBeCalledWith( expect.anything(), - expect.objectContaining({ - method: 'POST' + expect.objectContaining({ + method: 'POST' }) - ) - }) - }) + ); + }); + }); describe('.undoTask()', () => { - let t = {}, storage = {}, API, id + const t = {}; + const storage = {}; + let API; + let id; beforeAll(() => { - id = 123 - }) + id = 123; + }); beforeEach(() => { - API = new HabiticaApi(t, storage) - API.request = jest.fn() - }) + API = new HabiticaApi(t, storage); + API.request = jest.fn(); + }); it('starts request to proper url', () => { - API.undoTask(id) + API.undoTask(id); expect(API.request).toBeCalledWith( expect.stringContaining(`/tasks/${id}/score/down`), expect.anything() - ) - }) + ); + }); it('starts request with proper method type', () => { - API.undoTask(id) + API.undoTask(id); expect(API.request).toBeCalledWith( expect.anything(), - expect.objectContaining({ - method: 'POST' + expect.objectContaining({ + method: 'POST' }) - ) - }) - }) + ); + }); + }); describe('.removeTask()', () => { - let t = {}, storage = {}, API, id + const t = {}; + const storage = {}; + let API; + let id; beforeAll(() => { - id = 123 - }) + id = 123; + }); beforeEach(() => { - API = new HabiticaApi(t, storage) - API.request = jest.fn() - }) + API = new HabiticaApi(t, storage); + API.request = jest.fn(); + }); it('starts request to proper url', () => { - API.removeTask(id) + API.removeTask(id); expect(API.request).toBeCalledWith( expect.stringContaining(`/tasks/${id}`), expect.anything() - ) - }) + ); + }); it('starts request with proper method type', () => { - API.removeTask(id) + API.removeTask(id); expect(API.request).toBeCalledWith( expect.anything(), - expect.objectContaining({ - method: 'DELETE' + expect.objectContaining({ + method: 'DELETE' }) - ) - }) - }) + ); + }); + }); describe('.getUserProfile()', () => { - let t = {}, storage = {}, API + const t = {}; + const storage = {}; + let API; beforeEach(() => { - API = new HabiticaApi(t, storage) - API.request = jest.fn() - }) + API = new HabiticaApi(t, storage); + API.request = jest.fn(); + }); it('starts request to proper url', () => { - API.getUserProfile() + API.getUserProfile(); expect(API.request).toBeCalledWith( expect.stringContaining('/user?userFields='), expect.anything() - ) - }) + ); + }); it('starts request with proper method type', () => { - API.getUserProfile() + API.getUserProfile(); expect(API.request).toBeCalledWith( expect.anything(), - expect.objectContaining({ - method: 'GET' + expect.objectContaining({ + method: 'GET' }) - ) - }) - }) -}) + ); + }); + }); +}); diff --git a/test/list.test.js b/test/list.test.js index b354be7..f9baaa7 100644 --- a/test/list.test.js +++ b/test/list.test.js @@ -1,171 +1,177 @@ -import Storage from '../src/js/storage' -import List from '../src/js/list' -import { LIST_TYPES } from '../src/js/constants' - -const minimalT = { - list: jest.fn(() => ({ id: 123, name: 'List name' }) ), - closePopup: jest.fn(), - alert: jest.fn() -} +import List from '../src/js/list'; +import { LIST_TYPES } from '../src/js/constants'; describe('List class', () => { describe('constructor', () => { - let t = {}, storage = {}, list + const t = {}; + const storage = {}; + let list; beforeEach(() => { - list = new List(t, storage) - }) + list = new List(t, storage); + }); it('assigns passed trello instance to local t variable', () => { - expect(list.t).toBeDefined() - expect(list.t).toBe(t) - }) + expect(list.t).toBeDefined(); + expect(list.t).toBe(t); + }); it('assigns passed storage to local storage variable', () => { - expect(list.storage).toBeDefined() - expect(list.storage).toBe(storage) - }) - }) + expect(list.storage).toBeDefined(); + expect(list.storage).toBe(storage); + }); + }); describe('.markAsDone()', () => { - let t = {}, storage = {}, list + const t = {}; + const storage = {}; + let list; beforeEach(() => { - list = new List(t, storage) - }) + list = new List(t, storage); + }); it('calls .mark() with proper args', () => { - expect(list.mark).toBeDefined() - list.mark = jest.fn() - list.markAsDone() - expect(list.mark).toBeCalledWith(LIST_TYPES.DONE) - }) - }) + expect(list.mark).toBeDefined(); + list.mark = jest.fn(); + list.markAsDone(); + expect(list.mark).toBeCalledWith(LIST_TYPES.DONE); + }); + }); describe('.markAsDoing()', () => { - let t = {}, storage = {}, list + const t = {}; + const storage = {}; + let list; beforeEach(() => { - list = new List(t, storage) - }) + list = new List(t, storage); + }); it('calls .mark() with proper args', () => { - expect(list.mark).toBeDefined() - list.mark = jest.fn() - list.markAsDoing() - expect(list.mark).toBeCalledWith(LIST_TYPES.DOING) - }) - }) + expect(list.mark).toBeDefined(); + list.mark = jest.fn(); + list.markAsDoing(); + expect(list.mark).toBeCalledWith(LIST_TYPES.DOING); + }); + }); describe('.mark()', () => { - let t, storage, list, lists = {}, listData, listType + const t = {}; + const storage = {}; + const lists = {}; + let list; + let listData; + let listType; beforeAll(() => { - listData = { id: 456 } - t = { - closePopup: jest.fn(), - alert: jest.fn(), - list: jest.fn(() => listData) - } - listType = LIST_TYPES.DOING - storage = { - getLists: jest.fn(async () => lists ), - setLists: jest.fn(async () => ({}) ) - } - }) + listType = LIST_TYPES.DOING; + listData = { id: 456 }; + + t.closePopup = jest.fn(); + t.alert = jest.fn(); + t.list = jest.fn(() => listData); + + storage.getLists = jest.fn(async () => lists); + storage.setLists = jest.fn(async () => ({})); + }); beforeEach(() => { - list = new List(t, storage) - }) + list = new List(t, storage); + }); it('gets lists from the storage', async () => { - await list.mark(listType) - expect(list.storage.getLists).toBeCalledWith() - }) + await list.mark(listType); + expect(list.storage.getLists).toBeCalledWith(); + }); it('gets list data', async () => { - await list.mark(listType) - expect(list.t.list).toBeCalled() - }) + await list.mark(listType); + expect(list.t.list).toBeCalled(); + }); it('updates current list type', async () => { - await list.mark(listType) - expect(list.storage.setLists).toBeCalledWith({ [listData.id]: listType }) - }) + await list.mark(listType); + expect(list.storage.setLists).toBeCalledWith({ [listData.id]: listType }); + }); it('closes popup', async () => { - await list.mark(listType) - expect(list.t.closePopup).toBeCalledWith() - }) + await list.mark(listType); + expect(list.t.closePopup).toBeCalledWith(); + }); it('notifies', async () => { - expect(list.notify).toBeDefined() - list.notify = jest.fn() - await list.mark(listType) - expect(list.notify).toBeCalled() - }) + expect(list.notify).toBeDefined(); + list.notify = jest.fn(); + await list.mark(listType); + expect(list.notify).toBeCalled(); + }); it('saves updated lists to storage', async () => { - await list.mark(listType) - expect(list.storage.setLists).toBeCalledWith(Object.assign({}, lists, { - [listData.id]: listType - })) - }) - }) + await list.mark(listType); + expect(list.storage.setLists).toBeCalledWith( + Object.assign({}, lists, { + [listData.id]: listType + }) + ); + }); + }); describe('.unmark()', () => { - let t, storage, list, listData, lists + const t = {}; + const storage = {}; + let list; + let listData; + let lists; beforeAll(() => { - listData = { id: 37, name: 'List name' } - lists = { [listData.id]: 'done' } - t = { - list: jest.fn(() => listData), - closePopup: jest.fn(), - alert: jest.fn() - } - storage = { - getLists: jest.fn(async () => lists), - setLists: jest.fn(async () => ({}) ) - } - }) + listData = { id: 37, name: 'List name' }; + lists = { [listData.id]: 'done' }; + + t.list = jest.fn(() => listData); + t.closePopup = jest.fn(); + t.alert = jest.fn(); + + storage.getLists = jest.fn(async () => lists); + storage.setLists = jest.fn(async () => ({})); + }); beforeEach(() => { - list = new List(t, storage) - }) + list = new List(t, storage); + }); it('gets lists from the storage', async () => { - await list.unmark() - expect(list.storage.getLists).toBeCalled() - }) + await list.unmark(); + expect(list.storage.getLists).toBeCalled(); + }); it('gets list data', async () => { - await list.unmark() - expect(list.t.list).toBeCalled() - }) + await list.unmark(); + expect(list.t.list).toBeCalled(); + }); it('deletes current list from lists', async () => { - await list.unmark() - expect(list.storage.setLists).toBeCalledWith({}) - }) + await list.unmark(); + expect(list.storage.setLists).toBeCalledWith({}); + }); it('closes popup', async () => { - await list.unmark() - expect(list.t.closePopup).toBeCalled() - }) + await list.unmark(); + expect(list.t.closePopup).toBeCalled(); + }); it('notifies', async () => { - expect(list.notify).toBeDefined() - list.notify = jest.fn() - await list.unmark() - expect(list.notify).toBeCalled() - }) + expect(list.notify).toBeDefined(); + list.notify = jest.fn(); + await list.unmark(); + expect(list.notify).toBeCalled(); + }); it('saves updated lists to storage', async () => { - await list.unmark() + await list.unmark(); expect(list.storage.setLists).toBeCalledWith( expect.not.objectContaining({ [listData.id]: expect.anything() }) - ) - }) - }) -}) + ); + }); + }); +}); diff --git a/test/settings-form.test.js b/test/settings-form.test.js index 8189851..3393241 100644 --- a/test/settings-form.test.js +++ b/test/settings-form.test.js @@ -1,208 +1,303 @@ -import SettingsForm from '../src/js/settings-form' +import SettingsForm from '../src/js/settings-form'; + +// eslint-disable-next-line func-names +const initializeElementsMock = function() { + this.$scope = {}; + this.$priority = {}; + this.$prependIcon = {}; + this.$showBadges = {}; + this.$showStats = {}; + this.$showStatsNotifications = {}; + this.$submitButton = { addEventListener: jest.fn() }; + this.$logoutButton = { addEventListener: jest.fn() }; +}; describe('SettingsForm class', () => { - describe('.initialize()', () => { - let t = {}, storage = {}, form, settings - - beforeAll(() => { - settings = { - scope: 'me', - priority: '1', - showBadges: true, - prependIcon: false - } - storage.getSettings = jest.fn(async () => settings ) - }) + const t = {}; + const storage = {}; + let form; beforeEach(() => { - form = new SettingsForm(t, storage) - form.initializeElements = jest.fn() - form.setScope = jest.fn() - form.setPriority = jest.fn() - form.setShowBadges = jest.fn() - form.setPrependIcon = jest.fn() - form.listenToSubmit = jest.fn() - form.listenToLogout = jest.fn() - }) - - it('gets settings from the storage', () => { - form.initialize() - expect(form.storage.getSettings).toBeCalledWith() - }) + form = new SettingsForm(t, storage); + form.initializeElements = jest.fn(); + form.assignValues = jest.fn(); + form.listenToSubmit = jest.fn(); + form.listenToLogout = jest.fn(); + }); - it('initializes dom elements', async () => { - await form.initialize() - expect(form.initializeElements).toBeCalledWith() - }) + it('defined', () => { + expect(form.initialize).toBeDefined(); + }); - it('sets scope value from the storage', async () => { - await form.initialize() - expect(form.setScope).toBeCalledWith(settings.scope) - }) - - it('sets priority value from the storage', async () => { - await form.initialize() - expect(form.setPriority).toBeCalledWith(settings.priority) - }) + it('initializes dom elements', async () => { + await form.initialize(); + expect(form.initializeElements).toBeCalledWith(); + }); - it('sets showBadges value from the storage', async () => { - await form.initialize() - expect(form.setShowBadges).toBeCalledWith(settings.showBadges) - }) - - it('sets prependIcon value from the storage', async () => { - await form.initialize() - expect(form.setPrependIcon).toBeCalledWith(settings.prependIcon) - }) + it('assigns values from the storage to dom elements', async () => { + await form.initialize(); + expect(form.assignValues).toBeCalledWith(); + }); it('listens to submit', async () => { - await form.initialize() - expect(form.listenToSubmit).toBeCalledWith() - }) + await form.initialize(); + expect(form.listenToSubmit).toBeCalledWith(); + }); it('listens to logout', async () => { - await form.initialize() - expect(form.listenToLogout).toBeCalledWith() - }) - }) + await form.initialize(); + expect(form.listenToLogout).toBeCalledWith(); + }); + }); - describe('.setScope()', () => { - let t = {}, storage = {}, form + describe('.initializeElements()', () => { + const t = {}; + const storage = {}; + let form; beforeEach(() => { - form = new SettingsForm(t, storage) - form.$scope = {} - }) + form = new SettingsForm(t, storage); + }); + + it('defined', () => { + expect(form.initializeElements).toBeDefined(); + }); + }); - it('assigns passed value to scope', () => { - let val = 'me' - form.setScope(val) - expect(form.$scope.value).toBe(val) - }) - }) + describe('.assignValues()', () => { + const t = {}; + const storage = {}; + let form; + let settings; - describe('.setPriority()', () => { - let t = {}, storage = {}, form + beforeAll(() => { + settings = { + scope: 'me', + priority: 1, + prependIcon: false, + showBadges: true, + showStats: true, + showStatsNotifications: true + }; + storage.getSettings = async () => settings; + }); beforeEach(() => { - form = new SettingsForm(t, storage) - form.$priority = {} - }) + form = new SettingsForm(t, storage); + form.initializeElements = initializeElementsMock; + }); + + it('defined', () => { + expect(form.assignValues).toBeDefined(); + }); + + it('sets scope value from the storage', async () => { + await form.initialize(); + expect(form.$scope.value).toBe(settings.scope); + }); + + it('sets priority value from the storage', async () => { + await form.initialize(); + expect(form.$priority.value).toBe(settings.priority); + }); + + it('sets prependIcon value from the storage', async () => { + await form.initialize(); + expect(form.$prependIcon.checked).toBe(settings.prependIcon); + }); - it('assigns passed value to priority', () => { - let val = '1' - form.setPriority(val) - expect(form.$priority.value).toBe(val) - }) - }) + it('sets showBadges value from the storage', async () => { + await form.initialize(); + expect(form.$showBadges.checked).toBe(settings.showBadges); + }); + + it('sets showStats value from the storage', async () => { + await form.initialize(); + expect(form.$showStats.checked).toBe(settings.showStats); + }); + + it('sets showStatsNotifications value from the storage', async () => { + await form.initialize(); + expect(form.$showStatsNotifications.checked).toBe( + settings.showStatsNotifications + ); + }); + }); describe('.listenToSubmit()', () => { - let t = {}, storage = {}, form + const t = {}; + const storage = {}; + let form; beforeEach(() => { - form = new SettingsForm(t, storage) - form.$submitButton = { addEventListener: jest.fn() } - }) + form = new SettingsForm(t, storage); + form.$submitButton = { addEventListener: jest.fn() }; + }); + + it('defined', () => { + expect(form.listenToSubmit).toBeDefined(); + }); it('adds on click listener to submit button', () => { - form.listenToSubmit() + form.listenToSubmit(); expect(form.$submitButton.addEventListener).toBeCalledWith( 'click', expect.any(Function) - ) - }) - }) + ); + }); + }); describe('.listenToLogout()', () => { - let t = {}, storage = {}, form + const t = {}; + const storage = {}; + let form; beforeEach(() => { - form = new SettingsForm(t, storage) - form.$logoutButton = { addEventListener: jest.fn() } - }) + form = new SettingsForm(t, storage); + form.$logoutButton = { addEventListener: jest.fn() }; + }); + + it('defined', () => { + expect(form.listenToLogout).toBeDefined(); + }); it('adds on click listener to logout button', () => { - form.listenToLogout() + form.listenToLogout(); expect(form.$logoutButton.addEventListener).toBeCalledWith( 'click', expect.any(Function) - ) - }) - }) + ); + }); + }); describe('.handleSubmit()', () => { - let t = {}, storage = {}, form + const t = {}; + const storage = {}; + let settings; + let form; beforeAll(() => { - t.closePopup = jest.fn() - storage.setSettings = jest.fn(async () => ({}) ) - }) + settings = { + scope: 'me', + priority: 1, + prependIcon: false, + showBadges: true, + showStats: true, + showStatsNotifications: false + }; + t.closePopup = jest.fn(); + storage.getSettings = async () => settings; + storage.setSettings = jest.fn(async () => ({})); + }); beforeEach(() => { - form = new SettingsForm(t, storage) - form.$submitButton = {} - form.$scope = { value: 'me' } - form.$priority = { value: '1' } - form.getShowBadges = jest.fn() - form.getPrependIcon = jest.fn() - }) + form = new SettingsForm(t, storage); + form.initializeElements = initializeElementsMock; + form.initialize(); + }); + + it('defined', () => { + expect(form.handleSubmit).toBeDefined(); + }); it('disables submit button', () => { - form.handleSubmit() - expect(form.$submitButton.disabled).toBe(true) - }) + form.handleSubmit(); + expect(form.$submitButton.disabled).toBe(true); + }); + + it('saves scope to the storage', () => { + form.handleSubmit(); + expect(form.storage.setSettings).toBeCalledWith( + expect.objectContaining({ + scope: settings.scope + }) + ); + }); - it('saves scope to storage', () => { - form.handleSubmit() + it('saves priority to the storage', () => { + form.handleSubmit(); expect(form.storage.setSettings).toBeCalledWith( expect.objectContaining({ - scope: form.$scope.value + priority: settings.priority }) - ) - }) + ); + }); - it('saves priority to storage', () => { - form.handleSubmit() + it('saves prependIcon to the storage', () => { + form.handleSubmit(); expect(form.storage.setSettings).toBeCalledWith( expect.objectContaining({ - priority: form.$priority.value + prependIcon: settings.prependIcon }) - ) - }) + ); + }); + + it('saves showBadges to the storage', () => { + form.handleSubmit(); + expect(form.storage.setSettings).toBeCalledWith( + expect.objectContaining({ + showBadges: settings.showBadges + }) + ); + }); + + it('saves showStats to the storage', () => { + form.handleSubmit(); + expect(form.storage.setSettings).toBeCalledWith( + expect.objectContaining({ + showStats: settings.showStats + }) + ); + }); + + it('saves showStatsNotifications to the storage', () => { + form.handleSubmit(); + expect(form.storage.setSettings).toBeCalledWith( + expect.objectContaining({ + showStatsNotifications: settings.showStatsNotifications + }) + ); + }); it('closes the popup', async () => { - await form.handleSubmit() - expect(form.t.closePopup).toBeCalledWith() - }) - }) + await form.handleSubmit(); + expect(form.t.closePopup).toBeCalledWith(); + }); + }); describe('.handleLogout()', () => { - let t = {}, storage = {}, form + const t = {}; + const storage = {}; + let form; beforeAll(() => { - t.closePopup = jest.fn() - storage.removeUser = jest.fn(async () => ({}) ) - }) + t.closePopup = jest.fn(); + storage.removeUser = jest.fn(async () => ({})); + }); beforeEach(() => { - form = new SettingsForm(t, storage) - form.$logoutButton = {} - }) + form = new SettingsForm(t, storage); + form.$logoutButton = {}; + }); + + it('defined', () => { + expect(form.handleLogout).toBeDefined(); + }); it('disables logout button', () => { - form.handleLogout() - expect(form.$logoutButton.disabled).toBe(true) - }) + form.handleLogout(); + expect(form.$logoutButton.disabled).toBe(true); + }); it('removes user data from the storage', () => { - form.handleLogout() - expect(form.storage.removeUser).toBeCalledWith() - }) + form.handleLogout(); + expect(form.storage.removeUser).toBeCalledWith(); + }); it('closes the popup', async () => { - await form.handleLogout() - expect(form.t.closePopup).toBeCalledWith() - }) - }) -}) + await form.handleLogout(); + expect(form.t.closePopup).toBeCalledWith(); + }); + }); +}); diff --git a/test/storage.test.js b/test/storage.test.js index c970b92..1046e12 100644 --- a/test/storage.test.js +++ b/test/storage.test.js @@ -1,321 +1,342 @@ -import Storage from '../src/js/storage' +import Storage from '../src/js/storage'; -// empty state of trello storage -let emptyStorage = { +// empty state of trello storage +const emptyStorage = { board: { shared: {}, private: {} }, card: { shared: {}, private: {} }, member: { shared: {}, private: {} }, organization: { shared: {}, private: {} } -} +}; describe('Storage class', () => { describe('constructor', () => { it('assigns passed trello instance to local t variable', () => { - let t = {} - let storage = new Storage(t) - expect(storage.t).toBeDefined() - expect(storage.t).toBe(t) - }) - }) + const t = {}; + const storage = new Storage(t); + expect(storage.t).toBeDefined(); + expect(storage.t).toBe(t); + }); + }); describe('.getAll()', () => { - let t, storage, storageData - + const t = {}; + let storage; + let storageData; + beforeAll(() => { - storageData = emptyStorage - t = { - getAll: jest.fn(async () => emptyStorage) - } - }) - + storageData = emptyStorage; + t.getAll = jest.fn(async () => emptyStorage); + }); + beforeEach(() => { - storage = new Storage(t) - }) - + storage = new Storage(t); + }); + it('calls function with proper args', async () => { - await storage.getAll() - expect(storage.t.getAll).toBeCalledWith() - }) - + await storage.getAll(); + expect(storage.t.getAll).toBeCalledWith(); + }); + it('returns all storage', async () => { - expect(await storage.getAll()).toBe(storageData) - }) - }) + expect(await storage.getAll()).toBe(storageData); + }); + }); describe('.removeAll()', () => { - let t, storage + const t = {}; + let storage; beforeAll(() => { - t = { - remove: jest.fn() - } - }) + t.remove = jest.fn(); + }); beforeEach(() => { - storage = new Storage(t) - }) + storage = new Storage(t); + }); it('removes the lists', async () => { - await storage.removeAll() - expect(storage.t.remove).toBeCalledWith('board', 'private', 'lists') - }) + await storage.removeAll(); + expect(storage.t.remove).toBeCalledWith('board', 'private', 'lists'); + }); it('removes the user', async () => { - await storage.removeAll() - expect(storage.t.remove).toBeCalledWith('board', 'private', 'user') - }) + await storage.removeAll(); + expect(storage.t.remove).toBeCalledWith('board', 'private', 'user'); + }); it('removes the settings', async () => { - await storage.removeAll() - expect(storage.t.remove).toBeCalledWith('board', 'private', 'settings') - }) - }) + await storage.removeAll(); + expect(storage.t.remove).toBeCalledWith('board', 'private', 'settings'); + }); + }); describe('.setUser()', () => { - let t, storage, userData, params - + const t = {}; + let storage; + let userData; + let params; + beforeAll(() => { - userData = { loggedIn: false } - params = { name: 'Alex', loggedIn: true } - t = { - set: jest.fn(), - get: jest.fn(async () => userData) - } - }) - + userData = { loggedIn: false }; + params = { name: 'Alex', loggedIn: true }; + t.set = jest.fn(); + t.get = jest.fn(async () => userData); + }); + beforeEach(() => { - storage = new Storage(t) - }) + storage = new Storage(t); + }); it('gets current value with proper args', async () => { - await storage.setUser(params) - expect(storage.t.get).toBeCalledWith('board', 'private', 'user', {}) - }) + await storage.setUser(params); + expect(storage.t.get).toBeCalledWith('board', 'private', 'user', {}); + }); it('sets a new value with proper args', async () => { - await storage.setUser(params) - expect(storage.t.set).toBeCalledWith('board', 'private', 'user', Object.assign({}, userData, params)) - }) - }) + await storage.setUser(params); + expect(storage.t.set).toBeCalledWith( + 'board', + 'private', + 'user', + Object.assign({}, userData, params) + ); + }); + }); describe('.getUser()', () => { - let t, storage, userData - + const t = {}; + let storage; + let userData; + beforeAll(() => { - userData = { name: 'Alex', loggedIn: true } - t = { - get: jest.fn(() => userData) - } - }) - + userData = { name: 'Alex', loggedIn: true }; + t.get = jest.fn(() => userData); + }); + beforeEach(() => { - storage = new Storage(t) - }) - + storage = new Storage(t); + }); + it('calls function with proper args', async () => { - await storage.getUser() - expect(storage.t.get).toBeCalledWith('board', 'private', 'user', {}) - }) - + await storage.getUser(); + expect(storage.t.get).toBeCalledWith('board', 'private', 'user', {}); + }); + it('returns user data', async () => { - expect(await storage.getUser()).toBe(userData) - }) - }) - + expect(await storage.getUser()).toBe(userData); + }); + }); + describe('.removeUser()', () => { - let t, storage - + const t = {}; + let storage; + beforeAll(() => { - t = { - remove: jest.fn(async () => ({}) ) - } - }) - + t.remove = jest.fn(async () => ({})); + }); + beforeEach(() => { - storage = new Storage(t) - }) - + storage = new Storage(t); + }); + it('calls function with proper args', async () => { - await storage.removeUser() - expect(storage.t.remove).toBeCalledWith('board', 'private', 'user') - }) - }) + await storage.removeUser(); + expect(storage.t.remove).toBeCalledWith('board', 'private', 'user'); + }); + }); describe('.setTask()', () => { - let t, storage, taskData, params - + const t = {}; + let storage; + let taskData; + let params; + beforeAll(() => { - taskData = { id: 123, done: false } - params = { done: true } - t = { - set: jest.fn(), - get: jest.fn(async () => taskData) - } - }) - + taskData = { id: 123, done: false }; + params = { done: true }; + + t.set = jest.fn(); + t.get = jest.fn(async () => taskData); + }); + beforeEach(() => { - storage = new Storage(t) - }) + storage = new Storage(t); + }); it('gets current value with proper args', async () => { - await storage.setTask(params) - expect(storage.t.get).toBeCalledWith('card', 'private', 'task', {}) - }) + await storage.setTask(params); + expect(storage.t.get).toBeCalledWith('card', 'private', 'task', {}); + }); it('sets a new value with proper args', async () => { - await storage.setTask(params) - expect(storage.t.set).toBeCalledWith('card', 'private', 'task', Object.assign({}, taskData, params)) - }) - }) + await storage.setTask(params); + expect(storage.t.set).toBeCalledWith( + 'card', + 'private', + 'task', + Object.assign({}, taskData, params) + ); + }); + }); describe('.getTask()', () => { - let t, storage, taskData - + const t = {}; + let storage; + let taskData; + beforeAll(() => { - taskData = { + taskData = { id: 123, - done: false, - priority: 1.5, + done: false, + priority: 1.5, text: 'To-do text', notes: 'To-do description' - } - t = { - get: jest.fn(async () => taskData) - } - }) - + }; + t.get = jest.fn(async () => taskData); + }); + beforeEach(() => { - storage = new Storage(t) - }) - + storage = new Storage(t); + }); + it('calls function with proper args', async () => { - await storage.getTask() - expect(storage.t.get).toBeCalledWith('card', 'private', 'task', {}) - }) - + await storage.getTask(); + expect(storage.t.get).toBeCalledWith('card', 'private', 'task', {}); + }); + it('returns task data', async () => { - expect(await storage.getTask()).toBe(taskData) - }) - }) + expect(await storage.getTask()).toBe(taskData); + }); + }); describe('.removeTask()', () => { - let t, storage - + const t = {}; + let storage; + beforeAll(() => { - t = { - remove: jest.fn(async () => ({}) ) - } - }) - + t.remove = jest.fn(async () => ({})); + }); + beforeEach(() => { - storage = new Storage(t) - }) - + storage = new Storage(t); + }); + it('calls function with proper args', async () => { - await storage.removeTask() - expect(storage.t.remove).toBeCalledWith('card', 'private', 'task') - }) - }) + await storage.removeTask(); + expect(storage.t.remove).toBeCalledWith('card', 'private', 'task'); + }); + }); describe('.setSettings()', () => { - let t, storage, settingsData, params - + const t = {}; + let storage; + let settingsData; + let params; + beforeAll(() => { - settingsData = { scope: 'me', priority: '1' } - params = { scope: 'all' } - t = { - set: jest.fn(), - get: jest.fn(async () => settingsData) - } - }) - + settingsData = { scope: 'me', priority: '1' }; + params = { scope: 'all' }; + t.set = jest.fn(); + t.get = jest.fn(async () => settingsData); + }); + beforeEach(() => { - storage = new Storage(t) - }) + storage = new Storage(t); + }); it('gets current value with proper args', async () => { - await storage.setSettings(params) - expect(storage.t.get).toBeCalledWith('board', 'private', 'settings', {}) - }) + await storage.setSettings(params); + expect(storage.t.get).toBeCalledWith('board', 'private', 'settings', {}); + }); it('sets a new value', async () => { - await storage.setSettings(params) - expect(storage.t.set).toBeCalled() - }) + await storage.setSettings(params); + expect(storage.t.set).toBeCalled(); + }); it('sets a new value with proper args', async () => { - await storage.setSettings(params) - expect(storage.t.set).toBeCalledWith('board', 'private', 'settings', Object.assign({}, settingsData, params)) - }) - }) + await storage.setSettings(params); + expect(storage.t.set).toBeCalledWith( + 'board', + 'private', + 'settings', + Object.assign({}, settingsData, params) + ); + }); + }); describe('.getSettings()', () => { - let t, storage, settingsData, defaultSettings + const t = {}; + let storage; + let settingsData; beforeAll(() => { - settingsData = { scope: 'me', priority: '1', showBadges: true, prependIcon: false } - defaultSettings = settingsData - t = { - get: jest.fn(async () => settingsData) - } - }) + settingsData = { + scope: 'me', + priority: 1, + prependIcon: false, + showBadges: true, + showStats: true, + showStatsNotifications: true + }; + t.get = jest.fn(async () => settingsData); + }); beforeEach(() => { - storage = new Storage(t) - }) - + storage = new Storage(t); + }); + it('calls function with proper args', async () => { - await storage.getSettings() - expect(storage.t.get).toBeCalledWith('board', 'private', 'settings', {}) - }) - + await storage.getSettings(); + expect(storage.t.get).toBeCalledWith('board', 'private', 'settings', {}); + }); + it('returns settings data', async () => { - expect(await storage.getSettings()).toEqual(settingsData) - }) - }) + expect(await storage.getSettings()).toEqual(settingsData); + }); + }); describe('.setLists()', () => { - let t, storage, params - + const t = {}; + let storage; + let params; + beforeAll(() => { - params = { 123: 'done' } - t = { - set: jest.fn(async () => ({}) ) - } - }) - + params = { 123: 'done' }; + t.set = jest.fn(async () => ({})); + }); + beforeEach(() => { - storage = new Storage(t) - }) + storage = new Storage(t); + }); it('sets a new value with proper args', async () => { - await storage.setLists(params) - expect(storage.t.set).toBeCalledWith('board', 'private', 'lists', params) - }) - }) + await storage.setLists(params); + expect(storage.t.set).toBeCalledWith('board', 'private', 'lists', params); + }); + }); describe('.getLists()', () => { - let t, storage, listData - + const t = {}; + let storage; + let listData; + beforeAll(() => { - listData = { 123: 'done', 456: 'doing' } - t = { - get: jest.fn(async () => listData) - } - }) - + listData = { 123: 'done', 456: 'doing' }; + t.get = jest.fn(async () => listData); + }); + beforeEach(() => { - storage = new Storage(t) - }) - + storage = new Storage(t); + }); + it('calls function with proper args', async () => { - await storage.getLists() - expect(storage.t.get).toBeCalledWith('board', 'private', 'lists', {}) - }) - - it('returns lists data', async () => { - expect(await storage.getLists()).toBe(listData) - }) - }) + await storage.getLists(); + expect(storage.t.get).toBeCalledWith('board', 'private', 'lists', {}); + }); -}) + it('returns lists data', async () => { + expect(await storage.getLists()).toBe(listData); + }); + }); +}); diff --git a/test/sync.test.js b/test/sync.test.js index 215a01a..d537167 100644 --- a/test/sync.test.js +++ b/test/sync.test.js @@ -1,343 +1,358 @@ -import Storage from '../src/js/storage' -import Sync from '../src/js/sync' -import { LIST_TYPES } from '../src/js/constants' +import Sync from '../src/js/sync'; +import { LIST_TYPES } from '../src/js/constants'; -let taskMock = { +const taskMock = { handleAdd: jest.fn(async () => {}), handleDo: jest.fn(), handleUndo: jest.fn(async () => {}), handleRemove: jest.fn() -} -let getTaskMock = jest.fn(() => taskMock) +}; +const currentTaskMock = jest.fn(() => taskMock); describe('Sync class', () => { describe('constructor', () => { - let t = {}, storage = {}, sync + const t = {}; + const storage = {}; + let sync; beforeEach(() => { - sync = new Sync(t, storage) - }) + sync = new Sync(t, storage); + }); it('assigns passed trello instance to local t variable', () => { - expect(sync.t).toBeDefined() - expect(sync.t).toBe(t) - }) + expect(sync.t).toBeDefined(); + expect(sync.t).toBe(t); + }); it('assigns passed storage to local storage variable', () => { - expect(sync.storage).toBeDefined() - expect(sync.storage).toBe(storage) - }) - }) + expect(sync.storage).toBeDefined(); + expect(sync.storage).toBe(storage); + }); + }); describe('.start()', () => { describe('preparations', () => { - let t, storage, sync, settings, cardData, context + const t = {}; + const storage = {}; + let sync; + let settings; + let cardData; + let context; beforeAll(() => { - cardData = { members: [] } - settings = { scope: 'all' } - context = { member: 123 } - t = { - card: jest.fn(async () => cardData), - getContext: jest.fn(async () => context) - } - storage = { - getTask: jest.fn(async () => ({}) ), - getSettings: jest.fn(async () => settings), - getLists: jest.fn(async () => ({}) ) - } - }) + cardData = { members: [] }; + settings = { scope: 'all' }; + context = { member: 123 }; + + t.card = jest.fn(async () => cardData); + t.getContext = jest.fn(async () => context); + + storage.getTask = jest.fn(async () => ({})); + storage.getSettings = jest.fn(async () => settings); + storage.getLists = jest.fn(async () => ({})); + }); beforeEach(() => { - sync = new Sync(t, storage) - }) + sync = new Sync(t, storage); + }); it('gets card data from the storage', async () => { - await sync.start() - expect(sync.t.card).toBeCalledWith('id', 'idList', 'members') - }) + await sync.start(); + expect(sync.t.card).toBeCalledWith('id', 'idList', 'members'); + }); it('gets settings from the storage', async () => { - await sync.start() - expect(sync.storage.getSettings).toBeCalledWith() - }) + await sync.start(); + expect(sync.storage.getSettings).toBeCalledWith(); + }); it('gets task data from the storage', async () => { - await sync.start() - expect(sync.storage.getTask).toBeCalledWith() - }) - - }) + await sync.start(); + expect(sync.storage.getTask).toBeCalledWith(); + }); + }); describe('when syncing only cards that was assigned to me', () => { - let t = {}, storage = {}, sync, me, settings, context + const t = {}; + const storage = {}; + let sync; + let me; + let settings; + let context; beforeAll(() => { - me = { id: 123 } - settings = { scope: 'me' } - context = { member: me.id } - t = { - getContext: () => context, - } - storage = { - getSettings: async () => settings, - getTask: async () => ({}), - getLists: async () => ({}), - } - }) + me = { id: 123 }; + settings = { scope: 'me' }; + context = { member: me.id }; - describe('when card is not assigned to me', () => { + t.getContext = () => context; + + storage.getSettings = async () => settings; + storage.getTask = async () => ({}); + storage.getLists = async () => ({}); + }); + describe('when card is not assigned to me', () => { beforeAll(() => { - t.card = () => ({ members : [] }) - }) + t.card = () => ({ members: [] }); + }); beforeEach(() => { - sync = new Sync(t, storage) - }) + sync = new Sync(t, storage); + }); it('unmarks the card', async () => { - expect(sync.handleScoped).toBeDefined() - sync.handleScoped = jest.fn() - await sync.start() - expect(sync.handleScoped).toBeCalled() - }) + expect(sync.handleScoped).toBeDefined(); + sync.handleScoped = jest.fn(); + await sync.start(); + expect(sync.handleScoped).toBeCalled(); + }); it('do not proceeds to the syncing', async () => { - expect(sync.handle).toBeDefined() - sync.handle = jest.fn() - await sync.start() - expect(sync.handle).not.toBeCalled() - }) - }) + expect(sync.handle).toBeDefined(); + sync.handle = jest.fn(); + await sync.start(); + expect(sync.handle).not.toBeCalled(); + }); + }); describe('when card is assigned to me', () => { - beforeAll(() => { - t.card = () => ({ members: [me] }) - }) + t.card = () => ({ members: [me] }); + }); beforeEach(() => { - sync = new Sync(t, storage) - }) + sync = new Sync(t, storage); + }); it('proceeds right to the syncing', async () => { - let handleScoped = jest.spyOn(sync, 'handleScoped') - let handle = jest.spyOn(sync, 'handle') - await sync.start() - expect(handleScoped).not.toBeCalled() - expect(handle).toBeCalled() - }) - }) - }) + const handleScoped = jest.spyOn(sync, 'handleScoped'); + const handle = jest.spyOn(sync, 'handle'); + await sync.start(); + expect(handleScoped).not.toBeCalled(); + expect(handle).toBeCalled(); + }); + }); + }); describe('when syncing all the cards', () => { - let t, storage, sync, card, lists, task, settings + const t = {}; + const storage = {}; + let sync; + let card; + let lists; + let task; + let settings; beforeAll(() => { - settings = { scope: 'all' } - card = { idList: 456 } - task = { id: 123, priority: 1 } - lists = { [card.idList]: 'doing' } - t = { - card: () => card, - } - storage = { - getSettings: () => settings, - getTask: () => task, - getLists: jest.fn(() => lists) - } - }) + settings = { scope: 'all' }; + card = { idList: 456 }; + task = { id: 123, priority: 1 }; + lists = { [card.idList]: 'doing' }; + + t.card = () => card; + + storage.getSettings = () => settings; + storage.getTask = () => task; + storage.getLists = jest.fn(() => lists); + }); beforeEach(() => { - sync = new Sync(t, storage) - }) + sync = new Sync(t, storage); + }); it('proceeds right to the syncing', async () => { - expect(sync.handleScoped).toBeDefined() - expect(sync.handle).toBeDefined() - sync.handleScoped = jest.fn() - sync.handle = jest.fn() - await sync.start() - expect(sync.handleScoped).not.toBeCalled() - expect(sync.handle).toBeCalled() - }) + expect(sync.handleScoped).toBeDefined(); + expect(sync.handle).toBeDefined(); + sync.handleScoped = jest.fn(); + sync.handle = jest.fn(); + await sync.start(); + expect(sync.handleScoped).not.toBeCalled(); + expect(sync.handle).toBeCalled(); + }); it('gets lists from the storage', async () => { - await sync.start() - expect(sync.storage.getLists).toBeCalledWith() - }) + await sync.start(); + expect(sync.storage.getLists).toBeCalledWith(); + }); it('calls sync handler with proper args', async () => { - expect(sync.handle).toBeDefined() - sync.handle = jest.fn() - let listType = lists[card.idList] - await sync.start() - expect(sync.handle).toBeCalledWith(task, listType) - }) - }) - }) + expect(sync.handle).toBeDefined(); + sync.handle = jest.fn(); + const listType = lists[card.idList]; + await sync.start(); + expect(sync.handle).toBeCalledWith(task, listType); + }); + }); + }); describe('.handleScoped()', () => { - let t = {}, storage = {}, sync, taskData = {} + const t = {}; + const storage = {}; + const taskData = {}; + let sync; beforeEach(() => { - sync = new Sync(t, storage) - sync.getTask = getTaskMock - }) + sync = new Sync(t, storage); + sync.currentTask = currentTaskMock; + }); describe('when to-do is present', () => { beforeAll(() => { - taskData.id = 123 - }) + taskData.id = 123; + }); describe('when to-do is done', () => { beforeAll(() => { - taskData.done = true - }) + taskData.done = true; + }); it('undo the to-do', () => { - sync.handleScoped(taskData) - expect(sync.getTask().handleUndo).toBeCalledWith() - }) + sync.handleScoped(taskData); + expect(sync.currentTask().handleUndo).toBeCalledWith(); + }); it('removes the to-do', async () => { - await sync.handleScoped(taskData) - expect(sync.getTask().handleRemove).toBeCalledWith() - }) - }) + await sync.handleScoped(taskData); + expect(sync.currentTask().handleRemove).toBeCalledWith(); + }); + }); describe('when task is not done', () => { beforeAll(() => { - taskData.done = false - }) + taskData.done = false; + }); it('removes the to-do', () => { - sync.handleScoped(taskData) - expect(sync.getTask().handleRemove).toBeCalledWith() - }) - }) - }) - }) + sync.handleScoped(taskData); + expect(sync.currentTask().handleRemove).toBeCalledWith(); + }); + }); + }); + }); describe('.handle()', () => { - let t = {}, storage = {}, sync, listType, taskData = {} + const t = {}; + const storage = {}; + const taskData = {}; + let sync; + let listType; beforeEach(() => { - sync = new Sync(t, storage) - sync.getTask = getTaskMock - }) + sync = new Sync(t, storage); + sync.currentTask = currentTaskMock; + }); describe('when card is moved to a list of type DOING', () => { beforeAll(() => { - listType = LIST_TYPES.DOING - }) + listType = LIST_TYPES.DOING; + }); describe('when task is present', () => { beforeAll(() => { - taskData.id = 123 - }) + taskData.id = 123; + }); describe('when task is done', () => { beforeAll(() => { - taskData.done = true - }) + taskData.done = true; + }); it('undo the task', () => { - sync.handle(taskData, listType) - expect(sync.getTask().handleUndo).toBeCalled() - }) - }) - }) + sync.handle(taskData, listType); + expect(sync.currentTask().handleUndo).toBeCalled(); + }); + }); + }); describe('when task is not present', () => { beforeAll(() => { - taskData.id = undefined - }) + taskData.id = undefined; + }); it('adds a task', () => { - sync.handle(taskData, listType) - expect(sync.getTask().handleAdd).toBeCalled() - }) - }) - }) + sync.handle(taskData, listType); + expect(sync.currentTask().handleAdd).toBeCalled(); + }); + }); + }); describe('when card is moved to a list of type DONE', () => { beforeAll(() => { - listType = LIST_TYPES.DONE - }) + listType = LIST_TYPES.DONE; + }); describe('when task is present', () => { beforeAll(() => { - taskData.id = 123 - }) + taskData.id = 123; + }); describe('when task is not done', () => { beforeAll(() => { - taskData.done = false - }) + taskData.done = false; + }); it('do the task', () => { - sync.handle(taskData, listType) - expect(sync.getTask().handleDo).toBeCalled() - }) - }) - }) + sync.handle(taskData, listType); + expect(sync.currentTask().handleDo).toBeCalled(); + }); + }); + }); describe('when task is not present', () => { beforeAll(() => { - taskData.id = undefined - }) + taskData.id = undefined; + }); it('adds a task', () => { - sync.handle(taskData, listType) - expect(sync.getTask().handleAdd).toBeCalled() - }) + sync.handle(taskData, listType); + expect(sync.currentTask().handleAdd).toBeCalled(); + }); it('do the task', async () => { - await sync.handle(taskData, listType) - expect(sync.getTask().handleDo).toBeCalled() - }) - }) - }) + await sync.handle(taskData, listType); + expect(sync.currentTask().handleDo).toBeCalled(); + }); + }); + }); describe('when card is moved to other lists', () => { beforeAll(() => { - listType = 'some other list' - }) + listType = 'some other list'; + }); describe('when task is present', () => { beforeAll(() => { - taskData.id = 123 - }) + taskData.id = 123; + }); describe('when task is done', () => { beforeAll(() => { - taskData.done = true - }) + taskData.done = true; + }); it('undo the task', () => { - sync.handle(taskData, listType) - expect(sync.getTask().handleUndo).toBeCalled() - }) + sync.handle(taskData, listType); + expect(sync.currentTask().handleUndo).toBeCalled(); + }); it('removes the task', async () => { - await sync.handle(taskData, listType) - expect(sync.getTask().handleRemove).toBeCalled() - }) - }) + await sync.handle(taskData, listType); + expect(sync.currentTask().handleRemove).toBeCalled(); + }); + }); describe('when task is not done', () => { beforeAll(() => { - taskData.done = false - }) + taskData.done = false; + }); it('removes the task', () => { - sync.handle(taskData, listType) - expect(sync.getTask().handleRemove).toBeCalled() - }) - }) - }) - }) - }) -}) + sync.handle(taskData, listType); + expect(sync.currentTask().handleRemove).toBeCalled(); + }); + }); + }); + }); + }); +}); diff --git a/test/task-form.test.js b/test/task-form.test.js index d7d42b0..481cfc9 100644 --- a/test/task-form.test.js +++ b/test/task-form.test.js @@ -1,133 +1,142 @@ -import TaskForm from '../src/js/task-form' +import TaskForm from '../src/js/task-form'; describe('TaskForm class', () => { - describe('.initialize()', () => { - let t = {}, storage = {}, form, task + const t = {}; + const storage = {}; + let form; + let task; beforeAll(() => { task = { priority: '1' - } - storage.getTask = jest.fn(async () => task ) - }) + }; + storage.getTask = jest.fn(async () => task); + }); beforeEach(() => { - form = new TaskForm(t, storage) - form.initializeElements = jest.fn() - form.setPriority = jest.fn() - form.listenToSubmit = jest.fn() - }) + form = new TaskForm(t, storage); + form.initializeElements = jest.fn(); + form.setPriority = jest.fn(); + form.listenToSubmit = jest.fn(); + }); it('gets task data from the storage', () => { - form.initialize() - expect(form.storage.getTask).toBeCalledWith() - }) + form.initialize(); + expect(form.storage.getTask).toBeCalledWith(); + }); it('initializes dom elements', async () => { - await form.initialize() - expect(form.initializeElements).toBeCalledWith() - }) + await form.initialize(); + expect(form.initializeElements).toBeCalledWith(); + }); it('sets priority value from the storage', async () => { - await form.initialize() - expect(form.setPriority).toBeCalledWith(task.priority) - }) + await form.initialize(); + expect(form.setPriority).toBeCalledWith(task.priority); + }); it('listens to submit', async () => { - await form.initialize() - expect(form.listenToSubmit).toBeCalledWith() - }) - }) + await form.initialize(); + expect(form.listenToSubmit).toBeCalledWith(); + }); + }); describe('.setPriority()', () => { - let t = {}, storage = {}, form + const t = {}; + const storage = {}; + let form; beforeEach(() => { - form = new TaskForm(t, storage) - form.$priority = {} - }) + form = new TaskForm(t, storage); + form.$priority = {}; + }); it('assigns passed value to priority', () => { - let val = '1' - form.setPriority(val) - expect(form.$priority.value).toBe(val) - }) - }) + const val = '1'; + form.setPriority(val); + expect(form.$priority.value).toBe(val); + }); + }); describe('.listenToSubmit()', () => { - let t = {}, storage = {}, form + const t = {}; + const storage = {}; + let form; beforeEach(() => { - form = new TaskForm(t, storage) - form.$submitButton = { addEventListener: jest.fn() } - }) + form = new TaskForm(t, storage); + form.$submitButton = { addEventListener: jest.fn() }; + }); it('adds on click listener to submit button', () => { - form.listenToSubmit() + form.listenToSubmit(); expect(form.$submitButton.addEventListener).toBeCalledWith( 'click', expect.any(Function) - ) - }) - }) + ); + }); + }); describe('.handleSubmit()', () => { - let t = {}, storage = {}, form, task + const t = {}; + const storage = {}; + let form; + let task; beforeAll(() => { task = { priority: '1' - } - storage.getTask = jest.fn(async () => task) - t.closePopup = jest.fn() - }) + }; + storage.getTask = jest.fn(async () => task); + t.closePopup = jest.fn(); + }); beforeEach(() => { - form = new TaskForm(t, storage) - form.$submitButton = {} - form.$priority = {} - form.updatePriority = jest.fn(async () => ({}) ) - }) + form = new TaskForm(t, storage); + form.$submitButton = {}; + form.$priority = {}; + form.updatePriority = jest.fn(async () => ({})); + }); it('disables submit button', () => { - form.handleSubmit() - expect(form.$submitButton.disabled).toBe(true) - }) + form.handleSubmit(); + expect(form.$submitButton.disabled).toBe(true); + }); it('gets task from the storage', () => { - form.handleSubmit() - expect(form.storage.getTask).toBeCalledWith() - }) + form.handleSubmit(); + expect(form.storage.getTask).toBeCalledWith(); + }); describe('when task was changed', () => { beforeAll(() => { - task.priority = '1' - }) + task.priority = 1; + }); beforeEach(() => { - form.$priority.value = '2' - }) + form.$priority.value = 2; + }); it('saves priority to storage', async () => { - await form.handleSubmit() - expect(form.updatePriority).toBeCalledWith(form.$priority.value) - }) - }) + await form.handleSubmit(); + expect(form.updatePriority).toBeCalledWith(form.$priority.value); + }); + }); describe('when task was not changed', () => { beforeAll(() => { - task.priority = '1' - }) + task.priority = 1; + }); beforeEach(() => { - form.$priority.value = '1' - }) + form.$priority.value = 1; + }); it('do nothing', async () => { - await form.handleSubmit() - expect(form.updatePriority).not.toBeCalled() - }) - }) - }) -}) + await form.handleSubmit(); + expect(form.updatePriority).not.toBeCalled(); + }); + }); + }); +}); diff --git a/test/task.test.js b/test/task.test.js index 04fee37..ff3cbf9 100644 --- a/test/task.test.js +++ b/test/task.test.js @@ -1,292 +1,359 @@ -import Storage from '../src/js/storage' -import HabiticaApi from '../src/js/habitica-api' -import Task from '../src/js/task' -import { TRELLO_ICON } from '../src/js/constants' +import Task from '../src/js/task'; +import { ICONS } from '../src/js/constants'; -jest.mock('../src/js/habitica-api') +jest.mock('../src/js/habitica-api'); describe('Task class', () => { describe('constructor', () => { - let t = {}, storage = {}, API = {}, task + const t = {}; + const storage = {}; + const API = {}; + let task; beforeEach(() => { - task = new Task(t, storage, API) - }) + task = new Task(t, storage, API); + }); it('assigns passed trello instance to local t variable', () => { - expect(task.t).toBeDefined() - expect(task.t).toBe(t) - }) + expect(task.t).toBeDefined(); + expect(task.t).toBe(t); + }); it('assigns passed storage to local storage variable', () => { - expect(task.storage).toBeDefined() - expect(task.storage).toBe(storage) - }) + expect(task.storage).toBeDefined(); + expect(task.storage).toBe(storage); + }); it('assings passed API instance to local API variable', () => { - expect(task.API).toBeDefined() - expect(task.API).toBe(API) - }) - }) + expect(task.API).toBeDefined(); + expect(task.API).toBe(API); + }); + }); describe('.getTemplate()', () => { - let t = {}, storage, API = {}, task, card, settings + const t = {}; + const storage = {}; + const API = {}; + let task; + let card; + let settings; beforeAll(() => { - settings = { priority: 1 } - storage = { - getSettings: jest.fn(async () => settings) - } + settings = { priority: 1 }; + + storage.getSettings = jest.fn(async () => settings); + card = { shortLink: 'asdf', name: 'Card name' - } - }) + }; + }); beforeEach(() => { - task = new Task(t, storage, API) - }) + task = new Task(t, storage, API); + }); it('gets settings from the storage', async () => { - await task.getTemplate(card) - expect(task.storage.getSettings).toBeCalled() - }) + await task.getTemplate(card); + expect(task.storage.getSettings).toBeCalled(); + }); it('assigns task type as "todo"', async () => { - let template = await task.getTemplate(card) - expect(template.type).toBe('todo') - }) + const template = await task.getTemplate(card); + expect(template.type).toBe('todo'); + }); it('assigns default difficulty(priority) for todo', async () => { - let template = await task.getTemplate(card) - expect(template.priority).toBe(settings.priority) - }) + const template = await task.getTemplate(card); + expect(template.priority).toBe(settings.priority); + }); it('uses card name as text for todo', async () => { - let template = await task.getTemplate(card) - expect(template.text).toMatch(card.name) - }) + const template = await task.getTemplate(card); + expect(template.text).toMatch(card.name); + }); it('generates card url from shortLink', async () => { - let template = await task.getTemplate(card) - expect(template.notes).toMatch(card.shortLink) - }) + const template = await task.getTemplate(card); + expect(template.notes).toMatch(card.shortLink); + }); describe('when chosen to prepend the icon to the text', () => { beforeAll(() => { - settings.prependIcon = true - }) + settings.prependIcon = true; + }); it('prepends the icon', async () => { - let template = await task.getTemplate(card) - expect(template.text).toMatch(TRELLO_ICON) - }) - }) + const template = await task.getTemplate(card); + expect(template.text).toMatch(ICONS.TRELLO_LOGO); + }); + }); describe('when chosen to not prepend the icon to the text', () => { beforeAll(() => { - settings.prependIcon = false - }) + settings.prependIcon = false; + }); it('not prepends the icon', async () => { - let template = await task.getTemplate(card) - expect(template.text).not.toMatch(TRELLO_ICON) - }) - }) - }) + const template = await task.getTemplate(card); + expect(template.text).not.toMatch(ICONS.TRELLO_LOGO); + }); + }); + }); describe('.handleAdd()', () => { - let t, storage, API, task, cardData, res, template + const t = {}; + const storage = {}; + let API; + let task; + let cardData; + let res; + let template; beforeAll(() => { cardData = { shortLink: 'asdf', name: 'Card name' - } - t = { - card: jest.fn(async () => cardData) - } - storage = { - setTask: jest.fn() - } - res = { + }; + + t.card = jest.fn(async () => cardData); + + storage.setTask = jest.fn(); + + res = { data: { id: 123, text: 'Card name', notes: 'Card url', priority: 1 - } - } - API = { - addTask: jest.fn(async () => res) - } + } + }; + API = { + addTask: jest.fn(async () => res) + }; template = { type: 'todo', text: 'Todo text' - } - }) + }; + }); beforeEach(() => { - task = new Task(t, storage, API) - task.getTemplate = jest.fn(async () => template) - }) + task = new Task(t, storage, API); + task.getTemplate = jest.fn(async () => template); + }); it('gets card data from the storage', async () => { - await task.handleAdd() - expect(task.t.card).toBeCalledWith('name', 'shortLink') - }) + await task.handleAdd(); + expect(task.t.card).toBeCalledWith('name', 'shortLink'); + }); it('generates request template', async () => { - await task.handleAdd() - expect(task.getTemplate).toBeCalledWith(cardData) - }) + await task.handleAdd(); + expect(task.getTemplate).toBeCalledWith(cardData); + }); it('adds a task by using API', async () => { - await task.handleAdd() - expect(task.API.addTask).toBeCalledWith(template) - }) + await task.handleAdd(); + expect(task.API.addTask).toBeCalledWith(template); + }); it('stores response in the storage', async () => { - await task.handleAdd() - expect(task.storage.setTask).toBeCalledWith(res.data) - }) - }) + await task.handleAdd(); + expect(task.storage.setTask).toBeCalledWith(res.data); + }); + }); describe('.handleRemove()', () => { - let t = {}, storage, API, task, taskData + const t = {}; + const storage = {}; + let API; + let task; + let taskData; beforeAll(() => { - taskData = { id: 123 } - storage = { - getTask: jest.fn(async () => taskData ), - removeTask: jest.fn(async () => ({}) ) - } + taskData = { id: 123 }; + + storage.getTask = jest.fn(async () => taskData); + storage.removeTask = jest.fn(async () => ({})); + API = { - removeTask: jest.fn(async () => ({}) ) - } - }) + removeTask: jest.fn(async () => ({})) + }; + }); beforeEach(() => { - task = new Task(t, storage, API) - }) + task = new Task(t, storage, API); + }); it('gets task data from the storage', async () => { - await task.handleRemove() - expect(task.storage.getTask).toBeCalledWith() - }) + await task.handleRemove(); + expect(task.storage.getTask).toBeCalledWith(); + }); it('removes the task by using API', async () => { - await task.handleRemove() - expect(task.API.removeTask).toBeCalledWith(taskData.id) - }) + await task.handleRemove(); + expect(task.API.removeTask).toBeCalledWith(taskData.id); + }); it('removes task data from the storage', async () => { - await task.handleRemove() - expect(task.storage.removeTask).toBeCalledWith() - }) - }) + await task.handleRemove(); + expect(task.storage.removeTask).toBeCalledWith(); + }); + }); describe('.handleDo()', () => { - let t = {}, storage, API, task, taskData + const t = {}; + const storage = {}; + let res; + let API; + let task; + let taskData; + let user; beforeAll(() => { - taskData = { id: 123 } - storage = { - getTask: jest.fn(async () => taskData), - setTask: jest.fn(async () => ({}) ) - } + taskData = { id: 123 }; + + storage.getTask = jest.fn(async () => taskData); + storage.setTask = jest.fn(async () => ({})); + + res = { + data: { + gp: 100, + exp: 200, + lvl: 37 + } + }; + API = { - doTask: jest.fn(async () => ({}) ) - } - }) + doTask: jest.fn(async () => res) + }; + + user = { + updateStats: jest.fn() + }; + }); beforeEach(() => { - task = new Task(t, storage, API) - }) + task = new Task(t, storage, API, user); + }); it('gets task data from the storage', async () => { - await task.handleDo() - expect(task.storage.getTask).toBeCalledWith() - }) + await task.handleDo(); + expect(task.storage.getTask).toBeCalledWith(); + }); it('do task by using API', async () => { - await task.handleDo() - expect(task.API.doTask).toBeCalledWith(taskData.id) - }) + await task.handleDo(); + expect(task.API.doTask).toBeCalledWith(taskData.id); + }); it('marks task as done in the storage', async () => { - await task.handleDo() - expect(task.storage.setTask).toBeCalledWith({ done: true }) - }) - }) + await task.handleDo(); + expect(task.storage.setTask).toBeCalledWith({ done: true }); + }); + + it('updates user stats with response data', async () => { + await task.handleDo(); + expect(task.currentUser.updateStats).toBeCalledWith(res.data); + }); + }); describe('.handleUndo()', () => { - let t, storage, API, task, taskData + const t = {}; + const storage = {}; + let API; + let task; + let taskData; + let user; + let res; beforeAll(() => { - taskData = { id: 123 } - storage = { - getTask: jest.fn(async () => taskData), - setTask: jest.fn(async () => ({}) ) - } + taskData = { id: 123 }; + + storage.getTask = jest.fn(async () => taskData); + storage.setTask = jest.fn(async () => ({})); + + res = { + data: { + gp: 100, + exp: 200, + lvl: 37 + } + }; + API = { - undoTask: jest.fn(async () => ({}) ) - } - }) + undoTask: jest.fn(async () => res) + }; + + user = { + updateStats: jest.fn() + }; + }); beforeEach(() => { - task = new Task(t, storage, API) - }) + task = new Task(t, storage, API, user); + }); it('gets task data from the storage', async () => { - await task.handleUndo() - expect(task.storage.getTask).toBeCalledWith() - }) + await task.handleUndo(); + expect(task.storage.getTask).toBeCalledWith(); + }); it('undo task by using API', async () => { - await task.handleUndo() - expect(task.API.undoTask).toBeCalledWith(taskData.id) - }) + await task.handleUndo(); + expect(task.API.undoTask).toBeCalledWith(taskData.id); + }); it('marks task as not done in the storage', async () => { - await task.handleUndo() - expect(task.storage.setTask).toBeCalledWith({ done: false }) - }) - }) + await task.handleUndo(); + expect(task.storage.setTask).toBeCalledWith({ done: false }); + }); + + it('updates user stats with response data', async () => { + await task.handleUndo(); + expect(task.currentUser.updateStats).toBeCalledWith(res.data); + }); + }); describe('.handleUpdate()', () => { - let t = {}, storage, API, task, res, params, taskData + const t = {}; + const storage = {}; + let API; + let task; + let res; + let params; + let taskData; beforeAll(() => { - taskData = { id: 123 } - params = { priority: 1 } - storage = { - getTask: jest.fn(async () => taskData), - setTask: jest.fn(async () => ({}) ), - } - res = { data: params } + taskData = { id: 123 }; + params = { priority: 1 }; + + storage.getTask = jest.fn(async () => taskData); + storage.setTask = jest.fn(async () => ({})); + + res = { data: params }; API = { updateTask: jest.fn(async () => res) - } - }) + }; + }); beforeEach(() => { - task = new Task(t, storage, API) - }) + task = new Task(t, storage, API); + }); it('gets task data from the storage', async () => { - await task.handleUpdate(params) - expect(task.storage.getTask).toBeCalledWith() - }) + await task.handleUpdate(params); + expect(task.storage.getTask).toBeCalledWith(); + }); it('updates the task by using API', async () => { - await task.handleUpdate(params) - expect(task.API.updateTask).toBeCalledWith(taskData.id, params) - }) + await task.handleUpdate(params); + expect(task.API.updateTask).toBeCalledWith(taskData.id, params); + }); it('stores response in the storage', async () => { - await task.handleUpdate(params) - expect(task.storage.setTask).toBeCalledWith(res.data) - }) - }) -}) + await task.handleUpdate(params); + expect(task.storage.setTask).toBeCalledWith(res.data); + }); + }); +}); diff --git a/test/user.test.js b/test/user.test.js new file mode 100644 index 0000000..c67f503 --- /dev/null +++ b/test/user.test.js @@ -0,0 +1,275 @@ +import User from '../src/js/user'; + +describe('User class', () => { + describe('constructor', () => { + const t = {}; + const storage = {}; + let user; + + beforeEach(() => { + user = new User(t, storage); + }); + + it('assigns passed trello instance to local t variable', () => { + expect(user.t).toBeDefined(); + expect(user.t).toBe(t); + }); + + it('assigns passed storage to local storage variable', () => { + expect(user.storage).toBeDefined(); + expect(user.storage).toBe(storage); + }); + }); + + describe('.updateStats()', () => { + const t = {}; + const storage = {}; + const expToNextLevel = 123; + let user; + let stats; + + beforeAll(() => { + stats = { + gp: 100, + exp: 200, + lvl: 37 + }; + + storage.setUser = jest.fn(async () => {}); + }); + + beforeEach(() => { + user = new User(t, storage); + user.notifyAboutStats = jest.fn(async () => {}); + user.calculateExpToNextLevel = jest.fn(() => expToNextLevel); + }); + + it('notifies about stats changes', async () => { + await user.updateStats(stats); + expect(user.notifyAboutStats).toBeCalledWith({ + gold: stats.gp, + exp: stats.exp + }); + }); + + it('calculates how many exp is left to the next level', async () => { + await user.updateStats(stats); + expect(user.calculateExpToNextLevel).toBeCalledWith(stats.lvl); + }); + + it('saves user stats to the storage', async () => { + await user.updateStats(stats); + expect(user.storage.setUser).toBeCalledWith({ + lvl: stats.lvl, + gold: stats.gp, + exp: stats.exp, + expToNextLevel + }); + }); + }); + + describe('.calculateExpToNextLevel()', () => { + const t = {}; + const storage = {}; + const stats = {}; + let user; + let currentUser; + + describe("when user's level was changed", () => { + beforeAll(() => { + stats.lvl = 37; + currentUser = { + // user gained a new level but we + // have outdated data in the storage + lvl: stats.lvl - 1, + expToNextLevel: 1 + }; + storage.getUser = async () => currentUser; + }); + + it('calculates how many exp is left to the next level', async () => { + user = new User(t, storage); + const expToNextLevel = await user.calculateExpToNextLevel(stats.lvl); + expect(expToNextLevel).toBeGreaterThan(0); + expect(expToNextLevel).not.toBe(currentUser.expToNextLevel); + }); + }); + + describe("when user's level wasn't changed", () => { + beforeAll(() => { + stats.lvl = 37; + currentUser = { + lvl: stats.lvl, + expToNextLevel: 1 + }; + storage.getUser = async () => currentUser; + }); + + it('returns already calculated exp from the storage', async () => { + user = new User(t, storage); + const expToNextLevel = await user.calculateExpToNextLevel(stats.lvl); + expect(expToNextLevel).toBe(currentUser.expToNextLevel); + }); + }); + }); + + describe('.notifyAboutStats()', () => { + const t = {}; + const storage = {}; + let user; + let currentUser; + let stats; + + beforeAll(() => { + currentUser = { + gold: 100, + exp: 200 + }; + + t.alert = jest.fn(async () => ({ notified: true })); + storage.getUser = async () => currentUser; + }); + + describe('when stats notifications are disabled', () => { + beforeAll(() => { + storage.getSettings = async () => ({ showStatsNotifications: false }); + }); + + it('does nothing', async () => { + user = new User(t, storage); + const result = await user.notifyAboutStats(currentUser); + expect(result).toBe(undefined); + }); + }); + + describe('when stats notifications are enabled', () => { + beforeAll(() => { + storage.getSettings = async () => ({ showStatsNotifications: true }); + }); + + describe('when user is gained exp and gold', () => { + beforeAll(() => { + stats = { + gold: 101, + exp: 201 + }; + }); + + beforeEach(() => { + user = new User(t, storage); + }); + + it('uses _gained_ word in the message', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + message: expect.stringMatching('gained') + }) + ); + }); + + it('display notification as success', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + display: 'success' + }) + ); + }); + + it('displays how many exp was gained', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + message: expect.stringMatching( + `${stats.exp - currentUser.exp} exp` + ) + }) + ); + }); + + it('displays how many gold was gained', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + message: expect.stringMatching( + `${(stats.gold - currentUser.gold).toFixed(2)} gold` + ) + }) + ); + }); + + it('displays the notification for 5 seconds', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + duration: 5 + }) + ); + }); + }); + + describe('when user is lost exp and gold', () => { + beforeAll(() => { + stats = { + gold: 99, + exp: 199 + }; + }); + + beforeEach(() => { + user = new User(t, storage); + }); + + it('uses _lost_ word in the message', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + message: expect.stringMatching('lost') + }) + ); + }); + + it('display notification as error', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + display: 'error' + }) + ); + }); + + it('displays how many exp was lost', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + message: expect.stringMatching( + `${stats.exp - currentUser.exp} exp` + ) + }) + ); + }); + + it('displays how many gold was lost', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + message: expect.stringMatching( + `${(stats.gold - currentUser.gold).toFixed(2)} gold` + ) + }) + ); + }); + + it('displays the notification for 5 seconds', async () => { + await user.notifyAboutStats(stats); + expect(user.t.alert).toBeCalledWith( + expect.objectContaining({ + duration: 5 + }) + ); + }); + }); + }); + }); +});