From 5257370e4ad2f70324c576128422a98f6d8fd456 Mon Sep 17 00:00:00 2001 From: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:56:28 -0700 Subject: [PATCH] web: small fixes for elements and forms (#11546) * web: small fixes for wdio and lint - Roll back another dependabot breaking change, this time to WebdriverIO - Remove the redundant scripts wrapping ESLint for Precommit mode. Access to those modes is available through the flags to the `./web/scripts/eslint.mjs` script. - Remove SonarJS checks until SonarJS is ESLint 9 compatible. - Minor nitpicking. * package-lock.json update * web: small fixes for wdio and lint **PLEASE** Stop trying to upgrade WebdriverIO following Dependabot's instructions. The changes between wdio8 and wdio9 are extensive enough to require a lot more manual intervention. The unit tests fail in wdio 9, with the testbed driver Wdio uses to compile content to push to the browser ([vite](https://vitejs.dev) complaining: ``` 2024-09-27T15:30:03.672Z WARN @wdio/browser-runner:vite: warning: Unrecognized default export in file /Users/ken/projects/dev/web/node_modules/@patternfly/patternfly/components/Dropdown/dropdown.css Plugin: postcss-lit File: /Users/ken/projects/dev/web/node_modules/@patternfly/patternfly/components/Dropdown/dropdown.css [0-6] 2024-09-27T15:30:04.083Z INFO webdriver: BIDI COMMAND script.callFunction {"functionDeclaration":"","awaitPromise":true,"arguments":[],"target":{"context":"8E608E6D13E355DFFC28112C236B73AF"}} [0-6] Error: Test failed due to following error(s): - ak-search-select.test.ts: The requested module '/src/common/styles/authentik.css' does not provide an export named 'default': SyntaxError: The requested module '/src/common/styles/authentik.css' does not provide an export named 'default' ``` So until we can figure out why the Vite installation isn't liking our CSS import scheme, we'll have to soldier on with what we have. At least with Wdio 8, we get: ``` Spec Files: 7 passed, 7 total (100% completed) in 00:00:19 ``` * Forgot to run prettier. * web: small fixes for elements and forms - provides a new utility, `_isSlug_`, used to verify a user input - extends the ak-horizontal-component wrapper to have a stronger identity and available value - updates the types that use the wrapper to be typed more strongly - (Why) The above are used in the wizard to get and store values - fixes a bug in SearchSelectEZ that broke the display if the user didn't supply a `groupBy` field. - Adds `@wdio/types` to the package file so eslint is satisfied wdio builds correctly - updates the end-to-end test to understand the revised button identities on the login page - Running the end-to-end tests verifies that changes to the components listed above did not break the semantics of those components. * Removing SonarJS comments. * Reverting to log level for tests. --- web/package-lock.json | 12 ++++++------ web/src/common/utils.ts | 6 ++++++ .../components/HorizontalLightComponent.ts | 6 +++++- web/src/components/ak-number-input.ts | 10 ++++++++-- web/src/components/ak-radio-input.ts | 2 +- web/src/components/ak-slug-input.ts | 2 +- web/src/components/ak-text-input.ts | 7 ++++++- web/src/components/ak-textarea-input.ts | 2 +- .../cards/tests/QuickActionCard.test.ts | 3 ++- .../forms/SearchSelect/ak-search-select-ez.ts | 6 ++++-- .../forms/SearchSelect/ak-search-select.ts | 19 ++++++------------- .../tests/ak-search-select-view.comp.ts | 5 +++-- .../tests/ak-search-select-view.test.ts | 12 ++++++------ .../tests/ak-search-select.test.ts | 5 ++--- web/wdio.conf.ts | 13 ++++++++++--- 15 files changed, 67 insertions(+), 43 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 89313ba225..a61f6518db 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -3647,9 +3647,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.21.0.tgz", - "integrity": "sha512-v50lnVbzS8mpMSnEVxR+G75XpvxHKtkJaQrNPE8+/fF6Ppr5z4bcdcBhcP8LPfEW+4BZcic6VifMXRwTopc+kw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.22.0.tgz", + "integrity": "sha512-AYuihByeAkW17tuf40nKhnnxDBkshr5An3XjEJoUiN1OPU3w+iVVWB4f0g3XC1TBWFDLnChYH9ODaSq7IkpjPQ==", "dev": true, "optional": true, "bin": { @@ -10198,9 +10198,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.31", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz", - "integrity": "sha512-QcDoBbQeYt0+3CWcK/rEbuHvwpbT/8SV9T3OSgs6cX1FlcUAkgrkqbg9zLnDrMM/rLamzQwal4LYFCiWk861Tg==", + "version": "1.5.32", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.32.tgz", + "integrity": "sha512-M+7ph0VGBQqqpTT2YrabjNKSQ2fEl9PVx6AK3N558gDH9NO8O6XN9SXXFWRo9u9PbEg/bWq+tjXQr+eXmxubCw==", "dev": true }, "node_modules/emoji-regex": { diff --git a/web/src/common/utils.ts b/web/src/common/utils.ts index 7c5dfff92a..f9e19234fa 100644 --- a/web/src/common/utils.ts +++ b/web/src/common/utils.ts @@ -25,6 +25,12 @@ export function convertToSlug(text: string): string { .replace(/[^\w-]+/g, ""); } +export function isSlug(text: string): boolean { + const lowered = text.toLowerCase(); + const forbidden = /([^\w-]|\s)/.test(lowered); + return lowered === text && !forbidden; +} + /** * Truncate a string based on maximum word count */ diff --git a/web/src/components/HorizontalLightComponent.ts b/web/src/components/HorizontalLightComponent.ts index 7d34c833d6..2f0dcccaf9 100644 --- a/web/src/components/HorizontalLightComponent.ts +++ b/web/src/components/HorizontalLightComponent.ts @@ -1,11 +1,12 @@ import { AKElement } from "@goauthentik/elements/Base"; +import "@goauthentik/elements/forms/HorizontalFormElement.js"; import { TemplateResult, html, nothing } from "lit"; import { property } from "lit/decorators.js"; type HelpType = TemplateResult | typeof nothing; -export class HorizontalLightComponent extends AKElement { +export class HorizontalLightComponent extends AKElement { // Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but // we're not actually using that and, for the meantime, we need the form handlers to be able to // find the children of this component. @@ -41,6 +42,9 @@ export class HorizontalLightComponent extends AKElement { @property({ attribute: false }) errorMessages: string[] = []; + @property({ attribute: false }) + value?: T; + renderControl() { throw new Error("Must be implemented in a subclass"); } diff --git a/web/src/components/ak-number-input.ts b/web/src/components/ak-number-input.ts index 917165bef1..726dca4726 100644 --- a/web/src/components/ak-number-input.ts +++ b/web/src/components/ak-number-input.ts @@ -5,13 +5,19 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { HorizontalLightComponent } from "./HorizontalLightComponent"; @customElement("ak-number-input") -export class AkNumberInput extends HorizontalLightComponent { +export class AkNumberInput extends HorizontalLightComponent { @property({ type: Number, reflect: true }) - value = 0; + value = NaN; renderControl() { + const setValue = (ev: InputEvent) => { + const value = (ev.target as HTMLInputElement).value; + this.value = value.trim() === "" ? NaN : parseInt(value, 10); + }; + return html` extends HorizontalLightComponent { +export class AkRadioInput extends HorizontalLightComponent { @property({ type: Object }) value!: T; diff --git a/web/src/components/ak-slug-input.ts b/web/src/components/ak-slug-input.ts index 5ad7e21a0c..449e985c79 100644 --- a/web/src/components/ak-slug-input.ts +++ b/web/src/components/ak-slug-input.ts @@ -7,7 +7,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { HorizontalLightComponent } from "./HorizontalLightComponent"; @customElement("ak-slug-input") -export class AkSlugInput extends HorizontalLightComponent { +export class AkSlugInput extends HorizontalLightComponent { @property({ type: String, reflect: true }) value = ""; diff --git a/web/src/components/ak-text-input.ts b/web/src/components/ak-text-input.ts index 72dbc3af5b..d31f90e148 100644 --- a/web/src/components/ak-text-input.ts +++ b/web/src/components/ak-text-input.ts @@ -5,13 +5,18 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { HorizontalLightComponent } from "./HorizontalLightComponent"; @customElement("ak-text-input") -export class AkTextInput extends HorizontalLightComponent { +export class AkTextInput extends HorizontalLightComponent { @property({ type: String, reflect: true }) value = ""; renderControl() { + const setValue = (ev: InputEvent) => { + this.value = (ev.target as HTMLInputElement).value; + }; + return html` { @property({ type: String, reflect: true }) value = ""; diff --git a/web/src/elements/cards/tests/QuickActionCard.test.ts b/web/src/elements/cards/tests/QuickActionCard.test.ts index c4cba27965..47beaa80d9 100644 --- a/web/src/elements/cards/tests/QuickActionCard.test.ts +++ b/web/src/elements/cards/tests/QuickActionCard.test.ts @@ -35,7 +35,8 @@ describe("ak-quick-actions-card", () => { >`, ); const component = await $("ak-quick-actions-card"); - const items = await component.$$(">>>.pf-c-list li").getElements(); + const items = await component.$$(">>>.pf-c-list li"); + // @ts-expect-error "Another ChainablePromise mistake" await expect(Array.from(items).length).toEqual(5); await expect(await component.$(">>>.pf-c-list li:nth-of-type(4)")).toHaveText( "Manage users", diff --git a/web/src/elements/forms/SearchSelect/ak-search-select-ez.ts b/web/src/elements/forms/SearchSelect/ak-search-select-ez.ts index 4160390443..1f55bb32c8 100644 --- a/web/src/elements/forms/SearchSelect/ak-search-select-ez.ts +++ b/web/src/elements/forms/SearchSelect/ak-search-select-ez.ts @@ -9,7 +9,7 @@ export interface ISearchSelectApi { renderDescription?: (element: T) => string | TemplateResult; value: (element: T | undefined) => unknown; selected?: (element: T, elements: T[]) => boolean; - groupBy: (items: T[]) => [string, T[]][]; + groupBy?: (items: T[]) => [string, T[]][]; } export interface ISearchSelectEz extends ISearchSelectBase { @@ -58,7 +58,9 @@ export class SearchSelectEz extends SearchSelectBase implements ISearchSel this.renderDescription = this.config.renderDescription; this.value = this.config.value; this.selected = this.config.selected; - this.groupBy = this.config.groupBy; + if (this.config.groupBy !== undefined) { + this.groupBy = this.config.groupBy; + } super.connectedCallback(); } } diff --git a/web/src/elements/forms/SearchSelect/ak-search-select.ts b/web/src/elements/forms/SearchSelect/ak-search-select.ts index 36b8b43325..284ae02098 100644 --- a/web/src/elements/forms/SearchSelect/ak-search-select.ts +++ b/web/src/elements/forms/SearchSelect/ak-search-select.ts @@ -21,14 +21,12 @@ export interface ISearchSelect extends ISearchSelectBase { * The API layer of ak-search-select * * - @prop fetchObjects (Function): The function by which objects are retrieved by the API. - * - @prop renderElement (Function | string): Either a function that can retrieve the string - * "label" of the element, or the name of the field from which the label can be retrieved.¹ - * - @prop renderDescription (Function | string): Either a function that can retrieve the string - * or TemplateResult "description" of the element, or the name of the field from which the - * description can be retrieved.¹ - * - @prop value (Function | string): Either a function that can retrieve the value (the current - * API object's primary key) selected or the name of the field from which the value can be - * retrieved.¹ + * - @prop renderElement (Function): A function that can retrieve the string + * "label" of the element + * - @prop renderDescription (Function): A function that can retrieve the string + * or TemplateResult "description" of the element + * - @prop value (Function | string): A function that can retrieve the value (the current + * API object's primary key) selected. * - @prop selected (Function): A function that retrieves the current "live" value from the list of objects fetched by the function above. * - @prop groupBy (Function): A function that can group the objects fetched from the API by @@ -41,11 +39,6 @@ export interface ISearchSelect extends ISearchSelectBase { * shown if `blankable` * - @attr selectedObject (Object): The current object, or undefined, selected * - * ¹ Due to a limitation in the parsing of properties-vs-attributes, these must be defined as - * properties, not attributes. As a consequence, they must be declared in property syntax. - * Example: - * - * `.renderElement=${"name"}` * * - @fires ak-change - When a value from the collection has been positively chosen, either as a * consequence of the user typing or when selecting from the list. diff --git a/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.comp.ts b/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.comp.ts index 3a230b7d83..ea5a0edb28 100644 --- a/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.comp.ts +++ b/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.comp.ts @@ -39,7 +39,8 @@ export class AkSearchSelectViewDriver { ); } const id = await element.getAttribute("data-ouia-component-id"); - const menu = await $(`[data-ouia-component-id="menu-${id}"]`).getElement(); + const menu = await $(`[data-ouia-component-id="menu-${id}"]`); + // @ts-expect-error "Another ChainablePromise mistake" return new AkSearchSelectViewDriver(element, menu); } @@ -52,7 +53,7 @@ export class AkSearchSelectViewDriver { } async listElements() { - return await this.menu.$$(">>>li").getElements(); + return await this.menu.$$(">>>li"); } async focusOnInput() { diff --git a/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.test.ts b/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.test.ts index bdeeadbc1f..b159ee0605 100644 --- a/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.test.ts +++ b/web/src/elements/forms/SearchSelect/tests/ak-search-select-view.test.ts @@ -21,9 +21,8 @@ describe("Search select: Test Input Field", () => { html` `, document.body, ); - select = await AkSearchSelectViewDriver.build( - await $("ak-search-select-view").getElement(), - ); + // @ts-expect-error "Another ChainablePromise mistake" + select = await AkSearchSelectViewDriver.build(await $("ak-search-select-view")); }); it("should open the menu when the input is clicked", async () => { @@ -58,9 +57,8 @@ describe("Search select: Test Input Field", () => { expect(await select.open).toBe(false); expect(await select.menuIsVisible()).toBe(false); await browser.keys("A"); - select = await AkSearchSelectViewDriver.build( - await $("ak-search-select-view").getElement(), - ); + // @ts-expect-error "Another ChainablePromise mistake" + select = await AkSearchSelectViewDriver.build(await $("ak-search-select-view")); expect(await select.open).toBe(true); expect(await select.menuIsVisible()).toBe(true); }); @@ -69,6 +67,7 @@ describe("Search select: Test Input Field", () => { await select.focusOnInput(); await browser.keys("Ap"); await expect(await select.menuIsVisible()).toBe(true); + // @ts-expect-error "Another ChainablePromise mistake" const elements = Array.from(await select.listElements()); await expect(elements.length).toBe(2); }); @@ -77,6 +76,7 @@ describe("Search select: Test Input Field", () => { await select.focusOnInput(); await browser.keys("Ap"); await expect(await select.menuIsVisible()).toBe(true); + // @ts-expect-error "Another ChainablePromise mistake" const elements = Array.from(await select.listElements()); await expect(elements.length).toBe(2); await browser.keys(Key.Tab); diff --git a/web/src/elements/forms/SearchSelect/tests/ak-search-select.test.ts b/web/src/elements/forms/SearchSelect/tests/ak-search-select.test.ts index 9d176d9091..d47f5d1de7 100644 --- a/web/src/elements/forms/SearchSelect/tests/ak-search-select.test.ts +++ b/web/src/elements/forms/SearchSelect/tests/ak-search-select.test.ts @@ -97,9 +97,8 @@ describe("Search select: event driven startup", () => { mock?.dispatchEvent(new Event("resolve")); }); expect(await $(">>>ak-search-select-loading-indicator")).not.toBeDisplayed(); - select = await AkSearchSelectViewDriver.build( - await $(">>>ak-search-select-view").getElement(), - ); + // @ts-expect-error "Another ChainablePromise mistake" + select = await AkSearchSelectViewDriver.build(await $(">>>ak-search-select-view")); expect(await select).toBeExisting(); }); diff --git a/web/wdio.conf.ts b/web/wdio.conf.ts index e142bbc54e..e5fab5f1f0 100644 --- a/web/wdio.conf.ts +++ b/web/wdio.conf.ts @@ -42,7 +42,14 @@ export const config: WebdriverIO.Config = { }, ], - tsConfigPath: "./tsconfig.json", + // @ts-expect-error TS2353: The types are not up-to-date with Wdio9. + autoCompileOpts: { + autoCompile: true, + tsNodeOpts: { + project: "./tsconfig.json", + transpileOnly: true, + }, + }, // // ================== @@ -141,11 +148,11 @@ export const config: WebdriverIO.Config = { // baseUrl: 'http://localhost:8080', // // Default timeout for all waitFor* commands. - waitforTimeout: 10000, + waitforTimeout: 12000, // // Default timeout in milliseconds for request // if browser driver or grid doesn't send response - connectionRetryTimeout: 120000, + connectionRetryTimeout: 12000, // // Default request retries count connectionRetryCount: 3,