Merge branch 'test/e2e-setup' into dev
This commit is contained in:
commit
37d88cfd24
15 changed files with 634 additions and 2982 deletions
|
@ -12,7 +12,7 @@
|
|||
"format": "prettier --write src/",
|
||||
"format-check": "prettier --check src/",
|
||||
"lint": "eslint . --fix",
|
||||
"pretest:unit": "npm run build",
|
||||
"pretest:unit": "tsx ../docs/api/generate.ts && npm run build",
|
||||
"test:unit": "vitest --run"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -26,7 +26,6 @@
|
|||
"cross": "^1.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"dwengo-1-common": "^0.1.1",
|
||||
"express": "^5.0.1",
|
||||
"express-jwt": "^8.5.1",
|
||||
"gift-pegjs": "^1.0.2",
|
||||
|
|
|
@ -8,7 +8,7 @@ import { getGroupRepository } from '../data/repositories.js';
|
|||
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||
import { Class } from '../entities/classes/class.entity.js';
|
||||
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||
import { mapToClassDTO } from './class';
|
||||
import { mapToClassDTO } from './class.js';
|
||||
|
||||
export function mapToGroup(groupDto: GroupDTO, clazz: Class): Group {
|
||||
const assignmentDto = groupDto.assignment as AssignmentDTO;
|
||||
|
|
|
@ -2,9 +2,9 @@ import { Submission } from '../entities/assignments/submission.entity.js';
|
|||
import { mapToGroupDTO } from './group.js';
|
||||
import { mapToStudentDTO } from './student.js';
|
||||
import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/submission';
|
||||
import { getSubmissionRepository } from '../data/repositories';
|
||||
import { Student } from '../entities/users/student.entity';
|
||||
import { Group } from '../entities/assignments/group.entity';
|
||||
import { getSubmissionRepository } from '../data/repositories.js';
|
||||
import { Student } from '../entities/users/student.entity.js';
|
||||
import { Group } from '../entities/assignments/group.entity.js';
|
||||
|
||||
export function mapToSubmissionDTO(submission: Submission): SubmissionDTO {
|
||||
return {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer';
|
|||
import { mapToAssignment } from '../interfaces/assignment.js';
|
||||
import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||
import { fetchStudent } from './students.js';
|
||||
import { NotFoundException } from '../exceptions/not-found-exception';
|
||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||
import { FALLBACK_VERSION_NUM } from '../config.js';
|
||||
|
||||
export async function getQuestionsAboutLearningObjectInAssignment(
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"oauth2DevicePollingInterval": 5,
|
||||
"enabled": true,
|
||||
"sslRequired": "external",
|
||||
"registrationAllowed": false,
|
||||
"registrationAllowed": true,
|
||||
"registrationEmailAsUsername": false,
|
||||
"rememberMe": false,
|
||||
"verifyEmail": false,
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"oauth2DevicePollingInterval": 5,
|
||||
"enabled": true,
|
||||
"sslRequired": "external",
|
||||
"registrationAllowed": false,
|
||||
"registrationAllowed": true,
|
||||
"registrationEmailAsUsername": false,
|
||||
"rememberMe": false,
|
||||
"verifyEmail": false,
|
||||
|
|
|
@ -52,11 +52,18 @@ npm run test:unit
|
|||
### Run End-to-End Tests with [Playwright](https://playwright.dev)
|
||||
|
||||
```sh
|
||||
cd frontend
|
||||
|
||||
# Install browsers for the first run
|
||||
npx playwright install
|
||||
# On Ubuntu, you can also use
|
||||
npx playwright install --with-deps
|
||||
# to additionally install the dependencies.
|
||||
|
||||
# When testing on CI, must build the project first
|
||||
cd ..
|
||||
npm run build
|
||||
cd frontend
|
||||
|
||||
# Runs the end-to-end tests
|
||||
npm run test:e2e
|
||||
|
|
82
frontend/e2e/basic-homepage.spec.ts
Normal file
82
frontend/e2e/basic-homepage.spec.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("User can pick their language", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByRole("button", { name: "translate" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "translate" }).click();
|
||||
await page.getByText("Nederlands").click();
|
||||
await expect(page.locator("h1")).toContainText("Onze sterke punten");
|
||||
await expect(page.getByRole("heading", { name: "Innovatief" })).toBeVisible();
|
||||
|
||||
await expect(page.getByRole("button", { name: "vertalen" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "vertalen" }).click();
|
||||
await page.getByText("English").click();
|
||||
await expect(page.locator("h1")).toContainText("Our strengths");
|
||||
await expect(page.getByRole("heading", { name: "Innovative" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Teacher can sign in", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("link", { name: "log in" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "teacher" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
|
||||
await expect(page.getByText("teacher")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible();
|
||||
|
||||
await expect(page).toHaveURL(/\/realms\/teacher\//);
|
||||
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerkracht1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
await expect(page.getByRole("link", { name: "Dwengo logo teacher" })).toBeVisible();
|
||||
await expect(page.getByRole("button").nth(1)).toBeVisible();
|
||||
});
|
||||
|
||||
test("Student can sign in", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("link", { name: "log in" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "student" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/realms\/student\//);
|
||||
|
||||
await expect(page.getByText("student")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerling1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
await expect(page.getByRole("link", { name: "Dwengo logo student" })).toBeVisible();
|
||||
await expect(page.getByRole("button").nth(1)).toBeVisible();
|
||||
});
|
||||
|
||||
test("Cannot sign in with invalid credentials", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("doesnotexist");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("wrong");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
await expect(page.getByText("Invalid username or password.")).toBeVisible();
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("doesnotexist");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("wrong");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
await expect(page.getByText("Invalid username or password.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Cannot skip login", async ({ page }) => {
|
||||
await page.goto("/user");
|
||||
await expect(page.getByRole("link", { name: "log in" })).toBeVisible();
|
||||
});
|
12
frontend/e2e/basic-learning.spec.ts
Normal file
12
frontend/e2e/basic-learning.spec.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { test, expect } from "./fixtures.js";
|
||||
|
||||
test("Users can filter", async ({ page }) => {
|
||||
await page.goto("/user");
|
||||
|
||||
await page.getByRole("combobox").filter({ hasText: "Select a themeAll" }).locator("i").click();
|
||||
await page.getByText("Nature and climate").click();
|
||||
await page.getByRole("combobox").filter({ hasText: "Select ageAll agesSelect age" }).locator("i").click();
|
||||
await page.getByText("and older").click();
|
||||
|
||||
await expect(page.getByRole("link", { name: "AI and Climate Students in" })).toBeVisible();
|
||||
});
|
5
frontend/e2e/basic-learning.ts
Normal file
5
frontend/e2e/basic-learning.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { test, expect } from "./fixtures.js";
|
||||
|
||||
test("myTest", async ({ page }) => {
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
116
frontend/e2e/fixtures.ts
Normal file
116
frontend/e2e/fixtures.ts
Normal file
|
@ -0,0 +1,116 @@
|
|||
/* eslint-disable no-await-in-loop */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { test as baseTest, expect } from "@playwright/test";
|
||||
import type { Browser } from "playwright-core";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
/* Based on https://playwright.dev/docs/auth#moderate-one-account-per-parallel-worker */
|
||||
|
||||
export * from "@playwright/test";
|
||||
export const ROOT_URL = "http://localhost:5173";
|
||||
|
||||
interface Account {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire an account by logging in or creating a new one.
|
||||
* @param id
|
||||
* @param browser
|
||||
*/
|
||||
async function acquireAccount(id: number, browser: Browser): Promise<Account> {
|
||||
const account = {
|
||||
username: `worker${id}`,
|
||||
password: "password",
|
||||
};
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.goto(ROOT_URL);
|
||||
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(account.username);
|
||||
await page.getByRole("textbox", { name: "Password", exact: true }).fill(account.password);
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
let failed = await page.getByText("Invalid username or password.").isVisible();
|
||||
|
||||
if (failed) {
|
||||
await page.getByRole("link", { name: "Register" }).click();
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
let retries = 0;
|
||||
while (failed && retries < MAX_RETRIES) {
|
||||
// Retry with a different username, based on Unix timestamp.
|
||||
account.username = `worker${id}-${Date.now()}`;
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(account.username);
|
||||
await page.getByRole("textbox", { name: "Password", exact: true }).fill(account.password);
|
||||
await page.getByRole("textbox", { name: "Confirm password" }).fill(account.password);
|
||||
await page.getByRole("textbox", { name: "Email" }).fill(`${account.username}@dwengo.org`);
|
||||
await page.getByRole("textbox", { name: "First name" }).fill("Worker");
|
||||
await page.getByRole("textbox", { name: "Last name" }).fill(id.toString());
|
||||
await page.getByRole("button", { name: "Register" }).click();
|
||||
|
||||
await page.waitForURL(/localhost/);
|
||||
|
||||
failed = await page.getByText("Username already exists.").isVisible();
|
||||
retries += failed ? 1 : 0;
|
||||
}
|
||||
|
||||
await page.waitForURL(/localhost/);
|
||||
await page.close();
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
export const test = baseTest.extend<object, { workerStorageState: string }>({
|
||||
// Use the same storage state for all tests in this worker.
|
||||
storageState: async ({ workerStorageState }, use) => use(workerStorageState),
|
||||
|
||||
// Authenticate once per worker with a worker-scoped fixture.
|
||||
workerStorageState: [
|
||||
async ({ browser }, use): Promise<void> => {
|
||||
// Use parallelIndex as a unique identifier for each worker.
|
||||
const id = test.info().parallelIndex;
|
||||
const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`);
|
||||
|
||||
if (fs.existsSync(fileName)) {
|
||||
// Reuse existing authentication state if any.
|
||||
await use(fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Important: make sure we authenticate in a clean environment by unsetting storage state.
|
||||
const page = await browser.newPage({ storageState: undefined });
|
||||
|
||||
// Acquire a unique account by creating a new one.
|
||||
const account = await acquireAccount(id, browser);
|
||||
|
||||
// Perform authentication steps. Replace these actions with your own.
|
||||
await page.goto(ROOT_URL);
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill(account.username);
|
||||
await page.getByRole("textbox", { name: "Password" }).fill(account.password);
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
// Wait until the page receives the cookies.
|
||||
//
|
||||
// Sometimes login flow sets cookies in the process of several redirects.
|
||||
// Wait for the final URL to ensure that the cookies are actually set.
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
// Alternatively, you can wait until the page reaches a state where all cookies are set.
|
||||
|
||||
// End of authentication steps.
|
||||
|
||||
await page.context().storageState({ path: fileName });
|
||||
await page.close();
|
||||
await use(fileName);
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
});
|
|
@ -1,8 +0,0 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
// See here how to get started:
|
||||
// https://playwright.dev/docs/intro
|
||||
test("visits the app root url", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.locator("h1")).toHaveText("You did it!");
|
||||
});
|
|
@ -24,10 +24,11 @@
|
|||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuetify": "^3.7.12"
|
||||
"vuetify": "^3.7.12",
|
||||
"wait-on": "^8.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.4",
|
||||
|
|
|
@ -24,7 +24,7 @@ export default defineConfig({
|
|||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
retries: process.env.CI ? 2 : 1,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
|
@ -65,18 +65,18 @@ export default defineConfig({
|
|||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// Name: 'Mobile Chrome',
|
||||
// Use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Name: 'Mobile Safari',
|
||||
// Use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
name: "Mobile Chrome",
|
||||
use: {
|
||||
...devices["Pixel 5"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Mobile Safari",
|
||||
use: {
|
||||
...devices["iPhone 12"],
|
||||
},
|
||||
},
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
|
@ -97,14 +97,28 @@ export default defineConfig({
|
|||
// OutputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
/**
|
||||
* Use the dev server by default for faster feedback loop.
|
||||
* Use the preview server on CI for more realistic testing.
|
||||
* Playwright will re-use the local server if there is already a dev-server running.
|
||||
*/
|
||||
command: process.env.CI ? "npm run preview" : "npm run dev",
|
||||
port: process.env.CI ? 4173 : 5173,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
webServer: [
|
||||
{
|
||||
command: process.env.CI ? "npm run preview" : "npm run dev",
|
||||
port: process.env.CI ? 4173 : 5173,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
command: "cd ../ && docker compose up",
|
||||
reuseExistingServer: false,
|
||||
gracefulShutdown: {
|
||||
signal: "SIGTERM",
|
||||
timeout: 5000,
|
||||
},
|
||||
},
|
||||
{
|
||||
command: process.env.CI ? "cd ../ && npm run dev -w backend" : "cd ../ && npm run start -w backend",
|
||||
port: 3000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
{
|
||||
command: "wait-on http://localhost:7080",
|
||||
timeout: 120000,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
3304
package-lock.json
generated
3304
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue