diff --git a/.travis.yml b/.travis.yml index aaf414d4e..b60270840 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,11 @@ env: - CLOUDDATA_HOST_VAR=CLOUDDATA_HOST_$TRAVIS_BRANCH - CLOUDDATA_HOST=${!CLOUDDATA_HOST_VAR} - CLOUDDATA_HOST=${CLOUDDATA_HOST:-$CLOUDDATA_HOST_STAGING} + - RECAPTCHA_SITE_KEY_master=6LeRbUwUAAAAAFYhKgk3G9OKWqE_OJ7Z-7VTUCbl + - RECAPTCHA_SITE_KEY_STAGING=6LfukK4UAAAAAFR44yoZMhv8fj6xh-PMiIxwryG3 + - RECAPTCHA_SITE=RECAPTCHA_SITE_KEY_$TRAVIS_BRANCH + - RECAPTCHA_SITE_KEY=${!RECAPTCHA_SITE_KEY_VAR} + - RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY:-$RECAPTCHA_SITE_KEY_STAGING} - ROOT_URL_master=https://scratch.mit.edu - ROOT_URL_STAGING=https://scratch.ly - ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH diff --git a/package-lock.json b/package-lock.json index 9ebb2def4..636680aed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,471 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/cli": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.6.0.tgz", + "integrity": "sha512-1CTDyGUjQqW3Mz4gfKZ04KGOckyyaNmKneAMlABPS+ZyuxWv3FrVEVz7Ag08kNIztVx8VaJ8YgvYLSNlMKAT5Q==", + "dev": true, + "requires": { + "chokidar": "^2.1.8", + "commander": "^2.8.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "lodash": "^4.17.13", + "mkdirp": "^0.5.1", + "output-file-sync": "^2.0.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "optional": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "optional": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true, + "optional": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true, + "optional": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "optional": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "optional": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "optional": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "optional": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "optional": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "optional": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "optional": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "optional": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "optional": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "optional": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "optional": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "optional": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "optional": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "optional": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true, + "optional": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "optional": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true + }, + "output-file-sync": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-2.0.1.tgz", + "integrity": "sha512-mDho4qm7WgIXIGf4eYU1RHN2UU5tPfVYVSRwDJw0uTmj35DQUt/eNp19N7v6T3SrR0ESTEf2up2CGO73qI35zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "is-plain-obj": "^1.1.0", + "mkdirp": "^0.5.1" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, "@babel/code-frame": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", @@ -14,18 +479,18 @@ } }, "@babel/core": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.5.tgz", - "integrity": "sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.6.0.tgz", + "integrity": "sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw==", "dev": true, "requires": { "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", - "@babel/helpers": "^7.5.5", - "@babel/parser": "^7.5.5", - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.5", - "@babel/types": "^7.5.5", + "@babel/generator": "^7.6.0", + "@babel/helpers": "^7.6.0", + "@babel/parser": "^7.6.0", + "@babel/template": "^7.6.0", + "@babel/traverse": "^7.6.0", + "@babel/types": "^7.6.0", "convert-source-map": "^1.1.0", "debug": "^4.1.0", "json5": "^2.1.0", @@ -45,12 +510,12 @@ } }, "@babel/generator": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", - "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz", + "integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==", "dev": true, "requires": { - "@babel/types": "^7.5.5", + "@babel/types": "^7.6.0", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0", @@ -58,32 +523,43 @@ } }, "@babel/parser": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", - "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", + "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==", "dev": true }, + "@babel/template": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", + "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.0" + } + }, "@babel/traverse": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", - "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz", + "integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==", "dev": true, "requires": { "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", + "@babel/generator": "^7.6.0", "@babel/helper-function-name": "^7.1.0", "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.5.5", - "@babel/types": "^7.5.5", + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.0", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" } }, "@babel/types": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", - "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", + "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", "dev": true, "requires": { "esutils": "^2.0.2", @@ -210,32 +686,23 @@ } }, "@babel/helpers": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.5.tgz", - "integrity": "sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.6.0.tgz", + "integrity": "sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ==", "dev": true, "requires": { - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.5", - "@babel/types": "^7.5.5" + "@babel/template": "^7.6.0", + "@babel/traverse": "^7.6.0", + "@babel/types": "^7.6.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, "@babel/generator": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", - "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.0.tgz", + "integrity": "sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA==", "dev": true, "requires": { - "@babel/types": "^7.5.5", + "@babel/types": "^7.6.0", "jsesc": "^2.5.1", "lodash": "^4.17.13", "source-map": "^0.5.0", @@ -243,32 +710,54 @@ } }, "@babel/parser": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", - "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.0.tgz", + "integrity": "sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ==", "dev": true }, + "@babel/template": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", + "integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.0" + } + }, "@babel/traverse": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", - "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.0.tgz", + "integrity": "sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ==", "dev": true, "requires": { "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", + "@babel/generator": "^7.6.0", "@babel/helper-function-name": "^7.1.0", "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.5.5", - "@babel/types": "^7.5.5", + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.0", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + } } }, "@babel/types": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", - "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz", + "integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==", "dev": true, "requires": { "esutils": "^2.0.2", @@ -516,9 +1005,9 @@ } }, "@types/babel__core": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.2.tgz", - "integrity": "sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", + "integrity": "sha512-8fBo0UR2CcwWxeX7WIIgJ7lXjasFxoYgRnFHUj+hRvKkpiBJbxhdAPTCY6/ZKM0uxANFVzt4yObSLuTiTnazDA==", "dev": true, "requires": { "@babel/parser": "^7.1.0", @@ -2780,9 +3269,9 @@ } }, "chromedriver": { - "version": "75.1.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-75.1.0.tgz", - "integrity": "sha512-N2P0fg6FS4c+tTG0R7cCOD5qiVo+E6uAz6xVjmbZesYv1xs1iGdcCUo0IqOY+ppD/4OOObG+XWV1CFWXT6UIgA==", + "version": "76.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-76.0.0.tgz", + "integrity": "sha512-jGyqs0N+lMo9iaNQxGKNPiLJWb2L9s2rwbRr1jJeQ37n6JQ1+5YMGviv/Fx5Z08vBWYbAvrKEzFsuYf8ppl+lw==", "dev": true, "requires": { "del": "^4.1.1", @@ -5011,22 +5500,25 @@ "dependencies": { "form-data": { "version": "0.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.0.3.tgz", + "integrity": "sha1-buoXtFeQtC13mh1YHRs2AP4MfA0=", "dev": true, "requires": { - "async": "~0.1.9", + "async": "0.1.9", "combined-stream": "0.0.3", - "mime": "~1.2.2" + "mime": "1.2.2" }, "dependencies": { "async": { "version": "0.1.9", - "resolved": false, + "resolved": "https://registry.npmjs.org/async/-/async-0.1.9.tgz", + "integrity": "sha1-+YTQc5tTgslJzDvqcC0h0NvVIEA=", "dev": true }, "combined-stream": { "version": "0.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.3.tgz", + "integrity": "sha1-odYiPEY6AAshyZN8SxXvQboAH3g=", "dev": true, "requires": { "delayed-stream": "0.0.5" @@ -5034,16 +5526,24 @@ "dependencies": { "delayed-stream": { "version": "0.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", + "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=", "dev": true } } + }, + "mime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.2.tgz", + "integrity": "sha1-udY1W/U+jX1WaTEw5FHa/zQBSM8=", + "dev": true } } }, "mime": { "version": "1.2.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.7.tgz", + "integrity": "sha1-x6E/M6cHPZkA8ohDawazoWIAhls=", "dev": true } } @@ -11045,7 +11545,8 @@ }, "isarray": { "version": "0.0.1", - "resolved": false + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "process-nextick-args": { "version": "1.0.6", @@ -11136,7 +11637,8 @@ }, "isarray": { "version": "0.0.1", - "resolved": false + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "string_decoder": { "version": "0.10.31", @@ -14184,6 +14686,23 @@ "symbol-observable": "^0.2.3" } }, + "redux-mock-store": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.3.tgz", + "integrity": "sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==", + "dev": true, + "requires": { + "lodash.isplainobject": "^4.0.6" + }, + "dependencies": { + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + } + } + }, "redux-thunk": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.0.1.tgz", @@ -15042,486 +15561,21 @@ } }, "scratch-gui": { - "version": "0.1.0-prerelease.20190828224521", - "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20190828224521.tgz", - "integrity": "sha512-kpcGP/l0SABRzTEOwlZsYI2My9D7ufecnoFQWoN0z433ThMHwjb3Ce/UDqumnw/97Lh8ndaVZq4TWwEfJxmskw==", + "version": "0.1.0-prerelease.20190912180550", + "resolved": "https://registry.npmjs.org/scratch-gui/-/scratch-gui-0.1.0-prerelease.20190912180550.tgz", + "integrity": "sha512-FPOuX1Za4vNAhOJfViEigGJ5t+dWzebgrUQWAvJDwrkhfKKHD2VE9vtaGH6H+6NNZIdW+2qcSqBPxutevdHlyA==", "dev": true }, "scratch-l10n": { - "version": "3.5.20190813223429", - "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.5.20190813223429.tgz", - "integrity": "sha512-rSxUSwv0RgZTXUknAWuc7BFZWewiNhrgyPUMos/qAw4GgVMdY1ZRSIHBEIItpCXXYLOzw4ObcNafIim6Taq9NA==", + "version": "3.5.20190910223701", + "resolved": "https://registry.npmjs.org/scratch-l10n/-/scratch-l10n-3.5.20190910223701.tgz", + "integrity": "sha512-WArZ1OZZz8KcTyEHcUnRJJE6gyh3CDcZ3KYxK8TNoHqzdLi+Gl/1QR/Xs+rJ1U587hnI2JLRfhFfvVPwsdmZag==", "dev": true, "requires": { "@babel/cli": "^7.1.2", "@babel/core": "^7.1.2", "babel-plugin-react-intl": "^3.0.1", "transifex": "1.6.6" - }, - "dependencies": { - "@babel/cli": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.5.5.tgz", - "integrity": "sha512-UHI+7pHv/tk9g6WXQKYz+kmXTI77YtuY3vqC59KIqcoWEjsJJSG6rAxKaLsgj3LDyadsPrCB929gVOKM6Hui0w==", - "dev": true, - "requires": { - "chokidar": "^2.0.4", - "commander": "^2.8.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.0.0", - "lodash": "^4.17.13", - "mkdirp": "^0.5.1", - "output-file-sync": "^2.0.0", - "slash": "^2.0.0", - "source-map": "^0.5.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "optional": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "optional": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true, - "optional": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true, - "optional": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "optional": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "optional": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", - "dev": true, - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "optional": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "optional": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "optional": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "optional": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "optional": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "optional": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "optional": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "optional": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "optional": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "optional": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "optional": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "optional": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "optional": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "optional": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "optional": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "optional": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "optional": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "optional": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "optional": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "optional": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "optional": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "optional": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "optional": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "optional": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true, - "optional": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "optional": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true, - "optional": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "optional": true - }, - "output-file-sync": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-2.0.1.tgz", - "integrity": "sha512-mDho4qm7WgIXIGf4eYU1RHN2UU5tPfVYVSRwDJw0uTmj35DQUt/eNp19N7v6T3SrR0ESTEf2up2CGO73qI35zQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "is-plain-obj": "^1.1.0", - "mkdirp": "^0.5.1" - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } } }, "scratch-parser": { diff --git a/package.json b/package.json index 1298622fa..4b1735e83 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "test:lint": "eslint . --ext .js,.jsx,.json", "test:integration": "npm run test:integration:jest && npm run test:smoke", "test:integration:jest": "jest ./test/integration/*.test.js", - "test:integration:remote": "npm run test:integration:jest && npm run test:smoke:sauce", + "test:integration:remote": "npm run test:integration:jest:remote && npm run test:smoke:sauce", + "test:integration:jest:remote": "SMOKE_REMOTE=true jest ./test/integration/*.test.js", "test:smoke": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R classic", "test:smoke:verbose": "tap ./test/integration-legacy/smoke-testing/*.js --timeout=3600 --no-coverage -R spec", "test:smoke:sauce": "SMOKE_REMOTE=true tap ./test/integration-legacy/smoke-testing/*.js --timeout=60000 --no-coverage -R classic", @@ -66,7 +67,7 @@ "babel-preset-react": "6.22.0", "bowser": "1.9.4", "cheerio": "1.0.0-rc.2", - "chromedriver": "75.1.0", + "chromedriver": "76.0.0", "classnames": "2.2.5", "cookie": "0.2.2", "copy-webpack-plugin": "0.2.0", @@ -122,9 +123,10 @@ "react-string-replace": "0.4.1", "react-telephone-input": "4.3.4", "redux": "3.5.2", + "redux-mock-store": "^1.2.3", "redux-thunk": "2.0.1", "sass-loader": "6.0.6", - "scratch-gui": "0.1.0-prerelease.20190828224521", + "scratch-gui": "0.1.0-prerelease.20190912180550", "scratch-l10n": "latest", "selenium-webdriver": "3.6.0", "slick-carousel": "1.6.0", diff --git a/src/_colors.scss b/src/_colors.scss index a963d81d2..03ffef6f4 100644 --- a/src/_colors.scss +++ b/src/_colors.scss @@ -8,6 +8,7 @@ $ui-orange: hsla(38, 100, 55, 1); // #FFAB19 Control Primary $ui-orange-high-contrast: hsla(30, 100, 55, 1); // #FFAB19 Control Primary $ui-orange-10percent: hsla(35, 90, 55, .1); $ui-orange-25percent: hsla(35, 90, 55, .25); +$ui-orange-90percent: hsla(38, 100, 55, .9); $ui-dark-orange: hsla(30, 100, 55, 1); // ##FF8C1A Variables Primary diff --git a/src/components/formik-forms/formik-checkbox.jsx b/src/components/formik-forms/formik-checkbox.jsx index 804c4b855..0c3253c0e 100644 --- a/src/components/formik-forms/formik-checkbox.jsx +++ b/src/components/formik-forms/formik-checkbox.jsx @@ -12,9 +12,10 @@ const FormikCheckboxSubComponent = ({ id, label, labelClassName, + outerClassName, ...props }) => ( -
+
( ); @@ -75,7 +79,8 @@ FormikCheckbox.propTypes = { id: PropTypes.string, label: PropTypes.string, labelClassName: PropTypes.string, - name: PropTypes.string + name: PropTypes.string, + outerClassName: PropTypes.string }; module.exports = FormikCheckbox; diff --git a/src/components/join-flow/country-step.jsx b/src/components/join-flow/country-step.jsx index 86eee7497..a99f10506 100644 --- a/src/components/join-flow/country-step.jsx +++ b/src/components/join-flow/country-step.jsx @@ -8,6 +8,7 @@ const {injectIntl, intlShape} = require('react-intl'); const countryData = require('../../lib/country-data'); const FormikSelect = require('../../components/formik-forms/formik-select.jsx'); const JoinFlowStep = require('./join-flow-step.jsx'); +const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx'); require('./join-flow-steps.scss'); @@ -94,6 +95,13 @@ class CountryStep extends React.Component { validate={this.validateSelect} validationClassName="validation-full-width-input" /> + {/* note that this is a hidden checkbox the user will never see */} +
); diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index 98836f1e7..3da7ef5f1 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -4,9 +4,9 @@ const React = require('react'); const PropTypes = require('prop-types'); import {Formik} from 'formik'; const {injectIntl, intlShape} = require('react-intl'); -const emailValidator = require('email-validator'); const FormattedMessage = require('react-intl').FormattedMessage; +const validate = require('../../lib/validate'); const JoinFlowStep = require('./join-flow-step.jsx'); const FormikInput = require('../../components/formik-forms/formik-input.jsx'); const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx'); @@ -20,30 +20,91 @@ class EmailStep extends React.Component { 'handleSetEmailRef', 'handleValidSubmit', 'validateEmail', - 'validateForm' + 'validateForm', + 'setCaptchaRef', + 'captchaSolved', + 'onCaptchaLoad', + 'onCaptchaError' ]); + this.state = { + captchaIsLoading: true + }; } + componentDidMount () { // automatically start with focus on username field if (this.emailInput) this.emailInput.focus(); + + // If grecaptcha doesn't exist on window, we havent loaded the captcha js yet. Load it. + if (!window.grecaptcha) { + // ReCaptcha calls a callback when the grecatpcha object is usable. That callback + // needs to be global so set it on the window. + window.grecaptchaOnLoad = this.onCaptchaLoad; + // Load Google ReCaptcha script. + const script = document.createElement('script'); + script.async = true; + script.onerror = this.onCaptchaError; + script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`; + document.body.appendChild(script); + } + } + componentWillUnmount () { + window.grecaptchaOnLoad = null; } handleSetEmailRef (emailInputRef) { this.emailInput = emailInputRef; } + onCaptchaError () { + // TODO send user to error step once we have one. + } + onCaptchaLoad () { + this.setState({captchaIsLoading: false}); + this.grecaptcha = window.grecaptcha; + if (!this.grecaptcha) { + // According to the reCaptcha documentation, this callback shouldn't get + // called unless window.grecaptcha exists. This is just here to be extra defensive. + // TODO: Put up the error screen when we have one. + } + // TODO: Add in error callback for render once we have an error screen. + this.widgetId = this.grecaptcha.render(this.captchaRef, + { + callback: this.captchaSolved, + sitekey: process.env.RECAPTCHA_SITE_KEY + }, + true); + } validateEmail (email) { if (!email) return this.props.intl.formatMessage({id: 'general.required'}); - const isValidLocally = emailValidator.validate(email); - if (isValidLocally) { - return null; // TODO: validate email address remotely - } - return this.props.intl.formatMessage({id: 'registration.validationEmailInvalid'}); + const localResult = validate.validateEmailLocally(email); + if (!localResult.valid) return this.props.intl.formatMessage({id: localResult.errMsgId}); + return validate.validateEmailRemotely(email).then( + remoteResult => { + if (remoteResult.valid === true) { + return null; + } + return this.props.intl.formatMessage({id: remoteResult.errMsgId}); + } + ); } validateForm () { return {}; } handleValidSubmit (formData, formikBag) { - formikBag.setSubmitting(false); - this.props.onNextStep(formData); + this.formData = formData; + this.formikBag = formikBag; + // Change set submitting to false so that if the user clicks out of + // the captcha, the button is clickable again (instead of a disabled button with a spinner). + this.formikBag.setSubmitting(false); + this.grecaptcha.execute(this.widgetId); + } + captchaSolved (token) { + // Now thatcaptcha is done, we can tell Formik we're submitting. + this.formikBag.setSubmitting(true); + this.formData['g-recaptcha-response'] = token; + this.props.onNextStep(this.formData); + } + setCaptchaRef (ref) { + this.captchaRef = ref; } render () { return ( @@ -63,6 +124,8 @@ class EmailStep extends React.Component { handleSubmit, isSubmitting, setFieldError, + setFieldTouched, + setFieldValue, validateField } = props; return ( @@ -88,7 +151,7 @@ class EmailStep extends React.Component { innerClassName="join-flow-inner-email-step" nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})} title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})} - waiting={isSubmitting} + waiting={this.props.waiting || isSubmitting || this.state.captchaIsLoading} onSubmit={handleSubmit} > validateField('email')} - onFocus={() => setFieldError('email', null)} + onChange={e => { + setFieldValue('email', e.target.value); + setFieldTouched('email'); + setFieldError('email', null); + }} /* eslint-enable react/jsx-no-bind */ onSetRef={this.handleSetEmailRef} /> @@ -116,6 +183,13 @@ class EmailStep extends React.Component { name="subscribe" />
+
); }} @@ -126,7 +200,8 @@ class EmailStep extends React.Component { EmailStep.propTypes = { intl: intlShape, - onNextStep: PropTypes.func + onNextStep: PropTypes.func, + waiting: PropTypes.bool }; diff --git a/src/components/join-flow/join-flow-steps.scss b/src/components/join-flow/join-flow-steps.scss index a8d9550ea..e4e71df6b 100644 --- a/src/components/join-flow/join-flow-steps.scss +++ b/src/components/join-flow/join-flow-steps.scss @@ -132,6 +132,10 @@ margin-left: -.5rem; } +.join-flow-registration-error { + padding-top: 5.5rem; +} + .join-flow-gender-description { margin-top: .625rem; margin-bottom: 1.25rem; @@ -180,3 +184,7 @@ a.join-flow-link:link, a.join-flow-link:visited, a.join-flow-link:active { text-decoration: underline; } + +.yesNoCheckbox { + display: none; +} diff --git a/src/components/join-flow/join-flow.jsx b/src/components/join-flow/join-flow.jsx index f743dc088..93554102d 100644 --- a/src/components/join-flow/join-flow.jsx +++ b/src/components/join-flow/join-flow.jsx @@ -1,10 +1,13 @@ const bindAll = require('lodash.bindall'); +const connect = require('react-redux').connect; const defaults = require('lodash.defaultsdeep'); const PropTypes = require('prop-types'); const React = require('react'); +const api = require('../../lib/api'); const injectIntl = require('../../lib/intl.jsx').injectIntl; const intlShape = require('../../lib/intl.jsx').intlShape; +const sessionActions = require('../../redux/session.js'); const Progression = require('../progression/progression.jsx'); const UsernameStep = require('./username-step.jsx'); @@ -13,44 +16,141 @@ const GenderStep = require('./gender-step.jsx'); const CountryStep = require('./country-step.jsx'); const EmailStep = require('./email-step.jsx'); const WelcomeStep = require('./welcome-step.jsx'); +const RegistrationErrorStep = require('./registration-error-step.jsx'); -/* -eslint-disable react/prefer-stateless-function, react/no-unused-prop-types, no-useless-constructor -*/ class JoinFlow extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleAdvanceStep' + 'handleAdvanceStep', + 'handlePrepareToRegister', + 'handleRegistrationResponse', + 'handleSubmitRegistration' ]); this.state = { formData: {}, registrationError: null, - step: 0 + step: 0, + waiting: false }; } - handleAdvanceStep (formData) { - formData = formData || {}; + handlePrepareToRegister (newFormData) { + newFormData = newFormData || {}; + const newState = { + formData: defaults({}, newFormData, this.state.formData) + }; + this.setState(newState, () => { + this.handleSubmitRegistration(this.state.formData); + }); + } + handleRegistrationResponse (err, body, res) { + // example of failing response: + // [ + // { + // "msg": "This field is required.", + // "errors": { + // "username": ["This field is required."], + // "recaptcha": ["Incorrect, please try again."] + // }, + // "success": false + // } + // ] + this.setState({waiting: false}, () => { + let errStr = ''; + if (!err && res.statusCode === 200) { + if (body && body[0]) { + if (body[0].success) { + this.props.refreshSession(); + this.setState({ + step: this.state.step + 1 + }); + return; + } + if (body[0].errors) { + // body can include zero or more error objects, each + // with its own key and description. Here we assemble + // all of them into a single string, errStr. + const errorKeys = Object.keys(body[0].errors); + errorKeys.forEach(key => { + const val = body[0].errors[key]; + if (val && val[0]) { + if (errStr.length) errStr += '; '; + errStr += `${key}: ${val[0]}`; + } + }); + } + if (!errStr.length && body[0].msg) errStr = body[0].msg; + } + } + this.setState({ + registrationError: errStr || + `${this.props.intl.formatMessage({ + id: 'registration.generalError' + })} (${res.statusCode})` + }); + }); + } + handleSubmitRegistration (formData) { + this.setState({waiting: true}, () => { + api({ + host: '', + uri: '/accounts/register_new_user/', + method: 'post', + useCsrf: true, + formData: { + 'username': formData.username, + 'email': formData.email, + 'password': formData.password, + 'birth_month': formData.birth_month, + 'birth_year': formData.birth_year, + 'g-recaptcha-response': formData['g-recaptcha-response'], + 'gender': formData.gender, + 'country': formData.country, + 'subscribe': true, + 'is_robot': formData.yesno + // no need to include csrfmiddlewaretoken; will be provided in + // X-CSRFToken header, which scratchr2 looks for in + // scratchr2/middleware/csrf.py line 237. + } + }, (err, body, res) => { + this.handleRegistrationResponse(err, body, res); + }); + }); + } + handleAdvanceStep (newFormData) { + newFormData = newFormData || {}; this.setState({ - step: this.state.step + 1, - formData: defaults({}, formData, this.state.formData) + formData: defaults({}, newFormData, this.state.formData), + step: this.state.step + 1 }); } render () { return ( - - - - - - - this.handleSubmitRegistration(this.state.formData)} + /* eslint-enable react/jsx-no-bind */ /> - + ) : ( + + + + + + + + + )} ); } @@ -58,11 +158,27 @@ class JoinFlow extends React.Component { JoinFlow.propTypes = { intl: intlShape, - onCompleteRegistration: PropTypes.func + onCompleteRegistration: PropTypes.func, + refreshSession: PropTypes.func }; -module.exports = injectIntl(JoinFlow); +const IntlJoinFlow = injectIntl(JoinFlow); -/* -eslint-enable -*/ +const mapDispatchToProps = dispatch => ({ + refreshSession: () => { + dispatch(sessionActions.refreshSession()); + } +}); + +// Allow incoming props to override redux-provided props. Used to mock in tests. +const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign( + {}, stateProps, dispatchProps, ownProps +); + +const ConnectedJoinFlow = connect( + () => ({}), + mapDispatchToProps, + mergeProps +)(IntlJoinFlow); + +module.exports = ConnectedJoinFlow; diff --git a/src/components/join-flow/next-step-button.scss b/src/components/join-flow/next-step-button.scss index 2831adcbd..0b4bfc7ad 100644 --- a/src/components/join-flow/next-step-button.scss +++ b/src/components/join-flow/next-step-button.scss @@ -4,10 +4,16 @@ .modal-flush-bottom-button { margin: 0; width: 100%; + border: none; border-bottom-left-radius: 1rem; border-bottom-right-radius: 1rem; height: 5.1875rem; background-color: $ui-orange; + + &:hover { + transition: background-color .25s ease; + background-color: $ui-orange-90percent; + } } .next-step-title { diff --git a/src/components/join-flow/registration-error-step.jsx b/src/components/join-flow/registration-error-step.jsx new file mode 100644 index 000000000..24457d26a --- /dev/null +++ b/src/components/join-flow/registration-error-step.jsx @@ -0,0 +1,45 @@ +const bindAll = require('lodash.bindall'); +const React = require('react'); +const PropTypes = require('prop-types'); +const {injectIntl, intlShape} = require('react-intl'); + +const JoinFlowStep = require('./join-flow-step.jsx'); + +require('./join-flow-steps.scss'); + +class RegistrationErrorStep extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleSubmit' + ]); + } + handleSubmit (e) { + // JoinFlowStep includes a
that handles a submit action. + // But here, we're not really submitting, so we need to prevent + // the form from navigating away from the current page. + e.preventDefault(); + this.props.onTryAgain(); + } + render () { + return ( + + ); + } +} + +RegistrationErrorStep.propTypes = { + errorMsg: PropTypes.string, + intl: intlShape, + onTryAgain: PropTypes.func +}; + +const IntlRegistrationErrorStep = injectIntl(RegistrationErrorStep); + +module.exports = IntlRegistrationErrorStep; diff --git a/src/components/navigation/www/navigation.jsx b/src/components/navigation/www/navigation.jsx index 7bc3ca663..90d34bcad 100644 --- a/src/components/navigation/www/navigation.jsx +++ b/src/components/navigation/www/navigation.jsx @@ -217,6 +217,7 @@ class Navigation extends React.Component { {this.props.registrationOpen && ( this.props.useScratch3Registration ? ( diff --git a/src/components/registration/scratch3-registration.jsx b/src/components/registration/scratch3-registration.jsx index 03f53d763..220c01433 100644 --- a/src/components/registration/scratch3-registration.jsx +++ b/src/components/registration/scratch3-registration.jsx @@ -23,17 +23,19 @@ const Registration = ({ ); Registration.propTypes = { + // used in mapDispatchToProps; eslint doesn't understand that this prop is used + createProjectOnComplete: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types handleCloseRegistration: PropTypes.func, handleCompleteRegistration: PropTypes.func, isOpen: PropTypes.bool }; -const mapDispatchToProps = dispatch => ({ +const mapDispatchToProps = (dispatch, ownProps) => ({ handleCloseRegistration: () => { dispatch(navigationActions.setRegistrationOpen(false)); }, handleCompleteRegistration: () => { - dispatch(navigationActions.handleCompleteRegistration()); + dispatch(navigationActions.handleCompleteRegistration(ownProps.createProjectOnComplete)); } }); diff --git a/src/l10n.json b/src/l10n.json index 555cbbe39..a88926612 100644 --- a/src/l10n.json +++ b/src/l10n.json @@ -89,6 +89,7 @@ "general.ideas": "Ideas", "general.tipsWindow": "Tips Window", "general.termsOfUse": "Terms of Use", + "general.tryAgain": "Try again", "general.unhandledError": "We are so sorry, but it looks like Scratch has crashed. This bug has been automatically reported to the Scratch Team.", "general.username": "Username", "general.validationEmail": "Please enter a valid email address", diff --git a/src/lib/validate.js b/src/lib/validate.js index b4fd84f31..5df20b0e4 100644 --- a/src/lib/validate.js +++ b/src/lib/validate.js @@ -1,5 +1,6 @@ module.exports = {}; const api = require('./api'); +const emailValidator = require('email-validator'); module.exports.validateUsernameLocally = username => { if (!username || username === '') { @@ -67,3 +68,36 @@ module.exports.validatePasswordConfirm = (password, passwordConfirm) => { } return {valid: true}; }; + +module.exports.validateEmailLocally = email => { + if (!email || email === '') { + return {valid: false, errMsgId: 'general.required'}; + } else if (emailValidator.validate(email)) { + return {valid: true}; + } + return ({valid: false, errMsgId: 'registration.validationEmailInvalid'}); +}; + +module.exports.validateEmailRemotely = email => ( + new Promise(resolve => { + api({ + host: '', // not handled by API; use existing infrastructure + params: {email: email}, + uri: '/accounts/check_email/' + }, (err, body, res) => { + if (err || res.statusCode !== 200 || !body || body.length < 1 || !body[0].msg) { + resolve({valid: false, errMsgId: 'general.apiError'}); + } + switch (body[0].msg) { + case 'valid email': + resolve({valid: true}); + break; + case 'Scratch is not allowed to send email to this address.': // e.g., bad TLD or block-listed + case 'Enter a valid email address.': + default: + resolve({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + break; + } + }); + }) +); diff --git a/src/redux/navigation.js b/src/redux/navigation.js index 0e71090ae..a7ff091a8 100644 --- a/src/redux/navigation.js +++ b/src/redux/navigation.js @@ -92,9 +92,13 @@ module.exports.setSearchTerm = searchTerm => ({ searchTerm: searchTerm }); -module.exports.handleCompleteRegistration = () => (dispatch => { - dispatch(sessionActions.refreshSession()); - dispatch(module.exports.setRegistrationOpen(false)); +module.exports.handleCompleteRegistration = createProject => (dispatch => { + if (createProject) { + window.location = '/projects/editor/?tutorial=getStarted'; + } else { + dispatch(sessionActions.refreshSession()); + dispatch(module.exports.setRegistrationOpen(false)); + } }); module.exports.handleLogIn = (formData, callback) => (dispatch => { diff --git a/src/routes.json b/src/routes.json index 43ea0737a..c1494768f 100644 --- a/src/routes.json +++ b/src/routes.json @@ -161,6 +161,13 @@ "view": "jobs/moderator/moderator", "title": "Community Moderator" }, + { + "name": "join", + "pattern": "^/join/?$", + "routeAlias": "/join/?$", + "view": "join/join", + "title": "Join Scratch" + }, { "name": "messages", "pattern": "^/messages/?$", diff --git a/src/views/download/download.jsx b/src/views/download/download.jsx index 35c5994ca..2c935f04b 100644 --- a/src/views/download/download.jsx +++ b/src/views/download/download.jsx @@ -114,8 +114,8 @@ class Download extends React.Component { className="download-button" href={ this.state.OS === OS_ENUM.WINDOWS ? - 'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop%20Setup%203.5.0.exe' : - 'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop-3.5.0.dmg' + 'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop%20Setup%203.6.0.exe' : + 'https://downloads.scratch.mit.edu/desktop/Scratch%20Desktop-3.6.0.dmg' } > diff --git a/src/views/join/join.jsx b/src/views/join/join.jsx new file mode 100644 index 000000000..ebb4019fc --- /dev/null +++ b/src/views/join/join.jsx @@ -0,0 +1,17 @@ +const React = require('react'); +const render = require('../../lib/render.jsx'); +const JoinModal = require('../../components/modal/join/modal.jsx'); +const ErrorBoundary = require('../../components/errorboundary/errorboundary.jsx'); +// Require this even though we don't use it because, without it, webpack runs out of memory... +const Page = require('../../components/page/www/page.jsx'); // eslint-disable-line no-unused-vars + +const openModal = true; +const Register = () => ( + + + +); +render(, document.getElementById('app')); diff --git a/test/integration-legacy/selenium-helpers.js b/test/integration-legacy/selenium-helpers.js index 710c00110..99e0b8ceb 100644 --- a/test/integration-legacy/selenium-helpers.js +++ b/test/integration-legacy/selenium-helpers.js @@ -68,7 +68,7 @@ class SeleniumHelper { let driverConfig = { browserName: 'chrome', platform: 'macOS 10.14', - version: '75.0' + version: '76.0' }; var driver = new webdriver.Builder() .withCapabilities({ diff --git a/test/integration-legacy/smoke-testing/test_signing_in_and_out_discuss.js b/test/integration-legacy/smoke-testing/test_signing_in_and_out_discuss.js deleted file mode 100644 index e95426548..000000000 --- a/test/integration-legacy/smoke-testing/test_signing_in_and_out_discuss.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Tests from: - * - * https://github.com/LLK/scratchr2/wiki/Smoke-Testing-Test-Cases - * - */ - -const SeleniumHelper = require('../selenium-helpers.js'); -const helper = new SeleniumHelper(); - -var tap = require('tap'); -const test = tap.test; - -const driver = helper.buildDriver('www-smoke test_sign_in_out_discuss'); - -const { - clickText, - findByXpath, - findText, - clickXpath, - clickButton -} = helper; - -var username = process.env.SMOKE_USERNAME; -var password = process.env.SMOKE_PASSWORD; - -var rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; -var url = rootUrl + '/discuss'; - -tap.plan(2); - -tap.tearDown(function () { - driver.quit(); -}); - -tap.beforeEach(function () { - return driver.get(url); -}); - -test('Sign in to Scratch using scratchr2 navbar', t => { - clickText('Sign in') - .then(() => findByXpath('//input[@id="login_dropdown_username"]')) - .then((element) => element.sendKeys(username)) - .then(() => findByXpath('//input[@name="password"]')) - .then((element) => element.sendKeys(password)) - .then(() => clickButton('Sign in')) - .then(() => findByXpath('//li[contains(@class, "logged-in-user")' + - 'and contains(@class, "dropdown")]/span')) - .then((element) => element.getText('span')) - .then((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(), - 'first part of username should be displayed in navbar')) - .then(() => t.end()); -}); - -test('Sign out of Scratch using scratchr2 navbar', t => { - clickXpath('//span[contains(@class, "user-name")' + - ' and contains(@class, "dropdown-toggle")]/img[contains(@class, "user-icon")]') - .then(() => clickXpath('//input[@value="Sign out"]')) - .then(() => findText('Sign in')) - .then((element) => t.ok(element, 'Sign in reappeared on the page after signing out')) - .then(() => t.end()); -}); diff --git a/test/integration-legacy/smoke-testing/test_signing_in_and_out_homepage.js b/test/integration-legacy/smoke-testing/test_signing_in_and_out_homepage.js deleted file mode 100644 index 15687b0cf..000000000 --- a/test/integration-legacy/smoke-testing/test_signing_in_and_out_homepage.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Tests from: - * - * https://github.com/LLK/scratchr2/wiki/Smoke-Testing-Test-Cases - * - */ - -const SeleniumHelper = require('../selenium-helpers.js'); -const helper = new SeleniumHelper(); - -var tap = require('tap'); -const test = tap.test; - -const driver = helper.buildDriver('www-smoke test_sign_in_out_homepage'); - -const { - clickText, - findText, - findByXpath, - clickXpath -} = helper; - -var username = process.env.SMOKE_USERNAME; -var password = process.env.SMOKE_PASSWORD; - -var rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; - -tap.plan(2); - -tap.tearDown(function () { - driver.quit(); -}); - -tap.beforeEach(function () { - return driver.get(rootUrl); -}); - -test('Sign in to Scratch using scratch-www navbar', {skip: true}, t => { - clickText('Sign in') - .then(() => findByXpath('//input[@id="frc-username-1088"]')) - .then((element) => element.sendKeys(username)) - .then(() => findByXpath('//input[@id="frc-password-1088"]')) - .then((element) => element.sendKeys(password)) - .then(() => clickXpath('//button[contains(@class, "button") and ' + - 'contains(@class, "submit-button") and contains(@class, "white")]')) - .then(() => findByXpath('//span[contains(@class, "profile-name")]')) - .then((element) => element.getText()) - .then((text) => t.match(text.toLowerCase(), username.substring(0, 10).toLowerCase(), - 'first part of username should be displayed in navbar')) - .then(() => t.end()); -}); - -test('Sign out of Scratch using scratch-www navbar', {skip: true}, t => { - clickXpath('//a[contains(@class, "user-info")]') - .then(() => clickText('Sign out')) - .then(() => findText('Sign in')) - .then((element) => t.ok(element, 'Sign in reappeared on the page after signing out')) - .then(() => t.end()); -}); diff --git a/test/integration/selenium-helpers.js b/test/integration/selenium-helpers.js index 2b2dfdfb0..99e0b8ceb 100644 --- a/test/integration/selenium-helpers.js +++ b/test/integration/selenium-helpers.js @@ -67,8 +67,8 @@ class SeleniumHelper { // https://wiki.saucelabs.com/display/DOCS/Platform+Configurator let driverConfig = { browserName: 'chrome', - platform: 'macOS 10.13', - version: '70.0' + platform: 'macOS 10.14', + version: '76.0' }; var driver = new webdriver.Builder() .withCapabilities({ diff --git a/test/integration/sign-in-and-out.test.js b/test/integration/sign-in-and-out.test.js new file mode 100644 index 000000000..7e2bdf974 --- /dev/null +++ b/test/integration/sign-in-and-out.test.js @@ -0,0 +1,100 @@ +const SeleniumHelper = require('./selenium-helpers.js'); + +const { + clickText, + findByXpath, + clickXpath, + clickButton, + buildDriver +} = new SeleniumHelper(); + +let username = process.env.SMOKE_USERNAME; +let password = process.env.SMOKE_PASSWORD; +let remote = process.env.SMOKE_REMOTE || false; +let rootUrl = process.env.ROOT_URL || 'https://scratch.ly'; +let scratchr2url = rootUrl + '/users/' + username; +let wwwURL = rootUrl; + +if (remote){ + jest.setTimeout(60000); +} + +let driver; + +describe('www-integration sign-in-and-out', () => { + beforeAll(async () => { + driver = await buildDriver('www-integration sign-in-out'); + }); + + describe('sign in', () => { + afterEach(async () => { + await driver.get(wwwURL); + await clickXpath('//div[@class="account-nav"]'); + await clickText('Sign out'); + }); + + test('sign in on www', async () => { + await driver.get(wwwURL); + await driver.sleep(1000); + await clickXpath('//li[@class="link right login-item"]/a'); + let name = await findByXpath('//input[@id="frc-username-1088"]'); + await name.sendKeys(username); + let word = await findByXpath('//input[@id="frc-password-1088"]'); + await word.sendKeys(password); + await clickXpath('//button[contains(@class, "button") and ' + + 'contains(@class, "submit-button") and contains(@class, "white")]'); + let element = await findByXpath('//span[contains(@class, "profile-name")]'); + let text = await element.getText(); + await expect(text.toLowerCase()).toEqual(username.toLowerCase()); + }); + + test('sign in on scratchr2', async () => { + await driver.get(scratchr2url); + await clickXpath('//li[@class="sign-in dropdown"]/span'); + let name = await findByXpath('//input[@id="login_dropdown_username"]'); + await name.sendKeys(username); + let word = await findByXpath('//input[@name="password"]'); + await word.sendKeys(password); + await clickButton('Sign in'); + let element = await findByXpath('//span[@class="user-name dropdown-toggle"]'); + let text = await element.getText(); + await expect(text.toLowerCase()).toEqual(username.toLowerCase()); + }); + }); + + describe('sign out', () => { + beforeEach(async () => { + await driver.get(wwwURL); + await clickXpath('//li[@class="link right login-item"]'); + let name = await findByXpath('//input[@id="frc-username-1088"]'); + await name.sendKeys(username); + let word = await findByXpath('//input[@id="frc-password-1088"]'); + await word.sendKeys(password); + await clickXpath('//button[contains(@class, "button") and ' + + 'contains(@class, "submit-button") and contains(@class, "white")]'); + }); + + test('sign out on www', async () => { + await clickXpath('//a[contains(@class, "user-info")]'); + await clickText('Sign out'); + let element = await findByXpath('//li[@class="link right login-item"]/a/span'); + let text = await element.getText(); + await expect(text.toLowerCase()).toEqual('Sign In'.toLowerCase()); + }); + + test('sign out on scratchr2', async () => { + await driver.get(scratchr2url); + await clickXpath('//span[@class="user-name dropdown-toggle"]'); + await clickXpath('//li[@id="logout"]'); + let element = await findByXpath('//li[@class="link right login-item"]/a/span'); + let text = await element.getText(); + await expect(text.toLowerCase()).toEqual('Sign In'.toLowerCase()); + }); + + }); + + afterAll(async () => { + await driver.quit(); + }); + +}); diff --git a/test/integration/test-integration.test.js b/test/integration/test-integration.test.js deleted file mode 100644 index 44c96735d..000000000 --- a/test/integration/test-integration.test.js +++ /dev/null @@ -1,5 +0,0 @@ -describe('test jest integration', () => { - test('testing test', () => { - expect('integration').toEqual('integration'); - }); -}); diff --git a/test/unit/components/email-step.test.jsx b/test/unit/components/email-step.test.jsx new file mode 100644 index 000000000..88af9b8b2 --- /dev/null +++ b/test/unit/components/email-step.test.jsx @@ -0,0 +1,140 @@ +const React = require('react'); +const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx'); +const EmailStep = require('../../../src/components/join-flow/email-step.jsx'); +const JoinFlowStep = require('../../../src/components/join-flow/join-flow-step.jsx'); +const FormikInput = require('../../../src/components/formik-forms/formik-input.jsx'); +const FormikCheckbox = require('../../../src/components/formik-forms/formik-checkbox.jsx'); + +describe('EmailStep test', () => { + test('send correct props to formik', () => { + const wrapper = shallowWithIntl(); + + const formikWrapper = wrapper.dive(); + expect(formikWrapper.props().initialValues.subscribe).toBe(false); + expect(formikWrapper.props().initialValues.email).toBe(''); + expect(formikWrapper.props().validateOnBlur).toBe(false); + expect(formikWrapper.props().validateOnChange).toBe(false); + expect(formikWrapper.props().validate).toBe(formikWrapper.instance().validateForm); + expect(formikWrapper.props().onSubmit).toBe(formikWrapper.instance().handleValidSubmit); + }); + + test('props sent to JoinFlowStep', () => { + const wrapper = shallowWithIntl(); + // Dive to get past the intl wrapper + const formikWrapper = wrapper.dive(); + // Dive to get past the anonymous component. + const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep); + expect(joinFlowWrapper).toHaveLength(1); + expect(joinFlowWrapper.props().description).toBe('registration.emailStepDescription'); + expect(joinFlowWrapper.props().footerContent.props.id).toBe('registration.acceptTermsOfUse'); + expect(joinFlowWrapper.props().headerImgSrc).toBe('/images/join-flow/email-header.png'); + expect(joinFlowWrapper.props().innerClassName).toBe('join-flow-inner-email-step'); + expect(joinFlowWrapper.props().nextButton).toBe('registration.createAccount'); + expect(joinFlowWrapper.props().title).toBe('registration.emailStepTitle'); + expect(joinFlowWrapper.props().waiting).toBe(true); + }); + + test('props sent to FormikInput for email', () => { + const wrapper = shallowWithIntl(); + // Dive to get past the intl wrapper + const formikWrapper = wrapper.dive(); + // Dive to get past the anonymous component. + const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep); + expect(joinFlowWrapper).toHaveLength(1); + const emailInputWrapper = joinFlowWrapper.find(FormikInput).first(); + expect(emailInputWrapper.props().id).toEqual('email'); + expect(emailInputWrapper.props().error).toBeUndefined(); + expect(emailInputWrapper.props().name).toEqual('email'); + expect(emailInputWrapper.props().placeholder).toEqual('general.emailAddress'); + expect(emailInputWrapper.props().validationClassName).toEqual('validation-full-width-input'); + expect(emailInputWrapper.props().onSetRef).toEqual(formikWrapper.instance().handleSetEmailRef); + expect(emailInputWrapper.props().validate).toEqual(formikWrapper.instance().validateEmail); + }); + test('props sent to FormikCheckbox for subscribe', () => { + const wrapper = shallowWithIntl(); + // Dive to get past the intl wrapper + const formikWrapper = wrapper.dive(); + // Dive to get past the anonymous component. + const joinFlowWrapper = formikWrapper.dive().find(JoinFlowStep); + expect(joinFlowWrapper).toHaveLength(1); + const checkboxWrapper = joinFlowWrapper.find(FormikCheckbox).first(); + expect(checkboxWrapper).toHaveLength(1); + expect(checkboxWrapper.first().props().id).toEqual('subscribeCheckbox'); + expect(checkboxWrapper.first().props().label).toEqual('registration.receiveEmails'); + expect(checkboxWrapper.first().props().name).toEqual('subscribe'); + }); + test('handleValidSubmit passes formData to next step', () => { + const formikBag = { + setSubmitting: jest.fn() + }; + global.grecaptcha = { + execute: jest.fn(), + render: jest.fn() + }; + const formData = {item1: 'thing', item2: 'otherthing'}; + const wrapper = shallowWithIntl( + ); + + const formikWrapper = wrapper.dive(); + formikWrapper.instance().onCaptchaLoad(); // to setup catpcha state + formikWrapper.instance().handleValidSubmit(formData, formikBag); + + expect(formikBag.setSubmitting).toHaveBeenCalledWith(false); + expect(global.grecaptcha.execute).toHaveBeenCalled(); + }); + test('captchaSolved sets token and goes to next step', () => { + const props = { + onNextStep: jest.fn() + }; + const formikBag = { + setSubmitting: jest.fn() + }; + global.grecaptcha = { + execute: jest.fn(), + render: jest.fn() + }; + const formData = {item1: 'thing', item2: 'otherthing'}; + const wrapper = shallowWithIntl( + ); + + const formikWrapper = wrapper.dive(); + // Call these to setup captcha. + formikWrapper.instance().onCaptchaLoad(); // to setup catpcha state + formikWrapper.instance().handleValidSubmit(formData, formikBag); + + const captchaToken = 'abcd'; + formikWrapper.instance().captchaSolved(captchaToken); + // Make sure captchaSolved calls onNextStep with formData that has + // a captcha token and left everything else in the object in place. + expect(props.onNextStep).toHaveBeenCalledWith( + expect.objectContaining({ + 'item1': formData.item1, + 'item2': formData.item2, + 'g-recaptcha-response': captchaToken + })); + expect(formikBag.setSubmitting).toHaveBeenCalledWith(true); + }); + test('validateEmail test email empty', () => { + const wrapper = shallowWithIntl( + ); + const formikWrapper = wrapper.dive(); + const val = formikWrapper.instance().validateEmail(''); + expect(val).toBe('general.required'); + }); + test('validateEmail test email null', () => { + const wrapper = shallowWithIntl( + ); + const formikWrapper = wrapper.dive(); + const val = formikWrapper.instance().validateEmail(null); + expect(val).toBe('general.required'); + }); + test('validateEmail test email undefined', () => { + const wrapper = shallowWithIntl( + ); + const formikWrapper = wrapper.dive(); + const val = formikWrapper.instance().validateEmail(); + expect(val).toBe('general.required'); + }); +}); diff --git a/test/unit/components/join-flow.test.jsx b/test/unit/components/join-flow.test.jsx new file mode 100644 index 000000000..80dfe5a14 --- /dev/null +++ b/test/unit/components/join-flow.test.jsx @@ -0,0 +1,165 @@ +import React from 'react'; +const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx'); +import configureStore from 'redux-mock-store'; +import JoinFlow from '../../../src/components/join-flow/join-flow'; +import Progression from '../../../src/components/progression/progression.jsx'; +import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step'; + +describe('JoinFlow', () => { + const mockStore = configureStore(); + let store; + + beforeEach(() => { + store = mockStore({sessionActions: { + refreshSession: jest.fn() + }}); + }); + + const getJoinFlowWrapper = props => { + const wrapper = shallowWithIntl( + + , {context: {store}} + ); + return wrapper + .dive() // unwrap redux connect(injectIntl(JoinFlow)) + .dive(); // unwrap injectIntl(JoinFlow) + }; + + test('handleRegistrationResponse with successful response', () => { + const props = { + refreshSession: jest.fn() + }; + const joinFlowInstance = getJoinFlowWrapper(props).instance(); + const responseErr = null; + const responseBody = [ + { + success: true + } + ]; + const responseObj = { + statusCode: 200 + }; + joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); + expect(joinFlowInstance.props.refreshSession).toHaveBeenCalled(); + expect(joinFlowInstance.state.registrationError).toBe(null); + }); + + test('handleRegistrationResponse with healthy response, indicating failure', () => { + const props = { + refreshSession: jest.fn() + }; + const joinFlowInstance = getJoinFlowWrapper(props).instance(); + const responseErr = null; + const responseBody = [ + { + msg: 'This field is required.', + errors: { + username: ['This field is required.'] + }, + success: false + } + ]; + const responseObj = { + statusCode: 200 + }; + joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); + expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled(); + expect(joinFlowInstance.state.registrationError).toBe('username: This field is required.'); + }); + + test('handleRegistrationResponse with failure response, with error fields missing', () => { + const props = { + refreshSession: jest.fn() + }; + const joinFlowInstance = getJoinFlowWrapper(props).instance(); + const responseErr = null; + const responseBody = [ + { + msg: 'This field is required.', + success: false + } + ]; + const responseObj = { + statusCode: 200 + }; + joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); + expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled(); + expect(joinFlowInstance.state.registrationError).toBe('This field is required.'); + }); + + test('handleRegistrationResponse with failure response, with no text explanation', () => { + const props = { + refreshSession: jest.fn() + }; + const joinFlowInstance = getJoinFlowWrapper(props).instance(); + const responseErr = null; + const responseBody = [ + { + success: false + } + ]; + const responseObj = { + statusCode: 200 + }; + joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); + expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled(); + expect(joinFlowInstance.state.registrationError).toBe('registration.generalError (200)'); + }); + + test('handleRegistrationResponse with failure status code', () => { + const props = { + refreshSession: jest.fn() + }; + const joinFlowInstance = getJoinFlowWrapper(props).instance(); + const responseErr = null; + const responseBody = [ + { + success: false + } + ]; + const responseObj = { + statusCode: 400 + }; + joinFlowInstance.handleRegistrationResponse(responseErr, responseBody, responseObj); + expect(joinFlowInstance.props.refreshSession).not.toHaveBeenCalled(); + expect(joinFlowInstance.state.registrationError).toBe('registration.generalError (400)'); + }); + + test('handleAdvanceStep', () => { + const joinFlowInstance = getJoinFlowWrapper().instance(); + joinFlowInstance.setState({formData: {username: 'ScratchCat123'}, step: 2}); + joinFlowInstance.handleAdvanceStep({email: 'scratchcat123@scratch.mit.edu'}); + expect(joinFlowInstance.state.formData.username).toBe('ScratchCat123'); + expect(joinFlowInstance.state.formData.email).toBe('scratchcat123@scratch.mit.edu'); + expect(joinFlowInstance.state.step).toBe(3); + }); + + test('when state.registrationError has error message, we show RegistrationErrorStep', () => { + const joinFlowWrapper = getJoinFlowWrapper(); + joinFlowWrapper.instance().setState({registrationError: 'halp there is a errors!!'}); + const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); + const progressionWrapper = joinFlowWrapper.find(Progression); + expect(registrationErrorWrapper).toHaveLength(1); + expect(progressionWrapper).toHaveLength(0); + }); + + test('when state.registrationError has null error message, we show Progression', () => { + const joinFlowWrapper = getJoinFlowWrapper(); + joinFlowWrapper.instance().setState({registrationError: null}); + const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); + const progressionWrapper = joinFlowWrapper.find(Progression); + expect(registrationErrorWrapper).toHaveLength(0); + expect(progressionWrapper).toHaveLength(1); + }); + + test('when state.registrationError has empty error message, we show Progression', () => { + const joinFlowWrapper = getJoinFlowWrapper(); + joinFlowWrapper.instance().setState({registrationError: ''}); + const registrationErrorWrapper = joinFlowWrapper.find(RegistrationErrorStep); + const progressionWrapper = joinFlowWrapper.find(Progression); + expect(registrationErrorWrapper).toHaveLength(0); + expect(progressionWrapper).toHaveLength(1); + }); +}); diff --git a/test/unit/components/registration-error-step.test.jsx b/test/unit/components/registration-error-step.test.jsx new file mode 100644 index 000000000..e87e9b88a --- /dev/null +++ b/test/unit/components/registration-error-step.test.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {shallowWithIntl} from '../../helpers/intl-helpers.jsx'; +import JoinFlowStep from '../../../src/components/join-flow/join-flow-step'; +import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step'; + +describe('RegistrationErrorStep', () => { + const onTryAgain = jest.fn(); + let wrapper; + + beforeEach(() => { + wrapper = shallowWithIntl( + + ); + }); + + test('shows JoinFlowStep with props', () => { + // Dive to get past the anonymous component. + const joinFlowStepWrapper = wrapper.dive().find(JoinFlowStep); + expect(joinFlowStepWrapper).toHaveLength(1); + expect(joinFlowStepWrapper.props().description).toBe('error message'); + expect(joinFlowStepWrapper.props().nextButton).toBe('general.tryAgain'); + }); + + test('when submitted, onTryAgain is called', () => { + // Dive to get past the anonymous component. + const joinFlowStepWrapper = wrapper.dive().find(JoinFlowStep); + joinFlowStepWrapper.props().onSubmit(new Event('event')); // eslint-disable-line no-undef + expect(onTryAgain).toHaveBeenCalled(); + }); +}); diff --git a/test/unit/lib/validate.test.js b/test/unit/lib/validate.test.js index 03922882f..a051e1faa 100644 --- a/test/unit/lib/validate.test.js +++ b/test/unit/lib/validate.test.js @@ -63,4 +63,52 @@ describe('unit test lib/validate.js', () => { response = validate.validatePasswordConfirm('', 'abcdefg'); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordConfirmNotEquals'}); }); + + test('validate email address locally', () => { + let response; + expect(typeof validate.validateEmailLocally).toBe('function'); + + // permitted addresses: + response = validate.validateEmailLocally('abc@def.com'); + expect(response).toEqual({valid: true}); + response = validate.validateEmailLocally('abcdefghijklmnopqrst@abcdefghijklmnopqrst.info'); + expect(response).toEqual({valid: true}); + response = validate.validateEmailLocally('abc-def-ghi@jkl-mno.org'); + expect(response).toEqual({valid: true}); + response = validate.validateEmailLocally('_______@example.com'); + expect(response).toEqual({valid: true}); + response = validate.validateEmailLocally('email@example.museum'); + expect(response).toEqual({valid: true}); + response = validate.validateEmailLocally('email@example.co.jp'); + expect(response).toEqual({valid: true}); + + // non-permitted addresses: + response = validate.validateEmailLocally(''); + expect(response).toEqual({valid: false, errMsgId: 'general.required'}); + response = validate.validateEmailLocally('a'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('abc@def'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('abc@def.c'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('abcπŸ˜„def@emoji.pizza'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('γ‚γ„γ†γˆγŠ@example.com'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('Abc..123@example.com'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('Joe Smith '); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('email@example@example.com'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('email@example..com'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + + // edge cases: + // these are strictly legal according to email addres spec, but rejected by library we use: + response = validate.validateEmailLocally('email@123.123.123.123'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + response = validate.validateEmailLocally('much."more unusual"@example.com'); + expect(response).toEqual({valid: false, errMsgId: 'registration.validationEmailInvalid'}); + }); }); diff --git a/webpack.config.js b/webpack.config.js index 3118a4bb5..a2fe4fcbc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -173,6 +173,8 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"' + (process.env.NODE_ENV || 'development') + '"', 'process.env.API_HOST': '"' + (process.env.API_HOST || 'https://api.scratch.mit.edu') + '"', + 'process.env.RECAPTCHA_SITE_KEY': '"' + + (process.env.RECAPTCHA_SITE_KEY || '6Lf6kK4UAAAAABKTyvdSqgcSVASEnMrCquiAkjVW') + '"', 'process.env.ASSET_HOST': '"' + (process.env.ASSET_HOST || 'https://assets.scratch.mit.edu') + '"', 'process.env.BACKPACK_HOST': '"' + (process.env.BACKPACK_HOST || 'https://backpack.scratch.mit.edu') + '"', 'process.env.CLOUDDATA_HOST': '"' + (process.env.CLOUDDATA_HOST || 'clouddata.scratch.mit.edu') + '"',