Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(combobox): update internal state after custom value is added #11405

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@ import {
import { html } from "../../../support/formatting";
import { CSS as ComboboxItemCSS } from "../combobox-item/resources";
import { CSS as XButtonCSS } from "../functional/XButton";
import { getElementXY, newProgrammaticE2EPage, skipAnimations } from "../../tests/utils";
import {
createEventTimePropValuesAsserter,
getElementXY,
newProgrammaticE2EPage,
skipAnimations,
} from "../../tests/utils";
import { assertCaretPosition } from "../../tests/utils";
import { DEBOUNCE } from "../../utils/resources";
import { CSS } from "./resources";
import { Combobox } from "./combobox";

const selectionModes = ["single", "single-persist", "ancestors", "multiple"];

Expand Down Expand Up @@ -2157,6 +2163,32 @@ describe("calcite-combobox", () => {
expect(eventSpy).toHaveReceivedEventTimes(1);
});

it("value and items are updated on change emit", async () => {
const page = await newE2EPage();
await page.setContent(html`
<calcite-combobox allow-custom-values>
<!-- intentionally empty to cover base case -->
</calcite-combobox>
`);
const propValueAsserter = await createEventTimePropValuesAsserter<Combobox>(
page,
{
selector: "calcite-combobox",
eventName: "calciteComboboxChange",
props: ["value", "selectedItems"],
},
async (propValues) => {
expect(propValues.value).toBe("K");
expect(propValues.selectedItems).toHaveLength(1);
},
);
const combobox = await page.find("calcite-combobox");
await combobox.callMethod("setFocus");
await combobox.press("K");
await combobox.press("Enter");
await expect(propValueAsserter()).resolves.toBe(undefined);
});

it("should allow enter unknown tag when tabbing away", async () => {
const page = await newE2EPage();
await page.setContent(html`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,14 @@ import { onToggleOpenCloseComponent, OpenCloseComponent } from "../../utils/open
import { DEBOUNCE } from "../../utils/resources";
import { Scale, SelectionMode, Status } from "../interfaces";
import { CSS as XButtonCSS, XButton } from "../functional/XButton";
import { getIconScale } from "../../utils/component";
import { getIconScale, isHidden } from "../../utils/component";
import { Validation } from "../functional/Validation";
import { IconNameOrString } from "../icon/interfaces";
import { useT9n } from "../../controllers/useT9n";
import type { Chip } from "../chip/chip";
import type { ComboboxItemGroup as HTMLCalciteComboboxItemGroupElement } from "../combobox-item-group/combobox-item-group";
import type { ComboboxItem as HTMLCalciteComboboxItemElement } from "../combobox-item/combobox-item";
import type { Label } from "../label/label";
import { isHidden } from "../../utils/component";
import T9nStrings from "./assets/t9n/messages.en.json";
import { ComboboxChildElement, GroupData, ItemData, SelectionDisplay } from "./interfaces";
import { ComboboxItemGroupSelector, ComboboxItemSelector, CSS, IDS } from "./resources";
Expand Down Expand Up @@ -1317,8 +1316,8 @@ export class Combobox
);
item.value = value;
item.heading = value;
item.selected = true;
this.el.prepend(item);
this.updateItems();
this.toggleSelection(item, true);
this.open = true;
if (focus) {
Expand Down
84 changes: 82 additions & 2 deletions packages/calcite-components/src/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-strict-ignore
import { BoundingBox, ElementHandle } from "puppeteer";
import { LuminaJsx } from "@arcgis/lumina";
import { newE2EPage, E2EPage, E2EElement } from "@arcgis/lumina-compiler/puppeteerTesting";
import { LitElement, LuminaJsx, ToElement } from "@arcgis/lumina";
import { E2EElement, E2EPage, newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting";
import { expect } from "vitest";
import { ComponentTag } from "./commonTests/interfaces";

Expand Down Expand Up @@ -521,3 +521,83 @@ export async function assertCaretPosition({
export async function toElementHandle(element: E2EElement): Promise<ElementHandle> {
return element.handle;
}

/**
* This util helps assert on a component's state at the time of an event firing.
*
* This is needed due to how Puppeteer works asynchronously, so the state at event emit-time might not be the same as the state at the time of the assertion.
*
* Note: values returned can only be serializable values.
*
* @example
*
* it("props are updated on change emit", async () => {
* const page = await newE2EPage();
* await page.setContent(html`
* <calcite-combobox>
* <!-- ... -->
* </calcite-combobox>
* `);
* const propValueAsserter = await createEventTimePropValuesAsserter<Combobox>(
* page,
* {
* selector: "calcite-combobox",
* eventName: "calciteComboboxChange",
* props: ["value", "selectedItems"],
* },
* async (propValues) => {
* expect(propValues.value).toBe("K");
* expect(propValues.selectedItems).toHaveLength(1);
* },
* );
* const combobox = await page.find("calcite-combobox");
* await combobox.callMethod("setFocus");
* await combobox.press("K");
* await combobox.press("Enter");
*
* await expect(propValueAsserter()).resolves.toBe(undefined);
* });
*
* @param page
* @param propValuesTarget
* @param propValuesTarget.selector
* @param propValuesTarget.eventName
* @param propValuesTarget.props
* @param onEvent
*/
export async function createEventTimePropValuesAsserter<
Component extends LitElement,
El extends ToElement<Component> = ToElement<Component>,
Keys extends Extract<keyof El, string> = Extract<keyof El, string>,
Events extends string = Keys extends `calcite${string}` ? Keys : never,
PropValues extends Record<Keys, El[Keys]> = Record<Keys, El[Keys]>,
>(
page: E2EPage,
propValuesTarget: {
selector: ComponentTag;
eventName: Events;
props: Keys[];
},
onEvent: (propValues: PropValues) => Promise<void>,
): Promise<() => Promise<void>> {
// we set this up early to we capture state as soon as the event fires
const callbackAfterEvent = page.$eval(
propValuesTarget.selector,
(element: El, eventName, props) => {
return new Promise<PropValues>((resolve) => {
element.addEventListener(
eventName,
() => {
const propValues = Object.fromEntries(props.map((prop) => [prop, element[prop]]));
resolve(propValues as PropValues);
},
{ once: true },
);
});
},
propValuesTarget.eventName,
propValuesTarget.props,
);

return () => callbackAfterEvent.then(onEvent);
}
Loading