web: bump API Client version, remove Webdriver dependencies (#16836)

* web: bump API Client version

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* web: Remove WDIO tests.

* web: bump tmp package.

---------

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Teffen Ellis <teffen@goauthentik.io>
This commit is contained in:
authentik-automation[bot]
2025-09-17 18:34:02 +00:00
committed by GitHub
parent 68684d1731
commit a7b02bcef4
54 changed files with 198 additions and 10620 deletions
-40
View File
@@ -1,40 +0,0 @@
/// <reference types="@wdio/globals/types" />
/// <reference types="./types/webdriver.js" />
/**
*
* @param {WebdriverIO.Browser} browser
*/
export function addCommands(browser) {
/**
* @file Custom WDIO browser commands
*/
browser.addCommand(
"focus",
/**
* @this {HTMLElement}
*/
// @ts-ignore
function () {
this.focus();
return this;
},
/* attachToElement */ true,
);
browser.addCommand(
"blur",
/**
* @this {HTMLElement}
*/
// @ts-ignore
function () {
this.blur();
return this;
},
/* attachToElement */ true,
);
}
+188 -7424
View File
File diff suppressed because it is too large Load Diff
+3 -50
View File
@@ -24,12 +24,8 @@
"pseudolocalize": "node ./scripts/pseudolocalize.mjs",
"storybook": "storybook dev -p 6006",
"storybook:build": "wireit",
"test": "wireit",
"test:e2e": "wireit",
"test:e2e:next": "playwright test",
"test:e2e:watch": "wireit",
"test:next": "vitest",
"test:watch": "wireit",
"test": "vitest",
"test:e2e": "playwright test",
"tsc": "wireit",
"watch": "run-s build-locales esbuild:watch"
},
@@ -200,10 +196,6 @@
"@rollup/rollup-darwin-arm64": "^4.50.2",
"@rollup/rollup-linux-arm64-gnu": "^4.50.2",
"@rollup/rollup-linux-x64-gnu": "^4.50.2",
"@wdio/browser-runner": "^9.19.2",
"@wdio/cli": "^9.19.2",
"@wdio/spec-reporter": "^9.19.2",
"@web/test-runner": "^0.20.2",
"chromedriver": "^140.0.2",
"p-iteration": "^1.1.8"
},
@@ -277,17 +269,13 @@
"lint:components": {
"command": "lit-analyzer src"
},
"lint:types:tests": {
"command": "tsc --noEmit -p ./tests"
},
"lint:types": {
"command": "tsc -p .",
"env": {
"NODE_OPTIONS": "--max_old_space_size=8192"
},
"dependencies": [
"build-locales",
"lint:types:tests"
"build-locales"
]
},
"lint:lockfile": {
@@ -313,41 +301,6 @@
"NODE_OPTIONS": "--max_old_space_size=8192"
}
},
"test": {
"command": "wdio ./wdio.conf.mjs --logLevel=warn",
"env": {
"CI": "true",
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:e2e": {
"command": "wdio run ./tests/wdio.conf.mjs",
"dependencies": [
"build"
],
"env": {
"CI": "true",
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
}
},
"test:e2e:watch": {
"command": "wdio run ./tests/wdio.conf.mjs",
"dependencies": [
"build"
],
"env": {
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
}
},
"test:watch": {
"command": "wdio run ./wdio.conf.mjs",
"dependencies": [
"build"
],
"env": {
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"tsc": {
"command": "tsc -p .",
"env": {
+7 -10
View File
@@ -1,7 +1,7 @@
import { type KnipConfig } from "knip";
const config: KnipConfig = {
"entry": [
entry: [
"./src/admin/AdminInterface/AdminInterface.ts",
"./src/user/UserInterface.ts",
"./src/flow/FlowInterface.ts",
@@ -10,18 +10,18 @@ const config: KnipConfig = {
"./src/standalone/loading/index.ts",
"./src/polyfill/poly.ts",
],
"project": ["src/**/*.ts", "src/**/*.js", "./scripts/*.mjs", ".storybook/*.ts"],
project: ["src/**/*.ts", "src/**/*.js", "./scripts/*.mjs", ".storybook/*.ts"],
// "ignore": ["src/**/*.test.ts", "src/**/*.stories.ts"],
// Prevent Knip from complaining about web components, which export their classes but also
// export their registration, and we don't always use both.
"ignoreExportsUsedInFile": true,
"typescript": {
ignoreExportsUsedInFile: true,
typescript: {
config: ["tsconfig.json"],
},
"wireit": {
wireit: {
config: ["package.json"],
},
"storybook": {
storybook: {
config: [".storybook/{main,test-runner}.{js,ts}"],
entry: [
".storybook/{manager,preview}.{js,jsx,ts,tsx}",
@@ -29,7 +29,7 @@ const config: KnipConfig = {
],
project: [".storybook/**/*.{js,jsx,ts,tsx}"],
},
"eslint": {
eslint: {
entry: [
"eslint.config.mjs",
"scripts/eslint.precommit.mjs",
@@ -40,9 +40,6 @@ const config: KnipConfig = {
],
config: ["package.json"],
},
"webdriver-io": {
config: ["wdio.conf.js"],
},
};
export default config;
@@ -1,69 +0,0 @@
import "../AdminSettingsFooterLinks.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
describe("ak-admin-settings-footer-link", () => {
afterEach(async () => {
await browser.execute(async () => {
await document.body.querySelector("ak-admin-settings-footer-link")?.remove();
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body._$litPart$;
}
});
});
it("should render an empty control", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" });
});
it("should not be valid if just a name is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await expect(await link.getProperty("isValid")).toStrictEqual(false);
await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" });
});
it("should be valid if just a URL is filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({
name: "",
href: "https://foo.com",
});
});
it("should be valid if both are filled in", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("https://foo.com");
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual({
name: "foo",
href: "https://foo.com",
});
});
it("should not be valid if the URL is not valid", async () => {
render(html`<ak-admin-settings-footer-link name="link"></ak-admin-settings-footer-link>`);
const link = await $("ak-admin-settings-footer-link");
await link.$('input[name="name"]').setValue("foo");
await link.$('input[name="href"]').setValue("never://foo.com");
await expect(await link.getProperty("toJson")).toEqual({
name: "foo",
href: "never://foo.com",
});
await expect(await link.getProperty("isValid")).toStrictEqual(false);
});
});
@@ -1,154 +0,0 @@
import "./EnterpriseStatusCard.js";
import { render } from "#elements/tests/utils";
import { LicenseForecast, LicenseSummary, LicenseSummaryStatusEnum } from "@goauthentik/api";
import { $, expect } from "@wdio/globals";
import { msg } from "@lit/localize";
import { html } from "lit";
describe("ak-enterprise-status-card", () => {
it("should not error when no data is loaded", async () => {
render(html`<ak-enterprise-status-card></ak-enterprise-status-card>`);
const status = await $("ak-enterprise-status-card");
await expect(status).toHaveText(msg("Loading"));
});
it("should render empty when unlicensed", async () => {
const forecast: LicenseForecast = {
externalUsers: 123,
internalUsers: 123,
forecastedExternalUsers: 123,
forecastedInternalUsers: 123,
};
const summary: LicenseSummary = {
status: LicenseSummaryStatusEnum.Unlicensed,
internalUsers: 0,
externalUsers: 0,
latestValid: new Date(0),
licenseFlags: [],
};
render(
html`<ak-enterprise-status-card .forecast=${forecast} .summary=${summary}>
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Unlicensed"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "0");
const externalUserProgress = await $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "0");
});
it("should show warnings when full", async () => {
const forecast: LicenseForecast = {
externalUsers: 123,
internalUsers: 123,
forecastedExternalUsers: 123,
forecastedInternalUsers: 123,
};
const summary: LicenseSummary = {
status: LicenseSummaryStatusEnum.Valid,
internalUsers: 123,
externalUsers: 123,
latestValid: new Date(),
licenseFlags: [],
};
render(
html`<ak-enterprise-status-card .forecast=${forecast} .summary=${summary}>
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Valid"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
const externalUserProgress = await $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
await expect(
await $("ak-enterprise-status-card").$(">>>#externalUsers"),
).toHaveElementClass("pf-m-warning");
});
it("should show infinity when not licensed for a user type", async () => {
const forecast: LicenseForecast = {
externalUsers: 123,
internalUsers: 123,
forecastedExternalUsers: 123,
forecastedInternalUsers: 123,
};
const summary: LicenseSummary = {
status: LicenseSummaryStatusEnum.Valid,
internalUsers: 123,
externalUsers: 0,
latestValid: new Date(),
licenseFlags: [],
};
render(
html`<ak-enterprise-status-card .forecast=${forecast} .summary=${summary}>
</ak-enterprise-status-card>`,
);
const status = await $("ak-enterprise-status-card").$(
">>>.pf-c-description-list__description > .pf-c-description-list__text",
);
await expect(status).toExist();
await expect(status).toHaveText(msg("Valid"));
const internalUserProgress = await $("ak-enterprise-status-card").$(
">>>#internalUsers > .pf-c-progress__bar",
);
await expect(internalUserProgress).toExist();
await expect(internalUserProgress).toHaveAttr("aria-valuenow", "100");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
const externalUserProgress = await $("ak-enterprise-status-card").$(
">>>#externalUsers > .pf-c-progress__bar",
);
await expect(externalUserProgress).toExist();
await expect(externalUserProgress).toHaveAttr("aria-valuenow", "∞");
await expect(
await $("ak-enterprise-status-card").$(">>>#internalUsers"),
).toHaveElementClass("pf-m-warning");
await expect(
await $("ak-enterprise-status-card").$(">>>#externalUsers"),
).toHaveElementClass("pf-m-danger");
});
});
@@ -1,149 +0,0 @@
import "../ak-select-table.js";
import { nutritionDbUSDA as unsortedNutritionDbUSDA } from "../stories/sample_nutrition_db.js";
import { render } from "#elements/tests/utils";
import { $, browser } from "@wdio/globals";
import { expect } from "expect-webdriverio";
import { slug } from "github-slugger";
import { html } from "lit";
type SortableRecord = Record<string, string | number>;
const dbSort = (a: SortableRecord, b: SortableRecord) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
const alphaSort = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
const nutritionDbUSDA = unsortedNutritionDbUSDA.toSorted(dbSort);
const columns = ["Name", "Calories", "Protein", "Fiber", "Sugar"];
const content = nutritionDbUSDA.map(({ name, calories, sugar, fiber, protein }) => ({
key: slug(name),
content: [name, calories, protein, fiber, sugar].map((a) => html`${a}`),
}));
const item3 = nutritionDbUSDA[2];
describe("Select Table", () => {
let selecttable: WebdriverIO.Element;
let table: WebdriverIO.Element;
beforeEach(async () => {
await render(
html`<ak-select-table .content=${content} .columns=${columns}> </ak-select-table>`,
document.body,
);
// @ts-ignore
selecttable = await $("ak-select-table");
// @ts-ignore
table = await selecttable.$(">>>table");
});
it("it should render a select table", async () => {
expect(table).toBeDisplayed();
});
it("the table should have as many entries as the data source", async () => {
const rows = await table.$(">>>tbody").$$(">>>tr");
expect(rows.length).toBe(content.length);
});
it(`the third item ought to have the name ${item3.name}`, async () => {
const rows = await table.$(">>>tbody").$$(">>>tr");
const cells = await rows[2].$$(">>>td");
const cell1Text = await cells[1].getText();
expect(cell1Text).toEqual(item3.name);
});
it("Selecting one item ought to result in the value of the table being set", async () => {
const rows = await table.$(">>>tbody").$$(">>>tr");
const control = await rows[2].$$(">>>td")[0].$(">>>input");
await control.click();
expect(await selecttable.getValue()).toEqual(slug(item3.name));
});
afterEach(async () => {
await browser.execute(() => {
document.body.querySelector("ak-select-table")?.remove();
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
delete document.body._$litPart$;
}
});
});
});
describe("Multiselect Table", () => {
let selecttable: WebdriverIO.Element;
let table: WebdriverIO.Element;
beforeEach(async () => {
await render(
html`<ak-select-table multiple .content=${content} .columns=${columns}>
</ak-select-table>`,
document.body,
);
// @ts-ignore
selecttable = await $("ak-select-table");
// @ts-ignore
table = await selecttable.$(">>>table");
});
it("it should render the select-all control", async () => {
const thead = await table.$(">>>thead");
const selall = await thead.$$(">>>tr")[0].$$(">>>td")[0];
if (selall === undefined) {
throw new Error("Could not find table header");
}
const input = await selall.$(">>>input");
expect(await input.getProperty("name")).toEqual("select-all-input");
});
it("it should set the value when one input is clicked", async () => {
const input = await table.$(">>>tbody").$$(">>>tr")[2].$$(">>>td")[0].$(">>>input");
await input.click();
expect(await selecttable.getValue()).toEqual(slug(nutritionDbUSDA[2].name));
});
it("it should select all when that control is clicked", async () => {
const selall = await table.$(">>>thead").$$(">>>tr")[0].$$(">>>td")[0];
if (selall === undefined) {
throw new Error("Could not find table header");
}
const input = await selall.$(">>>input");
await input.click();
const value = await selecttable.getValue();
const values = value.split(";").toSorted(alphaSort).join(";");
const expected = nutritionDbUSDA.map((a) => slug(a.name)).join(";");
expect(values).toEqual(expected);
});
it("it should clear all when that control is clicked twice", async () => {
const selall = await table.$(">>>thead").$$(">>>tr")[0].$$(">>>td")[0];
if (selall === undefined) {
throw new Error("Could not find table header");
}
const input = await selall.$(">>>input");
await input.click();
const value = await selecttable.getValue();
const values = value.split(";").toSorted(alphaSort).join(";");
const expected = nutritionDbUSDA.map((a) => slug(a.name)).join(";");
expect(values).toEqual(expected);
await input.click();
const newvalue = await selecttable.getValue();
expect(newvalue).toEqual("");
});
afterEach(async () => {
await browser.execute(() => {
document.body.querySelector("ak-select-table")?.remove();
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
delete document.body._$litPart$;
}
});
});
});
@@ -1,51 +0,0 @@
import "#elements/ak-table/ak-simple-table";
import { nutritionDbUSDA } from "../stories/sample_nutrition_db.js";
import { render } from "#elements/tests/utils";
import { $, browser } from "@wdio/globals";
import { expect } from "expect-webdriverio";
import { slug } from "github-slugger";
import { html } from "lit";
const columns = ["Name", "Calories", "Protein", "Fiber", "Sugar"];
const content = nutritionDbUSDA.map(({ name, calories, sugar, fiber, protein }) => ({
key: slug(name),
content: [name, calories, protein, fiber, sugar].map((a) => html`${a}`),
}));
describe("Simple Table", () => {
let table: WebdriverIO.Element;
beforeEach(async () => {
await render(
html`<ak-simple-table .content=${content} .columns=${columns}> </ak-simple-table>`,
document.body,
);
// @ts-ignore
table = await $("ak-simple-table").$(">>>table");
});
it("it should render a simple table", async () => {
expect(table).toBeDisplayed();
});
it("the table should have as many entries as the data source", async () => {
const tbody = await table.$(">>>tbody");
const rows = await tbody.$$(">>>tr");
expect(rows.length).toBe(content.length);
});
afterEach(async () => {
await browser.execute(() => {
document.body.querySelector("ak-simple-table")?.remove();
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
delete document.body._$litPart$;
}
});
});
});
@@ -1,71 +0,0 @@
import "../AggregateCard.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
describe("ak-aggregate-card", () => {
it("should render the standard card without an icon, link, or subtext", async () => {
render(
html`<ak-aggregate-card header="Loading"
><p>This is the main content</p></ak-aggregate-card
>`,
);
const component = await $("ak-aggregate-card");
await expect(await component.$(">>>.pf-c-card__header a")).not.toExist();
await expect(await component.$(">>>.pf-c-card__title i")).not.toExist();
await expect(await component.$(">>>.pf-c-card__title")).toHaveText("Loading");
await expect(await component.$(">>>.pf-c-card__body")).toHaveText(
"This is the main content",
);
await expect(await component.$(">>>.subtext")).not.toExist();
});
it("should render the standard card with an icon", async () => {
render(
html`<ak-aggregate-card icon="fa fa-bath" header="Loading"
><p>This is the main content</p></ak-aggregate-card
>`,
);
const component = await $("ak-aggregate-card");
await expect(await component.$(">>>.pf-c-card__title i")).toExist();
await expect(await component.$(">>>.pf-c-card__title")).toHaveText("Loading");
await expect(await component.$(">>>.pf-c-card__body")).toHaveText(
"This is the main content",
);
});
it("should render the standard card with an icon, a link, and slotted content", async () => {
render(
html`<ak-aggregate-card icon="fa fa-bath" header="Loading" headerLink="http://localhost"
><p>This is the main content</p></ak-aggregate-card
>`,
);
const component = await $("ak-aggregate-card");
await expect(await component.$(">>>.pf-c-card__header a")).toExist();
await expect(await component.$(">>>.pf-c-card__title i")).toExist();
await expect(await component.$(">>>.pf-c-card__title")).toHaveText("Loading");
await expect(await component.$(">>>.pf-c-card__body")).toHaveText(
"This is the main content",
);
});
it("should render the standard card with an icon, a link, and subtext", async () => {
render(
html`<ak-aggregate-card
icon="fa fa-bath"
header="Loading"
headerLink="http://localhost"
subtext="Xena had subtext"
><p>This is the main content</p></ak-aggregate-card
>`,
);
const component = await $("ak-aggregate-card");
await expect(await component.$(">>>.pf-c-card__header a")).toExist();
await expect(await component.$(">>>.pf-c-card__title i")).toExist();
await expect(await component.$(">>>.pf-c-card__title")).toHaveText("Loading");
await expect(await component.$(">>>.subtext")).toHaveText("Xena had subtext");
});
});
@@ -1,51 +0,0 @@
import "../AggregatePromiseCard.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
const DELAY = 1000; // milliseconds
describe("ak-aggregate-card-promise", () => {
it("should render the promise card and display the message after a 1 second timeout", async () => {
const text = "RESULT";
const runThis = (timeout: number, value: string) =>
new Promise((resolve, _reject) => setTimeout(resolve, timeout, value));
const promise = runThis(DELAY, text);
render(html`<ak-aggregate-card-promise .promise=${promise}></ak-aggregate-card-promise>`);
const component = await $("ak-aggregate-card-promise");
// Assert we're in pre-resolve mode
await expect(await component.$(">>>.pf-c-card__header a")).not.toExist();
await expect(await component.$(">>>ak-spinner")).toExist();
await promise;
await expect(await component.$(">>>ak-spinner")).not.toExist();
await expect(await component.$(">>>.pf-c-card__body")).toHaveText("RESULT");
});
it("should render the promise card and display failure after a 1 second timeout", async () => {
const text = "EXPECTED FAILURE";
const runThis = (timeout: number, value: string) =>
new Promise((_resolve, reject) => setTimeout(reject, timeout, value));
const promise = runThis(DELAY, text);
render(
html`<ak-aggregate-card-promise
.promise=${promise}
failureMessage=${text}
></ak-aggregate-card-promise>`,
);
const component = await $("ak-aggregate-card-promise");
// Assert we're in pre-resolve mode
await expect(await component.$(">>>.pf-c-card__header a")).not.toExist();
await expect(await component.$(">>>ak-spinner")).toExist();
try {
await promise;
} catch (_e: unknown) {
await expect(await component.$(">>>ak-spinner")).not.toExist();
await expect(await component.$(">>>.pf-c-card__body")).toHaveText(text);
}
});
});
@@ -1,35 +0,0 @@
import "../QuickActionsCard.js";
import { QuickAction } from "../QuickActionsCard.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
const ACTIONS: QuickAction[] = [
["Create a new application", "/core/applications"],
["Check the logs", "/events/log"],
["Explore integrations", "https://integrations.goauthentik.io/", true],
["Manage users", "/identity/users"],
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
];
describe("ak-quick-actions-card", () => {
it("display ak-quick-actions-card", async () => {
render(
html`<ak-quick-actions-card
title="Alt Title"
.actions=${ACTIONS}
></ak-quick-actions-card>`,
);
const component = await $("ak-quick-actions-card");
const items = await component.$$(">>>.pf-c-list li");
// @ts-expect-error "Another ChainablePromise mistake"
await expect(Array.from(items).length).toEqual(5);
await expect(await component.$(">>>.pf-c-list li:nth-of-type(4)")).toHaveText(
"Manage users",
);
});
});
@@ -1,75 +0,0 @@
import { $, browser } from "@wdio/globals";
browser.addCommand(
"focus",
function () {
browser.execute(function (domElement) {
domElement.focus();
// @ts-ignore
}, this);
},
true,
);
/**
* Search Select View Driver
*
* This class provides a collection of easy-to-use methods for interacting with and examining the
* results of an interaction with an `ak-search-select-view` via WebdriverIO.
*
* It's hoped that with the OUIA tags, we could automate testing further. The OUIA tag would
* instruct the test harness "use this driver to test this component," and we could test Forms and
* Tables with a small DSL of test language concepts
*/
export class AkSearchSelectViewDriver {
constructor(
public element: WebdriverIO.Element,
public menu: WebdriverIO.Element,
) {
/* no op */
}
static async build(element: WebdriverIO.Element) {
const tagname = await element.getTagName();
const comptype = await element.getAttribute("data-ouia-component-type");
if (comptype !== "ak-search-select-view") {
throw new Error(
`SearchSelectView driver passed incorrect component. Expected ak-search-select-view, got ${comptype ? `'${comptype}` : `No test data type, tag name: '${tagname}'`}`,
);
}
const id = await element.getAttribute("data-ouia-component-id");
const menu = await $(`[data-ouia-component-id="menu-${id}"]`);
// @ts-expect-error "Another ChainablePromise mistake"
return new AkSearchSelectViewDriver(element, menu);
}
get open() {
return this.element.getProperty("open");
}
async input() {
return await this.element.$(">>>input");
}
async listElements() {
return await this.menu.$$(">>>li");
}
async focusOnInput() {
// @ts-ignore
await (await this.input()).focus();
}
async inputIsVisible() {
return await this.element.$(">>>input").isDisplayed();
}
async menuIsVisible() {
return (await this.menu.isExisting()) && (await this.menu.isDisplayed());
}
async clickInput() {
return await (await this.input()).click();
}
}
@@ -1,113 +0,0 @@
import "../ak-search-select-view.js";
import { sampleData } from "../stories/sampleData.js";
import { AkSearchSelectViewDriver } from "./ak-search-select-view.comp.js";
import { render } from "#elements/tests/utils";
import { $, browser, expect } from "@wdio/globals";
import { slug } from "github-slugger";
import { Key } from "webdriverio";
import { html } from "lit";
const longGoodForYouPairs = {
grouped: false,
options: sampleData.map(({ produce }) => [slug(produce), produce]),
};
describe("Search select: Test Input Field", () => {
let select: AkSearchSelectViewDriver;
beforeEach(async () => {
render(
html`<ak-search-select-view .options=${longGoodForYouPairs}> </ak-search-select-view>`,
document.body,
);
// @ts-expect-error "Another ChainablePromise mistake"
select = await AkSearchSelectViewDriver.build(await $("ak-search-select-view"));
});
it("should open the menu when the input is clicked", async () => {
expect(await select.open).toBe(false);
expect(await select.menuIsVisible()).toBe(false);
await select.clickInput();
expect(await select.open).toBe(true);
// expect(await select.menuIsVisible()).toBe(true);
});
it("should not open the menu when the input is focused", async () => {
expect(await select.open).toBe(false);
await select.focusOnInput();
expect(await select.open).toBe(false);
expect(await select.menuIsVisible()).toBe(false);
});
it("should close the menu when the input is clicked a second time", async () => {
expect(await select.open).toBe(false);
expect(await select.menuIsVisible()).toBe(false);
await select.clickInput();
expect(await select.menuIsVisible()).toBe(true);
expect(await select.open).toBe(true);
await select.clickInput();
expect(await select.open).toBe(false);
expect(await select.open).toBe(false);
});
it("should open the menu from a focused but closed input when a search is begun", async () => {
expect(await select.open).toBe(false);
await select.focusOnInput();
expect(await select.open).toBe(false);
expect(await select.menuIsVisible()).toBe(false);
await browser.keys("A");
// @ts-expect-error "Another ChainablePromise mistake"
select = await AkSearchSelectViewDriver.build(await $("ak-search-select-view"));
expect(await select.open).toBe(true);
expect(await select.menuIsVisible()).toBe(true);
});
it("should update the list as the user types", async () => {
await select.focusOnInput();
await browser.keys("Ap");
await expect(await select.menuIsVisible()).toBe(true);
// @ts-expect-error "Another ChainablePromise mistake"
const elements = Array.from(await select.listElements());
await expect(elements.length).toBe(2);
});
it("set the value when a match is close", async () => {
await select.focusOnInput();
await browser.keys("Ap");
await expect(await select.menuIsVisible()).toBe(true);
// @ts-expect-error "Another ChainablePromise mistake"
const elements = Array.from(await select.listElements());
await expect(elements.length).toBe(2);
await browser.keys(Key.Tab);
await expect(await (await select.input()).getValue()).toBe("Apples");
});
it("should close the menu when the user clicks away", async () => {
document.body.insertAdjacentHTML(
"afterbegin",
'<input id="a-separate-component" type="text" />',
);
const input = await browser.$("#a-separate-component");
await select.clickInput();
expect(await select.open).toBe(true);
await input.click();
expect(await select.open).toBe(false);
});
afterEach(async () => {
await browser.execute(() => {
document.body.querySelector("#a-separate-component")?.remove();
document.body.querySelector("ak-search-select-view")?.remove();
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
delete document.body._$litPart$;
}
});
});
});
@@ -1,117 +0,0 @@
import "../ak-search-select.js";
import { SearchSelect } from "../ak-search-select.js";
import { sampleData, type ViewSample } from "../stories/sampleData.js";
import { AkSearchSelectViewDriver } from "./ak-search-select-view.comp.js";
/* eslint-env jest */
import { AKElement } from "#elements/Base";
import { bound } from "#elements/decorators/bound";
import { render } from "#elements/tests/utils";
import { CustomListenerElement } from "#elements/utils/eventEmitter";
import { $, browser, expect } from "@wdio/globals";
import { slug } from "github-slugger";
import { html } from "lit";
import { customElement, property, query } from "lit/decorators.js";
const renderElement = (fruit: ViewSample) => fruit.produce;
const renderDescription = (fruit: ViewSample) => html`${fruit.desc}`;
const renderValue = (fruit: ViewSample | undefined) => slug(fruit?.produce ?? "");
@customElement("ak-mock-search-group")
export class MockSearch extends CustomListenerElement(AKElement) {
/**
* The current fruit
*
* @attr
*/
@property({ type: String, reflect: true })
fruit?: string;
@query("ak-search-select")
search!: SearchSelect<ViewSample>;
selectedFruit?: ViewSample;
get value() {
return this.selectedFruit ? renderValue(this.selectedFruit) : undefined;
}
@bound
handleSearchUpdate(ev: CustomEvent) {
ev.stopPropagation();
this.selectedFruit = ev.detail.value;
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
}
@bound
selected(fruit: ViewSample) {
return this.fruit === slug(fruit.produce);
}
@bound
fetchObjects() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolver = (resolve: any) => {
this.addEventListener("resolve", () => {
resolve(sampleData);
});
};
return new Promise(resolver);
}
render() {
return html`
<ak-search-select
.fetchObjects=${this.fetchObjects}
.renderElement=${renderElement}
.renderDescription=${renderDescription}
.value=${renderValue}
.selected=${this.selected}
managed
@ak-change=${this.handleSearchUpdate}
blankable
>
</ak-search-select>
`;
}
}
describe("Search select: event driven startup", () => {
let select: AkSearchSelectViewDriver;
let wrapper: SearchSelect<ViewSample>;
beforeEach(async () => {
await render(html`<ak-mock-search-group></ak-mock-search-group>`, document.body);
// @ts-ignore
wrapper = await $(">>>ak-search-select");
});
it("should shift from the loading indicator to search select view on fetch event completed", async () => {
expect(await wrapper).toBeExisting();
expect(await $(">>>ak-search-select-loading-indicator")).toBeDisplayed();
await browser.execute(() => {
const mock = document.querySelector("ak-mock-search-group");
mock?.dispatchEvent(new Event("resolve"));
});
expect(await $(">>>ak-search-select-loading-indicator")).not.toBeDisplayed();
// @ts-expect-error "Another ChainablePromise mistake"
select = await AkSearchSelectViewDriver.build(await $(">>>ak-search-select-view"));
expect(await select).toBeExisting();
});
afterEach(async () => {
await browser.execute(() => {
document.body.querySelector("ak-mock-search-group")?.remove();
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
delete document.body._$litPart$;
}
});
});
});
-39
View File
@@ -1,39 +0,0 @@
import "../Alert.js";
import { akAlert, Level } from "../Alert.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
describe("ak-alert", () => {
it("should render an alert with the enum", async () => {
render(html`<ak-alert level=${Level.Info}>This is an alert</ak-alert>`, document.body);
await expect(await $("ak-alert").$("div")).not.toHaveElementClass("pf-m-inline");
await expect(await $("ak-alert").$("div")).toHaveElementClass("pf-c-alert");
await expect(await $("ak-alert").$("div")).toHaveElementClass("pf-m-info");
await expect(await $("ak-alert").$(".pf-c-alert__title")).toHaveText("This is an alert");
});
it("should render an alert with the attribute", async () => {
render(html`<ak-alert level="info">This is an alert</ak-alert>`, document.body);
await expect(await $("ak-alert").$("div")).toHaveElementClass("pf-m-info");
await expect(await $("ak-alert").$(".pf-c-alert__title")).toHaveText("This is an alert");
});
it("should render an alert with an inline class and the default level", async () => {
render(html`<ak-alert inline>This is an alert</ak-alert>`, document.body);
await expect(await $("ak-alert").$("div")).toHaveElementClass("pf-m-warning");
await expect(await $("ak-alert").$("div")).toHaveElementClass("pf-m-inline");
await expect(await $("ak-alert").$(".pf-c-alert__title")).toHaveText("This is an alert");
});
it("should render an alert as a function call", async () => {
render(akAlert({ level: "info" }, "This is an alert"));
await expect(await $("ak-alert").$("div")).toHaveElementClass("pf-m-info");
await expect(await $("ak-alert").$(".pf-c-alert__title")).toHaveText("This is an alert");
});
});
-37
View File
@@ -1,37 +0,0 @@
import "../Divider.js";
import { akDivider } from "../Divider.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
describe("ak-divider", () => {
it("should render the divider", async () => {
render(html`<ak-divider></ak-divider>`);
const empty = await $("ak-divider");
await expect(empty).toExist();
});
it("should render the divider with the specified text", async () => {
render(html`<ak-divider><span>Your Message Here</span></ak-divider>`);
const span = await $("ak-divider").$(">>>span");
await expect(span).toExist();
await expect(span).toHaveText("Your Message Here");
});
it("should render the divider as a function with the specified text", async () => {
render(akDivider("Your Message As A Function"));
const divider = await $("ak-divider");
await expect(divider).toExist();
await expect(divider).toHaveText("Your Message As A Function");
});
it("should render the divider as a function", async () => {
render(akDivider());
const empty = await $("ak-divider");
await expect(empty).toExist();
});
});
-94
View File
@@ -1,94 +0,0 @@
import "../EmptyState.js";
import { akEmptyState } from "../EmptyState.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { msg } from "@lit/localize";
import { html } from "lit";
describe("ak-empty-state", () => {
afterEach(async () => {
await browser.execute(async () => {
await document.body.querySelector("ak-empty-state")?.remove();
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body._$litPart$;
}
});
});
it("should render the default loader", async () => {
render(html`<ak-empty-state default-label></ak-empty-state>`);
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist();
const header = await $("ak-empty-state").$(">>>.pf-c-title");
await expect(header).toHaveText("Loading");
});
it("should handle standard boolean", async () => {
render(html`<ak-empty-state loading>Waiting</ak-empty-state>`);
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist();
const header = await $("ak-empty-state").$(">>>.pf-c-title");
await expect(header).toHaveText("Waiting");
});
it("should render a static empty state", async () => {
render(html`<ak-empty-state><span>${msg("No messages found")}</span> </ak-empty-state>`);
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist();
await expect(empty).toHaveClass("fa-question-circle");
const header = await $("ak-empty-state").$(">>>.pf-c-title");
await expect(header).toHaveText("No messages found");
});
it("should render a slotted message", async () => {
render(
html`<ak-empty-state
><span>${msg("No messages found")}</span>
<p slot="body">Try again with a different filter</p>
</ak-empty-state>`,
);
const message = await $("ak-empty-state").$(">>>.pf-c-empty-state__body").$(">>>p");
await expect(message).toHaveText("Try again with a different filter");
});
it("should render as a function call", async () => {
render(akEmptyState({ loading: true }, "Being Thoughtful"));
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist();
const header = await $("ak-empty-state").$(">>>.pf-c-empty-state__body");
await expect(header).toHaveText("Being Thoughtful");
});
it("should render as a complex function call", async () => {
render(
akEmptyState(
{ loading: true },
html` <span slot="body">Introspecting</span>
<span slot="primary">... carefully</span>`,
),
);
const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon");
await expect(empty).toExist();
const header = await $("ak-empty-state").$(">>>.pf-c-empty-state__body");
await expect(header).toHaveText("Introspecting");
const primary = await $("ak-empty-state").$(">>>.pf-c-empty-state__primary");
await expect(primary).toHaveText("... carefully");
});
});
-95
View File
@@ -1,95 +0,0 @@
import "../Expand.js";
import { akExpand } from "../Expand.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
describe("ak-expand", () => {
afterEach(async () => {
await browser.execute(async () => {
await document.body.querySelector("ak-expand")?.remove();
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body._$litPart$;
}
});
});
it("should render the expansion content hidden by default", async () => {
render(html`<ak-expand><p>This is the expanded text</p></ak-expand>`);
const text = await $("ak-expand").$(">>>.pf-c-expandable-section__content");
await expect(text).not.toBeDisplayed();
});
it("should render the expansion content visible on demand", async () => {
render(html`<ak-expand expanded><p>This is the expanded text</p></ak-expand>`);
const paragraph = await $("ak-expand").$(">>>p");
await expect(paragraph).toExist();
await expect(paragraph).toBeDisplayed();
await expect(paragraph).toHaveText("This is the expanded text");
});
it("should respond to the click event", async () => {
render(html`<ak-expand><p>This is the expanded text</p></ak-expand>`);
let content = await $("ak-expand").$(">>>.pf-c-expandable-section__content");
await expect(content).toExist();
await expect(content).not.toBeDisplayed();
const control = await $("ak-expand").$(">>>button");
await control.click();
content = await $("ak-expand").$(">>>.pf-c-expandable-section__content");
await expect(content).toExist();
await expect(content).toBeDisplayed();
await control.click();
content = await $("ak-expand").$(">>>.pf-c-expandable-section__content");
await expect(content).toExist();
await expect(content).not.toBeDisplayed();
});
it("should honor the header properties", async () => {
render(
html`<ak-expand text-open="Close it" text-closed="Open it" expanded
><p>This is the expanded text</p></ak-expand
>`,
);
const paragraph = await $("ak-expand").$(">>>p");
await expect(paragraph).toExist();
await expect(paragraph).toBeDisplayed();
await expect(paragraph).toHaveText("This is the expanded text");
await expect(await $("ak-expand").$(".pf-c-expandable-section__toggle-text")).toHaveText(
"Close it",
);
const control = await $("ak-expand").$(">>>button");
await control.click();
await expect(await $("ak-expand").$(".pf-c-expandable-section__toggle-text")).toHaveText(
"Open it",
);
});
it("should honor the header properties via a function call", async () => {
render(
akExpand(
{ "expanded": true, "text-open": "Close it now", "text-closed": "Open it now" },
html`<p>This is the new text.</p>`,
),
);
const paragraph = await $("ak-expand").$(">>>p");
await expect(paragraph).toExist();
await expect(paragraph).toBeDisplayed();
await expect(paragraph).toHaveText("This is the new text.");
await expect(await $("ak-expand").$(".pf-c-expandable-section__toggle-text")).toHaveText(
"Close it now",
);
const control = await $("ak-expand").$(">>>button");
await control.click();
await expect(await $("ak-expand").$(".pf-c-expandable-section__toggle-text")).toHaveText(
"Open it now",
);
});
});
-64
View File
@@ -1,64 +0,0 @@
import "../Label.js";
import { akLabel, PFColor } from "../Label.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
describe("ak-label", () => {
it("should render a label with the enum", async () => {
render(html`<ak-label color=${PFColor.Red}>This is a label</ak-label>`);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-c-label");
await expect(await $("ak-label").$(">>>span.pf-c-label")).not.toHaveElementClass(
"pf-m-compact",
);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-red");
await expect(await $("ak-label").$(">>>i.fas")).toHaveElementClass("fa-times");
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
});
it("should render a label with the attribute", async () => {
render(html`<ak-label color="success">This is a label</ak-label>`);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-green");
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
});
it("should render a compact label with the default level", async () => {
render(html`<ak-label compact>This is a label</ak-label>`);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-grey");
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass(
"pf-m-compact",
);
await expect(await $("ak-label").$(">>>i.fas")).toHaveElementClass("fa-info-circle");
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
});
it("should render a compact label with an icon and the default level", async () => {
render(html`<ak-label compact icon="fa-coffee">This is a label</ak-label>`);
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-grey");
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass(
"pf-m-compact",
);
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
await expect(await $("ak-label").$(">>>i.fas")).toHaveElementClass("fa-coffee");
});
it("should render a label with the function", async () => {
render(akLabel({ color: "success" }, "This is a label"));
await expect(await $("ak-label").$(">>>span.pf-c-label")).toHaveElementClass("pf-m-green");
await expect(await $("ak-label").$(">>>.pf-c-label__content")).toHaveText(
"This is a label",
);
});
});
@@ -1,35 +0,0 @@
import "../LoadingOverlay.js";
import { akLoadingOverlay } from "../LoadingOverlay.js";
import { render } from "#elements/tests/utils";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
describe("ak-loading-overlay", () => {
it("should render the default loader", async () => {
render(html`<ak-loading-overlay></ak-loading-overlay>`);
const empty = await $("ak-loading-overlay");
await expect(empty).toExist();
});
it("should render a slotted message", async () => {
render(
html`<ak-loading-overlay>
<p>Try again with a different filter</p>
</ak-loading-overlay>`,
);
const message = await $("ak-loading-overlay").$(">>>p");
await expect(message).toHaveText("Try again with a different filter");
});
it("as a function should render a slotted message", async () => {
render(akLoadingOverlay({}, "Try again with another filter"));
const overlay = await $("ak-loading-overlay");
await expect(overlay).toHaveText("Try again with another filter");
});
});
@@ -1,56 +0,0 @@
import "#admin/admin-settings/AdminSettingsFooterLinks";
import "../ak-array-input.js";
import { render } from "#elements/tests/utils";
import { FooterLink } from "@goauthentik/api";
import { $, expect } from "@wdio/globals";
import { html } from "lit";
const sampleItems: FooterLink[] = [
{ name: "authentik", href: "https://goauthentik.io" },
{ name: "authentik docs", href: "https://docs.goauthentik.io/docs/" },
];
describe("ak-array-input", () => {
afterEach(async () => {
await browser.execute(async () => {
await document.body.querySelector("ak-array-input")?.remove();
if (document.body._$litPart$) {
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
await delete document.body._$litPart$;
}
});
});
const component = (items: FooterLink[] = []) =>
render(
html` <ak-array-input
id="ak-array-input"
.items=${items}
.newItem=${() => ({ name: "", href: "" })}
.row=${(f?: FooterLink) =>
html`<ak-admin-settings-footer-link name="footerLink" .footerLink=${f}>
</ak-admin-settings-footer-link>`}
validate
></ak-array-input>`,
);
it("should render an empty control", async () => {
await component();
const link = await $("ak-array-input");
await browser.pause(500);
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual([]);
});
it("should render a populated component", async () => {
await component(sampleItems);
const link = await $("ak-array-input");
await browser.pause(500);
await expect(await link.getProperty("isValid")).toStrictEqual(true);
await expect(await link.getProperty("toJson")).toEqual(sampleItems);
});
});
-16
View File
@@ -1,16 +0,0 @@
import { applyDocumentTheme } from "#common/theme";
import { render as litRender, TemplateResult } from "lit";
/**
* A special version of render that ensures our stylesheets:
*
* - Will always be available to all elements under test.
* - Ensure they look right during testing.
* - CSS-based checks for visibility will return correct values.
*/
export const render = (body: TemplateResult) => {
applyDocumentTheme();
return litRender(body, document.body);
};
@@ -1,12 +0,0 @@
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();
-13
View File
@@ -1,13 +0,0 @@
import Page from "../pageobjects/page.js";
import { $ } from "@wdio/globals";
export default class AdminPage extends Page {
public pageHeader() {
return $(">>>ak-page-navbar").$(".page-title");
}
async openApplicationsListPage() {
await this.open("if/admin/#/core/applications");
}
}
@@ -1,82 +0,0 @@
import AdminPage from "./admin.page.js";
import ApplicationForm from "./forms/application.form.js";
import ForwardProxyForm from "./forms/forward-proxy.form.js";
import LdapForm from "./forms/ldap.form.js";
import OauthForm from "./forms/oauth.form.js";
import RadiusForm from "./forms/radius.form.js";
import SamlForm from "./forms/saml.form.js";
import ScimForm from "./forms/scim.form.js";
import TransparentProxyForm from "./forms/transparent-proxy.form.js";
import { $ } from "@wdio/globals";
/**
* sub page containing specific selectors and methods for a specific page
*/
class ApplicationWizardView extends AdminPage {
/**
* define selectors using getter methods
*/
ldap = LdapForm;
oauth = OauthForm;
transparentProxy = TransparentProxyForm;
forwardProxy = ForwardProxyForm;
saml = SamlForm;
scim = ScimForm;
radius = RadiusForm;
app = ApplicationForm;
async wizardTitle() {
return await $(">>>.pf-c-wizard__title");
}
async providerList() {
return await $(">>>ak-application-wizard-provider-choice-step");
}
async nextButton() {
return await $('>>>button[data-ouid-button-kind="wizard-next"]');
}
async getProviderType(type: string) {
// @ts-expect-error "TSC does not understand the ChainablePromiseElement type at all."
return await this.providerList().$(`>>>input[value="${type}"]`);
}
async submitPage() {
return await $(">>>ak-application-wizard-submit-step");
}
async successMessage() {
return await $('>>>[data-ouid-component-state="submitted"]');
}
}
type Pair = [string, string];
// Define a getter for each provider type in the radio button collection.
const providerValues: Pair[] = [
["oauth2provider", "oauth2Provider"],
["ldapprovider", "ldapProvider"],
["proxyprovider", "proxyProvider"],
["radiusprovider", "radiusProvider"],
["samlprovider", "samlProvider"],
["scimprovider", "scimProvider"],
];
providerValues.forEach(([value, name]: Pair) => {
Object.defineProperties(ApplicationWizardView.prototype, {
[name]: {
get: async function () {
return await (
await this.providerList()
).$(`>>>div[data-ouid-component-name="${value}"]`);
},
},
});
});
export default new ApplicationWizardView();
@@ -1,22 +0,0 @@
import AdminPage from "./admin.page.js";
import { $ } from "@wdio/globals";
/**
* sub page containing specific selectors and methods for a specific page
*/
class ApplicationsListPage extends AdminPage {
/**
* define selectors using getter methods
*/
async startWizardButton() {
return await $('>>>button[data-ouia-component-id="start-application-wizard"]');
}
async open() {
return await super.open("if/admin/#/core/applications");
}
}
export default new ApplicationsListPage();
-227
View File
@@ -1,227 +0,0 @@
import { browser } from "@wdio/globals";
import { Key } from "webdriverio";
export async function doBlur(el: WebdriverIO.Element | ChainablePromiseElement) {
const element = await el;
browser.execute((element) => element.blur(), element);
}
export function tap<A>(a: A) {
console.log("TAP:", a);
return a;
}
const makeComparator = (value: string | RegExp) =>
typeof value === "string"
? (sample: string) => sample === value
: (sample: string) => value.test(sample);
export async function checkIsPresent(name: string) {
await expect(await $(name)).toBeDisplayed();
}
export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
const context = ctx ?? browser;
const button = await (async () => {
for await (const button of context.$$("button")) {
if ((await button.isDisplayed()) && (await button.getText()).indexOf(name) !== -1) {
return button;
}
}
})();
if (!(button && (await button.isDisplayed()))) {
throw new Error(`Unable to find button '${name}'`);
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
export async function clickToggleGroup(name: string, value: string | RegExp) {
const comparator = makeComparator(value);
const button = await (async () => {
for await (const button of $(`>>>[data-ouid-component-name=${name}]`).$$(
">>>.pf-c-toggle-group__button",
)) {
if (comparator(await button.$(">>>.pf-c-toggle-group__text").getText())) {
return button;
}
}
})();
if (!(button && (await button?.isDisplayed()))) {
throw new Error(`Unable to locate toggle button ${name}:${value.toString()}`);
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
export async function setFormGroup(name: string | RegExp, setting: "open" | "closed") {
const comparator = makeComparator(name);
const formGroup = await (async () => {
for await (const group of browser.$$(">>>ak-form-group")) {
// Delightfully, wizards may have slotted elements that *exist* but are not *attached*,
// and this can break the damn tests.
if (!(await group.isDisplayed())) {
continue;
}
if (
comparator(
await group.$(">>>div.pf-c-form__field-group-header-title-text").getText(),
)
) {
return group;
}
}
})();
if (!(formGroup && (await formGroup.isDisplayed()))) {
throw new Error(`Unable to find ak-form-group[name="${name}"]`);
}
await formGroup.scrollIntoView();
const open = await formGroup.getAttribute("open").then((value) => value !== null);
if ((setting === "open" && !open) || (setting === "closed" && open)) {
await formGroup.$(">>>summary").click();
}
await doBlur(formGroup);
}
export async function setRadio(name: string, value: string | RegExp) {
const control = await $(`>>>ak-radio[name="${name}"]`);
await control.scrollIntoView();
const comparator = makeComparator(value);
const item = await (async () => {
for await (const item of control.$$(">>>div.pf-c-radio")) {
if (comparator(await item.$(">>>.pf-c-radio__label").getText())) {
return item;
}
}
})();
if (!(item && (await item.isDisplayed()))) {
throw new Error(`Unable to find a radio that matches ${name}:${value.toString()}`);
}
await item.scrollIntoView();
await item.click();
await doBlur(control);
}
export async function setSearchSelect(name: string, value: string | RegExp) {
const control = await (async () => {
try {
const control = await $(`>>>ak-search-select[name="${name}"]`);
await control.waitForExist({ timeout: 500 });
return control;
} catch (_e: unknown) {
const control = await $(`>>>ak-search-selects-ez[name="${name}"]`);
return control;
}
})();
if (!(control && (await control.isExisting()))) {
throw new Error(`Unable to find an ak-search-select variant matching ${name}}`);
}
// Find the search select input control and activate it.
const view = await control.$(">>>ak-search-select-view");
const input = await view.$('>>>input[type="text"]');
await input.scrollIntoView();
await input.click();
const comparator = makeComparator(value);
const button = await (async () => {
for await (const button of $(`>>>div[data-managed-for*="${name}"]`)
.$(">>>ak-list-select")
.$$("button")) {
if (comparator(await button.getText())) {
return button;
}
}
})();
if (!(button && (await button.isDisplayed()))) {
throw new Error(
`Unable to find an ak-search-select entry matching ${name}:${value.toString()}`,
);
}
await (await button).click();
await browser.keys(Key.Tab);
await doBlur(control);
}
export async function setTextInput(name: string, value: string) {
const control = await $(`>>>input[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setTextareaInput(name: string, value: string) {
const control = await $(`>>>textarea[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setToggle(name: string, set: boolean) {
const toggle = await $(`>>>input[name="${name}"]`);
await toggle.scrollIntoView();
await expect(await toggle.getAttribute("type")).toBe("checkbox");
const state = await toggle.isSelected();
if (set !== state) {
const control = await (await toggle.parentElement()).$(">>>.pf-c-switch__toggle");
await control.click();
await doBlur(control);
}
}
export async function setTypeCreate(name: string, value: string | RegExp) {
const control = await $(`>>>ak-wizard-page-type-create[name="${name}"]`);
await control.scrollIntoView();
const comparator = makeComparator(value);
const card = await (async () => {
for await (const card of $(">>>ak-wizard-page-type-create").$$(
'>>>[data-ouid-component-type="ak-type-create-grid-card"]',
)) {
if (comparator(await card.$(">>>.pf-c-card__title").getText())) {
return card;
}
}
})();
if (!(card && (await card.isDisplayed()))) {
throw new Error(`Unable to locate radio card ${name}:${value.toString()}`);
}
await card.scrollIntoView();
await card.click();
await doBlur(control);
}
export type TestInteraction =
| [typeof checkIsPresent, ...Parameters<typeof checkIsPresent>]
| [typeof clickButton, ...Parameters<typeof clickButton>]
| [typeof clickToggleGroup, ...Parameters<typeof clickToggleGroup>]
| [typeof setFormGroup, ...Parameters<typeof setFormGroup>]
| [typeof setRadio, ...Parameters<typeof setRadio>]
| [typeof setSearchSelect, ...Parameters<typeof setSearchSelect>]
| [typeof setTextInput, ...Parameters<typeof setTextInput>]
| [typeof setTextareaInput, ...Parameters<typeof setTextareaInput>]
| [typeof setToggle, ...Parameters<typeof setToggle>]
| [typeof setTypeCreate, ...Parameters<typeof setTypeCreate>];
export type TestSequence = TestInteraction[];
export type TestProvider = () => TestSequence;
@@ -1,19 +0,0 @@
import Page from "../page.js";
import { $ } from "@wdio/globals";
export class ApplicationForm extends Page {
async name() {
return await $('>>>ak-text-input[name="name"]').$(">>>input");
}
async uiSettings() {
return $('>>>ak-form-group[label="UI Settings"]');
}
async launchUrl() {
return await $('>>>input[name="metaLaunchUrl"]');
}
}
export default new ApplicationForm();
@@ -1,19 +0,0 @@
import Page from "../page.js";
import { $ } from "@wdio/globals";
export class ForwardProxyForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"]',
"authorizationFlow",
selector,
);
}
get externalHost() {
return $('>>>input[name="externalHost"]');
}
}
export default new ForwardProxyForm();
-13
View File
@@ -1,13 +0,0 @@
import Page from "../page.js";
export class LdapForm extends Page {
async setBindFlow() {
await this.searchSelect(
'>>>ak-search-select-view[name="authorizationFlow"]',
"authorizationFlow",
"default-authentication-flow",
);
}
}
export default new LdapForm();
-19
View File
@@ -1,19 +0,0 @@
import Page from "../page.js";
import { $ } from "@wdio/globals";
export class OauthForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"]',
"authorizationFlow",
`${selector}`,
);
}
async providerName() {
return await $('>>>ak-form-element-horizontal[name="name"]').$("input");
}
}
export default new OauthForm();
@@ -1,13 +0,0 @@
import Page from "../page.js";
export class RadiusForm extends Page {
async setAuthenticationFlow(selector: string) {
await this.searchSelect(
'>>>ak-branded-flow-search[name="authorizationFlow"]',
"authorizationFlow",
selector,
);
}
}
export default new RadiusForm();
-19
View File
@@ -1,19 +0,0 @@
import Page from "../page.js";
import { $ } from "@wdio/globals";
export class SamlForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"]',
"authorizationFlow",
selector,
);
}
get acsUrl() {
return $('>>>input[name="acsUrl"]');
}
}
export default new SamlForm();
-13
View File
@@ -1,13 +0,0 @@
import Page from "../page.js";
export class ScimForm extends Page {
get url() {
return $('>>>input[name="url"]');
}
get token() {
return $('>>>input[name="token"]');
}
}
export default new ScimForm();
@@ -1,23 +0,0 @@
import Page from "../page.js";
import { $ } from "@wdio/globals";
export class TransparentProxyForm extends Page {
async setAuthorizationFlow(selector: string) {
await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"]',
"authorizationFlow",
selector,
);
}
get externalHost() {
return $('>>>input[name="externalHost"]');
}
get internalHost() {
return $('>>>input[name="internalHost"]');
}
}
export default new TransparentProxyForm();
-68
View File
@@ -1,68 +0,0 @@
import Page from "./page.js";
import UserLibraryPage from "./user-library.page.js";
import { $ } from "@wdio/globals";
/**
* sub page containing specific selectors and methods for a specific page
*/
class LoginPage extends Page {
/**
* Selectors
*/
async inputUsername() {
return await $('>>>input[name="uidField"]');
}
async usernameBtnSubmit() {
return await $('>>>button[type="submit"]');
}
async inputPassword() {
return await $('>>>input[name="password"]');
}
async passwordBtnSubmit() {
return await $(">>>ak-stage-password").$('>>>button[type="submit"]');
}
async authFailure() {
return await $(">>>.pf-m-error");
}
/**
* Specific interactions
*/
async username(username: string) {
await (await this.inputUsername()).setValue(username);
const submitBtn = await this.usernameBtnSubmit();
await submitBtn.waitForEnabled();
await submitBtn.click();
}
async password(password: string) {
await (await this.inputPassword()).setValue(password);
const submitBtn = await this.passwordBtnSubmit();
await submitBtn.waitForEnabled();
await submitBtn.click();
}
async login(username: string, password: string) {
await this.username(username);
await this.pause();
await this.password(password);
await this.pause();
await this.pause(">>>header h1");
return UserLibraryPage;
}
/**
* URL for accessing this page (if necessary)
*/
open() {
return super.open("");
}
}
export default new LoginPage();
-133
View File
@@ -1,133 +0,0 @@
import { browser } from "@wdio/globals";
import { match } from "ts-pattern";
import { Key } from "webdriverio";
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 async open(path: string) {
return await browser.url(`http://localhost:9000/${path}`);
}
public async pause(selector?: string) {
if (selector) {
return await $(selector).waitForDisplayed();
}
return await browser.pause(CLICK_TIME_DELAY);
}
/**
* Target a specific entry in SearchSelect. Requires that the SearchSelect have the `name`
* attribute set, so that the managed selector can find the *right* SearchSelect if there are
* multiple open SearchSelects on the board. See `./ldap-form.view:LdapForm.setBindFlow` for an
* example, and see `./oauth-form.view:OauthForm:setAuthorizationFlow` for a further example of
* why it would be hard to simplify this further (`flow` vs `tentanted-flow` vs a straight-up
* SearchSelect each have different a `searchSelector`).
*/
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
const inputBind = await $(searchSelector);
const inputMain = await inputBind.$('>>>input[type="text"]');
await inputMain.click();
const searchBlock = await (
await $(`>>>div[data-managed-for="${managedSelector}"]`).$(">>>ak-list-select")
).$$("button");
let target: WebdriverIO.Element;
for (const button of searchBlock) {
if ((await button.getText()).includes(buttonSelector)) {
target = button;
break;
}
}
// @ts-expect-error "TSC cannot tell if the `for` loop actually performs the assignment."
if (!target) {
throw new Error(`Expected to find an entry matching the spec ${buttonSelector}`);
}
await (await target).click();
await browser.keys(Key.Tab);
}
async setSearchSelect(name: string, value: string) {
const control = await (async () => {
try {
const control = await $(`ak-search-select[name="${name}"]`);
await control.waitForExist({ timeout: 500 });
return control;
} catch (_e: unknown) {
const control = await $(`ak-search-selects-ez[name="${name}"]`);
return control;
}
})();
// Find the search select input control and activate it.
const view = await control.$("ak-search-select-view");
const input = await view.$('input[type="text"]');
await input.scrollIntoView();
await input.click();
// Weirdly necessary because it's portals!
const searchBlock = await (
await $(`>>>div[data-managed-for="${name}"]`).$(">>>ak-list-select")
).$$("button");
let target: WebdriverIO.Element;
for (const button of searchBlock) {
if ((await button.getText()).includes(value)) {
target = button;
break;
}
}
// @ts-expect-error "TSC cannot tell if the `for` loop actually performs the assignment."
if (!target) {
throw new Error(`Expected to find an entry matching the spec ${value}`);
}
await (await target).click();
await browser.keys(Key.Tab);
}
async setTextInput(name: string, value: string) {
const control = await $(`input[name="${name}"}`);
await control.scrollIntoView();
await control.setValue(value);
}
async setRadio(name: string, value: string) {
const control = await $(`ak-radio[name="${name}"]`);
await control.scrollIntoView();
const item = await control.$(`label.*=${value}`).parentElement();
await item.scrollIntoView();
await item.click();
}
async setTypeCreate(name: string, value: string) {
const control = await $(`ak-wizard-page-type-create[name="${name}"]`);
await control.scrollIntoView();
const selection = await $(`.pf-c-card__.*=${value}`);
await selection.scrollIntoView();
await selection.click();
}
async setFormGroup(name: string, setting: "open" | "closed") {
const formGroup = await $(`ak-form-group span[slot="header"].*=${name}`).parentElement();
await formGroup.scrollIntoView();
const toggle = await formGroup.$("div.pf-c-form__field-group-toggle-button button");
await match([await toggle.getAttribute("expanded"), setting])
.with(["false", "open"], async () => await toggle.click())
.with(["true", "closed"], async () => await toggle.click())
.otherwise(async () => {});
}
public async logout() {
await browser.url("http://localhost:9000/flows/-/default/invalidation/");
return await this.pause();
}
}
@@ -1,54 +0,0 @@
import AdminPage from "./admin.page.js";
import OauthForm from "./forms/oauth.form.js";
import { $ } from "@wdio/globals";
/**
* sub page containing specific selectors and methods for a specific page
*/
class ProviderWizardView extends AdminPage {
/**
* define selectors using getter methods
*/
oauth = OauthForm;
get wizardTitle() {
return $(">>>ak-wizard .pf-c-wizard__header h1.pf-c-title");
}
get providerList() {
return $(">>>ak-provider-wizard-initial");
}
get nextButton() {
return $(">>>ak-wizard footer button.pf-m-primary");
}
async getProviderType(type: string) {
return await this.providerList.$(`>>>input[value="${type}"]`);
}
get successMessage() {
return $('>>>[data-commit-state="success"]');
}
}
type Pair = [string, string];
// Define a getter for each provider type in the radio button collection.
const providerValues: Pair[] = [["oauth2", "oauth2Provider"]];
providerValues.forEach(([value, name]: Pair) => {
Object.defineProperties(ProviderWizardView.prototype, {
[name]: {
get: function () {
return this.providerList.$(`>>>input[id="ak-provider-${value}-form"]`);
},
},
});
});
export default new ProviderWizardView();
@@ -1,48 +0,0 @@
import AdminPage from "./admin.page.js";
import { $, browser } from "@wdio/globals";
import { Key } from "webdriverio";
/**
* sub page containing specific selectors and methods for a specific page
*/
class ApplicationsListPage extends AdminPage {
/**
* define selectors using getter methods
*/
get startWizardButton() {
return $('>>>ak-wizard button[slot="trigger"]');
}
get searchInput() {
return $('>>>ak-table-search input[name="search"]');
}
searchButton() {
return $('>>>ak-table-search button[type="submit"]');
}
// Sufficiently esoteric to justify having its own method
async clickSearchButton() {
await browser.execute(
function (searchButton: unknown) {
(searchButton as HTMLButtonElement).focus();
},
await $('>>>ak-table-search button[type="submit"]'),
);
return await browser.action("key").down(Key.Enter).up(Key.Enter).perform();
}
// Only use after a very precise search. :-)
async findProviderRow() {
return await $(">>>ak-provider-list td a");
}
async open() {
return await super.open("if/admin/#/core/providers");
}
}
export default new ApplicationsListPage();
@@ -1,23 +0,0 @@
import Page from "./page.js";
import { $ } from "@wdio/globals";
/**
* sub page containing specific selectors and methods for a specific page
*/
class UserLibraryPage extends Page {
/**
* define selectors using getter methods
*/
public async pageHeader() {
return $(">>>header h1");
}
public async goToAdmin() {
await $('>>>a[href="/if/admin"]').click();
return await $("ak-admin-overview").waitForDisplayed();
}
}
export default new UserLibraryPage();
@@ -1,131 +0,0 @@
import ApplicationWizardView from "../pageobjects/application-wizard.page.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import type { TestProvider, TestSequence } from "../pageobjects/controls.js";
import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js";
import {
completeForwardAuthDomainProxyProviderForm,
completeForwardAuthProxyProviderForm,
completeLDAPProviderForm,
completeOAuth2ProviderForm,
completeProxyProviderForm,
completeRadiusProviderForm,
completeSAMLProviderForm,
completeSCIMProviderForm,
simpleForwardAuthDomainProxyProviderForm,
simpleForwardAuthProxyProviderForm,
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleProxyProviderForm,
simpleRadiusProviderForm,
simpleSAMLProviderForm,
simpleSCIMProviderForm,
} from "./provider-shared-sequences.js";
// @ts-nocheck
// ^^^^^^^^^^^ Because TSC cannot handle metaprogramming, and metaprogramming
// via `defineProperties` is how we installed the OUID finders for the various
// wizard types.
import { expect } from "@wdio/globals";
const SUCCESS_MESSAGE = "Your application has been saved";
async function reachTheApplicationsPage() {
await ApplicationsListPage.logout();
await login();
await ApplicationsListPage.open();
await ApplicationsListPage.pause();
await expect(await ApplicationsListPage.pageHeader()).toBeDisplayed();
await expect(await ApplicationsListPage.pageHeader()).toHaveText("Applications");
}
async function fillOutTheApplication(title: string) {
const newPrefix = randomId();
await (await ApplicationsListPage.startWizardButton()).click();
await (await ApplicationWizardView.wizardTitle()).waitForDisplayed();
await expect(await ApplicationWizardView.wizardTitle()).toHaveText("New application");
await (await ApplicationWizardView.app.name()).setValue(`${title} - ${newPrefix}`);
await (await ApplicationWizardView.app.uiSettings()).scrollIntoView();
await (await ApplicationWizardView.app.uiSettings()).click();
await (await ApplicationWizardView.app.launchUrl()).scrollIntoView();
await (await ApplicationWizardView.app.launchUrl()).setValue("http://example.goauthentik.io");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
}
async function getCommitMessage() {
await (await ApplicationWizardView.successMessage()).waitForDisplayed();
return await ApplicationWizardView.successMessage();
}
async function fillOutTheProviderAndProceed(provider: TestSequence) {
// The wizard automagically provides a name. If it doesn't, that's a bug.
const wizardProvider = provider.filter((p) => p.length < 2 || p[1] !== "name");
await $(">>>ak-wizard-page-type-create").waitForDisplayed();
for await (const field of wizardProvider) {
const thefunc = field[0];
const args = field.slice(1);
console.log(`Running ${args.join(", ")}`);
// @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it."
await thefunc.apply($, args);
}
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
}
export async function findWizardTitle() {
return await (async () => {
for await (const item of $$(">>>ak-wizard-title")) {
if ((await item.isExisting()) && (await item.isDisplayed())) {
return item;
}
}
})();
}
async function passByPoliciesAndCommit() {
const title = await findWizardTitle();
// Expect to be on the Bindings panel
await expect(await title?.getText()).toEqual("Configure Policy/User/Group Bindings");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await (await ApplicationWizardView.submitPage()).waitForDisplayed();
await (await ApplicationWizardView.nextButton()).click();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
}
async function itShouldConfigureApplicationsViaTheWizard(name: string, provider: TestSequence) {
it(`Should successfully configure an application with a ${name} provider`, async () => {
await reachTheApplicationsPage();
await fillOutTheApplication(name);
await fillOutTheProviderAndProceed(provider);
await passByPoliciesAndCommit();
});
}
const providers: [string, TestProvider][] = [
["Simple LDAP", simpleLDAPProviderForm],
["Simple OAuth2", simpleOAuth2ProviderForm],
["Simple Radius", simpleRadiusProviderForm],
["Simple SAML", simpleSAMLProviderForm],
["Simple SCIM", simpleSCIMProviderForm],
["Simple Proxy", simpleProxyProviderForm],
["Simple Forward Auth (single)", simpleForwardAuthProxyProviderForm],
["Simple Forward Auth (domain)", simpleForwardAuthDomainProxyProviderForm],
["Complete OAuth2", completeOAuth2ProviderForm],
["Complete LDAP", completeLDAPProviderForm],
["Complete Radius", completeRadiusProviderForm],
["Complete SAML", completeSAMLProviderForm],
["Complete SCIM", completeSCIMProviderForm],
["Complete Proxy", completeProxyProviderForm],
["Complete Forward Auth (single)", completeForwardAuthProxyProviderForm],
["Complete Forward Auth (domain)", completeForwardAuthDomainProxyProviderForm],
];
describe("Configuring Applications Via the Wizard", () => {
for (const [name, provider] of providers) {
itShouldConfigureApplicationsViaTheWizard(name, provider());
}
});
-47
View File
@@ -1,47 +0,0 @@
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
import ProvidersListPage from "../pageobjects/providers-list.page.js";
import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js";
import { expect } from "@wdio/globals";
async function reachTheProvider() {
await ProvidersListPage.logout();
await login();
await ProvidersListPage.open();
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
await ProvidersListPage.startWizardButton.click();
await ProviderWizardView.wizardTitle.waitForDisplayed();
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
}
describe("Configure Oauth2 Providers", () => {
it("Should configure a simple LDAP Application", async () => {
const newProviderName = `New OAuth2 Provider - ${randomId()}`;
await reachTheProvider();
await $(">>>ak-wizard-page-type-create").waitForDisplayed();
await $('>>>div[data-ouid-component-name="oauth2provider"]').scrollIntoView();
await $('>>>div[data-ouid-component-name="oauth2provider"]').click();
await ProviderWizardView.nextButton.click();
await ProviderWizardView.pause();
return await $('>>>ak-form-element-horizontal[name="name"]').$(">>>input");
await ProviderWizardView.oauth.setAuthorizationFlow(
"default-provider-authorization-explicit-consent",
);
await ProviderWizardView.nextButton.click();
await ProviderWizardView.pause();
await ProvidersListPage.searchInput.setValue(newProviderName);
await ProvidersListPage.clickSearchButton();
await ProvidersListPage.pause();
const newProvider = await ProvidersListPage.findProviderRow();
await newProvider.waitForDisplayed();
expect(newProvider).toExist();
expect(await newProvider.getText()).toHaveText(newProviderName);
});
});
@@ -1,323 +0,0 @@
import {
checkIsPresent,
clickButton,
clickToggleGroup,
setFormGroup,
setRadio,
setSearchSelect,
setTextareaInput,
setTextInput,
setToggle,
setTypeCreate,
type TestProvider,
type TestSequence,
} from "../pageobjects/controls.js";
import { ascii_letters, digits, randomId, randomString } from "../utils/index.js";
const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`;
// components.schemas.OAuth2ProviderRequest
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - client_type
// - client_id
// - client_secret
// - access_code_validity
// - access_token_validity
// - refresh_token_validity
// - include_claims_in_id_token
// - signing_key
// - encryption_key
// - redirect_uris
// - sub_mode
// - issuer_mode
// - jwks_sources
//
export const simpleOAuth2ProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
];
export const completeOAuth2ProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setFormGroup, /Protocol settings/, "open"],
[setRadio, "clientType", "Public"],
// Switch back so we can make sure `clientSecret` is available.
[setRadio, "clientType", "Confidential"],
[checkIsPresent, '[name="clientId"]'],
[checkIsPresent, '[name="clientSecret"]'],
[setSearchSelect, "signingKey", /authentik Self-signed Certificate/],
[setSearchSelect, "encryptionKey", /authentik Self-signed Certificate/],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setFormGroup, /Advanced protocol settings/, "open"],
[setTextInput, "accessCodeValidity", "minutes=2"],
[setTextInput, "accessTokenValidity", "minutes=10"],
[setTextInput, "refreshTokenValidity", "days=40"],
[setToggle, "includeClaimsInIdToken", false],
[checkIsPresent, '[name="redirectUris"]'],
[setRadio, "subMode", "Based on the User's username"],
[setRadio, "issuerMode", "Same identifier is used for all providers"],
[setFormGroup, /Machine-to-Machine authentication settings/, "open"],
[checkIsPresent, '[name="jwtFederationSources"]'],
[checkIsPresent, '[name="jwtFederationProviders"]'],
];
// components.schemas.LDAPProviderRequest
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - base_dn
// - certificate
// - tls_server_name
// - uid_start_number
// - gid_start_number
// - search_mode
// - bind_mode
// - mfa_support
//
export const simpleLDAPProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")],
// This will never not weird me out.
[setFormGroup, /Flow settings/, "open"],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
];
export const completeLDAPProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")],
[setFormGroup, /Flow settings/, "open"],
[setFormGroup, /Protocol settings/, "open"],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setTextInput, "baseDn", "DC=ldap-2,DC=goauthentik,DC=io"],
[setSearchSelect, "certificate", /authentik Self-signed Certificate/],
[checkIsPresent, '[name="tlsServerName"]'],
[setTextInput, "uidStartNumber", "2001"],
[setTextInput, "gidStartNumber", "4001"],
[setRadio, "searchMode", "Direct querying"],
[setRadio, "bindMode", "Direct binding"],
[setToggle, "mfaSupport", false],
];
export const simpleRadiusProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Radius Provider")],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
];
export const completeRadiusProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Radius Provider")],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setFormGroup, /Protocol settings/, "open"],
[setToggle, "mfaSupport", false],
[setTextInput, "clientNetworks", ""],
[setTextInput, "clientNetworks", "0.0.0.0/0, ::/0"],
[setTextInput, "sharedSecret", randomString(128, ascii_letters + digits)],
[checkIsPresent, '[name="propertyMappings"]'],
];
// provider_components.schemas.SAMLProviderRequest.yml
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - acs_url
// - audience
// - issuer
// - assertion_valid_not_before
// - assertion_valid_not_on_or_after
// - session_valid_not_on_or_after
// - name_id_mapping
// - digest_algorithm
// - signature_algorithm
// - signing_kp
// - verification_kp
// - encryption_kp
// - sign_assertion
// - sign_response
// - sp_binding
// - default_relay_state
//
export const simpleSAMLProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SAML Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SAML Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setTextInput, "acsUrl", "http://example.com:8000/"],
];
export const completeSAMLProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SAML Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SAML Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setTextInput, "acsUrl", "http://example.com:8000/"],
[setTextInput, "issuer", "someone-else"],
[setRadio, "spBinding", "Post"],
[setTextInput, "audience", ""],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setFormGroup, /Advanced protocol settings/, "open"],
[checkIsPresent, '[name="propertyMappings"]'],
[setSearchSelect, "signingKp", /authentik Self-signed Certificate/],
[setSearchSelect, "verificationKp", /authentik Self-signed Certificate/],
[setSearchSelect, "encryptionKp", /authentik Self-signed Certificate/],
[setSearchSelect, "nameIdMapping", /authentik default SAML Mapping. Username/],
[setTextInput, "assertionValidNotBefore", "minutes=-10"],
[setTextInput, "assertionValidNotOnOrAfter", "minutes=10"],
[setTextInput, "sessionValidNotOnOrAfter", "minutes=172800"],
[checkIsPresent, '[name="defaultRelayState"]'],
[setRadio, "digestAlgorithm", "SHA512"],
[setRadio, "signatureAlgorithm", "RSA-SHA512"],
// These are only available after the signingKp is defined.
[setToggle, "signAssertion", true],
[setToggle, "signResponse", true],
];
// provider_components.schemas.SCIMProviderRequest.yml
//
// - name
// - property_mappings
// - property_mappings_group
// - url
// - verify_certificates
// - token
// - exclude_users_service_account
// - filter_group
//
export const simpleSCIMProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SCIM Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SCIM Provider")],
[setTextInput, "url", "http://example.com:8000/"],
[setTextInput, "token", "insert-real-token-here"],
];
export const completeSCIMProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SCIM Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SCIM Provider")],
[setTextInput, "url", "http://example.com:8000/"],
[setToggle, "verifyCertificates", false],
[setTextInput, "token", "insert-real-token-here"],
[setFormGroup, /Protocol settings/, "open"],
[setFormGroup, /User filtering/, "open"],
[setToggle, "excludeUsersServiceAccount", false],
[setSearchSelect, "filterGroup", /authentik Admins/],
[setFormGroup, /Attribute mapping/, "open"],
[checkIsPresent, '[name="propertyMappings"]'],
[checkIsPresent, '[name="propertyMappingsGroup"]'],
];
// provider_components.schemas.ProxyProviderRequest.yml
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - internal_host
// - external_host
// - internal_host_ssl_validation
// - certificate
// - skip_path_regex
// - basic_auth_enabled
// - basic_auth_password_attribute
// - basic_auth_user_attribute
// - mode
// - intercept_header_auth
// - cookie_domain
// - jwks_sources
// - access_token_validity
// - refresh_token_validity
// - refresh_token_validity is not handled in any of our forms. On purpose.
// - internal_host_ssl_validation
// - only on ProxyMode
export const simpleProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Proxy Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Proxy"],
[setTextInput, "externalHost", "http://example.com:8000/"],
[setTextInput, "internalHost", "http://example.com:8001/"],
];
export const simpleForwardAuthProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Forward Auth Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Forward auth (single application)"],
[setTextInput, "externalHost", "http://example.com:8000/"],
];
export const simpleForwardAuthDomainProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Forward Auth Domain Level Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Forward auth (domain level)"],
[setTextInput, "externalHost", "http://example.com:8000/"],
[setTextInput, "cookieDomain", "somedomain.tld"],
];
const proxyModeCompletions: TestSequence = [
[setTextInput, "accessTokenValidity", "hours=36"],
[setFormGroup, /Advanced protocol settings/, "open"],
[setSearchSelect, "certificate", /authentik Self-signed Certificate/],
[checkIsPresent, '[name="propertyMappings"]'],
[setTextareaInput, "skipPathRegex", "."],
[setFormGroup, /Authentication settings/, "open"],
[setToggle, "interceptHeaderAuth", false],
[setToggle, "basicAuthEnabled", true],
[setTextInput, "basicAuthUserAttribute", "authorized-user"],
[setTextInput, "basicAuthPasswordAttribute", "authorized-user-password"],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[checkIsPresent, '[name="jwtFederationSources"]'],
[checkIsPresent, '[name="jwtFederationProviders"]'],
];
export const completeProxyProviderForm: TestProvider = () => [
...simpleProxyProviderForm(),
[setToggle, "internalHostSslValidation", false],
...proxyModeCompletions,
];
export const completeForwardAuthProxyProviderForm: TestProvider = () => [
...simpleForwardAuthProxyProviderForm(),
...proxyModeCompletions,
];
export const completeForwardAuthDomainProxyProviderForm: TestProvider = () => [
...simpleForwardAuthProxyProviderForm(),
...proxyModeCompletions,
];
-98
View File
@@ -1,98 +0,0 @@
import { type TestProvider, type TestSequence } from "../pageobjects/controls.js";
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
import ProvidersListPage from "../pageobjects/providers-list.page.js";
import { login } from "../utils/login.js";
import {
completeForwardAuthDomainProxyProviderForm,
completeForwardAuthProxyProviderForm,
completeLDAPProviderForm,
completeOAuth2ProviderForm,
completeProxyProviderForm,
completeRadiusProviderForm,
completeSAMLProviderForm,
completeSCIMProviderForm,
simpleForwardAuthDomainProxyProviderForm,
simpleForwardAuthProxyProviderForm,
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleProxyProviderForm,
simpleRadiusProviderForm,
simpleSAMLProviderForm,
simpleSCIMProviderForm,
} from "./provider-shared-sequences.js";
import { expect } from "@wdio/globals";
async function reachTheProvider() {
await ProvidersListPage.logout();
await login();
await ProvidersListPage.open();
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
await expect(await containedMessages()).not.toContain("Successfully created provider.");
await ProvidersListPage.startWizardButton.click();
await ProviderWizardView.wizardTitle.waitForDisplayed();
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
}
const containedMessages = async () =>
await (async () => {
const messages = [];
for await (const alert of $("ak-message-container").$$("ak-message")) {
messages.push(await alert.$("p.pf-c-alert__title").getText());
}
return messages;
})();
const hasProviderSuccessMessage = async () =>
await browser.waitUntil(
async () => (await containedMessages()).includes("Successfully created provider."),
{ timeout: 1000, timeoutMsg: "Expected to see provider success message." },
);
async function fillOutFields(fields: TestSequence) {
for (const field of fields) {
const thefunc = field[0];
const args = field.slice(1);
// @ts-expect-error "This is a pretty alien call, so I'm not surprised Typescript doesn't like it."
await thefunc.apply($, args);
}
}
async function itShouldConfigureASimpleProvider(name: string, provider: TestSequence) {
it(`Should successfully configure a ${name} provider`, async () => {
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await fillOutFields(provider);
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
}
type ProviderTest = [string, TestProvider];
describe("Configuring Providers", () => {
const providers: ProviderTest[] = [
["Simple LDAP", simpleLDAPProviderForm],
["Simple OAuth2", simpleOAuth2ProviderForm],
["Simple Radius", simpleRadiusProviderForm],
["Simple SAML", simpleSAMLProviderForm],
["Simple SCIM", simpleSCIMProviderForm],
["Simple Proxy", simpleProxyProviderForm],
["Simple Forward Auth (single application)", simpleForwardAuthProxyProviderForm],
["Simple Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm],
["Complete OAuth2", completeOAuth2ProviderForm],
["Complete LDAP", completeLDAPProviderForm],
["Complete Radius", completeRadiusProviderForm],
["Complete SAML", completeSAMLProviderForm],
["Complete SCIM", completeSCIMProviderForm],
["Complete Proxy", completeProxyProviderForm],
["Complete Forward Auth (single application)", completeForwardAuthProxyProviderForm],
["Complete Forward Auth (domain level)", completeForwardAuthDomainProxyProviderForm],
];
for (const [name, provider] of providers) {
itShouldConfigureASimpleProvider(name, provider());
}
});
-25
View File
@@ -1,25 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"moduleResolution": "node",
"module": "ESNext",
"target": "es2022",
"types": [
"node",
"@wdio/globals/types",
"expect-webdriverio",
"@wdio/mocha-framework",
"@types/mocha"
],
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["."]
}
-5
View File
@@ -1,5 +0,0 @@
export const BAD_USERNAME = process.env.AK_BAD_USERNAME ?? "bad-username@bad-login.io";
export const GOOD_USERNAME = process.env.AK_GOOD_USERNAME ?? "test-admin@goauthentik.io";
export const BAD_PASSWORD = process.env.AK_BAD_PASSWORD ?? "-this-is-a-bad-password-";
export const GOOD_PASSWORD = process.env.AK_GOOD_PASSWORD ?? "test-runner";
-27
View File
@@ -1,27 +0,0 @@
// Taken from python's string module
export const ascii_lowercase = "abcdefghijklmnopqrstuvwxyz";
export const ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
export const ascii_letters = ascii_lowercase + ascii_uppercase;
export const digits = "0123456789";
export const hexdigits = digits + "abcdef" + "ABCDEF";
export const octdigits = "01234567";
export const punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
export function randomString(len: number, charset: string): string {
const chars = [];
const array = new Uint8Array(len);
globalThis.crypto.getRandomValues(array);
for (let index = 0; index < len; index++) {
chars.push(charset[Math.floor(charset.length * (array[index] / Math.pow(2, 8)))]);
}
return chars.join("");
}
export function randomId() {
let dt = new Date().getTime();
return "xxxxxxxx".replace(/x/g, (c) => {
const r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
});
}
-11
View File
@@ -1,11 +0,0 @@
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
import { GOOD_PASSWORD, GOOD_USERNAME } from "./constants.js";
import { expect } from "@wdio/globals";
export const login = async () => {
await LoginPage.open();
await LoginPage.login(GOOD_USERNAME, GOOD_PASSWORD);
await expect(await UserLibraryPage.pageHeader()).toHaveText("My applications");
};
-120
View File
@@ -1,120 +0,0 @@
/**
* @file WebdriverIO configuration file for **component unit tests**.
*
* @see https://webdriver.io/docs/configurationfile.html
*/
import * as path from "node:path";
import { addCommands } from "../commands.mjs";
import { createBundleDefinitions } from "#bundler/utils/node";
import { inlineCSSPlugin } from "#bundler/vite-plugin-lit-css/node";
import { PackageRoot } from "#paths/node";
const headless = !process.env.HEADLESS || !!process.env.CI;
const lemmeSee = !!process.env.WDIO_LEMME_SEE;
/**
* @type {WebdriverIO.Capabilities[]}
*/
const capabilities = [];
const DEFAULT_MAX_INSTANCES = 10;
let maxInstances = 1;
if (headless) {
maxInstances = process.env.MAX_INSTANCES
? parseInt(process.env.MAX_INSTANCES, 10)
: DEFAULT_MAX_INSTANCES;
}
if (!process.env.WDIO_SKIP_CHROME) {
/**
* @satisfies {WebdriverIO.Capabilities}
*/
const chromeBrowserConfig = {
"browserName": "chrome",
"goog:chromeOptions": {
args: ["disable-search-engine-choice-screen"],
},
};
if (headless) {
chromeBrowserConfig["goog:chromeOptions"].args.push(
"headless",
"disable-gpu",
"no-sandbox",
"window-size=1280,672",
"browser-test",
);
}
capabilities.push(chromeBrowserConfig);
}
if (process.env.WDIO_TEST_SAFARI) {
capabilities.push({
browserName: "safari",
});
}
if (process.env.WDIO_TEST_FIREFOX) {
capabilities.push({
browserName: "firefox",
});
}
/**
* @type {WebdriverIO.BrowserRunnerOptions}
*/
const browserRunnerOptions = {
viteConfig: {
define: createBundleDefinitions(),
plugins: [
// ---
inlineCSSPlugin(),
],
},
};
/**
* @satisfies {WebdriverIO.Config}
*/
export const config = {
runner: ["browser", browserRunnerOptions],
tsConfigPath: path.resolve(PackageRoot, "tests", "tsconfig.test.json"),
specs: [path.resolve(PackageRoot, "tests", "specs", "**", "*.ts")],
exclude: [],
maxInstances,
capabilities,
logLevel: "warn",
bail: 0,
waitforTimeout: 12000,
connectionRetryTimeout: 12000,
connectionRetryCount: 3,
framework: "mocha",
reporters: ["spec"],
mochaOpts: {
ui: "bdd",
timeout: 60000,
},
/**
* @param {WebdriverIO.Browser} browser
*/
before(_capabilities, _specs, browser) {
addCommands(browser);
},
afterTest() {
if (lemmeSee) return browser.pause(500);
},
};
-24
View File
@@ -1,24 +0,0 @@
// @file TSConfig used during tests.
{
"compilerOptions": {
"baseUrl": ".",
"types": ["node", "webdriverio/async", "@wdio/cucumber-framework", "expect-webdriverio"],
"target": "esnext",
"module": "esnext",
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"lib": [
"ES5",
"ES2015",
"ES2016",
"ES2017",
"ES2018",
"ES2019",
"ES2020",
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
]
}
}
-14
View File
@@ -1,14 +0,0 @@
declare namespace WebdriverIO {
interface Element {
/**
* Focus on the element.
* @monkeypatch
*/
focus(): Promise<void>;
/**
* Blur the element.
* @monkeypatch
*/
blur(): Promise<void>;
}
}
-114
View File
@@ -1,114 +0,0 @@
/**
* @file WebdriverIO configuration file for **integration tests**.
*
* @see https://webdriver.io/docs/configurationfile.html
*/
import * as path from "node:path";
import { addCommands } from "./commands.mjs";
import { createBundleDefinitions } from "#bundler/utils/node";
import { inlineCSSPlugin } from "#bundler/vite-plugin-lit-css/node";
import { PackageRoot } from "#paths/node";
import { browser } from "@wdio/globals";
/// <reference types="@wdio/globals/types" />
/// <reference types="./types/webdriver.js" />
const headless = !process.env.HEADLESS || !!process.env.CI;
const lemmeSee = !!process.env.WDIO_LEMME_SEE;
/**
* @type {WebdriverIO.Capabilities[]}
*/
const capabilities = [];
if (!process.env.WDIO_SKIP_CHROME) {
/**
* @satisfies {WebdriverIO.Capabilities}
*/
const chromeBrowserConfig = {
"browserName": "chrome",
"goog:chromeOptions": {
args: ["disable-search-engine-choice-screen"],
},
};
if (headless) {
chromeBrowserConfig["goog:chromeOptions"].args.push(
"headless",
"disable-gpu",
"no-sandbox",
"window-size=1280,672",
"browser-test",
);
}
capabilities.push(chromeBrowserConfig);
}
if (process.env.WDIO_TEST_SAFARI) {
capabilities.push({
browserName: "safari",
});
}
if (process.env.WDIO_TEST_FIREFOX) {
capabilities.push({
browserName: "firefox",
});
}
/**
* @type {WebdriverIO.BrowserRunnerOptions}
*/
const browserRunnerOptions = {
viteConfig: {
define: createBundleDefinitions(),
plugins: [
// ---
inlineCSSPlugin(),
],
},
};
/**
* @satisfies {WebdriverIO.Config}
*/
export const config = {
runner: ["browser", browserRunnerOptions],
tsConfigPath: path.resolve(PackageRoot, "tsconfig.test.json"),
specs: [path.resolve(PackageRoot, "src", "**", "*.test.ts")],
exclude: [],
maxInstances: 1,
capabilities,
logLevel: "warn",
baseUrl: "http://localhost",
waitforTimeout: 10000,
connectionRetryTimeout: 120000,
connectionRetryCount: 3,
framework: "mocha",
reporters: ["spec"],
mochaOpts: {
ui: "bdd",
timeout: 60000,
},
/**
* @param {WebdriverIO.Browser} browser
*/
before(_capabilities, _specs, browser) {
addCommands(browser);
},
afterTest() {
if (lemmeSee) return browser.pause(500);
},
};
-16
View File
@@ -1,16 +0,0 @@
/**
* @file Web Test Runner configuration.
* @see https://modern-web.dev/docs/test-runner/cli-and-configuration/
*/
/**
* @type {import('@web/test-runner').TestRunnerConfig}
*/
const config = {
files: ["dist/**/*.spec.js"],
nodeResolve: {
exportConditions: ["browser", "production"],
},
};
export default config;