浏览代码

test: migrate e2e test to puppeteer from nightwatch (#1778)

Kia King Ishii 4 年之前
父节点
当前提交
4f7a62bca8

+ 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'

+ 3 - 5
package.json

@@ -22,7 +22,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",
@@ -56,10 +56,8 @@
     "babel-loader": "^8.1.0",
     "brotli": "^1.3.2",
     "chalk": "^4.0.0",
-    "chromedriver": "^83.0.0",
     "conventional-changelog-cli": "^2.0.31",
     "cross-env": "^5.2.0",
-    "cross-spawn": "^6.0.5",
     "css-loader": "^2.1.0",
     "enquirer": "^2.3.5",
     "eslint": "^6.8.0",
@@ -67,12 +65,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": "^2.5.22",

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

@@ -0,0 +1,37 @@
+import { setupPuppeteer, E2E_TIMEOUT } from 'test/helpers'
+
+describe('e2e/cart', () => {
+  const { page, text, count, click, sleep } = setupPuppeteer()
+
+  test('cart app', async () => {
+    await page().goto('http://localhost:8080/shopping-cart/')
+
+    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)
+  }, E2E_TIMEOUT)
+})

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

@@ -0,0 +1,34 @@
+import { setupPuppeteer, E2E_TIMEOUT } from 'test/helpers'
+
+describe('e2e/chat', () => {
+  const { page, text, count, click, enterValue, sleep } = setupPuppeteer()
+
+  test('chat app', async () => {
+    await page().goto('http://localhost:8080/chat/')
+
+    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')
+  }, E2E_TIMEOUT)
+})

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

@@ -0,0 +1,30 @@
+import { setupPuppeteer, E2E_TIMEOUT } from 'test/helpers'
+
+describe('e2e/counter', () => {
+  const { page, text, click, sleep } = setupPuppeteer()
+
+  test('counter app', async () => {
+    await page().goto('http://localhost:8080/counter/')
+    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')
+  }, 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 - 30
test/e2e/specs/cart.js

@@ -1,30 +0,0 @@
-module.exports = {
-  'shopping cart': function (browser) {
-    browser
-      .url('http://localhost:8080/shopping-cart/')
-      .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()
-  }
-}

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

@@ -1,28 +0,0 @@
-module.exports = {
-  'chat': function (browser) {
-    browser
-      .url('http://localhost:8080/chat/')
-      .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')
-      .enterValue('.message-composer', 'hi')
-      .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')
-      .enterValue('.message-composer', 'hi')
-      .waitFor(50) // fake api
-      .assert.count('.message-list-item', 3)
-      .assert.containsText('.message-list-item:nth-child(3)', 'hi')
-  }
-}

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

@@ -1,23 +0,0 @@
-module.exports = {
-  'counter': function (browser) {
-    browser
-      .url('http://localhost:8080/counter/')
-      .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()
-  }
-}

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

@@ -1,160 +0,0 @@
-module.exports = {
-  'todomvc': function (browser) {
-    browser
-      .url('http://localhost:8080/todomvc/')
-      .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!')
-      .click('footer') // blur
-      .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!')
-      .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')
-    deleteValue('.todo:nth-child(1) .edit', 'edited again!')
-      .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')
-    deleteValue('.todo:nth-child(1) .edit', 'edited again!')
-      .enterValue('.todo:nth-child(1) .edit', ' ')
-      .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.enterValue('.new-todo', text)
-    }
-
-    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'))
-    }
-  }
-}

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

@@ -0,0 +1,154 @@
+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()
+
+  test('todomvc app', async () => {
+    await page().goto('http://localhost:8080/todomvc/')
+
+    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)
+  }, E2E_TIMEOUT)
+})

+ 132 - 0
test/helpers.js

@@ -0,0 +1,132 @@
+import puppeteer from 'puppeteer'
+
+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
+  }
+}
+

文件差异内容过多而无法显示
+ 164 - 388
yarn.lock


部分文件因为文件数量过多而无法显示