From 83bac1ff6d56b5a89ae9d7f4875e29cec8fb2664 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Wed, 15 May 2024 16:02:37 -0700 Subject: [PATCH] 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. --- web/.storybook/css-import-maps.ts | 8 +- web/src/elements/cards/AggregateCard.ts | 19 +++-- .../elements/cards/AggregatePromiseCard.ts | 7 +- web/src/elements/cards/QuickActionsCard.ts | 1 - .../cards/stories/AggregateCard.stories.ts | 2 +- .../cards/stories/AggregateCard.tests.ts | 83 +++++++++++++++++++ .../stories/AggregatePromiseCard.stories.ts | 11 ++- .../stories/AggregatePromiseCard.tests.ts | 59 +++++++++++++ .../cards/stories/QuickActionCard.tests.ts | 39 +++++++++ 9 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 web/src/elements/cards/stories/AggregateCard.tests.ts create mode 100644 web/src/elements/cards/stories/AggregatePromiseCard.tests.ts create mode 100644 web/src/elements/cards/stories/QuickActionCard.tests.ts diff --git a/web/.storybook/css-import-maps.ts b/web/.storybook/css-import-maps.ts index 5415a2b272..c321a2a724 100644 --- a/web/.storybook/css-import-maps.ts +++ b/web/.storybook/css-import-maps.ts @@ -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; diff --git a/web/src/elements/cards/AggregateCard.ts b/web/src/elements/cards/AggregateCard.ts index 847b2c5d83..e64d17952f 100644 --- a/web/src/elements/cards/AggregateCard.ts +++ b/web/src/elements/cards/AggregateCard.ts @@ -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`
-  ${this.renderHeader()} + ${this.icon + ? html` ` + : nothing}${this.renderHeader()}
${this.renderHeaderLink()}
${this.renderInner()} - ${this.subtext ? html`

${this.subtext}

` : html``} + ${this.subtext ? html`

${this.subtext}

` : nothing}
`; diff --git a/web/src/elements/cards/AggregatePromiseCard.ts b/web/src/elements/cards/AggregatePromiseCard.ts index e7170cd020..39148d3434 100644 --- a/web/src/elements/cards/AggregatePromiseCard.ts +++ b/web/src/elements/cards/AggregatePromiseCard.ts @@ -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>; /** - * 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"); diff --git a/web/src/elements/cards/QuickActionsCard.ts b/web/src/elements/cards/QuickActionsCard.ts index 4e8ec6e94f..dc68f4a89f 100644 --- a/web/src/elements/cards/QuickActionsCard.ts +++ b/web/src/elements/cards/QuickActionsCard.ts @@ -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"; diff --git a/web/src/elements/cards/stories/AggregateCard.stories.ts b/web/src/elements/cards/stories/AggregateCard.stories.ts index 1e1b4cf9e1..b7bf946464 100644 --- a/web/src/elements/cards/stories/AggregateCard.stories.ts +++ b/web/src/elements/cards/stories/AggregateCard.stories.ts @@ -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`
diff --git a/web/src/elements/cards/stories/AggregateCard.tests.ts b/web/src/elements/cards/stories/AggregateCard.tests.ts new file mode 100644 index 0000000000..85f4967f61 --- /dev/null +++ b/web/src/elements/cards/stories/AggregateCard.tests.ts @@ -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`

This is the main content

`, + ); + 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`

This is the main content

`, + ); + 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`

This is the main content

`, + ); + 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`

This is the main content

`, + ); + 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"); + }); +}); diff --git a/web/src/elements/cards/stories/AggregatePromiseCard.stories.ts b/web/src/elements/cards/stories/AggregatePromiseCard.stories.ts index 32e088c992..6de349c584 100644 --- a/web/src/elements/cards/stories/AggregatePromiseCard.stories.ts +++ b/web/src/elements/cards/stories/AggregatePromiseCard.stories.ts @@ -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)); diff --git a/web/src/elements/cards/stories/AggregatePromiseCard.tests.ts b/web/src/elements/cards/stories/AggregatePromiseCard.tests.ts new file mode 100644 index 0000000000..91ea5b43fc --- /dev/null +++ b/web/src/elements/cards/stories/AggregatePromiseCard.tests.ts @@ -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``); + + 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``, + ); + + 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); + } + }); +}); diff --git a/web/src/elements/cards/stories/QuickActionCard.tests.ts b/web/src/elements/cards/stories/QuickActionCard.tests.ts new file mode 100644 index 0000000000..9a4b938955 --- /dev/null +++ b/web/src/elements/cards/stories/QuickActionCard.tests.ts @@ -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``); + 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"); + }) +}) + +