web: Provide tests for the aggregate cards, fix a few minor things

This commit provides tests alongside the stories for the aggregate cards. The tests are fairly
basic, but they're good enough for starting *and* they provide a pretty good example of how to test
when a promise with a delay is involved.

Two minor fixes in this code:

- The subtext was given a small amount of whitespace above, to remove the crowding that happened.
  It looks much better with a half-rem of space.
- In the rare case that we have a card header with no icon, the ' ' symbol that separates the
  icon from the header is now not rendered. In the previous form, it would push the header to the
  left, making it "hang in space" one rem to the right of the visual line formed by the rightmost
  content border.  The padding between the header, body, and footer is odd; body is 1 rem, the
  header and footer 2rems. This looks good for the graphs, but for the text, not so much.
This commit is contained in:
Ken Sternberg
2024-05-15 16:02:37 -07:00
parent 04355d8c4e
commit 83bac1ff6d
9 changed files with 210 additions and 19 deletions

View File

@ -58,10 +58,12 @@ const rawCssImportMaps = [
'import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css";',
'import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";',
'import PFSpinner from "@patternfly/patternfly/components/Spinner/spinner.css";',
'import PFSplit from "@patternfly/patternfly/layouts/Split/split.css";',
'import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";',
'import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";',
'import PFTable from "@patternfly/patternfly/components/Table/table.css";',
'import PFTabs from "@patternfly/patternfly/components/Tabs/tabs.css";',
'import PFText from "@patternfly/patternfly/utilities/Text/text.css";',
'import PFTitle from "@patternfly/patternfly/components/Title/title.css";',
'import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";',
'import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";',
@ -71,8 +73,10 @@ const rawCssImportMaps = [
'import styles from "./LibraryPageImpl.css";',
];
const cssImportMaps = rawCssImportMaps.reduce((acc, line) => (
{...acc, [line]: line.replace(/\.css/, ".css?inline")}), {});
const cssImportMaps = rawCssImportMaps.reduce(
(acc, line) => ({ ...acc, [line]: line.replace(/\.css/, ".css?inline") }),
{},
);
export { cssImportMaps };
export default cssImportMaps;

View File

@ -1,6 +1,6 @@
import { AKElement } from "@goauthentik/elements/Base";
import { CSSResult, TemplateResult, css, html } from "lit";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@ -31,7 +31,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
* the header.
*
* @attr
*/
*/
@property()
icon?: string;
@ -39,7 +39,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
* The title of the card.
*
* @attr
*/
*/
@property()
header?: string;
@ -47,7 +47,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
* If this is non-empty, a link icon will be shown in the upper-right corner of the card.
*
* @attr
*/
*/
@property()
headerLink?: string;
@ -55,7 +55,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
* If this is non-empty, a small-text footer will be shown at the bottom of the card
*
* @attr
*/
*/
@property()
subtext?: string;
@ -64,7 +64,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
* centered by default.
*
* @attr
*/
*/
@property({ type: Boolean, attribute: "left-justified" })
leftJustified = false;
@ -82,6 +82,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
text-align: center;
}
.subtext {
margin-top: var(--pf-global--spacer--sm);
font-size: var(--pf-global--FontSize--sm);
}
.pf-c-card__body {
@ -119,13 +120,15 @@ export class AggregateCard extends AKElement implements IAggregateCard {
return html`<div class="pf-c-card pf-c-card-aggregate">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__title">
<i class="${ifDefined(this.icon)}"></i>&nbsp;${this.renderHeader()}
${this.icon
? html`<i class="${ifDefined(this.icon)}"></i>&nbsp;`
: nothing}${this.renderHeader()}
</div>
${this.renderHeaderLink()}
</div>
<div class="pf-c-card__body ${this.leftJustified ? "" : "center-value"}">
${this.renderInner()}
${this.subtext ? html`<p class="subtext">${this.subtext}</p>` : html``}
${this.subtext ? html`<p class="subtext">${this.subtext}</p>` : nothing}
</div>
<div class="pf-c-card__footer">&nbsp;</div>
</div>`;

View File

@ -23,21 +23,20 @@ export interface IAggregatePromiseCard extends IAggregateCard {
@customElement("ak-aggregate-card-promise")
export class AggregatePromiseCard extends AggregateCard implements IAggregatePromiseCard {
/**
* If this contains an `fa-` style string, the FontAwesome icon specified will be shown next to
* the header.
*
* @attr
*/
*/
@property({ attribute: false })
promise?: Promise<Record<string, unknown>>;
/**
* The error message if the promise is rejected or throws an exception.
* The error message if the promise is rejected or throws an exception.
*
* @attr
*/
*/
@property()
failureMessage = msg("Operation failed to complete");

View File

@ -4,7 +4,6 @@ import "@goauthentik/elements/cards/AggregateCard.js";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { map } from "lit/directives/map.js";
import PFList from "@patternfly/patternfly/components/List/list.css";

View File

@ -31,7 +31,7 @@ export const DefaultStory: StoryObj = {
header: "Default",
headerLink: undefined,
subtext: undefined,
isCenter: false,
leftJustified: false,
},
render: ({ icon, header, headerLink, subtext, leftJustified }: IAggregateCard) => {
return html` <div style="background-color: #f0f0f0; padding: 1rem;">

View File

@ -0,0 +1,83 @@
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet.js";
import { $, expect } from "@wdio/globals";
import { msg } from "@lit/localize";
import { TemplateResult, html, render as litRender } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import "../AggregateCard.js";
const render = (body: TemplateResult) => {
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
ensureCSSStyleSheet(PFBase),
ensureCSSStyleSheet(AKGlobal),
];
return litRender(body, document.body);
};
describe("ak-aggregate-card", () => {
it("should render the standard card without an icon, link, or subtext", async () => {
render(
html`<ak-aggregate-card header="Loading"
><p>This is the main content</p></ak-aggregate-card
>`,
);
const component = await $("ak-aggregate-card");
await expect(await component.$(">>>.pf-c-card__header a")).not.toExist();
await expect(await component.$(">>>.pf-c-card__title i")).not.toExist();
await expect(await component.$(">>>.pf-c-card__title")).toHaveText("Loading");
await expect(await component.$(">>>.pf-c-card__body")).toHaveText(
"This is the main content",
);
await expect(await component.$(">>>.subtext")).not.toExist();
});
it("should render the standard card with an icon", async () => {
render(
html`<ak-aggregate-card icon="fa fa-bath" header="Loading"
><p>This is the main content</p></ak-aggregate-card
>`,
);
const component = await $("ak-aggregate-card");
await expect(await component.$(">>>.pf-c-card__title i")).toExist();
await expect(await component.$(">>>.pf-c-card__title")).toHaveText("Loading");
await expect(await component.$(">>>.pf-c-card__body")).toHaveText(
"This is the main content",
);
});
it("should render the standard card with an icon, a link, and slotted content", async () => {
render(
html`<ak-aggregate-card icon="fa fa-bath" header="Loading" headerLink="http://localhost"
><p>This is the main content</p></ak-aggregate-card
>`,
);
const component = await $("ak-aggregate-card");
await expect(await component.$(">>>.pf-c-card__header a")).toExist();
await expect(await component.$(">>>.pf-c-card__title i")).toExist();
await expect(await component.$(">>>.pf-c-card__title")).toHaveText("Loading");
await expect(await component.$(">>>.pf-c-card__body")).toHaveText(
"This is the main content",
);
});
it("should render the standard card with an icon, a link, and subtext", async () => {
render(
html`<ak-aggregate-card
icon="fa fa-bath"
header="Loading"
headerLink="http://localhost"
subtext="Xena had subtext"
><p>This is the main content</p></ak-aggregate-card
>`,
);
const component = await $("ak-aggregate-card");
await expect(await component.$(">>>.pf-c-card__header a")).toExist();
await expect(await component.$(">>>.pf-c-card__title i")).toExist();
await expect(await component.$(">>>.pf-c-card__title")).toHaveText("Loading");
await expect(await component.$(">>>.subtext")).toHaveText("Xena had subtext");
});
});

View File

@ -71,9 +71,14 @@ export const PromiseRejected: StoryObj = {
leftJustified: false,
failureMessage: undefined,
},
render: (
{ icon, header, headerLink, subtext, leftJustified, failureMessage }: IAggregatePromiseCard,
) => {
render: ({
icon,
header,
headerLink,
subtext,
leftJustified,
failureMessage,
}: IAggregatePromiseCard) => {
const runThis = (timeout: number, value: string) =>
new Promise((_resolve, reject) => setTimeout(reject, timeout, value));

View File

@ -0,0 +1,59 @@
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet.js";
import { $, expect } from "@wdio/globals";
import { msg } from "@lit/localize";
import { TemplateResult, html, render as litRender } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import "../AggregatePromiseCard.js";
const render = (body: TemplateResult) => {
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
ensureCSSStyleSheet(PFBase),
ensureCSSStyleSheet(AKGlobal),
];
return litRender(body, document.body);
};
describe("ak-aggregate-card-promise", () => {
it("should render the promise card and display the message after a 1 second timeout", async () => {
const text = "RESULT";
const runThis = (timeout: number, value: string) =>
new Promise((resolve, _reject) => setTimeout(resolve, timeout, value));
const promise = runThis(1000, text);
render(html`<ak-aggregate-card-promise .promise=${promise}></ak-aggregate-card-promise>`);
const component = await $("ak-aggregate-card-promise");
await expect(await component.$(">>>.pf-c-card__header a")).not.toExist();
await expect(await component.$(">>>ak-spinner")).toExist();
await promise;
await expect(await component.$(">>>ak-spinner")).not.toExist();
await expect(await component.$(">>>.pf-c-card__body")).toHaveText("RESULT");
});
it("should render the promise card and display failure after a 1 second timeout", async () => {
const text = "EXPECTED FAILURE";
const runThis = (timeout: number, value: string) =>
new Promise((_resolve, reject) => setTimeout(reject, timeout, value));
const promise = runThis(1000, text);
render(
html`<ak-aggregate-card-promise
.promise=${promise}
failureMessage=${text}
></ak-aggregate-card-promise>`,
);
const component = await $("ak-aggregate-card-promise");
await expect(await component.$(">>>.pf-c-card__header a")).not.toExist();
await expect(await component.$(">>>ak-spinner")).toExist();
try {
await promise;
} catch (e) {
await expect(await component.$(">>>ak-spinner")).not.toExist();
await expect(await component.$(">>>.pf-c-card__body")).toHaveText(text);
}
});
});

View File

@ -0,0 +1,39 @@
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet.js";
import { $, expect } from "@wdio/globals";
import { TemplateResult, html, render as litRender } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { QuickAction } from "../QuickActionsCard.js";
import "../QuickActionsCard.js";
const render = (body: TemplateResult) => {
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
ensureCSSStyleSheet(PFBase),
ensureCSSStyleSheet(AKGlobal),
];
return litRender(body, document.body);
};
const ACTIONS: QuickAction[] = [
["Create a new application", "/core/applications"],
["Check the logs", "/events/log"],
["Explore integrations", "https://goauthentik.io/integrations/", true],
["Manage users", "/identity/users"],
["Check the release notes", "https://goauthentik.io/docs/releases/", true],
];
describe("ak-quick-actions-card", () => {
it("display ak-quick-actions-card", async () => {
render(html`<ak-quick-actions-card title="Alt Title" .actions=${ACTIONS}></ak-quick-actions-card>`);
const component = await $("ak-quick-actions-card");
const items = await component.$$('>>>.pf-c-list li');
await expect(Array.from(items).length).toEqual(5);
await expect(await component.$(">>>.pf-c-list li:nth-of-type(4)")).toHaveText("Manage users");
})
})