diff --git a/.gitignore b/.gitignore index 5f4df347..a867b674 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ yarn-error.log node_modules go.work go.work.sum + +playwright-results +test-results \ No newline at end of file diff --git a/e2e-tests/package-lock.json b/e2e-tests/package-lock.json new file mode 100644 index 00000000..6530fdc5 --- /dev/null +++ b/e2e-tests/package-lock.json @@ -0,0 +1,110 @@ +{ + "name": "e2e_tests", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "e2e_tests", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.28.0", + "typescript": "^4.9.3", + "uuid": "^9.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz", + "integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.28.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "node_modules/playwright-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", + "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/typescript": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + } + }, + "dependencies": { + "@playwright/test": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.0.tgz", + "integrity": "sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.28.0" + } + }, + "@types/node": { + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "dev": true + }, + "playwright-core": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.0.tgz", + "integrity": "sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==", + "dev": true + }, + "typescript": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", + "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "dev": true + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true + } + } +} diff --git a/e2e-tests/package.json b/e2e-tests/package.json new file mode 100644 index 00000000..092c0b62 --- /dev/null +++ b/e2e-tests/package.json @@ -0,0 +1,17 @@ +{ + "name": "e2e_tests", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test:e2e": "playwright test --trace on" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.28.0", + "typescript": "^4.9.3", + "uuid": "^9.0.0" + } +} diff --git a/e2e-tests/page-objects/BasePageObject.ts b/e2e-tests/page-objects/BasePageObject.ts new file mode 100644 index 00000000..6b21919f --- /dev/null +++ b/e2e-tests/page-objects/BasePageObject.ts @@ -0,0 +1,24 @@ +import { test, expect, type Page } from '@playwright/test'; + +export type BasePageObjectConstructor = { + page: Page; + baseUrl?: string; +} + +export default class BasePageObject { + public page: Page; + public baseUrl: string; + + constructor({ page, baseUrl }: BasePageObjectConstructor) { + this.baseUrl = baseUrl ?? ''; + this.page = page; + } + + // Override this method if needed + public async open() { + if (this.baseUrl) { + this.page.goto(this.baseUrl); + } + } + +} diff --git a/e2e-tests/page-objects/admin/AdminPage.ts b/e2e-tests/page-objects/admin/AdminPage.ts new file mode 100644 index 00000000..690c3987 --- /dev/null +++ b/e2e-tests/page-objects/admin/AdminPage.ts @@ -0,0 +1,28 @@ +import { expect } from "@playwright/test"; +import BasePageObject, { BasePageObjectConstructor } from "../BasePageObject"; +import BatchesPage from "./BatchesPage"; + + + + +export default class AdminPage extends BasePageObject { + private batchesPage: BatchesPage; + + constructor({ page, baseUrl }: BasePageObjectConstructor) { + super({ page, baseUrl }); + + this.batchesPage = new BatchesPage({ page }); + } + + public async open() { + await this.page.goto(`${this.baseUrl}/admin`) + + const leftPanel = await this.page.locator('[aria-label="Sidebar"]'); + + await expect(leftPanel).toBeVisible() + } + + public getBatchesPage() { + return this.batchesPage; + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/admin/BatchesPage.ts b/e2e-tests/page-objects/admin/BatchesPage.ts new file mode 100644 index 00000000..c66572a4 --- /dev/null +++ b/e2e-tests/page-objects/admin/BatchesPage.ts @@ -0,0 +1,71 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + +export enum GamesTypeTreatment { + 'Solo' = 'Solo', + 'TwoPlayers' = 'Two Players', +} + + +export default class BatchesPage extends BasePageObject { + private getBatchesLinkInSidebar() { + return this.page.locator('[data-test="batchesSidebarButton"]'); + } + private getNewBatchButton() { + return this.page.locator('[data-test="newBatchButton"]'); + } + + private getTreatmentsSelect() { + return this.page.locator('[data-test="treatmentSelect"]'); + } + + private getCreateBatchButton() { + return this.page.locator('[data-test="createBatchButton"]'); + } + + private getGameBatchLine() { + return this.page.locator('[data-test="batchLine"]'); + } + + private getStartGameButton() { + return this.page.locator('[data-test="startButton"]'); + } + + public async open() { + const batchesSidebarButton = await this.getBatchesLinkInSidebar(); + + await batchesSidebarButton.click(); + + const newBatchButton = await this.getNewBatchButton(); + + await expect(newBatchButton).toBeVisible() + } + + public async createBatch({ mode, gamesCount }: { mode: GamesTypeTreatment, gamesCount: number}) { + const newBatchButton = await this.getNewBatchButton(); + + await newBatchButton.click(); + + const treatmentsSelect = await this.getTreatmentsSelect(); + + await treatmentsSelect.selectOption( + '[object Object]' + ); // TODO: fix displayed value in the app + + const createBatchButton = await this.getCreateBatchButton(); + + await createBatchButton.click(); + } + + public async startGame() { + const batchLine = await this.getGameBatchLine(); + + await expect(batchLine).toBeVisible(); + + const startGameButton = await this.getStartGameButton(); + + await startGameButton.click(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/admin/ExportPage.ts b/e2e-tests/page-objects/admin/ExportPage.ts new file mode 100644 index 00000000..5e12d47c --- /dev/null +++ b/e2e-tests/page-objects/admin/ExportPage.ts @@ -0,0 +1,19 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class ExportPage extends BasePageObject { + public async open() { + await this.page.goto(`${this.baseUrl}/admin`) + + const leftPanel = await this.page.locator('[aria-label="Sidebar"]'); + + await expect(leftPanel).toBeVisible() + } + + public async export() { + // TODO: implement export functionality + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/admin/PlayersPage.ts b/e2e-tests/page-objects/admin/PlayersPage.ts new file mode 100644 index 00000000..80cb5038 --- /dev/null +++ b/e2e-tests/page-objects/admin/PlayersPage.ts @@ -0,0 +1,15 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class PlayersPage extends BasePageObject { + public async open() { + await this.page.goto(`${this.baseUrl}/admin`) + + const leftPanel = await this.page.locator('[aria-label="Sidebar"]'); + + await expect(leftPanel).toBeVisible() + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/admin/TreatmentsPage.ts b/e2e-tests/page-objects/admin/TreatmentsPage.ts new file mode 100644 index 00000000..825af5a5 --- /dev/null +++ b/e2e-tests/page-objects/admin/TreatmentsPage.ts @@ -0,0 +1,15 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class TreatmentsPage extends BasePageObject { + public async open() { + await this.page.goto(`${this.baseUrl}/admin`) + + const leftPanel = await this.page.locator('[aria-label="Sidebar"]'); + + await expect(leftPanel).toBeVisible() + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/ConsentElement.ts b/e2e-tests/page-objects/main/ConsentElement.ts new file mode 100644 index 00000000..a29b6f79 --- /dev/null +++ b/e2e-tests/page-objects/main/ConsentElement.ts @@ -0,0 +1,19 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class ConsentElement extends BasePageObject { + getAcceptConsentButton() { + return this.page.locator('button[type="button"]'); // TODO: add test id! + } + + public async acceptConsent() { + const acceptConsentButton = await this.getAcceptConsentButton(); + + await expect(acceptConsentButton).toBeVisible(); + + await acceptConsentButton.click(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/ExitSurveyElement.ts b/e2e-tests/page-objects/main/ExitSurveyElement.ts new file mode 100644 index 00000000..a0d26da5 --- /dev/null +++ b/e2e-tests/page-objects/main/ExitSurveyElement.ts @@ -0,0 +1,45 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class ExitSurveyElement extends BasePageObject { + getBonusTitleElement() { + return this.page.getByText('Bonus'); + } + + getAgeInput() { + return this.page.locator('[id="age"]'); + } + + getGenderInput() { + return this.page.locator('[id="gender"]'); + } + + getEducationInput() { + return this.page.locator('[name="education"]'); + } + + getSubmitButton() { + return this.page.locator('button[type="submit"]'); + } + + public async fillSurvey({ age, gender }: { age: number, gender: string }) { + const ageInput = await this.getAgeInput(); + + await expect(ageInput).toBeVisible(); + + ageInput.fill(age.toString()); + + const genderInput = await this.getGenderInput(); + + await expect(genderInput).toBeVisible(); + + genderInput.fill(gender); + + const submitButton = await this.getSubmitButton(); + + await submitButton.click(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/ExperimentPage.ts b/e2e-tests/page-objects/main/ExperimentPage.ts new file mode 100644 index 00000000..262813c8 --- /dev/null +++ b/e2e-tests/page-objects/main/ExperimentPage.ts @@ -0,0 +1,72 @@ +import { expect } from "@playwright/test"; +import BasePageObject, { BasePageObjectConstructor } from "../BasePageObject"; +import NoExperimentsElement from "./NoExperimentsElement"; +import LoginPage from "./LoginPage"; +import InstructionsElement from "./InstructionElement"; +import JellyBeansGameElement from "./JellyBeansGameElement"; +import FinishedElement from "./FinishedElement"; +import ExitSurveyElement from "./ExitSurveyElement"; +import ConsentElement from "./ConsentElement"; +import MinesweeperGameElement from "./MinesweeperGameElement"; + + +export default class ExperimentPage extends BasePageObject { + private noExperimentsElement: NoExperimentsElement + private loginPage: LoginPage; + private jellyBeansGame: JellyBeansGameElement; + private exitSurveyElement: ExitSurveyElement; + private instructionsElement: InstructionsElement; + private minesweeperGameElement: MinesweeperGameElement; + private consentElement: ConsentElement; + private finishedElement: FinishedElement; + + constructor({ page, baseUrl }: BasePageObjectConstructor) { + super({ page, baseUrl }); + + this.loginPage = new LoginPage({ page }); + this.noExperimentsElement = new NoExperimentsElement({ page }); + this.consentElement = new ConsentElement({ page }); + this.instructionsElement = new InstructionsElement({ page }); + this.exitSurveyElement = new ExitSurveyElement({ page }); + this.jellyBeansGame = new JellyBeansGameElement({ page }); + this.minesweeperGameElement = new MinesweeperGameElement({ page }); + this.finishedElement = new FinishedElement({ page }); + } + + public async open() { + await this.page.goto(`${this.baseUrl}`) + } + + public async passInstructions() { + await this.instructionsElement.gotoNextPage() + } + + public async login({ playerId} : { playerId: string}) { + await this.loginPage.login({ playerId }); + } + + public async playJellyBeanGame({ count}: { count: number}) { + await this.jellyBeansGame.selectJellyBeansCount(count); + await this.jellyBeansGame.submitResult(); + await this.jellyBeansGame.finishGame(); + } + + public async playMinesweeper() { + const fieldNumber = 0; + + await this.minesweeperGameElement.openMinefieldElement(fieldNumber); + await this.minesweeperGameElement.finishGame(); + } + + public async acceptConsent() { + await this.consentElement.acceptConsent(); + } + + public async fillExitSurvey({ age, gender} : { age: number, gender: string}) { + await this.exitSurveyElement.fillSurvey({ age, gender }); + } + + public async checkIfFinished() { + await this.finishedElement.checkIfVisible(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/FinishedElement.ts b/e2e-tests/page-objects/main/FinishedElement.ts new file mode 100644 index 00000000..389573a4 --- /dev/null +++ b/e2e-tests/page-objects/main/FinishedElement.ts @@ -0,0 +1,15 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + +export default class FinishedElement extends BasePageObject { + getFinishedText() { + return this.page.getByText('Finished'); // TODO: add test id + } + + public async checkIfVisible() { + const finishedText = await this.getFinishedText(); + + await expect(finishedText).toBeVisible(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/InstructionElement.ts b/e2e-tests/page-objects/main/InstructionElement.ts new file mode 100644 index 00000000..fbf58a9c --- /dev/null +++ b/e2e-tests/page-objects/main/InstructionElement.ts @@ -0,0 +1,19 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class InstructionsElement extends BasePageObject { + getNextButtonElement() { + return this.page.getByText('Next'); // TODO: add test id + } + + public async gotoNextPage() { + const enterButton = await this.getNextButtonElement(); + + await expect(enterButton).toBeVisible(); + + await enterButton.click(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/JellyBeansGameElement.ts b/e2e-tests/page-objects/main/JellyBeansGameElement.ts new file mode 100644 index 00000000..39320934 --- /dev/null +++ b/e2e-tests/page-objects/main/JellyBeansGameElement.ts @@ -0,0 +1,51 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class JellyBeansGameElement extends BasePageObject { + getTitleElement() { + return this.page.getByText('Round 1 - Jelly Beans'); // TODO: add test id + } + + getSubmitButton() { + return this.page.locator('button[type="button"]'); // TODO: add test id + } + + getCountsSlider() { + return this.page.locator('input[type="range"]'); // TODO: add test id + } + + public async selectJellyBeansCount(count: number) { + const countSlider = await this.getCountsSlider(); + + await expect(countSlider).toBeVisible(); + + await countSlider.fill(count.toString()); + } + + public async checkSubmittedCount() { + const submitButton = await this.getSubmitButton(); + + await expect(submitButton).toBeVisible(); + + await submitButton.click(); + } + + public async submitResult() { + const submitButton = await this.getSubmitButton(); + + await expect(submitButton).toBeVisible(); + + await submitButton.click(); + } + + public async finishGame() { + const submitButton = await this.getSubmitButton(); + + await expect(submitButton).toBeVisible(); + + await submitButton.click(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/LoginPage.ts b/e2e-tests/page-objects/main/LoginPage.ts new file mode 100644 index 00000000..3a30b27e --- /dev/null +++ b/e2e-tests/page-objects/main/LoginPage.ts @@ -0,0 +1,27 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class LoginPage extends BasePageObject { + getLoginElement() { + return this.page.locator('[id="playerID"]'); // TODO: add test id + } + + getEnterButtonElement() { + return this.page.locator('button[type="submit"]'); // TODO: add test id + } + + public async login({ playerId }: {playerId: string}) { + const loginInput = await this.getLoginElement(); + + await expect(loginInput).toBeVisible(); + + loginInput.fill(playerId); + + const enterButton = await this.getEnterButtonElement(); + + await enterButton.click(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/MinesweeperGameElement.ts b/e2e-tests/page-objects/main/MinesweeperGameElement.ts new file mode 100644 index 00000000..24415c42 --- /dev/null +++ b/e2e-tests/page-objects/main/MinesweeperGameElement.ts @@ -0,0 +1,43 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class MinesweeperGameElement extends BasePageObject { + getTitleElement() { + return this.page.getByText('Round 2 - Minesweeper'); + } + + getSubmitButton() { + return this.page.locator('button[type="button"]'); // TODO: add test id + } + + getMinefieldElement(number: number) { + return this.page.locator(`.h-full.w-full.flex >> nth=${number}`); // TODO: add test id! + } + + public async openMinefieldElement(number: number) { + const minefieldElement = await this.getMinefieldElement(number); + + await expect(minefieldElement).toBeVisible(); + + await minefieldElement.click(); + } + + public async checkState(number: number) { + const submitButton = await this.getMinefieldElement(number); + + await expect(submitButton).toBeVisible(); + + await submitButton.click(); + } + + public async finishGame() { + const submitButton = await this.getSubmitButton(); + + await expect(submitButton).toBeVisible(); + + await submitButton.click(); + } +} \ No newline at end of file diff --git a/e2e-tests/page-objects/main/NoExperimentsElement.ts b/e2e-tests/page-objects/main/NoExperimentsElement.ts new file mode 100644 index 00000000..f7fb7747 --- /dev/null +++ b/e2e-tests/page-objects/main/NoExperimentsElement.ts @@ -0,0 +1,11 @@ +import { expect } from "@playwright/test"; +import BasePageObject from "../BasePageObject"; + + + + +export default class NoExperimentsElement extends BasePageObject { + getElement() { + return this.page.getByText('No experiments available'); + } +} \ No newline at end of file diff --git a/e2e-tests/playwright.config.ts b/e2e-tests/playwright.config.ts new file mode 100644 index 00000000..0df7163a --- /dev/null +++ b/e2e-tests/playwright.config.ts @@ -0,0 +1,107 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 240 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 10000 + }, + /* 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: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* 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: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +export default config; diff --git a/e2e-tests/setup/EmpiricaTestFactory.ts b/e2e-tests/setup/EmpiricaTestFactory.ts new file mode 100644 index 00000000..ecfb42b7 --- /dev/null +++ b/e2e-tests/setup/EmpiricaTestFactory.ts @@ -0,0 +1,103 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import * as uuid from 'uuid'; +import * as childProcess from 'node:child_process'; + +const EMPIRICA_CMD = 'empirica' +const EMPIRICA_CONFIG_RELATIVE_PATH = path.join('.empirica', 'local'); + +type InstallMode = 'NPM' | 'CACHED'; + +type EmpiricaTestFactoryOptions = { + installMode: InstallMode +} + +export default class EmpiricaTestFactory { + private uniqueProjectId: string; + private projectDirName: string; + private installMode: InstallMode; + private empiricaProcess: childProcess.ChildProcess; + + constructor(options: EmpiricaTestFactoryOptions) { + this.uniqueProjectId = uuid.v4(); + this.projectDirName = `test-experiment-${this.uniqueProjectId}`; + this.installMode = options.installMode || 'NPM'; // TODO: implement caching the setup + + } + + public async init() { + + await this.createEmpiricaProject(); + await this.startEmpiricaProject(); + } + + async teardown() { + await this.stopEmpiricaProject(); + await this.fullCleanup(); + } + + async fullCleanup() { + await fs.rm(this.projectDirName, { recursive: true }); + } + + async removeConfigFolder() { + const configDir = path.join(this.projectDirName, EMPIRICA_CONFIG_RELATIVE_PATH); + + await fs.rm(configDir, { recursive: true }); + } + + private async createEmpiricaProject() { + return new Promise((resolve, reject) => { + const process = childProcess.spawn(EMPIRICA_CMD, ['create', this.projectDirName]); + + process.stdout.on('data', (data) => { + console.log(`${data}`); + }); + + process.stderr.on('data', (data) => { + console.error(`create project stderr: ${data}`); + }); + + process.on('close', (code) => { + + if (code === 0) { + resolve(true) + } else { + console.log(`"${EMPIRICA_CMD} create" process exited with code ${code}`); + + reject(code) + } + }); + }) + } + + private async startEmpiricaProject() { + return new Promise((resolve, reject) => { + this.empiricaProcess = childProcess.spawn(EMPIRICA_CMD, { cwd: this.projectDirName }); + + resolve(true) + + process.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + }); + + process.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + process.on('close', (code) => { + console.log(`"${EMPIRICA_CMD}" process exited with code ${code}`); + }); + }) + + } + + private async stopEmpiricaProject() { + return new Promise((resolve, reject) => { + this.empiricaProcess.kill() + + resolve(true); + }); + } + +} \ No newline at end of file diff --git a/e2e-tests/tests/basic.spec.ts b/e2e-tests/tests/basic.spec.ts new file mode 100644 index 00000000..89a42edb --- /dev/null +++ b/e2e-tests/tests/basic.spec.ts @@ -0,0 +1,98 @@ +import { test, expect } from '@playwright/test'; +import * as uuid from 'uuid'; +import ExperimentPage from '../page-objects/main/ExperimentPage'; +import AdminPage from '../page-objects/admin/AdminPage'; +import NoExperimentsElement from '../page-objects/main/NoExperimentsElement'; +import EmpiricaTestFactory from '../setup/EmpiricaTestFactory'; +import { GamesTypeTreatment } from '../page-objects/admin/BatchesPage'; + +const baseUrl = 'http://localhost:3000'; + +const testFactory = new EmpiricaTestFactory({ + installMode: 'NPM' // TODO: implement caching of the empirica project +}); + +test.beforeAll(async () => { + await testFactory.init(); +}); + +test.afterAll(async () => { + await testFactory.teardown(); +}); + + +test.describe('Empirica', () => { + + test('Empty experiemnt page loads successfully', async ({ page }) => { + const experimentPage = new ExperimentPage({ + page, + baseUrl + }) + + await experimentPage.open(); + + const noExperimentsElement = new NoExperimentsElement({ page }); + + await expect(await noExperimentsElement.getElement()).toBeVisible(); + + }); + + test('Admin page loads successfully', async ({ page }) => { + const adminPage = new AdminPage({ + page, + baseUrl + }) + + await adminPage.open(); + + }); + + test.only('creates batch with 1 game with one player, into view, player passes through the game', async ({ page }) => { + const adminPage = new AdminPage({ + page, + baseUrl + }); + + + const playerId = `player-${uuid.v4()}`; + const gamesCount = 1; + const gameMode = GamesTypeTreatment.Solo; + const playerAge = 25; + const playerGender = 'male'; + const jellyBeansCount = 1200; + + await adminPage.open(); + + const batchesPage = adminPage.getBatchesPage(); + + await batchesPage.open(); + + await batchesPage.createBatch({ + mode: gameMode, + gamesCount + }); + + await batchesPage.startGame(); + + const experimentPage = new ExperimentPage({ + page, + baseUrl + }) + + await experimentPage.open(); + + await experimentPage.acceptConsent(); + + await experimentPage.login({ playerId }); + + await experimentPage.passInstructions(); + + await experimentPage.playJellyBeanGame({ count: jellyBeansCount }); + + await experimentPage.playMinesweeper(); + + await experimentPage.fillExitSurvey({ age: playerAge, gender: playerGender }); + + await experimentPage.checkIfFinished() + }); +});