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:
		| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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"> | ||||
|  | ||||
							
								
								
									
										37
									
								
								web/src/elements/tests/Alert.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/elements/tests/Alert.test.ts
									
									
									
									
									
										Normal 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"); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										35
									
								
								web/src/elements/tests/Divider.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/elements/tests/Divider.test.ts
									
									
									
									
									
										Normal 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(); | ||||
|     }); | ||||
| }); | ||||
| @ -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"); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										93
									
								
								web/src/elements/tests/Expand.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								web/src/elements/tests/Expand.test.ts
									
									
									
									
									
										Normal 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", | ||||
|         ); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										62
									
								
								web/src/elements/tests/Label.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								web/src/elements/tests/Label.test.ts
									
									
									
									
									
										Normal 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", | ||||
|         ); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										33
									
								
								web/src/elements/tests/LoadingOverlay.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/src/elements/tests/LoadingOverlay.test.ts
									
									
									
									
									
										Normal 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"); | ||||
|     }); | ||||
| }); | ||||
| @ -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 }; | ||||
|  | ||||
| @ -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...")}`} | ||||
|  | ||||
| @ -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> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Ken Sternberg
					Ken Sternberg