Skip to content

Commit

Permalink
fix(combobox): update internal state after custom value is added (#11405
Browse files Browse the repository at this point in the history
)

**Related Issue:** #10731, #11382

## Summary

This updates the combobox internal state after an custom item is added.

**Note**: this also adds `createEventTimePropValuesAsserter` to help
assert on event-emit time component state.
  • Loading branch information
jcfranco authored Jan 29, 2025
1 parent 8c99132 commit b6b6525
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 6 deletions.
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);
}

0 comments on commit b6b6525

Please sign in to comment.