diff --git a/web/src/elements/tests/Alert.test.ts b/web/src/elements/tests/Alert.test.ts
new file mode 100644
index 0000000000..db6fba72f9
--- /dev/null
+++ b/web/src/elements/tests/Alert.test.ts
@@ -0,0 +1,37 @@
+import { render } from "@goauthentik/elements/tests/utils.js";
+import { $, expect } from "@wdio/globals";
+
+import { html } from "lit";
+
+import "../Alert.js";
+import { Level, akAlert } from "../Alert.js";
+
+describe("ak-alert", () => {
+ it("should render an alert with the enum", async () => {
+ render(html`
This is an 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`
This is an 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`
This is an 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");
+ });
+});
diff --git a/web/src/elements/tests/Divider.test.ts b/web/src/elements/tests/Divider.test.ts
new file mode 100644
index 0000000000..e431672537
--- /dev/null
+++ b/web/src/elements/tests/Divider.test.ts
@@ -0,0 +1,35 @@
+import { render } from "@goauthentik/elements/tests/utils.js";
+import { $, expect } from "@wdio/globals";
+
+import { html } from "lit";
+
+import "../Divider.js";
+import { akDivider } from "../Divider.js";
+
+describe("ak-divider", () => {
+ it("should render the divider", async () => {
+ render(html`
`);
+ const empty = await $("ak-divider");
+ await expect(empty).toExist();
+ });
+
+ it("should render the divider with the specified text", async () => {
+ render(html`
Your Message Here`);
+ 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();
+ });
+});
diff --git a/web/src/elements/EmptyState.test.ts b/web/src/elements/tests/EmptyState.test.ts
similarity index 53%
rename from web/src/elements/EmptyState.test.ts
rename to web/src/elements/tests/EmptyState.test.ts
index 4ebe3ceccd..265b1f91e3 100644
--- a/web/src/elements/EmptyState.test.ts
+++ b/web/src/elements/tests/EmptyState.test.ts
@@ -1,12 +1,23 @@
+import { render } from "@goauthentik/elements/tests/utils.js";
import { $, expect } from "@wdio/globals";
import { msg } from "@lit/localize";
import { html } from "lit";
-import "./EmptyState.js";
-import { render } from "./tests/utils.js";
+import "../EmptyState.js";
+import { akEmptyState } from "../EmptyState.js";
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`
`);
@@ -48,4 +59,33 @@ describe("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`
Introspecting
+
... carefully`,
+ ),
+ );
+
+ 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");
+ });
});
diff --git a/web/src/elements/tests/Expand.test.ts b/web/src/elements/tests/Expand.test.ts
new file mode 100644
index 0000000000..bc878b2cb8
--- /dev/null
+++ b/web/src/elements/tests/Expand.test.ts
@@ -0,0 +1,93 @@
+import { render } from "@goauthentik/elements/tests/utils.js";
+import { $, expect } from "@wdio/globals";
+
+import { html } from "lit";
+
+import "../Expand.js";
+import { akExpand } from "../Expand.js";
+
+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`
This is the expanded text
`);
+ 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`
This is the expanded text
`);
+ 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`
This is the expanded text
`);
+ 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`
This is the expanded text
`,
+ );
+ 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`
This is the new text.
`,
+ ),
+ );
+ 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",
+ );
+ });
+});
diff --git a/web/src/elements/tests/Label.test.ts b/web/src/elements/tests/Label.test.ts
new file mode 100644
index 0000000000..97e3ff4b82
--- /dev/null
+++ b/web/src/elements/tests/Label.test.ts
@@ -0,0 +1,62 @@
+import { render } from "@goauthentik/elements/tests/utils.js";
+import { $, expect } from "@wdio/globals";
+
+import { html } from "lit";
+
+import "../Label.js";
+import { PFColor, akLabel } from "../Label.js";
+
+describe("ak-label", () => {
+ it("should render a label with the enum", async () => {
+ render(html`
This is a 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`
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",
+ );
+ });
+
+ it("should render a compact label with the default level", async () => {
+ render(html`
This is a 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`
This is a 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",
+ );
+ });
+});
diff --git a/web/src/elements/tests/LoadingOverlay.test.ts b/web/src/elements/tests/LoadingOverlay.test.ts
new file mode 100644
index 0000000000..35bbfa878f
--- /dev/null
+++ b/web/src/elements/tests/LoadingOverlay.test.ts
@@ -0,0 +1,33 @@
+import { render } from "@goauthentik/elements/tests/utils.js";
+import { $, expect } from "@wdio/globals";
+
+import { html } from "lit";
+
+import "../LoadingOverlay.js";
+import { akLoadingOverlay } from "../LoadingOverlay.js";
+
+describe("ak-loading-overlay", () => {
+ it("should render the default loader", async () => {
+ render(html`
`);
+
+ const empty = await $("ak-loading-overlay");
+ await expect(empty).toExist();
+ });
+
+ it("should render a slotted message", async () => {
+ render(
+ html`
+ Try again with a different filter
+ `,
+ );
+
+ 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");
+ });
+});
diff --git a/web/src/elements/types.ts b/web/src/elements/types.ts
index 76f8cb231d..141e596846 100644
--- a/web/src/elements/types.ts
+++ b/web/src/elements/types.ts
@@ -1,6 +1,6 @@
import { AKElement } from "@goauthentik/elements/Base";
-import { TemplateResult } from "lit";
+import { TemplateResult, nothing } from "lit";
import { ReactiveControllerHost } from "lit";
export type ReactiveElementHost
= Partial & T;
@@ -73,3 +73,6 @@ export type SelectGrouped = {
*/
export type GroupedOptions = SelectGrouped | SelectFlat;
export type SelectOptions = SelectOption[] | GroupedOptions;
+
+export type SlottedTemplateResult = string | TemplateResult | typeof nothing;
+export type Spread = { [key: string]: unknown };
diff --git a/web/src/enterprise/rac/index.ts b/web/src/enterprise/rac/index.ts
index d90e98d6e0..7234257ca3 100644
--- a/web/src/enterprise/rac/index.ts
+++ b/web/src/enterprise/rac/index.ts
@@ -316,7 +316,7 @@ export class RacInterface extends Interface {
${this.clientState !== GuacClientState.CONNECTED
? html`
-
+
${this.hasConnected
? html`${this.reconnectingMessage}`
: html`${msg("Connecting...")}`}
diff --git a/web/src/user/LibraryApplication/index.ts b/web/src/user/LibraryApplication/index.ts
index 85f5ccad4d..6c4ec6623f 100644
--- a/web/src/user/LibraryApplication/index.ts
+++ b/web/src/user/LibraryApplication/index.ts
@@ -68,7 +68,7 @@ export class LibraryApplication extends AKElement {
renderExpansion(application: Application) {
const me = rootInterface()?.me;
- return html`
+ return html`
${application.metaPublisher}