From 0a3906afdde8cab92a36fdd5f685a815f5ad4736 Mon Sep 17 00:00:00 2001 From: Carine Dengler Date: Wed, 3 Apr 2024 16:11:57 +0200 Subject: [PATCH] feat: add automated tests powered by Playwright --- web/frontend/.eslintrc | 4 +- web/frontend/.gitignore | 5 +- web/frontend/package-lock.json | 122 ++++- web/frontend/package.json | 10 +- web/frontend/playwright.config.ts | 42 ++ web/frontend/tests/ballot.spec.ts | 146 ++++++ web/frontend/tests/footer.spec.ts | 31 ++ web/frontend/tests/formIndex.spec.ts | 159 +++++++ web/frontend/tests/forms.spec.ts | 424 ++++++++++++++++++ .../tests/json/api/personal_info/123456.json | 25 ++ .../tests/json/api/personal_info/654321.json | 7 + .../tests/json/api/personal_info/789012.json | 11 + .../tests/json/api/personal_info/987654.json | 25 ++ .../tests/json/api/proxies/dela-worker-0.json | 4 + .../tests/json/api/proxies/dela-worker-1.json | 4 + .../tests/json/api/proxies/dela-worker-2.json | 4 + .../tests/json/api/proxies/dela-worker-3.json | 4 + .../json/evoting/dkgActors/certified.json | 9 + .../json/evoting/dkgActors/initialized.json | 9 + .../tests/json/evoting/dkgActors/setup.json | 9 + .../json/evoting/dkgActors/uninitialized.json | 10 + .../tests/json/evoting/forms/canceled.json | 124 +++++ .../tests/json/evoting/forms/closed.json | 124 +++++ .../tests/json/evoting/forms/combined.json | 217 +++++++++ .../tests/json/evoting/forms/created.json | 118 +++++ .../tests/json/evoting/forms/decrypted.json | 124 +++++ .../tests/json/evoting/forms/open.json | 123 +++++ .../tests/json/evoting/forms/shuffled.json | 124 +++++ web/frontend/tests/json/formIndex.json | 114 +++++ web/frontend/tests/mocks/api.ts | 136 ++++++ web/frontend/tests/mocks/evoting.ts | 80 ++++ web/frontend/tests/mocks/shared.ts | 1 + web/frontend/tests/navbar.spec.ts | 86 ++++ web/frontend/tests/result.spec.ts | 130 ++++++ web/frontend/tests/shared.ts | 71 +++ web/frontend/tests/tsconfig.json | 29 ++ 36 files changed, 2645 insertions(+), 20 deletions(-) create mode 100644 web/frontend/playwright.config.ts create mode 100644 web/frontend/tests/ballot.spec.ts create mode 100644 web/frontend/tests/footer.spec.ts create mode 100644 web/frontend/tests/formIndex.spec.ts create mode 100644 web/frontend/tests/forms.spec.ts create mode 100644 web/frontend/tests/json/api/personal_info/123456.json create mode 100644 web/frontend/tests/json/api/personal_info/654321.json create mode 100644 web/frontend/tests/json/api/personal_info/789012.json create mode 100644 web/frontend/tests/json/api/personal_info/987654.json create mode 100644 web/frontend/tests/json/api/proxies/dela-worker-0.json create mode 100644 web/frontend/tests/json/api/proxies/dela-worker-1.json create mode 100644 web/frontend/tests/json/api/proxies/dela-worker-2.json create mode 100644 web/frontend/tests/json/api/proxies/dela-worker-3.json create mode 100644 web/frontend/tests/json/evoting/dkgActors/certified.json create mode 100644 web/frontend/tests/json/evoting/dkgActors/initialized.json create mode 100644 web/frontend/tests/json/evoting/dkgActors/setup.json create mode 100644 web/frontend/tests/json/evoting/dkgActors/uninitialized.json create mode 100644 web/frontend/tests/json/evoting/forms/canceled.json create mode 100644 web/frontend/tests/json/evoting/forms/closed.json create mode 100644 web/frontend/tests/json/evoting/forms/combined.json create mode 100644 web/frontend/tests/json/evoting/forms/created.json create mode 100644 web/frontend/tests/json/evoting/forms/decrypted.json create mode 100644 web/frontend/tests/json/evoting/forms/open.json create mode 100644 web/frontend/tests/json/evoting/forms/shuffled.json create mode 100644 web/frontend/tests/json/formIndex.json create mode 100644 web/frontend/tests/mocks/api.ts create mode 100644 web/frontend/tests/mocks/evoting.ts create mode 100644 web/frontend/tests/mocks/shared.ts create mode 100644 web/frontend/tests/navbar.spec.ts create mode 100644 web/frontend/tests/result.spec.ts create mode 100644 web/frontend/tests/shared.ts create mode 100644 web/frontend/tests/tsconfig.json diff --git a/web/frontend/.eslintrc b/web/frontend/.eslintrc index c24bdd758..21c8df0b5 100644 --- a/web/frontend/.eslintrc +++ b/web/frontend/.eslintrc @@ -14,7 +14,7 @@ ], "parser": "@typescript-eslint/parser", "parserOptions": { - "project": "tsconfig.json" + "project": ["tsconfig.json", "tests/tsconfig.json"] }, "plugins": ["react", "@typescript-eslint", "jest", "unused-imports"], "root": true, @@ -32,7 +32,7 @@ "import/no-extraneous-dependencies": ["error", {"devDependencies": [ "**/*.test.ts", "**/*.test.tsx", "**/setupTest.ts", "**/setupProxy.js", - "**/mocks/*" + "**/mocks/*", "tests/*.ts" ]}], // conflicts with the index.ts (eslint prefers default exports which are not compatible with index.ts) 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/package-lock.json b/web/frontend/package-lock.json index 30dcfa2ce..f4dcc5296 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", @@ -40,6 +41,7 @@ "@types/react-dom": "^17.0.11", "@types/react-router": "^5.1.18", "@types/react-router-dom": "^5.3.3", + "dotenv": "^16.3.1", "eslint": "^8.16.0", "eslint-config-airbnb-typescript": "^16.1.0", "eslint-config-prettier": "^8.3.0", @@ -2865,6 +2867,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", @@ -5359,9 +5376,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001354", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz", - "integrity": "sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==", + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==", "funding": [ { "type": "opencollective", @@ -5370,6 +5387,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -6703,11 +6724,15 @@ } }, "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true, "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, "node_modules/dotenv-expand": { @@ -12221,6 +12246,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", @@ -14009,6 +14064,14 @@ } } }, + "node_modules/react-scripts/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -18706,6 +18769,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", @@ -20591,9 +20663,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001354", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001354.tgz", - "integrity": "sha512-mImKeCkyGDAHNywYFA4bqnLAzTUvVkqPvhY4DV47X+Gl2c5Z8c3KNETnXp14GQt11LvxE8AwjzGxJ+rsikiOzg==" + "version": "1.0.30001591", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz", + "integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -21602,9 +21674,10 @@ } }, "dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "dev": true }, "dotenv-expand": { "version": "5.1.0", @@ -25657,6 +25730,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", @@ -26780,6 +26869,13 @@ "webpack-dev-server": "^4.6.0", "webpack-manifest-plugin": "^4.0.2", "workbox-webpack-plugin": "^6.4.1" + }, + "dependencies": { + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + } } }, "read-cache": { diff --git a/web/frontend/package.json b/web/frontend/package.json index b3a63d718..88651460f 100644 --- a/web/frontend/package.json +++ b/web/frontend/package.json @@ -8,8 +8,8 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "eslint": "./node_modules/.bin/eslint src/", - "eslint-fix": "./node_modules/.bin/eslint src/ --fix", + "eslint": "./node_modules/.bin/eslint src/ tests/", + "eslint-fix": "./node_modules/.bin/eslint src/ tests/ --fix", "prettier": "./node_modules/.bin/prettier --write src/", "prettier-check": "./node_modules/.bin/prettier --check src/" }, @@ -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", @@ -46,6 +47,7 @@ "@types/react-dom": "^17.0.11", "@types/react-router": "^5.1.18", "@types/react-router-dom": "^5.3.3", + "dotenv": "^16.3.1", "eslint": "^8.16.0", "eslint-config-airbnb-typescript": "^16.1.0", "eslint-config-prettier": "^8.3.0", diff --git a/web/frontend/playwright.config.ts b/web/frontend/playwright.config.ts new file mode 100644 index 000000000..c91e2b76a --- /dev/null +++ b/web/frontend/playwright.config.ts @@ -0,0 +1,42 @@ +import { defineConfig, devices } from '@playwright/test'; + +/* import environment variables used in development instance */ +require("dotenv").config({ path: "./../../.env" }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: false, + /* 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', + }, + workers: 1, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + ], + +}); diff --git a/web/frontend/tests/ballot.spec.ts b/web/frontend/tests/ballot.spec.ts new file mode 100644 index 000000000..47bc5f84f --- /dev/null +++ b/web/frontend/tests/ballot.spec.ts @@ -0,0 +1,146 @@ +import { expect, test } from '@playwright/test'; +import { default as i18n } from 'i18next'; +import { assertHasFooter, assertHasNavBar, initI18n, logIn, setUp } from './shared'; +import { FORMID } from './mocks/shared'; +import { + SCIPER_ADMIN, + SCIPER_OTHER_USER, + SCIPER_USER, + mockFormsVote, + mockPersonalInfo, +} from './mocks/api'; +import { mockFormsFormID } from './mocks/evoting'; +import Form from './json/evoting/forms/open.json'; + +initI18n(); + +test.beforeEach(async ({ page }) => { + await mockFormsFormID(page, 1); + await logIn(page, SCIPER_ADMIN); + await setUp(page, `/ballot/show/${FORMID}`); +}); + +test('Assert navigation bar is present', async ({ page }) => { + await assertHasNavBar(page); +}); + +test('Assert footer is present', async ({ page }) => { + await assertHasFooter(page); +}); + +test('Assert ballot form is correctly handled for anonymous users, non-voter users and voter users', async ({ + page, +}) => { + const castVoteButton = await page.getByRole('button', { name: i18n.t('castVote') }); + await test.step('Assert anonymous is redirected to login page', async () => { + await mockPersonalInfo(page); + await page.reload({ waitUntil: 'networkidle' }); + await expect(page).toHaveURL('/login'); + }); + await test.step('Assert non-voter gets page that they are not allowed to vote', async () => { + await logIn(page, SCIPER_OTHER_USER); + await page.goto(`/ballot/show/${FORMID}`, { waitUntil: 'networkidle' }); + await expect(page).toHaveURL(`/ballot/show/${FORMID}`); + await expect(castVoteButton).toBeHidden(); + await expect(page.getByText(i18n.t('voteNotVoter'))).toBeVisible(); + await expect(page.getByText(i18n.t('voteNotVoterDescription'))).toBeVisible(); + }); + await test.step('Assert voter gets ballot', async () => { + await logIn(page, SCIPER_USER); + await page.goto(`/ballot/show/${FORMID}`, { waitUntil: 'networkidle' }); + await expect(page).toHaveURL(`/ballot/show/${FORMID}`); + await expect(castVoteButton).toBeVisible(); + await expect(page.getByText(i18n.t('vote'), { exact: true })).toBeVisible(); + await expect(page.getByText(i18n.t('voteExplanation'))).toBeVisible(); + }); +}); + +test('Assert ballot is displayed properly', async ({ page }) => { + const content = await page.getByTestId('content'); + // TODO integrate localisation + i18n.changeLanguage('en'); // force 'en' for this test + await expect(content.locator('xpath=./div/div[3]/h3')).toContainText(Form.Configuration.Title.En); + for (const [index, scaffold] of Form.Configuration.Scaffold.entries()) { + await expect(content.locator(`xpath=./div/div[3]/div/div[${index + 1}]/h3`)).toContainText( + scaffold.Title.En + ); + const select = scaffold.Selects.at(0); + await expect( + content.locator(`xpath=./div/div[3]/div/div[${index + 1}]/div/div/div/div[1]/div[1]/h3`) + ).toContainText(select.Title.En); + await expect( + page.getByText(i18n.t('selectBetween', { minSelect: select.MinN, maxSelect: select.MaxN })) + ).toBeVisible(); + for (const choice of select.Choices.map((x) => JSON.parse(x))) { + await expect(page.getByRole('checkbox', { name: choice.en })).toBeVisible(); + } + } + i18n.changeLanguage(); // unset language for the other tests +}); + +test('Assert minimum/maximum number of choices are handled correctly', async ({ page }) => { + const content = await page.getByTestId('content'); + const castVoteButton = await page.getByRole('button', { name: i18n.t('castVote') }); + for (const [index, scaffold] of Form.Configuration.Scaffold.entries()) { + const select = scaffold.Selects.at(0); + await test.step( + `Assert minimum number of choices (${select.MinN}) are handled correctly`, + async () => { + await castVoteButton.click(); + await expect( + content.locator(`xpath=./div/div[3]/div/div[${index + 1}]`).getByText( + i18n.t('minSelectError', { + min: select.MinN, + singularPlural: i18n.t('singularAnswer'), + }) + ) + ).toBeVisible(); + } + ); + await test.step( + `Assert maximum number of choices (${select.MaxN}) are handled correctly`, + async () => { + for (const choice of select.Choices.map((x) => JSON.parse(x))) { + await page.getByRole('checkbox', { name: choice.en }).setChecked(true); + } + await castVoteButton.click(); + await expect( + content.locator(`xpath=./div/div[3]/div/div[${index + 1}]`).getByText( + i18n.t('maxSelectError', { + max: select.MaxN, + singularPlural: i18n.t('singularAnswer'), + }) + ) + ).toBeVisible(); + } + ); + } +}); + +test('Assert that correct number of choices are accepted', async ({ page, baseURL }) => { + await mockFormsVote(page); + page.waitForRequest(async (request) => { + const body = await request.postDataJSON(); + return ( + request.url() === `${baseURL}/api/evoting/forms/${FORMID}/vote` && + request.method() === 'POST' && + body.UserID === null && + body.Ballot.length === 2 && + body.Ballot.at(0).K.length === 32 && + body.Ballot.at(0).C.length === 32 && + body.Ballot.at(1).K.length === 32 && + body.Ballot.at(1).C.length === 32 + ); + }); + await page + .getByRole('checkbox', { + name: JSON.parse(Form.Configuration.Scaffold.at(0).Selects.at(0).Choices.at(0)).en, + }) + .setChecked(true); + await page + .getByRole('checkbox', { + name: JSON.parse(Form.Configuration.Scaffold.at(1).Selects.at(0).Choices.at(0)).en, + }) + .setChecked(true); + await page.getByRole('button', { name: i18n.t('castVote') }).click(); +}); diff --git a/web/frontend/tests/footer.spec.ts b/web/frontend/tests/footer.spec.ts new file mode 100644 index 000000000..39b8bc6a0 --- /dev/null +++ b/web/frontend/tests/footer.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test'; +import { default as i18n } from 'i18next'; +import { initI18n, setUp } from './shared'; +import { mockPersonalInfo } from './mocks/api'; + +initI18n(); + +test.beforeEach(async ({ page }) => { + await mockPersonalInfo(page); + await setUp(page, '/about'); +}); + +test('Assert copyright notice is visible', async ({ page }) => { + const footerCopyright = await page.getByTestId('footerCopyright'); + await expect(footerCopyright).toBeVisible(); + await expect(footerCopyright).toHaveText( + `© ${new Date().getFullYear()} ${i18n.t('footerCopyright')} https://github.com/c4dt/d-voting` + ); +}); + +test('Assert version information is visible', async ({ page }) => { + const footerVersion = await page.getByTestId('footerVersion'); + await expect(footerVersion).toBeVisible(); + await expect(footerVersion).toHaveText( + [ + `${i18n.t('footerVersion')} ${process.env.REACT_APP_VERSION || i18n.t('footerUnknown')}`, + `${i18n.t('footerBuild')} ${process.env.REACT_APP_BUILD || i18n.t('footerUnknown')}`, + `${i18n.t('footerBuildTime')} ${process.env.REACT_APP_BUILD_TIME || i18n.t('footerUnknown')}`, + ].join(' - ') + ); +}); diff --git a/web/frontend/tests/formIndex.spec.ts b/web/frontend/tests/formIndex.spec.ts new file mode 100644 index 000000000..0b6a9b10a --- /dev/null +++ b/web/frontend/tests/formIndex.spec.ts @@ -0,0 +1,159 @@ +import { Locator, Page, expect, test } from '@playwright/test'; +import { default as i18n } from 'i18next'; +import { assertHasFooter, assertHasNavBar, initI18n, logIn, setUp, translate } from './shared'; +import { SCIPER_ADMIN, SCIPER_USER, mockPersonalInfo } from './mocks/api'; +import { mockForms } from './mocks/evoting'; +import Forms from './json/formIndex.json'; +import User from './json/api/personal_info/789012.json'; +import Admin from './json/api/personal_info/123456.json'; + +initI18n(); + +async function goForward(page: Page) { + await page.getByRole('button', { name: i18n.t('next') }).click(); +} + +test.beforeEach(async ({ page }) => { + // mock empty list per default + await mockForms(page); + await mockPersonalInfo(page); + await setUp(page, '/form/index'); +}); + +// main elements + +test('Assert navigation bar is present', async ({ page }) => { + await assertHasNavBar(page); +}); + +test('Assert footer is present', async ({ page }) => { + await assertHasFooter(page); +}); + +// pagination bar + +test('Assert pagination bar is present', async ({ page }) => { + await expect(page.getByTestId('navPagination')).toBeVisible(); + await expect(page.getByRole('button', { name: i18n.t('previous') })).toBeVisible(); + await expect(page.getByRole('button', { name: i18n.t('next') })).toBeVisible(); +}); + +test('Assert pagination works correctly for empty list', async ({ page }) => { + await expect(page.getByTestId('navPaginationMessage')).toHaveText( + i18n.t('showingNOverMOfXResults', { n: 1, m: 1, x: 0 }) + ); + for (let key of ['next', 'previous']) { + await expect(page.getByRole('button', { name: i18n.t(key) })).toBeDisabled(); + } +}); + +test('Assert pagination works correctly for non-empty list', async ({ page }) => { + // mock non-empty list w/ 11 elements i.e. 2 pages + await mockForms(page, false); + await page.reload(); + const next = await page.getByRole('button', { name: i18n.t('next') }); + const previous = await page.getByRole('button', { name: i18n.t('previous') }); + // 1st page + await expect(page.getByTestId('navPaginationMessage')).toHaveText( + i18n.t('showingNOverMOfXResults', { n: 1, m: 2, x: 11 }) + ); + await expect(previous).toBeDisabled(); + await expect(next).toBeEnabled(); + await next.click(); + // 2nd page + await expect(page.getByTestId('navPaginationMessage')).toHaveText( + i18n.t('showingNOverMOfXResults', { n: 2, m: 2, x: 11 }) + ); + await expect(next).toBeDisabled(); + await expect(previous).toBeEnabled(); + await previous.click(); + // back to 1st page + await expect(page.getByTestId('navPaginationMessage')).toHaveText( + i18n.t('showingNOverMOfXResults', { n: 1, m: 2, x: 11 }) + ); + await expect(previous).toBeDisabled(); + await expect(next).toBeEnabled(); +}); + +test('Assert no forms are displayed for empty list', async ({ page }) => { + // 1 header row + await expect + .poll(async () => { + const rows = await page.getByRole('table').getByRole('row'); + return rows.all(); + }) + .toHaveLength(1); +}); + +async function assertQuickAction(row: Locator, form: any, sciper?: string) { + const user = sciper === SCIPER_USER ? User : (sciper === SCIPER_ADMIN ? Admin : undefined); // eslint-disable-line + const quickAction = row.getByTestId('quickAction'); + switch (form.Status) { + case 1: + // only authenticated user w/ right to vote sees 'vote' button + if ( + user !== undefined && + form.FormID in user.authorization && + // @ts-ignore + user.authorization[form.FormID].includes('vote') + ) { + await expect(quickAction).toHaveText(i18n.t('vote')); + await expect(await quickAction.getByRole('link')).toHaveAttribute( + 'href', + `/ballot/show/${form.FormID}` + ); + await expect(quickAction).toBeVisible(); + } else { + await expect(quickAction).toBeHidden(); + } + break; + case 5: + // any user can see the results of a past election + await expect(quickAction).toHaveText(i18n.t('seeResult')); + await expect(await quickAction.getByRole('link')).toHaveAttribute( + 'href', + `/forms/${form.FormID}/result` + ); + break; + default: + await expect(quickAction).toBeHidden(); + } +} + +test('Assert forms are displayed correctly for unauthenticated user', async ({ page }) => { + await mockForms(page, false); + await page.reload(); + const table = await page.getByRole('table'); + for (let form of Forms.Forms.slice(0, -1)) { + let name = translate(form.Title); + let row = await table.getByRole('row', { name: name }); + await expect(row).toBeVisible(); + // row entry leads to form view + let link = await row.getByRole('link', { name: name }); + await expect(link).toBeVisible(); + await expect(link).toHaveAttribute('href', `/forms/${form.FormID}`); + await assertQuickAction(row, form); + } + await goForward(page); + let row = await table.getByRole('row', { name: translate(Forms.Forms.at(-1)!.Title) }); + await expect(row).toBeVisible(); + await assertQuickAction(row, Forms.Forms.at(-1)!); +}); + +test('Assert quick actions are displayed correctly for authenticated users', async ({ page }) => { + for (let sciper of [SCIPER_USER, SCIPER_ADMIN]) { + await logIn(page, sciper); + await mockForms(page, false); + await page.reload(); + const table = await page.getByRole('table'); + for (let form of Forms.Forms.slice(0, -1)) { + let row = await table.getByRole('row', { name: translate(form.Title) }); + await assertQuickAction(row, form, sciper); + } + await goForward(page); + await assertQuickAction( + await table.getByRole('row', { name: translate(Forms.Forms.at(-1)!.Title) }), + Forms.Forms.at(-1)! + ); + } +}); diff --git a/web/frontend/tests/forms.spec.ts b/web/frontend/tests/forms.spec.ts new file mode 100644 index 000000000..a4a8e5a6b --- /dev/null +++ b/web/frontend/tests/forms.spec.ts @@ -0,0 +1,424 @@ +import { Locator, Page, expect, test } from '@playwright/test'; +import { default as i18n } from 'i18next'; +import { assertHasFooter, assertHasNavBar, initI18n, logIn, setUp } from './shared'; +import { + SCIPER_ADMIN, + SCIPER_OTHER_ADMIN, + SCIPER_OTHER_USER, + SCIPER_USER, + mockDKGActors as mockAPIDKGActors, + mockAddRole, + mockDKGActorsFormID, + mockForms, + mockPersonalInfo, + mockProxies, + mockServicesShuffle, +} from './mocks/api'; +import { mockDKGActors, mockFormsFormID } from './mocks/evoting'; +import { FORMID } from './mocks/shared'; +import Worker0 from './json/api/proxies/dela-worker-0.json'; +import Worker1 from './json/api/proxies/dela-worker-1.json'; +import Worker2 from './json/api/proxies/dela-worker-2.json'; +import Worker3 from './json/api/proxies/dela-worker-3.json'; + +initI18n(); + +const prettyFormStates = [ + 'Initial', + 'Open', + 'Closed', + 'ShuffledBallots', + 'PubSharesSubmitted', + 'ResultAvailable', + 'Canceled', +]; + +// main elements + +async function setUpMocks( + page: Page, + formStatus: number, + dkgActorsStatus: number, + initialized?: boolean +) { + // the nodes must have been initialized if they changed state + initialized = initialized || dkgActorsStatus > 0; + await mockFormsFormID(page, formStatus); + for (const i of [0, 1, 2, 3]) { + await mockProxies(page, i); + } + await mockDKGActors(page, dkgActorsStatus, initialized); + await mockAPIDKGActors(page); + await mockPersonalInfo(page); + await mockDKGActorsFormID(page); + await mockServicesShuffle(page); + await mockForms(page); +} + +test.beforeEach(async ({ page }) => { + // mock empty list per default + setUpMocks(page, 0, 0, false); + await setUp(page, `/forms/${FORMID}`); +}); + +test('Assert navigation bar is present', async ({ page }) => { + await assertHasNavBar(page); +}); + +test('Assert footer is present', async ({ page }) => { + await assertHasFooter(page); +}); + +async function assertIsOnlyVisibleToOwner(page: Page, locator: Locator) { + await test.step('Assert is hidden to unauthenticated user', async () => { + await expect(locator).toBeHidden(); + }); + await test.step('Assert is hidden to authenticated non-admin user', async () => { + await logIn(page, SCIPER_USER); + await expect(locator).toBeHidden(); + }); + await test.step('Assert is hidden to non-owner admin', async () => { + await logIn(page, SCIPER_ADMIN); + await expect(locator).toBeHidden(); + }); + await test.step('Assert is visible to owner admin', async () => { + await logIn(page, SCIPER_OTHER_ADMIN); + await expect(locator).toBeVisible(); + }); +} + +async function assertIsOnlyVisibleInStates( + page: Page, + locator: Locator, + states: Array, + assert: Function, + dkgActorsStatus?: number, + initialized?: boolean +) { + for (const i of states) { + await test.step(`Assert is visible in form state '${prettyFormStates.at(i)}'`, async () => { + await setUpMocks(page, i, dkgActorsStatus === undefined ? 6 : dkgActorsStatus, initialized); + await page.reload({ waitUntil: 'networkidle' }); + await assert(page, locator); + }); + } + for (const i of [0, 1, 2, 3, 4, 5].filter((x) => !states.includes(x))) { + await test.step(`Assert is not visible in form state '${prettyFormStates.at(i)}'`, async () => { + await setUpMocks(page, i, dkgActorsStatus === undefined ? 6 : dkgActorsStatus, initialized); + await page.reload({ waitUntil: 'networkidle' }); + await expect(locator).toBeHidden(); + }); + } +} + +async function assertRouteIsCalled( + page: Page, + url: string, + key: string, + action: string, + formStatus: number, + confirmation: boolean, + dkgActorsStatus?: number, + initialized?: boolean +) { + await setUpMocks( + page, + formStatus, + dkgActorsStatus === undefined ? 6 : dkgActorsStatus, + initialized + ); + await logIn(page, SCIPER_OTHER_ADMIN); + page.waitForRequest(async (request) => { + const body = await request.postDataJSON(); + return request.url() === url && request.method() === 'PUT' && body.Action === action; + }); + await page.getByRole('button', { name: i18n.t(key) }).click(); + if (confirmation) { + await page.getByRole('button', { name: i18n.t('yes') }).click(); + } +} + +test('Assert "Add voters" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByTestId('addVotersButton'), + [0, 1], + assertIsOnlyVisibleToOwner + ); +}); + +test('Assert "Add voters" button allows to add voters', async ({ page, baseURL }) => { + await setUpMocks(page, 0, 6); + await mockAddRole(page); + await logIn(page, SCIPER_OTHER_ADMIN); + // we expect one call per new voter + for (const sciper of [SCIPER_OTHER_ADMIN, SCIPER_ADMIN, SCIPER_USER]) { + page.waitForRequest(async (request) => { + const body = await request.postDataJSON(); + return ( + request.url() === `${baseURL}/api/add_role` && + request.method() === 'POST' && + body.permission === 'vote' && + body.subject === FORMID && + body.userId.toString() === sciper + ); + }); + } + await page.getByTestId('addVotersButton').click(); + // menu should be visible + const textbox = await page.getByRole('textbox', { name: 'SCIPERs' }); + await expect(textbox).toBeVisible(); + // add 3 voters (owner admin, non-owner admin, user) + await textbox.fill(`${SCIPER_OTHER_ADMIN}\n${SCIPER_ADMIN}\n${SCIPER_USER}`); + // click on confirmation + await page.getByTestId('addVotersConfirm').click(); +}); + +test('Assert "Initialize" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('initialize') }), + [0], + assertIsOnlyVisibleToOwner, + 0, + false + ); +}); + +test('Assert "Initialize" button calls route to initialize nodes', async ({ page, baseURL }) => { + await setUpMocks(page, 0, 0, false); + await logIn(page, SCIPER_OTHER_ADMIN); + // we expect one call per worker node + for (const worker of [Worker0, Worker1, Worker2, Worker3]) { + page.waitForRequest(async (request) => { + const body = await request.postDataJSON(); + return ( + request.url() === `${baseURL}/api/evoting/services/dkg/actors` && + request.method() === 'POST' && + body.FormID === FORMID && + body.Proxy === worker.Proxy + ); + }); + } + await page.getByRole('button', { name: i18n.t('initialize') }).click(); +}); + +test('Assert "Setup" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('setup') }), + [0], + assertIsOnlyVisibleToOwner, + 0, + true + ); +}); + +test('Assert "Setup" button calls route to setup node', async ({ page, baseURL }) => { + await setUpMocks(page, 0, 0, true); + await logIn(page, SCIPER_OTHER_ADMIN); + // we expect one call with the chosen worker node + page.waitForRequest(async (request) => { + const body = await request.postDataJSON(); + return ( + request.url() === `${baseURL}/api/evoting/services/dkg/actors/${FORMID}` && + request.method() === 'PUT' && + body.Action === 'setup' && + body.Proxy === Worker1.Proxy + ); + }); + // open node selection window + await page.getByRole('button', { name: i18n.t('setup') }).click(); + await expect(page.getByTestId('nodeSetup')).toBeVisible(); + // choose second worker node + await page.getByLabel(Worker1.NodeAddr).check(); + // confirm + await page.getByRole('button', { name: i18n.t('setupNode') }).click(); +}); + +test('Assert "Open" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('open') }), + [0], + assertIsOnlyVisibleToOwner + ); +}); + +test('Assert "Open" button calls route to open form', async ({ page, baseURL }) => { + await assertRouteIsCalled( + page, + `${baseURL}/api/evoting/forms/${FORMID}`, + 'open', + 'open', + 0, + false, + 6 + ); +}); + +test('Assert "Cancel" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('cancel') }), + [1], + assertIsOnlyVisibleToOwner + ); +}); + +test('Assert "Cancel" button calls route to cancel form', async ({ page, baseURL }) => { + await assertRouteIsCalled( + page, + `${baseURL}/api/evoting/forms/${FORMID}`, + 'cancel', + 'cancel', + 1, + true + ); +}); + +test('Assert "Close" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('close') }), + [1], + assertIsOnlyVisibleToOwner + ); +}); + +test('Assert "Close" button calls route to close form', async ({ page, baseURL }) => { + await assertRouteIsCalled( + page, + `${baseURL}/api/evoting/forms/${FORMID}`, + 'close', + 'close', + 1, + true + ); +}); + +test('Assert "Shuffle" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('shuffle') }), + [2], + assertIsOnlyVisibleToOwner + ); +}); + +test('Assert "Shuffle" button calls route to shuffle form', async ({ page, baseURL }) => { + await assertRouteIsCalled( + page, + `${baseURL}/api/evoting/services/shuffle/${FORMID}`, + 'shuffle', + 'shuffle', + 2, + false + ); +}); + +test('Assert "Decrypt" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('decrypt') }), + [3], + assertIsOnlyVisibleToOwner + ); +}); + +test('Assert "Decrypt" button calls route to decrypt form', async ({ page, baseURL }) => { + await assertRouteIsCalled( + page, + `${baseURL}/api/evoting/services/dkg/actors/${FORMID}`, + 'decrypt', + 'computePubshares', + 3, + false + ); +}); + +test('Assert "Combine" button is only visible to owner', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('combine') }), + [4], + assertIsOnlyVisibleToOwner + ); +}); + +test('Assert "Combine" button calls route to combine form', async ({ page, baseURL }) => { + await assertRouteIsCalled( + page, + `${baseURL}/api/evoting/forms/${FORMID}`, + 'combine', + 'combineShares', + 4, + false + ); +}); + +test('Assert "Delete" button is only visible to owner', async ({ page }) => { + test.setTimeout(60000); // Firefox is exceeding the default timeout on this test + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('delete') }), + [0, 1, 2, 3, 4, 6], + assertIsOnlyVisibleToOwner + ); +}); + +test('Assert "Delete" button calls route to delete form', async ({ page, baseURL }) => { + for (const i of [0, 1, 2, 3, 4, 6]) { + await setUpMocks(page, i, 6); + await logIn(page, SCIPER_OTHER_ADMIN); + page.waitForRequest(async (request) => { + return ( + request.url() === `${baseURL}/api/evoting/forms/${FORMID}` && request.method() === 'DELETE' + ); + }); + await page.getByRole('button', { name: i18n.t('delete') }).click(); + await page.getByRole('button', { name: i18n.t('yes') }).click(); + } +}); + +test('Assert "Vote" button is visible to admin/non-admin voter user', async ({ page }) => { + await assertIsOnlyVisibleInStates( + page, + page.getByRole('button', { name: i18n.t('vote'), exact: true }), // by default name is not matched exactly which returns both the "Vote" and the "Add voters" button + [1], + // eslint-disable-next-line @typescript-eslint/no-shadow + async function (page: Page, locator: Locator) { + await test.step('Assert is hidden to unauthenticated user', async () => { + await expect(locator).toBeHidden(); + }); + await test.step('Assert is hidden to authenticated non-voter user', async () => { + await logIn(page, SCIPER_OTHER_USER); + await expect(locator).toBeHidden(); + }); + await test.step('Assert is visible to authenticated voter user', async () => { + await logIn(page, SCIPER_USER); + await expect(locator).toBeVisible(); + }); + await test.step('Assert is hidden to non-voter admin', async () => { + await logIn(page, SCIPER_OTHER_ADMIN); + await expect(locator).toBeHidden(); + }); + await test.step('Assert is visible to voter admin', async () => { + await logIn(page, SCIPER_ADMIN); + await expect(locator).toBeVisible(); + }); + } + ); +}); + +test('Assert "Vote" button gets voting form', async ({ page }) => { + await setUpMocks(page, 1, 6); + await logIn(page, SCIPER_USER); + page.waitForRequest(`${process.env.DELA_PROXY_URL}/evoting/forms/${FORMID}`); + await page.getByRole('button', { name: i18n.t('vote') }).click(); + // go back to form management page + await setUp(page, `/forms/${FORMID}`); + await logIn(page, SCIPER_ADMIN); + page.waitForRequest(`${process.env.DELA_PROXY_URL}/evoting/forms/${FORMID}`); + await page.getByRole('button', { name: i18n.t('vote') }).click(); +}); diff --git a/web/frontend/tests/json/api/personal_info/123456.json b/web/frontend/tests/json/api/personal_info/123456.json new file mode 100644 index 000000000..af6de5623 --- /dev/null +++ b/web/frontend/tests/json/api/personal_info/123456.json @@ -0,0 +1,25 @@ +{ + "sciper": 123456, + "lastName": "123456", + "firstName": "sciper-#", + "isLoggedIn": true, + "authorization": { + "roles": [ + "add", + "list", + "remove" + ], + "proxies": [ + "post", + "put", + "delete" + ], + "election": [ + "create" + ], + "fdf8bfb702e8883e330a2b303b24212b6fc16df5a53a097998b77ba74632dc72": ["own"], + "ed26713245824d44ee46ec90507ef521962f2313706934cdfe76ff1823738109": ["vote"], + "9f50ad723805a6419ba1a9f83dd0aa582f3e13b94f14727cd0c8c01744e0dba2": ["vote", "own"], + "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4": ["vote"] + } +} diff --git a/web/frontend/tests/json/api/personal_info/654321.json b/web/frontend/tests/json/api/personal_info/654321.json new file mode 100644 index 000000000..5650be68f --- /dev/null +++ b/web/frontend/tests/json/api/personal_info/654321.json @@ -0,0 +1,7 @@ +{ + "sciper": 654321, + "lastName": "654321", + "firstName": "sciper-#", + "isLoggedIn": true, + "authorization": {} +} diff --git a/web/frontend/tests/json/api/personal_info/789012.json b/web/frontend/tests/json/api/personal_info/789012.json new file mode 100644 index 000000000..065f71f27 --- /dev/null +++ b/web/frontend/tests/json/api/personal_info/789012.json @@ -0,0 +1,11 @@ +{ + "sciper": 789012, + "lastName": "789012", + "firstName": "sciper-#", + "isLoggedIn": true, + "authorization": { + "fdf8bfb702e8883e330a2b303b24212b6fc16df5a53a097998b77ba74632dc72": ["vote"], + "ed26713245824d44ee46ec90507ef521962f2313706934cdfe76ff1823738109": ["vote"], + "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4": ["vote"] + } +} diff --git a/web/frontend/tests/json/api/personal_info/987654.json b/web/frontend/tests/json/api/personal_info/987654.json new file mode 100644 index 000000000..e24cb3e6a --- /dev/null +++ b/web/frontend/tests/json/api/personal_info/987654.json @@ -0,0 +1,25 @@ +{ + "sciper": 987654, + "lastName": "987654", + "firstName": "sciper-#", + "isLoggedIn": true, + "authorization": { + "roles": [ + "add", + "list", + "remove" + ], + "proxies": [ + "post", + "put", + "delete" + ], + "election": [ + "create" + ], + "fdf8bfb702e8883e330a2b303b24212b6fc16df5a53a097998b77ba74632dc72": ["own"], + "ed26713245824d44ee46ec90507ef521962f2313706934cdfe76ff1823738109": ["vote", "own"], + "9f50ad723805a6419ba1a9f83dd0aa582f3e13b94f14727cd0c8c01744e0dba2": ["vote"], + "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4": ["own"] + } +} diff --git a/web/frontend/tests/json/api/proxies/dela-worker-0.json b/web/frontend/tests/json/api/proxies/dela-worker-0.json new file mode 100644 index 000000000..443a40641 --- /dev/null +++ b/web/frontend/tests/json/api/proxies/dela-worker-0.json @@ -0,0 +1,4 @@ +{ + "NodeAddr": "grpc://dela-worker-0:2000", + "Proxy": "http://172.19.44.254:8080" +} diff --git a/web/frontend/tests/json/api/proxies/dela-worker-1.json b/web/frontend/tests/json/api/proxies/dela-worker-1.json new file mode 100644 index 000000000..00f2076a6 --- /dev/null +++ b/web/frontend/tests/json/api/proxies/dela-worker-1.json @@ -0,0 +1,4 @@ +{ + "NodeAddr": "grpc://dela-worker-1:2000", + "Proxy": "http://172.19.44.253:8080" +} diff --git a/web/frontend/tests/json/api/proxies/dela-worker-2.json b/web/frontend/tests/json/api/proxies/dela-worker-2.json new file mode 100644 index 000000000..96773ff91 --- /dev/null +++ b/web/frontend/tests/json/api/proxies/dela-worker-2.json @@ -0,0 +1,4 @@ +{ + "NodeAddr": "grpc://dela-worker-2:2000", + "Proxy": "http://172.19.44.252:8080" +} diff --git a/web/frontend/tests/json/api/proxies/dela-worker-3.json b/web/frontend/tests/json/api/proxies/dela-worker-3.json new file mode 100644 index 000000000..827759dde --- /dev/null +++ b/web/frontend/tests/json/api/proxies/dela-worker-3.json @@ -0,0 +1,4 @@ +{ + "NodeAddr": "grpc://dela-worker-3:2000", + "Proxy": "http://172.19.44.251:8080" +} diff --git a/web/frontend/tests/json/evoting/dkgActors/certified.json b/web/frontend/tests/json/evoting/dkgActors/certified.json new file mode 100644 index 000000000..771f8d130 --- /dev/null +++ b/web/frontend/tests/json/evoting/dkgActors/certified.json @@ -0,0 +1,9 @@ +{ + "Status": 6, + "Error": { + "Title": "", + "Code": 0, + "Message": "", + "Args": null + } +} diff --git a/web/frontend/tests/json/evoting/dkgActors/initialized.json b/web/frontend/tests/json/evoting/dkgActors/initialized.json new file mode 100644 index 000000000..57116eb93 --- /dev/null +++ b/web/frontend/tests/json/evoting/dkgActors/initialized.json @@ -0,0 +1,9 @@ +{ + "Status": 0, + "Error": { + "Title": "", + "Code": 0, + "Message": "", + "Args": null + } +} diff --git a/web/frontend/tests/json/evoting/dkgActors/setup.json b/web/frontend/tests/json/evoting/dkgActors/setup.json new file mode 100644 index 000000000..7dc8c8a04 --- /dev/null +++ b/web/frontend/tests/json/evoting/dkgActors/setup.json @@ -0,0 +1,9 @@ +{ + "Status": 1, + "Error": { + "Title": "", + "Code": 0, + "Message": "", + "Args": null + } +} diff --git a/web/frontend/tests/json/evoting/dkgActors/uninitialized.json b/web/frontend/tests/json/evoting/dkgActors/uninitialized.json new file mode 100644 index 000000000..355d98ecf --- /dev/null +++ b/web/frontend/tests/json/evoting/dkgActors/uninitialized.json @@ -0,0 +1,10 @@ +{ + "Title": "not found", + "Code": 404, + "Message": "A problem occurred on the proxy", + "Args": { + "error": "actor not found", + "method": "GET", + "url": "/evoting/services/dkg/actors/b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4" + } +} diff --git a/web/frontend/tests/json/evoting/forms/canceled.json b/web/frontend/tests/json/evoting/forms/canceled.json new file mode 100644 index 000000000..fbd7b5f89 --- /dev/null +++ b/web/frontend/tests/json/evoting/forms/canceled.json @@ -0,0 +1,124 @@ +{ + "FormID": "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4", + "Configuration": { + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Scaffold": [ + { + "ID": "yOakwFnR", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "CLgNiLbC" + ], + "Subjects": [], + "Selects": [ + { + "ID": "CLgNiLbC", + "Title": { + "En": "RGB", + "Fr": "RGB", + "De": "RGB" + }, + "MaxN": 2, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Red\",\"fr\":\"Rouge\",\"de\":\"Rot\"}", + "URL": "http://red.example.com" + }, + { + "Choice": "{\"en\":\"Green\",\"fr\":\"Vert\",\"de\":\"Grün\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Blue\",\"fr\":\"Bleu\",\"de\":\"Blau\"}", + "URL": "http://blue.example.com" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + }, + { + "ID": "1NqhDffw", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "riJFjw0q" + ], + "Subjects": [], + "Selects": [ + { + "ID": "riJFjw0q", + "Title": { + "En": "CMYK", + "Fr": "CMJN", + "De": "CMYK" + }, + "MaxN": 3, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", + "URL": "http://cyan.example.com" + }, + { + "Choice": "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", + "URL": "http://magenta.example.com" + }, + { + "Choice": "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}", + "URL": "" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + } + ] + }, + "Status": 2, + "Pubkey": "612fdc867be1a5faccf16e5aed946880005840ee04aaa382fe181a2b46dc9a24", + "Result": [], + "Roster": [ + "grpc://dela-worker-0:2000", + "grpc://dela-worker-1:2000", + "grpc://dela-worker-2:2000", + "grpc://dela-worker-3:2000" + ], + "ChunksPerBallot": 2, + "BallotSize": 48, + "Voters": [ + "brcLwsgGcU", + "JThb56JvGF", + "zXcZU5QNwn", + "bWxTfeq4t5" + ] +} + diff --git a/web/frontend/tests/json/evoting/forms/closed.json b/web/frontend/tests/json/evoting/forms/closed.json new file mode 100644 index 000000000..fbd7b5f89 --- /dev/null +++ b/web/frontend/tests/json/evoting/forms/closed.json @@ -0,0 +1,124 @@ +{ + "FormID": "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4", + "Configuration": { + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Scaffold": [ + { + "ID": "yOakwFnR", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "CLgNiLbC" + ], + "Subjects": [], + "Selects": [ + { + "ID": "CLgNiLbC", + "Title": { + "En": "RGB", + "Fr": "RGB", + "De": "RGB" + }, + "MaxN": 2, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Red\",\"fr\":\"Rouge\",\"de\":\"Rot\"}", + "URL": "http://red.example.com" + }, + { + "Choice": "{\"en\":\"Green\",\"fr\":\"Vert\",\"de\":\"Grün\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Blue\",\"fr\":\"Bleu\",\"de\":\"Blau\"}", + "URL": "http://blue.example.com" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + }, + { + "ID": "1NqhDffw", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "riJFjw0q" + ], + "Subjects": [], + "Selects": [ + { + "ID": "riJFjw0q", + "Title": { + "En": "CMYK", + "Fr": "CMJN", + "De": "CMYK" + }, + "MaxN": 3, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", + "URL": "http://cyan.example.com" + }, + { + "Choice": "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", + "URL": "http://magenta.example.com" + }, + { + "Choice": "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}", + "URL": "" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + } + ] + }, + "Status": 2, + "Pubkey": "612fdc867be1a5faccf16e5aed946880005840ee04aaa382fe181a2b46dc9a24", + "Result": [], + "Roster": [ + "grpc://dela-worker-0:2000", + "grpc://dela-worker-1:2000", + "grpc://dela-worker-2:2000", + "grpc://dela-worker-3:2000" + ], + "ChunksPerBallot": 2, + "BallotSize": 48, + "Voters": [ + "brcLwsgGcU", + "JThb56JvGF", + "zXcZU5QNwn", + "bWxTfeq4t5" + ] +} + diff --git a/web/frontend/tests/json/evoting/forms/combined.json b/web/frontend/tests/json/evoting/forms/combined.json new file mode 100644 index 000000000..fd418159d --- /dev/null +++ b/web/frontend/tests/json/evoting/forms/combined.json @@ -0,0 +1,217 @@ +{ + "FormID": "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4", + "Configuration": { + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Scaffold": [ + { + "ID": "yOakwFnR", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "CLgNiLbC" + ], + "Subjects": [], + "Selects": [ + { + "ID": "CLgNiLbC", + "Title": { + "En": "RGB", + "Fr": "RGB", + "De": "RGB" + }, + "MaxN": 2, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Red\",\"fr\":\"Rouge\",\"de\":\"Rot\"}", + "URL": "http://red.example.com" + }, + { + "Choice": "{\"en\":\"Green\",\"fr\":\"Vert\",\"de\":\"Grün\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Blue\",\"fr\":\"Bleu\",\"de\":\"Blau\"}", + "URL": "http://blue.example.com" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + }, + { + "ID": "1NqhDffw", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "riJFjw0q" + ], + "Subjects": [], + "Selects": [ + { + "ID": "riJFjw0q", + "Title": { + "En": "CMYK", + "Fr": "CMJN", + "De": "CMYK" + }, + "MaxN": 3, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", + "URL": "http://cyan.example.com" + }, + { + "Choice": "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", + "URL": "http://magenta.example.com" + }, + { + "Choice": "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}", + "URL": "" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + } + ] + }, + "Status": 5, + "Pubkey": "612fdc867be1a5faccf16e5aed946880005840ee04aaa382fe181a2b46dc9a24", + "Result": [ + { + "SelectResultIDs": [ + "CLgNiLbC", + "riJFjw0q" + ], + "SelectResult": [ + [ + true, + false, + false + ], + [ + true, + true, + false, + false + ] + ], + "RankResultIDs": [], + "RankResult": [], + "TextResultIDs": [], + "TextResult": [] + }, + { + "SelectResultIDs": [ + "CLgNiLbC", + "riJFjw0q" + ], + "SelectResult": [ + [ + false, + true, + true + ], + [ + true, + false, + false, + true + ] + ], + "RankResultIDs": [], + "RankResult": [], + "TextResultIDs": [], + "TextResult": [] + }, + { + "SelectResultIDs": [ + "CLgNiLbC", + "riJFjw0q" + ], + "SelectResult": [ + [ + false, + true, + true + ], + [ + false, + true, + false, + false + ] + ], + "RankResultIDs": [], + "RankResult": [], + "TextResultIDs": [], + "TextResult": [] + }, + { + "SelectResultIDs": [ + "CLgNiLbC", + "riJFjw0q" + ], + "SelectResult": [ + [ + false, + false, + true + ], + [ + false, + false, + true, + false + ] + ], + "RankResultIDs": [], + "RankResult": [], + "TextResultIDs": [], + "TextResult": [] + } + ], + "Roster": [ + "grpc://dela-worker-0:2000", + "grpc://dela-worker-1:2000", + "grpc://dela-worker-2:2000", + "grpc://dela-worker-3:2000" + ], + "ChunksPerBallot": 2, + "BallotSize": 48, + "Voters": [ + "brcLwsgGcU", + "JThb56JvGF", + "zXcZU5QNwn", + "bWxTfeq4t5" + ] +} + diff --git a/web/frontend/tests/json/evoting/forms/created.json b/web/frontend/tests/json/evoting/forms/created.json new file mode 100644 index 000000000..0cc4803eb --- /dev/null +++ b/web/frontend/tests/json/evoting/forms/created.json @@ -0,0 +1,118 @@ +{ + "FormID": "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4", + "Configuration": { + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Scaffold": [ + { + "ID": "yOakwFnR", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "CLgNiLbC" + ], + "Subjects": [], + "Selects": [ + { + "ID": "CLgNiLbC", + "Title": { + "En": "RGB", + "Fr": "RGB", + "De": "RGB" + }, + "MaxN": 2, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Red\",\"fr\":\"Rouge\",\"de\":\"Rot\"}", + "URL": "http://red.example.com" + }, + { + "Choice": "{\"en\":\"Green\",\"fr\":\"Vert\",\"de\":\"Grün\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Blue\",\"fr\":\"Bleu\",\"de\":\"Blau\"}", + "URL": "http://blue.example.com" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + }, + { + "ID": "1NqhDffw", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "riJFjw0q" + ], + "Subjects": [], + "Selects": [ + { + "ID": "riJFjw0q", + "Title": { + "En": "CMYK", + "Fr": "CMJN", + "De": "CMYK" + }, + "MaxN": 3, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", + "URL": "http://cyan.example.com" + }, + { + "Choice": "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", + "URL": "http://magenta.example.com" + }, + { + "Choice": "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}", + "URL": "" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + } + ] + }, + "Status": 0, + "Pubkey": "", + "Result": [], + "Roster": [ + "grpc://dela-worker-0:2000", + "grpc://dela-worker-1:2000", + "grpc://dela-worker-2:2000", + "grpc://dela-worker-3:2000" + ], + "ChunksPerBallot": 2, + "BallotSize": 48, + "Voters": [] +} diff --git a/web/frontend/tests/json/evoting/forms/decrypted.json b/web/frontend/tests/json/evoting/forms/decrypted.json new file mode 100644 index 000000000..2a0764847 --- /dev/null +++ b/web/frontend/tests/json/evoting/forms/decrypted.json @@ -0,0 +1,124 @@ +{ + "FormID": "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4", + "Configuration": { + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Scaffold": [ + { + "ID": "yOakwFnR", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "CLgNiLbC" + ], + "Subjects": [], + "Selects": [ + { + "ID": "CLgNiLbC", + "Title": { + "En": "RGB", + "Fr": "RGB", + "De": "RGB" + }, + "MaxN": 2, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Red\",\"fr\":\"Rouge\",\"de\":\"Rot\"}", + "URL": "http://red.example.com" + }, + { + "Choice": "{\"en\":\"Green\",\"fr\":\"Vert\",\"de\":\"Grün\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Blue\",\"fr\":\"Bleu\",\"de\":\"Blau\"}", + "URL": "http://blue.example.com" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + }, + { + "ID": "1NqhDffw", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "riJFjw0q" + ], + "Subjects": [], + "Selects": [ + { + "ID": "riJFjw0q", + "Title": { + "En": "CMYK", + "Fr": "CMJN", + "De": "CMYK" + }, + "MaxN": 3, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", + "URL": "http://cyan.example.com" + }, + { + "Choice": "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", + "URL": "http://magenta.example.com" + }, + { + "Choice": "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}", + "URL": "" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + } + ] + }, + "Status": 4, + "Pubkey": "612fdc867be1a5faccf16e5aed946880005840ee04aaa382fe181a2b46dc9a24", + "Result": [], + "Roster": [ + "grpc://dela-worker-0:2000", + "grpc://dela-worker-1:2000", + "grpc://dela-worker-2:2000", + "grpc://dela-worker-3:2000" + ], + "ChunksPerBallot": 2, + "BallotSize": 48, + "Voters": [ + "brcLwsgGcU", + "JThb56JvGF", + "zXcZU5QNwn", + "bWxTfeq4t5" + ] +} + diff --git a/web/frontend/tests/json/evoting/forms/open.json b/web/frontend/tests/json/evoting/forms/open.json new file mode 100644 index 000000000..c89aee0cb --- /dev/null +++ b/web/frontend/tests/json/evoting/forms/open.json @@ -0,0 +1,123 @@ +{ + "FormID": "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4", + "Configuration": { + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Scaffold": [ + { + "ID": "yOakwFnR", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "CLgNiLbC" + ], + "Subjects": [], + "Selects": [ + { + "ID": "CLgNiLbC", + "Title": { + "En": "RGB", + "Fr": "RGB", + "De": "RGB" + }, + "MaxN": 2, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Red\",\"fr\":\"Rouge\",\"de\":\"Rot\"}", + "URL": "http://red.example.com" + }, + { + "Choice": "{\"en\":\"Green\",\"fr\":\"Vert\",\"de\":\"Grün\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Blue\",\"fr\":\"Bleu\",\"de\":\"Blau\"}", + "URL": "http://blue.example.com" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + }, + { + "ID": "1NqhDffw", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "riJFjw0q" + ], + "Subjects": [], + "Selects": [ + { + "ID": "riJFjw0q", + "Title": { + "En": "CMYK", + "Fr": "CMJN", + "De": "CMYK" + }, + "MaxN": 3, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", + "URL": "http://cyan.example.com" + }, + { + "Choice": "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", + "URL": "http://magenta.example.com" + }, + { + "Choice": "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}", + "URL": "" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + } + ] + }, + "Status": 1, + "Pubkey": "612fdc867be1a5faccf16e5aed946880005840ee04aaa382fe181a2b46dc9a24", + "Result": [], + "Roster": [ + "grpc://dela-worker-0:2000", + "grpc://dela-worker-1:2000", + "grpc://dela-worker-2:2000", + "grpc://dela-worker-3:2000" + ], + "ChunksPerBallot": 2, + "BallotSize": 48, + "Voters": [ + "brcLwsgGcU", + "JThb56JvGF", + "zXcZU5QNwn", + "bWxTfeq4t5" + ] +} diff --git a/web/frontend/tests/json/evoting/forms/shuffled.json b/web/frontend/tests/json/evoting/forms/shuffled.json new file mode 100644 index 000000000..e9e36ac3e --- /dev/null +++ b/web/frontend/tests/json/evoting/forms/shuffled.json @@ -0,0 +1,124 @@ +{ + "FormID": "b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4", + "Configuration": { + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Scaffold": [ + { + "ID": "yOakwFnR", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "CLgNiLbC" + ], + "Subjects": [], + "Selects": [ + { + "ID": "CLgNiLbC", + "Title": { + "En": "RGB", + "Fr": "RGB", + "De": "RGB" + }, + "MaxN": 2, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Red\",\"fr\":\"Rouge\",\"de\":\"Rot\"}", + "URL": "http://red.example.com" + }, + { + "Choice": "{\"en\":\"Green\",\"fr\":\"Vert\",\"de\":\"Grün\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Blue\",\"fr\":\"Bleu\",\"de\":\"Blau\"}", + "URL": "http://blue.example.com" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + }, + { + "ID": "1NqhDffw", + "Title": { + "En": "Colours", + "Fr": "Couleurs", + "De": "Farben" + }, + "Order": [ + "riJFjw0q" + ], + "Subjects": [], + "Selects": [ + { + "ID": "riJFjw0q", + "Title": { + "En": "CMYK", + "Fr": "CMJN", + "De": "CMYK" + }, + "MaxN": 3, + "MinN": 1, + "Choices": [ + { + "Choice": "{\"en\":\"Cyan\",\"fr\":\"Cyan\",\"de\":\"Cyan\"}", + "URL": "http://cyan.example.com" + }, + { + "Choice": "{\"en\":\"Magenta\",\"fr\":\"Magenta\",\"de\":\"Magenta\"}", + "URL": "http://magenta.example.com" + }, + { + "Choice": "{\"en\":\"Yellow\",\"fr\":\"Jaune\",\"de\":\"Gelb\"}", + "URL": "" + }, + { + "Choice": "{\"en\":\"Key\",\"fr\":\"Noir\",\"de\":\"Schwarz\"}", + "URL": "" + } + ], + "Hint": { + "En": "", + "Fr": "", + "De": "" + } + } + ], + "Ranks": [], + "Texts": [] + } + ] + }, + "Status": 3, + "Pubkey": "612fdc867be1a5faccf16e5aed946880005840ee04aaa382fe181a2b46dc9a24", + "Result": [], + "Roster": [ + "grpc://dela-worker-0:2000", + "grpc://dela-worker-1:2000", + "grpc://dela-worker-2:2000", + "grpc://dela-worker-3:2000" + ], + "ChunksPerBallot": 2, + "BallotSize": 48, + "Voters": [ + "brcLwsgGcU", + "JThb56JvGF", + "zXcZU5QNwn", + "bWxTfeq4t5" + ] +} + diff --git a/web/frontend/tests/json/formIndex.json b/web/frontend/tests/json/formIndex.json new file mode 100644 index 000000000..1ddd282be --- /dev/null +++ b/web/frontend/tests/json/formIndex.json @@ -0,0 +1,114 @@ +{ + "Forms": [ + { + "FormID": "f1700a27cef992db7eac71f006a1566369d21e4b76933f43e84a2ae23195e678", + "Title": { + "En": "Colours", + "Fr": "", + "De": "" + }, + "Status": 5, + "Pubkey": "d27836cec530a5e4d255ab704547438b8eede9af7ba78833a7da907064613a71" + }, + { + "FormID": "6783449fb12c481d8e06a27cfdfb7971ad12dcff2083bfe90be35f44fb572d67", + "Title": { + "En": "Foo", + "Fr": "Toto", + "De": "" + }, + "Status": 3, + "Pubkey": "ff83fcc6018685fc3b90f5029eec0f948ff57e220b87c538bdb1a5e17f5c549d" + }, + { + "FormID": "ed26713245824d44ee46ec90507ef521962f2313706934cdfe76ff1823738109", + "Title": { + "En": "Christmas Tree", + "Fr": "Sapin de Noël", + "De": "Weihnachtsbaum" + }, + "Status": 2, + "Pubkey": "3964e8383919eafdeb998d6a694d9a8a74a5438d9d00868fdabcb07fa1a1be28" + }, + { + "FormID": "fdf8bfb702e8883e330a2b303b24212b6fc16df5a53a097998b77ba74632dc72", + "Title": { + "En": "Line 6", + "Fr": "", + "De": "" + }, + "Status": 1, + "Pubkey": "725502be5772ec458f061abeaeb50f45c0f5c07abd530bfc95334d80f3184fbc" + }, + { + "FormID": "4440182e69ef1fbbcdd5cd870e46a59a09fd32a89b8353bab677fe84a5c2f073", + "Title": { + "En": "RER A", + "Fr": "", + "De": "" + }, + "Status": 0, + "Pubkey": "" + }, + { + "FormID": "9f50ad723805a6419ba1a9f83dd0aa582f3e13b94f14727cd0c8c01744e0dba2", + "Title": { + "En": "Languages", + "Fr": "", + "De": "" + }, + "Status": 1, + "Pubkey": "348f56a444e1b75214a9c675587222099007ce739a04651667c054d9be626e07" + }, + { + "FormID": "1269a8507dc316a9ec983ede527705078bfef2b151a49f7ffae6e903ef1bb38f", + "Title": { + "En": "Seasons", + "Fr": "", + "De": "" + }, + "Status": 0, + "Pubkey": "" + }, + { + "FormID": "576be424ee2f37699e74f3752b8912c8cdbfa735a939e1c238863d5d2012bb26", + "Title": { + "En": "Pets", + "Fr": "", + "De": "" + }, + "Status": 0, + "Pubkey": "" + }, + { + "FormID": "06861f854608924f42c99f2c78b22263ea79a9b27d6c616de8f73a8cb7d09152", + "Title": { + "En": "Weather", + "Fr": "", + "De": "" + }, + "Status": 0, + "Pubkey": "" + }, + { + "FormID": "f17100c712db07ec81923c33394ff2d5e56146135ce908754ce610b898d9ba1a", + "Title": { + "En": "Currency", + "Fr": "", + "De": "" + }, + "Status": 0, + "Pubkey": "" + }, + { + "FormID": "a3776492297f71c4c0c75f0fd21d3d25a90ea52a60a660f4e2cb5cc3026bd396", + "Title": { + "En": "Fruits", + "Fr": "", + "De": "" + }, + "Status": 0, + "Pubkey": "" + } + ] +} diff --git a/web/frontend/tests/mocks/api.ts b/web/frontend/tests/mocks/api.ts new file mode 100644 index 000000000..70aae8ef0 --- /dev/null +++ b/web/frontend/tests/mocks/api.ts @@ -0,0 +1,136 @@ +import { FORMID } from './shared'; +export const SCIPER_ADMIN = '123456'; +export const SCIPER_OTHER_ADMIN = '987654'; +export const SCIPER_USER = '789012'; +export const SCIPER_OTHER_USER = '654321'; + +// /api/evoting + +export async function mockDKGActors(page: page) { + await page.route('/api/evoting/services/dkg/actors', async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ status: 200 }); + } + }); +} + +export async function mockDKGActorsFormID(page: page) { + await page.route(`/api/evoting/services/dkg/actors/${FORMID}`, async (route) => { + if (route.request().method() === 'PUT') { + await route.fulfill({ status: 200 }); + } + }); +} + +export async function mockServicesShuffle(page: page) { + await page.route('/api/evoting/services/shuffle/', async (route) => { + if (route.request().method() === 'PUT') { + await route.fulfill({ status: 200 }); + } + }); +} + +export async function mockForms(page: page) { + await page.route(`/api/evoting/forms/${FORMID}`, async (route) => { + if (route.request().method() === 'PUT') { + await route.fulfill({ status: 200 }); + } + }); +} + +export async function mockFormsVote(page: page) { + await page.route(`/api/evoting/forms/${FORMID}/vote`, async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: { + Status: 0, + Token: + 'eyJTdGF0dXMiOjAsIlRyYW5zYWN0aW9uSUQiOiJQQWluaEVjNVNzM2JiVWkxbldNWU55dWdPVkFpdVZ3YklZcGpKTFJ1SUdnPSIsIkxhc3RCbG9ja0lkeCI6NSwiVGltZSI6MTcwODAxNDgyMSwiSGFzaCI6ImtVT3g3Ykw0eC9IYXdwanppTityTVFIL3Fmb1pnRHBFUFc3S2tzRWl1TTA9IiwiU2lnbmF0dXJlIjoiZXlKT1lXMWxJam9pUWt4VExVTlZVbFpGTFVKT01qVTJJaXdpUkdGMFlTSTZJbU00UjFwUVYyWjZTRlpqT1dGV1dWaGhTbEUwWVhKT1UyRTRVbXB2VEVOSGIyZFBlbWRpVDFKSlYxVnVSbkE0ZFdaemQyMWlVVXA2ZWpScWJHNVFRa3QzUzJwWmVEaDJkVmgzZUhCWE5FeE1hVFZWUkRsUlBUMGlmUT09In0=', + }, + }); + } + }); +} + +// /api/config + +export async function mockProxy(page: page) { + await page.route('/api/config/proxy', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: `${process.env.DELA_PROXY_URL}`, + headers: { + 'set-cookie': + 'connect.sid=s%3A5srES5h7hQ2fN5T71W59qh3cUSQL3Mix.fPoO3rOxui8yfTG7tFd7RPyasaU5VTkhxgdzVRWJyNk', + }, + }); + }); +} + +// /api + +export async function mockProxies(page: page, workerNumber: number) { + await page.route( + `/api/proxies/grpc%3A%2F%2Fdela-worker-${workerNumber}%3A2000`, + async (route) => { + if (route.request().method() === 'OPTIONS') { + await route.fulfill({ + status: 200, + headers: { + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Origin': '*', + }, + }); + } else { + await route.fulfill({ + path: `./tests/json/api/proxies/dela-worker-${workerNumber}.json`, + }); + } + } + ); +} + +export async function mockPersonalInfo(page: page, sciper?: string) { + // clear current mock + await page.unroute('/api/personal_info'); + await page.route('/api/personal_info', async (route) => { + if (sciper) { + route.fulfill({ path: `./tests/json/api/personal_info/${sciper}.json` }); + } else { + route.fulfill({ status: 401, contentType: 'text/html', body: 'Unauthenticated' }); + } + }); +} + +export async function mockGetDevLogin(page: page) { + await page.route(`/api/get_dev_login/${SCIPER_ADMIN}`, async (route) => { + await route.fulfill({}); + }); + await page.route(`/api/get_dev_login/${SCIPER_USER}`, async (route) => { + await route.fulfill({}); + }); + 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: page) { + await page.route('/api/logout', async (route) => { + await route.fulfill({}); + }); +} + +export async function mockAddRole(page: page) { + await page.route('/api/add_role', async (route) => { + await route.fulfill({ status: 200 }); + }); +} diff --git a/web/frontend/tests/mocks/evoting.ts b/web/frontend/tests/mocks/evoting.ts new file mode 100644 index 000000000..6aa297779 --- /dev/null +++ b/web/frontend/tests/mocks/evoting.ts @@ -0,0 +1,80 @@ +import Worker0 from './../json/api/proxies/dela-worker-0.json'; +import Worker1 from './../json/api/proxies/dela-worker-1.json'; +import Worker2 from './../json/api/proxies/dela-worker-2.json'; +import Worker3 from './../json/api/proxies/dela-worker-3.json'; +import { FORMID } from './shared'; + +export async function mockForms(page: page, empty: boolean = true) { + // clear current mock + await page.unroute(`${process.env.DELA_PROXY_URL}/evoting/forms`); + await page.route(`${process.env.DELA_PROXY_URL}/evoting/forms`, async (route) => { + if (route.request().method() === 'OPTIONS') { + await route.fulfill({ + status: 200, + headers: { + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Origin': '*', + }, + }); + } else if (empty) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: '{"Forms": []}', + }); + } else { + await route.fulfill({ + path: './tests/json/formIndex.json', + }); + } + }); +} + +export async function mockFormsFormID(page: page, formStatus: number) { + // clear current mock + await page.unroute(`${process.env.DELA_PROXY_URL}/evoting/forms/${FORMID}`); + await page.route(`${process.env.DELA_PROXY_URL}/evoting/forms/${FORMID}`, async (route) => { + const formFile = [ + 'created.json', + 'open.json', + 'closed.json', + 'shuffled.json', + 'decrypted.json', + 'combined.json', + 'canceled.json', + ][formStatus]; + await route.fulfill({ + path: `./tests/json/evoting/forms/${formFile}`, + }); + }); +} + +export async function mockDKGActors(page: page, dkgActorsStatus: number, initialized: boolean) { + for (const worker of [Worker0, Worker1, Worker2, Worker3]) { + await page.route(`${worker.Proxy}/evoting/services/dkg/actors/${FORMID}`, async (route) => { + if (route.request().method() === 'PUT') { + await route.fulfill({ + status: 200, + headers: { + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Allow-Origin': '*', + }, + }); + } else { + let dkgActorsFile = ''; + switch (dkgActorsStatus) { + case 0: + dkgActorsFile = initialized ? 'initialized.json' : 'uninitialized.json'; + break; + case 6: + dkgActorsFile = worker === Worker0 ? 'setup.json' : 'certified.json'; // one node is set up, the remaining nodes are certified + break; + } + await route.fulfill({ + status: initialized ? 200 : 400, + path: `./tests/json/evoting/dkgActors/${dkgActorsFile}`, + }); + } + }); + } +} diff --git a/web/frontend/tests/mocks/shared.ts b/web/frontend/tests/mocks/shared.ts new file mode 100644 index 000000000..887386d78 --- /dev/null +++ b/web/frontend/tests/mocks/shared.ts @@ -0,0 +1 @@ +export const FORMID = 'b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4'; diff --git a/web/frontend/tests/navbar.spec.ts b/web/frontend/tests/navbar.spec.ts new file mode 100644 index 000000000..6190e9228 --- /dev/null +++ b/web/frontend/tests/navbar.spec.ts @@ -0,0 +1,86 @@ +import { default as i18n } from 'i18next'; +import { expect, test } from '@playwright/test'; +import { + assertOnlyVisibleToAdmin, + assertOnlyVisibleToAuthenticated, + initI18n, + logIn, + setUp, +} from './shared'; +import { SCIPER_USER, mockLogout, mockPersonalInfo } from './mocks/api'; + +initI18n(); + +test.beforeEach(async ({ page }) => { + await mockPersonalInfo(page); + await setUp(page, '/about'); +}); + +// unauthenticated + +test('Assert cookie is set', async ({ page }) => { + const cookies = await page.context().cookies(); + await expect(cookies.find((cookie) => cookie.name === 'connect.sid')).toBeTruthy(); +}); + +test('Assert D-Voting logo is present', async ({ page }) => { + const logo = await page.getByTestId('leftSideNavBarLogo'); + await expect(logo).toBeVisible(); + await expect(await logo.getByRole('link')).toHaveAttribute('href', '/'); +}); + +test('Assert EPFL logo is present', async ({ page }) => { + const logo = await page.getByTestId('leftSideNavBarEPFLLogo'); + await expect(logo).toBeVisible(); + await expect(await logo.getByRole('link')).toHaveAttribute('href', 'https://epfl.ch'); +}); + +test('Assert link to form table is present', async ({ page }) => { + 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 }) => { + 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 }) => { + 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); + page.waitForRequest( + (request) => request.url() === `${baseURL}/api/logout` && request.method() === 'POST' + ); + for (const [role, key] of [ + ['button', 'Profile'], + ['menuitem', 'logout'], + ['button', 'continue'], + ]) { + // @ts-ignore + await page.getByRole(role, { name: i18n.t(key) }).click(); + } +}); + +// admin + +test('Assert "Create form" button is (only) visible to admin', async ({ page }) => { + await assertOnlyVisibleToAdmin( + page, + page.getByRole('link', { name: i18n.t('navBarCreateForm') }) + ); +}); + +test('Assert "Admin" button is (only) visible to admin', async ({ page }) => { + await assertOnlyVisibleToAdmin(page, page.getByRole('link', { name: i18n.t('navBarAdmin') })); +}); diff --git a/web/frontend/tests/result.spec.ts b/web/frontend/tests/result.spec.ts new file mode 100644 index 000000000..f65f46bd5 --- /dev/null +++ b/web/frontend/tests/result.spec.ts @@ -0,0 +1,130 @@ +import { expect, test } from '@playwright/test'; +import { default as i18n } from 'i18next'; +import { assertHasFooter, assertHasNavBar, initI18n, setUp } from './shared'; +import { FORMID } from './mocks/shared'; +import { mockFormsFormID } from './mocks/evoting'; +import Form from './json/evoting/forms/combined.json'; + +initI18n(); + +test.beforeEach(async ({ page }) => { + // TODO integrate localisation + i18n.changeLanguage('en'); // force 'en' for these tests + await mockFormsFormID(page, 5); // mock clear election result per default + await setUp(page, `/forms/${FORMID}/result`); +}); + +test.afterAll(async () => { + i18n.changeLanguage(); // unset language for the other tests +}); + +test('Assert navigation bar is present', async ({ page }) => { + await assertHasNavBar(page); +}); + +test('Assert footer is present', async ({ page }) => { + await assertHasFooter(page); +}); + +test('Assert form titles are displayed correctly', async ({ page }) => { + await expect(page.getByText(i18n.t('navBarResult'))).toBeVisible(); + await expect( + page.getByText(i18n.t('totalNumberOfVotes', { votes: Form.Result.length })) + ).toBeVisible(); + const content = await page.getByTestId('content'); + await expect(content.locator('xpath=./div/div/div[2]/h3')).toContainText( + Form.Configuration.Title.En + ); + await expect(page.getByRole('tab', { name: i18n.t('resGroup') })).toBeVisible(); + await expect(page.getByRole('tab', { name: i18n.t('resIndiv') })).toBeVisible(); + for (const [index, scaffold] of Form.Configuration.Scaffold.entries()) { + await expect( + content.locator(`xpath=./div/div/div[2]/div/div[2]/div/div/div[${index + 1}]/h2`) + ).toContainText(scaffold.Title.En); + await expect( + content.locator( + `xpath=./div/div/div[2]/div/div[2]/div/div/div[${index + 1}]/div/div/div[1]/h2` + ) + ).toContainText(scaffold.Selects.at(0).Title.En); + } +}); + +test('Assert grouped results are displayed correctly', async ({ page }) => { + // grouped results are displayed by default + let i = 1; + for (const expected of [ + [ + ['Blue', '3/4'], + ['Green', '2/4'], + ['Red', '1/4'], + ], + [ + ['Cyan', '2/4'], + ['Magenta', '2/4'], + ['Yellow', '1/4'], + ['Key', '1/4'], + ], + ]) { + const resultGrid = await page + .getByTestId('content') + .locator(`xpath=./div/div/div[2]/div/div[2]/div/div/div[${i}]/div/div/div[2]`); + i += 1; + let j = 1; + for (const [title, totalCount] of expected) { + await expect(resultGrid.locator(`xpath=./div[${j}]/span`)).toContainText(title); + await expect(resultGrid.locator(`xpath=./div[${j + 1}]/div/div[2]`)).toContainText( + totalCount + ); + j += 2; + } + } +}); + +test('Assert individual results are displayed correctly', async ({ page }) => { + // individual results are displayed after toggling view + await page.getByRole('tab', { name: i18n.t('resIndiv') }).click(); + const content = await page.getByTestId('content'); + for (const i of [0, 1, 2, 3]) { + // for each ballot + for (const [index, scaffold] of Form.Configuration.Scaffold.entries()) { + // for each form + const result = [ + [ + [true, false, false], + [true, true, false, false], + ], + [ + [false, true, true], + [true, false, false, true], + ], + [ + [false, true, true], + [false, true, false, false], + ], + [ + [false, false, true], + [false, false, true, false], + ], + ] + .at(i) + .at(index); // get results for this ballot and this form + for (const [j, choice] of scaffold.Selects.at(0).Choices.entries()) { + const resultRow = await content.locator( + `xpath=./div/div/div[2]/div/div[2]/div/div/div[${index + 2}]/div[1]/div[1]/div[2]/div[${ + j + 1 + }]` + ); + await expect(resultRow.locator('xpath=./div[2]')).toContainText(JSON.parse(choice).en); + if (result.at(j)) { + await expect(resultRow.getByRole('checkbox')).toBeChecked(); + } else { + await expect(resultRow.getByRole('checkbox')).not.toBeChecked(); + } + } + } + if ([0, 1, 2].includes(i)) { + // display next ballot + await page.getByRole('button', { name: i18n.t('next') }).click(); + } + } +}); diff --git a/web/frontend/tests/shared.ts b/web/frontend/tests/shared.ts new file mode 100644 index 000000000..7849f7177 --- /dev/null +++ b/web/frontend/tests/shared.ts @@ -0,0 +1,71 @@ +import { default as i18n } from 'i18next'; +import { Locator, Page, 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, + mockGetDevLogin, + mockLogout, + mockPersonalInfo, + mockProxy, +} from './mocks/api'; + +export const FORMID = 'b63bcb854121051f2d8cff04bf0ac9b524b534b704509a16a423448bde3321b4'; + +export function initI18n() { + i18n.init({ + resources: { en, fr, de }, + fallbackLng: ['en', 'fr', 'de'], + }); +} + +export async function setUp(page: Page, url: string) { + await mockProxy(page); + await mockGetDevLogin(page); + await mockLogout(page); + // make sure that page is loaded + await page.goto(url, { waitUntil: 'networkidle' }); + await expect(page).toHaveURL(url); +} + +export async function logIn(page: Page, sciper: string) { + await mockPersonalInfo(page, sciper); + await page.reload({ waitUntil: 'networkidle' }); +} + +export async function assertOnlyVisibleToAuthenticated(page: Page, locator: Locator) { + 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: Page, locator: Locator) { + 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 +} + +export async function assertHasNavBar(page: Page) { + await expect(page.getByTestId('navBar')).toBeVisible(); +} + +export async function assertHasFooter(page: Page) { + await expect(page.getByTestId('footer')).toBeVisible(); +} + +export function translate(internationalizable: any) { + switch (i18n.language) { + case 'en': + return internationalizable.En; + case 'fr': + return internationalizable.Fr || internationalizable.En; + case 'de': + return internationalizable.De || internationalizable.En; + default: + return internationalizable.En; + } +} diff --git a/web/frontend/tests/tsconfig.json b/web/frontend/tests/tsconfig.json new file mode 100644 index 000000000..3d605c3e6 --- /dev/null +++ b/web/frontend/tests/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "paths": { + "*": [ + "*" + ] + }, + }, +} +