Skip to content

Commit

Permalink
web: fix e2e tests to work with latest WebdriverIO and authentik 2024…
Browse files Browse the repository at this point in the history
….8 (goauthentik#11105)

* web: fix e2e tests to work with latest WebdriverIO and authentik 2024.8

- Adjust the ApplicationWizard "select provider type" typeslist to have the right OUIA tags when
  running
- Add OUIA tags to TypeCreateWizard
- Provide default values for `.jwksSources` when needed.
- Upgrade E2E WebUI tests to use WebdriverIO 9.
- Upgrade the linters to include `package.json` and `package-lock.json`.
- Adjust a *lot* of the WebdriverIO selectors!
- Provide a driver for the TypeCreate card-based radio interface.
- Split `Bad Logins` into two separate files.

Aside from the obvious, "because testing needs this" or "because there were warnings on the console
when this was running," the real issue is that WebdriverIO 9 has changed the syntax and semantics of
its ShadowDOM-piercing `$` mechanism.

For Oauth2 and Proxy, the field `.jwksSources` may be undefined, but `undefined` is not a legal
value for ak-dual-select's `selected` field. Provide a default or use `ifDefined()`. I chose to
provide a default of `[]`.

In the previous iteration, `$(">>>ak-search-select input")` would be sufficient for WebdriverIO to
find an input inside a component. Now, it needs to be written as: `$("ak-search-select").$("input")`.
And in rare cases, when you have a floating component that is separated from its invocation (such as
Notification or SearchSelect), even that doesn't work well and you have to fall back to some
old-school hacking (see `./tests/wdio/test/pageobjects/page.ts` for an example) to find some child
elements.

Also, the monadic nature of `$` seems to have faded a bit. `$` used to wrap all child invocations in
promises, making the entire expression a single valid promise; it seems that it is now necessary to
unwrap the promises yourself under some circumstances, resulting in a lot of `await (await (await
... )))` blocks in the tests.

We've slightly changed the semantics of our login mechanism, and now the default behavior is to not
reveal when a username is invalid, but to treat the entire login as a single failure mechanism, so
as not to expose any details about the username database.

The problem arises that now, we (or Chrome) cache the username between roundtrips, and WebdriverIO's
second pass was becoming confused by its presence.  By putting the Bad Logins into two separate
files, I get two separate browser instances with cleared caches, so each test can be run in the
pristine environment it needs to validate the behavior I'm expecting.

* web: added comment to explain the  hack

* Add comment to TypeCreateWizardPage to explain the component name hack.

* web: fix some lint found by CI/CD
  • Loading branch information
kensternberg-authentik authored Aug 29, 2024
1 parent 59fa449 commit 9c45ec1
Show file tree
Hide file tree
Showing 25 changed files with 2,572 additions and 2,236 deletions.
4,426 changes: 2,360 additions & 2,066 deletions tests/wdio/package-lock.json

Large diffs are not rendered by default.

27 changes: 16 additions & 11 deletions tests/wdio/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"name": "@goauthentik/web-tests",
"private": true,
"type": "module",
"dependencies": {
"chromedriver": "^128.0.0",
"lockfile-lint": "^4.14.0",
"syncpack": "^13.0.0"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/mocha": "^10.0.7",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"@wdio/cli": "^9.0.3",
Expand All @@ -19,19 +23,20 @@
"typescript": "^5.5.4",
"wdio-wait-for": "^3.0.11"
},
"engines": {
"node": ">=20"
},
"private": true,
"scripts": {
"wdio": "wdio run ./wdio.conf.ts",
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
"lint": "eslint . --max-warnings 0 --fix",
"lint:lockfile": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https",
"lint:package": "syncpack format -i ' '",
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
"lint:spelling": "codespell -D - -D $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-dictionary.txt -I $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-words.txt ./test -s",
"precommit": "run-s lint:precommit lint:spelling prettier",
"prettier": "prettier --write .",
"prettier-check": "prettier --check .",
"prettier": "prettier --write ."
},
"engines": {
"node": ">=20"
"wdio": "wdio run ./wdio.conf.ts"
},
"dependencies": {
"chromedriver": "^128.0.0"
}
"type": "module"
}
18 changes: 2 additions & 16 deletions tests/wdio/test/pageobjects/admin.page.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
import Page from "../pageobjects/page.js";
import { browser } from "@wdio/globals";

const CLICK_TIME_DELAY = 250;

export default class AdminPage extends Page {
public get pageHeader() {
return $('>>>ak-page-header slot[name="header"]');
public async pageHeader() {
return await $("ak-page-header").$('slot[name="header"]');
}

async openApplicationsListPage() {
await this.open("if/admin/#/core/applications");
}

public open(path: string) {
return browser.url(`http://localhost:9000/${path}`);
}

public pause(selector?: string) {
if (selector) {
return $(selector).waitForDisplayed();
}
return browser.pause(CLICK_TIME_DELAY);
}
}
24 changes: 13 additions & 11 deletions tests/wdio/test/pageobjects/application-wizard.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,24 @@ class ApplicationWizardView extends AdminPage {
radius = RadiusForm;
app = ApplicationForm;

get wizardTitle() {
return $(">>>ak-wizard-frame .pf-c-wizard__header h1.pf-c-title");
async wizardTitle() {
return await $("ak-wizard-frame").$(".pf-c-wizard__title");
}

get providerList() {
return $(">>>ak-application-wizard-authentication-method-choice");
async providerList() {
return await $("ak-application-wizard-authentication-method-choice");
}

get nextButton() {
return $(">>>ak-wizard-frame footer button.pf-m-primary");
async nextButton() {
return await $("ak-wizard-frame").$("footer button.pf-m-primary");
}

async getProviderType(type: string) {
return await this.providerList.$(`>>>input[value="${type}"]`);
return await this.providerList().$(`input[value="${type}"]`);
}

get successMessage() {
return $('>>>[data-commit-state="success"]');
async successMessage() {
return await $('[data-commit-state="success"]');
}
}

Expand All @@ -65,8 +65,10 @@ const providerValues: Pair[] = [
providerValues.forEach(([value, name]: Pair) => {
Object.defineProperties(ApplicationWizardView.prototype, {
[name]: {
get: function () {
return this.providerList.$(`>>>input[value="${value}"]`);
get: async function () {
return await (
await this.providerList()
).$(`div[data-ouid-component-name="${value}"]`);
},
},
});
Expand Down
4 changes: 2 additions & 2 deletions tests/wdio/test/pageobjects/applications-list.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class ApplicationsListPage extends AdminPage {
* define selectors using getter methods
*/

get startWizardButton() {
return $('>>>ak-wizard-frame button[slot="trigger"]');
async startWizardButton() {
return await $("ak-application-wizard").$('button[slot="trigger"]');
}

async open() {
Expand Down
12 changes: 6 additions & 6 deletions tests/wdio/test/pageobjects/forms/application.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import Page from "../page.js";
import { $ } from "@wdio/globals";

export class ApplicationForm extends Page {
get name() {
return $('>>>ak-form-element-horizontal input[name="name"]');
async name() {
return await $('ak-text-input[name="name"]').$("input");
}

get uiSettings() {
return $('>>>ak-form-group button[aria-label="UI Settings"]');
async uiSettings() {
return await $("ak-form-group").$('button[aria-label="UI Settings"]');
}

get launchUrl() {
return $('>>>input[name="metaLaunchUrl"]');
async launchUrl() {
return await $('input[name="metaLaunchUrl"]');
}
}

Expand Down
6 changes: 3 additions & 3 deletions tests/wdio/test/pageobjects/forms/forward-proxy.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
export class ForwardProxyForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
'ak-flow-search[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
selector,
);
}

get externalHost() {
return $('>>>input[name="externalHost"]');
return $('input[name="externalHost"]');
}
}

Expand Down
6 changes: 3 additions & 3 deletions tests/wdio/test/pageobjects/forms/ldap.form.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Page from "../page.js";

export class LdapForm extends Page {
async setBindFlow(selector: string) {
async setBindFlow(_selector: string) {
await this.searchSelect(
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
'ak-search-select-view[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
"default-authentication-flow",
);
}
}
Expand Down
8 changes: 4 additions & 4 deletions tests/wdio/test/pageobjects/forms/oauth.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
export class OauthForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
'ak-flow-search[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
`${selector}`,
);
}

get providerName() {
return $('>>>ak-form-element-horizontal[name="name"] input');
async providerName() {
return await $('ak-form-element-horizontal[name="name"]').$("input");
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/wdio/test/pageobjects/forms/radius.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import Page from "../page.js";
export class RadiusForm extends Page {
async setAuthenticationFlow(selector: string) {
await this.searchSelect(
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
'ak-branded-flow-search[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
selector,
);
}
}
Expand Down
6 changes: 3 additions & 3 deletions tests/wdio/test/pageobjects/forms/saml.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
export class SamlForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
'ak-flow-search[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
selector,
);
}

get acsUrl() {
return $('>>>input[name="acsUrl"]');
return $('input[name="acsUrl"]');
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/wdio/test/pageobjects/forms/scim.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import Page from "../page.js";

export class ScimForm extends Page {
get url() {
return $('>>>input[name="url"]');
return $('input[name="url"]');
}

get token() {
return $('>>>input[name="token"]');
return $('input[name="token"]');
}
}

Expand Down
8 changes: 4 additions & 4 deletions tests/wdio/test/pageobjects/forms/transparent-proxy.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import { $ } from "@wdio/globals";
export class TransparentProxyForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
'ak-flow-search[name="authorizationFlow"]',
"authorizationFlow",
`button*=${selector}`,
selector,
);
}

get externalHost() {
return $('>>>input[name="externalHost"]');
return $('input[name="externalHost"]');
}

get internalHost() {
return $('>>>input[name="internalHost"]');
return $('input[name="internalHost"]');
}
}

Expand Down
32 changes: 15 additions & 17 deletions tests/wdio/test/pageobjects/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,44 @@ class LoginPage extends Page {
/**
* Selectors
*/
get inputUsername() {
return $('>>>input[name="uidField"]');
async inputUsername() {
return await $('input[name="uidField"]');
}

get inputPassword() {
return $('>>>input[name="password"]');
async inputPassword() {
return await $('input[name="password"]');
}

get btnSubmit() {
return $('>>>button[type="submit"]');
async btnSubmit() {
return await $('button[type="submit"]');
}

get authFailure() {
return $(">>>h4.pf-c-alert__title");
async authFailure() {
return await $(".pf-m-error");
}

/**
* Specific interactions
*/

async username(username: string) {
await this.inputUsername.waitForClickable();
await this.inputUsername.setValue(username);
await this.btnSubmit.waitForEnabled();
await this.btnSubmit.click();
await (await this.inputUsername()).setValue(username);
await (await this.btnSubmit()).waitForEnabled();
await (await this.btnSubmit()).click();
}

async password(password: string) {
await this.inputPassword.waitForClickable();
await this.inputPassword.setValue(password);
await this.btnSubmit.waitForEnabled();
await this.btnSubmit.click();
await (await this.inputPassword()).setValue(password);
await (await this.btnSubmit()).waitForEnabled();
await (await this.btnSubmit()).click();
}

async login(username: string, password: string) {
await this.username(username);
await this.pause();
await this.password(password);
await this.pause();
await this.pause(">>>div.header h1");
await this.pause("div.header h1");
return UserLibraryPage;
}

Expand Down
29 changes: 20 additions & 9 deletions tests/wdio/test/pageobjects/page.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { browser } from "@wdio/globals";
import { Key } from "webdriverio";

const CLICK_TIME_DELAY = 250;

Expand All @@ -11,15 +12,15 @@ export default class Page {
* Opens a sub page of the page
* @param path path of the sub page (e.g. /path/to/page.html)
*/
public open(path: string) {
return browser.url(`http://localhost:9000/${path}`);
public async open(path: string) {
return await browser.url(`http://localhost:9000/${path}`);
}

public pause(selector?: string) {
public async pause(selector?: string) {
if (selector) {
return $(selector).waitForDisplayed();
return await $(selector).waitForDisplayed();
}
return browser.pause(CLICK_TIME_DELAY);
return await browser.pause(CLICK_TIME_DELAY);
}

/**
Expand All @@ -33,10 +34,20 @@ export default class Page {

async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
const inputBind = await $(searchSelector);
await inputBind.click();
const searchBlock = await $(`>>>div[data-managed-for="${managedSelector}"]`);
const target = searchBlock.$(buttonSelector);
return await target.click();
const inputMain = await inputBind.$('input[type="text"]');
await inputMain.click();
const searchBlock = await (
await $(`div[data-managed-for="${managedSelector}"]`).$("ak-list-select")
).shadow$$("button");
let target: WebdriverIO.Element;
for (const button of searchBlock) {
if ((await button.getText()).includes(buttonSelector)) {
target = button;
break;
}
}
await (await target).click();
await browser.keys(Key.Tab);
}

public async logout() {
Expand Down
Loading

0 comments on commit 9c45ec1

Please sign in to comment.