web: fix e2e tests to work with latest WebdriverIO and authentik 2024.8 (#11105)
* web: fix e2e tests to work with latest WebdriverIO and authentik 2024.8 - Adjust the ApplicationWizard "select provider type" typeslist to have the right OUIA tags when running - Add OUIA tags to TypeCreateWizard - Provide default values for `.jwksSources` when needed. - Upgrade E2E WebUI tests to use WebdriverIO 9. - Upgrade the linters to include `package.json` and `package-lock.json`. - Adjust a *lot* of the WebdriverIO selectors! - Provide a driver for the TypeCreate card-based radio interface. - Split `Bad Logins` into two separate files. Aside from the obvious, "because testing needs this" or "because there were warnings on the console when this was running," the real issue is that WebdriverIO 9 has changed the syntax and semantics of its ShadowDOM-piercing `$` mechanism. For Oauth2 and Proxy, the field `.jwksSources` may be undefined, but `undefined` is not a legal value for ak-dual-select's `selected` field. Provide a default or use `ifDefined()`. I chose to provide a default of `[]`. In the previous iteration, `$(">>>ak-search-select input")` would be sufficient for WebdriverIO to find an input inside a component. Now, it needs to be written as: `$("ak-search-select").$("input")`. And in rare cases, when you have a floating component that is separated from its invocation (such as Notification or SearchSelect), even that doesn't work well and you have to fall back to some old-school hacking (see `./tests/wdio/test/pageobjects/page.ts` for an example) to find some child elements. Also, the monadic nature of `$` seems to have faded a bit. `$` used to wrap all child invocations in promises, making the entire expression a single valid promise; it seems that it is now necessary to unwrap the promises yourself under some circumstances, resulting in a lot of `await (await (await ... )))` blocks in the tests. We've slightly changed the semantics of our login mechanism, and now the default behavior is to not reveal when a username is invalid, but to treat the entire login as a single failure mechanism, so as not to expose any details about the username database. The problem arises that now, we (or Chrome) cache the username between roundtrips, and WebdriverIO's second pass was becoming confused by its presence. By putting the Bad Logins into two separate files, I get two separate browser instances with cleared caches, so each test can be run in the pristine environment it needs to validate the behavior I'm expecting. * web: added comment to explain the hack * Add comment to TypeCreateWizardPage to explain the component name hack. * web: fix some lint found by CI/CD
This commit is contained in:
4264
tests/wdio/package-lock.json
generated
4264
tests/wdio/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@goauthentik/web-tests",
|
"name": "@goauthentik/web-tests",
|
||||||
"private": true,
|
"dependencies": {
|
||||||
"type": "module",
|
"chromedriver": "^128.0.0",
|
||||||
|
"lockfile-lint": "^4.14.0",
|
||||||
|
"syncpack": "^13.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||||
|
"@types/mocha": "^10.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
"@typescript-eslint/eslint-plugin": "^7.17.0",
|
||||||
"@typescript-eslint/parser": "^7.17.0",
|
"@typescript-eslint/parser": "^7.17.0",
|
||||||
"@wdio/cli": "^9.0.3",
|
"@wdio/cli": "^9.0.3",
|
||||||
@ -19,19 +23,20 @@
|
|||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"wdio-wait-for": "^3.0.11"
|
"wdio-wait-for": "^3.0.11"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"wdio": "wdio run ./wdio.conf.ts",
|
|
||||||
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
|
|
||||||
"lint": "eslint . --max-warnings 0 --fix",
|
|
||||||
"lint:spelling": "codespell -D - -D $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-dictionary.txt -I $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-words.txt ./test -s",
|
|
||||||
"precommit": "run-s lint:precommit lint:spelling prettier",
|
|
||||||
"prettier-check": "prettier --check .",
|
|
||||||
"prettier": "prettier --write ."
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"private": true,
|
||||||
"chromedriver": "^128.0.0"
|
"scripts": {
|
||||||
}
|
"lint": "eslint . --max-warnings 0 --fix",
|
||||||
|
"lint:lockfile": "lockfile-lint --path package.json --type npm --allowed-hosts npm --validate-https",
|
||||||
|
"lint:package": "syncpack format -i ' '",
|
||||||
|
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
|
||||||
|
"lint:spelling": "codespell -D - -D $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-dictionary.txt -I $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-words.txt ./test -s",
|
||||||
|
"precommit": "run-s lint:precommit lint:spelling prettier",
|
||||||
|
"prettier": "prettier --write .",
|
||||||
|
"prettier-check": "prettier --check .",
|
||||||
|
"wdio": "wdio run ./wdio.conf.ts"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,11 @@
|
|||||||
import Page from "../pageobjects/page.js";
|
import Page from "../pageobjects/page.js";
|
||||||
import { browser } from "@wdio/globals";
|
|
||||||
|
|
||||||
const CLICK_TIME_DELAY = 250;
|
|
||||||
|
|
||||||
export default class AdminPage extends Page {
|
export default class AdminPage extends Page {
|
||||||
public get pageHeader() {
|
public async pageHeader() {
|
||||||
return $('>>>ak-page-header slot[name="header"]');
|
return await $("ak-page-header").$('slot[name="header"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
async openApplicationsListPage() {
|
async openApplicationsListPage() {
|
||||||
await this.open("if/admin/#/core/applications");
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -27,24 +27,24 @@ class ApplicationWizardView extends AdminPage {
|
|||||||
radius = RadiusForm;
|
radius = RadiusForm;
|
||||||
app = ApplicationForm;
|
app = ApplicationForm;
|
||||||
|
|
||||||
get wizardTitle() {
|
async wizardTitle() {
|
||||||
return $(">>>ak-wizard-frame .pf-c-wizard__header h1.pf-c-title");
|
return await $("ak-wizard-frame").$(".pf-c-wizard__title");
|
||||||
}
|
}
|
||||||
|
|
||||||
get providerList() {
|
async providerList() {
|
||||||
return $(">>>ak-application-wizard-authentication-method-choice");
|
return await $("ak-application-wizard-authentication-method-choice");
|
||||||
}
|
}
|
||||||
|
|
||||||
get nextButton() {
|
async nextButton() {
|
||||||
return $(">>>ak-wizard-frame footer button.pf-m-primary");
|
return await $("ak-wizard-frame").$("footer button.pf-m-primary");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProviderType(type: string) {
|
async getProviderType(type: string) {
|
||||||
return await this.providerList.$(`>>>input[value="${type}"]`);
|
return await this.providerList().$(`input[value="${type}"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get successMessage() {
|
async successMessage() {
|
||||||
return $('>>>[data-commit-state="success"]');
|
return await $('[data-commit-state="success"]');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +65,10 @@ const providerValues: Pair[] = [
|
|||||||
providerValues.forEach(([value, name]: Pair) => {
|
providerValues.forEach(([value, name]: Pair) => {
|
||||||
Object.defineProperties(ApplicationWizardView.prototype, {
|
Object.defineProperties(ApplicationWizardView.prototype, {
|
||||||
[name]: {
|
[name]: {
|
||||||
get: function () {
|
get: async function () {
|
||||||
return this.providerList.$(`>>>input[value="${value}"]`);
|
return await (
|
||||||
|
await this.providerList()
|
||||||
|
).$(`div[data-ouid-component-name="${value}"]`);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -9,8 +9,8 @@ class ApplicationsListPage extends AdminPage {
|
|||||||
* define selectors using getter methods
|
* define selectors using getter methods
|
||||||
*/
|
*/
|
||||||
|
|
||||||
get startWizardButton() {
|
async startWizardButton() {
|
||||||
return $('>>>ak-wizard-frame button[slot="trigger"]');
|
return await $("ak-application-wizard").$('button[slot="trigger"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
async open() {
|
async open() {
|
||||||
|
@ -2,16 +2,16 @@ import Page from "../page.js";
|
|||||||
import { $ } from "@wdio/globals";
|
import { $ } from "@wdio/globals";
|
||||||
|
|
||||||
export class ApplicationForm extends Page {
|
export class ApplicationForm extends Page {
|
||||||
get name() {
|
async name() {
|
||||||
return $('>>>ak-form-element-horizontal input[name="name"]');
|
return await $('ak-text-input[name="name"]').$("input");
|
||||||
}
|
}
|
||||||
|
|
||||||
get uiSettings() {
|
async uiSettings() {
|
||||||
return $('>>>ak-form-group button[aria-label="UI Settings"]');
|
return await $("ak-form-group").$('button[aria-label="UI Settings"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
get launchUrl() {
|
async launchUrl() {
|
||||||
return $('>>>input[name="metaLaunchUrl"]');
|
return await $('input[name="metaLaunchUrl"]');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
|
|||||||
export class ForwardProxyForm extends Page {
|
export class ForwardProxyForm extends Page {
|
||||||
async setAuthorizationFlow(selector: string) {
|
async setAuthorizationFlow(selector: string) {
|
||||||
await this.searchSelect(
|
await this.searchSelect(
|
||||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
'ak-flow-search[name="authorizationFlow"]',
|
||||||
"authorizationFlow",
|
"authorizationFlow",
|
||||||
`button*=${selector}`,
|
selector,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get externalHost() {
|
get externalHost() {
|
||||||
return $('>>>input[name="externalHost"]');
|
return $('input[name="externalHost"]');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import Page from "../page.js";
|
import Page from "../page.js";
|
||||||
|
|
||||||
export class LdapForm extends Page {
|
export class LdapForm extends Page {
|
||||||
async setBindFlow(selector: string) {
|
async setBindFlow(_selector: string) {
|
||||||
await this.searchSelect(
|
await this.searchSelect(
|
||||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
'ak-search-select-view[name="authorizationFlow"]',
|
||||||
"authorizationFlow",
|
"authorizationFlow",
|
||||||
`button*=${selector}`,
|
"default-authentication-flow",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
|
|||||||
export class OauthForm extends Page {
|
export class OauthForm extends Page {
|
||||||
async setAuthorizationFlow(selector: string) {
|
async setAuthorizationFlow(selector: string) {
|
||||||
await this.searchSelect(
|
await this.searchSelect(
|
||||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
'ak-flow-search[name="authorizationFlow"]',
|
||||||
"authorizationFlow",
|
"authorizationFlow",
|
||||||
`button*=${selector}`,
|
`${selector}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get providerName() {
|
async providerName() {
|
||||||
return $('>>>ak-form-element-horizontal[name="name"] input');
|
return await $('ak-form-element-horizontal[name="name"]').$("input");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,9 +3,9 @@ import Page from "../page.js";
|
|||||||
export class RadiusForm extends Page {
|
export class RadiusForm extends Page {
|
||||||
async setAuthenticationFlow(selector: string) {
|
async setAuthenticationFlow(selector: string) {
|
||||||
await this.searchSelect(
|
await this.searchSelect(
|
||||||
'>>>ak-branded-flow-search[name="authorizationFlow"] input[type="text"]',
|
'ak-branded-flow-search[name="authorizationFlow"]',
|
||||||
"authorizationFlow",
|
"authorizationFlow",
|
||||||
`button*=${selector}`,
|
selector,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,14 @@ import { $ } from "@wdio/globals";
|
|||||||
export class SamlForm extends Page {
|
export class SamlForm extends Page {
|
||||||
async setAuthorizationFlow(selector: string) {
|
async setAuthorizationFlow(selector: string) {
|
||||||
await this.searchSelect(
|
await this.searchSelect(
|
||||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
'ak-flow-search[name="authorizationFlow"]',
|
||||||
"authorizationFlow",
|
"authorizationFlow",
|
||||||
`button*=${selector}`,
|
selector,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get acsUrl() {
|
get acsUrl() {
|
||||||
return $('>>>input[name="acsUrl"]');
|
return $('input[name="acsUrl"]');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,11 @@ import Page from "../page.js";
|
|||||||
|
|
||||||
export class ScimForm extends Page {
|
export class ScimForm extends Page {
|
||||||
get url() {
|
get url() {
|
||||||
return $('>>>input[name="url"]');
|
return $('input[name="url"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
get token() {
|
get token() {
|
||||||
return $('>>>input[name="token"]');
|
return $('input[name="token"]');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,18 +4,18 @@ import { $ } from "@wdio/globals";
|
|||||||
export class TransparentProxyForm extends Page {
|
export class TransparentProxyForm extends Page {
|
||||||
async setAuthorizationFlow(selector: string) {
|
async setAuthorizationFlow(selector: string) {
|
||||||
await this.searchSelect(
|
await this.searchSelect(
|
||||||
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
|
'ak-flow-search[name="authorizationFlow"]',
|
||||||
"authorizationFlow",
|
"authorizationFlow",
|
||||||
`button*=${selector}`,
|
selector,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get externalHost() {
|
get externalHost() {
|
||||||
return $('>>>input[name="externalHost"]');
|
return $('input[name="externalHost"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
get internalHost() {
|
get internalHost() {
|
||||||
return $('>>>input[name="internalHost"]');
|
return $('input[name="internalHost"]');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,20 +9,20 @@ class LoginPage extends Page {
|
|||||||
/**
|
/**
|
||||||
* Selectors
|
* Selectors
|
||||||
*/
|
*/
|
||||||
get inputUsername() {
|
async inputUsername() {
|
||||||
return $('>>>input[name="uidField"]');
|
return await $('input[name="uidField"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
get inputPassword() {
|
async inputPassword() {
|
||||||
return $('>>>input[name="password"]');
|
return await $('input[name="password"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
get btnSubmit() {
|
async btnSubmit() {
|
||||||
return $('>>>button[type="submit"]');
|
return await $('button[type="submit"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
get authFailure() {
|
async authFailure() {
|
||||||
return $(">>>h4.pf-c-alert__title");
|
return await $(".pf-m-error");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,17 +30,15 @@ class LoginPage extends Page {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async username(username: string) {
|
async username(username: string) {
|
||||||
await this.inputUsername.waitForClickable();
|
await (await this.inputUsername()).setValue(username);
|
||||||
await this.inputUsername.setValue(username);
|
await (await this.btnSubmit()).waitForEnabled();
|
||||||
await this.btnSubmit.waitForEnabled();
|
await (await this.btnSubmit()).click();
|
||||||
await this.btnSubmit.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async password(password: string) {
|
async password(password: string) {
|
||||||
await this.inputPassword.waitForClickable();
|
await (await this.inputPassword()).setValue(password);
|
||||||
await this.inputPassword.setValue(password);
|
await (await this.btnSubmit()).waitForEnabled();
|
||||||
await this.btnSubmit.waitForEnabled();
|
await (await this.btnSubmit()).click();
|
||||||
await this.btnSubmit.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(username: string, password: string) {
|
async login(username: string, password: string) {
|
||||||
@ -48,7 +46,7 @@ class LoginPage extends Page {
|
|||||||
await this.pause();
|
await this.pause();
|
||||||
await this.password(password);
|
await this.password(password);
|
||||||
await this.pause();
|
await this.pause();
|
||||||
await this.pause(">>>div.header h1");
|
await this.pause("div.header h1");
|
||||||
return UserLibraryPage;
|
return UserLibraryPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { browser } from "@wdio/globals";
|
import { browser } from "@wdio/globals";
|
||||||
|
import { Key } from "webdriverio";
|
||||||
|
|
||||||
const CLICK_TIME_DELAY = 250;
|
const CLICK_TIME_DELAY = 250;
|
||||||
|
|
||||||
@ -11,15 +12,15 @@ export default class Page {
|
|||||||
* Opens a sub page of the page
|
* Opens a sub page of the page
|
||||||
* @param path path of the sub page (e.g. /path/to/page.html)
|
* @param path path of the sub page (e.g. /path/to/page.html)
|
||||||
*/
|
*/
|
||||||
public open(path: string) {
|
public async open(path: string) {
|
||||||
return browser.url(`http://localhost:9000/${path}`);
|
return await browser.url(`http://localhost:9000/${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public pause(selector?: string) {
|
public async pause(selector?: string) {
|
||||||
if (selector) {
|
if (selector) {
|
||||||
return $(selector).waitForDisplayed();
|
return await $(selector).waitForDisplayed();
|
||||||
}
|
}
|
||||||
return browser.pause(CLICK_TIME_DELAY);
|
return await browser.pause(CLICK_TIME_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,10 +34,20 @@ export default class Page {
|
|||||||
|
|
||||||
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
|
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
|
||||||
const inputBind = await $(searchSelector);
|
const inputBind = await $(searchSelector);
|
||||||
await inputBind.click();
|
const inputMain = await inputBind.$('input[type="text"]');
|
||||||
const searchBlock = await $(`>>>div[data-managed-for="${managedSelector}"]`);
|
await inputMain.click();
|
||||||
const target = searchBlock.$(buttonSelector);
|
const searchBlock = await (
|
||||||
return await target.click();
|
await $(`div[data-managed-for="${managedSelector}"]`).$("ak-list-select")
|
||||||
|
).shadow$$("button");
|
||||||
|
let target: WebdriverIO.Element;
|
||||||
|
for (const button of searchBlock) {
|
||||||
|
if ((await button.getText()).includes(buttonSelector)) {
|
||||||
|
target = button;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await (await target).click();
|
||||||
|
await browser.keys(Key.Tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async logout() {
|
public async logout() {
|
||||||
|
@ -9,13 +9,13 @@ class UserLibraryPage extends Page {
|
|||||||
* define selectors using getter methods
|
* define selectors using getter methods
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public get pageHeader() {
|
public async pageHeader() {
|
||||||
return $('>>>h1[aria-level="1"]');
|
return await $('h1[aria-level="1"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async goToAdmin() {
|
public async goToAdmin() {
|
||||||
await $('>>>a[href="/if/admin"]').click();
|
await $('a[href="/if/admin"]').click();
|
||||||
await $(">>>ak-admin-overview").waitForDisplayed();
|
return await $("ak-admin-overview").waitForDisplayed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
tests/wdio/test/specs/bad-logins-2.ts
Normal file
15
tests/wdio/test/specs/bad-logins-2.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import LoginPage from "../pageobjects/login.page.js";
|
||||||
|
import { BAD_PASSWORD, GOOD_USERNAME } from "../utils/constants.js";
|
||||||
|
import { expect } from "@wdio/globals";
|
||||||
|
|
||||||
|
describe("Log into authentik", () => {
|
||||||
|
it("should fail on a bad password", async () => {
|
||||||
|
await LoginPage.open();
|
||||||
|
await LoginPage.username(GOOD_USERNAME);
|
||||||
|
await LoginPage.pause();
|
||||||
|
await LoginPage.password(BAD_PASSWORD);
|
||||||
|
const failure = await LoginPage.authFailure();
|
||||||
|
await expect(failure).toBeDisplayedInViewport();
|
||||||
|
await expect(failure).toHaveText("Invalid password");
|
||||||
|
});
|
||||||
|
});
|
@ -1,21 +1,15 @@
|
|||||||
import LoginPage from "../pageobjects/login.page.js";
|
import LoginPage from "../pageobjects/login.page.js";
|
||||||
import { BAD_PASSWORD, BAD_USERNAME, GOOD_USERNAME } from "../utils/constants.js";
|
import { BAD_USERNAME, GOOD_PASSWORD } from "../utils/constants.js";
|
||||||
import { expect } from "@wdio/globals";
|
import { expect } from "@wdio/globals";
|
||||||
|
|
||||||
describe("Log into authentik", () => {
|
describe("Log into authentik", () => {
|
||||||
it("should fail on a bad username", async () => {
|
it("should fail on a bad username", async () => {
|
||||||
await LoginPage.open();
|
await LoginPage.open();
|
||||||
await LoginPage.username(BAD_USERNAME);
|
await LoginPage.username(BAD_USERNAME);
|
||||||
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(GOOD_USERNAME);
|
|
||||||
await LoginPage.pause();
|
await LoginPage.pause();
|
||||||
await LoginPage.password(BAD_PASSWORD);
|
await LoginPage.password(GOOD_PASSWORD);
|
||||||
const failure = await LoginPage.authFailure;
|
const failure = await LoginPage.authFailure();
|
||||||
expect(failure).toHaveText("Failed to authenticate.");
|
await expect(failure).toBeDisplayedInViewport();
|
||||||
|
await expect(failure).toHaveText("Invalid password");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,25 +10,27 @@ async function reachTheProvider(title: string) {
|
|||||||
await ApplicationsListPage.logout();
|
await ApplicationsListPage.logout();
|
||||||
await login();
|
await login();
|
||||||
await ApplicationsListPage.open();
|
await ApplicationsListPage.open();
|
||||||
await expect(await ApplicationsListPage.pageHeader).toHaveText("Applications");
|
await ApplicationsListPage.pause("ak-page-header");
|
||||||
|
await expect(await ApplicationsListPage.pageHeader()).toBeDisplayed();
|
||||||
|
await expect(await ApplicationsListPage.pageHeader()).toHaveText("Applications");
|
||||||
|
|
||||||
await ApplicationsListPage.startWizardButton.click();
|
await (await ApplicationsListPage.startWizardButton()).click();
|
||||||
await ApplicationWizardView.wizardTitle.waitForDisplayed();
|
await (await ApplicationWizardView.wizardTitle()).waitForDisplayed();
|
||||||
await expect(await ApplicationWizardView.wizardTitle).toHaveText("New application");
|
await expect(await ApplicationWizardView.wizardTitle()).toHaveText("New application");
|
||||||
|
|
||||||
await ApplicationWizardView.app.name.setValue(`${title} - ${newPrefix}`);
|
await (await ApplicationWizardView.app.name()).setValue(`${title} - ${newPrefix}`);
|
||||||
await ApplicationWizardView.app.uiSettings.scrollIntoView();
|
await (await ApplicationWizardView.app.uiSettings()).scrollIntoView();
|
||||||
await ApplicationWizardView.app.uiSettings.click();
|
await (await ApplicationWizardView.app.uiSettings()).click();
|
||||||
await ApplicationWizardView.app.launchUrl.scrollIntoView();
|
await (await ApplicationWizardView.app.launchUrl()).scrollIntoView();
|
||||||
await ApplicationWizardView.app.launchUrl.setValue("http://example.goauthentik.io");
|
await (await ApplicationWizardView.app.launchUrl()).setValue("http://example.goauthentik.io");
|
||||||
|
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
return await ApplicationWizardView.pause();
|
return await ApplicationWizardView.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCommitMessage() {
|
async function getCommitMessage() {
|
||||||
await ApplicationWizardView.successMessage.waitForDisplayed();
|
await (await ApplicationWizardView.successMessage()).waitForDisplayed();
|
||||||
return await ApplicationWizardView.successMessage;
|
return await ApplicationWizardView.successMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUCCESS_MESSAGE = "Your application has been saved";
|
const SUCCESS_MESSAGE = "Your application has been saved";
|
||||||
@ -38,97 +40,97 @@ describe("Configure Applications with the Application Wizard", () => {
|
|||||||
it("Should configure a simple LDAP Application", async () => {
|
it("Should configure a simple LDAP Application", async () => {
|
||||||
await reachTheProvider("New LDAP Application");
|
await reachTheProvider("New LDAP Application");
|
||||||
|
|
||||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||||
await ApplicationWizardView.ldapProvider.scrollIntoView();
|
await (await ApplicationWizardView.ldapProvider).scrollIntoView();
|
||||||
await ApplicationWizardView.ldapProvider.click();
|
await (await ApplicationWizardView.ldapProvider).click();
|
||||||
|
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow");
|
await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow");
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should configure a simple Oauth2 Application", async () => {
|
it("Should configure a simple Oauth2 Application", async () => {
|
||||||
await reachTheProvider("New Oauth2 Application");
|
await reachTheProvider("New Oauth2 Application");
|
||||||
|
|
||||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||||
await ApplicationWizardView.oauth2Provider.scrollIntoView();
|
await (await ApplicationWizardView.oauth2Provider).scrollIntoView();
|
||||||
await ApplicationWizardView.oauth2Provider.click();
|
await (await ApplicationWizardView.oauth2Provider).click();
|
||||||
|
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT);
|
await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should configure a simple SAML Application", async () => {
|
it("Should configure a simple SAML Application", async () => {
|
||||||
await reachTheProvider("New SAML Application");
|
await reachTheProvider("New SAML Application");
|
||||||
|
|
||||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||||
await ApplicationWizardView.samlProvider.scrollIntoView();
|
await (await ApplicationWizardView.samlProvider).scrollIntoView();
|
||||||
await ApplicationWizardView.samlProvider.click();
|
await (await ApplicationWizardView.samlProvider).click();
|
||||||
|
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT);
|
await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||||
await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/");
|
await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/");
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should configure a simple SCIM Application", async () => {
|
it("Should configure a simple SCIM Application", async () => {
|
||||||
await reachTheProvider("New SCIM Application");
|
await reachTheProvider("New SCIM Application");
|
||||||
|
|
||||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||||
await ApplicationWizardView.scimProvider.scrollIntoView();
|
await (await ApplicationWizardView.scimProvider).scrollIntoView();
|
||||||
await ApplicationWizardView.scimProvider.click();
|
await (await ApplicationWizardView.scimProvider).click();
|
||||||
|
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await ApplicationWizardView.scim.url.setValue("http://example.com:8000/");
|
await ApplicationWizardView.scim.url.setValue("http://example.com:8000/");
|
||||||
await ApplicationWizardView.scim.token.setValue("a-very-basic-token");
|
await ApplicationWizardView.scim.token.setValue("a-very-basic-token");
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should configure a simple Radius Application", async () => {
|
it("Should configure a simple Radius Application", async () => {
|
||||||
await reachTheProvider("New Radius Application");
|
await reachTheProvider("New Radius Application");
|
||||||
|
|
||||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||||
await ApplicationWizardView.radiusProvider.scrollIntoView();
|
await (await ApplicationWizardView.radiusProvider).scrollIntoView();
|
||||||
await ApplicationWizardView.radiusProvider.click();
|
await (await ApplicationWizardView.radiusProvider).click();
|
||||||
|
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await ApplicationWizardView.radius.setAuthenticationFlow("default-authentication-flow");
|
await ApplicationWizardView.radius.setAuthenticationFlow("default-authentication-flow");
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should configure a simple Transparent Proxy Application", async () => {
|
it("Should configure a simple Transparent Proxy Application", async () => {
|
||||||
await reachTheProvider("New Transparent Proxy Application");
|
await reachTheProvider("New Transparent Proxy Application");
|
||||||
|
|
||||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||||
await ApplicationWizardView.proxyProviderProxy.scrollIntoView();
|
await (await ApplicationWizardView.proxyProviderProxy).scrollIntoView();
|
||||||
await ApplicationWizardView.proxyProviderProxy.click();
|
await (await ApplicationWizardView.proxyProviderProxy).click();
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await ApplicationWizardView.transparentProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
|
await ApplicationWizardView.transparentProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||||
@ -139,19 +141,19 @@ describe("Configure Applications with the Application Wizard", () => {
|
|||||||
"http://internal.example.com",
|
"http://internal.example.com",
|
||||||
);
|
);
|
||||||
|
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should configure a simple Forward Proxy Application", async () => {
|
it("Should configure a simple Forward Proxy Application", async () => {
|
||||||
await reachTheProvider("New Forward Proxy Application");
|
await reachTheProvider("New Forward Proxy Application");
|
||||||
|
|
||||||
await ApplicationWizardView.providerList.waitForDisplayed();
|
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||||
await ApplicationWizardView.proxyProviderForwardsingle.scrollIntoView();
|
await (await ApplicationWizardView.proxyProviderForwardsingle).scrollIntoView();
|
||||||
await ApplicationWizardView.proxyProviderForwardsingle.click();
|
await (await ApplicationWizardView.proxyProviderForwardsingle).click();
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
|
await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||||
@ -159,9 +161,9 @@ describe("Configure Applications with the Application Wizard", () => {
|
|||||||
"http://external.example.com",
|
"http://external.example.com",
|
||||||
);
|
);
|
||||||
|
|
||||||
await ApplicationWizardView.nextButton.click();
|
await (await ApplicationWizardView.nextButton()).click();
|
||||||
await ApplicationWizardView.pause();
|
await ApplicationWizardView.pause();
|
||||||
|
|
||||||
await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -6,5 +6,5 @@ import { expect } from "@wdio/globals";
|
|||||||
export const login = async () => {
|
export const login = async () => {
|
||||||
await LoginPage.open();
|
await LoginPage.open();
|
||||||
await LoginPage.login(GOOD_USERNAME, GOOD_PASSWORD);
|
await LoginPage.login(GOOD_USERNAME, GOOD_PASSWORD);
|
||||||
await expect(UserLibraryPage.pageHeader).toHaveText("My applications");
|
await expect(await UserLibraryPage.pageHeader()).toHaveText("My applications");
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "es2022",
|
"target": "es2022",
|
||||||
"types": ["node", "@wdio/globals/types", "expect-webdriverio", "@wdio/mocha-framework"],
|
"types": ["node", "@wdio/globals/types", "expect-webdriverio", "@wdio/mocha-framework", "@types/mocha"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
@ -21,10 +21,20 @@ export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSumm
|
|||||||
const selectedTypes = providerModelsList.filter(
|
const selectedTypes = providerModelsList.filter(
|
||||||
(t) => t.formName === this.wizard.providerModel,
|
(t) => t.formName === this.wizard.providerModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// As a hack, the Application wizard has separate provider paths for our three types of
|
||||||
|
// proxy providers. This patch swaps the form we want to be directed to on page 3 from the
|
||||||
|
// modelName to the formName, so we get the right one. This information isn't modified
|
||||||
|
// or forwarded, so the proxy-plus-subtype is correctly mapped on submission.
|
||||||
|
const typesForWizard = providerModelsList.map((provider) => ({
|
||||||
|
...provider,
|
||||||
|
modelName: provider.formName,
|
||||||
|
}));
|
||||||
|
|
||||||
return providerModelsList.length > 0
|
return providerModelsList.length > 0
|
||||||
? html`<form class="pf-c-form pf-m-horizontal">
|
? html`<form class="pf-c-form pf-m-horizontal">
|
||||||
<ak-wizard-page-type-create
|
<ak-wizard-page-type-create
|
||||||
.types=${providerModelsList}
|
.types=${typesForWizard}
|
||||||
layout=${TypeCreateWizardPageLayouts.grid}
|
layout=${TypeCreateWizardPageLayouts.grid}
|
||||||
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
||||||
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
||||||
|
@ -265,7 +265,7 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
|
|||||||
>
|
>
|
||||||
<ak-dual-select-provider
|
<ak-dual-select-provider
|
||||||
.provider=${oauth2SourcesProvider}
|
.provider=${oauth2SourcesProvider}
|
||||||
.selected=${provider?.jwksSources}
|
.selected=${provider?.jwksSources ?? []}
|
||||||
available-label=${msg("Available Sources")}
|
available-label=${msg("Available Sources")}
|
||||||
selected-label=${msg("Selected Sources")}
|
selected-label=${msg("Selected Sources")}
|
||||||
></ak-dual-select-provider>
|
></ak-dual-select-provider>
|
||||||
|
@ -230,7 +230,7 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
|||||||
>
|
>
|
||||||
<ak-dual-select-provider
|
<ak-dual-select-provider
|
||||||
.provider=${oauth2SourcesProvider}
|
.provider=${oauth2SourcesProvider}
|
||||||
.selected=${this.instance?.jwksSources}
|
.selected=${this.instance?.jwksSources ?? []}
|
||||||
available-label=${msg("Available Sources")}
|
available-label=${msg("Available Sources")}
|
||||||
selected-label=${msg("Selected Sources")}
|
selected-label=${msg("Selected Sources")}
|
||||||
></ak-dual-select-provider>
|
></ak-dual-select-provider>
|
||||||
|
@ -69,9 +69,19 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderGrid(): TemplateResult {
|
renderGrid(): TemplateResult {
|
||||||
return html`<div class="pf-l-grid pf-m-gutter">
|
return html`<div
|
||||||
|
class="pf-l-grid pf-m-gutter"
|
||||||
|
data-ouid-component-type="ak-type-create-grid"
|
||||||
|
>
|
||||||
${this.types.map((type, idx) => {
|
${this.types.map((type, idx) => {
|
||||||
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
|
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
|
||||||
|
|
||||||
|
// It's valid to pass in a local modelName or the full name with application
|
||||||
|
// part. If the latter, we only want the part after the dot to appear as our
|
||||||
|
// OUIA tag for test automation.
|
||||||
|
const componentName = type.modelName.includes(".")
|
||||||
|
? (type.modelName.split(".")[1] ?? "--unknown--")
|
||||||
|
: type.modelName;
|
||||||
return html`<div
|
return html`<div
|
||||||
class="pf-l-grid__item pf-m-3-col pf-c-card ${requiresEnterprise
|
class="pf-l-grid__item pf-m-3-col pf-c-card ${requiresEnterprise
|
||||||
? "pf-m-non-selectable-raised"
|
? "pf-m-non-selectable-raised"
|
||||||
@ -79,6 +89,8 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
|||||||
? "pf-m-selected-raised"
|
? "pf-m-selected-raised"
|
||||||
: ""}"
|
: ""}"
|
||||||
tabindex=${idx}
|
tabindex=${idx}
|
||||||
|
data-ouid-component-type="ak-type-create-grid-card"
|
||||||
|
data-ouid-component-name=${componentName}
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
if (requiresEnterprise) {
|
if (requiresEnterprise) {
|
||||||
return;
|
return;
|
||||||
@ -107,10 +119,17 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderList(): TemplateResult {
|
renderList(): TemplateResult {
|
||||||
return html`<form class="pf-c-form pf-m-horizontal">
|
return html`<form
|
||||||
|
class="pf-c-form pf-m-horizontal"
|
||||||
|
data-ouid-component-type="ak-type-create-list"
|
||||||
|
>
|
||||||
${this.types.map((type) => {
|
${this.types.map((type) => {
|
||||||
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
|
const requiresEnterprise = type.requiresEnterprise && !this.hasEnterpriseLicense;
|
||||||
return html`<div class="pf-c-radio">
|
return html`<div
|
||||||
|
class="pf-c-radio"
|
||||||
|
data-ouid-component-type="ak-type-create-list-card"
|
||||||
|
data-ouid-component-name=${type.modelName.split(".")[1] ?? "--unknown--"}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
class="pf-c-radio__input"
|
class="pf-c-radio__input"
|
||||||
type="radio"
|
type="radio"
|
||||||
|
Reference in New Issue
Block a user