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

@ -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

@ -0,0 +1,91 @@
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 { 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>`);
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 header=${msg("Loading")}> </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 render a static empty state", async () => {
render(html`<ak-empty-state header=${msg("No messages found")}> </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 header=${msg("No messages found")}>
<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");
});
});

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