You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by sh...@apache.org on 2019/09/03 08:51:34 UTC

[incubator-echarts] branch master updated: Add tool to run all tests automatically. (#11154)

This is an automated email from the ASF dual-hosted git repository.

shenyi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git


The following commit(s) were added to refs/heads/master by this push:
     new 4af1779  Add tool to run all tests automatically. (#11154)
4af1779 is described below

commit 4af177937bac7e6e6429b17e27d9e5ab520d5e28
Author: Yi Shen <bm...@gmail.com>
AuthorDate: Tue Sep 3 16:51:27 2019 +0800

    Add tool to run all tests automatically. (#11154)
    
    * test: Run the tests automatically for visual regression test.
    
    * doc: Update CONTRIBUTING doc for test section
    
    * test: Improve visual regression test UI
    
    * test: Add tests select and run control in dashboard
    
    * test: Use child process to run test. Add logs in dashboard
    
    * test: Fix some logic issue of controlling test in dashboard.
    
    * test(autorun): Improve Dashboard UI
    
    * test: Remove puppeteer from devDependencies
    
    * Update package-lock.json
    
    * style: fix code style.
---
 .github/CONTRIBUTING.md           |   10 +-
 package-lock.json                 | 1504 +++++++++++++++++++------------------
 package.json                      |   11 +-
 test/runTest/blacklist.js         |    4 +
 test/runTest/cli.js               |  271 +++++++
 test/runTest/client/client.css    |  163 ++++
 test/runTest/client/client.js     |  182 +++++
 test/runTest/client/index.html    |  168 +++++
 test/runTest/compareScreenshot.js |   41 +
 test/runTest/config.js            |    4 +
 test/runTest/runtime/main.js      |   53 ++
 test/runTest/runtime/shim.js      |    9 +
 test/runTest/server.js            |  127 ++++
 test/runTest/store.js             |   78 ++
 test/runTest/util.js              |   66 ++
 15 files changed, 1951 insertions(+), 740 deletions(-)

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 96e432e..4577ff4 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -103,7 +103,15 @@ Then, open the test cases under `~/workspace/echarts/test` in Web browser. You c
 
 ![Chrome inspect](../asset/contributing-inspect.png)
 
-### 4. Make a pull request
+### 4. Run test
+
+```bash
+node test/runTest/cli.js
+```
+
+It will run all the test cases under `~/workspace/echarts/test` automatically to compare with the previous version. You can use this to check if your code bring some breaking change.
+
+### 5. Make a pull request
 
 Fork ECharts project into your own project. Checkout a branch from master branch named `fix-xxxx`, where xxxx is the issue id related. If there's no related issue, you need to create one in most cases to describe what's wrong or what new feature is required.
 
diff --git a/package-lock.json b/package-lock.json
index bbc437b..0c8483c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,16 @@
 {
   "name": "echarts",
-  "version": "4.2.1-rc.2",
+  "version": "4.2.1",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
     "@babel/code-frame": {
-      "version": "7.0.0-beta.31",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz",
-      "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==",
+      "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": {
-        "chalk": "^2.0.0",
-        "esutils": "^2.0.2",
-        "js-tokens": "^3.0.0"
+        "@babel/highlight": "7.5.0"
       }
     },
     "@babel/core": {
@@ -21,77 +19,68 @@
       "integrity": "sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA==",
       "dev": true,
       "requires": {
-        "@babel/code-frame": "^7.0.0",
-        "@babel/generator": "^7.3.4",
-        "@babel/helpers": "^7.2.0",
-        "@babel/parser": "^7.3.4",
-        "@babel/template": "^7.2.2",
-        "@babel/traverse": "^7.3.4",
-        "@babel/types": "^7.3.4",
-        "convert-source-map": "^1.1.0",
-        "debug": "^4.1.0",
-        "json5": "^2.1.0",
-        "lodash": "^4.17.11",
-        "resolve": "^1.3.2",
-        "semver": "^5.4.1",
-        "source-map": "^0.5.0"
+        "@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",
+        "convert-source-map": "1.6.0",
+        "debug": "4.1.1",
+        "json5": "2.1.0",
+        "lodash": "4.17.15",
+        "resolve": "1.12.0",
+        "semver": "5.7.1",
+        "source-map": "0.5.7"
       },
       "dependencies": {
-        "@babel/code-frame": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
-          "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
-          "dev": true,
-          "requires": {
-            "@babel/highlight": "^7.0.0"
-          }
-        },
         "@babel/template": {
-          "version": "7.2.2",
-          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
-          "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==",
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
+          "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
           "dev": true,
           "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "@babel/parser": "^7.2.2",
-            "@babel/types": "^7.2.2"
+            "@babel/code-frame": "7.5.5",
+            "@babel/parser": "7.5.5",
+            "@babel/types": "7.5.5"
           }
         },
         "@babel/types": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
-          "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+          "version": "7.5.5",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
+          "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
           "dev": true,
           "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.11",
-            "to-fast-properties": "^2.0.0"
+            "esutils": "2.0.3",
+            "lodash": "4.17.15",
+            "to-fast-properties": "2.0.0"
           }
         }
       }
     },
     "@babel/generator": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.4.tgz",
-      "integrity": "sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==",
+      "version": "7.5.5",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz",
+      "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.3.4",
-        "jsesc": "^2.5.1",
-        "lodash": "^4.17.11",
-        "source-map": "^0.5.0",
-        "trim-right": "^1.0.1"
+        "@babel/types": "7.5.5",
+        "jsesc": "2.5.2",
+        "lodash": "4.17.15",
+        "source-map": "0.5.7",
+        "trim-right": "1.0.1"
       },
       "dependencies": {
         "@babel/types": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
-          "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+          "version": "7.5.5",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
+          "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
           "dev": true,
           "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.11",
-            "to-fast-properties": "^2.0.0"
+            "esutils": "2.0.3",
+            "lodash": "4.17.15",
+            "to-fast-properties": "2.0.0"
           }
         }
       }
@@ -102,40 +91,31 @@
       "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==",
       "dev": true,
       "requires": {
-        "@babel/helper-get-function-arity": "^7.0.0",
-        "@babel/template": "^7.1.0",
-        "@babel/types": "^7.0.0"
+        "@babel/helper-get-function-arity": "7.0.0",
+        "@babel/template": "7.4.4",
+        "@babel/types": "7.5.5"
       },
       "dependencies": {
-        "@babel/code-frame": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
-          "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
-          "dev": true,
-          "requires": {
-            "@babel/highlight": "^7.0.0"
-          }
-        },
         "@babel/template": {
-          "version": "7.2.2",
-          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
-          "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==",
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
+          "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
           "dev": true,
           "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "@babel/parser": "^7.2.2",
-            "@babel/types": "^7.2.2"
+            "@babel/code-frame": "7.5.5",
+            "@babel/parser": "7.5.5",
+            "@babel/types": "7.5.5"
           }
         },
         "@babel/types": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
-          "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+          "version": "7.5.5",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
+          "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
           "dev": true,
           "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.11",
-            "to-fast-properties": "^2.0.0"
+            "esutils": "2.0.3",
+            "lodash": "4.17.15",
+            "to-fast-properties": "2.0.0"
           }
         }
       }
@@ -146,18 +126,18 @@
       "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.0.0"
+        "@babel/types": "7.5.5"
       },
       "dependencies": {
         "@babel/types": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
-          "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+          "version": "7.5.5",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
+          "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
           "dev": true,
           "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.11",
-            "to-fast-properties": "^2.0.0"
+            "esutils": "2.0.3",
+            "lodash": "4.17.15",
+            "to-fast-properties": "2.0.0"
           }
         }
       }
@@ -169,7 +149,7 @@
       "dev": true,
       "requires": {
         "@babel/types": "7.0.0-beta.31",
-        "lodash": "^4.2.0"
+        "lodash": "4.17.15"
       }
     },
     "@babel/helper-module-transforms": {
@@ -182,7 +162,7 @@
         "@babel/helper-simple-access": "7.0.0-beta.31",
         "@babel/template": "7.0.0-beta.31",
         "@babel/types": "7.0.0-beta.31",
-        "lodash": "^4.2.0"
+        "lodash": "4.17.15"
       }
     },
     "@babel/helper-simple-access": {
@@ -193,98 +173,81 @@
       "requires": {
         "@babel/template": "7.0.0-beta.31",
         "@babel/types": "7.0.0-beta.31",
-        "lodash": "^4.2.0"
+        "lodash": "4.17.15"
       }
     },
     "@babel/helper-split-export-declaration": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz",
-      "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==",
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
+      "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.0.0"
+        "@babel/types": "7.5.5"
       },
       "dependencies": {
         "@babel/types": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
-          "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+          "version": "7.5.5",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
+          "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
           "dev": true,
           "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.11",
-            "to-fast-properties": "^2.0.0"
+            "esutils": "2.0.3",
+            "lodash": "4.17.15",
+            "to-fast-properties": "2.0.0"
           }
         }
       }
     },
     "@babel/helpers": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.3.1.tgz",
-      "integrity": "sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==",
+      "version": "7.5.5",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.5.tgz",
+      "integrity": "sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g==",
       "dev": true,
       "requires": {
-        "@babel/template": "^7.1.2",
-        "@babel/traverse": "^7.1.5",
-        "@babel/types": "^7.3.0"
+        "@babel/template": "7.4.4",
+        "@babel/traverse": "7.5.5",
+        "@babel/types": "7.5.5"
       },
       "dependencies": {
-        "@babel/code-frame": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
-          "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
-          "dev": true,
-          "requires": {
-            "@babel/highlight": "^7.0.0"
-          }
-        },
         "@babel/template": {
-          "version": "7.2.2",
-          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
-          "integrity": "sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==",
+          "version": "7.4.4",
+          "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
+          "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
           "dev": true,
           "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "@babel/parser": "^7.2.2",
-            "@babel/types": "^7.2.2"
+            "@babel/code-frame": "7.5.5",
+            "@babel/parser": "7.5.5",
+            "@babel/types": "7.5.5"
           }
         },
         "@babel/types": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
-          "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+          "version": "7.5.5",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
+          "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
           "dev": true,
           "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.11",
-            "to-fast-properties": "^2.0.0"
+            "esutils": "2.0.3",
+            "lodash": "4.17.15",
+            "to-fast-properties": "2.0.0"
           }
         }
       }
     },
     "@babel/highlight": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
-      "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz",
+      "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==",
       "dev": true,
       "requires": {
-        "chalk": "^2.0.0",
-        "esutils": "^2.0.2",
-        "js-tokens": "^4.0.0"
-      },
-      "dependencies": {
-        "js-tokens": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
-          "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
-          "dev": true
-        }
+        "chalk": "2.4.2",
+        "esutils": "2.0.3",
+        "js-tokens": "4.0.0"
       }
     },
     "@babel/parser": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.4.tgz",
-      "integrity": "sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==",
+      "version": "7.5.5",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz",
+      "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==",
       "dev": true
     },
     "@babel/template": {
@@ -296,44 +259,54 @@
         "@babel/code-frame": "7.0.0-beta.31",
         "@babel/types": "7.0.0-beta.31",
         "babylon": "7.0.0-beta.31",
-        "lodash": "^4.2.0"
-      }
-    },
-    "@babel/traverse": {
-      "version": "7.3.4",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.3.4.tgz",
-      "integrity": "sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==",
-      "dev": true,
-      "requires": {
-        "@babel/code-frame": "^7.0.0",
-        "@babel/generator": "^7.3.4",
-        "@babel/helper-function-name": "^7.1.0",
-        "@babel/helper-split-export-declaration": "^7.0.0",
-        "@babel/parser": "^7.3.4",
-        "@babel/types": "^7.3.4",
-        "debug": "^4.1.0",
-        "globals": "^11.1.0",
-        "lodash": "^4.17.11"
+        "lodash": "4.17.15"
       },
       "dependencies": {
         "@babel/code-frame": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
-          "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+          "version": "7.0.0-beta.31",
+          "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz",
+          "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==",
           "dev": true,
           "requires": {
-            "@babel/highlight": "^7.0.0"
+            "chalk": "2.4.2",
+            "esutils": "2.0.3",
+            "js-tokens": "3.0.2"
           }
         },
+        "js-tokens": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+          "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+          "dev": true
+        }
+      }
+    },
+    "@babel/traverse": {
+      "version": "7.5.5",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz",
+      "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "7.5.5",
+        "@babel/generator": "7.5.5",
+        "@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",
+        "debug": "4.1.1",
+        "globals": "11.12.0",
+        "lodash": "4.17.15"
+      },
+      "dependencies": {
         "@babel/types": {
-          "version": "7.3.4",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz",
-          "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==",
+          "version": "7.5.5",
+          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
+          "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
           "dev": true,
           "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.11",
-            "to-fast-properties": "^2.0.0"
+            "esutils": "2.0.3",
+            "lodash": "4.17.15",
+            "to-fast-properties": "2.0.0"
           }
         }
       }
@@ -344,11 +317,50 @@
       "integrity": "sha512-exAHB+NeFGxkfQ5dSUD03xl3zYGneeSk2Mw2ldTt/nTvYxuDiuSp3DlxgUBgzbdTFG4fbwPk0WtKWOoTXCmNGg==",
       "dev": true,
       "requires": {
-        "esutils": "^2.0.2",
-        "lodash": "^4.2.0",
-        "to-fast-properties": "^2.0.0"
+        "esutils": "2.0.3",
+        "lodash": "4.17.15",
+        "to-fast-properties": "2.0.0"
       }
     },
+    "accepts": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+      "dev": true,
+      "requires": {
+        "mime-types": "2.1.24",
+        "negotiator": "0.6.2"
+      },
+      "dependencies": {
+        "mime-db": {
+          "version": "1.40.0",
+          "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+          "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
+          "dev": true
+        },
+        "mime-types": {
+          "version": "2.1.24",
+          "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+          "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+          "dev": true,
+          "requires": {
+            "mime-db": "1.40.0"
+          }
+        }
+      }
+    },
+    "acorn": {
+      "version": "5.7.3",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+      "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==",
+      "dev": true
+    },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
+      "dev": true
+    },
     "amdefine": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
@@ -362,9 +374,15 @@
       "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
       "dev": true,
       "requires": {
-        "color-convert": "^1.9.0"
+        "color-convert": "1.9.3"
       }
     },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
+      "dev": true
+    },
     "assert": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
@@ -374,25 +392,64 @@
         "util": "0.10.3"
       }
     },
+    "async-limiter": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
+      "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
+      "dev": true
+    },
     "babylon": {
       "version": "7.0.0-beta.31",
       "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz",
       "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==",
       "dev": true
     },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
+      "dev": true
+    },
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
       "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
       "dev": true
     },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
+      "dev": true
+    },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
+      "dev": true
+    },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "dev": true,
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
+    "blob": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
+      "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==",
+      "dev": true
+    },
     "brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
       "dev": true,
       "requires": {
-        "balanced-match": "^1.0.0",
+        "balanced-match": "1.0.0",
         "concat-map": "0.0.1"
       }
     },
@@ -419,15 +476,27 @@
       "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
       "dev": true
     },
+    "bytes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+      "dev": true
+    },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
+      "dev": true
+    },
     "chalk": {
       "version": "2.4.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
       "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
       "dev": true,
       "requires": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
+        "ansi-styles": "3.2.1",
+        "escape-string-regexp": "1.0.5",
+        "supports-color": "5.5.0"
       }
     },
     "color-convert": {
@@ -451,21 +520,51 @@
       "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
       "dev": true
     },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
+      "dev": true
+    },
+    "component-emitter": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
+      "dev": true
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
+      "dev": true
+    },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
       "dev": true
     },
+    "content-disposition": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+      "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
+      "dev": true
+    },
     "convert-source-map": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz",
       "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==",
       "dev": true,
       "requires": {
-        "safe-buffer": "~5.1.1"
+        "safe-buffer": "5.1.2"
       }
     },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+      "dev": true
+    },
     "coordtransform": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/coordtransform/-/coordtransform-2.0.2.tgz",
@@ -478,7 +577,7 @@
       "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
       "dev": true,
       "requires": {
-        "ms": "^2.1.1"
+        "ms": "2.1.2"
       }
     },
     "deep-is": {
@@ -487,6 +586,86 @@
       "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
       "dev": true
     },
+    "engine.io": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz",
+      "integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==",
+      "dev": true,
+      "requires": {
+        "accepts": "1.3.7",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "3.1.0",
+        "engine.io-parser": "2.1.3",
+        "ws": "6.1.4"
+      },
+      "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"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-client": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.2.tgz",
+      "integrity": "sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ==",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "3.1.0",
+        "engine.io-parser": "2.1.3",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "6.1.4",
+        "xmlhttprequest-ssl": "1.5.5",
+        "yeast": "0.1.2"
+      },
+      "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"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz",
+      "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==",
+      "dev": true,
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "0.0.7",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.5",
+        "has-binary2": "1.0.3"
+      }
+    },
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -499,11 +678,11 @@
       "integrity": "sha1-skaq6CnOc9WeLFVyc1nt0cEwqBs=",
       "dev": true,
       "requires": {
-        "esprima": "^2.7.1",
-        "estraverse": "^1.9.1",
-        "esutils": "^2.0.2",
-        "optionator": "^0.8.1",
-        "source-map": "~0.2.0"
+        "esprima": "2.7.2",
+        "estraverse": "1.9.3",
+        "esutils": "2.0.3",
+        "optionator": "0.8.2",
+        "source-map": "0.2.0"
       },
       "dependencies": {
         "estraverse": {
@@ -519,7 +698,7 @@
           "dev": true,
           "optional": true,
           "requires": {
-            "amdefine": ">=0.0.4"
+            "amdefine": "1.0.1"
           }
         }
       }
@@ -536,10 +715,16 @@
       "integrity": "sha1-9srKcokzqFDvkGYdDheYK6RxEaI=",
       "dev": true
     },
+    "estree-walker": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.5.2.tgz",
+      "integrity": "sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig==",
+      "dev": true
+    },
     "esutils": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
-      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
       "dev": true
     },
     "fast-levenshtein": {
@@ -548,17 +733,26 @@
       "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
       "dev": true
     },
+    "fast-url-parser": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
+      "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=",
+      "dev": true,
+      "requires": {
+        "punycode": "1.4.1"
+      }
+    },
     "fs-extra": {
       "version": "0.26.7",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz",
       "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=",
       "dev": true,
       "requires": {
-        "graceful-fs": "^4.1.2",
-        "jsonfile": "^2.1.0",
-        "klaw": "^1.0.0",
-        "path-is-absolute": "^1.0.0",
-        "rimraf": "^2.2.8"
+        "graceful-fs": "4.2.2",
+        "jsonfile": "2.4.0",
+        "klaw": "1.3.1",
+        "path-is-absolute": "1.0.1",
+        "rimraf": "2.7.1"
       }
     },
     "fs.realpath": {
@@ -573,23 +767,38 @@
       "integrity": "sha1-OyCjV//89GuzhK7W+K6aZH/basQ=",
       "dev": true,
       "requires": {
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "2 || 3",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
+        "inflight": "1.0.6",
+        "inherits": "2.0.1",
+        "minimatch": "3.0.4",
+        "once": "1.4.0",
+        "path-is-absolute": "1.0.1"
       }
     },
     "globals": {
-      "version": "11.11.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz",
-      "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==",
+      "version": "11.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
       "dev": true
     },
     "graceful-fs": {
-      "version": "4.1.15",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
-      "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==",
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz",
+      "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==",
+      "dev": true
+    },
+    "has-binary2": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+      "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+      "dev": true,
+      "requires": {
+        "isarray": "2.0.1"
+      }
+    },
+    "has-cors": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=",
       "dev": true
     },
     "has-flag": {
@@ -598,14 +807,20 @@
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
       "dev": true
     },
+    "indexof": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
+      "dev": true
+    },
     "inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
       "dev": true,
       "requires": {
-        "once": "^1.3.0",
-        "wrappy": "1"
+        "once": "1.4.0",
+        "wrappy": "1.0.2"
       }
     },
     "inherits": {
@@ -620,10 +835,16 @@
       "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
       "dev": true
     },
+    "isarray": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+      "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=",
+      "dev": true
+    },
     "js-tokens": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
-      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
       "dev": true
     },
     "jsesc": {
@@ -638,7 +859,7 @@
       "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==",
       "dev": true,
       "requires": {
-        "minimist": "^1.2.0"
+        "minimist": "1.2.0"
       }
     },
     "jsonfile": {
@@ -647,7 +868,7 @@
       "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
       "dev": true,
       "requires": {
-        "graceful-fs": "^4.1.6"
+        "graceful-fs": "4.2.2"
       }
     },
     "klaw": {
@@ -656,7 +877,7 @@
       "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
       "dev": true,
       "requires": {
-        "graceful-fs": "^4.1.9"
+        "graceful-fs": "4.2.2"
       }
     },
     "levn": {
@@ -665,23 +886,53 @@
       "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
       "dev": true,
       "requires": {
-        "prelude-ls": "~1.1.2",
-        "type-check": "~0.3.2"
+        "prelude-ls": "1.1.2",
+        "type-check": "0.3.2"
       }
     },
     "lodash": {
-      "version": "4.17.11",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+      "dev": true
+    },
+    "lolex": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.2.0.tgz",
+      "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==",
+      "dev": true
+    },
+    "magic-string": {
+      "version": "0.22.5",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz",
+      "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==",
+      "dev": true,
+      "requires": {
+        "vlq": "0.2.3"
+      }
+    },
+    "mime-db": {
+      "version": "1.33.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
+      "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
       "dev": true
     },
+    "mime-types": {
+      "version": "2.1.18",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
+      "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
+      "dev": true,
+      "requires": {
+        "mime-db": "1.33.0"
+      }
+    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
       "dev": true,
       "requires": {
-        "brace-expansion": "^1.1.7"
+        "brace-expansion": "1.1.11"
       }
     },
     "minimist": {
@@ -691,9 +942,21 @@
       "dev": true
     },
     "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==",
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "negotiator": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
+      "dev": true
+    },
+    "object-component": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=",
       "dev": true
     },
     "once": {
@@ -702,7 +965,7 @@
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
       "dev": true,
       "requires": {
-        "wrappy": "1"
+        "wrappy": "1.0.2"
       }
     },
     "optionator": {
@@ -711,12 +974,30 @@
       "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
       "dev": true,
       "requires": {
-        "deep-is": "~0.1.3",
-        "fast-levenshtein": "~2.0.4",
-        "levn": "~0.3.0",
-        "prelude-ls": "~1.1.2",
-        "type-check": "~0.3.2",
-        "wordwrap": "~1.0.0"
+        "deep-is": "0.1.3",
+        "fast-levenshtein": "2.0.6",
+        "levn": "0.3.0",
+        "prelude-ls": "1.1.2",
+        "type-check": "0.3.2",
+        "wordwrap": "1.0.0"
+      }
+    },
+    "parseqs": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "dev": true,
+      "requires": {
+        "better-assert": "1.0.2"
+      }
+    },
+    "parseuri": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "dev": true,
+      "requires": {
+        "better-assert": "1.0.2"
       }
     },
     "path-is-absolute": {
@@ -725,48 +1006,87 @@
       "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
       "dev": true
     },
+    "path-is-inside": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+      "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+      "dev": true
+    },
     "path-parse": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
       "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
       "dev": true
     },
+    "path-to-regexp": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz",
+      "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==",
+      "dev": true
+    },
+    "pixelmatch": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.0.2.tgz",
+      "integrity": "sha512-b65UpTI40rGFY8QwN6IYuCbpmwAOL6M8d6voX4F3zR99UmDqh7r2QWLxoeHOazBRgEmDUdqNVESDREqFxQS7rQ==",
+      "dev": true,
+      "requires": {
+        "pngjs": "3.4.0"
+      }
+    },
+    "pngjs": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
+      "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
+      "dev": true
+    },
     "prelude-ls": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
       "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
       "dev": true
     },
+    "punycode": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
+      "dev": true
+    },
+    "range-parser": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+      "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=",
+      "dev": true
+    },
     "resolve": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
-      "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
+      "version": "1.12.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
+      "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
       "dev": true,
       "requires": {
-        "path-parse": "^1.0.6"
+        "path-parse": "1.0.6"
       }
     },
     "rimraf": {
-      "version": "2.6.3",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
-      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+      "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
       "dev": true,
       "requires": {
-        "glob": "^7.1.3"
+        "glob": "7.1.4"
       },
       "dependencies": {
         "glob": {
-          "version": "7.1.3",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
-          "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+          "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"
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.1",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
           }
         }
       }
@@ -777,16 +1097,29 @@
       "integrity": "sha512-7RqCBQ9iwsOBPkjYgoIaeUij606mSkDMExP0NT7QDI3bqkHYQHrQ83uoNIXwPcQm/vP2VbsUz3kiyZZ1qPlLTQ==",
       "dev": true
     },
+    "rollup-plugin-commonjs": {
+      "version": "8.4.1",
+      "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-8.4.1.tgz",
+      "integrity": "sha512-mg+WuD+jlwoo8bJtW3Mvx7Tz6TsIdMsdhuvCnDMoyjh0oxsVgsjB/N0X984RJCWwc5IIiqNVJhXeeITcc73++A==",
+      "dev": true,
+      "requires": {
+        "acorn": "5.7.3",
+        "estree-walker": "0.5.2",
+        "magic-string": "0.22.5",
+        "resolve": "1.12.0",
+        "rollup-pluginutils": "2.8.1"
+      }
+    },
     "rollup-plugin-node-resolve": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.0.0.tgz",
       "integrity": "sha1-i4l8TDAw1QASd7BRSyXSygloPuA=",
       "dev": true,
       "requires": {
-        "browser-resolve": "^1.11.0",
-        "builtin-modules": "^1.1.0",
-        "is-module": "^1.0.0",
-        "resolve": "^1.1.6"
+        "browser-resolve": "1.11.3",
+        "builtin-modules": "1.1.1",
+        "is-module": "1.0.0",
+        "resolve": "1.12.0"
       }
     },
     "rollup-plugin-uglify": {
@@ -795,7 +1128,24 @@
       "integrity": "sha1-Z7N60e/a+9g69MNrQMGJ7khmyWk=",
       "dev": true,
       "requires": {
-        "uglify-js": "^3.0.9"
+        "uglify-js": "3.6.0"
+      }
+    },
+    "rollup-pluginutils": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz",
+      "integrity": "sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg==",
+      "dev": true,
+      "requires": {
+        "estree-walker": "0.6.1"
+      },
+      "dependencies": {
+        "estree-walker": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+          "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+          "dev": true
+        }
       }
     },
     "safe-buffer": {
@@ -804,12 +1154,127 @@
       "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
       "dev": true
     },
+    "seedrandom": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.3.tgz",
+      "integrity": "sha512-PJLhhxIMjlMJaiIRtqiVW061EZn3cS+waZkbFe7eCa2R3g88HbNdWmw4NTFG1w5unxd0GeNaUUxZJP7gPAzSDQ==",
+      "dev": true
+    },
     "semver": {
-      "version": "5.6.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
-      "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
       "dev": true
     },
+    "serve-handler": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.1.tgz",
+      "integrity": "sha512-LQPvxGia2TYqyMkHKH4jW9jx6jlQUMcWz6gJavZ3+4vsnB+SaWbYTncb9YsK5YBR6SlvyumREZJAzLw8VaFAUQ==",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "content-disposition": "0.5.2",
+        "fast-url-parser": "1.1.3",
+        "mime-types": "2.1.18",
+        "minimatch": "3.0.4",
+        "path-is-inside": "1.0.2",
+        "path-to-regexp": "2.2.1",
+        "range-parser": "1.2.0"
+      }
+    },
+    "slugify": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.4.tgz",
+      "integrity": "sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw==",
+      "dev": true
+    },
+    "socket.io": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz",
+      "integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==",
+      "dev": true,
+      "requires": {
+        "debug": "4.1.1",
+        "engine.io": "3.3.2",
+        "has-binary2": "1.0.3",
+        "socket.io-adapter": "1.1.1",
+        "socket.io-client": "2.2.0",
+        "socket.io-parser": "3.3.0"
+      }
+    },
+    "socket.io-adapter": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
+      "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=",
+      "dev": true
+    },
+    "socket.io-client": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz",
+      "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==",
+      "dev": true,
+      "requires": {
+        "backo2": "1.0.2",
+        "base64-arraybuffer": "0.1.5",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "3.1.0",
+        "engine.io-client": "3.3.2",
+        "has-binary2": "1.0.3",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "3.3.0",
+        "to-array": "0.1.4"
+      },
+      "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"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz",
+      "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.2.1",
+        "debug": "3.1.0",
+        "isarray": "2.0.1"
+      },
+      "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"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+          "dev": true
+        }
+      }
+    },
     "source-map": {
       "version": "0.5.7",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -822,9 +1287,15 @@
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
       "dev": true,
       "requires": {
-        "has-flag": "^3.0.0"
+        "has-flag": "3.0.0"
       }
     },
+    "to-array": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=",
+      "dev": true
+    },
     "to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -843,23 +1314,23 @@
       "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
       "dev": true,
       "requires": {
-        "prelude-ls": "~1.1.2"
+        "prelude-ls": "1.1.2"
       }
     },
     "uglify-js": {
-      "version": "3.4.9",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
-      "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
+      "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
       "dev": true,
       "requires": {
-        "commander": "~2.17.1",
-        "source-map": "~0.6.1"
+        "commander": "2.20.0",
+        "source-map": "0.6.1"
       },
       "dependencies": {
         "commander": {
-          "version": "2.17.1",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
-          "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+          "version": "2.20.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+          "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
           "dev": true
         },
         "source-map": {
@@ -879,6 +1350,12 @@
         "inherits": "2.0.1"
       }
     },
+    "vlq": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz",
+      "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==",
+      "dev": true
+    },
     "wordwrap": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
@@ -891,480 +1368,31 @@
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
       "dev": true
     },
+    "ws": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz",
+      "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==",
+      "dev": true,
+      "requires": {
+        "async-limiter": "1.0.1"
+      }
+    },
+    "xmlhttprequest-ssl": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+      "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=",
+      "dev": true
+    },
+    "yeast": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=",
+      "dev": true
+    },
     "zrender": {
       "version": "4.0.7",
-      "dependencies": {
-        "@babel/code-frame": {
-          "version": "7.0.0",
-          "bundled": true,
-          "requires": {
-            "@babel/highlight": "^7.0.0"
-          }
-        },
-        "@babel/core": {
-          "version": "7.3.4",
-          "bundled": true,
-          "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "@babel/generator": "^7.3.4",
-            "@babel/helpers": "^7.2.0",
-            "@babel/parser": "^7.3.4",
-            "@babel/template": "^7.2.2",
-            "@babel/traverse": "^7.3.4",
-            "@babel/types": "^7.3.4",
-            "convert-source-map": "^1.1.0",
-            "debug": "^4.1.0",
-            "json5": "^2.1.0",
-            "lodash": "^4.17.11",
-            "resolve": "^1.3.2",
-            "semver": "^5.4.1",
-            "source-map": "^0.5.0"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            },
-            "source-map": {
-              "version": "0.5.7",
-              "bundled": true
-            }
-          }
-        },
-        "@babel/generator": {
-          "version": "7.3.4",
-          "bundled": true,
-          "requires": {
-            "@babel/types": "^7.3.4",
-            "jsesc": "^2.5.1",
-            "lodash": "^4.17.11",
-            "source-map": "^0.5.0",
-            "trim-right": "^1.0.1"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            },
-            "source-map": {
-              "version": "0.5.7",
-              "bundled": true
-            }
-          }
-        },
-        "@babel/helper-function-name": {
-          "version": "7.1.0",
-          "bundled": true,
-          "requires": {
-            "@babel/helper-get-function-arity": "^7.0.0",
-            "@babel/template": "^7.1.0",
-            "@babel/types": "^7.0.0"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/helper-get-function-arity": {
-          "version": "7.0.0",
-          "bundled": true,
-          "requires": {
-            "@babel/types": "^7.0.0"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/helper-module-imports": {
-          "version": "7.0.0",
-          "bundled": true,
-          "requires": {
-            "@babel/types": "^7.0.0"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/helper-module-transforms": {
-          "version": "7.2.2",
-          "bundled": true,
-          "requires": {
-            "@babel/helper-module-imports": "^7.0.0",
-            "@babel/helper-simple-access": "^7.1.0",
-            "@babel/helper-split-export-declaration": "^7.0.0",
-            "@babel/template": "^7.2.2",
-            "@babel/types": "^7.2.2",
-            "lodash": "^4.17.10"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/helper-simple-access": {
-          "version": "7.1.0",
-          "bundled": true,
-          "requires": {
-            "@babel/template": "^7.1.0",
-            "@babel/types": "^7.0.0"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/helper-split-export-declaration": {
-          "version": "7.0.0",
-          "bundled": true,
-          "requires": {
-            "@babel/types": "^7.0.0"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/helpers": {
-          "version": "7.3.1",
-          "bundled": true,
-          "requires": {
-            "@babel/template": "^7.1.2",
-            "@babel/traverse": "^7.1.5",
-            "@babel/types": "^7.3.0"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/highlight": {
-          "version": "7.0.0",
-          "bundled": true,
-          "requires": {
-            "chalk": "^2.0.0",
-            "esutils": "^2.0.2",
-            "js-tokens": "^4.0.0"
-          }
-        },
-        "@babel/parser": {
-          "version": "7.3.4",
-          "bundled": true
-        },
-        "@babel/template": {
-          "version": "7.2.2",
-          "bundled": true,
-          "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "@babel/parser": "^7.2.2",
-            "@babel/types": "^7.2.2"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/traverse": {
-          "version": "7.3.4",
-          "bundled": true,
-          "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "@babel/generator": "^7.3.4",
-            "@babel/helper-function-name": "^7.1.0",
-            "@babel/helper-split-export-declaration": "^7.0.0",
-            "@babel/parser": "^7.3.4",
-            "@babel/types": "^7.3.4",
-            "debug": "^4.1.0",
-            "globals": "^11.1.0",
-            "lodash": "^4.17.11"
-          },
-          "dependencies": {
-            "@babel/types": {
-              "version": "7.3.4",
-              "bundled": true,
-              "requires": {
-                "esutils": "^2.0.2",
-                "lodash": "^4.17.11",
-                "to-fast-properties": "^2.0.0"
-              }
-            }
-          }
-        },
-        "@babel/types": {
-          "version": "7.0.0-beta.31",
-          "bundled": true,
-          "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.2.0",
-            "to-fast-properties": "^2.0.0"
-          }
-        },
-        "ansi-styles": {
-          "version": "3.2.1",
-          "bundled": true,
-          "requires": {
-            "color-convert": "^1.9.0"
-          }
-        },
-        "assert": {
-          "version": "1.4.1",
-          "bundled": true,
-          "requires": {
-            "util": "0.10.3"
-          }
-        },
-        "chalk": {
-          "version": "2.4.2",
-          "bundled": true,
-          "requires": {
-            "ansi-styles": "^3.2.1",
-            "escape-string-regexp": "^1.0.5",
-            "supports-color": "^5.3.0"
-          }
-        },
-        "color-convert": {
-          "version": "1.9.3",
-          "bundled": true,
-          "requires": {
-            "color-name": "1.1.3"
-          }
-        },
-        "color-name": {
-          "version": "1.1.3",
-          "bundled": true
-        },
-        "commander": {
-          "version": "2.11.0",
-          "bundled": true
-        },
-        "convert-source-map": {
-          "version": "1.6.0",
-          "bundled": true,
-          "requires": {
-            "safe-buffer": "~5.1.1"
-          }
-        },
-        "debug": {
-          "version": "4.1.1",
-          "bundled": true,
-          "requires": {
-            "ms": "^2.1.1"
-          }
-        },
-        "escape-string-regexp": {
-          "version": "1.0.5",
-          "bundled": true
-        },
-        "esutils": {
-          "version": "2.0.2",
-          "bundled": true
-        },
-        "fs-extra": {
-          "version": "4.0.2",
-          "bundled": true,
-          "requires": {
-            "graceful-fs": "^4.1.2",
-            "jsonfile": "^4.0.0",
-            "universalify": "^0.1.0"
-          }
-        },
-        "globals": {
-          "version": "11.11.0",
-          "bundled": true
-        },
-        "graceful-fs": {
-          "version": "4.1.11",
-          "bundled": true
-        },
-        "has-flag": {
-          "version": "3.0.0",
-          "bundled": true
-        },
-        "inherits": {
-          "version": "2.0.1",
-          "bundled": true
-        },
-        "is-equal": {
-          "version": "0.1.0",
-          "bundled": true
-        },
-        "js-tokens": {
-          "version": "4.0.0",
-          "bundled": true
-        },
-        "jsdiff": {
-          "version": "1.1.1",
-          "bundled": true,
-          "requires": {
-            "is-equal": "~0.1.0"
-          }
-        },
-        "jsesc": {
-          "version": "2.5.2",
-          "bundled": true
-        },
-        "json5": {
-          "version": "2.1.0",
-          "bundled": true,
-          "requires": {
-            "minimist": "^1.2.0"
-          }
-        },
-        "jsonfile": {
-          "version": "4.0.0",
-          "bundled": true,
-          "requires": {
-            "graceful-fs": "^4.1.6"
-          }
-        },
-        "lodash": {
-          "version": "4.17.11",
-          "bundled": true
-        },
-        "minimist": {
-          "version": "1.2.0",
-          "bundled": true
-        },
-        "ms": {
-          "version": "2.1.1",
-          "bundled": true
-        },
-        "path-parse": {
-          "version": "1.0.6",
-          "bundled": true
-        },
-        "resolve": {
-          "version": "1.10.0",
-          "bundled": true,
-          "requires": {
-            "path-parse": "^1.0.6"
-          }
-        },
-        "rollup": {
-          "version": "0.50.0",
-          "bundled": true
-        },
-        "rollup-plugin-uglify": {
-          "version": "2.0.1",
-          "bundled": true,
-          "requires": {
-            "uglify-js": "^3.0.9"
-          }
-        },
-        "safe-buffer": {
-          "version": "5.1.2",
-          "bundled": true
-        },
-        "semver": {
-          "version": "5.6.0",
-          "bundled": true
-        },
-        "source-map": {
-          "version": "0.6.1",
-          "bundled": true
-        },
-        "supports-color": {
-          "version": "5.5.0",
-          "bundled": true,
-          "requires": {
-            "has-flag": "^3.0.0"
-          }
-        },
-        "to-fast-properties": {
-          "version": "2.0.0",
-          "bundled": true
-        },
-        "trim-right": {
-          "version": "1.0.1",
-          "bundled": true
-        },
-        "uglify-js": {
-          "version": "3.1.6",
-          "bundled": true,
-          "requires": {
-            "commander": "~2.11.0",
-            "source-map": "~0.6.1"
-          }
-        },
-        "universalify": {
-          "version": "0.1.1",
-          "bundled": true
-        },
-        "util": {
-          "version": "0.10.3",
-          "bundled": true,
-          "requires": {
-            "inherits": "2.0.1"
-          }
-        }
-      }
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-4.0.7.tgz",
+      "integrity": "sha512-TNloHe0ums6zxbHfnaCryM61J4IWDajZwNq6dHk9vfWhhysO/OeFvvR0drBs/nbXha2YxSzfQj2FiCd6RVBe+Q=="
     }
   }
 }
diff --git a/package.json b/package.json
index 18ad5a2..16fde6c 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
   },
   "scripts": {
     "prepublish": "node build/build.js --prepublish",
+    "test:visual": "node test/runTest/server.js",
     "test": "node build/build.js"
   },
   "dependencies": {
@@ -33,8 +34,16 @@
     "estraverse": "4.1.1",
     "fs-extra": "0.26.7",
     "glob": "7.0.0",
+    "lolex": "^4.2.0",
+    "pixelmatch": "^5.0.2",
+    "pngjs": "^3.4.0",
     "rollup": "0.50.0",
+    "rollup-plugin-commonjs": "^8.4.1",
     "rollup-plugin-node-resolve": "3.0.0",
-    "rollup-plugin-uglify": "2.0.1"
+    "rollup-plugin-uglify": "2.0.1",
+    "seedrandom": "^3.0.3",
+    "serve-handler": "^6.1.1",
+    "slugify": "^1.3.4",
+    "socket.io": "^2.2.0"
   }
 }
diff --git a/test/runTest/blacklist.js b/test/runTest/blacklist.js
new file mode 100644
index 0000000..9e9ad44
--- /dev/null
+++ b/test/runTest/blacklist.js
@@ -0,0 +1,4 @@
+module.exports = [
+    '-cases.html',
+    'geo-random-stream.html'
+];
diff --git a/test/runTest/cli.js b/test/runTest/cli.js
new file mode 100644
index 0000000..e44ebe7
--- /dev/null
+++ b/test/runTest/cli.js
@@ -0,0 +1,271 @@
+const puppeteer = require('puppeteer');
+const slugify = require('slugify');
+const fse = require('fs-extra');
+const fs = require('fs');
+const path = require('path');
+const compareScreenshot = require('./compareScreenshot');
+const {getTestName, getVersionDir, buildRuntimeCode} = require('./util');
+const {origin} = require('./config');
+
+function getScreenshotDir() {
+    return 'tmp/__screenshot__';
+}
+
+function sortScreenshots(list) {
+    return list.sort((a, b) => {
+        return a.testName.localeCompare(b.testName);
+    });
+}
+
+function getClientRelativePath(absPath) {
+    return path.join('../', path.relative(__dirname, absPath));
+}
+
+function replaceEChartsVersion(interceptedRequest, version) {
+    // TODO Extensions and maps
+    if (interceptedRequest.url().endsWith('dist/echarts.js')) {
+        interceptedRequest.continue({
+            url: `${origin}/test/runTest/${getVersionDir(version)}/echarts.js`
+        });
+    }
+    else {
+        interceptedRequest.continue();
+    }
+}
+
+function waitPageForFinish(page) {
+    return new Promise(resolve => {
+        page.exposeFunction('puppeteerFinishTest', () => {
+            resolve();
+        });
+    });
+}
+
+function createWaitTimeout(maxTime) {
+    let timeoutHandle;
+    let resolve;
+    function keepWait(newMaxTime) {
+        newMaxTime = newMaxTime == null ? maxTime : newMaxTime;
+        clearTimeout(timeoutHandle);
+        createTimeout(newMaxTime);
+    }
+
+    function createTimeout(maxTime) {
+        timeoutHandle = setTimeout(() => {
+            resolve();
+        }, maxTime);
+    }
+
+    function waitTimeout() {
+        return new Promise(_resolve => {
+            resolve = _resolve;
+            createTimeout(maxTime);
+        });
+    }
+
+    return {keepWait, waitTimeout};
+}
+
+async function takeScreenshot(page, elementQuery, fileUrl, desc, version) {
+    let target = elementQuery ? await page.$(elementQuery) : page;
+    if (!target) {
+        console.error(`Can't find element '${elementQuery}'`);
+        return;
+    }
+    let fullPage = !elementQuery;
+    let testName = getTestName(fileUrl);
+    if (desc) {
+        testName += '-' + slugify(desc, { replacement: '-', lower: true })
+    }
+    let screenshotPrefix = version ? 'expected' : 'actual';
+    fse.ensureDirSync(path.join(__dirname, getScreenshotDir()));
+    let screenshotPath = path.join(__dirname, `${getScreenshotDir()}/${testName}-${screenshotPrefix}.png`);
+    await target.screenshot({
+        path: screenshotPath,
+        fullPage
+    });
+
+    return {testName, screenshotPath};
+}
+
+async function runTestPage(browser, fileUrl, version, runtimeCode) {
+    const {keepWait, waitTimeout} = createWaitTimeout(1500);
+    const screenshots = [];
+    const logs = [];
+    const errors = [];
+    let screenshotPromises = [];
+
+    const page = await browser.newPage();
+    page.setRequestInterception(true);
+    page.on('request', replaceEChartsVersion);
+    await page.evaluateOnNewDocument(runtimeCode);
+
+    let descAutoCounter = 0;
+
+    page.exposeFunction('puppeteerScreenshot', (desc, elementQuery) => {
+        keepWait();
+        desc = desc || (descAutoCounter++).toString();
+        let promise = takeScreenshot(page, elementQuery, fileUrl, desc, version).then((result) => {
+            if (!result) {
+                return;
+            }
+            const {testName, screenshotPath} = result;
+            screenshots.push({testName, desc, screenshotPath});
+        });
+        screenshotPromises.push(promise);
+
+        return promise;
+    });
+    page.on('console', msg => {
+        logs.push(msg.text());
+    });
+    page.on('pageerror', error => {
+        errors.push(error);
+    });
+
+    let pageFinishPromise = waitPageForFinish(page);
+
+    try {
+        await page.setViewport({
+            width: 800,
+            height: 600
+        });
+        await page.goto(`${origin}/test/${fileUrl}`, {
+            waitUntil: 'networkidle2',
+            // waitUntil: 'domcontentloaded',
+            timeout: 10000
+        });
+    }
+    catch(e) {
+        // TODO Timeout Error
+        console.error(e);
+    }
+
+    // TODO Animation
+
+    // Do auto screenshot after 100ms for animation.
+    // let autoSnapshotTimeout = setTimeout(async () => {
+    //     let desc = `Animation Interval`;
+    //     let promise = takeScreenshot(page, '', fileUrl, desc, version)
+    //         .then(({testName, screenshotPath}) => {
+    //             screenshots.push({testName, desc, screenshotPath});
+    //         });
+    //     screenshotPromises.push(promise);
+    // }, 100);
+
+    // Wait for puppeteerFinishTest() is called
+    // Or compare the whole page if nothing happened after 10 seconds.
+    await Promise.race([
+        pageFinishPromise,
+        waitTimeout().then(() => {
+            // console.warn('Test timeout after 3 seconds.');
+            // Final shot.
+            let desc = 'Final Shot';
+            return takeScreenshot(page, '', fileUrl, desc, version)
+                .then(({testName, screenshotPath}) => {
+                    screenshots.push({testName, desc, screenshotPath});
+                });
+        })
+    ]);
+
+    // clearTimeout(autoSnapshotTimeout);
+
+    // Wait for screenshot finished.
+    await Promise.all(screenshotPromises);
+
+    await page.close();
+
+    return {
+        logs,
+        errors,
+        screenshots: screenshots
+    };
+}
+
+async function writePNG(diffPNG, diffPath) {
+    return new Promise(resolve => {
+        let writer = fs.createWriteStream(diffPath);
+        diffPNG.pack().pipe(writer);
+        writer.on('finish', () => {resolve();});
+    });
+};
+
+async function runTest(browser, testOpt, runtimeCode) {
+    testOpt.status === 'running';
+    const fileUrl = testOpt.fileUrl;
+    const expectedResult = await runTestPage(browser, fileUrl, '4.2.1', runtimeCode);
+    const actualResult = await runTestPage(browser, fileUrl, '', runtimeCode);
+
+    sortScreenshots(expectedResult.screenshots);
+    sortScreenshots(actualResult.screenshots);
+
+    const screenshots = [];
+    let idx = 0;
+    for (let shot of expectedResult.screenshots) {
+        let expected = shot;
+        let actual = actualResult.screenshots[idx++];
+        let {diffRatio, diffPNG} = await compareScreenshot(
+            expected.screenshotPath,
+            actual.screenshotPath
+        );
+
+        let diffPath = `${path.resolve(__dirname, getScreenshotDir())}/${shot.testName}-diff.png`;
+        await writePNG(diffPNG, diffPath);
+
+        screenshots.push({
+            actual: getClientRelativePath(actual.screenshotPath),
+            expected: getClientRelativePath(expected.screenshotPath),
+            diff: getClientRelativePath(diffPath),
+            name: actual.testName,
+            desc: actual.desc,
+            diffRatio
+        });
+    }
+
+    testOpt.results = screenshots;
+    testOpt.status = 'finished';
+    testOpt.actualLogs = actualResult.logs;
+    testOpt.expectedLogs = expectedResult.logs;
+    testOpt.actualErrors = actualResult.errors;
+    testOpt.expectedErrors = expectedResult.errors;
+
+}
+
+async function runTests(pendingTests) {
+    const browser = await puppeteer.launch({ headless: true });
+    // TODO Not hardcoded.
+    // let runtimeCode = fs.readFileSync(path.join(__dirname, 'tmp/testRuntime.js'), 'utf-8');
+    let runtimeCode = await buildRuntimeCode();
+
+    try {
+        for (let testOpt of pendingTests) {
+            console.log('Running Test', testOpt.name);
+            try {
+                await runTest(browser, testOpt, runtimeCode);
+            }
+            catch (e) {
+                // Restore status
+                testOpt.status = 'pending';
+                console.log(e);
+            }
+
+            process.send(testOpt);
+        }
+    }
+    catch(e) {
+        console.log(e);
+    }
+
+    await browser.close();
+}
+
+// Handling input arguments.
+const testsFileUrlList = process.argv[2] || '';
+runTests(testsFileUrlList.split(',').map(fileUrl => {
+    return {
+        fileUrl,
+        name: getTestName(fileUrl),
+        results: [],
+        status: 'unsettled'
+    };
+}));
\ No newline at end of file
diff --git a/test/runTest/client/client.css b/test/runTest/client/client.css
new file mode 100644
index 0000000..cfaad1a
--- /dev/null
+++ b/test/runTest/client/client.css
@@ -0,0 +1,163 @@
+#main {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+}
+
+* {
+    font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
+}
+
+.header {
+    background-color: #293c55;
+    box-shadow: 0 0  20px rgba(0, 0, 0, 0.2);
+    position: relative;
+    z-index: 10;
+}
+
+.header h1 {
+    color: #fff;
+    line-height: 50px;
+    margin: 0;
+    font-weight: 200;
+    font-size: 20px;
+}
+
+#logo>* {
+    display: inline-block;
+    vertical-align: middle;
+}
+
+#logo img {
+    height: 30px;
+    margin-right: 20px;
+}
+
+.nav-toolbar {
+    padding: 10px 10px;
+    background: #162436;
+    box-shadow: inset 0 0 5px black;
+    position: fixed;
+    top: 50px;
+    width: 280px;
+    z-index: 2;
+}
+.nav-toolbar .controls {
+    margin-top: 10px;
+}
+
+.test-list {
+    overflow-x: hidden;
+    background: #293c55;
+    margin: 0;
+    padding: 0;
+    margin-top: 80px;
+}
+.test-list li {
+    list-style: none;
+    padding-left: 10px;
+    cursor: pointer;
+    color: #f3f3f3;
+}
+.test-list li a.menu-link {
+    display: inline-block;
+    text-decoration: none;
+    font-size: 14px;
+    line-height: 40px;
+    color: #f3f3f3;
+    margin-left: 3px;
+    cursor: pointer;
+}
+
+.test-list li.active {
+    background: #e43c59;
+}
+.test-list li:hover {
+    background: #162436;
+}
+.test-list li>* {
+    vertical-align: middle;
+    display: inline-block
+}
+
+.test-list .el-progress__text {
+    font-size: 12px!important;
+}
+
+.test-result h3 {
+    font-size: 40px;
+    font-weight: 200;
+    padding: 0;
+    margin: 0;
+}
+
+.test-result .title>* {
+    display: inline-block;
+    vertical-align: middle;
+}
+.test-result .title a {
+    margin-left: 10px;
+    font-size: 20px;
+    font-weight: 200;
+    text-decoration: none;
+}
+.test-result .title a:hover {
+    text-decoration: underline;
+}
+
+.test-screenshots {
+    margin-top: 80px;
+    padding: 0 20px;
+}
+
+.test-screenshots img {
+    /* height: 200px; */
+    width: 100%;
+}
+.test-screenshots h4 {
+    font-size: 30px;
+    font-weight: 200;
+    margin-left: -20px;
+    color: #162436;
+}
+
+.test-errors, .test-logs {
+    margin-top: 20px;
+    padding: 0 50px;
+}
+
+.test-logs .log-item {
+    margin: 10px 20px;
+    color: #909399;
+}
+
+.test-errors .error-item {
+    margin: 10px 20px;
+    color: #f56c6c
+}
+
+
+::-webkit-scrollbar {
+    height:8px;
+    width:8px;
+    transition:all 0.3s ease-in-out;
+    border-radius:2px;
+    background: transparent;
+}
+
+::-webkit-scrollbar-button {
+    display:none;
+}
+
+::-webkit-scrollbar-thumb {
+    width:8px;
+    min-height:15px;
+    background:rgba(50, 50, 50, 0.6) !important;
+    transition:all 0.3s ease-in-out;border-radius:2px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+    background:rgba(0, 0, 0, 0.5) !important;
+}
\ No newline at end of file
diff --git a/test/runTest/client/client.js b/test/runTest/client/client.js
new file mode 100644
index 0000000..0dc2607
--- /dev/null
+++ b/test/runTest/client/client.js
@@ -0,0 +1,182 @@
+const socket = io();
+
+function processTestsData(tests, oldTestsData) {
+    tests.forEach((test, idx) => {
+        let passed = 0;
+        test.index = idx;
+        test.results.forEach(result => {
+            // Threshold?
+            if (result.diffRatio < 0.001) {
+                passed++;
+            }
+        });
+        test.percentage = passed === 0 ? 0 : Math.round(passed / test.results.length * 100);
+        if (test.percentage === 100) {
+            test.summary = 'success';
+        }
+        else if (test.percentage < 50) {
+            test.summary = 'exception';
+        }
+        else {
+            test.summary = 'warning';
+        }
+
+        // Keep select status not change.
+        if (oldTestsData && oldTestsData[idx]) {
+            test.selected = oldTestsData[idx].selected;
+        }
+        else {
+            test.selected = false;
+        }
+    });
+    return tests;
+}
+
+socket.on('connect', () => {
+    console.log('Connected');
+    const app = new Vue({
+        el: '#app',
+        data: {
+            fullTests: [],
+            currentTestName: '',
+            sortBy: 'name',
+            searchString: '',
+            running: false,
+
+            allSelected: false,
+            lastSelectedIndex: -1
+        },
+        computed: {
+            tests() {
+                let sortFunc = this.sortBy === 'name'
+                    ? (a, b) => a.name.localeCompare(b.name)
+                    : (a, b) => {
+                        if (a.percentage === b.percentage) {
+                            return a.name.localeCompare(b.name);
+                        }
+                        return a.percentage - b.percentage;
+                    };
+
+                if (!this.searchString) {
+                    // Not modify the original tests data.
+                    return this.fullTests.slice().sort(sortFunc);
+                }
+
+                return this.fullTests.filter(test => {
+                    return test.name.match(this.searchString);
+                }).sort(sortFunc);
+            },
+
+            currentTest() {
+                let currentTest = this.fullTests.find(item => item.name === this.currentTestName);
+                if (!currentTest) {
+                    currentTest = this.fullTests[0];
+                }
+                return currentTest;
+            },
+
+            currentTestUrl() {
+                return window.location.origin + '/test/' + this.currentTest.fileUrl;
+            },
+
+            isSelectAllIndeterminate: {
+                get() {
+                    if (!this.tests.length) {
+                        return true;
+                    }
+                    return this.tests.some(test => {
+                        return test.selected !== this.tests[0].selected;
+                    });
+                },
+                set() {}
+            }
+        },
+        methods: {
+            goto(url) {
+                window.location.hash = '#' + url;
+            },
+            toggleSort() {
+                this.sortBy = this.sortBy === 'name' ? 'percentage' : 'name';
+            },
+            handleSelectAllChange(val) {
+                // Only select filtered tests.
+                this.tests.forEach(test => {
+                    test.selected = val;
+                });
+                this.isSelectAllIndeterminate = false;
+            },
+            handleSelect(idx) {
+                Vue.nextTick(() => {
+                    this.lastSelectedIndex = idx;
+                });
+            },
+            handleShiftSelect(idx) {
+                if (this.lastSelectedIndex < 0) {
+                    return;
+                }
+                let start = Math.min(this.lastSelectedIndex, idx);
+                let end = Math.max(this.lastSelectedIndex, idx);
+                let selected = !this.tests[idx].selected;   // Will change
+                for (let i = start; i < end; i++) {
+                    this.tests[i].selected = selected;
+                }
+            },
+            refreshList() {
+
+            },
+            runSelectedTests() {
+                const tests = this.fullTests.filter(test => {
+                    return test.selected;
+                }).map(test => {
+                    return test.name;
+                });
+                if (tests.length > 0) {
+                    this.running = true;
+                    socket.emit('run', tests);
+                }
+            },
+            stopTests() {
+                this.running = false;
+                socket.emit('stop');
+            }
+        }
+    });
+    app.$el.style.display = 'block';
+
+    let firstUpdate = true;
+    socket.on('update', msg => {
+        let hasFinishedTest = !!msg.tests.find(test => test.status === 'finished');
+        if (!hasFinishedTest && firstUpdate) {
+            app.$confirm('It seems you haven\'t run any test yet!<br />Do you want to start now?', 'Tip', {
+                confirmButtonText: 'Yes',
+                cancelButtonText: 'No',
+                dangerouslyUseHTMLString: true,
+                center: true
+            }).then(value => {
+                app.running = true;
+                socket.emit('run', msg.tests.map(test => test.name));
+            }).catch(() => {});
+        }
+        // TODO
+        // app.running = !!msg.running;
+        app.fullTests = processTestsData(msg.tests, app.fullTests);
+
+        firstUpdate = false;
+    });
+    socket.on('finish', () => {
+        app.$notify({
+            title: 'Test Complete',
+            position: 'bottom-right'
+        });
+        app.running = false;
+    });
+
+    function updateTestHash() {
+        let testName = window.location.hash.slice(1);
+        app.currentTestName = testName;
+    }
+
+    updateTestHash();
+    window.addEventListener('hashchange', updateTestHash);
+});
+
diff --git a/test/runTest/client/index.html b/test/runTest/client/index.html
new file mode 100644
index 0000000..c45b49d
--- /dev/null
+++ b/test/runTest/client/index.html
@@ -0,0 +1,168 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+</head>
+<body>
+<div id="app" style="display: none">
+    <el-container id="main">
+        <el-header class="header" height="50">
+            <div id="logo">
+                <img src="https://echarts.apache.org/zh/images/logo.png" />
+                <h1>Visual Regression Test</h1>
+            </div>
+        </el-header>
+        <el-container style="min-height: 0"> <!-- https://juejin.im/post/5c642f2ff265da2de660ecfc -->
+            <el-aside width="300px">
+                <div class="nav-toolbar">
+                    <el-input v-model="searchString" size="mini" placeholder="Filter Tests"></el-input>
+                    <div class="controls">
+                        <el-checkbox :indeterminate="isSelectAllIndeterminate" v-model="allSelected" @change="handleSelectAllChange"></el-checkbox>
+                        <el-button-group>
+                            <el-button title="Run Selected" @click="runSelectedTests" :loading="running" circle size="mini" type="primary" icon="el-icon-caret-right"></el-button>
+                            <el-button v-if="running" title="Run Selected" @click="stopTests" circle size="mini" type="primary" icon="el-icon-close"></el-button>
+                        </el-button-group>
+
+                        <el-button
+                            style="margin-left: 10px"
+                            title="Sort By Failue Percentage" @click="toggleSort" circle size="mini" type="primary" icon="el-icon-sort"
+                        ></el-button>
+                        <!-- <el-button-group>
+                            <el-button title="Select All" @click="selectAllTests" circle size="mini" type="primary" icon="el-icon-check"></el-button>
+                            <el-button title="Unselect All" @click="unselectAllTests" circle size="mini" type="primary" icon="el-icon-close"></el-button>
+                        </el-button-group> -->
+                        <!-- <el-button title="Refresh List" @click="refreshList" circle size="mini" type="primary" icon="el-icon-refresh"></el-button> -->
+                    </div>
+                </div>
+                <ul class="test-list">
+                    <li v-for="(test, index) in tests"
+                        :title="test.name"
+                        :class="{active: currentTest && currentTest.name === test.name}"
+                        @click.self="goto(test.name)"
+                    >
+                        <span @mouseup="handleSelect(index)" @mouseup.shift="handleShiftSelect(index)">
+                            <el-checkbox v-model="test.selected"></el-checkbox>
+                        </span>
+                        <i class="el-icon-loading" v-if="test.status === 'pending' && running"></i>
+                        <el-progress
+                            v-if="test.status === 'finished'"
+                            type="circle"
+                            :width="20"
+                            :stroke-width="2"
+                            :percentage="test.percentage"
+                            :status="test.summary"
+                        ></el-progress>
+                        <a :href="'#' + test.name" class="menu-link">{{test.name}}</a>
+
+                    </li>
+                </ul>
+            </el-aside>
+            <el-main>
+                <div v-if="currentTest" class="test-result">
+                    <div class="title">
+                        <h3>{{currentTest.name}}</h3>
+                        <a target="_blank" :href="currentTestUrl"><i class="el-icon-link"></i>Open Demo</a>
+                    </div>
+
+                    <div class="test-screenshots" v-for="result in currentTest.results">
+                        <h4>{{result.desc || result.name}}</h4>
+                        <el-row :gutter="40" class="screenshots">
+                            <el-col :span="8">
+                                <el-card>
+                                    <div slot="header" class="clearfix">
+                                        <span>Expected</span>
+                                    </div>
+                                    <el-image :src="result.expected" :preview-src-list="[result.expected]"></el-image>
+                                </el-card>
+                            </el-col>
+
+                            <el-col :span="8">
+                                <el-card>
+                                    <div slot="header" class="clearfix">
+                                        <span>Actual</span>
+                                    </div>
+                                    <el-image :src="result.actual" :preview-src-list="[result.actual]"></el-image>
+                                </el-card>
+                            </el-col>
+
+                            <el-col :span="8">
+                                <el-card>
+                                    <div slot="header" class="clearfix">
+                                        <span>Diff({{result.diffRatio.toFixed(4)}})</span>
+                                    </div>
+                                    <el-image :src="result.diff" :preview-src-list="[result.diff]"></el-image>
+                                </el-card>
+                            </el-col>
+                        </el-row>
+                    </div>
+
+                    <div class="test-errors">
+                        <el-row :gutter="40">
+                            <el-col :span="12">
+                                <el-alert
+                                    title="Expected Errors"
+                                    type="error"
+                                    show-icon>
+                                </el-alert>
+                                <div class="error-item" v-for="error in currentTest.expectedErrors">{{error}}</div>
+                            </el-col>
+                            <el-col :span="12">
+                                <el-alert
+                                    title="Actual Errors"
+                                    type="error"
+                                    show-icon>
+                                </el-alert>
+                                <div class="error-item"  v-for="error in currentTest.actualErrors">{{error}}</div>
+                            </el-col>
+                        </el-row>
+                    </div>
+
+                    <div class="test-logs">
+                        <el-row :gutter="40">
+                            <el-col :span="12">
+                                <el-alert
+                                    title="Expected Logs"
+                                    type="info"
+                                    show-icon>
+                                </el-alert>
+                                <div class="log-item"  v-for="log in currentTest.expectedLogs">{{log}}</div>
+                            </el-col>
+                            <el-col :span="12">
+                                <el-alert
+                                    title="Actual Logs"
+                                    type="info"
+                                    show-icon>
+                                </el-alert>
+                                <div class="log-item"  v-for="log in currentTest.actualLogs">{{log}}</div>
+                            </el-col>
+                        </el-row>
+                    </div>
+
+                    <div class="test-result-nav">
+
+                    </div>
+                </div>
+            </el-main>
+        </el-container>
+    </el-container>
+
+
+
+</div>
+
+<script src="../../../node_modules/socket.io-client/dist/socket.io.js"></script>
+<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>
+
+<!-- Element UI -->
+<link rel="stylesheet" href="https://unpkg.com/element-ui@2.11.1/lib/theme-chalk/index.css">
+<script src="https://unpkg.com/element-ui@2.11.1/lib/index.js"></script>
+
+<script src="client.js"></script>
+
+<link rel="stylesheet" href="client.css">
+<link rel="stylesheet" href="">
+
+</body>
+</html>
\ No newline at end of file
diff --git a/test/runTest/compareScreenshot.js b/test/runTest/compareScreenshot.js
new file mode 100644
index 0000000..8fd36c8
--- /dev/null
+++ b/test/runTest/compareScreenshot.js
@@ -0,0 +1,41 @@
+const PNG = require('pngjs').PNG;
+const pixelmatch = require('pixelmatch');
+const fs = require('fs');
+
+function readPNG(path) {
+    return new Promise(resolve => {
+        fs.createReadStream(path)
+            .pipe(new PNG())
+            .on('parsed', function () {
+                resolve({
+                    data: this.data,
+                    width: this.width,
+                    height: this.height
+                });
+            });
+    });
+}
+
+module.exports = function (expectedShotPath, actualShotPath, threshold = 0.1) {
+    return Promise.all([
+        readPNG(expectedShotPath),
+        readPNG(actualShotPath)
+    ]).then(([expectedImg, actualImg]) => {
+        let width = expectedImg.width;
+        let height = expectedImg.height;
+        if (
+            (width !== actualImg.width)
+          || (height !== actualImg.height)
+        ) {
+            throw new Error('Image size not match');
+        }
+        const diffPNG = new PNG({width, height});
+        let diffPixelsCount = pixelmatch(expectedImg.data, actualImg.data, diffPNG.data, width, height, {threshold});
+        let totalPixelsCount = width * height;
+
+        return {
+            diffRatio: diffPixelsCount / totalPixelsCount,
+            diffPNG
+        };
+    });
+};
diff --git a/test/runTest/config.js b/test/runTest/config.js
new file mode 100644
index 0000000..7234965
--- /dev/null
+++ b/test/runTest/config.js
@@ -0,0 +1,4 @@
+module.exports = {
+    port: 8866,
+    origin: 'http://localhost:8866'
+};
\ No newline at end of file
diff --git a/test/runTest/runtime/main.js b/test/runTest/runtime/main.js
new file mode 100644
index 0000000..35da2f1
--- /dev/null
+++ b/test/runTest/runtime/main.js
@@ -0,0 +1,53 @@
+import seedrandom from 'seedrandom';
+import lolex from 'lolex';
+
+// const NativeDate = window.Date;
+
+// const fixedTimestamp = 1566458693300;
+// const actualTimestamp = NativeDate.now();
+// function MockDate(params) {
+//     if (!params) {
+//         const elapsedTime = NativeDate.now() - actualTimestamp;
+//         return new NativeDate(fixedTimestamp + elapsedTime);
+//     }
+//     else {
+//         return new NativeDate(params);
+//     }
+// }
+// MockDate.prototype = new Date();
+// // Fixed date
+// window.Date = MockDate;
+
+// export function createScreenshotTest (desc, elementQuery, waitTime) {
+
+// };
+
+/**
+ * Take screenshot immediately.
+ * @param {string} desc
+ * @param {string} [elementQuery] If only screenshot specifed element. Will do full page screenshot if it's not give.
+ */
+export function compareScreenshot (desc, elementQuery) {
+    return puppeteerScreenshot(desc, elementQuery);
+};
+
+/**
+ * Finish the test.
+ */
+export function finish() {
+    return puppeteerFinishTest();
+};
+
+
+let myRandom = new seedrandom('echarts-test');
+// Fixed random generator
+Math.random = function () {
+    const val = myRandom();
+    return val;
+};
+
+// lolex.install({
+//     shouldAdvanceTime: true,
+//     advanceTimeDelta: 200,
+//     now: fixedTimestamp
+// });
diff --git a/test/runTest/runtime/shim.js b/test/runTest/runtime/shim.js
new file mode 100644
index 0000000..d0febb4
--- /dev/null
+++ b/test/runTest/runtime/shim.js
@@ -0,0 +1,9 @@
+(function () {
+    if (typeof autorun !== 'undefined') {
+        return;
+    }
+    var autorun = {};
+    autorun.createScreenshotTest = function () {};
+    autorun.compareScreenshot = function () {};
+    autorun.finish = function () {};
+})();
\ No newline at end of file
diff --git a/test/runTest/server.js b/test/runTest/server.js
new file mode 100644
index 0000000..8900f94
--- /dev/null
+++ b/test/runTest/server.js
@@ -0,0 +1,127 @@
+const handler = require('serve-handler');
+const http = require('http');
+const path = require('path');
+// const open = require('open');
+const fse = require('fs-extra');
+const {fork} = require('child_process');
+const {port, origin} = require('./config');
+const {getTestsList, prepareTestsList, saveTestsList, mergeTestsResults} = require('./store');
+const {prepareEChartsVersion, buildRuntimeCode} = require('./util');
+
+function serve() {
+    const server = http.createServer((request, response) => {
+        return handler(request, response, {
+            cleanUrls: false,
+            // Root folder of echarts
+            public: __dirname + '/../../'
+        });
+    });
+
+    server.listen(port, () => {
+        console.log(`Server started. ${origin}`);
+    });
+
+
+    const io = require('socket.io')(server);
+    return {
+        io
+    };
+};
+
+let testProcess;
+let pendingTests;
+
+function stopRunningTests() {
+    if (testProcess) {
+        testProcess.kill();
+        testProcess = null;
+    }
+    if (pendingTests) {
+        pendingTests.forEach(testOpt => {
+            if (testOpt.status === 'pending') {
+                testOpt.status = 'unsettled';
+            }
+        });
+        pendingTests = null;
+    }
+}
+
+function startTests(testsNameList, socket) {
+    console.log(testsNameList.join(','));
+
+    stopRunningTests();
+
+    return new Promise(resolve => {
+        pendingTests = getTestsList().filter(testOpt => {
+            return testsNameList.includes(testOpt.name);
+        });
+        pendingTests.forEach(testOpt => {
+            // Reset all tests results
+            testOpt.status = 'pending';
+            testOpt.results = [];
+        });
+
+        socket.emit('update', {tests: getTestsList()});
+
+        testProcess = fork(path.join(__dirname, 'cli.js'), [
+            pendingTests.map(testOpt => testOpt.fileUrl)
+        ]);
+        // Finished one test
+        testProcess.on('message', testOpt => {
+            mergeTestsResults([testOpt]);
+            // Merge tests.
+            socket.emit('update', {tests: getTestsList(), running: true});
+            saveTestsList();
+        });
+        // Finished all
+        testProcess.on('exit', () => {
+            resolve();
+        });
+    });
+}
+
+function checkPuppeteer() {
+    try {
+        return require('puppeteer');
+    }
+    catch (e) {
+        return null;
+    }
+}
+
+async function start() {
+    if (!checkPuppeteer()) {
+        // TODO Check version.
+        console.error(`Can't find puppeteer, use 'npm install puppeteer' to install`);
+        return;
+    }
+
+    await prepareEChartsVersion('4.2.1'); // Expected version.
+    await prepareEChartsVersion(); // Version to test
+
+    let runtimeCode = await buildRuntimeCode();
+    fse.outputFileSync(path.join(__dirname, 'tmp/testRuntime.js'), runtimeCode, 'utf-8');
+
+    // Start a static server for puppeteer open the html test cases.
+    let {io} = serve();
+
+    io.on('connect', async socket => {
+        await prepareTestsList();
+
+        socket.emit('update', {tests: getTestsList()});
+        // TODO Stop previous?
+        socket.on('run', async testsNameList => {
+            await startTests(testsNameList, socket);
+            socket.emit('finish');
+        });
+        socket.on('stop', () => {
+            stopRunningTests();
+        });
+    });
+
+    console.log(`Dashboard: ${origin}/test/runTest/client/index.html`);
+    // open(`${origin}/test/runTest/client/index.html`);
+
+}
+
+start();
\ No newline at end of file
diff --git a/test/runTest/store.js b/test/runTest/store.js
new file mode 100644
index 0000000..4cd4fc9
--- /dev/null
+++ b/test/runTest/store.js
@@ -0,0 +1,78 @@
+const path = require('path');
+const fse = require('fs-extra');
+const fs = require('fs');
+const glob = require('glob');
+const {getTestName} = require('./util');
+const util = require('util');
+const blacklist = require('./blacklist');
+
+let _tests = [];
+let _testsMap = {};
+
+function getCacheFilePath() {
+    return path.join(__dirname, 'tmp/__cache__.json');;
+}
+
+module.exports.getTestsList = function () {
+    return _tests;
+};
+
+module.exports.getTestByFileUrl = function (url) {
+    return _testsMap[url];
+};
+
+module.exports.prepareTestsList = async function () {
+    let tmpFolder = path.join(__dirname, 'tmp');
+    fse.ensureDirSync(tmpFolder);
+    _tests = [];
+    _testsMap = {};
+    try {
+        let cachedStr = fs.readFileSync(getCacheFilePath(), 'utf-8');
+        _tests = JSON.parse(cachedStr);
+        _tests.forEach(test => {
+            // In somehow tests are stopped and leave the status pending.
+            // Set the status to unsettled again.
+            if (test.status === 'pending') {
+                test.status = 'unsettled';
+            }
+            _testsMap[test.fileUrl] = test;
+        });
+    }
+    catch(e) {
+        _tests = [];
+    }
+    // Find if there is new html file
+    let files = await util.promisify(glob)('**.html', { cwd: path.resolve(__dirname, '../') });
+    files.forEach(fileUrl => {
+        if (blacklist.includes(fileUrl)) {
+            return;
+        }
+        if (_testsMap[fileUrl]) {
+            return;
+        }
+
+        let test = {
+            fileUrl,
+            name: getTestName(fileUrl),
+            // Default status should be unkown
+            // status: 'pending',
+            results: []
+        };
+
+        _tests.push(test);
+        _testsMap[fileUrl] = test;
+    });
+    return _tests;
+};
+
+module.exports.saveTestsList = function () {
+    fse.outputFileSync(getCacheFilePath(), JSON.stringify(_tests, null, 2), 'utf-8');
+};
+
+module.exports.mergeTestsResults = function (testsResults) {
+    testsResults.forEach(testResult => {
+        if (_testsMap[testResult.fileUrl]) {
+            Object.assign(_testsMap[testResult.fileUrl], testResult);
+        }
+    });
+};
\ No newline at end of file
diff --git a/test/runTest/util.js b/test/runTest/util.js
new file mode 100644
index 0000000..95cab47
--- /dev/null
+++ b/test/runTest/util.js
@@ -0,0 +1,66 @@
+const path = require('path');
+const fse = require('fs-extra');
+const https = require('https');
+const fs = require('fs');
+const rollup = require('rollup');
+const resolve = require('rollup-plugin-node-resolve');
+const commonjs = require('rollup-plugin-commonjs');
+
+module.exports.getTestName = function(fileUrl) {
+    return path.basename(fileUrl, '.html');
+};
+
+
+function getVersionDir(version) {
+    version = version || 'developing';
+    return `tmp/__version__/${version}`;
+};
+module.exports.getVersionDir = getVersionDir;
+
+
+module.exports.prepareEChartsVersion = function (version) {
+    let versionFolder = path.join(__dirname, getVersionDir(version));
+    fse.ensureDirSync(versionFolder);
+    if (!version) {
+        // Developing version, make sure it's new build
+        return fse.copy(
+            path.join(__dirname, '../../dist/echarts.js'),
+            `${versionFolder}/echarts.js`
+        );
+    }
+    return new Promise(resolve => {
+        if (!fs.existsSync(`${versionFolder}/echarts.js`)) {
+            const file = fs.createWriteStream(`${versionFolder}/echarts.js`);
+
+            console.log('Downloading echarts4.2.1 from ', `https://cdn.jsdelivr.net/npm/echarts@${version}/dist/echarts.js`);
+            https.get(`https://cdn.jsdelivr.net/npm/echarts@${version}/dist/echarts.js`, response => {
+                response.pipe(file);
+
+                file.on('finish', () => {
+                    resolve();
+                });
+            });
+        }
+        else {
+            resolve();
+        }
+    });
+};
+
+module.exports.buildRuntimeCode = async function () {
+    const bundle = await rollup.rollup({
+        input: path.join(__dirname, 'runtime/main.js'),
+        plugins: [
+            resolve(),
+            commonjs()
+        ]
+    });
+    const output = await bundle.generate({
+        format: 'iife',
+        name: 'autorun'
+    });
+
+    // seedrandom use crypto as external module. Set it to null to avoid not defined error.
+    // TODO
+    return 'window.crypto = null\n' + output.code;
+};
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org