* web: fix Flash of Unstructured Content while SearchSelect is loading from the backend Provide an alternative, readonly, disabled, unindexed input object with the text "Loading...", to be replaced with the _real_ input element after the content is loaded. This provides the correct appearance and spacing so the content doesn't jiggle about between the start of loading and the SearchSelect element being finalized. It was visually distracting and unappealing. * web: comment on state management in API layer, move file to point to correct component under test. * web: test for flash of unstructured content - Add a unit test to ensure the "Loading..." element is displayed correctly before data arrives - Demo how to mock a `fetchObjects()` call in testing. Very cool. - Make distinguishing rule sets for code, tests, and scripts in nightmare mode - In SearchSelect, Move the `styles()` declaration to the top of the class for consistency. - To test for the FLOUC issue in SearchSelect. This is both an exercise in mocking @beryju's `fetchObjects()` protocol, and shows how we can unit test generic components that render API objects.
113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
/* eslint-env jest */
|
|
import { AKElement } from "@goauthentik/elements/Base";
|
|
import { bound } from "@goauthentik/elements/decorators/bound.js";
|
|
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
|
import { $, browser } from "@wdio/globals";
|
|
import { slug } from "github-slugger";
|
|
|
|
import { html, render } from "lit";
|
|
import { customElement } from "lit/decorators.js";
|
|
import { property, query } from "lit/decorators.js";
|
|
|
|
import "../ak-search-select.js";
|
|
import { SearchSelect } from "../ak-search-select.js";
|
|
import { type ViewSample, sampleData } from "../stories/sampleData.js";
|
|
import { AkSearchSelectViewDriver } from "./ak-search-select-view.comp.js";
|
|
|
|
const renderElement = (fruit: ViewSample) => fruit.produce;
|
|
|
|
const renderDescription = (fruit: ViewSample) => html`${fruit.desc}`;
|
|
|
|
const renderValue = (fruit: ViewSample | undefined) => slug(fruit?.produce ?? "");
|
|
|
|
@customElement("ak-mock-search-group")
|
|
export class MockSearch extends CustomListenerElement(AKElement) {
|
|
/**
|
|
* The current fruit
|
|
*
|
|
* @attr
|
|
*/
|
|
@property({ type: String, reflect: true })
|
|
fruit?: string;
|
|
|
|
@query("ak-search-select")
|
|
search!: SearchSelect<ViewSample>;
|
|
|
|
selectedFruit?: ViewSample;
|
|
|
|
get value() {
|
|
return this.selectedFruit ? renderValue(this.selectedFruit) : undefined;
|
|
}
|
|
|
|
@bound
|
|
handleSearchUpdate(ev: CustomEvent) {
|
|
ev.stopPropagation();
|
|
this.selectedFruit = ev.detail.value;
|
|
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
|
|
}
|
|
|
|
@bound
|
|
selected(fruit: ViewSample) {
|
|
return this.fruit === slug(fruit.produce);
|
|
}
|
|
|
|
@bound
|
|
fetchObjects() {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const resolver = (resolve: any) => {
|
|
this.addEventListener("resolve", () => {
|
|
resolve(sampleData);
|
|
});
|
|
};
|
|
return new Promise(resolver);
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
<ak-search-select
|
|
.fetchObjects=${this.fetchObjects}
|
|
.renderElement=${renderElement}
|
|
.renderDescription=${renderDescription}
|
|
.value=${renderValue}
|
|
.selected=${this.selected}
|
|
managed
|
|
@ak-change=${this.handleSearchUpdate}
|
|
?blankable=${true}
|
|
>
|
|
</ak-search-select>
|
|
`;
|
|
}
|
|
}
|
|
|
|
describe("Search select: event driven startup", () => {
|
|
let select: AkSearchSelectViewDriver;
|
|
let wrapper: SearchSelect<ViewSample>;
|
|
|
|
beforeEach(async () => {
|
|
await render(html`<ak-mock-search-group></ak-mock-search-group>`, document.body);
|
|
// @ts-ignore
|
|
wrapper = await $(">>>ak-search-select");
|
|
});
|
|
|
|
it("should shift from the loading indicator to search select view on fetch event completed", async () => {
|
|
expect(await wrapper).toBeExisting();
|
|
expect(await $(">>>ak-search-select-loading-indicator")).toBeDisplayed();
|
|
await browser.execute(() => {
|
|
const mock = document.querySelector("ak-mock-search-group");
|
|
mock?.dispatchEvent(new Event("resolve"));
|
|
});
|
|
expect(await $(">>>ak-search-select-loading-indicator")).not.toBeDisplayed();
|
|
select = await AkSearchSelectViewDriver.build(await $(">>>ak-search-select-view"));
|
|
expect(await select).toBeExisting();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await document.body.querySelector("ak-mock-search-group")?.remove();
|
|
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
|
|
if (document.body["_$litPart$"]) {
|
|
// @ts-expect-error expression of type '"_$litPart$"' is added by Lit
|
|
delete document.body["_$litPart$"];
|
|
}
|
|
});
|
|
});
|