|
1 | 1 | import { check } from "k6" |
2 | 2 | import { browser, Page } from "k6/browser" |
3 | | -import { randomIntBetween } from "https://jslib.k6.io/k6-utils/1.2.0/index.js" |
4 | | -import { BROWSER_CONTEXT_OPTIONS, FRONTEND_BASE_URL } from "../config.ts" |
| 3 | +import { |
| 4 | + randomIntBetween, |
| 5 | + randomItem, |
| 6 | +} from "https://jslib.k6.io/k6-utils/1.2.0/index.js" |
| 7 | +import { |
| 8 | + BROWSER_CONTEXT_OPTIONS, |
| 9 | + FRONTEND_BASE_URL, |
| 10 | + SSO_BASE_URL, |
| 11 | +} from "../config.ts" |
| 12 | +import { AuthCredential, credentials } from "../auth.ts" |
| 13 | +import { escapeRegex } from "../utils.ts" |
5 | 14 |
|
6 | | -async function home(page: Page) { |
| 15 | +type Context = { |
| 16 | + loggedIn: boolean |
| 17 | + credential: AuthCredential | null |
| 18 | +} |
| 19 | + |
| 20 | +async function home(page: Page, context: Context) { |
7 | 21 | await page.goto(FRONTEND_BASE_URL) |
8 | 22 |
|
9 | 23 | const carousel = await page.getByTestId("resource-carousel") |
10 | | - const articlesCount = await carousel.locator("article").count() |
| 24 | + const articles = carousel.locator("article") |
| 25 | + |
| 26 | + try { |
| 27 | + await articles.first().waitFor({ timeout: 10_000 }) |
| 28 | + } catch { |
| 29 | + // carousel never populated; let the assertion below report it |
| 30 | + } |
| 31 | + |
| 32 | + const articlesCount = await articles.count() |
| 33 | + |
11 | 34 | await check(carousel, { |
12 | | - "has 12 items": () => articlesCount === 12, |
| 35 | + "home page carousel has 12 items": () => articlesCount === 12, |
13 | 36 | }) |
14 | 37 |
|
15 | | - await carousel |
16 | | - .locator("article") |
17 | | - .nth(randomIntBetween(0, articlesCount - 1)) |
18 | | - .click() |
| 38 | + if (articlesCount === 0) { |
| 39 | + return |
| 40 | + } |
| 41 | + |
| 42 | + await articles.nth(randomIntBetween(0, articlesCount - 1)).click() |
19 | 43 | } |
20 | 44 |
|
21 | | -async function search(page: Page) { |
| 45 | +async function search(page: Page, context: Context) { |
22 | 46 | await page.goto(`${FRONTEND_BASE_URL}/search?sortby=-views`) |
23 | 47 | await page.goto(`${FRONTEND_BASE_URL}/search?resource_type_group=program`) |
24 | 48 | await page.goto( |
25 | 49 | `${FRONTEND_BASE_URL}/search?resource_type_group=learning_material`, |
26 | 50 | ) |
27 | 51 | } |
28 | 52 |
|
29 | | -async function topics(page: Page) { |
| 53 | +async function topics(page: Page, context: Context) { |
30 | 54 | await page.goto(`${FRONTEND_BASE_URL}/topics`) |
31 | 55 | } |
32 | 56 |
|
33 | | -async function departments(page: Page) { |
| 57 | +async function departments(page: Page, context: Context) { |
34 | 58 | await page.goto(`${FRONTEND_BASE_URL}/departments`) |
35 | 59 | } |
36 | 60 |
|
37 | | -async function units(page: Page) { |
| 61 | +async function units(page: Page, context: Context) { |
38 | 62 | await page.goto(`${FRONTEND_BASE_URL}/units`) |
39 | 63 | } |
40 | 64 |
|
| 65 | +async function login(page: Page, context: Context) { |
| 66 | + const credential: AuthCredential | undefined = randomItem(credentials) |
| 67 | + |
| 68 | + if (credential == null) { |
| 69 | + console.log("Login > skipping because no credentials provided") |
| 70 | + return |
| 71 | + } |
| 72 | + |
| 73 | + if (!SSO_BASE_URL) { |
| 74 | + throw Error("SSO_BASE_URL must be set to run the login flow") |
| 75 | + } |
| 76 | + |
| 77 | + if (page.url() === "about:blank") { |
| 78 | + await page.goto(FRONTEND_BASE_URL) |
| 79 | + } |
| 80 | + |
| 81 | + console.log(`Login > using '${credential.email}'`) |
| 82 | + |
| 83 | + const loginButton = page.getByTestId("login-button-desktop") |
| 84 | + await loginButton.first().click() |
| 85 | + |
| 86 | + let loggedIn = false |
| 87 | + |
| 88 | + try { |
| 89 | + await loginKeycloak(page, credential, context) |
| 90 | + await page.locator('[aria-label="User Menu"]').waitFor({ timeout: 10_000 }) |
| 91 | + loggedIn = true |
| 92 | + console.log("Login > authenticated; user menu visible") |
| 93 | + } catch (err) { |
| 94 | + console.log( |
| 95 | + `Login > failed for '${credential.email}' at URL '${page.url()}': ${err}`, |
| 96 | + ) |
| 97 | + } |
| 98 | + |
| 99 | + check(null, { |
| 100 | + "login succeeded": () => loggedIn, |
| 101 | + }) |
| 102 | +} |
| 103 | + |
| 104 | +const ESCAPED_SSO_URL = escapeRegex(SSO_BASE_URL) |
| 105 | + |
| 106 | +const KEYCLOAK_USERNAME_URL_RE = new RegExp( |
| 107 | + `${ESCAPED_SSO_URL}\/realms\/[a-z-]+\/protocol\/openid-connect\/auth.*`, |
| 108 | +) |
| 109 | +const KEYCLOAK_PASSWORD_URL_RE = new RegExp( |
| 110 | + `${ESCAPED_SSO_URL}\/realms\/[a-z-]+\/login-actions\/authenticate.*`, |
| 111 | +) |
| 112 | + |
| 113 | +async function loginKeycloak( |
| 114 | + page: Page, |
| 115 | + credential: AuthCredential, |
| 116 | + context: Context, |
| 117 | +) { |
| 118 | + await page.waitForURL(KEYCLOAK_USERNAME_URL_RE, { |
| 119 | + waitUntil: "load", |
| 120 | + }) |
| 121 | + |
| 122 | + console.log("Login > Keycloak > on login email page") |
| 123 | + |
| 124 | + const credentialnameInput = await page.locator("input[name=username]") |
| 125 | + console.log("Login > Keycloak > found username input") |
| 126 | + await credentialnameInput.focus() |
| 127 | + await credentialnameInput.fill(credential.email) |
| 128 | + console.log("Login > Keycloak > entered email address") |
| 129 | + |
| 130 | + await page.locator("button[type=submit]").click() |
| 131 | + console.log("Login > Keycloak > submitting email address") |
| 132 | + |
| 133 | + await page.waitForURL(KEYCLOAK_PASSWORD_URL_RE) |
| 134 | + console.log("Login > Keycloak > on login password page") |
| 135 | + |
| 136 | + const passwordInput = await page.locator("input[name=password]") |
| 137 | + console.log("Login > Keycloak > found password input") |
| 138 | + await passwordInput.focus() |
| 139 | + await passwordInput.fill(credential.password, { |
| 140 | + force: true, |
| 141 | + }) |
| 142 | + await page.locator("button[type=submit]").click() |
| 143 | + console.log("Login > Keycloak > submitting password") |
| 144 | + await page.waitForNavigation() |
| 145 | + |
| 146 | + context.loggedIn = true |
| 147 | + context.credential = credential |
| 148 | +} |
| 149 | + |
| 150 | +async function dashboard(page: Page, context: Context) { |
| 151 | + if (!context.loggedIn) { |
| 152 | + return |
| 153 | + } |
| 154 | + await page.goto(`${FRONTEND_BASE_URL}/dashboard`) |
| 155 | +} |
| 156 | + |
41 | 157 | export async function testFrontend() { |
42 | 158 | const page = await browser.newPage(BROWSER_CONTEXT_OPTIONS) |
| 159 | + const context: Context = { |
| 160 | + loggedIn: false, |
| 161 | + credential: null, |
| 162 | + } |
43 | 163 |
|
44 | 164 | try { |
45 | | - await home(page) |
46 | | - await search(page) |
47 | | - await topics(page) |
48 | | - await departments(page) |
49 | | - await units(page) |
| 165 | + // always home page first |
| 166 | + await home(page, context) |
| 167 | + await search(page, context) |
| 168 | + await topics(page, context) |
| 169 | + await departments(page, context) |
| 170 | + await units(page, context) |
| 171 | + await login(page, context) |
| 172 | + await dashboard(page, context) |
| 173 | + await search(page, context) |
| 174 | + await topics(page, context) |
| 175 | + await departments(page, context) |
| 176 | + await units(page, context) |
| 177 | + await home(page, context) |
50 | 178 | } finally { |
51 | 179 | await page.close() |
52 | 180 | } |
|
0 commit comments