Browse Source

test: migrate e2e test to puppeteer

Kia Ishii 4 years ago
parent
commit
5a2938793a

+ 1 - 1
jest.config.js

@@ -7,7 +7,7 @@ module.exports = {
     '^@/(.*)$': '<rootDir>/src/$1',
     '^test/(.*)$': '<rootDir>/test/$1'
   },
-  testMatch: ['<rootDir>/test/unit/**/*.spec.js'],
+  testMatch: ['<rootDir>/test/**/*.spec.js'],
   testPathIgnorePatterns: ['/node_modules/'],
   setupFilesAfterEnv: ['./test/setup.js'],
   transform: {

+ 3 - 4
package.json

@@ -23,7 +23,7 @@
     "lint": "eslint src test",
     "test": "npm run lint && npm run test:types && npm run test:unit && npm run test:ssr && npm run test:e2e",
     "test:unit": "jest --testPathIgnorePatterns test/e2e",
-    "test:e2e": "node test/e2e/runner.js",
+    "test:e2e": "start-server-and-test dev http://localhost:8080 'jest --testPathIgnorePatterns test/unit'",
     "test:ssr": "cross-env VUE_ENV=server jest --testPathIgnorePatterns test/e2e",
     "test:types": "tsc -p types/test",
     "coverage": "jest --testPathIgnorePatterns test/e2e --coverage",
@@ -58,7 +58,6 @@
     "babel-loader": "^8.1.0",
     "brotli": "^1.3.2",
     "chalk": "^4.0.0",
-    "chromedriver": "^80.0.1",
     "conventional-changelog-cli": "^2.0.31",
     "cross-env": "^5.2.0",
     "cross-spawn": "^6.0.5",
@@ -69,12 +68,12 @@
     "execa": "^4.0.0",
     "express": "^4.17.1",
     "jest": "^26.0.1",
-    "nightwatch": "^1.3.1",
-    "nightwatch-helpers": "^1.2.0",
+    "puppeteer": "^4.0.0",
     "regenerator-runtime": "^0.13.5",
     "rollup": "^2.8.2",
     "rollup-plugin-terser": "^5.3.0",
     "semver": "^7.3.2",
+    "start-server-and-test": "^1.11.0",
     "todomvc-app-css": "^2.1.0",
     "typescript": "^3.8.3",
     "vue": "^3.0.0-beta.10",

+ 45 - 0
test/e2e/cart.spec.js

@@ -0,0 +1,45 @@
+import { setupPuppeteer, E2E_TIMEOUT } from 'test/helpers'
+
+describe('e2e/cart', () => {
+  const { page, text, count, click, sleep } = setupPuppeteer()
+
+  async function testCart (url) {
+    await page().goto(url)
+
+    await sleep(120) // api simulation
+
+    expect(await count('li')).toBe(3)
+    expect(await count('.cart button[disabled]')).toBe(1)
+    expect(await text('li:nth-child(1)')).toContain('iPad 4 Mini')
+    expect(await text('.cart')).toContain('Please add some products to cart')
+    expect(await text('.cart')).toContain('Total: $0.00')
+
+    await click('li:nth-child(1) button')
+    expect(await text('.cart')).toContain('iPad 4 Mini - $500.01 x 1')
+    expect(await text('.cart')).toContain('Total: $500.01')
+
+    await click('li:nth-child(1) button')
+    expect(await text('.cart')).toContain('iPad 4 Mini - $500.01 x 2')
+    expect(await text('.cart')).toContain('Total: $1,000.02')
+    expect(await count('li:nth-child(1) button[disabled]')).toBe(1)
+
+    await click('li:nth-child(2) button')
+    expect(await text('.cart')).toContain('H&M T-Shirt White - $10.99 x 1')
+    expect(await text('.cart')).toContain('Total: $1,011.01')
+
+    await click('.cart button')
+    await sleep(200)
+    expect(await text('.cart')).toContain('Please add some products to cart')
+    expect(await text('.cart')).toContain('Total: $0.00')
+    expect(await text('.cart')).toContain('Checkout successful')
+    expect(await count('.cart button[disabled]')).toBe(1)
+  }
+
+  test('classic', async () => {
+    await testCart('http://localhost:8080/classic/shopping-cart/')
+  }, E2E_TIMEOUT)
+
+  test('composition', async () => {
+    await testCart('http://localhost:8080/composition/shopping-cart/')
+  }, E2E_TIMEOUT)
+})

+ 42 - 0
test/e2e/chat.spec.js

@@ -0,0 +1,42 @@
+import { setupPuppeteer, E2E_TIMEOUT } from 'test/helpers'
+
+describe('e2e/chat', () => {
+  const { page, text, count, click, enterValue, sleep } = setupPuppeteer()
+
+  async function testChat (url) {
+    await page().goto(url)
+
+    expect(await text('.thread-count')).toContain('Unread threads: 2')
+    expect(await count('.thread-list-item')).toBe(3)
+    expect(await text('.thread-list-item.active')).toContain('Functional Heads')
+    expect(await text('.message-thread-heading')).toContain('Functional Heads')
+    expect(await count('.message-list-item')).toBe(2)
+    expect(await text('.message-list-item:nth-child(1) .message-author-name')).toContain('Bill')
+    expect(await text('.message-list-item:nth-child(1) .message-text')).toContain('Hey Brian')
+
+    await enterValue('.message-composer', 'hi')
+    await sleep(50) // fake api
+    expect(await count('.message-list-item')).toBe(3)
+    expect(await text('.message-list-item:nth-child(3)')).toContain('hi')
+
+    await click('.thread-list-item:nth-child(2)')
+    expect(await text('.thread-list-item.active')).toContain('Dave and Bill')
+    expect(await text('.message-thread-heading')).toContain('Dave and Bill')
+    expect(await count('.message-list-item')).toBe(2)
+    expect(await text('.message-list-item:nth-child(1) .message-author-name')).toContain('Bill')
+    expect(await text('.message-list-item:nth-child(1) .message-text')).toContain('Hey Dave')
+
+    await enterValue('.message-composer', 'hi')
+    await sleep(50) // fake api
+    expect(await count('.message-list-item')).toBe(3)
+    expect(await text('.message-list-item:nth-child(3)')).toContain('hi')
+  }
+
+  test('classic', async () => {
+    await testChat('http://localhost:8080/classic/chat/')
+  }, E2E_TIMEOUT)
+
+  test('composition', async () => {
+    await testChat('http://localhost:8080/composition/chat/')
+  }, E2E_TIMEOUT)
+})

+ 38 - 0
test/e2e/counter.spec.js

@@ -0,0 +1,38 @@
+import { setupPuppeteer, E2E_TIMEOUT } from 'test/helpers'
+
+describe('e2e/counter', () => {
+  const { page, text, click, sleep } = setupPuppeteer()
+
+  async function testCounter (url) {
+    await page().goto(url)
+    expect(await text('#app')).toContain('Clicked: 0 times')
+
+    await click('button:nth-child(1)')
+    expect(await text('#app')).toContain('Clicked: 1 times')
+
+    await click('button:nth-child(2)')
+    expect(await text('#app')).toContain('Clicked: 0 times')
+
+    await click('button:nth-child(3)')
+    expect(await text('#app')).toContain('Clicked: 0 times')
+
+    await click('button:nth-child(1)')
+    expect(await text('#app')).toContain('Clicked: 1 times')
+
+    await click('button:nth-child(3)')
+    expect(await text('#app')).toContain('Clicked: 2 times')
+
+    await click('button:nth-child(4)')
+    expect(await text('#app')).toContain('Clicked: 2 times')
+    await sleep(1000)
+    expect(await text('#app')).toContain('Clicked: 3 times')
+  }
+
+  test('classic', async () => {
+    await testCounter('http://localhost:8080/classic/counter/')
+  }, E2E_TIMEOUT)
+
+  test('composition', async () => {
+    await testCounter('http://localhost:8080/composition/counter/')
+  }, E2E_TIMEOUT)
+})

+ 0 - 36
test/e2e/nightwatch.config.js

@@ -1,36 +0,0 @@
-// http://nightwatchjs.org/guide#settings-file
-module.exports = {
-  'src_folders': ['test/e2e/specs'],
-  'output_folder': 'test/e2e/reports',
-  'custom_commands_path': ['node_modules/nightwatch-helpers/commands'],
-  'custom_assertions_path': ['node_modules/nightwatch-helpers/assertions'],
-
-  'webdriver': {
-    'start_process': true,
-    'port': 9515,
-    'server_path': require('chromedriver').path
-  },
-
-  'test_settings': {
-    'default': {
-      'silent': true,
-      'screenshots': {
-        'enabled': true,
-        'on_failure': true,
-        'on_error': false,
-        'path': 'test/e2e/screenshots'
-      }
-    },
-
-    'chrome': {
-      'desiredCapabilities': {
-        'browserName': 'chrome',
-        'javascriptEnabled': true,
-        'acceptSslCerts': true,
-        'chromeOptions': {
-          'args': ['--headless']
-        }
-      }
-    }
-  }
-}

+ 0 - 31
test/e2e/runner.js

@@ -1,31 +0,0 @@
-var spawn = require('cross-spawn')
-var args = process.argv.slice(2)
-
-var server = args.indexOf('--dev') > -1
-  ? null
-  : require('../../examples/server')
-
-if (args.indexOf('--config') === -1) {
-  args = args.concat(['--config', 'test/e2e/nightwatch.config.js'])
-}
-if (args.indexOf('--env') === -1) {
-  args = args.concat(['--env', 'chrome'])
-}
-var i = args.indexOf('--test')
-if (i > -1) {
-  args[i + 1] = 'test/e2e/specs/' + args[i + 1]
-}
-
-var runner = spawn('./node_modules/.bin/nightwatch', args, {
-  stdio: 'inherit'
-})
-
-runner.on('exit', function (code) {
-  server && server.close()
-  process.exit(code)
-})
-
-runner.on('error', function (err) {
-  server && server.close()
-  throw err
-})

+ 0 - 37
test/e2e/specs/cart.js

@@ -1,37 +0,0 @@
-function test (browser, url) {
-  browser
-    .url(url)
-    .waitForElementVisible('#app', 1000)
-    .waitFor(120) // api simulation
-    .assert.count('li', 3)
-    .assert.count('.cart button[disabled]', 1)
-    .assert.containsText('li:nth-child(1)', 'iPad 4 Mini')
-    .assert.containsText('.cart', 'Please add some products to cart')
-    .assert.containsText('.cart', 'Total: $0.00')
-    .click('li:nth-child(1) button')
-    .assert.containsText('.cart', 'iPad 4 Mini - $500.01 x 1')
-    .assert.containsText('.cart', 'Total: $500.01')
-    .click('li:nth-child(1) button')
-    .assert.containsText('.cart', 'iPad 4 Mini - $500.01 x 2')
-    .assert.containsText('.cart', 'Total: $1,000.02')
-    .assert.count('li:nth-child(1) button[disabled]', 1)
-    .click('li:nth-child(2) button')
-    .assert.containsText('.cart', 'H&M T-Shirt White - $10.99 x 1')
-    .assert.containsText('.cart', 'Total: $1,011.01')
-    .click('.cart button')
-    .waitFor(200)
-    .assert.containsText('.cart', 'Please add some products to cart')
-    .assert.containsText('.cart', 'Total: $0.00')
-    .assert.containsText('.cart', 'Checkout successful')
-    .assert.count('.cart button[disabled]', 1)
-    .end()
-}
-
-module.exports = {
-  'shopping cart/classic': function (browser) {
-    test(browser, 'http://localhost:8080/classic/shopping-cart/')
-  },
-  'shopping cart/composition': function (browser) {
-    test(browser, 'http://localhost:8080/composition/shopping-cart/')
-  }
-}

+ 0 - 35
test/e2e/specs/chat.js

@@ -1,35 +0,0 @@
-function test (browser, url) {
-  browser
-    .url(url)
-    .waitForElementVisible('.chatapp', 1000)
-    .assert.containsText('.thread-count', 'Unread threads: 2')
-    .assert.count('.thread-list-item', 3)
-    .assert.containsText('.thread-list-item.active', 'Functional Heads')
-    .assert.containsText('.message-thread-heading', 'Functional Heads')
-    .assert.count('.message-list-item', 2)
-    .assert.containsText('.message-list-item:nth-child(1) .message-author-name', 'Bill')
-    .assert.containsText('.message-list-item:nth-child(1) .message-text', 'Hey Brian')
-    .setValue('.message-composer', ['hi', browser.Keys.ENTER])
-    .waitFor(50) // fake api
-    .assert.count('.message-list-item', 3)
-    .assert.containsText('.message-list-item:nth-child(3)', 'hi')
-    .click('.thread-list-item:nth-child(2)')
-    .assert.containsText('.thread-list-item.active', 'Dave and Bill')
-    .assert.containsText('.message-thread-heading', 'Dave and Bill')
-    .assert.count('.message-list-item', 2)
-    .assert.containsText('.message-list-item:nth-child(1) .message-author-name', 'Bill')
-    .assert.containsText('.message-list-item:nth-child(1) .message-text', 'Hey Dave')
-    .setValue('.message-composer', ['hi', browser.Keys.ENTER])
-    .waitFor(50) // fake api
-    .assert.count('.message-list-item', 3)
-    .assert.containsText('.message-list-item:nth-child(3)', 'hi')
-}
-
-module.exports = {
-  'chat/classic': function (browser) {
-    test(browser, 'http://localhost:8080/classic/chat/')
-  },
-  'chat/composition': function (browser) {
-    test(browser, 'http://localhost:8080/composition/chat/')
-  }
-}

+ 0 - 30
test/e2e/specs/counter.js

@@ -1,30 +0,0 @@
-function test (browser, url) {
-  browser
-    .url(url)
-    .waitForElementVisible('#app', 1000)
-    .assert.containsText('div', 'Clicked: 0 times')
-    .click('button:nth-child(1)')
-    .assert.containsText('div', 'Clicked: 1 times')
-    .click('button:nth-child(2)')
-    .assert.containsText('div', 'Clicked: 0 times')
-    .click('button:nth-child(3)')
-    .assert.containsText('div', 'Clicked: 0 times')
-    .click('button:nth-child(1)')
-    .assert.containsText('div', 'Clicked: 1 times')
-    .click('button:nth-child(3)')
-    .assert.containsText('div', 'Clicked: 2 times')
-    .click('button:nth-child(4)')
-    .assert.containsText('div', 'Clicked: 2 times')
-    .waitFor(1000)
-    .assert.containsText('div', 'Clicked: 3 times')
-    .end()
-}
-
-module.exports = {
-  'counter/classic': function (browser) {
-    test(browser, 'http://localhost:8080/classic/counter/')
-  },
-  'counter/composition': function (browser) {
-    test(browser, 'http://localhost:8080/composition/counter/')
-  }
-}

+ 0 - 167
test/e2e/specs/todomvc.js

@@ -1,167 +0,0 @@
-function test (browser, url) {
-  browser
-    .url(url)
-    .waitForElementVisible('.todoapp', 1000)
-    .assert.notVisible('.main')
-    .assert.notVisible('.footer')
-    .assert.count('.filters .selected', 1)
-    .assert.evaluate(function () {
-      return document.querySelector('.filters .selected').textContent === 'All'
-    }, null, 'filter should be "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)
-
-  // complted filter
-  browser.click('.filters li:nth-child(3) a')
-    .assert.count('.todo', 2)
-    .assert.count('.todo.completed', 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')
-  deleteValue('.todo:nth-child(1) .edit', 'test2')
-    .setValue('.todo:nth-child(1) .edit', ['edited!', browser.Keys.TAB])
-    .assert.count('.todo.editing', 0)
-    .assert.containsText('.todo:nth-child(1) label', 'edited!')
-
-  // editing triggered by enter
-  browser
-    .dblClick('.todo label')
-  deleteValue('.todo:nth-child(1) .edit', 'edited!')
-    .setValue('.todo:nth-child(1) .edit', ['edited again!', browser.Keys.ENTER])
-    .assert.count('.todo.editing', 0)
-    .assert.containsText('.todo:nth-child(1) label', 'edited again!')
-
-  // cancel
-  browser
-    .dblClick('.todo label')
-  deleteValue('.todo:nth-child(1) .edit', 'edited again!')
-    .setValue('.todo:nth-child(1) .edit', ['edited!', browser.Keys.ESCAPE])
-    .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')
-  deleteValue('.todo:nth-child(1) .edit', 'edited again!')
-    .setValue('.todo:nth-child(1) .edit', [' ', browser.Keys.ENTER])
-    .assert.count('.todo', 3)
-
-  // toggle all
-  browser
-    .click('label[for="toggle-all"]')
-    .assert.count('.todo.completed', 3)
-    .click('label[for="toggle-all"]')
-    .assert.count('.todo:not(.completed)', 3)
-    .end()
-
-  function createNewItem (text) {
-    return browser
-      .setValue('.new-todo', [text, browser.Keys.ENTER])
-  }
-
-  function removeItemAt (n) {
-    return browser
-      .moveToElement('.todo:nth-child(' + n + ')', 10, 10)
-      .click('.todo:nth-child(' + n + ') .destroy')
-  }
-
-  function deleteValue (el, text) {
-    return browser.setValue(el, text.split('').map(() => '\u0008'))
-  }
-}
-
-module.exports = {
-  'todomvc/classic': function (browser) {
-    test(browser, 'http://localhost:8080/classic/todomvc/')
-  },
-  'todomvc/composition': function (browser) {
-    test(browser, 'http://localhost:8080/composition/todomvc/')
-  }
-}

+ 162 - 0
test/e2e/todomvc.spec.js

@@ -0,0 +1,162 @@
+import { setupPuppeteer, E2E_TIMEOUT } from 'test/helpers'
+
+describe('e2e/todomvc', () => {
+  const {
+    page,
+    isVisible,
+    isChecked,
+    isFocused,
+    text,
+    value,
+    count,
+    hasClass,
+    hover,
+    click,
+    keyUp,
+    setValue,
+    enterValue,
+    clearValue
+  } = setupPuppeteer()
+
+  async function testTodoMVC (url) {
+    await page().goto(url)
+
+    expect(await isVisible('.main')).toBe(false)
+    expect(await isVisible('.footer')).toBe(false)
+    expect(await count('.filters .selected')).toBe(1)
+
+    await enterValue('.new-todo', 'test')
+    expect(await count('.todo')).toBe(1)
+    expect(await isVisible('.todo .edit')).toBe(false)
+    expect(await text('.todo label')).toContain('test')
+    expect(await text('.todo-count strong')).toContain('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')).toContain('test2')
+    expect(await text('.todo-count strong')).toContain('2')
+
+    // toggle
+    await click('.todo .toggle')
+    expect(await count('.todo.completed')).toBe(1)
+    expect(await hasClass('.todo:nth-child(1)', 'completed')).toBe(true)
+    expect(await text('.todo-count strong')).toContain('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')).toContain('test3')
+    expect(await text('.todo-count strong')).toContain('2')
+
+    await enterValue('.new-todo', 'test4')
+    await enterValue('.new-todo', 'test5')
+    expect(await count('.todo')).toBe(5)
+    expect(await text('.todo-count strong')).toContain('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')).toContain('2')
+
+    // remove
+    await hover('.todo:nth-child(1)')
+    await click('.todo:nth-child(1) .destroy')
+    expect(await count('.todo')).toBe(4)
+    expect(await count('.todo.completed')).toBe(2)
+    expect(await text('.todo-count strong')).toContain('2')
+
+    await hover('.todo:nth-child(2)')
+    await click('.todo:nth-child(2) .destroy')
+    expect(await count('.todo')).toBe(3)
+    expect(await count('.todo.completed')).toBe(2)
+    expect(await text('.todo-count strong')).toContain('1')
+
+    // remove all
+    await click('.clear-completed')
+    expect(await count('.todo')).toBe(1)
+    expect(await text('.todo label')).toContain('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', 2)).toBe(2)
+
+    // complted filter
+    await click('.filters li:nth-child(3) a')
+    expect(await count('.todo')).toBe(2)
+    expect(await count('.todo.completed')).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 setValue('.todo:nth-child(1) .edit', 'edited!')
+    await click('footer') // 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 clearValue('.todo:nth-child(1) .edit')
+    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 setValue('.todo:nth-child(1) .edit', 'edited!')
+    await keyUp('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 clearValue('.todo:nth-child(1) .edit')
+    await enterValue('.todo:nth-child(1) .edit', ' ')
+    expect(await count('.todo')).toBe(3)
+
+    // toggle all
+    await click('label[for="toggle-all"]')
+    expect(await count('.todo.completed')).toBe(3)
+    await click('label[for="toggle-all"]')
+    expect(await count('.todo:not(.completed)')).toBe(3)
+  }
+
+  test('classic', async () => {
+    await testTodoMVC('http://localhost:8080/classic/todomvc/')
+  }, E2E_TIMEOUT)
+
+  test('composition', async () => {
+    await testTodoMVC('http://localhost:8080/composition/todomvc/')
+  }, E2E_TIMEOUT)
+})

+ 131 - 0
test/helpers.js

@@ -1,4 +1,5 @@
 import { createApp } from 'vue'
+import puppeteer from 'puppeteer'
 
 export function mount (store, component) {
   const el = createElement()
@@ -19,3 +20,133 @@ function createElement () {
 
   return el
 }
+
+export const E2E_TIMEOUT = 30 * 1000
+
+const puppeteerOptions = process.env.CI
+  ? { args: ['--no-sandbox', '--disable-setuid-sandbox'] }
+  : {}
+
+export function setupPuppeteer () {
+  let browser
+  let page
+
+  beforeEach(async () => {
+    browser = await puppeteer.launch(puppeteerOptions)
+    page = await browser.newPage()
+
+    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 browser.close()
+  })
+
+  async function click (selector, options) {
+    await page.click(selector, options)
+  }
+
+  async function hover (selector) {
+    await page.hover(selector)
+  }
+
+  async function keyUp (key) {
+    await page.keyboard.up(key)
+  }
+
+  async function count (selector) {
+    return (await page.$$(selector)).length
+  }
+
+  async function text (selector) {
+    return await page.$eval(selector, (node) => node.textContent)
+  }
+
+  async function value (selector) {
+    return await page.$eval(selector, (node) => node.value)
+  }
+
+  async function html (selector) {
+    return await page.$eval(selector, (node) => node.innerHTML)
+  }
+
+  async function classList (selector) {
+    return await page.$eval(selector, (node) => {
+      const list = []
+      for (const index in node.classList) {
+        list.push(node.classList[index])
+      }
+      return list
+    })
+  }
+
+  async function hasClass (selector, name) {
+    return (await classList(selector)).find(c => c === name) !== undefined
+  }
+
+  async function isVisible (selector) {
+    const display = await page.$eval(selector, (node) => {
+      return window.getComputedStyle(node).display
+    })
+    return display !== 'none'
+  }
+
+  async function isChecked (selector) {
+    return await page.$eval(selector, (node) => node.checked)
+  }
+
+  async function isFocused (selector) {
+    return await page.$eval(selector, (node) => node === document.activeElement)
+  }
+
+  async function setValue (selector, value) {
+    const el = (await page.$(selector))
+    await el.evaluate((node) => { node.value = '' })
+    await el.type(value)
+  }
+
+  async function enterValue (selector, value) {
+    const el = (await page.$(selector))
+    await el.evaluate((node) => { node.value = '' })
+    await el.type(value)
+    await el.press('Enter')
+  }
+
+  async function clearValue (selector) {
+    return await page.$eval(selector, (node) => { node.value = '' })
+  }
+
+  async function sleep (ms = 0) {
+    return new Promise((resolve) => {
+      setTimeout(resolve, ms)
+    })
+  }
+
+  return {
+    page: () => page,
+    click,
+    hover,
+    keyUp,
+    count,
+    text,
+    value,
+    html,
+    classList,
+    hasClass,
+    isVisible,
+    isChecked,
+    isFocused,
+    setValue,
+    enterValue,
+    clearValue,
+    sleep
+  }
+}

File diff suppressed because it is too large
+ 144 - 388
yarn.lock


Some files were not shown because too many files changed in this diff