From 0e62cef583f8618287157bb44b5df97436b5f714 Mon Sep 17 00:00:00 2001 From: "Kate.Dev" <96145786+katedev21@users.noreply.github.com> Date: Mon, 7 Nov 2022 12:28:21 -0500 Subject: [PATCH] E2E integration (#269) Adds playwright dev dependency, folders, and tests to support end-to-end tests. Add http-server as a dev dependency and use for playwright. --- .github/workflows/node.js.yml | 8 ++ .gitignore | 3 + e2e/ward-leaders-home.spec.js | 20 +++++ package-lock.json | 159 ++++++++++++++++++++++++++++++++++ package.json | 9 +- playwright.config.js | 114 ++++++++++++++++++++++++ tests/app.spec.js | 25 +++--- 7 files changed, 323 insertions(+), 15 deletions(-) create mode 100644 e2e/ward-leaders-home.spec.js create mode 100644 playwright.config.js diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index c814936f..33506cb7 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -30,3 +30,11 @@ jobs: - run: npm run lint - run: npm run build - run: npm test + - run: npm run e2e-ci + - name: Upload E2E Test Report + uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index a2e0ea9a..9a3ddf0b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ data-scripts/*.pyc *.ntvs* *.njsproj *.sln +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/e2e/ward-leaders-home.spec.js b/e2e/ward-leaders-home.spec.js new file mode 100644 index 00000000..b038ce25 --- /dev/null +++ b/e2e/ward-leaders-home.spec.js @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test' + +test('homepage has "Philly Ward Leaders" as the page title and clicking "Get started" navigates to "/leaders/democratic"', async ({ page }) => { + await page.goto('localhost:8080') + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle('Philly Ward Leaders') + + // create a locator + const getStarted = page.getByText('Get started') + + // Expect an attribute "to be strictly equal" to the value. + await expect(getStarted).toHaveAttribute('href', '/leaders') + + // Click the get started link. + await getStarted.click() + + // Expects the URL to contain /leaders/democratic + await expect(page).toHaveURL(/.*leaders\/democratic/) +}) diff --git a/package-lock.json b/package-lock.json index 823e6023..3bf6803d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,6 +86,24 @@ "uglify-js": "2.7.4" } }, + "@playwright/test": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz", + "integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.27.1" + }, + "dependencies": { + "playwright-core": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz", + "integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==", + "dev": true + } + } + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -2125,6 +2143,15 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -3159,6 +3186,12 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true + }, "cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -6443,6 +6476,105 @@ } } }, + "http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "requires": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -9587,6 +9719,12 @@ "integrity": "sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==", "dev": true }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -11873,6 +12011,12 @@ } } }, + "secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -13307,6 +13451,15 @@ "which-boxed-primitive": "^1.0.2" } }, + "union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "requires": { + "qs": "^6.4.0" + } + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -13444,6 +13597,12 @@ } } }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, "url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", diff --git a/package.json b/package.json index 575a2602..c7ac2d33 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,11 @@ "scripts": { "start": "cross-env NODE_ENV=development webpack-dev-server --host 0.0.0.0 --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", - "test": "npm run lint && jest", - "lint": "standard --plugin html '**/*.{js,vue}'" + "test": "jest --testPathIgnorePatterns ./e2e", + "lint": "standard --plugin html '**/*.{js,vue}'", + "e2e": "playwright test", + "e2e-ci": "playwright install --with-deps && playwright test", + "serve-http": "http-server ./public -p 8080" }, "dependencies": { "@mapbox/leaflet-pip": "^1.1.0", @@ -31,6 +34,7 @@ "vuex": "^2.4.0" }, "devDependencies": { + "@playwright/test": "^1.27.1", "@vue/test-utils": "^1.3.0", "autoprefixer": "^9.8.8", "babel-core": "^6.0.0", @@ -44,6 +48,7 @@ "css-loader": "^1.0.1", "eslint-plugin-html": "^3.2.2", "file-loader": "^1.1.11", + "http-server": "^14.1.1", "imports-loader": "^0.7.1", "jest": "^21.2.1", "jest-serializer-vue": "^0.2.0", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 00000000..5d6ee8ea --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,114 @@ +// @ts-check +const { devices } = require('@playwright/test') + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * @see https://playwright.dev/docs/test-configuration + * @type {import('@playwright/test').PlaywrightTestConfig} + */ +const config = { + testDir: './e2e', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + webServer: { + command: 'npm run serve-http', + url: 'http://localhost:8080', + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + }, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry' + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'] + } + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'] + } + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'] + } + } + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ] + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +} + +module.exports = config diff --git a/tests/app.spec.js b/tests/app.spec.js index 6857032d..4122498f 100644 --- a/tests/app.spec.js +++ b/tests/app.spec.js @@ -1,4 +1,4 @@ -import { shallow } from '@vue/test-utils' +import { shallowMount } from '@vue/test-utils' import Vue from 'vue' import Vuex from 'vuex' import Buefy from 'buefy' @@ -16,14 +16,14 @@ describe('App', () => { } }) - const wrapper = shallow(App, { + const wrapper = shallowMount(App, { mocks: { $store }, stubs: ['router-view'] }) - const indicator = wrapper.find(Buefy.Loading) - const isActive = indicator.hasProp('active', false) - expect(isActive).toBe(true) + const indicator = wrapper.findComponent(Buefy.Loading) + + expect(indicator.props('active')).toBe(false) }) test('Shows loading indicator when there are pending requests', () => { @@ -35,14 +35,13 @@ describe('App', () => { } }) - const wrapper = shallow(App, { + const wrapper = shallowMount(App, { mocks: { $store }, stubs: ['router-view'] }) - const indicator = wrapper.find(Buefy.Loading) - const isActive = indicator.hasProp('active', true) - expect(isActive).toBe(true) + const indicator = wrapper.findComponent(Buefy.Loading) + expect(indicator.props('active')).toBe(true) }) test('Shows notifications', () => { @@ -55,12 +54,12 @@ describe('App', () => { } }) - const wrapper = shallow(App, { + const wrapper = shallowMount(App, { mocks: { $store }, stubs: ['router-view'] }) - const notifications = wrapper.findAll(Notification) + const notifications = wrapper.findAllComponents(Notification) expect(notifications.length).toBe(2) }) @@ -77,12 +76,12 @@ describe('App', () => { } }) - const wrapper = shallow(App, { + const wrapper = shallowMount(App, { mocks: { $store }, stubs: ['router-view'] }) - const notification = wrapper.find(Notification) + const notification = wrapper.findComponent(Notification) notification.vm.$emit('dismiss') expect(removeNotification).toBeCalled() })