web: revise tests for wizard

This commit replaces the previous WDIO instance with a more formal and straightforward process using
the [pageobjects](https://martinfowler.com/bliki/PageObject.html).  In this form, every major
component has its own test suite, and a test is a sequence of exercises of those components.

A test then becomes something as straightforward as:

```
        await LoginPage.open();
        await LoginPage.login("ken@goauthentik.io", "eat10bugs");

        expect(await UserLibraryPage.pageHeader).toHaveText("My Applications");
        await UserLibraryPage.goToAdmin();

        expect(await AdminOverviewPage.pageHeader).toHaveText("Welcome, ");
        await AdminOverviewPage.openApplicationsListPage();

        expect(await ApplicationsListPage.pageHeader).toHaveText("Applications");
        ApplicationsListPage.startCreateApplicationWizard();

        await ApplicationWizard.app.name.setValue(`Test application ${newId}`);
        await ApplicationWizard.nextButton.click();
        await (await ApplicationWizard.getProviderType("ldapprovider")).click();
        await ApplicationWizard.nextButton.click();
        await ApplicationWizard.ldap.setBindFlow("default-authentication-flow");
        await ApplicationWizard.nextButton.click();
        await expect(await ApplicationWizard.commitMessage).toHaveText(
            "Your application has been saved"
        );
```

Whether or not there's another layer of DSL in there or not, this is a pretty nice idiom for
maintaining tests.
This commit is contained in:
Ken Sternberg 2023-09-21 14:46:09 -07:00
parent 53f89ef2f8
commit 0a43ea286e
31 changed files with 7364 additions and 20541 deletions

View file

@ -1,4 +1,3 @@
reports/
# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node
@ -108,5 +107,3 @@ tmp/
temp/
# End of https://www.gitignore.io/api/node
api/**
storybook-static/

View file

@ -1,22 +0,0 @@
.PHONY: help
help: ## Print out this help message.
@M=$$(perl -ne 'm/((\w|-)*):.*##/ && print length($$1)."\n"' Makefile | \
sort -nr | head -1) && \
perl -ne "m/^((\w|-)*):.*##\s*(.*)/ && print(sprintf(\"%s: %s\t%s\n\", \$$1, \" \"x($$M-length(\$$1)), \$$3))" Makefile
@echo ""
.PHONY: update-local-chromedriver
update-local-chromedriver: ## Update the chrome driver to match the local chrome version, restoring package.json
@ scripts/update_local_chromedriver
.PHONY: check-chromedriver
check-chromedriver: ## Report if the chrome driver and the local chrome version match
@ scripts/check_local_chromedriver
RUNNER=npx wdio wdio.conf.js
.PHONY: application-plus-ldap
application-plus-ldap: check-chromedriver ## Run the "Wizard: Application With LDAP Provider, successful" test
@ ${RUNNER} --spec=./tests/application-plus-ldap.test.js

View file

@ -1,51 +0,0 @@
"use strict";
const CLICK_TIME_DELAY = 250;
async function text(selector, value) {
const input = await $(selector);
return await input.setValue(value);
}
async function button(selector) {
const button = await $(selector);
return await button.click();
}
async function search(searchSelector, buttonSelector) {
const inputBind = await $(searchSelector);
await inputBind.click();
const searchBlock = await $('>>>div[data-managed-by="ak-search-select"]');
const target = searchBlock.$(buttonSelector);
return await target.click();
}
async function pause(selector) {
if (selector) {
return await $(selector).waitForDisplayed();
}
return await browser.pause(CLICK_TIME_DELAY);
}
async function waitfor(selector) {
return await $(selector).waitForDisplayed();
}
async function deletebox(selector) {
return await $(selector)
.parentElement()
.parentElement()
.$(".pf-c-table__check")
.$('input[type="checkbox"]')
.click();
}
exports.$AkSel = {
button,
pause,
search,
text,
waitfor,
deletebox,
};

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,16 @@
{
"name": "authentik-live-tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"author": "",
"license": "ISC",
"name": "my-new-project",
"type": "module",
"devDependencies": {
"@wdio/junit-reporter": "^8.15.6",
"@wdio/local-runner": "^8.15.6",
"@wdio/mocha-framework": "^8.15.6",
"@wdio/spec-reporter": "^8.15.6",
"@wdio/sync": "^7.27.0",
"chromedriver": "^116.0.0",
"wdio-chromedriver-service": "^8.1.1",
"wdio-safaridriver-service": "^2.1.1"
"@wdio/cli": "^8.16.11",
"@wdio/local-runner": "^8.16.11",
"@wdio/mocha-framework": "^8.16.11",
"@wdio/spec-reporter": "^8.16.9",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"wdio-wait-for": "^3.0.7"
},
"dependencies": {
"@wdio/cli": "^8.15.6",
"@wdio/types": "^8.15.7",
"prettier": "^3.0.2"
"scripts": {
"wdio": "wdio run ./wdio.conf.ts"
}
}

View file

@ -1,63 +0,0 @@
#!/usr/bin/env bash
# -u: Treat an unset variable as a fatal syntax error.
# -eo pipefile: Fail on first error, even if it happens inside a pipeline.
set -ueo pipefail
VERBOSE=""
if [ "$#" -gt 0 ] && [ "$1" = "-v" ]; then
VERBOSE="verbose";
fi;
if [ "$#" -gt 0 ] && [ "$1" = "-h" ]; then
echo "Usage: "
echo " -v: On success, show a message. (Default behavior only shows a message on failure)"
echo " -h: This help message"
echo ""
exit 0
fi;
# The path to the working folder for the test project, as a subfolder of the monorepo. This will be
# help us find where the driver is kept for comparison.
SUBFOLDER="wdio"
# The variant of Chrome we expect under Linux. There are a lot of variants, like Midori, chromium,
# chromium-browser, etc. If you're not running the version supplied by Google, you'll have to change
# this variable.
LINUX_VARIANT="google-chrome"
CURRENT_OS=$(uname -s)
if [ "$CURRENT_OS" == "Linux" ]; then
CHROME_LOCATION=$(command -v "$LINUX_VARIANT")
if [ "$CHROME_LOCATION" == "" ]; then
echo "Could not find google-chrome installed on this Linux system."
exit 1
fi
CHROME_VERSION=$("$LINUX_VARIANT" --version)
else
CHROME_LOCATION="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
if [ ! -f "$CHROME_LOCATION" ]; then
echo "Could not find Google Chrome app installed on this MacOS system."
exit 1
fi
CHROME_VERSION=$("$CHROME_LOCATION" --version)
fi
CHROME_MAJOR_VER=$(echo "$CHROME_VERSION" | sed 's/^Google Chrome //' | cut -d'.' -f1)
PROJECT_TOPLEVEL=$(git rev-parse --show-toplevel)
TEST_HOME=$(find $PROJECT_TOPLEVEL -not \( -path "*/node_modules" -prune \) -type d -name "$SUBFOLDER" | head -1)
DRIVER_VER=$(grep '^ "version":' "$TEST_HOME/node_modules/chromedriver/package.json")
DRIVER_MAJOR_VER=$(echo "$DRIVER_VER" | cut -d':' -f2 | sed 's/"//g' | cut -d'.' -f1 | sed 's/ *//')
if [ "$CHROME_MAJOR_VER" -ne "$DRIVER_MAJOR_VER" ]; then
echo "Driver: $DRIVER_MAJOR_VER, Chrome: $CHROME_MAJOR_VER, update required."
exit 1
fi
if [ "$VERBOSE" ]; then
echo "Driver: $DRIVER_MAJOR_VER, Chrome: $CHROME_MAJOR_VER. No update required."
fi
# SUCCESS!
exit 0

View file

@ -1,77 +0,0 @@
#!/usr/bin/env bash
# The below is helpful because Chrome is an evergreen browser, meaning that it will auto-updated on
# most desktop computers without even informing the user. So we need a way, if you hit the mismatch,
# to update the local chromedriver easily.
#
# Testing with WDIO requires a specific version of the chromedriver, specified in the current
# package.json. Updating the chromedriver will change that version number in package.json.
#
# The environment block derives the major version for the local (MacOS) version of chrome and the
# current driver version for the project and, if they're mismatch, updates the local driver then
# resets the content of package.json.
# -u: Treat an unset variable as a fatal syntax error.
# -eo pipefile: Fail on first error, even if it happens inside a pipeline.
set -ueo pipefail
VERBOSE=""
if [ "$#" -gt 0 ] && [ "$1" = "-v" ]; then
VERBOSE="verbose";
fi;
if [ "$#" -gt 0 ] && [ "$1" = "-h" ]; then
echo "Usage: "
echo " -v: On success, show a message. (Default behavior only shows a message on failure)"
echo " -h: This help message"
echo ""
exit 0
fi;
# The path to the working folder for the test project, as a subfolder of the monorepo. This will be
# help us find where the driver is kept for comparison.
SUBFOLDER="wdio"
# The variant of Chrome we expect under Linux. There are a lot of variants, like Midori, chromium,
# chromium-browser, etc. If you're not running the version supplied by Google, you'll have to change
# this variable.
LINUX_VARIANT="google-chrome"
CURRENT_OS=$(uname -s)
CURRENT_OS=$(uname -s)
if [ "$CURRENT_OS" == "Linux" ]; then
CHROME_LOCATION=$(command -v "$LINUX_VARIANT")
if [ "$CHROME_LOCATION" == "" ]; then
echo "Could not find google-chrome installed on this Linux system."
exit 1
fi
CHROME_VERSION=$("$LINUX_VARIANT" --version)
else
CHROME_LOCATION="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
if [ ! -f "$CHROME_LOCATION" ]; then
echo "Could not find Google Chrome app installed on this MacOS system."
exit 1
fi
CHROME_VERSION=$("$CHROME_LOCATION" --version)
fi
CHROME_MAJOR_VER=$(echo "$CHROME_VERSION" | sed 's/^Google Chrome //' | cut -d'.' -f1)
PROJECT_TOPLEVEL=$(git rev-parse --show-toplevel)
TEST_HOME=$(find $PROJECT_TOPLEVEL -not \( -path "*/node_modules" -prune \) -type d -name "$SUBFOLDER" | head -1)
DRIVER_VER=$(grep '^ "version":' "$TEST_HOME/node_modules/chromedriver/package.json")
DRIVER_MAJOR_VER=$(echo "$DRIVER_VER" | cut -d':' -f2 | sed 's/"//g' | cut -d'.' -f1 | sed 's/ *//')
if [ "$CHROME_MAJOR_VER" -ne "$DRIVER_MAJOR_VER" ]; then
echo "Driver: $DRIVER_MAJOR_VER, Chrome: $CHROME_MAJOR_VER, updating..."
npm install "chromedriver@$CHROME_MAJOR_VER"
git checkout package.json package-lock.json
exit 0
fi
if [ "$VERBOSE" ]; then
echo "Driver: $DRIVER_MAJOR_VER, Chrome: $CHROME_MAJOR_VER. No update required."
fi
# SUCCESS!
exit 0

View file

@ -0,0 +1,13 @@
import { $ } from "@wdio/globals";
import AdminPage from "./admin.page.js";
/**
* sub page containing specific selectors and methods for a specific page
*/
class AdminOverviewPage extends AdminPage {
/**
* define selectors using getter methods
*/
}
export default new AdminOverviewPage();

View file

@ -0,0 +1,25 @@
import { browser } from "@wdio/globals";
import Page from "../pageobjects/page.js";
const CLICK_TIME_DELAY = 250;
export default class AdminPage extends Page {
public get pageHeader() {
return $(">>>ak-page-header h1");
}
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);
}
}

View file

@ -0,0 +1,10 @@
import { $ } from "@wdio/globals";
import Page from "./page.js";
export class ApplicationForm extends Page {
get name() {
return $('>>>ak-form-element-horizontal input[name="name"]');
}
}
export default new ApplicationForm();

View file

@ -0,0 +1,40 @@
import { $ } from "@wdio/globals";
import AdminPage from "./admin.page.js";
import ApplicationForm from "./application-form.view.js";
import LdapForm from "./ldap-form.view.js";
import OauthForm from "./oauth-form.view.js";
/**
* sub page containing specific selectors and methods for a specific page
*/
class ApplicationWizardView extends AdminPage {
/**
* define selectors using getter methods
*/
ldap = LdapForm;
oauth = OauthForm;
app = ApplicationForm;
get wizardTitle() {
return $(">>>ak-application-wizard-commit-application h1.pf-c-title");
}
get providerList() {
return $(">>>ak-application-wizard-authentication-method-choice");
}
get nextButton() {
return $(">>>ak-wizard-frame footer button.pf-m-primary");
}
async getProviderType(type: string) {
return await this.providerList.$(`>>>input[value="${type}"]`);
}
get commitMessage() {
return $(">>>ak-application-wizard-commit-application h1.pf-c-title");
}
}
export default new ApplicationWizardView();

View file

@ -0,0 +1,17 @@
import { $ } from "@wdio/globals";
import AdminPage from "./admin.page.js";
/**
* sub page containing specific selectors and methods for a specific page
*/
class ApplicationsListPage extends AdminPage {
/**
* define selectors using getter methods
*/
async startCreateApplicationWizard() {
await $('>>>ak-wizard-frame button[slot="trigger"]').click();
}
}
export default new ApplicationsListPage();

View file

@ -0,0 +1,13 @@
import Page from "./page.js";
export class LdapForm extends Page {
async setBindFlow(selector: string) {
await this.searchSelect(
'>>>ak-tenanted-flow-search[name="authorizationFlow"] input[type="text"]',
"authorizationFlow",
`button*=${selector}`
);
}
}
export default new LdapForm();

View file

@ -0,0 +1,63 @@
import { $ } from "@wdio/globals";
import Page from "./page.js";
import UserLibraryPage from "./user-library.page.js";
/**
* sub page containing specific selectors and methods for a specific page
*/
class LoginPage extends Page {
/**
* define selectors using getter methods
*/
get inputUsername() {
return $('>>>input[name="uidField"]');
}
get inputPassword() {
return $('>>>input[name="password"]');
}
get btnSubmit() {
return $('>>>button[type="submit"]');
}
get authFailure() {
return $(">>>h4.pf-c-alert__title");
}
/**
* a method to encapsule automation code to interact with the page
* e.g. to login using username and password
*/
async username(username: string) {
await this.inputPassword.isDisplayed();
await this.inputUsername.setValue(username);
await this.btnSubmit.isEnabled();
await this.btnSubmit.click();
}
async password(password: string) {
await this.inputPassword.isDisplayed();
await this.inputPassword.setValue(password);
await this.btnSubmit.isEnabled();
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");
return UserLibraryPage;
}
/**
* overwrite specific options to adapt it to page object
*/
open() {
return super.open("");
}
}
export default new LoginPage();

View file

@ -0,0 +1,13 @@
import Page from "./page.js";
export class OauthForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
"authorizationFlow",
`button*=${selector}`
);
}
}
export default new OauthForm();

View file

@ -0,0 +1,32 @@
import { browser } from "@wdio/globals";
const CLICK_TIME_DELAY = 250;
/**
* main page object containing all methods, selectors and functionality
* that is shared across all page objects
*/
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 pause(selector?: string) {
if (selector) {
return $(selector).waitForDisplayed();
}
return browser.pause(CLICK_TIME_DELAY);
}
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();
}
}

View file

@ -0,0 +1 @@
export type Constructor<T = object> = new (...args: any[]) => T;

View file

@ -0,0 +1,22 @@
import { $ } from "@wdio/globals";
import Page from "./page.js";
/**
* sub page containing specific selectors and methods for a specific page
*/
class UserLibraryPage extends Page {
/**
* define selectors using getter methods
*/
public get pageHeader() {
return $('>>>h1[aria-level="1"]');
}
public async goToAdmin() {
await $('>>>a[href="/if/admin"]').click();
await $(">>>ak-admin-overview").waitForDisplayed();
}
}
export default new UserLibraryPage();

View file

@ -0,0 +1,35 @@
import { expect } from "@wdio/globals";
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
import AdminOverviewPage from "../pageobjects/admin-overview.page.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import ApplicationWizard from "../pageobjects/application-wizard.page.js";
import { randomId } from "../utils/index.js";
describe("Configure new Application", () => {
it("should navigate to the wizard and configure LDAP", async () => {
const newId = randomId();
await LoginPage.open();
await LoginPage.login("ken@goauthentik.io", "eat10bugs");
expect(await UserLibraryPage.pageHeader).toHaveText("My Applications");
await UserLibraryPage.goToAdmin();
expect(await AdminOverviewPage.pageHeader).toHaveText("Welcome, ");
await AdminOverviewPage.openApplicationsListPage();
expect(await ApplicationsListPage.pageHeader).toHaveText("Applications");
ApplicationsListPage.startCreateApplicationWizard();
await ApplicationWizard.app.name.setValue(`Test application ${newId}`);
await ApplicationWizard.nextButton.click();
await (await ApplicationWizard.getProviderType("ldapprovider")).click();
await ApplicationWizard.nextButton.click();
await ApplicationWizard.ldap.setBindFlow("default-authentication-flow");
await ApplicationWizard.nextButton.click();
await expect(await ApplicationWizard.commitMessage).toHaveText(
"Your application has been saved"
);
});
});

View file

@ -0,0 +1,37 @@
import { expect } from "@wdio/globals";
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
import AdminOverviewPage from "../pageobjects/admin-overview.page.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import ApplicationWizard from "../pageobjects/application-wizard.page.js";
import { randomId } from "../utils/index.js";
describe("Configure new Application", () => {
it("should navigate to the wizard and configure Oauth2", async () => {
const newId = randomId();
await LoginPage.open();
await LoginPage.login("ken@goauthentik.io", "eat10bugs");
expect(await UserLibraryPage.pageHeader).toHaveText("My Applications");
await UserLibraryPage.goToAdmin();
expect(await AdminOverviewPage.pageHeader).toHaveText("Welcome, ");
await AdminOverviewPage.openApplicationsListPage();
expect(await ApplicationsListPage.pageHeader).toHaveText("Applications");
ApplicationsListPage.startCreateApplicationWizard();
await ApplicationWizard.app.name.setValue(`Test application ${newId}`);
await ApplicationWizard.nextButton.click();
await (await ApplicationWizard.getProviderType("oauth2provider")).click();
await ApplicationWizard.nextButton.click();
await ApplicationWizard.oauth.setAuthorizationFlow(
"default-provider-authorization-explicit-consent"
);
await ApplicationWizard.nextButton.click();
await expect(await ApplicationWizard.commitMessage).toHaveText(
"Your application has been saved"
);
});
});

View file

@ -0,0 +1,21 @@
import { expect } from "@wdio/globals";
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
describe("Log into Authentik", () => {
it("should fail on a bad username", async () => {
await LoginPage.open();
await LoginPage.username("bad-username@bad-logio.io");
const failure = await LoginPage.authFailure;
expect(failure).toHaveText("Failed to authenticate.");
});
it("should fail on a bad password", async () => {
await LoginPage.open();
await LoginPage.username("ken@goauthentik.io");
await LoginPage.pause();
await LoginPage.password("-this-is-a-bad-password-");
const failure = await LoginPage.authFailure;
expect(failure).toHaveText("Failed to authenticate.");
});
});

View file

@ -0,0 +1,11 @@
import { expect } from "@wdio/globals";
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
describe("Log into Authentik", () => {
it("should login with valid credentials", async () => {
await LoginPage.open();
await LoginPage.login("ken@goauthentik.io", "eat10bugs");
await expect(UserLibraryPage.header).toHaveText("My applications");
});
});

View file

@ -1,4 +1,4 @@
function randomPrefix() {
export function randomId() {
let dt = new Date().getTime();
return "xxxxxxxx".replace(/x/g, (c) => {
const r = (dt + Math.random() * 16) % 16 | 0;
@ -7,11 +7,9 @@ function randomPrefix() {
});
}
function convertToSlug(text) {
export function convertToSlug(text: string) {
return text
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^\w-]+/g, "");
}
module.exports = { randomPrefix, convertToSlug };

View file

@ -1,93 +0,0 @@
const { execSync } = require("child_process");
const { readdirSync } = require("fs");
const path = require("path");
const { $AkSel } = require("../lib/idiom");
const { randomPrefix, convertToSlug } = require("../lib/utils");
const CLICK_TIME_DELAY = 250;
const login = [
["text", '>>>input[name="uidField"]', "ken@goauthentik.io"],
["button", '>>>button[type="submit"]'],
["pause"],
["text", '>>>input[name="password"]', "eat10bugs"],
["button", '>>>button[type="submit"]'],
["pause", ">>>div.header h1"],
];
const navigateToWizard = [
["button", '>>>a[href="/if/admin"]'],
["waitfor", ">>>ak-admin-overview"],
["button", '>>>a[href="#/core/applications;%7B%22createForm%22%3Atrue%7D"]'],
["waitfor", ">>>ak-application-list"],
["button", '>>>ak-wizard-frame button[slot="trigger"]']
];
// prettier-ignore
const ldapApplication = (theCode) => ([
["text", '>>>ak-form-element-horizontal input[name="name"]', `This Is My Application ${theCode}`],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
["button", '>>>input[value="ldapprovider"]'],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
["search", '>>>ak-tenanted-flow-search input[type="text"]', "button*=default-authentication-flow",],
["text", '>>>ak-form-element-horizontal input[name="tlsServerName"]', "example.goauthentik.io"],
["button", ">>>ak-wizard-frame footer button.pf-m-primary"],
]);
const deleteProvider = (theSlug) => ([
["button", '>>>ak-sidebar-item a[href="#/core/providers"]'],
["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`],
["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'],
["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button'],
]);
const deleteApplication = (theSlug) => ([
["button", '>>>ak-sidebar-item a[href="#/core/applications"'],
["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`],
["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'],
["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button']
]);
function prepApplicationAndSlug() {
const newSuffix = randomPrefix();
const thisApplication = ldapApplication(newSuffix);
return [thisApplication, convertToSlug(thisApplication[0][2])];
}
async function runSequence(sequence, watch = false) {
for ([command, ...args] of sequence) {
await $AkSel[command].apply($, args);
if (watch) {
await browser.pause(250);
}
}
}
describe("Login", () => {
it("Should correctly log in to Authentik}", async () => {
const [theApplication, theSlug] = prepApplicationAndSlug();
await browser.reloadSession();
await browser.url("http://localhost:9000");
runSequence(login, true);
const home = await $(">>>div.header h1");
await expect(home).toHaveText("My applications");
await runSequence(navigateToWizard, true);
await runSequence(theApplication, true)
const success = await $(">>>ak-application-wizard-commit-application h1.pf-c-title");
await expect(success).toHaveText("Your application has been saved");
await $AkSel.button(">>>ak-wizard-frame .pf-c-wizard__footer-cancel button");
await runSequence(deleteProvider(theSlug), true);
await runSequence(deleteApplication(theSlug), true);
const expectedApplication = await $(`>>>a[href="#/core/applications/${theSlug}"]`);
expect(expectedApplication).not.toExist();
});
});

25
tests/wdio/tsconfig.json Normal file
View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "ESNext",
"target": "es2022",
"types": [
"node",
"@wdio/globals/types",
"expect-webdriverio",
"@wdio/mocha-framework"
],
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"test"
]
}

View file

@ -1,294 +0,0 @@
const fs = require("fs");
const path = require("path");
const debug = process.env.DEBUG;
const defaultTimeoutInterval = 60000;
const buildNumber = process.env.BUILD_NUMBER ? process.env.BUILD_NUMBER : "0";
const reportsOutputDir = "./reports";
exports.config = {
//
// ====================
// Runner Configuration
// ====================
//
// WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or
// on a remote machine).
runner: "local",
//
// ==================
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
// directory is where your package.json resides, so `wdio` will be called from there.
//
specs: ["./tests/*.js"],
// Patterns to exclude.
exclude: [
// 'path/to/excluded/files'
],
//
// ============
// Capabilities
// ============
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
// time. Depending on the number of capabilities, WebdriverIO launches several test
// sessions. Within your capabilities you can overwrite the spec and exclude options in
// order to group specific specs to a specific capability.
//
// First, you can define how many instances should be started at the same time. Let's
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
// files and you set maxInstances to 10, all spec files will get tested at the same time
// and 30 processes will get spawned. The property handles how many capabilities
// from the same test should run tests.
//
maxInstances: 10,
//
// If you have trouble getting all important capabilities together, check out the
// Sauce Labs platform configurator - a great tool to configure your capabilities:
// https://docs.saucelabs.com/reference/platforms-configurator
//
capabilities: [
{
// maxInstances can get overwritten per capability. So if you have an in-house Selenium
// grid with only 5 firefox instances available you can make sure that not more than
// 5 instances get started at a time.
maxInstances: 1,
//
browserName: "Safari",
// acceptInsecureCerts: true,
// If outputDir is provided WebdriverIO can capture driver session logs
// it is possible to configure which logTypes to include/exclude.
// excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs
// excludeDriverLogs: ['bugreport', 'server'],
},
],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: "info",
//
// Set specific log levels per logger
// loggers:
// - webdriver, webdriverio
// - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
// - @wdio/mocha-framework, @wdio/jasmine-framework
// - @wdio/local-runner
// - @wdio/sumologic-reporter
// - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils
// Level of logging verbosity: trace | debug | info | warn | error | silent
// logLevels: {
// webdriver: 'info',
// '@wdio/applitools-service': 'info'
// },
//
// If you only want to run your tests until a specific amount of tests have failed use
// bail (default is 0 - don't bail, run all tests).
bail: 0,
//
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
// gets prepended directly.
baseUrl: "http://localhost",
//
// Default timeout for all waitFor* commands.
waitforTimeout: 10000,
//
// Default timeout in milliseconds for request
// if browser driver or grid doesn't send response
connectionRetryTimeout: 120000,
//
// Default request retries count
connectionRetryCount: 3,
//
// Test runner services
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
services: ["safaridriver"],
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: https://webdriver.io/docs/frameworks.html
//
// Make sure you have the wdio adapter package for the specific framework installed
// before running any tests.
framework: "mocha",
//
// The number of times to retry the entire specfile when it fails as a whole
// specFileRetries: 1,
//
// Delay in seconds between the spec file retry attempts
// specFileRetriesDelay: 0,
//
// Whether or not retried specfiles should be retried immediately or deferred to the end of the queue
// specFileRetriesDeferred: false,
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: https://webdriver.io/docs/dot-reporter.html
reporters: [
"spec",
[
"junit",
{
outputDir: reportsOutputDir,
outputFileFormat(options) {
return `authentik-${buildNumber}-${options.cid}.xml`;
},
errorOptions: {
failure: "message",
stacktrace: "stack",
},
},
],
],
//
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: "bdd",
timeout: debug ? 24 * 60 * 60 * 1000 : defaultTimeoutInterval,
},
//
// =====
// Hooks
// =====
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
// it and to build services around it. You can either apply a single function or an array of
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
// resolved to continue.
/**
* Gets executed once before all workers get launched.
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
*/
// onPrepare: function (config, capabilities) {
// },
/**
* Gets executed before a worker process is spawned and can be used to initialise specific service
* for that worker as well as modify runtime environments in an async fashion.
* @param {String} cid capability id (e.g 0-0)
* @param {[type]} caps object containing capabilities for session that will be spawn in the worker
* @param {[type]} specs specs to be run in the worker process
* @param {[type]} args object that will be merged with the main configuration once worker is initialised
* @param {[type]} execArgv list of string arguments passed to the worker process
*/
// onWorkerStart: function (cid, caps, specs, args, execArgv) {
// },
/**
* Gets executed just before initialising the webdriver session and test framework. It allows you
* to manipulate configurations depending on the capability or spec.
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
*/
// beforeSession: function (config, capabilities, specs) {
// },
/**
* Gets executed before test execution begins. At this point you can access to all global
* variables like `browser`. It is the perfect place to define custom commands.
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {Object} browser instance of created browser/device session
*/
// before: function (capabilities, specs) {
// },
/**
* Runs before a WebdriverIO command gets executed.
* @param {String} commandName hook command name
* @param {Array} args arguments that command would receive
*/
// beforeCommand: function (commandName, args) {
// },
/**
* Hook that gets executed before the suite starts
* @param {Object} suite suite details
*/
// beforeSuite: function (suite) {
// },
/**
* Function to be executed before a test (in Mocha/Jasmine) starts.
*/
// beforeTest: function (test, context) {
// },
/**
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
* beforeEach in Mocha)
*/
// beforeHook: function (test, context) {
// },
/**
* Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
* afterEach in Mocha)
*/
// afterHook: function (test, context, { error, result, duration, passed, retries }) {
// },
/**
* Function to be executed after a test (in Mocha/Jasmine).
*/
// afterTest: function(test, context, { error, result, duration, passed, retries }) {
// },
/**
* Hook that gets executed after the suite has ended
* @param {Object} suite suite details
*/
// afterSuite: function (suite) {
// },
/**
* Runs after a WebdriverIO command gets executed
* @param {String} commandName hook command name
* @param {Array} args arguments that command would receive
* @param {Number} result 0 - command success, 1 - command error
* @param {Object} error error object if any
*/
// afterCommand: function (commandName, args, result, error) {
// },
/**
* Gets executed after all tests are done. You still have access to all global variables from
* the test.
* @param {Number} result 0 - test pass, 1 - test fail
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
// after: function (result, capabilities, specs) {
// },
/**
* Gets executed right after terminating the webdriver session.
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
// afterSession: function (config, capabilities, specs) {
// },
/**
* Gets executed after all workers got shut down and the process is about to exit. An error
* thrown in the onComplete hook will result in the test run failing.
* @param {Object} exitCode 0 - success, 1 - fail
* @param {Object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {<Object>} results object containing test results
*/
onComplete(exitCode, config, capabilities, results) {
if (exitCode !== 0) {
fs.writeFileSync(path.join(reportsOutputDir, "./failure.txt"), "Tests failed");
}
},
/**
* Gets executed when a refresh happens.
* @param {String} oldSessionId session ID of the old session
* @param {String} newSessionId session ID of the new session
*/
// onReload: function(oldSessionId, newSessionId) {
// }
};

View file

@ -1,30 +1,36 @@
const fs = require("fs");
const path = require("path");
const debug = process.env.DEBUG;
const defaultTimeoutInterval = 200000;
const buildNumber = process.env.BUILD_NUMBER ? process.env.BUILD_NUMBER : "0";
const reportsOutputDir = "./reports";
exports.config = {
import type { Options } from "@wdio/types";
export const config: Options.Testrunner = {
//
// ====================
// Runner Configuration
// ====================
//
// WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or
// on a remote machine).
// WebdriverIO supports running e2e tests as well as unit and component tests.
runner: "local",
autoCompileOpts: {
autoCompile: true,
tsNodeOpts: {
project: "./tsconfig.json",
transpileOnly: true,
},
},
//
// ==================
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
// directory is where your package.json resides, so `wdio` will be called from there.
// of the configuration file being run.
//
specs: ["./tests/*.js"],
// The specs are defined as an array of spec files (optionally using wildcards
// that will be expanded). The test for each spec file will be run in a separate
// worker process. In order to have a group of spec files run in the same worker
// process simply enclose them in an array within the specs array.
//
// If you are calling `wdio` from an NPM script (see https://docs.npmjs.com/cli/run-script),
// then the current working directory is where your `package.json` resides, so `wdio`
// will be called from there.
//
specs: ["./test/specs/**/*.ts"],
// Patterns to exclude.
exclude: [
// 'path/to/excluded/files'
@ -45,27 +51,18 @@ exports.config = {
// and 30 processes will get spawned. The property handles how many capabilities
// from the same test should run tests.
//
maxInstances: 10,
maxInstances: 1,
//
// If you have trouble getting all important capabilities together, check out the
// Sauce Labs platform configurator - a great tool to configure your capabilities:
// https://docs.saucelabs.com/reference/platforms-configurator
// https://saucelabs.com/platform/platform-configurator
//
capabilities: [
{
// maxInstances can get overwritten per capability. So if you have an in-house Selenium
// grid with only 5 firefox instances available you can make sure that not more than
// 5 instances get started at a time.
maxInstances: 1,
//
browserName: "chrome",
acceptInsecureCerts: true,
// If outputDir is provided WebdriverIO can capture driver session logs
// it is possible to configure which logTypes to include/exclude.
// excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs
// excludeDriverLogs: ['bugreport', 'server'],
},
],
//
// ===================
// Test Configurations
@ -73,20 +70,20 @@ exports.config = {
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: "warn",
logLevel: "info",
//
// Set specific log levels per logger
// loggers:
// - webdriver, webdriverio
// - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
// - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
// - @wdio/mocha-framework, @wdio/jasmine-framework
// - @wdio/local-runner
// - @wdio/sumologic-reporter
// - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils
// - @wdio/cli, @wdio/config, @wdio/utils
// Level of logging verbosity: trace | debug | info | warn | error | silent
// logLevels: {
// webdriver: 'info',
// '@wdio/applitools-service': 'info'
// '@wdio/appium-service': 'info'
// },
//
// If you only want to run your tests until a specific amount of tests have failed use
@ -113,11 +110,11 @@ exports.config = {
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
services: ["chromedriver"],
// services: [],
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: https://webdriver.io/docs/frameworks.html
// see also: https://webdriver.io/docs/frameworks
//
// Make sure you have the wdio adapter package for the specific framework installed
// before running any tests.
@ -134,30 +131,15 @@ exports.config = {
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: https://webdriver.io/docs/dot-reporter.html
reporters: [
"spec",
[
"junit",
{
outputDir: reportsOutputDir,
outputFileFormat(options) {
return `authentik-${buildNumber}-${options.cid}.xml`;
},
errorOptions: {
failure: "message",
stacktrace: "stack",
},
},
],
],
// see also: https://webdriver.io/docs/dot-reporter
reporters: ["spec"],
//
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: "bdd",
timeout: debug ? 24 * 60 * 60 * 1000 : defaultTimeoutInterval,
timeout: 60000,
},
//
// =====
@ -169,7 +151,7 @@ exports.config = {
// resolved to continue.
/**
* Gets executed once before all workers get launched.
* @param {Object} config wdio configuration object
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
*/
// onPrepare: function (config, capabilities) {
@ -177,42 +159,52 @@ exports.config = {
/**
* Gets executed before a worker process is spawned and can be used to initialise specific service
* for that worker as well as modify runtime environments in an async fashion.
* @param {String} cid capability id (e.g 0-0)
* @param {[type]} caps object containing capabilities for session that will be spawn in the worker
* @param {[type]} specs specs to be run in the worker process
* @param {[type]} args object that will be merged with the main configuration once worker is initialised
* @param {[type]} execArgv list of string arguments passed to the worker process
* @param {string} cid capability id (e.g 0-0)
* @param {object} caps object containing capabilities for session that will be spawn in the worker
* @param {object} specs specs to be run in the worker process
* @param {object} args object that will be merged with the main configuration once worker is initialized
* @param {object} execArgv list of string arguments passed to the worker process
*/
// onWorkerStart: function (cid, caps, specs, args, execArgv) {
// },
/**
* Gets executed just after a worker process has exited.
* @param {string} cid capability id (e.g 0-0)
* @param {number} exitCode 0 - success, 1 - fail
* @param {object} specs specs to be run in the worker process
* @param {number} retries number of retries used
*/
// onWorkerEnd: function (cid, exitCode, specs, retries) {
// },
/**
* Gets executed just before initialising the webdriver session and test framework. It allows you
* to manipulate configurations depending on the capability or spec.
* @param {Object} config wdio configuration object
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {string} cid worker id (e.g. 0-0)
*/
// beforeSession: function (config, capabilities, specs) {
// beforeSession: function (config, capabilities, specs, cid) {
// },
/**
* Gets executed before test execution begins. At this point you can access to all global
* variables like `browser`. It is the perfect place to define custom commands.
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {Object} browser instance of created browser/device session
* @param {object} browser instance of created browser/device session
*/
// before: function (capabilities, specs) {
// },
/**
* Runs before a WebdriverIO command gets executed.
* @param {String} commandName hook command name
* @param {string} commandName hook command name
* @param {Array} args arguments that command would receive
*/
// beforeCommand: function (commandName, args) {
// },
/**
* Hook that gets executed before the suite starts
* @param {Object} suite suite details
* @param {object} suite suite details
*/
// beforeSuite: function (suite) {
// },
@ -234,30 +226,37 @@ exports.config = {
// afterHook: function (test, context, { error, result, duration, passed, retries }) {
// },
/**
* Function to be executed after a test (in Mocha/Jasmine).
* Function to be executed after a test (in Mocha/Jasmine only)
* @param {object} test test object
* @param {object} context scope object the test was executed with
* @param {Error} result.error error object in case the test fails, otherwise `undefined`
* @param {*} result.result return object of test function
* @param {number} result.duration duration of test
* @param {boolean} result.passed true if test has passed, otherwise false
* @param {object} result.retries information about spec related retries, e.g. `{ attempts: 0, limit: 0 }`
*/
// afterTest: function(test, context, { error, result, duration, passed, retries }) {
// },
/**
* Hook that gets executed after the suite has ended
* @param {Object} suite suite details
* @param {object} suite suite details
*/
// afterSuite: function (suite) {
// },
/**
* Runs after a WebdriverIO command gets executed
* @param {String} commandName hook command name
* @param {string} commandName hook command name
* @param {Array} args arguments that command would receive
* @param {Number} result 0 - command success, 1 - command error
* @param {Object} error error object if any
* @param {number} result 0 - command success, 1 - command error
* @param {object} error error object if any
*/
// afterCommand: function (commandName, args, result, error) {
// },
/**
* Gets executed after all tests are done. You still have access to all global variables from
* the test.
* @param {Number} result 0 - test pass, 1 - test fail
* @param {number} result 0 - test pass, 1 - test fail
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
@ -265,7 +264,7 @@ exports.config = {
// },
/**
* Gets executed right after terminating the webdriver session.
* @param {Object} config wdio configuration object
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
@ -274,20 +273,17 @@ exports.config = {
/**
* Gets executed after all workers got shut down and the process is about to exit. An error
* thrown in the onComplete hook will result in the test run failing.
* @param {Object} exitCode 0 - success, 1 - fail
* @param {Object} config wdio configuration object
* @param {object} exitCode 0 - success, 1 - fail
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {<Object>} results object containing test results
*/
onComplete(exitCode, config, capabilities, results) {
if (exitCode !== 0) {
fs.writeFileSync(path.join(reportsOutputDir, "./failure.txt"), "Tests failed");
}
},
// onComplete: function(exitCode, config, capabilities, results) {
// },
/**
* Gets executed when a refresh happens.
* @param {String} oldSessionId session ID of the old session
* @param {String} newSessionId session ID of the new session
* @param {string} oldSessionId session ID of the old session
* @param {string} newSessionId session ID of the new session
*/
// onReload: function(oldSessionId, newSessionId) {
// }

View file

@ -9,7 +9,7 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { TemplateResult, html, nothing } from "lit";
import { TemplateResult, css, html, nothing } from "lit";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
@ -43,14 +43,40 @@ const idleState: State = { state: "idle", label: "" };
const runningState: State = { state: "running", label: msg("Saving Application...") };
const errorState: State = {
state: "error",
label: msg(html`There was an error in saving your application.<br />The error message was:`),
label: msg("There was an error in saving your application:"),
};
const doneState: State = { state: "done", label: msg("Your application has been saved") };
function extract(o: Record<string, any>): string[] {
function inner(o: Record<string, any>): string[] {
if (typeof o !== "object") {
return [];
}
if (Array.isArray(o)) {
return o;
}
return Object.keys(o)
.map((k) => inner(o[k]))
.flat();
}
return inner(o);
}
@customElement("ak-application-wizard-commit-application")
export class ApplicationWizardCommitApplication extends BasePanel {
static get styles() {
return [...super.styles, PFBullseye, PFEmptyState, PFTitle, PFProgressStepper];
return [
...super.styles,
PFBullseye,
PFEmptyState,
PFTitle,
PFProgressStepper,
css`
.pf-c-title {
padding-bottom: var(--pf-global--spacer--md);
}
`,
];
}
@state()
@ -67,15 +93,15 @@ export class ApplicationWizardCommitApplication extends BasePanel {
this.response = undefined;
this.commitState = runningState;
const provider = providerModelsList.find(
({ formName }) => formName === this.wizard.providerModel,
({ formName }) => formName === this.wizard.providerModel
);
if (!provider) {
throw new Error(
`Could not determine provider model from user request: ${JSON.stringify(
this.wizard,
null,
2,
)}`,
2
)}`
);
}
@ -91,33 +117,25 @@ export class ApplicationWizardCommitApplication extends BasePanel {
}
async send(
data: TransactionApplicationRequest,
data: TransactionApplicationRequest
): Promise<TransactionApplicationResponse | void> {
this.errors = [];
const timeout = new Promise((resolve) => {
setTimeout(resolve, 1200);
});
const network = new CoreApi(DEFAULT_CONFIG).coreTransactionalApplicationsUpdate({
new CoreApi(DEFAULT_CONFIG)
.coreTransactionalApplicationsUpdate({
transactionApplicationRequest: data,
});
Promise.allSettled([network, timeout]).then(([network_resolution]) => {
if (network_resolution.status === "rejected") {
this.commitState = errorState;
return;
}
if (network_resolution.status === "fulfilled") {
if (!network_resolution.value.valid) {
this.commitState = errorState;
this.errors = network_resolution.value.logs;
return;
}
this.response = network_resolution.value;
})
.then((response: TransactionApplicationResponse) => {
this.response = response;
this.dispatchCustomEvent(EVENT_REFRESH);
this.dispatchWizardUpdate({ status: "submitted" });
this.commitState = doneState;
}
})
.catch((resolution: any) => {
resolution.response.json().then((body: Record<string, any>) => {
this.errors = extract(body);
this.commitState = errorState;
});
});
}
@ -135,7 +153,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
${this.errors.length > 0
? html`<ul>
${this.errors.map(
(msg) => html`<li><code>${msg}</code></li>`,
(msg) => html`<li><code>${msg}</code></li>`
)}
</ul>`
: nothing}

View file

@ -7,6 +7,7 @@ import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"
import { html } from "lit";
import { property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
import type { Flow, FlowsInstancesListRequest } from "@goauthentik/api";
@ -123,6 +124,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
.renderElement=${renderElement}
.renderDescription=${renderDescription}
.value=${getFlowValue}
.name=${this.name}
?blankable=${!this.required}
>
</ak-search-select>

View file

@ -83,7 +83,7 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
this.open = false;
this.shadowRoot
?.querySelectorAll<HTMLInputElement>(
".pf-c-form-control.pf-c-select__toggle-typeahead",
".pf-c-form-control.pf-c-select__toggle-typeahead"
)
.forEach((input) => {
input.blur();
@ -134,6 +134,9 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
super.connectedCallback();
this.dropdownContainer = document.createElement("div");
this.dropdownContainer.dataset["managedBy"] = "ak-search-select";
if (this.name) {
this.dropdownContainer.dataset["managedFor"] = this.name;
}
document.body.append(this.dropdownContainer);
this.updateData();
this.addEventListener(EVENT_REFRESH, this.updateData);
@ -261,7 +264,7 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
</ul>
</div>`,
this.dropdownContainer,
{ host: this },
{ host: this }
);
}
@ -303,7 +306,7 @@ export class SearchSelect<T> extends CustomEmitterElement(AKElement) {
// Check if we're losing focus to one of our dropdown items, and if such don't blur
if (ev.relatedTarget instanceof HTMLButtonElement) {
const parentMenu = ev.relatedTarget.closest(
"ul.pf-c-dropdown__menu.pf-m-static",
"ul.pf-c-dropdown__menu.pf-m-static"
);
if (parentMenu && parentMenu.id === this.dropdownUID) {
return;

View file

@ -67,7 +67,7 @@ export class LibraryPage extends AKElement {
this.filteredApps = this.apps?.results;
if (this.filteredApps === undefined) {
throw new Error(
"Application.results should never be undefined when passed to the Library Page.",
"Application.results should never be undefined when passed to the Library Page."
);
}
this.addEventListener(SEARCH_UPDATED, this.searchUpdated);
@ -136,7 +136,9 @@ export class LibraryPage extends AKElement {
render() {
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<div class="pf-c-content header">
<h1 id="library-page-title">${msg("My applications")}</h1>
<h1 role="heading" aria-level="1" id="library-page-title">
${msg("My applications")}
</h1>
${this.uiConfig.searchEnabled ? this.renderSearch() : html``}
</div>
<section class="pf-c-page__main-section">
@ -144,7 +146,7 @@ export class LibraryPage extends AKElement {
this.apps,
html`${this.filteredApps.find(appHasLaunchUrl)
? this.renderApps()
: this.renderEmptyState()}`,
: this.renderEmptyState()}`
)}
</section>
</main>`;