Merge pull request #193 from SELab-2/test/e2e-setup
test: Eerste End-to-End Testen
This commit is contained in:
		
						commit
						cdcc75a101
					
				
					 12 changed files with 1711 additions and 932 deletions
				
			
		|  | @ -13,8 +13,10 @@ DWENGO_DB_NAME=":memory:" | |||
| DWENGO_DB_UPDATE=true | ||||
| 
 | ||||
| DWENGO_AUTH_STUDENT_URL=http://localhost:7080/realms/student | ||||
| DWENGO_AUTH_STUDENT_CLIENT_ID=dwengo | ||||
| DWENGO_AUTH_STUDENT_JWKS_ENDPOINT=http://localhost:7080/realms/student/protocol/openid-connect/certs | ||||
| DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher | ||||
| DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo | ||||
| DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs | ||||
| 
 | ||||
| DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000 | ||||
| DWENGO_CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:9876,* | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										86
									
								
								frontend/e2e/basic-homepage.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								frontend/e2e/basic-homepage.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| 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 page.getByRole("heading", { name: "Innovatief" }).click(); | ||||
| 
 | ||||
|     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"); | ||||
|     // Should redirect to login
 | ||||
|     await expect(page.getByText("login")).toBeVisible(); | ||||
|     await expect(page.getByRole("button", { name: "teacher" })).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!"); | ||||
| }); | ||||
|  | @ -25,10 +25,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,25 @@ 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", | ||||
|     webServer: [ | ||||
|         // Assuming the idp is already running (because it is slow)
 | ||||
|         { | ||||
|             /* Frontend */ | ||||
|             command: `VITE_API_BASE_URL='http://localhost:9876/api' ${process.env.CI ? "npm run preview" : "npm run dev"}`, | ||||
|             port: process.env.CI ? 4173 : 5173, | ||||
|             timeout: 120 * 1000, | ||||
|             reuseExistingServer: !process.env.CI, | ||||
|         }, | ||||
|         { | ||||
|             /* Backend */ | ||||
|             command: ` | ||||
|             cd .. \ | ||||
|             && npx tsc --build common/tsconfig.json \ | ||||
|             && cd backend \ | ||||
|             && npx tsx --env-file=./.env.test ./tool/startTestApp.ts | ||||
|             `,
 | ||||
|             port: 9876, | ||||
|             reuseExistingServer: !process.env.CI, | ||||
|         }, | ||||
|     ], | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										2339
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										2339
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
		Reference in a new issue
	
	 GitHub
							GitHub