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:
parent
53f89ef2f8
commit
0a43ea286e
3
tests/wdio/.gitignore
vendored
3
tests/wdio/.gitignore
vendored
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
@ -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,
|
||||
};
|
18720
tests/wdio/package-lock.json
generated
18720
tests/wdio/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
13
tests/wdio/test/pageobjects/admin-overview.page.ts
Normal file
13
tests/wdio/test/pageobjects/admin-overview.page.ts
Normal 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();
|
25
tests/wdio/test/pageobjects/admin.page.ts
Normal file
25
tests/wdio/test/pageobjects/admin.page.ts
Normal 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);
|
||||
}
|
||||
}
|
10
tests/wdio/test/pageobjects/application-form.view.ts
Normal file
10
tests/wdio/test/pageobjects/application-form.view.ts
Normal 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();
|
40
tests/wdio/test/pageobjects/application-wizard.page.ts
Normal file
40
tests/wdio/test/pageobjects/application-wizard.page.ts
Normal 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();
|
17
tests/wdio/test/pageobjects/applications-list.page.ts
Normal file
17
tests/wdio/test/pageobjects/applications-list.page.ts
Normal 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();
|
13
tests/wdio/test/pageobjects/ldap-form.view.ts
Normal file
13
tests/wdio/test/pageobjects/ldap-form.view.ts
Normal 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();
|
63
tests/wdio/test/pageobjects/login.page.ts
Normal file
63
tests/wdio/test/pageobjects/login.page.ts
Normal 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();
|
13
tests/wdio/test/pageobjects/oauth-form.view.ts
Normal file
13
tests/wdio/test/pageobjects/oauth-form.view.ts
Normal 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();
|
32
tests/wdio/test/pageobjects/page.ts
Normal file
32
tests/wdio/test/pageobjects/page.ts
Normal 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();
|
||||
}
|
||||
}
|
1
tests/wdio/test/pageobjects/types.ts
Normal file
1
tests/wdio/test/pageobjects/types.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type Constructor<T = object> = new (...args: any[]) => T;
|
22
tests/wdio/test/pageobjects/user-library.page.ts
Normal file
22
tests/wdio/test/pageobjects/user-library.page.ts
Normal 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();
|
35
tests/wdio/test/specs/application-wizard-ldap.ts
Normal file
35
tests/wdio/test/specs/application-wizard-ldap.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
37
tests/wdio/test/specs/application-wizard-oauth.ts
Normal file
37
tests/wdio/test/specs/application-wizard-oauth.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
21
tests/wdio/test/specs/bad-logins.ts
Normal file
21
tests/wdio/test/specs/bad-logins.ts
Normal 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.");
|
||||
});
|
||||
});
|
11
tests/wdio/test/specs/good-login.ts
Normal file
11
tests/wdio/test/specs/good-login.ts
Normal 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");
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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
25
tests/wdio/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
|
@ -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) {
|
||||
// }
|
||||
};
|
|
@ -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) {
|
||||
// }
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>`;
|
||||
|
|
Reference in a new issue