diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a8ac146..9c277af 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -7,17 +7,15 @@ name: Docker deploy on: push: - branches: [ "development_1.x" ] + branches: [ "development_2.1" ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: - branches: [ "development_1.x" ] + branches: [ "development_2.1" ] env: - # Use docker.io for Docker Hub if empty - REGISTRY: docker.io - # github.repository as / - IMAGE_NAME: "grisera_ui" + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} jobs: @@ -56,8 +54,8 @@ jobs: uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: registry: ${{ env.REGISTRY }} - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Bump version and push tag if: github.event_name != 'pull_request' @@ -66,7 +64,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.THIS_GITHUB_TOKEN }} DEFAULT_BRANCH: ${{ github.ref_name }} - DEFAULT_BUMP: minor + DEFAULT_BUMP: patch WITH_V: true # Extract metadata (tags, labels) for Docker @@ -75,8 +73,10 @@ jobs: id: meta uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} - tags: type=semver,pattern={{version}},value=${{ steps.version_bump.outputs.new_tag }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}},value=${{ steps.version_bump.outputs.new_tag }} + 2.1-latest # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action diff --git a/.gitignore b/.gitignore index 3206f34..e1b1fac 100644 --- a/.gitignore +++ b/.gitignore @@ -148,4 +148,8 @@ node_modules .DS_Store /dist .env.local -.env.*.local \ No newline at end of file +.env.*.local +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/README.md b/README.md index 1f786b1..05e5a66 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,5 @@ npm install axios ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). +### Deployment +For security reasons it is required for OAuth client to regenerate client secrets and update them in configuration. \ No newline at end of file diff --git a/config.js b/config.js index 25bc0a2..4868f05 100644 --- a/config.js +++ b/config.js @@ -1,7 +1,10 @@ export default { - // 'process.env.VUE_APP_API_URL' and 'process.env.VUE_APP_AUTH_MS_URL' are needed for entrypoint.sh script + // 'VUE_APP_API_URL' and 'VUE_APP_AUTH_MS_URL' are needed for entrypoint.sh script // used to create prod image of grisera-ui using prod.dockerfile - apiUrl: process.env.VUE_APP_API_URL || 'process.env.VUE_APP_API_URL', - authUrl: process.env.VUE_APP_AUTH_MS_URL || 'process.env.VUE_APP_AUTH_MS_URL', + apiUrl: 'VUE_APP_API_URL' || 'http://localhost:8085', + authUrl: 'VUE_APP_AUTH_MS_URL' || 'http://localhost:8081/api', + keycloakUrl: 'VUE_APP_KEYCLOAK_URL' || 'http://localhost:8090', + keycloakClientId: 'VUE_APP_KEYCLOAK_CLIENT_ID' || 'grisera-ui', + realm: 'VUE_APP_KEYCLOAK_REALM' || 'grisera', sessionDurationMinutes: 30, }; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..d64d744 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,67 @@ +version: "3.9" # optional since v1.27.0 +services: + grisera_api_mongodb: + image: maczomen/grisera_core_mongo:0.2.3 + depends_on: + - mongodb + ports: + - "18085:80" + environment: + - MONGO_API_HOST=user:password@mongodb + - MONGO_API_PORT=27017 + - TIMEOUT=300 + + + frontend-ms: + build: + context: . + dockerfile: prod.dockerfile + depends_on: + - auth-ms + - grisera_api_mongodb + ports: + - "8080:80" + environment: + - VUE_APP_API_URL=http://localhost:18085 + - VUE_APP_AUTH_MS_URL=http://localhost:8081/api + + + auth-ms: + image: maczomen/grisera_auth:0.3.3 + depends_on: + - mongodb + ports: + - "8081:3000" + environment: + - JWT_SECRET=jwtsecret + - DB_URL=mongodb://user:password@mongodb:27017/auth?authSource=admin + - DB_NAME=auth + + mongodb: + image: mongo + restart: always + volumes: + - mongodbdata:/data/db + environment: + - MONGO_INITDB_ROOT_USERNAME=user + - MONGO_INITDB_ROOT_PASSWORD=password + - MONGO_INITDB_DATABASE=auth + + mongodb-show: + image: mongo-express + ports: + - "8084:8081" + depends_on: + - mongodb + restart: always + environment: + ME_CONFIG_MONGODB_SERVER: mongodb + ME_CONFIG_MONGODB_PORT: 27017 + ME_CONFIG_BASICAUTH_USERNAME: user + ME_CONFIG_BASICAUTH_PASSWORD: password + ME_CONFIG_MONGODB_AUTH_DATABASE: admin + ME_CONFIG_MONGODB_ADMINUSERNAME: user + ME_CONFIG_MONGODB_ADMINPASSWORD: password + +volumes: + mongodbdata: diff --git a/docker-compose.tests.yaml b/docker-compose.tests.yaml new file mode 100644 index 0000000..df3e23c --- /dev/null +++ b/docker-compose.tests.yaml @@ -0,0 +1,11 @@ +services: + playwright-tests: + image: playwright-tests + build: + context: . + dockerfile: tests.Dockerfile + volumes: + - .:/app + ports: + - "9323:9323" + network_mode: host diff --git a/docker-compose.yml b/docker-compose.yml index d64d744..47af477 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,41 +1,69 @@ version: "3.9" # optional since v1.27.0 services: grisera_api_mongodb: - image: maczomen/grisera_core_mongo:0.2.3 + image: ghcr.io/grisera/grisera-core-mongo:2.1.1 depends_on: - mongodb ports: - - "18085:80" + - "8085:80" environment: - - MONGO_API_HOST=user:password@mongodb + - MONGO_API_HOST=mongodb - MONGO_API_PORT=27017 - - TIMEOUT=300 + - MONGO_API_USER=user + - MONGO_API_PASSWORD=password + - KEYCLOAK_SERVER=http://keycloak:8080 + - REALM=grisera + - PERMISSIONS_ENDPOINT=http://auth-ms:3000/api/permissions + - CLIENT_SECRET=6UkCrp7UqFy78vh5TVhkaYP0OuVagNTd + frontend-ms: build: context: . dockerfile: prod.dockerfile + volumes: + - .:/app depends_on: - auth-ms - grisera_api_mongodb ports: - - "8080:80" + - "3000:80" environment: - - VUE_APP_API_URL=http://localhost:18085 + - VUE_APP_API_URL=http://localhost:8085 - VUE_APP_AUTH_MS_URL=http://localhost:8081/api - + - VUE_APP_KEYCLOAK_URL=http://localhost:8090 + - VUE_APP_KEYCLOAK_CLIENT_ID=grisera-ui + - VUE_APP_KEYCLOAK_REALM=grisera auth-ms: - image: maczomen/grisera_auth:0.3.3 + image: ghcr.io/grisera/grisera-auth:2.1.8 depends_on: - mongodb ports: - "8081:3000" environment: - - JWT_SECRET=jwtsecret - DB_URL=mongodb://user:password@mongodb:27017/auth?authSource=admin - DB_NAME=auth + - KEYCLOAK_URL=http://keycloak:8080 + - REALM=grisera + - CLIENT_ID=grisera-auth + - CLIENT_SECRET=1FrVLc5QFTDNvrJVNvJdqkeWkcpulPJZ + + keycloak: + image: ghcr.io/grisera/grisera-auth-keycloak:2.1.8 + depends_on: + - postgres + ports: + - "8090:8080" + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + KC_DB: postgres + KC_DB_URL_HOST: postgres + KC_DB_URL_DATABASE: keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: password mongodb: image: mongo @@ -50,7 +78,7 @@ services: mongodb-show: image: mongo-express ports: - - "8084:8081" + - "8888:8081" depends_on: - mongodb restart: always @@ -63,5 +91,15 @@ services: ME_CONFIG_MONGODB_ADMINUSERNAME: user ME_CONFIG_MONGODB_ADMINPASSWORD: password + postgres: + image: postgres:latest + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + volumes: + - postgresdata:/var/lib/postgresql/data + volumes: mongodbdata: + postgresdata: diff --git a/e2e-tests/pages/AccessPermissionsTab.ts b/e2e-tests/pages/AccessPermissionsTab.ts new file mode 100644 index 0000000..d09be1b --- /dev/null +++ b/e2e-tests/pages/AccessPermissionsTab.ts @@ -0,0 +1,45 @@ +import { expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class AccessPermissionsTab extends BasePage { + async visit(): Promise { + await this.page.getByRole('tab', { name: 'Access Permissions' }).click(); + } + + private get usernameInput() { + return this.page.getByRole('textbox', { name: 'Username' }); + } + + private get roleInput() { + return this.page.getByRole('textbox', { name: 'Role' }); + } + + private get addButton() { + return this.page.getByRole('Button', {name: 'add'}); + } + + private get confirmButton() { + return this.page.getByRole('Button', {name: 'confirm'}); + } + + async addPermission(username: string, role: string) { + await this.usernameInput.fill(username); + await this.page.getByRole('option', { name: username }).click(); + + await this.roleInput.click(); + await this.page.getByRole("option", {name: role}).click(); + + + await this.addButton.click(); + + if (await this.confirmButton.count() > 0) { + await this.confirmButton.click(); + } + + await expect(this.page.getByRole("cell", {name: username})).toBeVisible(); + await expect(this.page.getByRole("cell", {name: role})).toBeVisible(); + + } +} + + diff --git a/e2e-tests/pages/ActivityPage.ts b/e2e-tests/pages/ActivityPage.ts new file mode 100644 index 0000000..ec16570 --- /dev/null +++ b/e2e-tests/pages/ActivityPage.ts @@ -0,0 +1,39 @@ +import {BasePage} from "./BasePage"; + +export class ActivityPage extends BasePage { + async visit(): Promise { + await this.page.goto('/activities'); + await this.waitForPageLoad(); + } + + private get createButton() { + return this.page.getByRole('button', { name: 'Create' }); + } + + private get nameInput() { + return this.page.getByLabel('Name'); + } + + private get descriptionInput() { + return this.page.getByLabel('Description'); + } + + private get typeDropdown() { + return this.page.getByLabel('Type'); + } + + private get createActivityButton() { + return this.page.getByRole('button', { name: 'Create new activity' }); + } + + + async createActivity(name: string, description: string, type: string): Promise { + await this.createButton.click(); + await this.waitForPageLoad(); + await this.nameInput.fill(name); + await this.descriptionInput.fill(description); + await this.typeDropdown.click(); + await this.page.getByText(type).click(); + await this.createActivityButton.click(); + } +} diff --git a/e2e-tests/pages/BasePage.ts b/e2e-tests/pages/BasePage.ts new file mode 100644 index 0000000..ac5abe7 --- /dev/null +++ b/e2e-tests/pages/BasePage.ts @@ -0,0 +1,15 @@ +import { Page } from '@playwright/test'; + +export abstract class BasePage { + protected page: Page; + + protected constructor(page: Page) { + this.page = page; + } + + abstract visit(): Promise; + + async waitForPageLoad(): Promise { + await this.page.waitForLoadState('domcontentloaded'); + } +} diff --git a/e2e-tests/pages/DatasetCreatePage.ts b/e2e-tests/pages/DatasetCreatePage.ts new file mode 100644 index 0000000..a777a10 --- /dev/null +++ b/e2e-tests/pages/DatasetCreatePage.ts @@ -0,0 +1,83 @@ +import { BasePage } from './BasePage'; + +export class DatasetCreatePage extends BasePage { + async visit(): Promise { + await this.page.goto('/datasets/create'); + await this.waitForPageLoad(); + } + + private get nameField() { + return this.page.getByLabel('Name'); + } + + private get creatorField() { + return this.page.getByLabel('Creator'); + } + + private get rightsField() { + return this.page.getByLabel('Rights'); + } + + private get dateField() { + return this.page.getByLabel('Date'); + } + + private get descriptionField() { + return this.page.getByLabel('Description'); + } + + private get cancelButton() { + return this.page.getByRole('button', { name: 'Cancel' }); + } + + private get submitButton() { + return this.page.getByRole('button', { name: 'create' }); + } + + async fillName(name: string): Promise { + await this.nameField.fill(name); + } + + async fillCreator(creator: string): Promise { + await this.creatorField.fill(creator); + } + + async fillRights(rights: string): Promise { + await this.rightsField.fill(rights); + } + + async fillDescription(description: string): Promise { + await this.descriptionField.fill(description); + } + + async selectDate(date: string): Promise { + await this.dateField.click(); + const day = new Date(date).getDate().toString(); + await this.page.getByRole('button', { name: day }).first().click(); + await this.page.getByRole('button', { name: 'OK' }).click(); + } + + async submitForm(): Promise { + await this.submitButton.click({ force: true }); + await this.page.waitForURL('/datasets'); + } + + async cancel(): Promise { + await this.cancelButton.click(); + } + + async createDataset(dataset: { + name: string; + creator: string; + rights: string; + date: string; + description: string; + }): Promise { + await this.fillName(dataset.name); + await this.fillCreator(dataset.creator); + await this.fillRights(dataset.rights); + await this.selectDate(dataset.date); + await this.fillDescription(dataset.description); + await this.submitForm(); + } +} diff --git a/e2e-tests/pages/DatasetListPage.ts b/e2e-tests/pages/DatasetListPage.ts new file mode 100644 index 0000000..037e0e9 --- /dev/null +++ b/e2e-tests/pages/DatasetListPage.ts @@ -0,0 +1,60 @@ +import { expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class DatasetListPage extends BasePage { + async visit(): Promise { + await this.page.goto('/datasets'); + await this.waitForPageLoad(); + } + + datasetCardWithName(name: string) { + return this.page.locator('.dataset-card', { hasText: name }).last(); + } + + async verifyDatasetDetails(dataset: { + name: string; + creator: string; + rights: string; + date: string; + description: string; + }): Promise { + const card = this.datasetCardWithName(dataset.name); + await expect(card).toContainText(dataset.name); + await expect(card).toContainText(dataset.creator); + await expect(card).toContainText(dataset.rights); + await expect(card).toContainText(dataset.date); + await expect(card).toContainText(dataset.description); + } + + async verifyDatasetEditable(dataset: { + name: string; + }): Promise { + const card = this.datasetCardWithName(dataset.name); + await expect(card + .locator('[data-testid^="edit-button"]') + .first()).toBeVisible(); + } + + async useAnyDataset(): Promise { + const card = this.page.locator('.dataset-card').first(); + await card.locator('button:has-text("Select and proceed")').click(); + await this.page.waitForURL('/'); + await this.waitForPageLoad(); + } + + async useDatasetByName(name: string): Promise { + await this.page.locator(`.dataset-card:has([test-data="${ name }"])`) + .first() + .getByRole('button', { name: 'Select and proceed' }) + .click(); + await this.page.waitForURL('/'); + await this.waitForPageLoad(); + await expect(this.page.getByText(name).first()).toBeVisible(); + } + + async usingAnyDataset(): Promise { + await this.visit(); + await this.waitForPageLoad(); + await this.useAnyDataset(); + } +} diff --git a/e2e-tests/pages/ExperimentListPage.ts b/e2e-tests/pages/ExperimentListPage.ts new file mode 100644 index 0000000..61ee144 --- /dev/null +++ b/e2e-tests/pages/ExperimentListPage.ts @@ -0,0 +1,27 @@ +import { BasePage } from './BasePage'; + +export class ExperimentListPage extends BasePage { + async visit(): Promise { + await this.page.goto('/experiments'); + await this.waitForPageLoad(); + } + + async useAnyExperiment(): Promise { + await this.page.getByText('Go to details').first().click(); + await this.page.waitForURL('/experiments/**'); + await this.waitForPageLoad(); + } + + async useExperimentByName(name: string): Promise { + await this.visit(); + await this.page.locator(`tr:has([test-data="${ name }"])`).getByRole('button', { name: 'Go to details' }).click(); + await this.page.waitForURL('/experiments/**'); + await this.waitForPageLoad(); + } + + async usingAnyExperiment(): Promise { + await this.visit(); + await this.waitForPageLoad(); + await this.useAnyExperiment(); + } +} diff --git a/e2e-tests/pages/ExperimentPage.ts b/e2e-tests/pages/ExperimentPage.ts new file mode 100644 index 0000000..3431757 --- /dev/null +++ b/e2e-tests/pages/ExperimentPage.ts @@ -0,0 +1,49 @@ +import { BasePage } from './BasePage'; + +export class ExperimentPage extends BasePage { + + async useAnyExperiment(): Promise { + await this.visit(); + await this.page.getByTestId('go-to-details-button').first().click(); + } + + + private get createButton() { + return this.page.getByRole('button', { name: 'Create' }); + } + + private get nameInput() { + return this.page.getByLabel('Name'); + } + + private get authorInput() { + return this.page.getByLabel('Author'); + } + + private get descriptionInput() { + return this.page.getByLabel('Description'); + } + + private get footnoteInput() { + return this.page.getByLabel('Footnote'); + } + + private get createExperimentButton() { + return this.page.getByRole('button', { name: 'Create new experiment' }); + } + + async visit(): Promise { + await this.page.goto('/experiments'); + await this.waitForPageLoad(); + } + + async createExperiment({ name, author, description, footnote }): Promise { + await this.createButton.click(); + await this.page.waitForURL('/experiments/create'); + await this.nameInput.fill(name); + await this.authorInput.fill(author); + await this.descriptionInput.fill(description); + await this.footnoteInput.fill(footnote); + await this.createExperimentButton.click(); + } +} \ No newline at end of file diff --git a/e2e-tests/pages/LoginPage.ts b/e2e-tests/pages/LoginPage.ts new file mode 100644 index 0000000..64faaf3 --- /dev/null +++ b/e2e-tests/pages/LoginPage.ts @@ -0,0 +1,43 @@ +import { expect } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class LoginPage extends BasePage { + private get loginInput() { + return this.page.getByLabel('Username or email'); + } + + private get passwordInput() { + return this.page.getByLabel('Password'); + } + + private get loginButton() { + return this.page.getByRole('button', { name: 'Log in' }); + } + + async visit(): Promise { + await this.page.goto('/'); + await this.waitForPageLoad(); + } + + async login(email: string, password: string): Promise { + await this.loginInput.fill(email); + await this.passwordInput.fill(password); + await this.loginButton.click(); + } + + async loggedInAsDefaultUser(): Promise { + await this.visit(); + await this.login('test@example.com', 'test@example.com'); + await this.verifyLoggedIn('test@example.com'); + } + + async loggedInAsUser({ email, password }): Promise { + await this.visit(); + await this.login(email, password); + await this.verifyLoggedIn(email); + } + + async verifyLoggedIn(email: string): Promise { + await expect(this.page.getByText(email).first()).toBeVisible(); + } +} diff --git a/e2e-tests/pages/MeasurePage.ts b/e2e-tests/pages/MeasurePage.ts new file mode 100644 index 0000000..8634baa --- /dev/null +++ b/e2e-tests/pages/MeasurePage.ts @@ -0,0 +1,8 @@ +import {BasePage} from './BasePage'; + +export class MeasurePage extends BasePage { + async visit(): Promise { + await this.page.goto('/measures'); + await this.waitForPageLoad(); + } +} diff --git a/e2e-tests/pages/ParticipantCreatePage.ts b/e2e-tests/pages/ParticipantCreatePage.ts new file mode 100644 index 0000000..b4239d6 --- /dev/null +++ b/e2e-tests/pages/ParticipantCreatePage.ts @@ -0,0 +1,52 @@ +import { Locator } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class ParticipantCreatePage extends BasePage { + async visit(): Promise { + await this.page.goto('/participants/create'); + await this.waitForPageLoad(); + } + + private get nameInput(): Locator { + return this.page.getByLabel('Name', { exact: true }); + } + + private get surnameInput(): Locator { + return this.page.getByLabel('Surname', { exact: true }); + } + + private get birthDateInput(): Locator { + return this.page.getByLabel('Date of birth'); + } + + private get sexSelect(): Locator { + return this.page.getByLabel('Sex'); + } + + private get createButton(): Locator { + return this.page.getByRole('button', { name: 'Create new participant' }); + } + + async selectDate(date: string): Promise { + await this.birthDateInput.click(); + const day = new Date(date).getDate().toString(); + await this.page.getByRole('button', { name: day }).first().click(); + await this.page.getByRole('button', { name: 'OK' }).click(); + } + + async selectSex(sex: string): Promise { + await this.sexSelect.click(); + await this.page.getByText(sex, { exact: true }).click(); + } + + async fillParticipantForm({ name, surname, birthDate, sex }) { + await this.nameInput.fill(name); + await this.surnameInput.fill(surname); + await this.selectDate(birthDate); + await this.selectSex(sex); + } + + async submitForm() { + await this.createButton.click(); + } +} diff --git a/e2e-tests/pages/ParticipantPage.ts b/e2e-tests/pages/ParticipantPage.ts new file mode 100644 index 0000000..47d0582 --- /dev/null +++ b/e2e-tests/pages/ParticipantPage.ts @@ -0,0 +1,8 @@ +import { BasePage } from './BasePage'; + +export class ParticipantPage extends BasePage { + async visit(): Promise { + await this.page.goto('/participants'); + await this.waitForPageLoad(); + } +} diff --git a/e2e-tests/pages/ParticipantsTab.ts b/e2e-tests/pages/ParticipantsTab.ts new file mode 100644 index 0000000..42b592e --- /dev/null +++ b/e2e-tests/pages/ParticipantsTab.ts @@ -0,0 +1,14 @@ +import { BasePage } from './BasePage'; + +export class ParticipantsTab extends BasePage { + async visit(): Promise { + await this.page.getByRole('tab', { name: 'Participants' }).click(); + } + + async addParticipant(participantName: string): Promise { + await this.page.getByTestId('experiment-participant-add-button').click(); + await this.page.getByLabel('Participant').click(); + await this.page.locator(`div[role="option"]:has-text("${ participantName }")`).first().click(); + await this.page.getByRole('dialog').getByRole('button', { name: 'Add' }).click(); + } +} diff --git a/e2e-tests/pages/RecordingsTab.ts b/e2e-tests/pages/RecordingsTab.ts new file mode 100644 index 0000000..d774e05 --- /dev/null +++ b/e2e-tests/pages/RecordingsTab.ts @@ -0,0 +1,31 @@ +import { BasePage } from './BasePage'; + +export class RecordingsTab extends BasePage { + async visit(): Promise { + await this.page.getByRole('tab', { name: 'Recordings' }).click(); + } + + async addRecording({ scenarioExecutionName, activityExecutionName, name, description, link, channel, participant }): Promise { + await this.page.getByRole('button', { name: 'Create' }).click(); + + await this.page.getByLabel('Scenario Executions').click(); + await this.page.locator('div[role="option"]').filter({ hasText: scenarioExecutionName }).first().click(); + + await this.page.getByLabel('Activity Execution').click(); + await this.page.locator('div[role="option"]').filter({ hasText: activityExecutionName }).first().click(); + + await this.page.getByLabel('Name').fill(name); + + await this.page.getByLabel('Description').fill(description); + + await this.page.getByLabel('Link').fill(link); + + await this.page.getByLabel('Channel').click(); + await this.page.locator('div[role="option"]').filter({ hasText: channel }).first().click(); + + await this.page.getByLabel('Participants').click(); + await this.page.locator('div[role="option"]').filter({ hasText: participant }).first().click(); + + await this.page.getByRole('button', { name: 'Create' }).click(); + } +} diff --git a/e2e-tests/pages/RegistrationPage.ts b/e2e-tests/pages/RegistrationPage.ts new file mode 100644 index 0000000..8802416 --- /dev/null +++ b/e2e-tests/pages/RegistrationPage.ts @@ -0,0 +1,62 @@ +import { expect, Response } from '@playwright/test'; +import { BasePage } from './BasePage'; + +export class RegistrationPage extends BasePage { + async visit(): Promise { + await this.page.goto('/'); + await this.createAccountButton.click(); + await this.waitForPageLoad(); + } + + private get usernameField() { + return this.page.getByLabel('Username'); + } + + private get emailField() { + return this.page.getByLabel('Email'); + } + + private get passwordField() { + return this.page.getByLabel('Password', { exact: true }); + } + + private get confirmPasswordField() { + return this.page.getByLabel('Confirm password'); + } + + private get firstName() { + return this.page.getByLabel('First name'); + } + + private get lastName() { + return this.page.getByLabel('Last name'); + } + + private get createAccountButton() { + return this.page.getByText('create account'); + } + + async fillRegistrationForm(username: string, email: string, password: string, firstName: string, lastName: string): Promise { + await this.usernameField.fill(username); + await this.emailField.fill(email); + await this.passwordField.fill(password); + await this.confirmPasswordField.fill(password); + await this.firstName.fill(firstName); + await this.lastName.fill(lastName); + await this.submitForm(); + } + + async submitForm(): Promise { + await this.createAccountButton.click(); + } + + async register(username: string, email: string, password: string, firstName: string, lastName: string): Promise { + await this.fillRegistrationForm(username, email, password, firstName, lastName); + await this.verifyLoggedIn(email); + } + + + async verifyLoggedIn(email: string): Promise { + await expect(this.page.getByText(email).first()).toBeVisible(); + } +} diff --git a/e2e-tests/pages/ScenariosExecutionsTab.ts b/e2e-tests/pages/ScenariosExecutionsTab.ts new file mode 100644 index 0000000..ae0bc48 --- /dev/null +++ b/e2e-tests/pages/ScenariosExecutionsTab.ts @@ -0,0 +1,39 @@ +import { BasePage } from './BasePage'; + +export class ScenariosExecutionsTab extends BasePage { + async visit(): Promise { + await this.page.getByRole('tab', { name: 'Scenarios Executions' }).click(); + } + + async addScenarioExecution(scenarioName: string, executionName: string): Promise { + await this.page.getByLabel('Scenarios').click(); + + await this.page.locator('div[role="option"]').filter({ hasText: scenarioName }).first().click(); + + await this.page.getByLabel('Name').fill(executionName); + + await this.page.getByRole('button', { name: 'Create' }).click(); + } + + async addParticipantJamesToScenario(button: any): Promise { + + await button.click(); + + await this.page.getByLabel('Participants').click(); + + await this.page.locator('div[role="option"]:has-text("James")').first().click(); + + await this.page.getByRole('button', { name: 'Update' }).click(); + } + + async addParticipantToScenariosActivity({ participantName, scenarioExecutionName, activityName }): Promise { + await this.page.getByRole('button', { name: scenarioExecutionName }).first().click(); + await this.page.getByRole('tab', { name: 'Scenarios Executions' }).click(); + await this.page.locator(`tr:has([test-data="${ activityName }"])`) + .locator('[data-testid^="edit-activity-execution"]') + .click(); + await this.page.getByLabel('Participants').click(); + await this.page.locator(`div[role="option"]:has-text("${ participantName }")`).first().click(); + await this.page.getByRole('button', { name: 'Update' }).click(); + } +} diff --git a/e2e-tests/pages/ScenariosTab.ts b/e2e-tests/pages/ScenariosTab.ts new file mode 100644 index 0000000..935f083 --- /dev/null +++ b/e2e-tests/pages/ScenariosTab.ts @@ -0,0 +1,22 @@ +import { BasePage } from './BasePage'; + +export class ScenariosTab extends BasePage { + async visit(): Promise { + await this.page.getByRole('tab', { name: 'Scenarios', exact: true }).click(); + } + + async addScenario({ name, description, activities }): Promise { + await this.page.getByRole('button', { name: 'Create' }).click(); + await this.page.getByLabel('Name').fill(name); + await this.page.getByLabel('Description').fill(description); + + for (const activityName of activities) { + await this.page.getByRole('button', { name: 'Add next activity' }).click(); + await this.page.getByLabel('Activity').click(); + await this.page.locator('div.v-list-item', { hasText: activityName }).first().click(); + await this.page.getByRole('button', { name: 'Add' }).click(); + } + + await this.page.getByRole('button', { name: 'Create' }).click(); + } +} diff --git a/e2e-tests/pages/SettingsPage.ts b/e2e-tests/pages/SettingsPage.ts new file mode 100644 index 0000000..e8540ed --- /dev/null +++ b/e2e-tests/pages/SettingsPage.ts @@ -0,0 +1,8 @@ +import {BasePage} from './BasePage'; + +export class SettingsPage extends BasePage { + async visit(): Promise { + await this.page.goto('/settings'); + await this.waitForPageLoad(); + } +} diff --git a/e2e-tests/pages/TimeSeriesCreatePage.ts b/e2e-tests/pages/TimeSeriesCreatePage.ts new file mode 100644 index 0000000..0329c01 --- /dev/null +++ b/e2e-tests/pages/TimeSeriesCreatePage.ts @@ -0,0 +1,124 @@ +// +// +// test('test', async ({ page }) => { +// await page.goto('http://localhost:8080/datasets'); +// await page.getByRole('button', { name: 'select and proceed' }).click(); +// await page.getByRole('link', { name: 'Experiments' }).click(); +// await page.getByRole('button', { name: 'Go to details' }).click(); +// await page.getByRole('tab', { name: 'Scenarios executions' }).click(); +// await page.getByRole('button', { name: 'Scenario ex 1 󰆴' }).click(); +// await page.getByRole('button', { name: '󰅀' }).first().click(); +// await page.getByRole('button', { name: 'Time Series' }).click(); +// await page.getByRole('button', { name: 'Create' }).click(); + + +// await page.getByLabel('Link').click(); +// await page.getByLabel('Link').fill('asdasd@test.com'); +// await page.locator('div').filter({ hasText: /^Timestamp$/ }).locator('div').nth(1).click(); +// await page.locator('div').filter({ hasText: /^Regular$/ }).locator('div').nth(1).click(); + +// await page.getByRole('option', { name: '' }).click(); +// await page.getByRole('button', { name: 'Create' }).click(); +// await page.getByLabel('Link').click(); +// await page.getByLabel('Link').press('ControlOrMeta+a'); +// await page.getByLabel('Link').fill('https://wp.pl'); +// await page.getByRole('button', { name: 'Create' }).click(); +// await expect(page.getByRole('cell', { name: 'https://wp.pl' })).toBeVisible(); +// await expect(page.getByRole('cell', { name: 'Familiarity' }).first()).toBeVisible(); +// }); + +import { BasePage } from './BasePage'; +import { Locator } from '@playwright/test'; + +export class TimeSeriesCreatePage extends BasePage { + async visit(): Promise { + await this.page.goto('/experiments/67d6ff5e3075786c4f00bc00/activity-executions/67d7025913cc6dc06528d475/participants/67d6fdc14a5301c720455c97/time-series'); + await this.page.goto('/experiments/67d6ff5e3075786c4f00bc00/activity-executions/67d7025913cc6dc06528d475/participants/67d6fdc14a5301c720455c97/time-series/create'); + await this.waitForPageLoad(); + } + + private get linkInput(): Locator { + return this.page.getByLabel('Link', { exact: true }); + } + + async selectType(name: string): Promise { + if ('Epoch' === name) { + return await this.page.locator('div') + .filter({ hasText: /^Epoch/ }) + .locator('div') + .nth(1) + .click(); + } + + if ('Timestamp' === name) { + return await this.page.locator('div') + .filter({ hasText: /^Timestamp$/ }) + .locator('div') + .nth(1) + .click(); + } + } + + async selectSpacing(name: string): Promise { + if ('Irregular' === name) { + return await this.page.locator('div') + .filter({ hasText: /^Irregular/ }) + .locator('div') + .nth(1) + .click(); + } + + if ('Regular' === name) { + return await this.page.locator('div') + .filter({ hasText: /^Regular$/ }) + .locator('div') + .nth(1) + .click(); + } + } + + async selectMeasure(name: string): Promise { + await this.page.getByLabel('Measure').click(); + await this.page.getByRole('option', { name }).click(); + } + + async selectRecording(name: string): Promise { + await this.page.getByLabel('Recording').click(); + await this.page.getByRole('option', { name: `(recording name: ${ name })` }).click(); + } + + async selectChannel(name: string): Promise { + await this.page.getByLabel('Channel').click(); + await this.page.getByRole('option', { name }).click(); + } + + async selectModality(name: string): Promise { + await this.page.getByLabel('Modality').click(); + await this.page.getByRole('option', { name }).click(); + } + + async selectLiveActivity(name: string): Promise { + await this.page.getByLabel('Live activity').click(); + await this.page.getByRole('option', { name }).click(); + } + + private get createButton(): Locator { + return this.page.getByRole('button', { name: 'Create' }); + } + + async fillForm({ link, type, spacing, measure, recording, channel, modality, liveActivity }): Promise { + await this.linkInput.fill(link); + await this.selectType(type); + await this.selectSpacing(spacing); + await this.selectMeasure(measure); + await this.selectRecording(recording); + await this.selectChannel(channel); + await this.selectModality(modality); + await this.selectLiveActivity(liveActivity); + } + + async submitForm() { + await this.createButton.click(); + await this.page.waitForURL('/experiments/67d6ff5e3075786c4f00bc00/activity-executions/67d7025913cc6dc06528d475/participants/67d6fdc14a5301c720455c97/time-series'); + } +} diff --git a/e2e-tests/tests/activity.spec.ts b/e2e-tests/tests/activity.spec.ts new file mode 100644 index 0000000..30ac58e --- /dev/null +++ b/e2e-tests/tests/activity.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test'; +import { LoginPage } from '../pages/LoginPage'; +import { ActivityPage } from '../pages/ActivityPage'; +import { DatasetListPage } from '../pages/DatasetListPage'; + +test.beforeEach(async ({ page }) => { + await new LoginPage(page).loggedInAsDefaultUser(); + await new DatasetListPage(page).usingAnyDataset(); +}); +test('Użytkownik może stworzyć nowe aktywności', async ({ page }) => { + const activities = [ + { + name: 'Answering Initial Metric Questions', + description: 'Participants answer questions to assess their familiarity with the Moodle platform.', + type: 'Individual', + }, + { + name: 'Logging into the Learning Platform', + description: 'Participants perform a task involving logging into the Moodle e-learning platform.', + type: 'Individual', + }, + { + name: 'Locating a Course', + description: 'Participants search for and locate a specific course on the Moodle platform.', + type: 'Individual', + }, + { + name: 'Completing an Emotional State Questionnaire', + description: 'Participants fill out questionnaires regarding their emotional state at different points during the experiment.', + type: 'Individual', + }, + { + name: 'Listening to a Lecture', + description: 'Participants listen to a pre-recorded lecture on an assigned topic.', + type: 'Individual', + }, + { + name: 'Completing a Quiz', + description: 'Participants answer questions related to the material presented in the lecture.', + type: 'Individual', + }, + { name: 'Listening to a Lecture', description: 'Participants listen to a lecture on an assigned topic.', type: 'Group' }, + { + name: 'Discussion and Joint Analysis', + description: 'Two participants summarize the lecture, ask each other clarifying questions, and collaboratively identify key points for deeper understanding.', + type: 'Two persons activity', + }, + ]; + + const activityPage = new ActivityPage(page); + await activityPage.visit(); + + for (const activity of activities) { + await activityPage.createActivity(activity.name, activity.description, activity.type); + await expect(page.getByRole('cell', { name: 'Answering Initial Metric' }).last()).toBeVisible(); + } +}); diff --git a/e2e-tests/tests/dataset.spec.ts b/e2e-tests/tests/dataset.spec.ts new file mode 100644 index 0000000..42f10a4 --- /dev/null +++ b/e2e-tests/tests/dataset.spec.ts @@ -0,0 +1,25 @@ +import { test } from '@playwright/test'; +import { DatasetCreatePage } from '../pages/DatasetCreatePage'; +import { LoginPage } from '../pages/LoginPage'; +import { DatasetListPage } from '../pages/DatasetListPage'; + +test.beforeEach(async ({ page }) => new LoginPage(page).loggedInAsDefaultUser()); + +test('Użytkownik może utworzyć nowy zbiór danych', async ({ page }) => { + const newDataset = { + name: 'Inconsistency dataset', + creator: 'Gdańsk University of Technology', + rights: 'Creative Commons Attribution 4.0 International License', + date: '2025-03-01', + description: `The dataset from Gdańsk University of Technology results from an experiment where facial expressions were recorded using four cameras placed at the corners of a screen. The goal was to recognize emotional states using three systems: Noldus FaceReader, QuantumLab Express Engine, and Luxand-based. FaceReader identified nine emotional states, including six basic Ekman emotions, neutral, and PAD model's valence and arousal. QuantumLab Express Engine recognized five basic emotions (excluding fear) and neutral, while Luxand identified six basic emotions and neutral. The experiment analyzed consistency between systems and cameras, as well as the impact of variables like sex, glasses, and facial hair. Twelve participants recorded their faces while completing tasks on an e-learning platform.`, + }; + + const datasetPage = new DatasetCreatePage(page); + await datasetPage.visit(); + await datasetPage.createDataset(newDataset); + + const datasetListPage = new DatasetListPage(page); + await datasetListPage.visit(); + await datasetListPage.waitForPageLoad(); + await datasetListPage.verifyDatasetDetails(newDataset); +}); diff --git a/e2e-tests/tests/experiment-participant.spec.ts b/e2e-tests/tests/experiment-participant.spec.ts new file mode 100644 index 0000000..d3c2a45 --- /dev/null +++ b/e2e-tests/tests/experiment-participant.spec.ts @@ -0,0 +1,22 @@ +import { expect, test } from '@playwright/test'; +import { LoginPage } from '../pages/LoginPage'; +import { DatasetListPage } from '../pages/DatasetListPage'; +import { ExperimentPage } from '../pages/ExperimentPage'; +import { ParticipantsTab } from '../pages/ParticipantsTab'; + +test.beforeEach(async ({ page }) => { + await new LoginPage(page).loggedInAsDefaultUser(); + await new DatasetListPage(page).usingAnyDataset(); + await new ExperimentPage(page).useAnyExperiment(); +}); + +test('Użytkownik może dodać uczestnika do eksperymentu', async ({ page }) => { + + const participantsTab = new ParticipantsTab(page); + + await participantsTab.visit(); + + await participantsTab.addParticipant('James Anderson'); + + await expect(page.getByRole('cell', { name: 'James' }).first()).toBeVisible(); +}); diff --git a/e2e-tests/tests/experiment.spec.ts b/e2e-tests/tests/experiment.spec.ts new file mode 100644 index 0000000..90e8c1a --- /dev/null +++ b/e2e-tests/tests/experiment.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import { LoginPage } from '../pages/LoginPage'; +import { DatasetListPage } from '../pages/DatasetListPage'; +import { ExperimentPage } from '../pages/ExperimentPage'; + +test.beforeEach(async ({ page }) => { + await new LoginPage(page).loggedInAsDefaultUser(); + await new DatasetListPage(page).usingAnyDataset(); +}); + + +test('Użytkownik może stworzyć nowy eksperyment', async ({ page }) => { + const experimentPage = new ExperimentPage(page); + + await experimentPage.visit(); + await experimentPage.createExperiment({ + name: 'Emotion recognition', + author: 'Gdańsk University of Technology', + description: 'Multi-camera facial expression analysis with multiple recognition systems.', + footnote: '-', + }); + + await page.getByRole('cell', { name: 'Emotion recognition' }).last().click(); +}); \ No newline at end of file diff --git a/e2e-tests/tests/login.spec.ts b/e2e-tests/tests/login.spec.ts new file mode 100644 index 0000000..0e3f748 --- /dev/null +++ b/e2e-tests/tests/login.spec.ts @@ -0,0 +1,9 @@ +import { test } from '@playwright/test'; +import { LoginPage } from '../pages/LoginPage'; + +test('Użytkownik może się zalogować', async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.visit(); + await loginPage.login('test@example.com', 'test@example.com'); + await loginPage.verifyLoggedIn('test@example.com'); +}); diff --git a/e2e-tests/tests/participant.spec.ts b/e2e-tests/tests/participant.spec.ts new file mode 100644 index 0000000..f867696 --- /dev/null +++ b/e2e-tests/tests/participant.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; +import { ParticipantCreatePage } from '../pages/ParticipantCreatePage'; +import { LoginPage } from '../pages/LoginPage'; +import { DatasetListPage } from '../pages/DatasetListPage'; + +test.beforeEach(async ({ page }) => { + await new LoginPage(page).loggedInAsDefaultUser(); + await new DatasetListPage(page).usingAnyDataset(); +}); + +test('Użytkownik może utworzyć nowego uczestnika 1', async ({ page }) => { + const participantPage = new ParticipantCreatePage(page); + await participantPage.visit(); + await participantPage.fillParticipantForm({ + name: 'James', + surname: 'Anderson', + birthDate: '1992-07-01', + sex: 'Male', + }); + await participantPage.submitForm(); + + await expect(page).toHaveURL('/participants'); + await expect(page.locator('text=James Anderson').last()).toBeVisible(); +}); + +test('Użytkownik może utworzyć nowego uczestnika 2', async ({ page }) => { + const participantPage = new ParticipantCreatePage(page); + await participantPage.visit(); + await participantPage.fillParticipantForm({ + name: 'Emma', + surname: 'Carter', + birthDate: '1987-03-01', + sex: 'Female', + }); + await participantPage.submitForm(); + + await expect(page).toHaveURL('/participants'); + await expect(page.locator('text=Emma Carter').last()).toBeVisible(); +}); + +test('Użytkownik może utworzyć nowego uczestnika 3', async ({ page }) => { + const participantPage = new ParticipantCreatePage(page); + await participantPage.visit(); + await participantPage.fillParticipantForm({ + name: 'Caroline', + surname: 'Kane', + birthDate: '1999-01-01', + sex: 'Female', + }); + await participantPage.submitForm(); + + await expect(page).toHaveURL('/participants'); + await expect(page.locator('text=Caroline Kane').last()).toBeVisible(); +}); diff --git a/e2e-tests/tests/process.spec.ts b/e2e-tests/tests/process.spec.ts new file mode 100644 index 0000000..8eae517 --- /dev/null +++ b/e2e-tests/tests/process.spec.ts @@ -0,0 +1,497 @@ +import {expect, Page, test as base} from '@playwright/test'; +import { LoginPage } from '../pages/LoginPage'; +import { DatasetCreatePage } from '../pages/DatasetCreatePage'; +import { DatasetListPage } from '../pages/DatasetListPage'; +import { ParticipantCreatePage } from '../pages/ParticipantCreatePage'; +import { ExperimentPage } from '../pages/ExperimentPage'; +import { ActivityPage } from '../pages/ActivityPage'; +import { ParticipantsTab } from '../pages/ParticipantsTab'; +import { ExperimentListPage } from '../pages/ExperimentListPage'; +import { ScenariosTab } from '../pages/ScenariosTab'; +import { ScenariosExecutionsTab } from '../pages/ScenariosExecutionsTab'; +import { RecordingsTab } from '../pages/RecordingsTab'; +import { RegistrationPage } from '../pages/RegistrationPage'; +import {SettingsPage} from "../pages/SettingsPage"; +import {AccessPermissionsTab} from "../pages/AccessPermissionsTab"; +import generateDate from '../utils/generate-date-util'; +import {MeasurePage} from "../pages/MeasurePage"; + +const hash = Math.random().toString(36).substring(2); + +const firstDayOfMonth = generateDate(1); + + +const newDataset = { + name: `Inconsistency dataset ${ hash }`, + creator: 'Gdańsk University of Technology', + rights: 'Creative Commons Attribution 4.0 International License', + date: firstDayOfMonth, + description: `The dataset from Gdańsk University of Technology results from an experiment where facial expressions were recorded using four cameras placed at the corners of a screen. The goal was to recognize emotional states using three systems: Noldus FaceReader, QuantumLab Express Engine, and Luxand-based. FaceReader identified nine emotional states, including six basic Ekman emotions, neutral, and PAD model's valence and arousal. QuantumLab Express Engine recognized five basic emotions (excluding fear) and neutral, while Luxand identified six basic emotions and neutral. The experiment analyzed consistency between systems and cameras, as well as the impact of variables like sex, glasses, and facial hair. Twelve participants recorded their faces while completing tasks on an e-learning platform.`, +}; + +const newExperiment = { + name: `Emotion recognition ${ hash }`, + author: 'Gdańsk University of Technology', + description: 'Multi-camera facial expression analysis with multiple recognition systems.', + footnote: '-', +}; + +const newParticipants = [ + { + name: 'James', + surname: 'Anderson', + birthDate: '1992-07-01', + sex: 'Male', + }, + { + name: 'Emma', + surname: 'Carter', + birthDate: '1987-03-01', + sex: 'Female', + }, + { + name: 'Caroline', + surname: 'Kane', + birthDate: '1999-01-01', + sex: 'Female', + }, +]; + +const newActivities = [ + { + name: 'Answering Initial Metric Questions', + description: 'Participants answer questions to assess their familiarity with the Moodle platform.', + type: 'Individual', + participants: ['James Anderson'], + }, + { + name: 'Logging into the Learning Platform', + description: 'Participants perform a task involving logging into the Moodle e-learning platform.', + type: 'Individual', + participants: ['James Anderson'], + }, + { + name: 'Locating a Course', + description: 'Participants search for and locate a specific course on the Moodle platform.', + type: 'Individual', + participants: ['James Anderson'], + }, + { + name: 'Completing an Emotional State Questionnaire', + description: 'Participants fill out questionnaires regarding their emotional state at different points during the experiment.', + type: 'Individual', + participants: ['James Anderson'], + }, + { + name: 'Listening to a Lecture', + description: 'Participants listen to a pre-recorded lecture on an assigned topic.', + type: 'Individual', + participants: ['James Anderson'], + }, + { + name: 'Completing a Quiz', + description: 'Participants answer questions related to the material presented in the lecture.', + type: 'Individual', + participants: ['James Anderson'], + }, + // { + // name: 'Listening to a Lecture', + // description: 'Participants listen to a lecture on an assigned topic.', + // type: 'Group', + // participants: ['James Anderson', 'Emma Carter', 'Caroline Kane'], + // }, + // { + // name: 'Discussion and Joint Analysis', + // description: 'Two participants summarize the lecture, ask each other clarifying questions, and collaboratively identify key points for deeper understanding.', + // type: 'Two persons activity', + // participants: ['James Anderson', 'Emma Carter'], + // }, +]; + +const newScenario = { + name: 'Online Learning Session Emotional Response', + description: 'This scenario tracks a user\'s emotional response throughout an online learning session.', + activities: newActivities.map(e => e.name), +}; + +const newScenarioExecutions = [ + { + scenarioName: newScenario.name, + executionName: 'ex-01', + }, + { + scenarioName: newScenario.name, + executionName: 'ex-02', + }, +]; + + +const newRecordings = [ + { + scenarioExecutionName: 'ex-01', + activityExecutionName: 'Activity Execution: 1', + name: 'Audio Recording 1', + description: 'Audio recording for individual activity execution 1.', + link: 'https://www.test.com', + channel: 'Audio', + participant: 'James Anderson', + }, + { + scenarioExecutionName: 'ex-01', + activityExecutionName: 'Activity Execution: 2', + name: 'BVP Recording 2', + description: 'BVP recording for individual activity execution 2.', + link: 'https://www.test.com', + channel: 'BVP', + participant: 'James Anderson', + }, + { + scenarioExecutionName: 'ex-01', + activityExecutionName: 'Activity Execution: 3', + name: 'ECG Recording 3', + description: 'ECG recording for individual activity execution 3.', + link: 'https://www.test.com', + channel: 'ECG', + participant: 'James Anderson', + }, + { + scenarioExecutionName: 'ex-01', + activityExecutionName: 'Activity Execution: 4', + name: 'EDA Recording 4', + description: 'EDA recording for individual activity execution 4.', + link: 'https://www.test.com', + channel: 'EDA', + participant: 'James Anderson', + }, + { + scenarioExecutionName: 'ex-01', + activityExecutionName: 'Activity Execution: 5', + name: 'Depth Video 5', + description: 'Depth video recording for individual activity 5.', + link: 'https://www.test.com', + channel: 'Depth video', + participant: 'James Anderson', + }, + { + scenarioExecutionName: 'ex-01', + activityExecutionName: 'Activity Execution: 6', + name: 'RGB Video 6', + description: 'RGB video recording for individual activity 6.', + link: 'https://www.test.com', + channel: 'RGB video', + participant: 'James Anderson', + }, + // { + // scenarioExecutionName: 'ex-01', + // activityExecutionName: 'Activity Execution: 7', + // name: 'Group EEG Recording 7', + // description: 'EEG recording for group activity with three participants.', + // link: 'https://www.test.com', + // channel: 'EEG', + // participant: 'James Anderson, Emma Carter, Caroline Kane', + // }, + // { + // scenarioExecutionName: 'ex-01', + // activityExecutionName: 'Activity Execution: 8', + // name: 'Temperature Recording 8', + // description: 'Temperature recording for two-person activity.', + // link: 'https://www.test.com', + // channel: 'Temperature', + // participant: 'James Anderson, Emma Carter', + // }, +]; + +const username = `${ hash }`; +const firstName = `${ hash }FirstName`; +const lastName = `${ hash }LastName`; +const email = `${ hash }@example.com`; + +const usernameOther = `other${ hash }`; +const emailOther = `other${ hash }@example.com`; +const firstNameOther = `other${ hash }FirstName`; +const lastNameOther = `other${ hash }LastName`; + +type LoggedInFixtures = { + userPage: Page; + otherUserPage: Page; +}; + +const test = base.extend({ + userPage: async({page}, use) => { + const loginPage = await new LoginPage(page); + await loginPage.loggedInAsUser({ email, password: email }) + await use(page) + }, + + otherUserPage: async({page}, use) => { + const loginPage = await new LoginPage(page); + await loginPage.loggedInAsUser({ email: emailOther, password: emailOther }) + await use(page) + } +}); + +test.describe.serial('Przejście całego procesu', () => { + test('[00] - Użytkownik rejestruje konto', async ({ page }) => { + const registrationPage = new RegistrationPage(page); + await registrationPage.visit(); + await registrationPage.register(username, email, email, firstName, lastName); + }); + + test('[01] - Użytkownik tworzy zbiór danych', async ({ userPage }) => { + const datasetPage = new DatasetCreatePage(userPage); + await datasetPage.visit(); + await datasetPage.createDataset(newDataset); + const datasetListPage = new DatasetListPage(userPage); + await datasetListPage.visit(); + await datasetListPage.waitForPageLoad(); + await datasetListPage.verifyDatasetDetails(newDataset); + await datasetListPage.useDatasetByName(newDataset.name); + }); + + test('[02] - Użytkownik tworzy uczestników', async ({ userPage }) => { + await selectDataset(userPage); + + for (const newParticipant of newParticipants) { + const participantPage = new ParticipantCreatePage(userPage); + await participantPage.visit(); + await participantPage.fillParticipantForm(newParticipant); + await participantPage.submitForm(); + await expect(userPage).toHaveURL('/participants'); + await expect(userPage.locator(`text=${ newParticipant.name } ${ newParticipant.surname }`).last()).toBeVisible(); + } + }); + + test('[03] - Użytkownik tworzy eksperyment', async ({ userPage }) => { + await selectDataset(userPage); + + const experimentPage = new ExperimentPage(userPage); + await experimentPage.visit(); + await experimentPage.createExperiment(newExperiment); + await userPage.getByRole('cell', { name: newExperiment.name }).last().click(); + }); + + test('[04] - Użytkownik tworzy aktywności', async ({ userPage }) => { + await selectDataset(userPage); + + const activityPage = new ActivityPage(userPage); + await activityPage.visit(); + + for (const newActivity of newActivities) { + await activityPage.createActivity(newActivity.name, newActivity.description, newActivity.type); + await expect(userPage.getByRole('cell', { name: newActivity.name }).last()).toBeVisible(); + } + }); + + test('[05] - Użytkownik dodaje uczestników do eksperymentu', async ({ userPage }) => { + await selectDataset(userPage); + await (new ExperimentListPage(userPage)).useExperimentByName(newExperiment.name); + + for (const newParticipant of newParticipants) { + const participantsTab = new ParticipantsTab(userPage); + await participantsTab.visit(); + await participantsTab.addParticipant(`${ newParticipant.surname } ${ newParticipant.name }`); + await expect(userPage.getByRole('cell', { name: newParticipant.name }).first()).toBeVisible(); + } + }); + + test('[06] - Użytkownik tworzy scenariusz testowy', async ({ userPage }) => { + await selectDataset(userPage); + await (new ExperimentListPage(userPage)).useExperimentByName(newExperiment.name); + + const scenariosTab = new ScenariosTab(userPage); + await scenariosTab.visit(); + await scenariosTab.addScenario(newScenario); + + await scenariosTab.visit(); + await expect(userPage.getByRole('button', { name: newScenario.name }).first()).toBeVisible(); + }); + + test('[07] - Użytkownik dodaje wykonania scenariusza', async ({ userPage }) => { + await selectDataset(userPage); + await (new ExperimentListPage(userPage)).useExperimentByName(newExperiment.name); + + for (const newScenarioExecution of newScenarioExecutions) { + const scenariosExecutionTab = new ScenariosExecutionsTab(userPage); + await scenariosExecutionTab.visit(); + await scenariosExecutionTab.addScenarioExecution(newScenarioExecution.scenarioName, newScenarioExecution.executionName); + await scenariosExecutionTab.visit(); + await expect(userPage.getByRole('button', { name: newScenarioExecution.executionName }).first()).toBeVisible(); + } + }); + + test('[08] - Użytkownik dodaje uczestników do wykonań scenariusza', async ({ userPage }) => { + await selectDataset(userPage); + await (new ExperimentListPage(userPage)).useExperimentByName(newExperiment.name); + + for (const scenarioExecution of newScenarioExecutions) { + let count = 1; + for (const newActivity of newActivities) { + for (const participant of newActivity.participants) { + const scenariosExecutionTab = new ScenariosExecutionsTab(userPage); + await scenariosExecutionTab.visit(); + await scenariosExecutionTab.addParticipantToScenariosActivity({ + participantName: participant, + activityName: `Activity Execution: ${ count }`, + scenarioExecutionName: scenarioExecution.executionName, + }); + } + count++; + } + } + }); + + test('[09] - Użytkownik tworzy nagrania', async ({ userPage }) => { + await selectDataset(userPage); + await (new ExperimentListPage(userPage)).useExperimentByName(newExperiment.name); + + for (const recording of newRecordings) { + const recordingsTab = new RecordingsTab(userPage); + await recordingsTab.visit(); + await recordingsTab.addRecording(recording); + } + }); + + test('[10] - Użytkownik tworzy szeregi czasowe', async ({ userPage }) => { + // TODO: przenieść testy z time-series.spec.ts + }); + + test('[11] - Inny użytkownik rejestruje konto', async ({ page }) => { + const registrationPage = new RegistrationPage(page); + await registrationPage.visit(); + await registrationPage.register(usernameOther, emailOther, emailOther, firstNameOther, lastNameOther); + }); + + test('[12] - Użytkownik dodaje uprawnienia dostępu do odczytu', async ({ userPage }) => { + await selectDataset(userPage); + await new SettingsPage(userPage).visit(); + const accessPermissionsTab = new AccessPermissionsTab(userPage); + await accessPermissionsTab.visit(); + await accessPermissionsTab.addPermission(usernameOther, 'Reader'); + }); + + test('[13] - Inny użytkonik [Reader] nie może tworzyć eksperymentu', async ({ otherUserPage }) => { + await selectDataset(otherUserPage); + + const experimentPage = new ExperimentPage(otherUserPage); + await experimentPage.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(0); + }); + + test('[14] - Inny użytkonik [Reader] nie może edytować składowych eksperymentu', async ({ otherUserPage }) => { + + await selectDataset(otherUserPage); + + await (new ExperimentListPage(otherUserPage)).useExperimentByName(newExperiment.name); + + const participantsTab = new ParticipantsTab(otherUserPage); + await participantsTab.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Add' }).first()).toHaveCount(0); + + const scenariosTab = new ScenariosTab(otherUserPage); + await scenariosTab.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(0); + + const scenariosExecutionTab = new ScenariosExecutionsTab(otherUserPage); + await scenariosExecutionTab.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(0); + + const recordingsTab = new RecordingsTab(otherUserPage); + await recordingsTab.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(0); + }); + + test('[15] - Inny użytkonik [Reader] nie może tworzyć aktywności', async ({ otherUserPage }) => { + await selectDataset(otherUserPage); + + const activityPage = new ActivityPage(otherUserPage); + await activityPage.visit(); + await otherUserPage.waitForTimeout(3000); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(0); + }); + + test('[16] - Inny użytkonik [Reader] nie może tworzyć uczestników', async ({ otherUserPage }) => { + await selectDataset(otherUserPage); + + const measurePage = new MeasurePage(otherUserPage); + await measurePage.visit(); + await otherUserPage.waitForTimeout(3000); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(0); + }); + + test('[17] - Inny użytkonik [Reader] nie może odwiedzić ustawień', async ({ otherUserPage }) => { + await selectDataset(otherUserPage); + + const settingsPage = new SettingsPage(otherUserPage); + await settingsPage.visit(); + await expect(otherUserPage).toHaveURL(/.*\/access-denied/); + }); + + test('[18] - Użytkownik dodaje uprawnienia dostępu do edycji', async ({ userPage }) => { + await selectDataset(userPage); + await new SettingsPage(userPage).visit(); + const accessPermissionsTab = new AccessPermissionsTab(userPage); + await accessPermissionsTab.visit(); + await accessPermissionsTab.addPermission(usernameOther, 'Editor'); + }); + + test('[19] - Inny użytkonik [Editor] może tworzyć eksperyment', async ({ otherUserPage }) => { + await selectDataset(otherUserPage); + + const experimentPage = new ExperimentPage(otherUserPage); + await experimentPage.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(0); + }); + + test('[20] - Inny użytkonik [Editor] może edytować składowe eksperymentu', async ({ otherUserPage }) => { + + await selectDataset(otherUserPage); + + await (new ExperimentListPage(otherUserPage)).useExperimentByName(newExperiment.name); + + const participantsTab = new ParticipantsTab(otherUserPage); + await participantsTab.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Add' }).first()).toHaveCount(1); + + const scenariosTab = new ScenariosTab(otherUserPage); + await scenariosTab.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(1); + + const scenariosExecutionTab = new ScenariosExecutionsTab(otherUserPage); + await scenariosExecutionTab.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(1); + + const recordingsTab = new RecordingsTab(otherUserPage); + await recordingsTab.visit(); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(1); + }); + + test('[21] - Inny użytkonik [Editor] może tworzyć aktywności', async ({ otherUserPage }) => { + await selectDataset(otherUserPage); + + const activityPage = new ActivityPage(otherUserPage); + await activityPage.visit(); + await otherUserPage.waitForTimeout(3000); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(1); + }); + + test('[22] - Inny użytkonik [Editor] może tworzyć uczestników', async ({ otherUserPage }) => { + await selectDataset(otherUserPage); + + const measurePage = new MeasurePage(otherUserPage); + await measurePage.visit(); + await otherUserPage.waitForTimeout(3000); + await expect(otherUserPage.getByRole('Button', { name: 'Create' }).first()).toHaveCount(1); + }); + + test('[23] - Inny użytkonik [Editor] widzi ustawień', async ({ userPage }) => { + await selectDataset(userPage); + await expect(userPage.getByRole('link', { name: 'Settings' })).toHaveCount(1); + }); + +}); + +async function selectDataset(page: Page) { + const datasetPage = new DatasetListPage(page); + await datasetPage.visit() + await datasetPage.useDatasetByName(newDataset.name); +} diff --git a/e2e-tests/tests/register.spec.ts b/e2e-tests/tests/register.spec.ts new file mode 100644 index 0000000..d8b4107 --- /dev/null +++ b/e2e-tests/tests/register.spec.ts @@ -0,0 +1,10 @@ +import { test } from '@playwright/test'; +import { RegistrationPage } from '../pages/RegistrationPage'; +import generateRandomEmail from '../utils/generate-random-email'; + +test('Użytkownik może się zarejestrować', async ({ page }) => { + const username = generateRandomEmail(); + const registrationPage = new RegistrationPage(page); + await registrationPage.visit(); + await registrationPage.register(username, username, username, username, username); +}); diff --git a/e2e-tests/tests/scenario-execution.spec.ts b/e2e-tests/tests/scenario-execution.spec.ts new file mode 100644 index 0000000..98a4853 --- /dev/null +++ b/e2e-tests/tests/scenario-execution.spec.ts @@ -0,0 +1,27 @@ +import {expect, test} from "@playwright/test"; +import {LoginPage} from "../pages/LoginPage"; +import {DatasetListPage} from "../pages/DatasetListPage"; +import {ExperimentPage} from "../pages/ExperimentPage"; +import {ParticipantsTab} from "../pages/ParticipantsTab"; +import {ScenariosTab} from "../pages/ScenariosTab"; +import {ScenariosExecutionsTab} from "../pages/ScenariosExecutionsTab"; + +test.beforeEach(async ({ page })=> { + await new LoginPage(page).loggedInAsDefaultUser(); + await new DatasetListPage(page).usingAnyDataset(); + await new ExperimentPage(page).useAnyExperiment(); +}); + +test('Użytkownik może dodać wykonanie scenariusza', async ({ page }) => { + + + const scenariosExecutionTab = new ScenariosExecutionsTab(page); + await scenariosExecutionTab.visit(); + await scenariosExecutionTab.addScenarioExecution( + 'Online Learning Session Emotional Response', + 'ex-01' + ); + + await scenariosExecutionTab.visit(); + await expect(page.getByRole('button', { name: 'ex-01' }).first()).toBeVisible(); +}); diff --git a/e2e-tests/tests/scenario-participant.spec.ts b/e2e-tests/tests/scenario-participant.spec.ts new file mode 100644 index 0000000..de43be8 --- /dev/null +++ b/e2e-tests/tests/scenario-participant.spec.ts @@ -0,0 +1,29 @@ +import {expect, test} from "@playwright/test"; +import {LoginPage} from "../pages/LoginPage"; +import {DatasetListPage} from "../pages/DatasetListPage"; +import {ExperimentPage} from "../pages/ExperimentPage"; +import {ParticipantsTab} from "../pages/ParticipantsTab"; +import {ScenariosTab} from "../pages/ScenariosTab"; +import {ScenariosExecutionsTab} from "../pages/ScenariosExecutionsTab"; + +test.beforeEach(async ({page}) => { + await new LoginPage(page).loggedInAsDefaultUser(); + await new DatasetListPage(page).usingAnyDataset(); + await new ExperimentPage(page).useAnyExperiment(); +}); + +test('Użytkownik może dodać uczestnika do wykonania scenariusza', async ({page}) => { + + + const scenariosExecutionTab = new ScenariosExecutionsTab(page); + await scenariosExecutionTab.visit(); + + await page.getByRole('button', {name: 'ex-01'}).first().click(); + const editButton = await page.locator('[data-testid^="edit-activity-execution"]').first(); + + + await scenariosExecutionTab.addParticipantJamesToScenario(editButton); + await scenariosExecutionTab.visit(); + await page.getByRole('button', {name: 'ex-01'}).first().click(); + +}); diff --git a/e2e-tests/tests/scenario.spec.ts b/e2e-tests/tests/scenario.spec.ts new file mode 100644 index 0000000..10de994 --- /dev/null +++ b/e2e-tests/tests/scenario.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from '@playwright/test'; +import { LoginPage } from '../pages/LoginPage'; +import { DatasetListPage } from '../pages/DatasetListPage'; +import { ExperimentPage } from '../pages/ExperimentPage'; +import { ParticipantsTab } from '../pages/ParticipantsTab'; +import { ScenariosTab } from '../pages/ScenariosTab'; + +test.beforeEach(async ({ page }) => { + await new LoginPage(page).loggedInAsDefaultUser(); + await new DatasetListPage(page).usingAnyDataset(); + await new ExperimentPage(page).useAnyExperiment(); +}); + +test('Użytkownik może dodać scenariusz do eksperymentu', async ({ page }) => { + + const scenariosTab = new ScenariosTab(page); + await scenariosTab.visit(); + await scenariosTab.addScenario({ + name: 'Online Learning Session Emotional Response', + description: 'This scenario tracks a user\'s emotional response throughout an online learning session.', + activities: ['Answering Initial Metric Questions'], + }); + + await scenariosTab.visit(); + await expect(page.getByRole('button', { name: 'Online Learning Session Emotional Response' }).first()).toBeVisible(); +}); diff --git a/e2e-tests/tests/time-series.spec.ts b/e2e-tests/tests/time-series.spec.ts new file mode 100644 index 0000000..596ef2a --- /dev/null +++ b/e2e-tests/tests/time-series.spec.ts @@ -0,0 +1,108 @@ +import { test, expect } from '@playwright/test'; +import { LoginPage } from '../pages/LoginPage'; +import { DatasetListPage } from '../pages/DatasetListPage'; +import { TimeSeriesCreatePage } from '../pages/TimeSeriesCreatePage'; +import { ExperimentListPage } from '../pages/ExperimentListPage'; + +test.beforeEach(async ({ page }) => { + await new LoginPage(page).loggedInAsDefaultUser(); + await new DatasetListPage(page).usingAnyDataset(); + await new ExperimentListPage(page).useExperimentByName('Nyssa Rivas'); +}); + +test('Użytkownik może utworzyć nowy szeregi czasowe', async ({ page }) => { + const activityData = [ + { + link: 'https://www.test2.com', + type: 'Epoch', + spacing: 'Regular', + measure: 'Sadness', + recording: 'Audio Recording 1', + channel: 'Audio', + modality: 'Prosody of speech', + liveActivity: 'Sound', + }, + // { + // link: 'https://www.test2.com', + // type: 'Timestamp', + // spacing: 'Irregular', + // measure: 'Surprise', + // recording: 'BVP Recording 2', + // channel: 'BVP', + // modality: 'Heart rate', + // liveActivity: 'Heart activity', + // }, + // { + // link: 'https://www.test2.com', + // type: 'Epoch', + // spacing: 'Regular', + // measure: 'Neutral state', + // recording: 'ECG Recording 3', + // channel: 'ECG', + // modality: 'HRV', + // liveActivity: 'Heart activity', + // }, + // { + // link: 'https://www.test2.com', + // type: 'Timestamp', + // spacing: 'Irregular', + // measure: 'Dominance', + // recording: 'EDA Recording 4', + // channel: 'EDA', + // modality: 'Skin conductance', + // liveActivity: 'Perspiration', + // }, + // { + // link: 'https://www.test2.com', + // type: 'Epoch', + // spacing: 'Regular', + // measure: 'Arousal', + // recording: 'Depth Video 5', + // channel: 'Depth video', + // modality: 'Muscle tension', + // liveActivity: 'Muscles activity', + // }, + // { + // link: 'https://www.test2.com', + // type: 'Timestamp', + // spacing: 'Irregular', + // measure: 'Valence', + // recording: 'RGB Video 6', + // channel: 'RGB video', + // modality: 'RESP intensity', + // liveActivity: 'Respiration', + // }, + // { + // link: 'https://www.test2.com', + // type: 'Epoch', + // spacing: 'Regular', + // measure: 'Familiarity Liking', + // recording: 'Group EEG Recording 7', + // channel: 'EEG', + // modality: 'Neural activity', + // liveActivity: 'Brain activity', + // }, + // { + // link: 'https://www.test2.com', + // type: 'Timestamp', + // spacing: 'Irregular', + // measure: 'Anger', + // recording: 'Temperature Recording 8', + // channel: 'Temperature', + // modality: 'Head movement', + // liveActivity: 'Movement', + // }, + ]; + + for (const timeSeriesData of activityData) { + const timeSeriesCreatePage = new TimeSeriesCreatePage(page); + await timeSeriesCreatePage.visit(); + await timeSeriesCreatePage.fillForm(timeSeriesData); + await timeSeriesCreatePage.submitForm(); + + await expect(page.getByRole('cell', { name: timeSeriesData.measure }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: timeSeriesData.link }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: timeSeriesData.type }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: timeSeriesData.spacing }).first()).toBeVisible(); + } +}); diff --git a/e2e-tests/utils/generate-date-util.ts b/e2e-tests/utils/generate-date-util.ts new file mode 100644 index 0000000..7e76677 --- /dev/null +++ b/e2e-tests/utils/generate-date-util.ts @@ -0,0 +1,5 @@ +export default (dayOfMonth: number): string => { + const day = new Date(); + day.setDate(dayOfMonth) + return day.toISOString().split("T")[0] +}; diff --git a/e2e-tests/utils/generate-random-email.ts b/e2e-tests/utils/generate-random-email.ts new file mode 100644 index 0000000..7fcffb0 --- /dev/null +++ b/e2e-tests/utils/generate-random-email.ts @@ -0,0 +1,4 @@ +export default (domain: string = 'example.com'): string => { + const timestamp = Date.now().toString().slice(-6); + return `user-${ timestamp }@${ domain }`; +}; diff --git a/entrypoint.sh b/entrypoint.sh index d6f8e72..0b790e2 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -8,9 +8,11 @@ echo "Replacing env constants in JS" for file in $ROOT_DIR/js/*.js $ROOT_DIR/index.html; do echo "Processing $file ..."; - - sed -i 's|process.env.VUE_APP_API_URL|'"${VUE_APP_API_URL}"'|g' $file - sed -i 's|process.env.VUE_APP_AUTH_MS_URL|'"${VUE_APP_AUTH_MS_URL}"'|g' $file + sed -i 's|VUE_APP_API_URL|'"${VUE_APP_API_URL}"'|g' $file + sed -i 's|VUE_APP_AUTH_MS_URL|'"${VUE_APP_AUTH_MS_URL}"'|g' $file + sed -i 's|VUE_APP_KEYCLOAK_URL|'"${VUE_APP_KEYCLOAK_URL}"'|g' $file + sed -i 's|VUE_APP_KEYCLOAK_CLIENT_ID|'"${VUE_APP_KEYCLOAK_CLIENT_ID}"'|g' $file + sed -i 's|VUE_APP_KEYCLOAK_REALM|'"${VUE_APP_KEYCLOAK_REALM}"'|g' $file done echo "Finished replacing" diff --git a/package-lock.json b/package-lock.json index 1b6fc85..5ba45aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "axios": "^1.7.7", "core-js": "^3.8.3", "date-fns": "^2.30.0", - "jwt-decode": "^3.1.2", + "keycloak-js": "^26.0.6", "vue": "^2.6.14", "vue-router": "^3.5.1", "vuetify": "^2.6.0", @@ -21,6 +21,8 @@ "devDependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", + "@playwright/test": "^1.49.1", + "@types/node": "^22.10.5", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0", @@ -1960,6 +1962,21 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "dev": true, + "dependencies": { + "playwright": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -2208,10 +2225,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", - "dev": true + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -7207,10 +7227,11 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jwt-decode": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", - "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + "node_modules/keycloak-js": { + "version": "26.0.6", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.0.6.tgz", + "integrity": "sha512-HHRuMOm5w4vAkhWyqs7VPQqYUCSyPagTdQcUIi7xF6XU9GoSZgh2jRtry5P4t6XxhyuMiCwWg7JN8W9cDyqRfQ==", + "license": "Apache-2.0" }, "node_modules/kind-of": { "version": "6.0.3", @@ -8581,6 +8602,36 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dev": true, + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -10906,6 +10957,12 @@ "node": ">= 0.6" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", diff --git a/package.json b/package.json index 7b7060d..4701dd1 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,14 @@ "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", - "lint": "vue-cli-service lint" + "lint": "vue-cli-service lint", + "test": "npx playwright test" }, "dependencies": { "axios": "^1.7.7", "core-js": "^3.8.3", "date-fns": "^2.30.0", - "jwt-decode": "^3.1.2", + "keycloak-js": "^26.0.6", "vue": "^2.6.14", "vue-router": "^3.5.1", "vuetify": "^2.6.0", @@ -21,6 +22,8 @@ "devDependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", + "@playwright/test": "^1.49.1", + "@types/node": "^22.10.5", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..68b91cd --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,80 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e-tests/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', + timeout: 30 * 1000, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost: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'] }, + // }, + // + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..0ce24f2 --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +echo "Running tests with args: $@" + +docker compose -f docker-compose.tests.yaml run --rm playwright-tests "$@" diff --git a/src/api/BaseAPI2.js b/src/api/BaseAPI2.js index c61e0db..8e7d8ca 100644 --- a/src/api/BaseAPI2.js +++ b/src/api/BaseAPI2.js @@ -1,15 +1,32 @@ import axios from 'axios'; import config from '../../config.js'; import Vue from 'vue'; +import AuthService from '@/services/AuthService'; export const apiService = axios.create({ baseURL: config.apiUrl, headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); +apiService.interceptors.request.use( + async config => { + try { + const token = await AuthService.getToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + } catch (error) { + console.error(error); + } + return config; + }, + error => { + return Promise.reject(error); + }, +); + export default class BaseAPI2 { static getBasePath() { return ''; @@ -19,15 +36,18 @@ export default class BaseAPI2 { return `dataset_id=${Vue.prototype.$store.state?.dataset?.id || null}`; } - static getReturnValues(){ + + static getReturnValues() { return this.getBasePath(); } - static dTOFrontToAPI(data){ + + static dTOFrontToAPI(data) { return {}; } - static dTOAPIToFront(data){ + + static dTOAPIToFront(data) { return {}; } @@ -40,12 +60,13 @@ export default class BaseAPI2 { } static show(id, depth = 0) { - if(!id) + if (!id) { return {}; + } - return apiService.get(`/${this.getBasePath()}/${id}?${this.getDatasetName()}&depth=${depth}`).then(({ data }) => { - data = this.dTOAPIToFront(data); - return { data }; + return apiService.get(`/${ this.getBasePath() }/${ id }?${ this.getDatasetName() }&depth=${ depth }`).then(({ data }) => { + data = this.dTOAPIToFront(data); + return { data }; }); } diff --git a/src/api/TimeSeriesApi.js b/src/api/TimeSeriesApi.js index 1b36fc4..610ffc4 100644 --- a/src/api/TimeSeriesApi.js +++ b/src/api/TimeSeriesApi.js @@ -8,11 +8,13 @@ export default class extends BaseAPI2 { return DatabaseName.TIME_SERIES; } - static getReturnValues(){ + + static getReturnValues() { return 'time_series_nodes'; } - static dTOFrontToAPI(data){ + + static dTOFrontToAPI(data) { return { observable_information_ids: data.observableInformationIds, measure_id: data.measure.id, @@ -28,7 +30,8 @@ export default class extends BaseAPI2 { }; } - static dTOAPIToFront(data){ + + static dTOAPIToFront(data) { return { id: data.id, observableInformationIds: data.observable_information_ids, @@ -45,30 +48,33 @@ export default class extends BaseAPI2 { }; } - static index(activityExecutionId, participantId){ + + static index(activityExecutionId, participantId) { return super.index().then(({ data }) => { - return Promise.all(data.map(timeSeries => + return Promise.all(data.map(timeSeries => this.show(timeSeries.id, 4), )).then(multipleTimeSeries => { multipleTimeSeries = multipleTimeSeries.map(e => e.data); - multipleTimeSeries = multipleTimeSeries.filter(timeSeries => timeSeries.observableInformations[0].recording.participation.participant_state.participant_id === participantId && timeSeries.observableInformations[0].recording.participation.activity_execution_id === activityExecutionId); + multipleTimeSeries = multipleTimeSeries.filter(timeSeries => timeSeries.observableInformations?.[0].recording.participation.participant_state.participant_id === participantId && timeSeries.observableInformations[0].recording.participation.activity_execution_id === activityExecutionId); return { data: multipleTimeSeries }; }); }); } - + + static store(data) { return Promise.all(data.observableInformations .map(observableInformation => ObservableInformationsAPI.store(observableInformation), )).then((observableInformations) => { - data.observableInformationIds = observableInformations.map(item => item.data.id); - return super.store(data); - }); + data.observableInformationIds = observableInformations.map(item => item.data.id); + return super.store(data); + }); } + static update(data) { - + return this.show(data.id, 4).then(oldTimeSeries => { oldTimeSeries = oldTimeSeries.data; @@ -81,11 +87,11 @@ export default class extends BaseAPI2 { const observableInformationsToUpdate = newObservableInformations.filter(e => oldObservableInformations.some(oldObservableInformation => oldObservableInformation.id === e.id)) .map(observableInformation => ObservableInformationsAPI.update(observableInformation)); - return Promise.all([...observableInformationsToDelete, ...observableInformationsToAdd, ...observableInformationsToUpdate]).then(async(results) => { + return Promise.all([...observableInformationsToDelete, ...observableInformationsToAdd, ...observableInformationsToUpdate]).then(async (results) => { data.observableInformationIds = results.slice(observableInformationsToDelete.length).map(e => e.data.id); const updatedValues = super.update(data); - const updatedRelations = apiService.put(`/${this.getBasePath()}/${data.id}/relationships?${this.getDatasetName()}`, this.dTOFrontToAPI(data)); + const updatedRelations = apiService.put(`/${ this.getBasePath() }/${ data.id }/relationships?${ this.getDatasetName() }`, this.dTOFrontToAPI(data)); await updatedValues; await updatedRelations; return updatedRelations; @@ -93,5 +99,5 @@ export default class extends BaseAPI2 { }); } - + } \ No newline at end of file diff --git a/src/components/ActivityExecutionsListingComponent.vue b/src/components/ActivityExecutionsListingComponent.vue index 52ea4e1..415749b 100644 --- a/src/components/ActivityExecutionsListingComponent.vue +++ b/src/components/ActivityExecutionsListingComponent.vue @@ -4,6 +4,11 @@ :items="activityExecutions" :show-expand="true" > + - - - - - + + \ No newline at end of file diff --git a/src/components/AddExistingParticipantDialog.vue b/src/components/AddExistingParticipantDialog.vue index ffe1494..20f8653 100644 --- a/src/components/AddExistingParticipantDialog.vue +++ b/src/components/AddExistingParticipantDialog.vue @@ -4,11 +4,12 @@ v-model="dialog" max-width="500px" > -