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"]
}