diff --git a/README.docker.md b/README.docker.md
index e99fef976..23f8ff29e 100644
--- a/README.docker.md
+++ b/README.docker.md
@@ -21,7 +21,7 @@ DATABASE_PASSWORD=XXX # choose any PostgreSQL password
DATABASE_HOST=db
DATABASE_PORT=5432
DB_PATH=dvoting # LMDB database path
-FRONT_END_URL=http://127.0.0.1:3000
+FRONT_END_URL=http://127.0.0.1:3000 # the automated frontend tests expect this value do not change it
BACKEND_HOST=backend
BACKEND_PORT=5000
SESSION_SECRET=XXX # choose any secret
diff --git a/web/frontend/.gitignore b/web/frontend/.gitignore
index f2e59e5f7..0f2aa48fc 100644
--- a/web/frontend/.gitignore
+++ b/web/frontend/.gitignore
@@ -25,4 +25,7 @@ yarn-error.log*
.idea
.vscode
-
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/web/frontend/README.md b/web/frontend/README.md
new file mode 100644
index 000000000..6625e1319
--- /dev/null
+++ b/web/frontend/README.md
@@ -0,0 +1,52 @@
+# Automated Tests w/ Playwright
+
+## Setup
+
+To install Playwright run
+
+```
+npm ci
+npx playwright install
+```
+
+which will install the module and its dependencies. In
+order to execute the environment necessary for the tests,
+additional system dependencies need to be installed.
+
+Run
+
+```
+npx playwright install-deps --dry-run
+```
+
+to be shown the dependencies that you need to install on your machine (requires `root` access).
+
+Your local frontend must be accessible at `http://127.0.0.1:3000`.
+
+## Run tests
+
+Run
+
+```
+npx playwright test
+```
+
+to run the tests. This will open a window in your browser w/ the test results.
+
+To run interactive tests, run
+
+```
+npx playwright test --ui
+```
+
+this will open an user interface where you can interactively run and evaluate tests.
+
+## Update HAR files
+
+To update the HAR files, you need to make sure
+
+* that a complete D-Voting setup is running, as the API will be called for real, and
+* the `REACT_APP_SCIPER_ADMIN` of the D-Voting instance value is set to `123456` (i.e. the `SCIPER_ADMIN` value in the mocks).
+
+You then change the `UPDATE = false` value in `tests/mocks.ts` to `UPDATE = true` and execute
+the tests as usual. The tests that update the mocks will be run and the other tests will be skipped.
diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json
index 30dcfa2ce..3d558a594 100644
--- a/web/frontend/package-lock.json
+++ b/web/frontend/package-lock.json
@@ -31,6 +31,7 @@
"yup": "^0.32.11"
},
"devDependencies": {
+ "@playwright/test": "^1.40.1",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.5",
"@types/file-saver": "^2.0.5",
@@ -2865,6 +2866,21 @@
"integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==",
"dev": true
},
+ "node_modules/@playwright/test": {
+ "version": "1.40.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz",
+ "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==",
+ "dev": true,
+ "dependencies": {
+ "playwright": "1.40.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz",
@@ -12221,6 +12237,36 @@
"node": ">=6"
}
},
+ "node_modules/playwright": {
+ "version": "1.40.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz",
+ "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==",
+ "dev": true,
+ "dependencies": {
+ "playwright-core": "1.40.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.40.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz",
+ "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
@@ -18706,6 +18752,15 @@
"integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==",
"dev": true
},
+ "@playwright/test": {
+ "version": "1.40.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz",
+ "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==",
+ "dev": true,
+ "requires": {
+ "playwright": "1.40.1"
+ }
+ },
"@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz",
@@ -25657,6 +25712,22 @@
}
}
},
+ "playwright": {
+ "version": "1.40.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz",
+ "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==",
+ "dev": true,
+ "requires": {
+ "fsevents": "2.3.2",
+ "playwright-core": "1.40.1"
+ }
+ },
+ "playwright-core": {
+ "version": "1.40.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz",
+ "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==",
+ "dev": true
+ },
"postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
diff --git a/web/frontend/package.json b/web/frontend/package.json
index b3a63d718..ccb6f6526 100644
--- a/web/frontend/package.json
+++ b/web/frontend/package.json
@@ -23,6 +23,7 @@
"file-saver": "^2.0.5",
"i18next": "^21.6.10",
"i18next-browser-languagedetector": "^6.1.3",
+ "pg": "^8.11.1",
"prop-types": "^15.8.1",
"react": "^17.0.1",
"react-beautiful-dnd": "^13.1.0",
@@ -33,10 +34,10 @@
"react-scripts": "5.0.0",
"short-unique-id": "^4.4.4",
"web-vitals": "^2.1.4",
- "yup": "^0.32.11",
- "pg": "^8.11.1"
+ "yup": "^0.32.11"
},
"devDependencies": {
+ "@playwright/test": "^1.40.1",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.5",
"@types/file-saver": "^2.0.5",
diff --git a/web/frontend/playwright.config.ts b/web/frontend/playwright.config.ts
new file mode 100644
index 000000000..e29a0ce76
--- /dev/null
+++ b/web/frontend/playwright.config.ts
@@ -0,0 +1,38 @@
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ /* 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',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL: 'http://127.0.0.1:3000',
+ /* 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'] },
+ },
+ ],
+
+});
diff --git a/web/frontend/src/layout/NavBar.tsx b/web/frontend/src/layout/NavBar.tsx
index 3ac090269..fc2721824 100644
--- a/web/frontend/src/layout/NavBar.tsx
+++ b/web/frontend/src/layout/NavBar.tsx
@@ -179,7 +179,6 @@ const LeftSideNavBar = ({ authCtx, t }) => (
-
diff --git a/web/frontend/tests/hars/123456/a03037964db4d746517f562ccb09b049db7f7a12.json b/web/frontend/tests/hars/123456/a03037964db4d746517f562ccb09b049db7f7a12.json
new file mode 100644
index 000000000..18abd32b4
--- /dev/null
+++ b/web/frontend/tests/hars/123456/a03037964db4d746517f562ccb09b049db7f7a12.json
@@ -0,0 +1 @@
+{"sciper":123456,"lastName":"123456","firstName":"sciper-#","isLoggedIn":true,"authorization":{"roles":["add","list","remove"],"proxies":["post","put","delete"],"election":["create"]}}
\ No newline at end of file
diff --git a/web/frontend/tests/hars/123456/get_dev_login.har b/web/frontend/tests/hars/123456/get_dev_login.har
new file mode 100644
index 000000000..3b9cb5b4c
--- /dev/null
+++ b/web/frontend/tests/hars/123456/get_dev_login.har
@@ -0,0 +1,14 @@
+{
+ "log": {
+ "version": "1.2",
+ "creator": {
+ "name": "Playwright",
+ "version": "1.40.1"
+ },
+ "browser": {
+ "name": "chromium",
+ "version": "120.0.6099.28"
+ },
+ "entries": []
+ }
+}
\ No newline at end of file
diff --git a/web/frontend/tests/hars/123456/personal_info.har b/web/frontend/tests/hars/123456/personal_info.har
new file mode 100644
index 000000000..1482d5dda
--- /dev/null
+++ b/web/frontend/tests/hars/123456/personal_info.har
@@ -0,0 +1,61 @@
+{
+ "log": {
+ "version": "1.2",
+ "creator": {
+ "name": "Playwright",
+ "version": "1.40.1"
+ },
+ "browser": {
+ "name": "chromium",
+ "version": "120.0.6099.28"
+ },
+ "entries": [
+ {
+ "startedDateTime": "2023-12-07T15:16:05.427Z",
+ "time": 63.36,
+ "request": {
+ "method": "GET",
+ "url": "http://127.0.0.1:3000/api/personal_info",
+ "httpVersion": "HTTP/1.1",
+ "cookies": [],
+ "headers": [
+ { "name": "Accept", "value": "*/*" },
+ { "name": "Accept-Language", "value": "en-US" },
+ { "name": "Cookie", "value": "connect.sid=s%3AxeUPf49ZYc9AUrUtzig9-O4g0Z_RVtVV.ChZVAx%2FL%2BIzfgIlkxzUxTLVduImdZIp68OdvODtveTU" },
+ { "name": "Referer", "value": "http://127.0.0.1:3000/about" },
+ { "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.28 Safari/537.36" }
+ ],
+ "queryString": [],
+ "headersSize": -1,
+ "bodySize": -1
+ },
+ "response": {
+ "status": 200,
+ "statusText": "OK",
+ "httpVersion": "HTTP/1.1",
+ "cookies": [],
+ "headers": [
+ { "name": "access-control-allow-origin", "value": "*" },
+ { "name": "connection", "value": "keep-alive" },
+ { "name": "content-length", "value": "184" },
+ { "name": "content-type", "value": "application/json; charset=utf-8" },
+ { "name": "date", "value": "Thu, 07 Dec 2023 15:16:05 GMT" },
+ { "name": "etag", "value": "W/\"b8-oDA3lk2010ZRf1YsywmwSdt/ehI\"" },
+ { "name": "keep-alive", "value": "timeout=5" },
+ { "name": "x-powered-by", "value": "Express" }
+ ],
+ "content": {
+ "size": -1,
+ "mimeType": "application/json; charset=utf-8",
+ "_file": "a03037964db4d746517f562ccb09b049db7f7a12.json"
+ },
+ "headersSize": -1,
+ "bodySize": -1,
+ "redirectURL": ""
+ },
+ "cache": {},
+ "timings": { "send": -1, "wait": -1, "receive": 63.36 }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/web/frontend/tests/hars/789012/5f05240cb33cb9e581ea0ce24c4e728a04e2a5d1.json b/web/frontend/tests/hars/789012/5f05240cb33cb9e581ea0ce24c4e728a04e2a5d1.json
new file mode 100644
index 000000000..fbbb5fb20
--- /dev/null
+++ b/web/frontend/tests/hars/789012/5f05240cb33cb9e581ea0ce24c4e728a04e2a5d1.json
@@ -0,0 +1 @@
+{"sciper":789012,"lastName":"789012","firstName":"sciper-#","isLoggedIn":true,"authorization":{}}
\ No newline at end of file
diff --git a/web/frontend/tests/hars/789012/get_dev_login.har b/web/frontend/tests/hars/789012/get_dev_login.har
new file mode 100644
index 000000000..3b9cb5b4c
--- /dev/null
+++ b/web/frontend/tests/hars/789012/get_dev_login.har
@@ -0,0 +1,14 @@
+{
+ "log": {
+ "version": "1.2",
+ "creator": {
+ "name": "Playwright",
+ "version": "1.40.1"
+ },
+ "browser": {
+ "name": "chromium",
+ "version": "120.0.6099.28"
+ },
+ "entries": []
+ }
+}
\ No newline at end of file
diff --git a/web/frontend/tests/hars/789012/personal_info.har b/web/frontend/tests/hars/789012/personal_info.har
new file mode 100644
index 000000000..2f68ed6a7
--- /dev/null
+++ b/web/frontend/tests/hars/789012/personal_info.har
@@ -0,0 +1,61 @@
+{
+ "log": {
+ "version": "1.2",
+ "creator": {
+ "name": "Playwright",
+ "version": "1.40.1"
+ },
+ "browser": {
+ "name": "chromium",
+ "version": "120.0.6099.28"
+ },
+ "entries": [
+ {
+ "startedDateTime": "2023-12-07T15:14:39.129Z",
+ "time": 56.608,
+ "request": {
+ "method": "GET",
+ "url": "http://127.0.0.1:3000/api/personal_info",
+ "httpVersion": "HTTP/1.1",
+ "cookies": [],
+ "headers": [
+ { "name": "Accept", "value": "*/*" },
+ { "name": "Accept-Language", "value": "en-US" },
+ { "name": "Cookie", "value": "connect.sid=s%3AAN1F283daySUs-3ytVagTfigWaedTgqO.iahChcGgJKx0khpkcm1jBBrQ89QsRN5cW9tOWnIgKgs" },
+ { "name": "Referer", "value": "http://127.0.0.1:3000/about" },
+ { "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.28 Safari/537.36" }
+ ],
+ "queryString": [],
+ "headersSize": -1,
+ "bodySize": -1
+ },
+ "response": {
+ "status": 200,
+ "statusText": "OK",
+ "httpVersion": "HTTP/1.1",
+ "cookies": [],
+ "headers": [
+ { "name": "access-control-allow-origin", "value": "*" },
+ { "name": "connection", "value": "keep-alive" },
+ { "name": "content-length", "value": "97" },
+ { "name": "content-type", "value": "application/json; charset=utf-8" },
+ { "name": "date", "value": "Thu, 07 Dec 2023 15:14:39 GMT" },
+ { "name": "etag", "value": "W/\"61-XwUkDLM8ueWB6gziTE5yigTipdE\"" },
+ { "name": "keep-alive", "value": "timeout=5" },
+ { "name": "x-powered-by", "value": "Express" }
+ ],
+ "content": {
+ "size": -1,
+ "mimeType": "application/json; charset=utf-8",
+ "_file": "5f05240cb33cb9e581ea0ce24c4e728a04e2a5d1.json"
+ },
+ "headersSize": -1,
+ "bodySize": -1,
+ "redirectURL": ""
+ },
+ "cache": {},
+ "timings": { "send": -1, "wait": -1, "receive": 56.608 }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/web/frontend/tests/hars/anonymous/9fd2ff4fc1fb163717539fb04aec420feeb11e5a.html b/web/frontend/tests/hars/anonymous/9fd2ff4fc1fb163717539fb04aec420feeb11e5a.html
new file mode 100644
index 000000000..e2c1c215e
--- /dev/null
+++ b/web/frontend/tests/hars/anonymous/9fd2ff4fc1fb163717539fb04aec420feeb11e5a.html
@@ -0,0 +1 @@
+Unauthenticated
\ No newline at end of file
diff --git a/web/frontend/tests/hars/anonymous/personal_info.har b/web/frontend/tests/hars/anonymous/personal_info.har
new file mode 100644
index 000000000..91474fd6a
--- /dev/null
+++ b/web/frontend/tests/hars/anonymous/personal_info.har
@@ -0,0 +1,60 @@
+{
+ "log": {
+ "version": "1.2",
+ "creator": {
+ "name": "Playwright",
+ "version": "1.40.1"
+ },
+ "browser": {
+ "name": "chromium",
+ "version": "120.0.6099.28"
+ },
+ "entries": [
+ {
+ "startedDateTime": "2023-12-07T15:11:00.609Z",
+ "time": 50.718,
+ "request": {
+ "method": "GET",
+ "url": "http://127.0.0.1:3000/api/personal_info",
+ "httpVersion": "HTTP/1.1",
+ "cookies": [],
+ "headers": [
+ { "name": "Accept", "value": "*/*" },
+ { "name": "Accept-Language", "value": "en-US" },
+ { "name": "Referer", "value": "http://127.0.0.1:3000/about" },
+ { "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.28 Safari/537.36" }
+ ],
+ "queryString": [],
+ "headersSize": -1,
+ "bodySize": -1
+ },
+ "response": {
+ "status": 401,
+ "statusText": "Unauthorized",
+ "httpVersion": "HTTP/1.1",
+ "cookies": [],
+ "headers": [
+ { "name": "connection", "value": "keep-alive" },
+ { "name": "content-length", "value": "15" },
+ { "name": "content-type", "value": "text/html; charset=utf-8" },
+ { "name": "date", "value": "Thu, 07 Dec 2023 15:11:00 GMT" },
+ { "name": "etag", "value": "W/\"f-n9L/T8H7FjcXU5+wSuxCD+6xHlo\"" },
+ { "name": "keep-alive", "value": "timeout=5" },
+ { "name": "set-cookie", "value": "connect.sid=s%3AQhEmafnnGB6uPjxRFKwNdJg9MDQYmbkB.r0PBLNSKHdFLqMKbOVv%2FrgQVtN1i6JnbLElS%2Fzt7q%2F8; Path=/; Expires=Fri, 08 Dec 2023 15:11:00 GMT; HttpOnly" },
+ { "name": "x-powered-by", "value": "Express" }
+ ],
+ "content": {
+ "size": -1,
+ "mimeType": "text/html; charset=utf-8",
+ "_file": "9fd2ff4fc1fb163717539fb04aec420feeb11e5a.html"
+ },
+ "headersSize": -1,
+ "bodySize": -1,
+ "redirectURL": ""
+ },
+ "cache": {},
+ "timings": { "send": -1, "wait": -1, "receive": 50.718 }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/web/frontend/tests/mocks.ts b/web/frontend/tests/mocks.ts
new file mode 100644
index 000000000..fdbef3b07
--- /dev/null
+++ b/web/frontend/tests/mocks.ts
@@ -0,0 +1,43 @@
+export const SCIPER_ADMIN = '123456';
+export const SCIPER_USER = '789012';
+export const UPDATE = false;
+
+export async function mockPersonalInfo (page: any, sciper?: string) {
+ // clear current mock
+ await page.unroute('/api/personal_info');
+ await page.routeFromHAR(
+ `./tests/hars/${sciper ?? 'anonymous'}/personal_info.har`,
+ {
+ url: '/api/personal_info',
+ update: UPDATE,
+ });
+}
+
+export async function mockGetDevLogin (page: any) {
+ await page.routeFromHAR(
+ `./tests/hars/${SCIPER_ADMIN}/get_dev_login.har`,
+ {
+ url: `/api/get_dev_login/${SCIPER_ADMIN}`,
+ update: UPDATE,
+ });
+ await page.routeFromHAR(
+ `./tests/hars/${SCIPER_USER}/get_dev_login.har`,
+ {
+ url: `/api/get_dev_login/${SCIPER_USER}`,
+ update: UPDATE,
+ });
+ if (process.env.REACT_APP_SCIPER_ADMIN !== undefined && process.env.REACT_APP_SCIPER_ADMIN !== SCIPER_ADMIN) {
+ // dummy route for "Login" button depending on local configuration
+ await page.route(
+ `/api/get_dev_login/${process.env.REACT_APP_SCIPER_ADMIN}`,
+ async route => {await route.fulfill({});}
+ );
+ }
+}
+
+export async function mockLogout (page: any) {
+ await page.route(
+ '/api/logout',
+ async route => {await route.fulfill({});}
+ );
+}
diff --git a/web/frontend/tests/navigation.spec.ts b/web/frontend/tests/navigation.spec.ts
new file mode 100644
index 000000000..06d96a527
--- /dev/null
+++ b/web/frontend/tests/navigation.spec.ts
@@ -0,0 +1,112 @@
+import { default as i18n } from 'i18next';
+import { test, expect } from '@playwright/test';
+import {
+ initI18n,
+ setUp,
+ logIn,
+ logOut,
+ assertOnlyVisibleToAuthenticated,
+ assertOnlyVisibleToAdmin,
+} from './shared';
+import { SCIPER_ADMIN, SCIPER_USER, UPDATE, mockPersonalInfo, mockLogout } from './mocks';
+
+initI18n();
+
+test.beforeEach(async ({ page }) => {
+ if (UPDATE === true) {
+ return;
+ }
+ await mockPersonalInfo(page);
+ await setUp(page, '/about');
+});
+
+// helper tests to update related HAR files
+
+test('Assert anonymous user HAR files are up-to-date', async({ page }) => {
+ // comment the next line to update HAR files
+ test.skip(UPDATE === false, 'Do not update HAR files');
+ await mockPersonalInfo(page);
+ await setUp(page, '/about');
+});
+
+test('Assert non-admin user HAR files are up-to-date', async({ page }) => {
+ // comment the next line to update HAR files
+ test.skip(UPDATE === false, 'Do not update HAR files');
+ await mockPersonalInfo(page, SCIPER_USER);
+ await page.context().request.get(`/api/get_dev_login/${SCIPER_USER}`);
+ await setUp(page, '/about');
+});
+
+test('Assert admin user HAR files are up-to-date', async({ page }) => {
+ // comment the next line to update HAR files
+ test.skip(UPDATE === false, 'Do not update HAR files');
+ await mockPersonalInfo(page, SCIPER_ADMIN);
+ await page.context().request.get(`/api/get_dev_login/${SCIPER_ADMIN}`);
+ await setUp(page, '/about');
+});
+
+// unauthenticated
+
+test('Assert cookie is set', async({ page }) => {
+ const cookies = await page.context().cookies();
+ expect(cookies.find(cookie => cookie.name === 'connect.sid')).toBeTruthy();
+});
+
+test('Assert D-Voting logo is present', async({ page }) => {
+ test.skip(UPDATE === true, 'Do not run regular tests when updating HAR files');
+ const logo = await page.getByAltText(i18n.t('Workflow'));
+ await expect(logo).toBeVisible();
+ await logo.click();
+ await expect(page).toHaveURL('/');
+});
+
+test('Assert link to form table is present', async({ page }) => {
+ test.skip(UPDATE === true, 'Do not run regular tests when updating HAR files');
+ const forms = await page.getByRole('link', { name: i18n.t('navBarStatus') });
+ await expect(forms).toBeVisible();
+ await forms.click();
+ await expect(page).toHaveURL('/form/index');
+});
+
+test('Assert "Login" button calls login API', async({ page }) => {
+ const loginRequest = page.waitForRequest(
+ new RegExp("/api/get_dev_login/[0-9]{6}")
+ );
+ await page.getByRole('button', { name: i18n.t('login') }).click();
+});
+
+// authenticated non-admin
+
+test('Assert "Profile" button is visible upon logging in', async({ page }) => {
+ test.skip(UPDATE === true, 'Do not run regular tests when updating HAR files');
+ await assertOnlyVisibleToAuthenticated(
+ page, page.getByRole('button', { name: i18n.t('Profile') })
+ );
+});
+
+test('Assert "Logout" calls logout API', async({ page, baseURL }) => {
+ await mockLogout(page);
+ await logIn(page, SCIPER_USER);
+ const logoutRequestPromise = page.waitForRequest(
+ request => request.url() === `${baseURL}/api/logout` && request.method() === 'POST'
+ );
+ for (const [role, key] of [['button', 'Profile'], ['menuitem', 'logout'], ['button', 'continue']]) {
+ await page.getByRole(role, { name: i18n.t(key) }).click();
+ }
+});
+
+// admin
+
+test('Assert "Create form" button is (only) visible to admin', async({ page }) => {
+ test.skip(UPDATE === true, 'Do not run regular tests when updating HAR files');
+ await assertOnlyVisibleToAdmin(
+ page, page.getByRole('link', { name: i18n.t('navBarCreateForm')})
+ );
+});
+
+test('Assert "Admin" button is (only) visible to admin', async({ page }) => {
+ test.skip(UPDATE === true, 'Do not run regular tests when updating HAR files');
+ await assertOnlyVisibleToAdmin(
+ page, page.getByRole('link', { name: i18n.t('navBarAdmin') })
+ );
+});
diff --git a/web/frontend/tests/shared.ts b/web/frontend/tests/shared.ts
new file mode 100644
index 000000000..d33f532c1
--- /dev/null
+++ b/web/frontend/tests/shared.ts
@@ -0,0 +1,40 @@
+import { default as i18n } from 'i18next';
+import { test, expect } from '@playwright/test';
+import en from './../src/language/en.json';
+import fr from './../src/language/fr.json';
+import de from './../src/language/de.json';
+import { SCIPER_ADMIN, SCIPER_USER, mockPersonalInfo, mockGetDevLogin, mockLogout } from './mocks';
+
+export function initI18n () {
+ i18n.init({
+ resources: { en, fr, de },
+ fallbackLng: ['en', 'fr', 'de'],
+ });
+}
+
+export async function setUp(page: any, url: string) {
+ await mockGetDevLogin(page);
+ await mockLogout(page);
+ await page.goto(url);
+ await expect(page).toHaveURL(url); // make sure that page is loaded
+}
+
+export async function logIn (page: any, sciper: string) {
+ await mockPersonalInfo(page, sciper);
+ await page.reload();
+ await expect(page).toHaveURL(page.url()); // make sure that page is loaded
+}
+
+export async function assertOnlyVisibleToAuthenticated (page: any, locator: any) {
+ await expect(locator).toBeHidden(); // assert is hidden to unauthenticated user
+ await logIn(page, SCIPER_USER);
+ await expect(locator).toBeVisible(); // assert is visible to authenticated user
+}
+
+export async function assertOnlyVisibleToAdmin (page: any, locator: any) {
+ await expect(locator).toBeHidden(); // assert is hidden to unauthenticated user
+ await logIn(page, SCIPER_USER);
+ await expect(locator).toBeHidden(); // assert is hidden to authenticated non-admin user
+ await logIn(page, SCIPER_ADMIN);
+ await expect(locator).toBeVisible(); // assert is visible to admin user
+}