web: unit tests for the simple things, with fixes that the tests revealed (#11633)

* Added tests and refinements as tests indicate.

* Building out the test suite.

* web: test the simple things. Fix what the tests revealed.

- Move `EmptyState.test.ts` into the `./tests` folder.
- Provide unit tests for:
  - Alert
  - Divider
  - Expand
  - Label
  - LoadingOverlay
- Give all tested items an Interface and a functional variant for rendering
- Give Label an alternative syntax for declaring alert levels
- Remove the slot name in LoadingOverlay
  - Change the slot call in `./enterprise/rac/index.ts` to not need the slot name as well
- Change the attribute names `topMost`, `textOpen`, and `textClosed` to `topmost`, `text-open`, and
  `text-closed`, respectively.
  - Change locations in the code where those are used to correspond

** Why interfaces: **

Provides another check on the input/output boundaries of our elements, gives Storybook and
WebdriverIO another validation to check, and guarantees any rendering functions cannot be passed
invalid property names.

** Why functions for rendering: **

Providing functions for rendering gets us one step closer to dynamically defining our forms-in-code
at runtime without losing any type safety.

** Why rename the attributes: **

A *very* subtle bug:
[Element:setAttribute()](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute)
automatically "converts an attribute name to all lower-case when called on an HTML element in an
HTML document." The three attributes renamed are all treated *as* attributes, either classic boolean
or stringly-typed attributes, and attempting to manipulate them with `setAttribute()` will fail.

All of these attributes are presentational; none of them end up in a transaction with the back-end,
so kebab-to-camel conversions are not a concern.

Also, ["topmost" is one word](https://www.merriam-webster.com/dictionary/topmost).

** Why remove the slot name: **

Because there was only one slot.  A name is not needed.

* Fix minor spelling error.
This commit is contained in:
Ken Sternberg
2024-10-10 15:14:29 -07:00
committed by GitHub
parent 795e0ff100
commit 058a388518
16 changed files with 495 additions and 65 deletions

View File

@ -1,7 +1,10 @@
import { AKElement } from "@goauthentik/elements/Base";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";
import { CSSResult, TemplateResult, html } from "lit";
import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@ -13,36 +16,84 @@ export enum Level {
Danger = "pf-m-danger",
}
export const levelNames = ["warning", "info", "success", "danger"];
export type Levels = (typeof levelNames)[number];
export interface IAlert {
inline?: boolean;
plain?: boolean;
icon?: string;
level?: string;
}
/**
* @class Alert
* @element ak-alert
*
* Alerts are in-page elements intended to draw the user's attention and alert them to important
* details. Alerts are used alongside form elements to warn users of potential mistakes they can
* make, as well as in in-line documentation.
*/
@customElement("ak-alert")
export class Alert extends AKElement {
export class Alert extends AKElement implements IAlert {
/**
* Whether or not to display the entire component's contents in-line or not.
*
* @attr
*/
@property({ type: Boolean })
inline = false;
@property({ type: Boolean })
plain = false;
/**
* Method of determining severity
*
* @attr
*/
@property()
level: Level = Level.Warning;
level: Level | Levels = Level.Warning;
static get styles(): CSSResult[] {
/**
* Icon to display
*
* @attr
*/
@property()
icon = "fa-exclamation-circle";
static get styles() {
return [PFBase, PFAlert];
}
render(): TemplateResult {
return html`<div
class="pf-c-alert ${this.inline ? "pf-m-inline" : ""} ${this.plain
? "pf-m-plain"
: ""} ${this.level}"
>
get classmap() {
const level = levelNames.includes(this.level)
? `pf-m-${this.level}`
: (this.level as string);
return {
"pf-c-alert": true,
"pf-m-inline": this.inline,
"pf-m-plain": this.plain,
[level]: true,
};
}
render() {
return html`<div class="${classMap(this.classmap)}">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
<i class="fas ${this.icon}"></i>
</div>
<h4 class="pf-c-alert__title">
<slot></slot>
</h4>
<h4 class="pf-c-alert__title"><slot></slot></h4>
</div>`;
}
}
export function akAlert(properties: IAlert, content: SlottedTemplateResult = nothing) {
const message = typeof content === "string" ? html`<span>${content}</span>` : content;
return html`<ak-alert ${spread(properties as Spread)}>${message}</ak-alert>`;
}
declare global {
interface HTMLElementTagNameMap {
"ak-alert": Alert;

View File

@ -1,13 +1,14 @@
import { AKElement } from "@goauthentik/elements/Base";
import { type SlottedTemplateResult } from "@goauthentik/elements/types";
import { CSSResult, TemplateResult, css, html } from "lit";
import { css, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-divider")
export class Divider extends AKElement {
static get styles(): CSSResult[] {
static get styles() {
return [
PFBase,
css`
@ -35,11 +36,18 @@ export class Divider extends AKElement {
];
}
render(): TemplateResult {
return html`<div class="separator"><slot></slot></div>`;
render() {
return html`<div class="separator">
<slot></slot>
</div>`;
}
}
export function akDivider(content: SlottedTemplateResult = nothing) {
const message = typeof content === "string" ? html`<span>${content}</span>` : content;
return html`<ak-divider>${message}</ak-divider>`;
}
declare global {
interface HTMLElementTagNameMap {
"ak-divider": Divider;

View File

@ -1,17 +1,26 @@
import { PFSize } from "@goauthentik/common/enums.js";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Spinner";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
export interface IEmptyState {
icon?: string;
loading?: boolean;
fullHeight?: boolean;
header?: string;
}
@customElement("ak-empty-state")
export class EmptyState extends AKElement {
export class EmptyState extends AKElement implements IEmptyState {
@property({ type: String })
icon = "";
@ -24,7 +33,7 @@ export class EmptyState extends AKElement {
@property()
header?: string;
static get styles(): CSSResult[] {
static get styles() {
return [
PFBase,
PFEmptyState,
@ -38,7 +47,7 @@ export class EmptyState extends AKElement {
];
}
render(): TemplateResult {
render() {
return html`<div class="pf-c-empty-state ${this.fullHeight && "pf-m-full-height"}">
<div class="pf-c-empty-state__content">
${this.loading
@ -64,6 +73,12 @@ export class EmptyState extends AKElement {
}
}
export function akEmptyState(properties: IEmptyState, content: SlottedTemplateResult = nothing) {
const message =
typeof content === "string" ? html`<span slot="body">${content}</span>` : content;
return html`<ak-empty-state ${spread(properties as Spread)}>${message}</ak-empty-state>`;
}
declare global {
interface HTMLElementTagNameMap {
"ak-empty-state": EmptyState;

View File

@ -1,24 +1,32 @@
import { AKElement } from "@goauthentik/elements/Base";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFExpandableSection from "@patternfly/patternfly/components/ExpandableSection/expandable-section.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
export interface IExpand {
expanded?: boolean;
textOpen?: string;
textClosed?: string;
}
@customElement("ak-expand")
export class Expand extends AKElement {
export class Expand extends AKElement implements IExpand {
@property({ type: Boolean })
expanded = false;
@property()
@property({ type: String, attribute: "text-open" })
textOpen = msg("Show less");
@property()
@property({ type: String, attribute: "text-closed" })
textClosed = msg("Show more");
static get styles(): CSSResult[] {
static get styles() {
return [
PFBase,
PFExpandableSection,
@ -30,7 +38,7 @@ export class Expand extends AKElement {
];
}
render(): TemplateResult {
render() {
return html`<div
class="pf-c-expandable-section pf-m-display-lg pf-m-indented ${this.expanded
? "pf-m-expanded"
@ -58,6 +66,11 @@ export class Expand extends AKElement {
}
}
export function akExpand(properties: IExpand, content: SlottedTemplateResult = nothing) {
const message = typeof content === "string" ? html`<span>${content}</span>` : content;
return html`<ak-expand ${spread(properties as Spread)}>${message}</ak-expand>`;
}
declare global {
interface HTMLElementTagNameMap {
"ak-expand": Expand;

View File

@ -1,7 +1,10 @@
import { AKElement } from "@goauthentik/elements/Base";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";
import { CSSResult, TemplateResult, html } from "lit";
import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import PFLabel from "@patternfly/patternfly/components/Label/label.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@ -13,8 +16,25 @@ export enum PFColor {
Grey = "",
}
export const levelNames = ["warning", "info", "success", "danger"];
export type Level = (typeof levelNames)[number];
type Chrome = [Level, PFColor, string, string];
const chromeList: Chrome[] = [
["danger", PFColor.Red, "pf-m-red", "fa-times"],
["warning", PFColor.Orange, "pf-m-orange", "fa-exclamation-triangle"],
["success", PFColor.Green, "pf-m-green", "fa-check"],
["info", PFColor.Grey, "pf-m-grey", "fa-info-circle"],
];
export interface ILabel {
icon?: string;
compact?: boolean;
color?: string;
}
@customElement("ak-label")
export class Label extends AKElement {
export class Label extends AKElement implements ILabel {
@property()
color: PFColor = PFColor.Grey;
@ -24,33 +44,31 @@ export class Label extends AKElement {
@property({ type: Boolean })
compact = false;
static get styles(): CSSResult[] {
static get styles() {
return [PFBase, PFLabel];
}
getDefaultIcon(): string {
switch (this.color) {
case PFColor.Green:
return "fa-check";
case PFColor.Orange:
return "fa-exclamation-triangle";
case PFColor.Red:
return "fa-times";
case PFColor.Grey:
return "fa-info-circle";
default:
return "";
}
get classesAndIcon() {
const chrome = chromeList.find(
([level, color]) => this.color === level || this.color === color,
);
const [illo, icon] = chrome ? chrome.slice(2) : ["pf-m-grey", "fa-info-circle"];
return {
classes: {
"pf-c-label": true,
"pf-m-compact": this.compact,
...(illo ? { [illo]: true } : {}),
},
icon: this.icon ? this.icon : icon,
};
}
render(): TemplateResult {
return html`<span class="pf-c-label ${this.color} ${this.compact ? "pf-m-compact" : ""}">
render() {
const { classes, icon } = this.classesAndIcon;
return html`<span class=${classMap(classes)}>
<span class="pf-c-label__content">
<span class="pf-c-label__icon">
<i
class="fas fa-fw ${this.icon || this.getDefaultIcon()}"
aria-hidden="true"
></i>
<i class="fas fa-fw ${icon}" aria-hidden="true"></i>
</span>
<slot></slot>
</span>
@ -58,6 +76,11 @@ export class Label extends AKElement {
}
}
export function akLabel(properties: ILabel, content: SlottedTemplateResult = nothing) {
const message = typeof content === "string" ? html`<span>${content}</span>` : content;
return html`<ak-label ${spread(properties as Spread)}>${message}</ak-label>`;
}
declare global {
interface HTMLElementTagNameMap {
"ak-label": Label;

View File

@ -1,17 +1,24 @@
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";
import { CSSResult, TemplateResult, css, html } from "lit";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-loading-overlay")
export class LoadingOverlay extends AKElement {
@property({ type: Boolean })
topMost = false;
interface ILoadingOverlay {
topmost?: boolean;
}
static get styles(): CSSResult[] {
@customElement("ak-loading-overlay")
export class LoadingOverlay extends AKElement implements ILoadingOverlay {
// Do not camelize: https://www.merriam-webster.com/dictionary/topmost
@property({ type: Boolean, attribute: "topmost" })
topmost = false;
static get styles() {
return [
PFBase,
css`
@ -25,20 +32,30 @@ export class LoadingOverlay extends AKElement {
background-color: var(--pf-global--BackgroundColor--dark-transparent-200);
z-index: 1;
}
:host([topMost]) {
:host([topmost]) {
z-index: 999;
}
`,
];
}
render(): TemplateResult {
render() {
return html`<ak-empty-state loading header="">
<slot name="body" slot="body"></slot>
<slot></slot>
</ak-empty-state>`;
}
}
export function akLoadingOverlay(
properties: ILoadingOverlay,
content: SlottedTemplateResult = nothing,
) {
const message = typeof content === "string" ? html`<span>${content}</span>` : content;
return html`<ak-loading-overlay ${spread(properties as Spread)}
>${message}</ak-loading-overlay
>`;
}
declare global {
interface HTMLElementTagNameMap {
"ak-loading-overlay": LoadingOverlay;

View File

@ -56,7 +56,7 @@ export class ModalForm extends ModalButton {
renderModalInner(): TemplateResult {
return html`${this.loading
? html`<ak-loading-overlay ?topMost=${true}></ak-loading-overlay>`
? html`<ak-loading-overlay topmost></ak-loading-overlay>`
: html``}
<section class="pf-c-modal-box__header pf-c-page__main-section pf-m-light">
<div class="pf-c-content">

View File

@ -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`<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");
});
});

View File

@ -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`<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();
});
});

View File

@ -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`<ak-empty-state ?loading=${true} header=${msg("Loading")}> </ak-empty-state>`);
@ -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` <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");
});
});

View File

@ -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`<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",
);
});
});

View File

@ -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`<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",
);
});
});

View File

@ -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`<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");
});
});

View File

@ -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<T = AKElement> = Partial<ReactiveControllerHost> & T;
@ -73,3 +73,6 @@ export type SelectGrouped<T = never> = {
*/
export type GroupedOptions<T = never> = SelectGrouped<T> | SelectFlat<T>;
export type SelectOptions<T = never> = SelectOption<T>[] | GroupedOptions<T>;
export type SlottedTemplateResult = string | TemplateResult | typeof nothing;
export type Spread = { [key: string]: unknown };

View File

@ -316,7 +316,7 @@ export class RacInterface extends Interface {
${this.clientState !== GuacClientState.CONNECTED
? html`
<ak-loading-overlay>
<span slot="body">
<span>
${this.hasConnected
? html`${this.reconnectingMessage}`
: html`${msg("Connecting...")}`}

View File

@ -68,7 +68,7 @@ export class LibraryApplication extends AKElement {
renderExpansion(application: Application) {
const me = rootInterface<UserInterface>()?.me;
return html`<ak-expand textOpen=${msg("Less details")} textClosed=${msg("More details")}>
return html`<ak-expand text-open=${msg("Less details")} text-closed=${msg("More details")}>
<div class="pf-c-content">
<small>${application.metaPublisher}</small>
</div>