diff --git a/.eslintignore b/.eslintignore index 48c125d8e..f1f51d4e4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ cypress.config.ts +playwright.config.ts diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml new file mode 100644 index 000000000..d4b729630 --- /dev/null +++ b/.github/workflows/ui-tests.yml @@ -0,0 +1,40 @@ +name: 'Run Playwright UI tests' + +on: + pull_request: + +env: + USER_EMAIL: "uitest-${{ github.run_id }}@lumeer.io" + USER_PASSWORD: ${{ secrets.USER_PASSWORD }} + +jobs: + run-tests: + timeout-minutes: 60 + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11.0.12' + - uses: actions/setup-node@v3 + with: + node-version: 16 + - name: Build (mvn clean install) + run: | + chmod +x ./playwright-scripts/build.sh + ./playwright-scripts/build.sh + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run tests + run: | + chmod +x ./playwright-scripts/playwright-tests.sh + ./playwright-scripts/playwright-tests.sh + shell: bash + - name: "Upload artifact" + uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 38fb7342f..eea6d8a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ src/assets/config/config.json .settings/ **/.DS_Store +/test-results/ +/playwright-report/ +/playwright/.cache/ +/playwright/.auth/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f06000219..4ae8ea289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "d3-time-format": "^4.1.0", "d3-zoom": "^3.0.0", "dompurify": "^2.4.4", + "dotenv": "^16.3.1", "driver.js": "^0.9.8", "file-saver": "^2.0.5", "flag-icons": "^6.6.6", @@ -91,6 +92,7 @@ "@angular/language-service": "^14.2.12", "@compodoc/compodoc": "^1.1.19", "@ngrx/schematics": "^14.3.3", + "@playwright/test": "^1.35.1", "@sentry/cli": "^2.7.0", "@types/auth0-js": "^9.14.7", "@types/big.js": "^6.1.6", @@ -4274,6 +4276,25 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@playwright/test": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", + "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.35.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/@plotly/d3-sankey": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", @@ -9755,6 +9776,17 @@ "dottojs": "bin/dot-packer" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/double-bits": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/double-bits/-/double-bits-1.1.1.tgz", @@ -19294,6 +19326,18 @@ "uniq": "^1.0.0" } }, + "node_modules/playwright-core": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", + "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/plotly.js": { "version": "1.54.7", "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.54.7.tgz", @@ -28208,6 +28252,17 @@ "which": "^2.0.2" } }, + "@playwright/test": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", + "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "dev": true, + "requires": { + "@types/node": "*", + "fsevents": "2.3.2", + "playwright-core": "1.35.1" + } + }, "@plotly/d3-sankey": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@plotly/d3-sankey/-/d3-sankey-0.7.2.tgz", @@ -32534,6 +32589,11 @@ "integrity": "sha512-/nt74Rm+PcfnirXGEdhZleTwGC2LMnuKTeeTIlI82xb5loBBoXNYzr2ezCroPSMtilK8EZIfcNZwOcHN+ib1Lg==", "dev": true }, + "dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + }, "double-bits": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/double-bits/-/double-bits-1.1.1.tgz", @@ -39928,6 +39988,12 @@ "uniq": "^1.0.0" } }, + "playwright-core": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", + "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "dev": true + }, "plotly.js": { "version": "1.54.7", "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.54.7.tgz", diff --git a/package.json b/package.json index 654640638..fad59cedb 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "test:watch": "ng test --watch", "cypress:open": "npx cypress open", "cypress:run": "npx cypress run", - "prepare": "husky install" + "prepare": "husky install", + "playwright:run": "playwright test" }, "repository": { "type": "git", @@ -79,6 +80,7 @@ "d3-time-format": "^4.1.0", "d3-zoom": "^3.0.0", "dompurify": "^2.4.4", + "dotenv": "^16.3.1", "driver.js": "^0.9.8", "file-saver": "^2.0.5", "flag-icons": "^6.6.6", @@ -121,6 +123,7 @@ "@angular/language-service": "^14.2.12", "@compodoc/compodoc": "^1.1.19", "@ngrx/schematics": "^14.3.3", + "@playwright/test": "^1.35.1", "@sentry/cli": "^2.7.0", "@types/auth0-js": "^9.14.7", "@types/big.js": "^6.1.6", diff --git a/playwright-scripts/build.sh b/playwright-scripts/build.sh new file mode 100755 index 000000000..7f7e54bdf --- /dev/null +++ b/playwright-scripts/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Abort on Error +set -e + +# Set up a repeating loop to send some output to Travis. +export PING_SLEEP=300s +bash -c "while true; do echo \$(date) - building ...; sleep $PING_SLEEP; done" & +export BUILD_PING_LOOP_PID=$! + +LUMEER_ENV=testing COMPILER_NODE_OPTIONS=--max_old_space_size=4000 mvn clean install war:war -Dcontext.root=/ + +kill $BUILD_PING_LOOP_PID 2> /dev/null || : diff --git a/playwright-scripts/playwright-tests.sh b/playwright-scripts/playwright-tests.sh new file mode 100755 index 000000000..3077bf182 --- /dev/null +++ b/playwright-scripts/playwright-tests.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Abort on Error +set -e + +# Set up a repeating loop to send some output to Github Actions environment. +export PING_SLEEP=300s +bash -c "while true; do echo \$(date) - building ...; sleep $PING_SLEEP; done" & +export PING_LOOP_PID=$! + +echo "Starting frontend..." +npm run http-server & +while ! curl --output /dev/null --silent -r 0-0 --fail "http://127.0.0.1:7000"; do + sleep 3 +done +cd playwright-scripts +echo "Starting backend..." +./start-engine.sh +RESPONSE=$(curl http://localhost:8080/lumeer-engine/) + +PASSED=false +cd ../ +echo "Running E2E tests..." +set +e +npm run playwright:run +if [[ $? -ne 0 ]]; then + set -e + npm run playwright:run + + if [[ $? -eq 0 ]]; then + PASSED=true + fi +else + set -e + PASSED=true +fi + +echo "Stopping frontend..." +pkill npm + +cd playwright-scripts +echo "Stopping backend..." +./stop-engine.sh + +echo "Printing bundle sizes..." +npm run bundlesize + +kill $PING_LOOP_PID diff --git a/playwright-scripts/start-engine.sh b/playwright-scripts/start-engine.sh new file mode 100755 index 000000000..4b34254c4 --- /dev/null +++ b/playwright-scripts/start-engine.sh @@ -0,0 +1,38 @@ +#!/bin/bash +ORIG=$(pwd) + +if [ -d ~/.engine -a -d ~/.engine/.git ]; then + echo "Pulling latest engine updates..." + cd ~/.engine + git checkout devel + git pull origin devel +else + echo "Downloading engine..." + git clone https://github.com/Lumeer/engine.git ~/.engine + cd ~/.engine + git checkout devel +fi + +echo ls +chmod +w ./lumeer-core/src/main/resources/defaults-dev.properties +echo $'\n' >> ./lumeer-core/src/main/resources/defaults-dev.properties +echo $"admin_user_emails=$USER_EMAIL" >> ./lumeer-core/src/main/resources/defaults-dev.properties +cat ./lumeer-core/src/main/resources/defaults-dev.properties +echo "Building engine..." +mvn install -DskipTests -DskipITs -B --quiet +cd war + +echo "Starting engine..." +export SKIP_LIMITS=true +mvn -s settings.xml wildfly:run -PstartEngine -B --quiet & +echo $! > $ORIG/engine.pid + +echo "Waiting for engine to start..." +while test $(curl -s -o /dev/null -I -w "%{http_code}" http://localhost:8080/lumeer-engine/rest/users) != 401; do + sleep 10 +done +sleep 5 + +echo "Engine started!" + +cd $ORIG diff --git a/playwright-scripts/stop-engine.sh b/playwright-scripts/stop-engine.sh new file mode 100755 index 000000000..1944ac5b0 --- /dev/null +++ b/playwright-scripts/stop-engine.sh @@ -0,0 +1,16 @@ +#!/bin/bash +PID=$(cat engine.pid) +if [ ! -z $PID ]; then + echo "Stopping engine..." + if ps -p $PID >/dev/null; then + kill $PID + sleep 5 + if ps -p $PID >/dev/null; then + kill -9 $PID + fi + fi + + rm engine.pid + + echo "Engine stopped!" +fi diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..64302bec5 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,55 @@ +import {defineConfig, devices} from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './playwright', + timeout: 60000, + 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: 'html', + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + baseURL: 'http://localhost:7000/ui', + }, + expect: { + timeout: 10000, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'onboarding', + use: {...devices['Desktop Chrome']}, + testMatch: /onboarding.spec\.ts/, + teardown: 'teardown', + }, + { + name: 'tests', + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/user.json', + }, + testMatch: '*playwright/*.spec.ts', + testIgnore: /onboarding.spec\.ts/, + dependencies: ['onboarding'], + }, + { + name: 'teardown', + testMatch: /global.teardown\.ts/, + }, + ], +}); diff --git a/playwright/global.teardown.ts b/playwright/global.teardown.ts new file mode 100644 index 000000000..870501b75 --- /dev/null +++ b/playwright/global.teardown.ts @@ -0,0 +1,46 @@ +/* + * Lumeer: Modern Data Definition and Processing Platform + * + * Copyright (C) since 2017 Lumeer.io, s.r.o. and/or its affiliates. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import {test} from '@playwright/test'; +import dotenv from 'dotenv'; + +dotenv.config(); +const userEmail = process.env.USER_EMAIL ?? ''; +const userPassword = process.env.USER_PASSWORD ?? ''; + +test('Remove user from Auth0', async ({page, request}) => { + const loginFormData = new URLSearchParams(); + loginFormData.append('userName', userEmail); + loginFormData.append('password', userPassword); + + const loginReponse = await request.post('http://localhost:8080/lumeer-engine/rest/users/login', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: loginFormData.toString(), + }); + + const parsed_body = JSON.parse(await loginReponse.text()); + + const deleteUserRequest = await request.delete('http://localhost:8080/lumeer-engine/rest/users/current', { + headers: { + Authorization: `Bearer ${parsed_body['access_token']}`, + }, + }); +}); diff --git a/playwright/onboarding.spec.ts b/playwright/onboarding.spec.ts new file mode 100644 index 000000000..c663769f2 --- /dev/null +++ b/playwright/onboarding.spec.ts @@ -0,0 +1,115 @@ +/* + * Lumeer: Modern Data Definition and Processing Platform + * + * Copyright (C) since 2017 Lumeer.io, s.r.o. and/or its affiliates. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +/* + * Lumeer: Modern Data Definition and Processing Platform + * + * Copyright (C) since 2017 Lumeer.io, s.r.o. and/or its affiliates. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import {test, expect} from '@playwright/test'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const authFile = 'playwright/.auth/user.json'; + +const userEmail = process.env.USER_EMAIL ?? ''; +const userPassword = process.env.USER_PASSWORD ?? ''; + +test('On boarding path', async ({page, request}) => { + await page.goto('http://localhost:7000/ui'); + + await expect(page.locator('form[class=auth0-lock-widget]')).toBeVisible(); + await page.click('a:text("Sign up")'); + + await page.locator('input[placeholder="your@work.email"]').fill(userEmail); + await page.locator('input[type=password]').fill(userPassword); + await page.click('button[type=submit]'); + + await expect(page.locator('span:text("Authorize App")')).toBeVisible(); + await page.click('button[id="allow"]'); + + await page.waitForLoadState('networkidle'); + + await expect(page.locator('div[class=card-body]')).toBeVisible(); + await page.click('button:has(span:text("Yes"))'); + + await page.waitForLoadState('networkidle'); + await expect(page.locator('form')).toBeVisible(); + await page.check('input[id=agreement]'); + await page.click('button[type=submit]'); + + await page.waitForLoadState('networkidle'); + await expect(page.locator('input[id=newsletter]')).toBeEnabled(); + await expect(page.locator('button[type=submit]:has-text("Continue")')).toBeEnabled(); + await page.click('button[type=submit]:has-text("Continue")'); + + await page.click('div:text("Scrum")'); + await page.locator('button[type=button]:has-text("Use this template")').click(); + await page.locator('button[type=button]:has-text("I\'ll do it later")').click(); + + await expect(page.locator('modal-wrapper')).toBeVisible(); + + const loginFormData = new URLSearchParams(); + loginFormData.append('userName', userEmail); + loginFormData.append('password', userPassword); + + const loginReponse = await request.post('http://localhost:8080/lumeer-engine/rest/users/login', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: loginFormData.toString(), + }); + + const parsed_body = JSON.parse(await loginReponse.text()); + + await request.post('http://localhost:8080/lumeer-engine/rest/users/current/emailVerified', { + headers: { + Authorization: `Bearer ${parsed_body['access_token']}`, + }, + }); + + await page.waitForTimeout(10000); + + if (await page.locator('button[type=button]:has-text("Reload")').isVisible()) { + await page.locator('button[type=button]:has-text("Reload")').click(); + } + + await expect(page.locator('iframe[title="Lumeer: Quick Application Overview"]')).toBeVisible(); + + await page.getByRole('button', {name: 'Get started'}).click(); + await page.waitForTimeout(1000); + + await page.getByRole('button', {name: 'Dismiss'}).click(); + await page.waitForTimeout(1000); + + await page.context().storageState({path: authFile}); +}); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 56fd56806..dad273abf 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "playwright/**/*.ts"] }