Browse Source

Added nightwatch.js test suite for confirming that the app loads correctly and can run operations from each module. Currently only support the latest version of Chrome.

n1474335 6 years ago
parent
commit
b0fb9db4b8
8 changed files with 910 additions and 4 deletions
  1. 1 1
      .editorconfig
  2. 9 0
      .eslintrc.json
  3. 1 0
      .gitignore
  4. 10 3
      Gruntfile.js
  5. 27 0
      nightwatch.json
  6. 678 0
      package-lock.json
  7. 3 0
      package.json
  8. 181 0
      tests/browser/nightwatch.js

+ 1 - 1
.editorconfig

@@ -9,6 +9,6 @@ trim_trailing_whitespace = true
 indent_style = space
 indent_size = 4
 
-[{package.json,.travis.yml}]
+[{package.json,.travis.yml,nightwatch.json}]
 indent_style = space
 indent_size = 2

+ 9 - 0
.eslintrc.json

@@ -87,6 +87,15 @@
         "no-var": "error",
         "prefer-const": "error"
     },
+    "overrides": [
+        {
+            "files": "tests/**/*",
+            "rules": {
+                "no-unused-expressions": "off",
+                "no-console": "off"
+            }
+        }
+    ],
     "globals": {
         "$": false,
         "jQuery": false,

+ 1 - 0
.gitignore

@@ -9,4 +9,5 @@ docs/*
 src/core/config/modules/*
 src/core/config/OperationConfig.json
 src/core/operations/index.mjs
+tests/browser/output/*
 

+ 10 - 3
Gruntfile.js

@@ -30,8 +30,12 @@ module.exports = function (grunt) {
         ["clean:node", "clean:config", "exec:generateConfig", "webpack:node", "chmod:build"]);
 
     grunt.registerTask("test",
-        "A task which runs all the tests in the tests directory.",
-        ["exec:generateConfig", "exec:tests"]);
+        "A task which runs all the operation tests in the tests directory.",
+        ["exec:generateConfig", "exec:opTests"]);
+
+    grunt.registerTask("testui",
+        "A task which runs all the UI tests in the tests directory. Requires the dev server to be running.",
+        ["exec:browserTests"]);
 
     grunt.registerTask("docs",
         "Compiles documentation in the /docs directory.",
@@ -386,8 +390,11 @@ module.exports = function (grunt) {
                     "echo '--- Config scripts finished. ---\n'"
                 ].join(";")
             },
-            tests: {
+            opTests: {
                 command: "node --experimental-modules --no-warnings --no-deprecation tests/operations/index.mjs"
+            },
+            browserTests: {
+                command: "./node_modules/.bin/nightwatch --env chrome"
             }
         },
     });

+ 27 - 0
nightwatch.json

@@ -0,0 +1,27 @@
+{
+  "src_folders": ["tests/browser"],
+  "output_folder": "tests/browser/output",
+
+  "test_settings": {
+
+    "default": {
+      "launch_url": "http://localhost:8080",
+      "webdriver": {
+        "start_process": true,
+        "log_path": false
+      }
+    },
+
+    "chrome": {
+      "webdriver": {
+        "server_path": "./node_modules/.bin/chromedriver",
+        "port": 9515
+      },
+      "desiredCapabilities": {
+        "browserName": "chrome"
+      }
+    }
+
+  }
+}
+

+ 678 - 0
package-lock.json

@@ -1440,6 +1440,26 @@
       "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==",
       "dev": true
     },
+    "agent-base": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
+      "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
+      "dev": true,
+      "requires": {
+        "es6-promisify": "^5.0.0"
+      },
+      "dependencies": {
+        "es6-promisify": {
+          "version": "5.0.0",
+          "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+          "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+          "dev": true,
+          "requires": {
+            "es6-promise": "^4.0.3"
+          }
+        }
+      }
+    },
     "ajv": {
       "version": "6.5.5",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
@@ -1695,12 +1715,24 @@
       "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
       "dev": true
     },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
     "assign-symbols": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
       "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
       "dev": true
     },
+    "ast-types": {
+      "version": "0.11.7",
+      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.11.7.tgz",
+      "integrity": "sha512-2mP3TwtkY/aTv5X3ZsMpNAbOnyoC/aMJwJSoaELPkHId0nSQgFcnU4dRW3isxiz7+zBexk0ym3WNVjMiQBnJSw==",
+      "dev": true
+    },
     "async": {
       "version": "2.6.1",
       "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
@@ -2182,6 +2214,13 @@
       "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==",
       "dev": true
     },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true,
+      "optional": true
+    },
     "browserify-aes": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -2447,6 +2486,24 @@
         "underscore-contrib": "~0.3.0"
       }
     },
+    "chai-nightwatch": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/chai-nightwatch/-/chai-nightwatch-0.2.1.tgz",
+      "integrity": "sha512-2lprSMi72sHq2ZGyPTYUDQNsd2O4z81SicascbI4bkU54Xzk5Ofunn2CbrExADGC7jBH2D8r66X/aSEl+/agXQ==",
+      "dev": true,
+      "requires": {
+        "assertion-error": "1.0.0",
+        "deep-eql": "0.1.3"
+      },
+      "dependencies": {
+        "assertion-error": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz",
+          "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=",
+          "dev": true
+        }
+      }
+    },
     "chalk": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
@@ -2515,6 +2572,19 @@
         "tslib": "^1.9.0"
       }
     },
+    "chromedriver": {
+      "version": "2.45.0",
+      "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-2.45.0.tgz",
+      "integrity": "sha512-Qwmcr+2mU3INeR6mVsQ8gO00vZpL8ZeTJLclX44C0dcs88jrSDgckPqbG+qkVX+m2L/aOPnF0lYgPdOiOiLt5w==",
+      "dev": true,
+      "requires": {
+        "del": "^3.0.0",
+        "extract-zip": "^1.6.7",
+        "mkdirp": "^0.5.1",
+        "request": "^2.88.0",
+        "tcp-port-used": "^1.0.1"
+      }
+    },
     "cipher-base": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
@@ -2638,6 +2708,12 @@
         "shallow-clone": "^1.0.0"
       }
     },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+      "dev": true
+    },
     "code-point-at": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -3170,6 +3246,12 @@
         "assert-plus": "^1.0.0"
       }
     },
+    "data-uri-to-buffer": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz",
+      "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==",
+      "dev": true
+    },
     "data-urls": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz",
@@ -3241,6 +3323,15 @@
       "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
       "dev": true
     },
+    "deep-eql": {
+      "version": "0.1.3",
+      "resolved": "http://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
+      "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=",
+      "dev": true,
+      "requires": {
+        "type-detect": "0.1.1"
+      }
+    },
     "deep-equal": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
@@ -3326,6 +3417,25 @@
         }
       }
     },
+    "degenerator": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz",
+      "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=",
+      "dev": true,
+      "requires": {
+        "ast-types": "0.x.x",
+        "escodegen": "1.x.x",
+        "esprima": "3.x.x"
+      },
+      "dependencies": {
+        "esprima": {
+          "version": "3.1.3",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
+          "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
+          "dev": true
+        }
+      }
+    },
     "del": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
@@ -4478,6 +4588,12 @@
       "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz",
       "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw=="
     },
+    "file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+      "dev": true
+    },
     "filesize": {
       "version": "3.6.1",
       "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz",
@@ -5313,6 +5429,24 @@
         "rimraf": "2"
       }
     },
+    "ftp": {
+      "version": "0.3.10",
+      "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz",
+      "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "1.1.x",
+        "xregexp": "2.0.0"
+      },
+      "dependencies": {
+        "xregexp": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz",
+          "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=",
+          "dev": true
+        }
+      }
+    },
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -5395,6 +5529,52 @@
       "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
       "dev": true
     },
+    "get-uri": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.2.tgz",
+      "integrity": "sha512-ZD325dMZOgerGqF/rF6vZXyFGTAay62svjQIT+X/oU2PtxYpFxvSkbsdi+oxIrsNxlZVd4y8wUDqkaExWTI/Cw==",
+      "dev": true,
+      "requires": {
+        "data-uri-to-buffer": "1",
+        "debug": "2",
+        "extend": "3",
+        "file-uri-to-path": "1",
+        "ftp": "~0.3.10",
+        "readable-stream": "2"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
+          "dev": true
+        },
+        "readable-stream": {
+          "version": "2.3.6",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+          "dev": true,
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.3",
+            "isarray": "~1.0.0",
+            "process-nextick-args": "~2.0.0",
+            "safe-buffer": "~5.1.1",
+            "string_decoder": "~1.1.1",
+            "util-deprecate": "~1.0.1"
+          }
+        },
+        "string_decoder": {
+          "version": "1.1.1",
+          "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+          "dev": true,
+          "requires": {
+            "safe-buffer": "~5.1.0"
+          }
+        }
+      }
+    },
     "get-value": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
@@ -5510,6 +5690,13 @@
       "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
       "dev": true
     },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+      "dev": true,
+      "optional": true
+    },
     "grunt": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.3.tgz",
@@ -6179,6 +6366,27 @@
         "requires-port": "^1.0.0"
       }
     },
+    "http-proxy-agent": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
+      "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
+      "dev": true,
+      "requires": {
+        "agent-base": "4",
+        "debug": "3.1.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
     "http-proxy-middleware": {
       "version": "0.18.0",
       "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
@@ -6208,6 +6416,33 @@
       "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
       "dev": true
     },
+    "https-proxy-agent": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
+      "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
+      "dev": true,
+      "requires": {
+        "agent-base": "^4.1.0",
+        "debug": "^3.1.0"
+      },
+      "dependencies": {
+        "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"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        }
+      }
+    },
     "i": {
       "version": "0.3.6",
       "resolved": "https://registry.npmjs.org/i/-/i-0.3.6.tgz",
@@ -6831,6 +7066,12 @@
       "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
       "dev": true
     },
+    "is-url": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+      "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
+      "dev": true
+    },
     "is-utf8": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@@ -6849,6 +7090,17 @@
       "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=",
       "dev": true
     },
+    "is2": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz",
+      "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==",
+      "dev": true,
+      "requires": {
+        "deep-is": "^0.1.3",
+        "ip-regex": "^2.1.0",
+        "is-url": "^1.2.2"
+      }
+    },
     "isarray": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@@ -7463,12 +7715,89 @@
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
       "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
     },
+    "lodash._arraycopy": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz",
+      "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=",
+      "dev": true
+    },
+    "lodash._arrayeach": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz",
+      "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=",
+      "dev": true
+    },
+    "lodash._baseassign": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
+      "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
+      "dev": true,
+      "requires": {
+        "lodash._basecopy": "^3.0.0",
+        "lodash.keys": "^3.0.0"
+      }
+    },
+    "lodash._baseclone": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz",
+      "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=",
+      "dev": true,
+      "requires": {
+        "lodash._arraycopy": "^3.0.0",
+        "lodash._arrayeach": "^3.0.0",
+        "lodash._baseassign": "^3.0.0",
+        "lodash._basefor": "^3.0.0",
+        "lodash.isarray": "^3.0.0",
+        "lodash.keys": "^3.0.0"
+      }
+    },
+    "lodash._basecopy": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
+      "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=",
+      "dev": true
+    },
+    "lodash._basefor": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz",
+      "integrity": "sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI=",
+      "dev": true
+    },
+    "lodash._bindcallback": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz",
+      "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=",
+      "dev": true
+    },
+    "lodash._getnative": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+      "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=",
+      "dev": true
+    },
+    "lodash._isiterateecall": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz",
+      "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=",
+      "dev": true
+    },
     "lodash.assign": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
       "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
       "dev": true
     },
+    "lodash.clone": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-3.0.3.tgz",
+      "integrity": "sha1-hGiMc9MrWpDKJWFpY/GJJSqZcEM=",
+      "dev": true,
+      "requires": {
+        "lodash._baseclone": "^3.0.0",
+        "lodash._bindcallback": "^3.0.0",
+        "lodash._isiterateecall": "^3.0.0"
+      }
+    },
     "lodash.clonedeep": {
       "version": "4.5.0",
       "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
@@ -7481,6 +7810,12 @@
       "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
       "dev": true
     },
+    "lodash.defaultsdeep": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz",
+      "integrity": "sha1-vsECT4WxvZbL6kBbI8FK1kQ6b4E=",
+      "dev": true
+    },
     "lodash.escaperegexp": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
@@ -7492,6 +7827,18 @@
       "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
       "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
     },
+    "lodash.isarguments": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+      "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=",
+      "dev": true
+    },
+    "lodash.isarray": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
+      "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=",
+      "dev": true
+    },
     "lodash.isboolean": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
@@ -7517,6 +7864,23 @@
       "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
       "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
     },
+    "lodash.keys": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
+      "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
+      "dev": true,
+      "requires": {
+        "lodash._getnative": "^3.0.0",
+        "lodash.isarguments": "^3.0.0",
+        "lodash.isarray": "^3.0.0"
+      }
+    },
+    "lodash.merge": {
+      "version": "4.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
+      "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==",
+      "dev": true
+    },
     "lodash.mergewith": {
       "version": "4.6.1",
       "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
@@ -7909,6 +8273,83 @@
         }
       }
     },
+    "mkpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/mkpath/-/mkpath-1.0.0.tgz",
+      "integrity": "sha1-67Opd+evHGg65v2hK1Raa6bFhT0=",
+      "dev": true
+    },
+    "mocha": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+      "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "browser-stdout": "1.3.1",
+        "commander": "2.15.1",
+        "debug": "3.1.0",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "glob": "7.1.2",
+        "growl": "1.10.5",
+        "he": "1.1.1",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "supports-color": "5.4.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.15.1",
+          "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+          "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+          "dev": true,
+          "optional": true
+        },
+        "debug": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+          "dev": true,
+          "optional": 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"
+          }
+        },
+        "he": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+          "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+          "dev": true,
+          "optional": true
+        },
+        "supports-color": {
+          "version": "5.4.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
+          "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
     "moment": {
       "version": "2.22.2",
       "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
@@ -8032,6 +8473,12 @@
       "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==",
       "dev": true
     },
+    "netmask": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
+      "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=",
+      "dev": true
+    },
     "ngeohash": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/ngeohash/-/ngeohash-0.6.0.tgz",
@@ -8043,6 +8490,36 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
       "dev": true
     },
+    "nightwatch": {
+      "version": "1.0.17",
+      "resolved": "https://registry.npmjs.org/nightwatch/-/nightwatch-1.0.17.tgz",
+      "integrity": "sha512-/du74poqA8JNKkgpo00sRxomfKwVtkgkIQ9S66VWK5AWsRRYYzh0wmCMMFjZm2EfagECymP395xwUZ+BW1K9qg==",
+      "dev": true,
+      "requires": {
+        "assertion-error": "^1.1.0",
+        "chai-nightwatch": "0.2.1",
+        "ejs": "^2.5.9",
+        "lodash.clone": "^3.0.3",
+        "lodash.defaultsdeep": "^4.6.0",
+        "lodash.merge": "^4.6.1",
+        "minimatch": "3.0.3",
+        "mkpath": "1.0.0",
+        "mocha": "^5.1.1",
+        "optimist": "^0.6.1",
+        "proxy-agent": "^3.0.0"
+      },
+      "dependencies": {
+        "minimatch": {
+          "version": "3.0.3",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz",
+          "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.0.0"
+          }
+        }
+      }
+    },
     "no-case": {
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
@@ -8458,6 +8935,30 @@
         "is-wsl": "^1.1.0"
       }
     },
+    "optimist": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "dev": true,
+      "requires": {
+        "minimist": "~0.0.1",
+        "wordwrap": "~0.0.2"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "0.0.10",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+          "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
+          "dev": true
+        },
+        "wordwrap": {
+          "version": "0.0.3",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+          "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
+          "dev": true
+        }
+      }
+    },
     "optionator": {
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
@@ -8573,6 +9074,79 @@
       "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
       "dev": true
     },
+    "pac-proxy-agent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.0.tgz",
+      "integrity": "sha512-AOUX9jES/EkQX2zRz0AW7lSx9jD//hQS8wFXBvcnd/J2Py9KaMJMqV/LPqJssj1tgGufotb2mmopGPR15ODv1Q==",
+      "dev": true,
+      "requires": {
+        "agent-base": "^4.2.0",
+        "debug": "^3.1.0",
+        "get-uri": "^2.0.0",
+        "http-proxy-agent": "^2.1.0",
+        "https-proxy-agent": "^2.2.1",
+        "pac-resolver": "^3.0.0",
+        "raw-body": "^2.2.0",
+        "socks-proxy-agent": "^4.0.1"
+      },
+      "dependencies": {
+        "bytes": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+          "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+          "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"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.23",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
+          "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
+          "dev": true,
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        },
+        "raw-body": {
+          "version": "2.3.3",
+          "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
+          "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+          "dev": true,
+          "requires": {
+            "bytes": "3.0.0",
+            "http-errors": "1.6.3",
+            "iconv-lite": "0.4.23",
+            "unpipe": "1.0.0"
+          }
+        }
+      }
+    },
+    "pac-resolver": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz",
+      "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==",
+      "dev": true,
+      "requires": {
+        "co": "^4.6.0",
+        "degenerator": "^1.0.4",
+        "ip": "^1.1.5",
+        "netmask": "^1.0.6",
+        "thunkify": "^2.1.2"
+      }
+    },
     "pad-stream": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/pad-stream/-/pad-stream-1.2.0.tgz",
@@ -9427,6 +10001,45 @@
         "ipaddr.js": "1.8.0"
       }
     },
+    "proxy-agent": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.0.3.tgz",
+      "integrity": "sha512-PXVVVuH9tiQuxQltFJVSnXWuDtNr+8aNBP6XVDDCDiUuDN8eRCm+ii4/mFWmXWEA0w8jjJSlePa4LXlM4jIzNA==",
+      "dev": true,
+      "requires": {
+        "agent-base": "^4.2.0",
+        "debug": "^3.1.0",
+        "http-proxy-agent": "^2.1.0",
+        "https-proxy-agent": "^2.2.1",
+        "lru-cache": "^4.1.2",
+        "pac-proxy-agent": "^3.0.0",
+        "proxy-from-env": "^1.0.0",
+        "socks-proxy-agent": "^4.0.1"
+      },
+      "dependencies": {
+        "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"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        }
+      }
+    },
+    "proxy-from-env": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
+      "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=",
+      "dev": true
+    },
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -10510,6 +11123,12 @@
         "is-fullwidth-code-point": "^2.0.0"
       }
     },
+    "smart-buffer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.1.tgz",
+      "integrity": "sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg==",
+      "dev": true
+    },
     "snackbarjs": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/snackbarjs/-/snackbarjs-1.1.0.tgz",
@@ -10678,6 +11297,26 @@
         }
       }
     },
+    "socks": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.2.tgz",
+      "integrity": "sha512-g6wjBnnMOZpE0ym6e0uHSddz9p3a+WsBaaYQaBaSCJYvrC4IXykQR9MNGjLQf38e9iIIhp3b1/Zk8YZI3KGJ0Q==",
+      "dev": true,
+      "requires": {
+        "ip": "^1.1.5",
+        "smart-buffer": "^4.0.1"
+      }
+    },
+    "socks-proxy-agent": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz",
+      "integrity": "sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw==",
+      "dev": true,
+      "requires": {
+        "agent-base": "~4.2.0",
+        "socks": "~2.2.0"
+      }
+    },
     "sortablejs": {
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.7.0.tgz",
@@ -11237,6 +11876,33 @@
         "inherits": "2"
       }
     },
+    "tcp-port-used": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz",
+      "integrity": "sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==",
+      "dev": true,
+      "requires": {
+        "debug": "4.1.0",
+        "is2": "2.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
+          "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "ms": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+          "dev": true
+        }
+      }
+    },
     "text-table": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -11302,6 +11968,12 @@
         }
       }
     },
+    "thunkify": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz",
+      "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=",
+      "dev": true
+    },
     "thunky": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.3.tgz",
@@ -11532,6 +12204,12 @@
         "prelude-ls": "~1.1.2"
       }
     },
+    "type-detect": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz",
+      "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=",
+      "dev": true
+    },
     "type-is": {
       "version": "1.6.16",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",

+ 3 - 0
package.json

@@ -35,6 +35,7 @@
     "autoprefixer": "^9.3.1",
     "babel-loader": "^8.0.4",
     "bootstrap": "^4.1.3",
+    "chromedriver": "^2.45.0",
     "colors": "^1.3.2",
     "css-loader": "^1.0.1",
     "eslint": "^5.8.0",
@@ -56,6 +57,7 @@
     "imports-loader": "^0.8.0",
     "ink-docstrap": "^1.3.2",
     "jsdoc-babel": "^0.5.0",
+    "nightwatch": "^1.0.17",
     "node-sass": "^4.10.0",
     "postcss-css-variables": "^0.11.0",
     "postcss-import": "^12.0.1",
@@ -133,6 +135,7 @@
     "start": "grunt dev",
     "build": "grunt prod",
     "test": "grunt test",
+    "testui": "grunt testui",
     "docs": "grunt docs",
     "lint": "grunt lint",
     "newop": "node --experimental-modules src/core/config/scripts/newOperation.mjs"

+ 181 - 0
tests/browser/nightwatch.js

@@ -0,0 +1,181 @@
+/**
+ * Tests to ensure that the app loads correctly in a reasonable time and that operations can br run.
+ *
+ * @author n1474335 [n1474335@gmail.com]
+ * @copyright Crown Copyright 2018
+ * @license Apache-2.0
+ */
+module.exports = {
+    before: function (browser) {
+        console.log("The dev server much be running on http://localhost:8080 for these tests to work.")
+        browser
+            .resizeWindow(1280, 800)
+            .url(browser.launchUrl);
+    },
+
+    "Loading screen": browser => {
+        // Check that the loading screen appears and then disappears within a reasonable time
+        browser
+            .waitForElementVisible("#preloader", 300)
+            .waitForElementNotPresent("#preloader", 10000);
+    },
+
+    "App loaded": browser => {
+        browser.useCss();
+        // Check that various important elements are loaded
+        browser.expect.element("#operations").to.be.visible;
+        browser.expect.element("#recipe").to.be.visible;
+        browser.expect.element("#input").to.be.present;
+        browser.expect.element("#output").to.be.present;
+        browser.expect.element(".op-list").to.be.present;
+        browser.expect.element("#rec-list").to.be.visible;
+        browser.expect.element("#controls").to.be.visible;
+        browser.expect.element("#input-text").to.be.visible;
+        browser.expect.element("#output-text").to.be.visible;
+    },
+
+    "Operations loaded": browser => {
+        browser.useXpath();
+        // Check an operation in every category
+        browser.expect.element("//li[contains(@class, 'operation') and text()='To Base64']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='To Binary']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='AES Decrypt']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='PEM to Hex']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Power Set']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Parse IP range']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Remove Diacritics']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Sort']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='To UNIX Timestamp']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Extract dates']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Gzip']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Keccak']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='JSON Beautify']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Detect File Type']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Play Media']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Disassemble x86']").to.be.present;
+        browser.expect.element("//li[contains(@class, 'operation') and text()='Register']").to.be.present;
+    },
+
+    "Recipe can be run": browser => {
+        const toHex = "//li[contains(@class, 'operation') and text()='To Hex']";
+        const op = "#rec-list .operation .op-title";
+
+        // Check that operation is visible
+        browser
+            .useXpath()
+            .expect.element(toHex).to.be.visible;
+
+        // Add it to the recipe by double clicking
+        browser
+            .useXpath()
+            .moveToElement(toHex, 10, 10)
+            .useCss()
+            .waitForElementVisible(".popover-body", 500)
+            .doubleClick()
+            .waitForElementVisible(op);
+
+        // Confirm that it has been added to the recipe
+        browser
+            .useCss()
+            .expect.element(op).text.to.contain("To Hex");
+
+        // Enter input
+        browser
+            .useCss()
+            .setValue("#input-text", "Don't Panic.");
+
+        // Check output
+        browser
+            .useCss()
+            .expect.element("#output-text").to.have.value.that.equals("44 6f 6e 27 74 20 50 61 6e 69 63 2e");
+
+        // Clear recipe
+        browser
+            .useCss()
+            .moveToElement(op, 10, 10)
+            .waitForElementNotPresent(".popover-body", 500)
+            .click("#clr-recipe")
+            .waitForElementNotPresent(op);
+    },
+
+    "Test every module": browser => {
+        browser.useCss();
+
+        // BSON
+        loadOp("BSON deserialise", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Ciphers
+        loadOp("AES Encrypt", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Code
+        loadOp("XPath expression", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Compression
+        loadOp("Gzip", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Crypto
+        loadOp("MD5", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Default
+        loadOp("Fork", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Diff
+        loadOp("Diff", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Encodings
+        loadOp("Encode text", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Image
+        loadOp("Extract EXIF", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // PGP
+        loadOp("PGP Encrypt", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // PublicKey
+        loadOp("Hex to PEM", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Regex
+        loadOp("Strings", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // Shellcode
+        loadOp("Disassemble x86", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // URL
+        loadOp("URL Encode", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+
+        // UserAgent
+        loadOp("Parse User Agent", browser)
+            .waitForElementNotVisible("#output-loader", 5000);
+    },
+
+    after: browser => {
+        browser.end();
+    }
+};
+
+/**
+ * Clears the current recipe and loads a new operation.
+ *
+ * @param {string} opName
+ * @param {Browser} browser
+ */
+function loadOp(opName, browser) {
+    return browser
+        .useCss()
+        .click("#clr-recipe")
+        .urlHash("op=" + opName);
+}