Evan You před 3 roky
rodič
revize
ab110cf8a9
51 změnil soubory, kde provedl 1207 přidání a 582 odebrání
  1. 15 15
      .github/workflows/ci.yml
  2. 6 0
      examples/.eslintrc.json
  3. 46 0
      examples/classic/commits/app.js
  4. 1 2
      examples/classic/commits/index.html
  5. 1 1
      examples/classic/elastic-header/index.html
  6. 0 0
      examples/classic/elastic-header/style.css
  7. 0 0
      examples/classic/firebase/app.js
  8. 1 1
      examples/classic/firebase/index.html
  9. 0 0
      examples/classic/firebase/style.css
  10. 0 0
      examples/classic/grid/grid.js
  11. 1 1
      examples/classic/grid/index.html
  12. 0 0
      examples/classic/grid/style.css
  13. 3 3
      examples/classic/markdown/index.html
  14. 0 0
      examples/classic/markdown/style.css
  15. 1 1
      examples/classic/modal/index.html
  16. 0 0
      examples/classic/modal/style.css
  17. 17 12
      examples/classic/move-animations/index.html
  18. 0 0
      examples/classic/select2/index.html
  19. 1 2
      examples/classic/svg/index.html
  20. 0 0
      examples/classic/svg/style.css
  21. 0 0
      examples/classic/svg/svg.js
  22. 0 0
      examples/classic/todomvc/app.js
  23. 4 4
      examples/classic/todomvc/index.html
  24. 0 0
      examples/classic/todomvc/readme.md
  25. 1 1
      examples/classic/tree/index.html
  26. 0 0
      examples/classic/tree/tree.js
  27. 0 56
      examples/commits/app.js
  28. 4 1
      package.json
  29. 188 0
      pnpm-lock.yaml
  30. 1 1
      test/e2e/async-edge-cases.html
  31. 44 0
      test/e2e/async-edge-cases.spec.ts
  32. 2 2
      test/e2e/basic-ssr.html
  33. 18 0
      test/e2e/basic-ssr.spec.ts
  34. 2 2
      test/e2e/commits.mock.ts
  35. 62 0
      test/e2e/commits.spec.ts
  36. 186 0
      test/e2e/e2eUtils.ts
  37. 115 0
      test/e2e/grid.spec.ts
  38. 46 0
      test/e2e/markdown.spec.ts
  39. 0 0
      test/e2e/modal.ts
  40. 0 0
      test/e2e/select2.ts
  41. 0 34
      test/e2e/specs/async-edge-cases.ts
  42. 0 8
      test/e2e/specs/basic-ssr.ts
  43. 0 23
      test/e2e/specs/commits.ts
  44. 0 105
      test/e2e/specs/grid.ts
  45. 0 19
      test/e2e/specs/markdown.ts
  46. 0 50
      test/e2e/specs/svg.ts
  47. 0 166
      test/e2e/specs/todomvc.ts
  48. 0 72
      test/e2e/specs/tree.ts
  49. 151 0
      test/e2e/svg.spec.ts
  50. 182 0
      test/e2e/todomvc.spec.ts
  51. 108 0
      test/e2e/tree.spec.ts

+ 15 - 15
.github/workflows/ci.yml

@@ -2,7 +2,7 @@ name: 'ci'
 on:
   push:
     branches:
-      - '**'
+      - main
   pull_request:
     branches:
       - main
@@ -45,24 +45,24 @@ jobs:
       - name: Run SSR tests
         run: pnpm run test:ssr
 
-  # e2e-test:
-  #   runs-on: ubuntu-latest
-  #   steps:
-  #     - uses: actions/checkout@v2
+  e2e-test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
 
-  #     - name: Install pnpm
-  #       uses: pnpm/action-setup@v2
+      - name: Install pnpm
+        uses: pnpm/action-setup@v2
 
-  #     - name: Set node version to 16
-  #       uses: actions/setup-node@v2
-  #       with:
-  #         node-version: 16
-  #         cache: 'pnpm'
+      - name: Set node version to 16
+        uses: actions/setup-node@v2
+        with:
+          node-version: 16
+          cache: 'pnpm'
 
-  #     - run: pnpm install
+      - run: pnpm install
 
-  #     - name: Run e2e tests
-  #       run: pnpm run test:e2e
+      - name: Run e2e tests
+        run: pnpm run test:e2e
 
   lint-and-test-dts:
     runs-on: ubuntu-latest

+ 6 - 0
examples/.eslintrc.json

@@ -0,0 +1,6 @@
+{
+  "globals": {
+    "Vue": true,
+    "firebase": true
+  }
+}

+ 46 - 0
examples/classic/commits/app.js

@@ -0,0 +1,46 @@
+var apiURL = 'https://api.github.com/repos/vuejs/vue/commits?per_page=3&sha='
+
+/**
+ * Actual demo
+ */
+
+new Vue({
+  el: '#demo',
+
+  data: {
+    branches: ['main', 'dev'],
+    currentBranch: 'main',
+    commits: null
+  },
+
+  created: function () {
+    this.fetchData()
+  },
+
+  watch: {
+    currentBranch: 'fetchData'
+  },
+
+  filters: {
+    truncate: function (v) {
+      var newline = v.indexOf('\n')
+      return newline > 0 ? v.slice(0, newline) : v
+    },
+    formatDate: function (v) {
+      return v.replace(/T|Z/g, ' ')
+    }
+  },
+
+  methods: {
+    fetchData: function () {
+      var self = this
+      var xhr = new XMLHttpRequest()
+      xhr.open('GET', apiURL + self.currentBranch)
+      xhr.onload = function () {
+        self.commits = JSON.parse(xhr.responseText)
+        console.log(self.commits[0].html_url)
+      }
+      xhr.send()
+    }
+  }
+})

+ 1 - 2
examples/commits/index.html → examples/classic/commits/index.html

@@ -19,7 +19,7 @@
       }
     </style>
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
   </head>
   <body>
     <div id="demo">
@@ -42,7 +42,6 @@
         </li>
       </ul>
     </div>
-    <script src="mock.js"></script>
     <script src="app.js"></script>
   </body>
 </html>

+ 1 - 1
examples/elastic-header/index.html → examples/classic/elastic-header/index.html

@@ -5,7 +5,7 @@
     <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
     <title>Vue.js elastic header example</title>
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
     <script src="http://dynamicsjs.com/lib/dynamics.js"></script>
     <link rel="stylesheet" href="style.css">
     <!-- template for the component -->

+ 0 - 0
examples/elastic-header/style.css → examples/classic/elastic-header/style.css


+ 0 - 0
examples/firebase/app.js → examples/classic/firebase/app.js


+ 1 - 1
examples/firebase/index.html → examples/classic/firebase/index.html

@@ -6,7 +6,7 @@
     <link rel="stylesheet" type="text/css" href="style.css">
     <!-- Vue -->
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
     <!-- Firebase -->
     <script src="https://www.gstatic.com/firebasejs/3.4.0/firebase.js"></script>
     <!-- VueFire -->

+ 0 - 0
examples/firebase/style.css → examples/classic/firebase/style.css


+ 0 - 0
examples/grid/grid.js → examples/classic/grid/grid.js


+ 1 - 1
examples/grid/index.html → examples/classic/grid/index.html

@@ -5,7 +5,7 @@
     <title>Vue.js grid component example</title>
     <link rel="stylesheet" href="style.css">
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
     </head>
   <body>
 

+ 0 - 0
examples/grid/style.css → examples/classic/grid/style.css


+ 3 - 3
examples/markdown/index.html → examples/classic/markdown/index.html

@@ -4,10 +4,10 @@
     <meta charset="utf-8">
     <title>Vue.js markdown editor example</title>
     <link rel="stylesheet" href="style.css">
-    <script src="https://unpkg.com/marked@0.3.6"></script>
-    <script src="https://unpkg.com/lodash@4.16.0"></script>
+    <script src="../../../node_modules/marked/marked.min.js"></script>
+    <script src="../../../node_modules/lodash/lodash.min.js"></script>
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
   </head>
   <body>
 

+ 0 - 0
examples/markdown/style.css → examples/classic/markdown/style.css


+ 1 - 1
examples/modal/index.html → examples/classic/modal/index.html

@@ -4,7 +4,7 @@
     <meta charset="utf-8">
     <title>Vue.js modal component example</title>
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
     <link rel="stylesheet" href="style.css">
   </head>
   <body>

+ 0 - 0
examples/modal/style.css → examples/classic/modal/style.css


+ 17 - 12
examples/move-animations/index.html → examples/classic/move-animations/index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
   <head>
-    <meta charset="utf-8">
+    <meta charset="utf-8" />
     <title>Move Animations</title>
     <style>
       .container {
@@ -16,11 +16,14 @@
         box-sizing: border-box;
       }
       /* 1. declare transition */
-      .fade-move, .fade-enter-active, .fade-leave-active {
-        transition: all .5s cubic-bezier(.55,0,.1,1);
+      .fade-move,
+      .fade-enter-active,
+      .fade-leave-active {
+        transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
       }
       /* 2. declare enter from and leave to state */
-      .fade-enter, .fade-leave-to {
+      .fade-enter,
+      .fade-leave-to {
         opacity: 0;
         transform: scaleY(0.01) translate(30px, 0);
       }
@@ -30,9 +33,9 @@
         position: absolute;
       }
     </style>
-    <script src="https://cdn.jsdelivr.net/lodash/4.3.0/lodash.min.js"></script>
+    <script src="../../../node_modules/lodash/lodash.min.js"></script>
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
   </head>
   <body>
     <div id="el">
@@ -40,11 +43,13 @@
       <button @click="reset">reset</button>
       <button @click="shuffle">shuffle</button>
       <transition-group tag="ul" name="fade" class="container">
-        <item v-for="item in items"
+        <item
+          v-for="item in items"
           class="item"
           :msg="item"
           :key="item"
-          @rm="remove(item)">
+          @rm="remove(item)"
+        >
         </item>
       </transition-group>
     </div>
@@ -65,17 +70,17 @@
           }
         },
         methods: {
-          insert () {
+          insert() {
             var i = Math.round(Math.random() * this.items.length)
             this.items.splice(i, 0, id++)
           },
-          reset () {
+          reset() {
             this.items = [1, 2, 3, 4, 5]
           },
-          shuffle () {
+          shuffle() {
             this.items = _.shuffle(this.items)
           },
-          remove (item) {
+          remove(item) {
             var i = this.items.indexOf(item)
             if (i > -1) {
               this.items.splice(i, 1)

+ 0 - 0
examples/select2/index.html → examples/classic/select2/index.html


+ 1 - 2
examples/svg/index.html → examples/classic/svg/index.html

@@ -5,8 +5,7 @@
     <title>Vue.js SVG graph example</title>
     <link rel="stylesheet" href="style.css">
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
-    <script src="https://unpkg.com/marky/dist/marky.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
   </head>
   <body>
 

+ 0 - 0
examples/svg/style.css → examples/classic/svg/style.css


+ 0 - 0
examples/svg/svg.js → examples/classic/svg/svg.js


+ 0 - 0
examples/todomvc/app.js → examples/classic/todomvc/app.js


+ 4 - 4
examples/todomvc/index.html → examples/classic/todomvc/index.html

@@ -3,8 +3,7 @@
   <head>
     <meta charset="utf-8">
     <title>Vue.js • TodoMVC</title>
-    <link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@2.0.4/index.css">
-    <script src="https://unpkg.com/director@1.2.8/build/director.js"></script>
+    <link rel="stylesheet" href="../../../node_modules/todomvc-app-css/index.css">
     <style>[v-cloak] { display: none; }</style>
   </head>
   <body>
@@ -18,7 +17,8 @@
           @keyup.enter="addTodo">
       </header>
       <section class="main" v-show="todos.length" v-cloak>
-        <input class="toggle-all" type="checkbox" v-model="allDone">
+        <input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
+        <label for="toggle-all">Mark all as complete</label>
         <ul class="todo-list">
           <li v-for="todo in filteredTodos"
             class="todo"
@@ -63,7 +63,7 @@
     if (navigator.userAgent.indexOf('PhantomJS') > -1) localStorage.clear()
     </script>
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
     <script src="app.js"></script>
   </body>
 </html>

+ 0 - 0
examples/todomvc/readme.md → examples/classic/todomvc/readme.md


+ 1 - 1
examples/tree/index.html → examples/classic/tree/index.html

@@ -21,7 +21,7 @@
       }
     </style>
     <!-- Delete ".min" for console warnings in development -->
-    <script src="../../dist/vue.min.js"></script>
+    <script src="../../../dist/vue.min.js"></script>
   </head>
   <body>
 

+ 0 - 0
examples/tree/tree.js → examples/classic/tree/tree.js


+ 0 - 56
examples/commits/app.js

@@ -1,56 +0,0 @@
-/* global Vue */
-
-var apiURL = 'https://api.github.com/repos/vuejs/vue/commits?per_page=3&sha='
-
-/**
- * Actual demo
- */
-
-new Vue({
-
-  el: '#demo',
-
-  data: {
-    branches: ['master', 'dev'],
-    currentBranch: 'master',
-    commits: null
-  },
-
-  created: function () {
-    this.fetchData()
-  },
-
-  watch: {
-    currentBranch: 'fetchData'
-  },
-
-  filters: {
-    truncate: function (v) {
-      var newline = v.indexOf('\n')
-      return newline > 0 ? v.slice(0, newline) : v
-    },
-    formatDate: function (v) {
-      return v.replace(/T|Z/g, ' ')
-    }
-  },
-
-  methods: {
-    fetchData: function () {
-      var self = this
-      if (navigator.userAgent.indexOf('PhantomJS') > -1) {
-        // use mocks in e2e to avoid dependency on network / authentication
-        setTimeout(function () {
-          self.commits = window.MOCKS[self.currentBranch]
-        }, 0)
-      } else {
-        var xhr = new XMLHttpRequest()
-        xhr.open('GET', apiURL + self.currentBranch)
-        xhr.onload = function () {
-          self.commits = JSON.parse(xhr.responseText)
-          console.log(self.commits[0].html_url)
-        }
-        xhr.send()
-      }
-    }
-  }
-})

+ 4 - 1
package.json

@@ -25,7 +25,7 @@
     "test": "npm run lint && npm run ts-check && npm run test:types && npm run test:unit && npm run test:e2e",
     "test:unit": "vitest run test/unit",
     "test:ssr": "npm run build:ssr && vitest run test/ssr",
-    "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.ts",
+    "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && vitest run test/e2e",
     "test:types": "tsc -p ./types/tsconfig.json",
     "lint": "eslint src scripts test",
     "ts-check": "tsc -p tsconfig.json --noEmit",
@@ -85,7 +85,9 @@
     "lodash.template": "^4.4.0",
     "lodash.uniq": "^4.5.0",
     "lru-cache": "^7.8.1",
+    "marked": "^3.0.8",
     "memory-fs": "^0.5.0",
+    "puppeteer": "^14.1.1",
     "resolve": "^1.22.0",
     "rollup": "^2.70.2",
     "rollup-plugin-typescript2": "^0.31.2",
@@ -93,6 +95,7 @@
     "shelljs": "^0.8.5",
     "source-map": "0.5.6",
     "terser": "^5.13.1",
+    "todomvc-app-css": "^2.4.2",
     "ts-node": "^10.7.0",
     "tslib": "^2.4.0",
     "typescript": "^4.6.4",

+ 188 - 0
pnpm-lock.yaml

@@ -28,7 +28,9 @@ specifiers:
   lodash.template: ^4.4.0
   lodash.uniq: ^4.5.0
   lru-cache: ^7.8.1
+  marked: ^3.0.8
   memory-fs: ^0.5.0
+  puppeteer: ^14.1.1
   resolve: ^1.22.0
   rollup: ^2.70.2
   rollup-plugin-typescript2: ^0.31.2
@@ -36,6 +38,7 @@ specifiers:
   shelljs: ^0.8.5
   source-map: 0.5.6
   terser: ^5.13.1
+  todomvc-app-css: ^2.4.2
   ts-node: ^10.7.0
   tslib: ^2.4.0
   typescript: ^4.6.4
@@ -71,7 +74,9 @@ devDependencies:
   lodash.template: 4.5.0
   lodash.uniq: 4.5.0
   lru-cache: 7.10.1
+  marked: 3.0.8
   memory-fs: 0.5.0
+  puppeteer: 14.1.1
   resolve: 1.22.0
   rollup: 2.74.0
   rollup-plugin-typescript2: 0.31.2_dyu3set7imqii5ytavmnwecwpy
@@ -79,6 +84,7 @@ devDependencies:
   shelljs: 0.8.5
   source-map: 0.5.6
   terser: 5.13.1
+  todomvc-app-css: 2.4.2
   ts-node: 10.7.0_3smuweqyuzdazdnyhhezld6mfa
   tslib: 2.4.0
   typescript: 4.6.4
@@ -378,6 +384,14 @@ packages:
       '@types/node': 17.0.34
     dev: true
 
+  /@types/yauzl/2.10.0:
+    resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
+    requiresBuild: true
+    dependencies:
+      '@types/node': 17.0.34
+    dev: true
+    optional: true
+
   /@typescript-eslint/eslint-plugin/5.25.0_qo2hgs5jt7x2a3p77h2rutcdae:
     resolution: {integrity: sha512-icYrFnUzvm+LhW0QeJNKkezBu6tJs9p/53dpPLFH8zoM9w1tfaKzVurkPotEpAqQ8Vf8uaFyL5jHd0Vs6Z0ZQg==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -953,6 +967,14 @@ packages:
     dev: true
     optional: true
 
+  /bl/4.1.0:
+    resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+    dependencies:
+      buffer: 5.7.1
+      inherits: 2.0.4
+      readable-stream: 3.6.0
+    dev: true
+
   /bluebird/3.7.2:
     resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
     dev: true
@@ -1060,6 +1082,10 @@ packages:
       pako: 1.0.11
     dev: true
 
+  /buffer-crc32/0.2.13:
+    resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
+    dev: true
+
   /buffer-from/1.1.2:
     resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
     dev: true
@@ -1076,6 +1102,13 @@ packages:
       isarray: 1.0.0
     dev: true
 
+  /buffer/5.7.1:
+    resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+    dev: true
+
   /builtin-modules/3.3.0:
     resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==}
     engines: {node: '>=6'}
@@ -1639,6 +1672,14 @@ packages:
     resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
     dev: true
 
+  /cross-fetch/3.1.5:
+    resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==}
+    dependencies:
+      node-fetch: 2.6.7
+    transitivePeerDependencies:
+      - encoding
+    dev: true
+
   /cross-spawn/5.1.0:
     resolution: {integrity: sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=}
     dependencies:
@@ -1869,6 +1910,10 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /devtools-protocol/0.0.982423:
+    resolution: {integrity: sha512-FnVW2nDbjGNw1uD/JRC+9U5768W7e1TfUwqbDTcSsAu1jXFjITSX8w3rkW5FEpHRMPPGpvNSmO1pOpqByiWscA==}
+    dev: true
+
   /diff/4.0.2:
     resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
     engines: {node: '>=0.3.1'}
@@ -2459,6 +2504,20 @@ packages:
       - supports-color
     dev: true
 
+  /extract-zip/2.0.1:
+    resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
+    engines: {node: '>= 10.17.0'}
+    hasBin: true
+    dependencies:
+      debug: 4.3.4
+      get-stream: 5.2.0
+      yauzl: 2.10.0
+    optionalDependencies:
+      '@types/yauzl': 2.10.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /fast-deep-equal/3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
     dev: true
@@ -2488,6 +2547,12 @@ packages:
       reusify: 1.0.4
     dev: true
 
+  /fd-slicer/1.1.0:
+    resolution: {integrity: sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=}
+    dependencies:
+      pend: 1.2.0
+    dev: true
+
   /figgy-pudding/3.5.2:
     resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==}
     dev: true
@@ -2655,6 +2720,10 @@ packages:
       readable-stream: 2.3.7
     dev: true
 
+  /fs-constants/1.0.0:
+    resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+    dev: true
+
   /fs-extra/10.1.0:
     resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
     engines: {node: '>=12'}
@@ -2738,6 +2807,13 @@ packages:
     engines: {node: '>=4'}
     dev: true
 
+  /get-stream/5.2.0:
+    resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
+    engines: {node: '>=8'}
+    dependencies:
+      pump: 3.0.0
+    dev: true
+
   /get-stream/6.0.1:
     resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
     engines: {node: '>=10'}
@@ -3742,6 +3818,12 @@ packages:
       object-visit: 1.0.1
     dev: true
 
+  /marked/3.0.8:
+    resolution: {integrity: sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==}
+    engines: {node: '>= 12'}
+    hasBin: true
+    dev: true
+
   /md5.js/1.3.5:
     resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
     dependencies:
@@ -3914,6 +3996,10 @@ packages:
       is-extendable: 1.0.1
     dev: true
 
+  /mkdirp-classic/0.5.3:
+    resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+    dev: true
+
   /mkdirp/0.5.6:
     resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
     hasBin: true
@@ -3987,6 +4073,18 @@ packages:
     resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
     dev: true
 
+  /node-fetch/2.6.7:
+    resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
+    engines: {node: 4.x || >=6.0.0}
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+    dependencies:
+      whatwg-url: 5.0.0
+    dev: true
+
   /node-libs-browser/2.2.1:
     resolution: {integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==}
     dependencies:
@@ -4364,6 +4462,10 @@ packages:
       sha.js: 2.4.11
     dev: true
 
+  /pend/1.2.0:
+    resolution: {integrity: sha1-elfrVQpng/kRUzH89GY9XI4AelA=}
+    dev: true
+
   /picocolors/1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
     dev: true
@@ -4448,6 +4550,11 @@ packages:
     engines: {node: '>= 0.6.0'}
     dev: true
 
+  /progress/2.0.3:
+    resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
+    engines: {node: '>=0.4.0'}
+    dev: true
+
   /promise-inflight/1.0.1_bluebird@3.7.2:
     resolution: {integrity: sha1-mEcocL8igTL8vdhoEputEsPAKeM=}
     peerDependencies:
@@ -4459,6 +4566,10 @@ packages:
       bluebird: 3.7.2
     dev: true
 
+  /proxy-from-env/1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+    dev: true
+
   /prr/1.0.1:
     resolution: {integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY=}
     dev: true
@@ -4517,6 +4628,30 @@ packages:
     engines: {node: '>=6'}
     dev: true
 
+  /puppeteer/14.1.1:
+    resolution: {integrity: sha512-4dC6GYR5YlXTmNO3TbYEHTdVSdml1cVD2Ok/h/f/xSTp4ITVdbRWkMjiOaEKRAhtIl6GqaP7B89zx+hfhcNGMQ==}
+    engines: {node: '>=14.1.0'}
+    requiresBuild: true
+    dependencies:
+      cross-fetch: 3.1.5
+      debug: 4.3.4
+      devtools-protocol: 0.0.982423
+      extract-zip: 2.0.1
+      https-proxy-agent: 5.0.1
+      pkg-dir: 4.2.0
+      progress: 2.0.3
+      proxy-from-env: 1.1.0
+      rimraf: 3.0.2
+      tar-fs: 2.1.1
+      unbzip2-stream: 1.4.3
+      ws: 8.6.0
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - supports-color
+      - utf-8-validate
+    dev: true
+
   /q/1.5.1:
     resolution: {integrity: sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=}
     engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
@@ -5295,6 +5430,26 @@ packages:
     engines: {node: '>=6'}
     dev: true
 
+  /tar-fs/2.1.1:
+    resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
+    dependencies:
+      chownr: 1.1.4
+      mkdirp-classic: 0.5.3
+      pump: 3.0.0
+      tar-stream: 2.2.0
+    dev: true
+
+  /tar-stream/2.2.0:
+    resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
+    engines: {node: '>=6'}
+    dependencies:
+      bl: 4.1.0
+      end-of-stream: 1.4.4
+      fs-constants: 1.0.0
+      inherits: 2.0.4
+      readable-stream: 3.6.0
+    dev: true
+
   /terser-webpack-plugin/1.4.5_webpack@4.46.0:
     resolution: {integrity: sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==}
     engines: {node: '>= 6.9.0'}
@@ -5421,6 +5576,10 @@ packages:
       safe-regex: 1.1.0
     dev: true
 
+  /todomvc-app-css/2.4.2:
+    resolution: {integrity: sha512-ViAkQ7ed89rmhFIGRsT36njN+97z8+s3XsJnB8E2IKOq+/SLD/6PtSvmTtiwUcVk39qPcjAc/OyeDys4LoJUVg==}
+    dev: true
+
   /tough-cookie/4.0.0:
     resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==}
     engines: {node: '>=6'}
@@ -5430,6 +5589,10 @@ packages:
       universalify: 0.1.2
     dev: true
 
+  /tr46/0.0.3:
+    resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=}
+    dev: true
+
   /tr46/1.0.1:
     resolution: {integrity: sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=}
     dependencies:
@@ -5563,6 +5726,13 @@ packages:
     dev: true
     optional: true
 
+  /unbzip2-stream/1.4.3:
+    resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==}
+    dependencies:
+      buffer: 5.7.1
+      through: 2.3.8
+    dev: true
+
   /union-value/1.0.1:
     resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==}
     engines: {node: '>=0.10.0'}
@@ -5768,6 +5938,10 @@ packages:
       - supports-color
     dev: true
 
+  /webidl-conversions/3.0.1:
+    resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=}
+    dev: true
+
   /webidl-conversions/4.0.2:
     resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
     dev: true
@@ -5852,6 +6026,13 @@ packages:
       webidl-conversions: 7.0.0
     dev: true
 
+  /whatwg-url/5.0.0:
+    resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=}
+    dependencies:
+      tr46: 0.0.3
+      webidl-conversions: 3.0.1
+    dev: true
+
   /whatwg-url/7.1.0:
     resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
     dependencies:
@@ -5983,6 +6164,13 @@ packages:
       yargs-parser: 20.2.9
     dev: true
 
+  /yauzl/2.10.0:
+    resolution: {integrity: sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=}
+    dependencies:
+      buffer-crc32: 0.2.13
+      fd-slicer: 1.1.0
+    dev: true
+
   /yn/3.1.1:
     resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
     engines: {node: '>=6'}

+ 1 - 1
test/e2e/specs/async-edge-cases.html → test/e2e/async-edge-cases.html

@@ -3,7 +3,7 @@
   <head>
     <meta charset="utf-8">
     <title></title>
-    <script src="../../../dist/vue.min.js"></script>
+    <script src="../../dist/vue.min.js"></script>
   </head>
   <body>
     <!-- #4510 click and change event on checkbox -->

+ 44 - 0
test/e2e/async-edge-cases.spec.ts

@@ -0,0 +1,44 @@
+// @vitest-environment node
+import path from 'path'
+import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
+
+describe('basic-ssr', () => {
+  const { page, text, click, isChecked } = setupPuppeteer()
+
+  test(
+    'should work',
+    async () => {
+      await page().goto(
+        `file://${path.resolve(__dirname, `async-edge-cases.html`)}`
+      )
+
+      // #4510
+      expect(await text('#case-1')).toContain('1')
+      expect(await isChecked('#case-1 input')).toBe(false)
+
+      await click('#case-1 input')
+      expect(await text('#case-1')).toContain('2')
+      expect(await isChecked('#case-1 input')).toBe(true)
+
+      await click('#case-1 input')
+      expect(await text('#case-1')).toContain('3')
+      expect(await isChecked('#case-1 input')).toBe(false)
+
+      // #6566
+      expect(await text('#case-2 button')).toContain('Expand is True')
+      expect(await text('.count-a')).toContain('countA: 0')
+      expect(await text('.count-b')).toContain('countB: 0')
+
+      await click('#case-2 button')
+      expect(await text('#case-2 button')).toContain('Expand is False')
+      expect(await text('.count-a')).toContain('countA: 1')
+      expect(await text('.count-b')).toContain('countB: 0')
+
+      await click('#case-2 button')
+      expect(await text('#case-2 button')).toContain('Expand is True')
+      expect(await text('.count-a')).toContain('countA: 1')
+      expect(await text('.count-b')).toContain('countB: 1')
+    },
+    E2E_TIMEOUT
+  )
+})

+ 2 - 2
test/e2e/specs/basic-ssr.html → test/e2e/basic-ssr.html

@@ -5,8 +5,8 @@
     <title></title>
   </head>
   <body>
-    <script src="../../../dist/vue.min.js"></script>
-    <script src="../../../packages/vue-server-renderer/basic.js"></script>
+    <script src="../../dist/vue.min.js"></script>
+    <script src="../../packages/vue-server-renderer/basic.js"></script>
 
     <div id="result">wtf</div>
 

+ 18 - 0
test/e2e/basic-ssr.spec.ts

@@ -0,0 +1,18 @@
+// @vitest-environment node
+import path from 'path'
+import { E2E_TIMEOUT, setupPuppeteer } from './e2eUtils'
+
+describe('basic-ssr', () => {
+  const { page, text } = setupPuppeteer()
+
+  test(
+    'should work',
+    async () => {
+      await page().goto(`file://${path.resolve(__dirname, `basic-ssr.html`)}`)
+      expect(await text('#result')).toContain(
+        `<div data-server-rendered="true">foo</div>`
+      )
+    },
+    E2E_TIMEOUT
+  )
+})

+ 2 - 2
examples/commits/mock.js → test/e2e/commits.mock.ts

@@ -1,5 +1,5 @@
-window.MOCKS = {
-  master: [
+export default {
+  main: [
     {
       sha: "0948d999f2fddf9f90991956493f976273c5da1f",
       node_id:

+ 62 - 0
test/e2e/commits.spec.ts

@@ -0,0 +1,62 @@
+// @vitest-environment node
+import { setupPuppeteer, getExampleUrl, E2E_TIMEOUT } from './e2eUtils'
+import mocks from './commits.mock'
+
+describe('e2e: commits', () => {
+  const { page, click, count, text, isChecked } = setupPuppeteer()
+
+  async function testCommits(apiType: 'classic' | 'composition') {
+    // intercept and mock the response to avoid hitting the actual API
+    await page().setRequestInterception(true)
+    page().on('request', (req) => {
+      const match = req.url().match(/&sha=(.*)$/)
+      if (!match) {
+        req.continue()
+      } else {
+        const ret = JSON.stringify(mocks[match[1] as 'main' | 'dev'])
+        req.respond({
+          status: 200,
+          contentType: 'application/json',
+          headers: { 'Access-Control-Allow-Origin': '*' },
+          body: ret
+        })
+      }
+    })
+
+    await page().goto(getExampleUrl('commits', apiType))
+    await page().waitForSelector('li')
+
+    expect(await count('input')).toBe(2)
+    expect(await count('label')).toBe(2)
+    expect(await text('label[for="main"]')).toBe('main')
+    expect(await text('label[for="dev"]')).toBe('dev')
+    expect(await isChecked('#main')).toBe(true)
+    expect(await isChecked('#dev')).toBe(false)
+    expect(await text('p')).toBe('vuejs/vue@main')
+    expect(await count('li')).toBe(3)
+    expect(await count('li .commit')).toBe(3)
+    expect(await count('li .message')).toBe(3)
+
+    await click('#dev')
+    expect(await text('p')).toBe('vuejs/vue@dev')
+    expect(await count('li')).toBe(3)
+    expect(await count('li .commit')).toBe(3)
+    expect(await count('li .message')).toBe(3)
+  }
+
+  test(
+    'classic',
+    async () => {
+      await testCommits('classic')
+    },
+    E2E_TIMEOUT
+  )
+
+  // test(
+  //   'composition',
+  //   async () => {
+  //     await testCommits('composition')
+  //   },
+  //   E2E_TIMEOUT
+  // )
+})

+ 186 - 0
test/e2e/e2eUtils.ts

@@ -0,0 +1,186 @@
+import path from 'path'
+import puppeteer from 'puppeteer'
+
+export function getExampleUrl(name: string, apiType: 'classic' | 'composition') {
+  return `file://${path.resolve(
+    __dirname,
+    `../../examples/${apiType}/${name}/index.html`
+  )}`
+}
+
+export const E2E_TIMEOUT = 30 * 1000
+
+const puppeteerOptions = process.env.CI
+  ? { args: ['--no-sandbox', '--disable-setuid-sandbox'] }
+  : { headless: !process.env.DEBUG }
+
+const maxTries = 30
+export const timeout = (n: number) => new Promise((r) => setTimeout(r, n))
+
+export async function expectByPolling(
+  poll: () => Promise<any>,
+  expected: string
+) {
+  for (let tries = 0; tries < maxTries; tries++) {
+    const actual = (await poll()) || ''
+    if (actual.indexOf(expected) > -1 || tries === maxTries - 1) {
+      expect(actual).toMatch(expected)
+      break
+    } else {
+      await timeout(50)
+    }
+  }
+}
+
+export function setupPuppeteer() {
+  let browser: puppeteer.Browser
+  let page: puppeteer.Page
+
+  beforeAll(async () => {
+    browser = await puppeteer.launch(puppeteerOptions)
+  })
+
+  beforeEach(async () => {
+    page = await browser.newPage()
+
+    await page.evaluateOnNewDocument(() => {
+      localStorage.clear()
+    })
+
+    page.on('console', (e) => {
+      if (e.type() === 'error') {
+        const err = e.args()[0]
+        console.error(
+          `Error from Puppeteer-loaded page:\n`,
+          err._remoteObject.description
+        )
+      }
+    })
+  })
+
+  afterEach(async () => {
+    await page.close()
+  })
+
+  afterAll(async () => {
+    await browser.close()
+  })
+
+  async function click(selector: string, options?: puppeteer.ClickOptions) {
+    await page.click(selector, options)
+  }
+
+  async function count(selector: string) {
+    return (await page.$$(selector)).length
+  }
+
+  async function text(selector: string) {
+    return await page.$eval(selector, (node) => node.textContent)
+  }
+
+  async function value(selector: string) {
+    return await page.$eval(
+      selector,
+      (node) => (node as HTMLInputElement).value
+    )
+  }
+
+  async function html(selector: string) {
+    return await page.$eval(selector, (node) => node.innerHTML)
+  }
+
+  async function classList(selector: string) {
+    return await page.$eval(selector, (node: any) => [...node.classList])
+  }
+
+  async function childrenCount(selector: string) {
+    return await page.$eval(selector, (node: any) => node.children.length)
+  }
+
+  async function isVisible(selector: string) {
+    const display = await page.$eval(selector, (node) => {
+      return window.getComputedStyle(node).display
+    })
+    return display !== 'none'
+  }
+
+  async function isChecked(selector: string) {
+    return await page.$eval(
+      selector,
+      (node) => (node as HTMLInputElement).checked
+    )
+  }
+
+  async function isFocused(selector: string) {
+    return await page.$eval(selector, (node) => node === document.activeElement)
+  }
+
+  async function setValue(selector: string, value: string) {
+    await page.$eval(
+      selector,
+      (node, value) => {
+        ;(node as HTMLInputElement).value = value as string
+        node.dispatchEvent(new Event('input'))
+      },
+      value
+    )
+  }
+
+  async function typeValue(selector: string, value: string) {
+    const el = (await page.$(selector))!
+    await el.evaluate((node) => ((node as HTMLInputElement).value = ''))
+    await el.type(value)
+  }
+
+  async function enterValue(selector: string, value: string) {
+    const el = (await page.$(selector))!
+    await el.evaluate((node) => ((node as HTMLInputElement).value = ''))
+    await el.type(value)
+    await el.press('Enter')
+  }
+
+  async function clearValue(selector: string) {
+    return await page.$eval(
+      selector,
+      (node) => ((node as HTMLInputElement).value = '')
+    )
+  }
+
+  function timeout(time: number) {
+    return page.evaluate((time) => {
+      return new Promise((r) => {
+        setTimeout(r, time)
+      })
+    }, time)
+  }
+
+  function nextFrame() {
+    return page.evaluate(() => {
+      return new Promise((resolve) => {
+        requestAnimationFrame(() => {
+          requestAnimationFrame(resolve)
+        })
+      })
+    })
+  }
+
+  return {
+    page: () => page,
+    click,
+    count,
+    text,
+    value,
+    html,
+    classList,
+    childrenCount,
+    isVisible,
+    isChecked,
+    isFocused,
+    setValue,
+    typeValue,
+    enterValue,
+    clearValue,
+    timeout,
+    nextFrame
+  }
+}

+ 115 - 0
test/e2e/grid.spec.ts

@@ -0,0 +1,115 @@
+import { setupPuppeteer, getExampleUrl, E2E_TIMEOUT } from './e2eUtils'
+
+interface TableData {
+  name: string
+  power: number
+}
+
+describe('e2e: grid', () => {
+  const { page, click, text, count, typeValue, clearValue } = setupPuppeteer()
+  const columns = ['name', 'power'] as const
+
+  async function assertTable(data: TableData[]) {
+    expect(await count('td')).toBe(data.length * columns.length)
+    for (let i = 0; i < data.length; i++) {
+      for (let j = 0; j < columns.length; j++) {
+        expect(
+          await text(`tr:nth-child(${i + 1}) td:nth-child(${j + 1})`)
+        ).toContain(`${data[i][columns[j]]}`)
+      }
+    }
+  }
+
+  async function testGrid(apiType: 'classic' | 'composition') {
+    await page().goto(getExampleUrl('grid', apiType))
+    await page().waitForSelector('table')
+    expect(await count('th')).toBe(2)
+    expect(await count('th.active')).toBe(0)
+    expect(await text('th:nth-child(1)')).toContain('Name')
+    expect(await text('th:nth-child(2)')).toContain('Power')
+    await assertTable([
+      { name: 'Chuck Norris', power: Infinity },
+      { name: 'Bruce Lee', power: 9000 },
+      { name: 'Jackie Chan', power: 7000 },
+      { name: 'Jet Li', power: 8000 }
+    ])
+
+    await click('th:nth-child(1)')
+    expect(await count('th.active:nth-child(1)')).toBe(1)
+    expect(await count('th.active:nth-child(2)')).toBe(0)
+    expect(await count('th:nth-child(1) .arrow.dsc')).toBe(1)
+    expect(await count('th:nth-child(2) .arrow.dsc')).toBe(0)
+    await assertTable([
+      { name: 'Jet Li', power: 8000 },
+      { name: 'Jackie Chan', power: 7000 },
+      { name: 'Chuck Norris', power: Infinity },
+      { name: 'Bruce Lee', power: 9000 }
+    ])
+
+    await click('th:nth-child(2)')
+    expect(await count('th.active:nth-child(1)')).toBe(0)
+    expect(await count('th.active:nth-child(2)')).toBe(1)
+    expect(await count('th:nth-child(1) .arrow.dsc')).toBe(1)
+    expect(await count('th:nth-child(2) .arrow.dsc')).toBe(1)
+    await assertTable([
+      { name: 'Chuck Norris', power: Infinity },
+      { name: 'Bruce Lee', power: 9000 },
+      { name: 'Jet Li', power: 8000 },
+      { name: 'Jackie Chan', power: 7000 }
+    ])
+
+    await click('th:nth-child(2)')
+    expect(await count('th.active:nth-child(1)')).toBe(0)
+    expect(await count('th.active:nth-child(2)')).toBe(1)
+    expect(await count('th:nth-child(1) .arrow.dsc')).toBe(1)
+    expect(await count('th:nth-child(2) .arrow.asc')).toBe(1)
+    await assertTable([
+      { name: 'Jackie Chan', power: 7000 },
+      { name: 'Jet Li', power: 8000 },
+      { name: 'Bruce Lee', power: 9000 },
+      { name: 'Chuck Norris', power: Infinity }
+    ])
+
+    await click('th:nth-child(1)')
+    expect(await count('th.active:nth-child(1)')).toBe(1)
+    expect(await count('th.active:nth-child(2)')).toBe(0)
+    expect(await count('th:nth-child(1) .arrow.asc')).toBe(1)
+    expect(await count('th:nth-child(2) .arrow.asc')).toBe(1)
+    await assertTable([
+      { name: 'Bruce Lee', power: 9000 },
+      { name: 'Chuck Norris', power: Infinity },
+      { name: 'Jackie Chan', power: 7000 },
+      { name: 'Jet Li', power: 8000 }
+    ])
+
+    await typeValue('input[name="query"]', 'j')
+    await assertTable([
+      { name: 'Jackie Chan', power: 7000 },
+      { name: 'Jet Li', power: 8000 }
+    ])
+
+    await typeValue('input[name="query"]', 'infinity')
+    await assertTable([{ name: 'Chuck Norris', power: Infinity }])
+
+    await clearValue('input[name="query"]')
+    expect(await count('p')).toBe(0)
+    await typeValue('input[name="query"]', 'stringthatdoesnotexistanywhere')
+    expect(await count('p')).toBe(1)
+  }
+
+  test(
+    'classic',
+    async () => {
+      await testGrid('classic')
+    },
+    E2E_TIMEOUT
+  )
+
+  // test(
+  //   'composition',
+  //   async () => {
+  //     await testGrid('composition')
+  //   },
+  //   E2E_TIMEOUT
+  // )
+})

+ 46 - 0
test/e2e/markdown.spec.ts

@@ -0,0 +1,46 @@
+import {
+  setupPuppeteer,
+  expectByPolling,
+  getExampleUrl,
+  E2E_TIMEOUT
+} from './e2eUtils'
+
+describe('e2e: markdown', () => {
+  const { page, isVisible, value, html } = setupPuppeteer()
+
+  async function testMarkdown(apiType: 'classic' | 'composition') {
+    await page().goto(getExampleUrl('markdown', apiType))
+    expect(await isVisible('#editor')).toBe(true)
+    expect(await value('textarea')).toBe('# hello')
+    expect(await html('#editor div')).toBe('<h1 id="hello">hello</h1>\n')
+
+    await page().type('textarea', '\n## foo\n\n- bar\n- baz')
+
+    // assert the output is not updated yet because of debounce
+    // debounce has become unstable on CI so this assertion is disabled
+    // expect(await html('#editor div')).toBe('<h1 id="hello">hello</h1>\n')
+
+    await expectByPolling(
+      () => html('#editor div'),
+      '<h1 id="hello">hello</h1>\n' +
+        '<h2 id="foo">foo</h2>\n' +
+        '<ul>\n<li>bar</li>\n<li>baz</li>\n</ul>\n'
+    )
+  }
+
+  test(
+    'classic',
+    async () => {
+      await testMarkdown('classic')
+    },
+    E2E_TIMEOUT
+  )
+
+  // test(
+  //   'composition',
+  //   async () => {
+  //     await testMarkdown('composition')
+  //   },
+  //   E2E_TIMEOUT
+  // )
+})

+ 0 - 0
test/e2e/specs/modal.ts → test/e2e/modal.ts


+ 0 - 0
test/e2e/specs/select2.ts → test/e2e/select2.ts


+ 0 - 34
test/e2e/specs/async-edge-cases.ts

@@ -1,34 +0,0 @@
-module.exports = {
-  'async edge cases': function (browser) {
-    browser
-    .url('http://localhost:8080/test/e2e/specs/async-edge-cases.html')
-      // #4510
-      .assert.containsText('#case-1', '1')
-      .assert.checked('#case-1 input', false)
-
-      .click('#case-1 input')
-      .assert.containsText('#case-1', '2')
-      .assert.checked('#case-1 input', true)
-
-      .click('#case-1 input')
-      .assert.containsText('#case-1', '3')
-      .assert.checked('#case-1 input', false)
-
-      // // #6566
-      .assert.containsText('#case-2 button', 'Expand is True')
-      .assert.containsText('.count-a', 'countA: 0')
-      .assert.containsText('.count-b', 'countB: 0')
-
-      .click('#case-2 button')
-      .assert.containsText('#case-2 button', 'Expand is False')
-      .assert.containsText('.count-a', 'countA: 1')
-      .assert.containsText('.count-b', 'countB: 0')
-
-      .click('#case-2 button')
-      .assert.containsText('#case-2 button', 'Expand is True')
-      .assert.containsText('.count-a', 'countA: 1')
-      .assert.containsText('.count-b', 'countB: 1')
-
-      .end()
-  }
-}

+ 0 - 8
test/e2e/specs/basic-ssr.ts

@@ -1,8 +0,0 @@
-module.exports = {
-  'basic SSR': function (browser) {
-    browser
-    .url('http://localhost:8080/test/e2e/specs/basic-ssr.html')
-      .assert.containsText('#result', '<div data-server-rendered="true">foo</div>')
-      .end()
-  }
-}

+ 0 - 23
test/e2e/specs/commits.ts

@@ -1,23 +0,0 @@
-module.exports = {
-  'commits': function (browser) {
-    browser
-    .url('http://localhost:8080/examples/commits/')
-      .waitForElementVisible('li', 5000)
-      .assert.count('input', 2)
-      .assert.count('label', 2)
-      .assert.containsText('label[for="master"]', 'master')
-      .assert.containsText('label[for="dev"]', 'dev')
-      .assert.checked('#master')
-      .assert.checked('#dev', false)
-      .assert.containsText('p', 'vuejs/vue@master')
-      .assert.count('li', 3)
-      .assert.count('li .commit', 3)
-      .assert.count('li .message', 3)
-      .click('#dev')
-      .assert.containsText('p', 'vuejs/vue@dev')
-      .assert.count('li', 3)
-      .assert.count('li .commit', 3)
-      .assert.count('li .message', 3)
-      .end()
-  }
-}

+ 0 - 105
test/e2e/specs/grid.ts

@@ -1,105 +0,0 @@
-module.exports = {
-  'grid': function (browser) {
-    const columns = ['name', 'power']
-
-    browser
-    .url('http://localhost:8080/examples/grid/')
-      .waitForElementVisible('table', 1000)
-      .assert.count('th', 2)
-      .assert.count('th.active', 0)
-      .assert.containsText('th:nth-child(1)', 'Name')
-      .assert.containsText('th:nth-child(2)', 'Power')
-      assertTable([
-        { name: 'Chuck Norris', power: Infinity },
-        { name: 'Bruce Lee', power: 9000 },
-        { name: 'Jackie Chan', power: 7000 },
-        { name: 'Jet Li', power: 8000 }
-      ])
-
-    browser
-      .click('th:nth-child(1)')
-      .assert.count('th.active:nth-child(1)', 1)
-      .assert.count('th.active:nth-child(2)', 0)
-      .assert.count('th:nth-child(1) .arrow.dsc', 1)
-      .assert.count('th:nth-child(2) .arrow.dsc', 0)
-      assertTable([
-        { name: 'Jet Li', power: 8000 },
-        { name: 'Jackie Chan', power: 7000 },
-        { name: 'Chuck Norris', power: Infinity },
-        { name: 'Bruce Lee', power: 9000 }
-      ])
-
-    browser
-      .click('th:nth-child(2)')
-      .assert.count('th.active:nth-child(1)', 0)
-      .assert.count('th.active:nth-child(2)', 1)
-      .assert.count('th:nth-child(1) .arrow.dsc', 1)
-      .assert.count('th:nth-child(2) .arrow.dsc', 1)
-      assertTable([
-        { name: 'Chuck Norris', power: Infinity },
-        { name: 'Bruce Lee', power: 9000 },
-        { name: 'Jet Li', power: 8000 },
-        { name: 'Jackie Chan', power: 7000 }
-      ])
-
-    browser
-      .click('th:nth-child(2)')
-      .assert.count('th.active:nth-child(1)', 0)
-      .assert.count('th.active:nth-child(2)', 1)
-      .assert.count('th:nth-child(1) .arrow.dsc', 1)
-      .assert.count('th:nth-child(2) .arrow.asc', 1)
-      assertTable([
-        { name: 'Jackie Chan', power: 7000 },
-        { name: 'Jet Li', power: 8000 },
-        { name: 'Bruce Lee', power: 9000 },
-        { name: 'Chuck Norris', power: Infinity }
-      ])
-
-    browser
-      .click('th:nth-child(1)')
-      .assert.count('th.active:nth-child(1)', 1)
-      .assert.count('th.active:nth-child(2)', 0)
-      .assert.count('th:nth-child(1) .arrow.asc', 1)
-      .assert.count('th:nth-child(2) .arrow.asc', 1)
-      assertTable([
-        { name: 'Bruce Lee', power: 9000 },
-        { name: 'Chuck Norris', power: Infinity },
-        { name: 'Jackie Chan', power: 7000 },
-        { name: 'Jet Li', power: 8000 }
-      ])
-
-    browser
-      .setValue('input[name="query"]', 'j')
-      assertTable([
-        { name: 'Jackie Chan', power: 7000 },
-        { name: 'Jet Li', power: 8000 }
-      ])
-
-    browser
-      .clearValue('input[name="query"]')
-      .setValue('input[name="query"]', 'infinity')
-      assertTable([
-        { name: 'Chuck Norris', power: Infinity }
-      ])
-
-    browser
-      .clearValue('input[name="query"]')
-      .assert.count('p', 0)
-      .setValue('input[name="query"]', 'stringthatdoesnotexistanywhere')
-      .assert.count('p', 1)
-
-    browser.end()
-
-    function assertTable (data) {
-      browser.assert.count('td', data.length * columns.length)
-      for (let i = 0; i < data.length; i++) {
-        for (let j = 0; j < columns.length; j++) {
-          browser.assert.containsText(
-            'tr:nth-child(' + (i + 1) + ') td:nth-child(' + (j + 1) + ')',
-            data[i][columns[j]]
-          )
-        }
-      }
-    }
-  }
-}

+ 0 - 19
test/e2e/specs/markdown.ts

@@ -1,19 +0,0 @@
-module.exports = {
-  'markdown': function (browser) {
-    browser
-    .url('http://localhost:8080/examples/markdown/')
-      .waitForElementVisible('#editor', 1000)
-      .assert.value('textarea', '# hello')
-      .assert.hasHTML('#editor div', '<h1 id="hello">hello</h1>')
-      .setValue('textarea', '\n## foo\n\n- bar\n- baz')
-      // assert the output is not updated yet because of debounce
-      .assert.hasHTML('#editor div', '<h1 id="hello">hello</h1>')
-      .waitFor(500)
-      .assert.hasHTML('#editor div',
-        '<h1 id="hello">hello</h1>\n' +
-        '<h2 id="foo">foo</h2>\n' +
-        '<ul>\n<li>bar</li>\n<li>baz</li>\n</ul>'
-      )
-      .end()
-  }
-}

+ 0 - 50
test/e2e/specs/svg.ts

@@ -1,50 +0,0 @@
-declare const stats: any
-declare const valueToPoint: Function
-
-module.exports = {
-  'svg': function (browser) {
-    browser
-    .url('http://localhost:8080/examples/svg/')
-      .waitForElementVisible('svg', 1000)
-      .assert.count('g', 1)
-      .assert.count('polygon', 1)
-      .assert.count('circle', 1)
-      .assert.count('text', 6)
-      .assert.count('label', 6)
-      .assert.count('button', 7)
-      .assert.count('input[type="range"]', 6)
-      .assert.evaluate(function () {
-        const points = stats.map(function (stat, i) {
-        const point = valueToPoint(stat.value, i, 6)
-          return point.x + ',' + point.y
-        }).join(' ')
-        return document.querySelector('polygon')!.attributes[0].value === points
-      })
-      .click('button.remove')
-      .assert.count('text', 5)
-      .assert.count('label', 5)
-      .assert.count('button', 6)
-      .assert.count('input[type="range"]', 5)
-      .assert.evaluate(function () {
-        const points = stats.map(function (stat, i) {
-        const point = valueToPoint(stat.value, i, 5)
-          return point.x + ',' + point.y
-        }).join(' ')
-        return document.querySelector('polygon')!.attributes[0].value === points
-      })
-      .setValue('input[name="newlabel"]', 'foo')
-      .click('#add > button')
-      .assert.count('text', 6)
-      .assert.count('label', 6)
-      .assert.count('button', 7)
-      .assert.count('input[type="range"]', 6)
-      .assert.evaluate(function () {
-        const points = stats.map(function (stat, i) {
-        const point = valueToPoint(stat.value, i, 6)
-          return point.x + ',' + point.y
-        }).join(' ')
-        return document.querySelector('polygon')!.attributes[0].value === points
-      })
-      .end()
-  }
-}

+ 0 - 166
test/e2e/specs/todomvc.ts

@@ -1,166 +0,0 @@
-module.exports = {
-  'todomvc': function (browser) {
-    browser
-    .url('http://localhost:8080/examples/todomvc/#test')
-      .waitForElementVisible('.todoapp', 1000)
-      .assert.notVisible('.main')
-      .assert.notVisible('.footer')
-      .assert.count('.filters .selected', 1)
-      .assert.evaluate(function () {
-        return document.querySelector('.filters .selected')!.textContent === 'All'
-      })
-
-    createNewItem('test')
-      .assert.count('.todo', 1)
-      .assert.notVisible('.todo .edit')
-      .assert.containsText('.todo label', 'test')
-      .assert.containsText('.todo-count strong', '1')
-      .assert.checked('.todo .toggle', false)
-      .assert.visible('.main')
-      .assert.visible('.footer')
-      .assert.notVisible('.clear-completed')
-      .assert.value('.new-todo', '')
-
-    createNewItem('test2')
-      .assert.count('.todo', 2)
-      .assert.containsText('.todo:nth-child(2) label', 'test2')
-      .assert.containsText('.todo-count strong', '2')
-
-    // toggle
-    browser
-      .click('.todo .toggle')
-      .assert.count('.todo.completed', 1)
-      .assert.cssClassPresent('.todo:nth-child(1)', 'completed')
-      .assert.containsText('.todo-count strong', '1')
-      .assert.visible('.clear-completed')
-
-    createNewItem('test3')
-      .assert.count('.todo', 3)
-      .assert.containsText('.todo:nth-child(3) label', 'test3')
-      .assert.containsText('.todo-count strong', '2')
-
-    createNewItem('test4')
-    createNewItem('test5')
-      .assert.count('.todo', 5)
-      .assert.containsText('.todo-count strong', '4')
-
-    // toggle more
-    browser
-      .click('.todo:nth-child(4) .toggle')
-      .click('.todo:nth-child(5) .toggle')
-      .assert.count('.todo.completed', 3)
-      .assert.containsText('.todo-count strong', '2')
-
-    // remove
-    removeItemAt(1)
-      .assert.count('.todo', 4)
-      .assert.count('.todo.completed', 2)
-      .assert.containsText('.todo-count strong', '2')
-    removeItemAt(2)
-      .assert.count('.todo', 3)
-      .assert.count('.todo.completed', 2)
-      .assert.containsText('.todo-count strong', '1')
-
-    // remove all
-    browser
-      .click('.clear-completed')
-      .assert.count('.todo', 1)
-      .assert.containsText('.todo label', 'test2')
-      .assert.count('.todo.completed', 0)
-      .assert.containsText('.todo-count strong', '1')
-      .assert.notVisible('.clear-completed')
-
-    // prepare to test filters
-    createNewItem('test')
-    createNewItem('test')
-      .click('.todo:nth-child(2) .toggle')
-      .click('.todo:nth-child(3) .toggle')
-
-    // active filter
-    browser
-      .click('.filters li:nth-child(2) a')
-      .assert.count('.todo', 1)
-      .assert.count('.todo.completed', 0)
-      // add item with filter active
-      createNewItem('test')
-      .assert.count('.todo', 2)
-
-    // completed filter
-    browser.click('.filters li:nth-child(3) a')
-      .assert.count('.todo', 2)
-      .assert.count('.todo.completed', 2)
-
-    // filter on page load
-    browser.url('http://localhost:8080/examples/todomvc/#active')
-      .assert.count('.todo', 2)
-      .assert.count('.todo.completed', 0)
-      .assert.containsText('.todo-count strong', '2')
-
-    // completed on page load
-    browser.url('http://localhost:8080/examples/todomvc/#completed')
-      .assert.count('.todo', 2)
-      .assert.count('.todo.completed', 2)
-      .assert.containsText('.todo-count strong', '2')
-
-    // toggling with filter active
-    browser
-      .click('.todo .toggle')
-      .assert.count('.todo', 1)
-      .click('.filters li:nth-child(2) a')
-      .assert.count('.todo', 3)
-      .click('.todo .toggle')
-      .assert.count('.todo', 2)
-
-    // editing triggered by blur
-    browser
-      .click('.filters li:nth-child(1) a')
-      .dblClick('.todo:nth-child(1) label')
-      .assert.count('.todo.editing', 1)
-      .assert.focused('.todo:nth-child(1) .edit')
-      .clearValue('.todo:nth-child(1) .edit')
-      .setValue('.todo:nth-child(1) .edit', 'edited!')
-      .click('.new-todo') // blur
-      .assert.count('.todo.editing', 0)
-      .assert.containsText('.todo:nth-child(1) label', 'edited!')
-
-    // editing triggered by enter
-    browser
-      .dblClick('.todo label')
-      .enterValue('.todo:nth-child(1) .edit', 'edited again!')
-      .assert.count('.todo.editing', 0)
-      .assert.containsText('.todo:nth-child(1) label', 'edited again!')
-
-    // cancel
-    browser
-      .dblClick('.todo label')
-      .clearValue('.todo:nth-child(1) .edit')
-      .setValue('.todo:nth-child(1) .edit', 'edited!')
-      .trigger('.todo:nth-child(1) .edit', 'keyup', 27)
-      .assert.count('.todo.editing', 0)
-      .assert.containsText('.todo:nth-child(1) label', 'edited again!')
-
-    // empty value should remove
-    browser
-      .dblClick('.todo label')
-      .enterValue('.todo:nth-child(1) .edit', ' ')
-      .assert.count('.todo', 3)
-
-    // toggle all
-    browser
-      .click('.toggle-all')
-      .assert.count('.todo.completed', 3)
-      .click('.toggle-all')
-      .assert.count('.todo:not(.completed)', 3)
-      .end()
-
-    function createNewItem (text) {
-      return browser.enterValue('.new-todo', text)
-    }
-
-    function removeItemAt (n) {
-      return browser
-        .moveToElement('.todo:nth-child(' + n + ')', 10, 10)
-        .click('.todo:nth-child(' + n + ') .destroy')
-    }
-  }
-}

+ 0 - 72
test/e2e/specs/tree.ts

@@ -1,72 +0,0 @@
-module.exports = {
-  'tree': function (browser) {
-    browser
-    .url('http://localhost:8080/examples/tree/')
-      .waitForElementVisible('li', 1000)
-      .assert.count('.item', 12)
-      .assert.count('.add', 4)
-      .assert.count('.item > ul', 4)
-      .assert.notVisible('#demo li ul')
-      .assert.containsText('#demo li div span', '[+]')
-
-      // expand root
-      .click('.bold')
-      .assert.visible('#demo ul')
-      .assert.evaluate(function () {
-        return document.querySelector('#demo li ul')!.children.length === 4
-      })
-      .assert.containsText('#demo li div span', '[-]')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(1)', 'hello')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(2)', 'wat')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(3)', 'child folder')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(3)', '[+]')
-
-      // add items to root
-      .click('#demo > .item > ul > .add')
-      .assert.evaluate(function () {
-        return document.querySelector('#demo li ul')!.children.length === 5
-      })
-      .assert.containsText('#demo > .item > ul > .item:nth-child(1)', 'hello')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(2)', 'wat')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(3)', 'child folder')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(3)', '[+]')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(4)', 'new stuff')
-
-      // add another item
-      .click('#demo > .item > ul > .add')
-      .assert.evaluate(function () {
-        return document.querySelector('#demo li ul')!.children.length === 6
-      })
-      .assert.containsText('#demo > .item > ul > .item:nth-child(1)', 'hello')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(2)', 'wat')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(3)', 'child folder')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(3)', '[+]')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(4)', 'new stuff')
-      .assert.containsText('#demo > .item > ul > .item:nth-child(5)', 'new stuff')
-
-      .click('#demo ul .bold')
-      .assert.visible('#demo ul ul')
-      .assert.containsText('#demo ul > .item:nth-child(3)', '[-]')
-      .assert.evaluate(function () {
-        return document.querySelector('#demo ul ul')!.children.length === 5
-      })
-
-      .click('.bold')
-      .assert.notVisible('#demo ul')
-      .assert.containsText('#demo li div span', '[+]')
-      .click('.bold')
-      .assert.visible('#demo ul')
-      .assert.containsText('#demo li div span', '[-]')
-
-      .dblClick('#demo ul > .item div')
-      .assert.count('.item', 15)
-      .assert.count('.item > ul', 5)
-      .assert.containsText('#demo ul > .item:nth-child(1)', '[-]')
-      .assert.evaluate(function () {
-        const firstItem = document.querySelector('#demo ul > .item:nth-child(1)')!
-        const ul = firstItem.querySelector('ul')!
-        return ul.children.length === 2
-      })
-      .end()
-  }
-}

+ 151 - 0
test/e2e/svg.spec.ts

@@ -0,0 +1,151 @@
+import { setupPuppeteer, getExampleUrl, E2E_TIMEOUT } from './e2eUtils'
+
+declare const stats: {
+  label: string
+  value: number
+}[]
+
+declare function valueToPoint(
+  value: number,
+  index: number,
+  total: number
+): {
+  x: number
+  y: number
+}
+
+describe('e2e: svg', () => {
+  const { page, click, count, setValue, typeValue } = setupPuppeteer()
+
+  // assert the shape of the polygon is correct
+  async function assertPolygon(total: number) {
+    expect(
+      await page().evaluate(
+        (total) => {
+          const points = stats
+            .map((stat, i) => {
+              const point = valueToPoint(stat.value, i, total)
+              return point.x + ',' + point.y
+            })
+            .join(' ')
+          return (
+            document.querySelector('polygon')!.attributes[0].value === points
+          )
+        },
+        [total]
+      )
+    ).toBe(true)
+  }
+
+  // assert the position of each label is correct
+  async function assertLabels(total: number) {
+    const positions = await page().evaluate(
+      (total) => {
+        return stats.map((stat, i) => {
+          const point = valueToPoint(+stat.value + 10, i, total)
+          return [point.x, point.y]
+        })
+      },
+      [total]
+    )
+    for (let i = 0; i < total; i++) {
+      const textPosition = await page().$eval(
+        `text:nth-child(${i + 3})`,
+        (node) => [+node.attributes[0].value, +node.attributes[1].value]
+      )
+      expect(textPosition).toEqual(positions[i])
+    }
+  }
+
+  // assert each value of stats is correct
+  async function assertStats(expected: number[]) {
+    const statsValue = await page().evaluate(() => {
+      return stats.map((stat) => +stat.value)
+    })
+    expect(statsValue).toEqual(expected)
+  }
+
+  function nthRange(n: number) {
+    return `#demo div:nth-child(${n + 1}) input[type="range"]`
+  }
+
+  async function testSvg(apiType: 'classic' | 'composition') {
+    await page().goto(getExampleUrl('svg', apiType))
+    await page().waitForSelector('svg')
+    expect(await count('g')).toBe(1)
+    expect(await count('polygon')).toBe(1)
+    expect(await count('circle')).toBe(1)
+    expect(await count('text')).toBe(6)
+    expect(await count('label')).toBe(6)
+    expect(await count('button')).toBe(7)
+    expect(await count('input[type="range"]')).toBe(6)
+    await assertPolygon(6)
+    await assertLabels(6)
+    await assertStats([100, 100, 100, 100, 100, 100])
+
+    await setValue(nthRange(1), '10')
+    await assertPolygon(6)
+    await assertLabels(6)
+    await assertStats([10, 100, 100, 100, 100, 100])
+
+    await click('button.remove')
+    expect(await count('text')).toBe(5)
+    expect(await count('label')).toBe(5)
+    expect(await count('button')).toBe(6)
+    expect(await count('input[type="range"]')).toBe(5)
+    await assertPolygon(5)
+    await assertLabels(5)
+    await assertStats([100, 100, 100, 100, 100])
+
+    await typeValue('input[name="newlabel"]', 'foo')
+    await click('#add > button')
+    expect(await count('text')).toBe(6)
+    expect(await count('label')).toBe(6)
+    expect(await count('button')).toBe(7)
+    expect(await count('input[type="range"]')).toBe(6)
+    await assertPolygon(6)
+    await assertLabels(6)
+    await assertStats([100, 100, 100, 100, 100, 100])
+
+    await setValue(nthRange(1), '10')
+    await assertPolygon(6)
+    await assertLabels(6)
+    await assertStats([10, 100, 100, 100, 100, 100])
+
+    await setValue(nthRange(2), '20')
+    await assertPolygon(6)
+    await assertLabels(6)
+    await assertStats([10, 20, 100, 100, 100, 100])
+
+    await setValue(nthRange(6), '60')
+    await assertPolygon(6)
+    await assertLabels(6)
+    await assertStats([10, 20, 100, 100, 100, 60])
+
+    await click('button.remove')
+    await assertPolygon(5)
+    await assertLabels(5)
+    await assertStats([20, 100, 100, 100, 60])
+
+    await setValue(nthRange(1), '10')
+    await assertPolygon(5)
+    await assertLabels(5)
+    await assertStats([10, 100, 100, 100, 60])
+  }
+
+  test(
+    'classic',
+    async () => {
+      await testSvg('classic')
+    },
+    E2E_TIMEOUT
+  )
+
+  // test(
+  //   'composition',
+  //   async () => {
+  //     await testSvg('composition')
+  //   },
+  //   E2E_TIMEOUT
+  // )
+})

+ 182 - 0
test/e2e/todomvc.spec.ts

@@ -0,0 +1,182 @@
+import { setupPuppeteer, getExampleUrl, E2E_TIMEOUT } from './e2eUtils'
+
+describe('e2e: todomvc', () => {
+  const {
+    page,
+    click,
+    isVisible,
+    count,
+    text,
+    value,
+    isChecked,
+    isFocused,
+    classList,
+    enterValue,
+    clearValue
+  } = setupPuppeteer()
+
+  async function removeItemAt(n: number) {
+    const item = (await page().$('.todo:nth-child(' + n + ')'))!
+    const itemBBox = (await item.boundingBox())!
+    await page().mouse.move(itemBBox.x + 10, itemBBox.y + 10)
+    await click('.todo:nth-child(' + n + ') .destroy')
+  }
+
+  async function testTodomvc(apiType: 'classic' | 'composition') {
+    const baseUrl = getExampleUrl('todomvc', apiType)
+    await page().goto(baseUrl)
+    expect(await isVisible('.main')).toBe(false)
+    expect(await isVisible('.footer')).toBe(false)
+    expect(await count('.filters .selected')).toBe(1)
+    expect(await text('.filters .selected')).toBe('All')
+    expect(await count('.todo')).toBe(0)
+
+    await enterValue('.new-todo', 'test')
+    expect(await count('.todo')).toBe(1)
+    expect(await isVisible('.todo .edit')).toBe(false)
+    expect(await text('.todo label')).toBe('test')
+    expect(await text('.todo-count strong')).toBe('1')
+    expect(await isChecked('.todo .toggle')).toBe(false)
+    expect(await isVisible('.main')).toBe(true)
+    expect(await isVisible('.footer')).toBe(true)
+    expect(await isVisible('.clear-completed')).toBe(false)
+    expect(await value('.new-todo')).toBe('')
+
+    await enterValue('.new-todo', 'test2')
+    expect(await count('.todo')).toBe(2)
+    expect(await text('.todo:nth-child(2) label')).toBe('test2')
+    expect(await text('.todo-count strong')).toBe('2')
+
+    // toggle
+    await click('.todo .toggle')
+    expect(await count('.todo.completed')).toBe(1)
+    expect(await classList('.todo:nth-child(1)')).toContain('completed')
+    expect(await text('.todo-count strong')).toBe('1')
+    expect(await isVisible('.clear-completed')).toBe(true)
+
+    await enterValue('.new-todo', 'test3')
+    expect(await count('.todo')).toBe(3)
+    expect(await text('.todo:nth-child(3) label')).toBe('test3')
+    expect(await text('.todo-count strong')).toBe('2')
+
+    await enterValue('.new-todo', 'test4')
+    await enterValue('.new-todo', 'test5')
+    expect(await count('.todo')).toBe(5)
+    expect(await text('.todo-count strong')).toBe('4')
+
+    // toggle more
+    await click('.todo:nth-child(4) .toggle')
+    await click('.todo:nth-child(5) .toggle')
+    expect(await count('.todo.completed')).toBe(3)
+    expect(await text('.todo-count strong')).toBe('2')
+
+    // remove
+    await removeItemAt(1)
+    expect(await count('.todo')).toBe(4)
+    expect(await count('.todo.completed')).toBe(2)
+    expect(await text('.todo-count strong')).toBe('2')
+    await removeItemAt(2)
+    expect(await count('.todo')).toBe(3)
+    expect(await count('.todo.completed')).toBe(2)
+    expect(await text('.todo-count strong')).toBe('1')
+
+    // remove all
+    await click('.clear-completed')
+    expect(await count('.todo')).toBe(1)
+    expect(await text('.todo label')).toBe('test2')
+    expect(await count('.todo.completed')).toBe(0)
+    expect(await text('.todo-count strong')).toBe('1')
+    expect(await isVisible('.clear-completed')).toBe(false)
+
+    // prepare to test filters
+    await enterValue('.new-todo', 'test')
+    await enterValue('.new-todo', 'test')
+    await click('.todo:nth-child(2) .toggle')
+    await click('.todo:nth-child(3) .toggle')
+
+    // active filter
+    await click('.filters li:nth-child(2) a')
+    expect(await count('.todo')).toBe(1)
+    expect(await count('.todo.completed')).toBe(0)
+    // add item with filter active
+    await enterValue('.new-todo', 'test')
+    expect(await count('.todo')).toBe(2)
+
+    // completed filter
+    await click('.filters li:nth-child(3) a')
+    expect(await count('.todo')).toBe(2)
+    expect(await count('.todo.completed')).toBe(2)
+
+    // filter on page load
+    await page().goto(`${baseUrl}#active`)
+    expect(await count('.todo')).toBe(2)
+    expect(await count('.todo.completed')).toBe(0)
+    expect(await text('.todo-count strong')).toBe('2')
+
+    // completed on page load
+    await page().goto(`${baseUrl}#completed`)
+    expect(await count('.todo')).toBe(2)
+    expect(await count('.todo.completed')).toBe(2)
+    expect(await text('.todo-count strong')).toBe('2')
+
+    // toggling with filter active
+    await click('.todo .toggle')
+    expect(await count('.todo')).toBe(1)
+    await click('.filters li:nth-child(2) a')
+    expect(await count('.todo')).toBe(3)
+    await click('.todo .toggle')
+    expect(await count('.todo')).toBe(2)
+
+    // editing triggered by blur
+    await click('.filters li:nth-child(1) a')
+    await click('.todo:nth-child(1) label', { clickCount: 2 })
+    expect(await count('.todo.editing')).toBe(1)
+    expect(await isFocused('.todo:nth-child(1) .edit')).toBe(true)
+    await clearValue('.todo:nth-child(1) .edit')
+    await page().type('.todo:nth-child(1) .edit', 'edited!')
+    await click('.new-todo') // blur
+    expect(await count('.todo.editing')).toBe(0)
+    expect(await text('.todo:nth-child(1) label')).toBe('edited!')
+
+    // editing triggered by enter
+    await click('.todo label', { clickCount: 2 })
+    await enterValue('.todo:nth-child(1) .edit', 'edited again!')
+    expect(await count('.todo.editing')).toBe(0)
+    expect(await text('.todo:nth-child(1) label')).toBe('edited again!')
+
+    // cancel
+    await click('.todo label', { clickCount: 2 })
+    await clearValue('.todo:nth-child(1) .edit')
+    await page().type('.todo:nth-child(1) .edit', 'edited!')
+    await page().keyboard.press('Escape')
+    expect(await count('.todo.editing')).toBe(0)
+    expect(await text('.todo:nth-child(1) label')).toBe('edited again!')
+
+    // empty value should remove
+    await click('.todo label', { clickCount: 2 })
+    await enterValue('.todo:nth-child(1) .edit', ' ')
+    expect(await count('.todo')).toBe(3)
+
+    // toggle all
+    await click('.toggle-all+label')
+    expect(await count('.todo.completed')).toBe(3)
+    await click('.toggle-all+label')
+    expect(await count('.todo:not(.completed)')).toBe(3)
+  }
+
+  test(
+    'classic',
+    async () => {
+      await testTodomvc('classic')
+    },
+    E2E_TIMEOUT
+  )
+
+  // test(
+  //   'composition',
+  //   async () => {
+  //     await testTodomvc('composition')
+  //   },
+  //   E2E_TIMEOUT
+  // )
+})

+ 108 - 0
test/e2e/tree.spec.ts

@@ -0,0 +1,108 @@
+import { setupPuppeteer, getExampleUrl, E2E_TIMEOUT } from './e2eUtils'
+
+describe('e2e: tree', () => {
+  const { page, click, count, text, childrenCount, isVisible } =
+    setupPuppeteer()
+
+  async function testTree(apiType: 'classic' | 'composition') {
+    await page().goto(getExampleUrl('tree', apiType))
+    expect(await count('.item')).toBe(12)
+    expect(await count('.add')).toBe(4)
+    expect(await count('.item > ul')).toBe(4)
+    expect(await isVisible('#demo li ul')).toBe(false)
+    expect(await text('#demo li div span')).toBe('[+]')
+
+    // expand root
+    await click('.bold')
+    expect(await isVisible('#demo ul')).toBe(true)
+    expect(await childrenCount('#demo li ul')).toBe(4)
+    expect(await text('#demo li div span')).toContain('[-]')
+    expect(await text('#demo > .item > ul > .item:nth-child(1)')).toContain(
+      'hello'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(2)')).toContain(
+      'wat'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
+      'child folder'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
+      '[+]'
+    )
+
+    // add items to root
+    await click('#demo > .item > ul > .add')
+    expect(await childrenCount('#demo li ul')).toBe(5)
+    expect(await text('#demo > .item > ul > .item:nth-child(1)')).toContain(
+      'hello'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(2)')).toContain(
+      'wat'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
+      'child folder'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
+      '[+]'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(4)')).toContain(
+      'new stuff'
+    )
+
+    // add another item
+    await click('#demo > .item > ul > .add')
+    expect(await childrenCount('#demo li ul')).toBe(6)
+    expect(await text('#demo > .item > ul > .item:nth-child(1)')).toContain(
+      'hello'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(2)')).toContain(
+      'wat'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
+      'child folder'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(3)')).toContain(
+      '[+]'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(4)')).toContain(
+      'new stuff'
+    )
+    expect(await text('#demo > .item > ul > .item:nth-child(5)')).toContain(
+      'new stuff'
+    )
+
+    await click('#demo ul .bold')
+    expect(await isVisible('#demo ul ul')).toBe(true)
+    expect(await text('#demo ul > .item:nth-child(3)')).toContain('[-]')
+    expect(await childrenCount('#demo ul ul')).toBe(5)
+
+    await click('.bold')
+    expect(await isVisible('#demo ul')).toBe(false)
+    expect(await text('#demo li div span')).toContain('[+]')
+    await click('.bold')
+    expect(await isVisible('#demo ul')).toBe(true)
+    expect(await text('#demo li div span')).toContain('[-]')
+
+    await click('#demo ul > .item div', { clickCount: 2 })
+    expect(await count('.item')).toBe(15)
+    expect(await count('.item > ul')).toBe(5)
+    expect(await text('#demo ul > .item:nth-child(1)')).toContain('[-]')
+    expect(await childrenCount('#demo ul > .item:nth-child(1) > ul')).toBe(2)
+  }
+
+  test(
+    'classic',
+    async () => {
+      await testTree('classic')
+    },
+    E2E_TIMEOUT
+  )
+
+  // test(
+  //   'composition',
+  //   async () => {
+  //     await testTree('composition')
+  //   },
+  //   E2E_TIMEOUT
+  // )
+})