Skip to content

Commit 9592f66

Browse files
authored
Playwright introduction (#571)
Playwright framework is now in place, initial ux auth and api search tests added, and playwright gradle tasks setup.
1 parent 40ff401 commit 9592f66

11 files changed

+261
-4
lines changed

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ running the following command:
8181
- [How To Create a Custom Skin](#how-to-create-a-custom-skin)
8282
- [How To Configure Your Deployment](#how-to-configure-your-deployment)
8383
- [How To Customize Text](#how-to-customize-text)
84+
- [How To Run Integration Tests](#how-to-run-integration-tests)
8485

8586
### How To Set Up Everything the First Time
8687

@@ -309,6 +310,18 @@ Use one of the following Gradle tasks to build the image(s) you need:
309310
:warning: Always make sure both `tomcatInstall` and `tomcatDeploy` have run and their output is
310311
intact before invoking any of the `dockerBuildImage<type>` tasks.
311312

313+
### How To Run Integration Tests
314+
uPortal-start comes with integration tests that leverages [Playwright][]. These tests are meant to run out-of-the-box on the quickstart data set of uPortal-start. uPortal should already be running before launching `playwrightRun`. It's encouraged for adopters to add additional tests to meet their specific needs.
315+
316+
The intent is for Playwright installation and execution to be controlled by the following Gradle tasks. Installation of Playwright and it's dependencies (including the browsers) are scoped to the uPortal-start directory.
317+
318+
```console
319+
./gradlew playwrightLint - Lints the tests/ directory across a number of tools. Can also be run as an npm script (see package.json)'
320+
./gradlew playwrightFormat - Formats the files in the tests/ directory via prettier. Can also be run as an npm script (see package.json)
321+
./gradlew playwrightRun - Runs Playwright scripts as per tests/uportal-pw.config.ts
322+
./gradlew playwrightDebug - Runs Playwright scripts as per tests/uportal-pw.config.ts in debug mode
323+
```
324+
312325
[Apereo uPortal]: https://www.apereo.org/projects/uportal
313326
[uPortal 5.0 Manual]: https://uPortal-Project.github.io/uPortal
314327
[Oracle JDK 8]: https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
@@ -319,3 +332,4 @@ intact before invoking any of the `dockerBuildImage<type>` tasks.
319332
[Apache Tomcat Servlet Container]: https://tomcat.apache.org/
320333
[Maven Central]: https://search.maven.org/
321334
[HSQLDB]: http://hsqldb.org/
335+
[Playwright]: https://playwright.dev/

build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ plugins {
66
// id 'cz.malohlava' version '1.0.3'
77

88
id 'net.foragerr.jmeter' version '1.1.0-4.0'
9+
id "com.github.node-gradle.node" version "3.4.0"
910
}
1011

1112
// support integration with intellij and eclipse
@@ -40,6 +41,7 @@ apply from: rootProject.file('gradle/tasks/portal.gradle')
4041
apply from: rootProject.file('gradle/tasks/portlet.gradle')
4142
apply from: rootProject.file('gradle/tasks/docker.gradle')
4243
apply from: rootProject.file('gradle/tasks/perf.gradle')
44+
apply from: rootProject.file('gradle/tasks/playwright.gradle')
4345

4446
/*
4547
* If gradle/tasks/custom.gradle exists, tasks from that

gradle/tasks/playwright.gradle

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
apply plugin: 'com.github.node-gradle.node'
2+
3+
node {
4+
download = true
5+
}
6+
7+
task playwrightNpxInstall(type: NpxTask, dependsOn: npmInstall) {
8+
group 'Testing'
9+
description 'Installs Playwright dependencies. Playwright/test is already installed via npmInstall'
10+
11+
environment = [
12+
'PLAYWRIGHT_BROWSERS_PATH': '0'
13+
]
14+
command = 'playwright'
15+
args = ['install', 'chromium']
16+
}
17+
18+
task playwrightLint(type: NpmTask, dependsOn: npmInstall) {
19+
group 'Testing'
20+
description 'Lints the tests/ directory across a number of tools. Can also be run as an npm script (see package.json)'
21+
22+
args = ['run', 'lint']
23+
}
24+
25+
task playwrightFormat(type: NpmTask, dependsOn: npmInstall) {
26+
group 'Testing'
27+
description 'Formats the files in the tests/ directory via prettier. Can also be run as an npm script (see package.json)'
28+
29+
args = ['run', 'format']
30+
}
31+
32+
task playwrightRun(type: NpxTask, dependsOn: [project.tasks.playwrightNpxInstall, project.tasks.playwrightLint]) {
33+
group 'Testing'
34+
description 'Runs Playwright scripts as per tests/uportal-pw.config.ts'
35+
environment = [
36+
'PLAYWRIGHT_BROWSERS_PATH': '0'
37+
]
38+
command = 'playwright'
39+
args = ['test', '--config=tests/uportal-pw.config.ts']
40+
}
41+
42+
// Works best when only a single test is enabled ( test.only(...) )
43+
task playwrightDebug(type: NpxTask, dependsOn: [project.tasks.playwrightNpxInstall, project.tasks.playwrightLint]) {
44+
group 'Testing'
45+
description 'Runs Playwright scripts as per tests/uportal-pw.config.ts in debug mode'
46+
environment = [
47+
'PLAYWRIGHT_BROWSERS_PATH': '0',
48+
'PWDEBUG': 'console'
49+
]
50+
command = 'playwright'
51+
args = ['test', '--config=tests/uportal-pw.config.ts', '--debug']
52+
}

overlays/uPortal/build.gradle

-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import org.apereo.portal.start.gradle.plugins.GradleImportExportPlugin
22
import org.apereo.portal.start.gradle.plugins.GradlePlutoPlugin
33

4-
plugins {
5-
id "com.github.node-gradle.node" version "3.4.0"
6-
}
7-
84
dependencies {
95
runtime "org.jasig.portal:uPortal-webapp:${uPortalVersion}@war"
106
compile configurations.jdbc

package.json

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"devDependencies": {
3+
"@playwright/test": "~1.23.0",
4+
"@typescript-eslint/eslint-plugin": "~5.30.0",
5+
"@typescript-eslint/parser": "~5.30.0",
6+
"eslint": "~8.20.0",
7+
"prettier": "~2.7.0",
8+
"type-coverage": "~2.22.0",
9+
"typescript": "~4.7.0"
10+
},
11+
"scripts": {
12+
"lint": "npm run lint:tsc && npm run lint:type-coverage && npm run lint:eslint",
13+
"lint:tsc": "tsc",
14+
"lint:type-coverage": "type-coverage",
15+
"lint:eslint": "eslint tests",
16+
"format": "prettier --write tests"
17+
},
18+
"eslintConfig": {
19+
"root": true,
20+
"parserOptions": {
21+
"project": "./tsconfig.json"
22+
},
23+
"extends": [
24+
"eslint:recommended",
25+
"plugin:@typescript-eslint/recommended",
26+
"plugin:@typescript-eslint/recommended-requiring-type-checking"
27+
]
28+
},
29+
"typeCoverage": {
30+
"atLeast": 100,
31+
"detail": true,
32+
"strict": true
33+
}
34+
}

tests/api/search-v5_0.spec.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { test, expect } from "@playwright/test";
2+
import { config } from "../general-config";
3+
import { loginViaApi } from "../ux/utils/ux-general-utils";
4+
5+
test("search all", async ({ request }) => {
6+
await loginViaApi(request, "admin", "admin", "Amy Administrator");
7+
const response = await request.get(
8+
`${config.url}api/v5-0/portal/search?q=cartoon`
9+
);
10+
expect(response.status()).toEqual(200);
11+
expect(await response.json()).toEqual({
12+
people: [],
13+
portlets: [
14+
{
15+
description: "Daily Business Cartoon by Ted Goff, www.tedgoff.com",
16+
fname: "daily-business-cartoon",
17+
name: "Daily Business Cartoon",
18+
score: "4.0",
19+
title: "Daily Business Cartoon",
20+
url: "/uPortal/p/daily-business-cartoon.ctf3/max/render.uP",
21+
},
22+
],
23+
});
24+
});
25+
26+
test("search type people", async ({ request }) => {
27+
await loginViaApi(request, "admin", "admin", "Amy Administrator");
28+
const response = await request.get(
29+
`${config.url}api/v5-0/portal/search?q=steven&type=people`
30+
);
31+
expect(response.status()).toEqual(200);
32+
expect(await response.json()).toEqual({
33+
people: [
34+
{
35+
uid: ["student"],
36+
telephoneNumber: ["(555) 555-5555"],
37+
mail: ["steven.student@example.org"],
38+
displayName: ["Steven Student"],
39+
givenName: ["Steven"],
40+
"user.login.id": ["student"],
41+
sn: ["Student"],
42+
username: ["student"],
43+
},
44+
],
45+
});
46+
});

tests/general-config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const config = {
2+
url: "http://localhost:8080/uPortal/",
3+
};

tests/uportal-pw.config.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { PlaywrightTestConfig } from "@playwright/test";
2+
const config: PlaywrightTestConfig = {
3+
use: {
4+
headless: true,
5+
viewport: { width: 1280, height: 720 },
6+
ignoreHTTPSErrors: true,
7+
video: "on-first-retry",
8+
},
9+
retries: 1,
10+
};
11+
export default config;

tests/ux/auth/uportal-auth.spec.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test, expect } from "@playwright/test";
2+
import { loginViaPage } from "../utils/ux-general-utils";
3+
4+
const SEL_UPORTAL_LOGOUT = ".portal-logout";
5+
6+
test("login, logout, login as different user", async ({ page }) => {
7+
// Login as admin
8+
await loginViaPage(page, "admin", "admin", "Amy Administrator");
9+
10+
// Logout via UX
11+
const uportalSignout = page.locator(SEL_UPORTAL_LOGOUT);
12+
await expect(uportalSignout).toHaveText("Sign Out");
13+
await expect(uportalSignout).toHaveAttribute("href", "/uPortal/Logout");
14+
await page.click(SEL_UPORTAL_LOGOUT);
15+
16+
// Confirm default CAS logout page
17+
const casLogoutSuccessful = page.locator("div > h2");
18+
await expect(casLogoutSuccessful).toHaveText("Logout successful");
19+
20+
// Login as student
21+
await loginViaPage(page, "student", "student", "Steven Student");
22+
});

tests/ux/utils/ux-general-utils.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { expect, Page, APIRequestContext } from "@playwright/test";
2+
import { config } from "../../general-config";
3+
4+
const SEL_UPORTAL_LOGIN = "#portalCASLoginLink";
5+
const SEL_UPORTAL_LOGIN_USERNAME = "#username";
6+
const SEL_UPORTAL_LOGIN_PASSWORD = "#password";
7+
const SEL_UPORTAL_LOGIN_SUBMIT = ".btn-submit";
8+
9+
/*
10+
* Log into uPortal via an APIRequestContext
11+
*/
12+
export async function loginViaApi(
13+
request: APIRequestContext,
14+
username: string,
15+
password: string,
16+
displayname: string
17+
): Promise<void> {
18+
const url = `${config.url}Login?userName=${username}&password=${password}`;
19+
const response = await request.get(url);
20+
expect(response.status()).toEqual(200);
21+
const responseText = await response.text();
22+
expect(responseText).toContain(displayname);
23+
}
24+
25+
/*
26+
* Log into uPortal via the UX
27+
*/
28+
export async function loginViaPage(
29+
page: Page,
30+
username: string,
31+
password: string,
32+
displayname: string
33+
): Promise<void> {
34+
// Navigate to uPortal welcome page prior to login
35+
const landingPageUrl = `${config.url}f/welcome/normal/render.uP`;
36+
await page.goto(landingPageUrl);
37+
38+
// Launch CAS Login via UX
39+
const uportalSignin = page.locator(SEL_UPORTAL_LOGIN);
40+
await expect(uportalSignin).toHaveText("Sign In");
41+
await page.click(SEL_UPORTAL_LOGIN);
42+
43+
// Fill in username and password, and submit
44+
await page.waitForSelector(SEL_UPORTAL_LOGIN_USERNAME);
45+
await page.fill(SEL_UPORTAL_LOGIN_USERNAME, username);
46+
await page.waitForSelector(SEL_UPORTAL_LOGIN_PASSWORD);
47+
await page.fill(SEL_UPORTAL_LOGIN_PASSWORD, password);
48+
await page.waitForSelector(SEL_UPORTAL_LOGIN_SUBMIT);
49+
await page.click(SEL_UPORTAL_LOGIN_SUBMIT);
50+
51+
// Confirm uPortal logo
52+
const uportalLogo = page.locator("h1.portal-logo > a");
53+
await expect(uportalLogo).toHaveText("uPortal");
54+
55+
// Confirm user is logged in
56+
const loggedInUserDisplay = page.locator("div.user-name");
57+
await expect(loggedInUserDisplay).toHaveText(
58+
`You are signed in as ${displayname}`
59+
);
60+
}

tsconfig.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"target": "esnext",
4+
"lib": ["esnext", "DOM"],
5+
"esModuleInterop": false,
6+
"allowSyntheticDefaultImports": false,
7+
"strict": true,
8+
"forceConsistentCasingInFileNames": true,
9+
"noFallthroughCasesInSwitch": true,
10+
"module": "esnext",
11+
"moduleResolution": "node",
12+
"resolveJsonModule": true,
13+
"isolatedModules": true,
14+
"noEmit": true
15+
},
16+
"include": ["tests"]
17+
}

0 commit comments

Comments
 (0)