@octopusdeploy/design-system-components
Version:
The design systems component library.
753 lines (752 loc) • 38.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const jsx_runtime_1 = require("react/jsx-runtime");
const testing_library_1 = require("@octopusdeploy/testing-library");
const react_1 = require("@testing-library/react");
const react_2 = require("react");
const vitest_1 = require("vitest");
const Select_1 = require("../Select");
const getSharedSelectInteractions_1 = require("./getSharedSelectInteractions");
const option1 = { label: "Option 1", value: "1" };
const option2 = { label: "Option 2", value: "2" };
const option3 = { label: "Option 3", value: "3" };
const items = [option1, option2, option3];
(0, vitest_1.describe)("<Select />", () => {
(0, vitest_1.test)("renders with correct label", () => {
const select = renderSelect({ label: "Custom Label" });
select.shouldHaveLabel("Custom Label");
select.shouldBeInDocument();
});
(0, vitest_1.test)("renders with placeholder text", () => {
const placeholder = "Select an option";
const select = renderSelect({ placeholder });
select.shouldHavePlaceholder(placeholder);
});
(0, vitest_1.test)("renders with selected value", () => {
const select = renderSelect({ items, value: option2.value });
select.shouldShowSelectedOption("Option 2");
});
(0, vitest_1.test)("renders with description", () => {
const description = "This is a helpful description";
const select = renderSelect({ description });
select.shouldHaveDescription(description);
});
(0, vitest_1.test)("renders with error state", () => {
const validationMessage = "This field is required";
const select = renderSelect({ validationMessage });
select.shouldHaveError(validationMessage);
select.shouldHaveAriaInvalid(true);
});
(0, vitest_1.test)("has autofocus when autoFocus is true", () => {
const select = renderSelect({ autoFocus: true });
select.shouldHaveFocus();
});
(0, vitest_1.test)("opens dropdown when clicked", async () => {
const select = renderSelect();
await select.click();
select.shouldShowDropdown();
});
(0, vitest_1.test)("displays all options when dropdown is open", async () => {
const select = renderSelect();
await select.click();
select.shouldShowAllOptions(items);
});
(0, vitest_1.test)("calls onChange when option is selected", async () => {
const select = renderSelect({ value: option1.value });
await select.click();
await select.selectOption("Option 2");
select.shouldHaveCalledOnChangeWith(option2.value);
});
(0, vitest_1.test)("closes dropdown when option is selected", async () => {
const select = renderSelect({ value: option1.value });
await select.click();
await select.selectOption("Option 2");
select.shouldNotShowDropdown();
});
(0, vitest_1.test)("shows no results message when there are no options", async () => {
const select = renderSelect({ items: [] });
await select.click();
select.shouldShowNoResultsMessage();
});
(0, vitest_1.test)("does not sort the options by default", async () => {
const unsortedOptions = [option3, option2, option1];
const select = renderSelect({ items: unsortedOptions });
await select.click();
select.shouldShowAllOptionsInOrder(unsortedOptions);
});
(0, vitest_1.test)("sorting the options can be enabled", async () => {
const unsortedOptions = [option3, option2, option1];
const sortedOptions = [option1, option2, option3];
const select = renderSelect({ items: unsortedOptions, sortItems: true });
await select.click();
select.shouldShowAllOptionsInOrder(sortedOptions);
});
(0, vitest_1.test)("does not trigger form submission when clicked inside a form", async () => {
const handleSubmit = vitest_1.vi.fn((e) => e.preventDefault());
const select = renderSelect({
withForm: true,
onSubmit: handleSubmit,
});
await select.click();
(0, vitest_1.expect)(handleSubmit).not.toHaveBeenCalled();
});
(0, vitest_1.describe)("keyboard navigation", () => {
(0, vitest_1.test)("opens dropdown with keyboard (Enter key)", async () => {
const select = renderSelect();
await select.tab();
await select.pressEnter();
select.shouldShowDropdown();
});
(0, vitest_1.test)("opens dropdown with keyboard (Space key)", async () => {
const select = renderSelect();
await select.tab();
await select.pressSpace();
select.shouldShowDropdown();
});
(0, vitest_1.test)("opens dropdown with keyboard (ArrowDown key)", async () => {
const select = renderSelect();
await select.tab();
await select.pressArrowDown();
select.shouldShowDropdown();
});
(0, vitest_1.describe)("within listbox", () => {
(0, vitest_1.test)("navigates down with ArrowDown key", async () => {
const select = renderSelect({ value: option1.value });
await select.click();
await select.pressArrowDown();
select.shouldHaveOptionFocused("Option 2");
});
(0, vitest_1.test)("navigates up with ArrowUp key", async () => {
const select = renderSelect({ value: option2.value });
await select.click();
await select.pressArrowUp();
select.shouldHaveOptionFocused("Option 1");
});
(0, vitest_1.test)("jumps to first option with Home key", async () => {
const select = renderSelect({ value: option3.value });
await select.click();
await select.pressHome();
select.shouldHaveOptionFocused("Option 1");
});
(0, vitest_1.test)("jumps to last option with End key", async () => {
const select = renderSelect({ value: option1.value });
await select.click();
await select.pressEnd();
select.shouldHaveOptionFocused("Option 3");
});
(0, vitest_1.test)("selects focused option with Enter key", async () => {
const select = renderSelect({ value: option1.value });
await select.click();
await select.pressArrowDown();
await select.pressEnter();
select.shouldHaveCalledOnChangeWith(option2.value);
});
(0, vitest_1.test)("selects focused option with Space key", async () => {
const select = renderSelect({ value: option1.value });
await select.click();
await select.pressArrowDown();
await select.pressSpace();
select.shouldHaveCalledOnChangeWith(option2.value);
});
(0, vitest_1.test)("closes dropdown with Escape key", async () => {
const select = renderSelect();
await select.click();
select.shouldShowDropdown();
await select.pressEscape();
select.shouldNotShowDropdown();
select.shouldHaveFocus();
});
(0, vitest_1.test)("does not call onChange when closing with Escape key", async () => {
const select = renderSelect({ value: option1.value });
await select.click();
await select.pressArrowDown();
await select.pressEscape();
select.shouldNotHaveCalledOnChange();
});
});
});
(0, vitest_1.describe)("disabled and readonly properties", () => {
(0, vitest_1.test)("renders as disabled when disabled prop is true", () => {
const select = renderSelect({ disabled: true });
select.shouldBeDisabled();
});
(0, vitest_1.test)("disabled select cannot be opened", async () => {
const select = renderSelect({ disabled: true });
await select.click();
select.shouldNotShowDropdown();
});
(0, vitest_1.test)("renders as readonly when readOnly prop is true", () => {
const select = renderSelect({ readOnly: true });
select.shouldBeReadOnly();
});
(0, vitest_1.test)("readonly select cannot be opened", async () => {
const select = renderSelect({ readOnly: true });
await select.click();
select.shouldNotShowDropdown();
});
});
(0, vitest_1.describe)("default, required and optional marker properties", () => {
(0, vitest_1.test)("renders with required marker when hasRequiredMarker is true", () => {
const select = renderSelect({ hasRequiredMarker: true });
select.shouldHaveRequiredMarker();
});
(0, vitest_1.test)("renders with optional marker when hasOptionalMarker is true", () => {
const select = renderSelect({ hasOptionalMarker: true });
select.shouldHaveOptionalMarker();
});
(0, vitest_1.test)("renders with default marker when hasDefaultMarker is true", () => {
const select = renderSelect({ hasDefaultMarker: true });
select.shouldHaveDefaultMarker();
});
});
(0, vitest_1.describe)("accessibility attributes", () => {
(0, vitest_1.test)("has correct accessibility attributes when error is present", () => {
const validationMessage = "Invalid selection";
const select = renderSelect({ validationMessage });
select.shouldHaveAriaInvalid(true);
select.shouldHaveAriaDescribedByError();
});
(0, vitest_1.test)("has correct accessibility attributes when description is present", () => {
const description = "Help text";
const select = renderSelect({ description });
select.shouldHaveAriaDescribedByDescription();
});
(0, vitest_1.test)("has correct accessibility attributes when both description and error are present", () => {
const description = "Help text";
const validationMessage = "Error message";
const select = renderSelect({ description, validationMessage });
select.shouldHaveAriaDescribedByBoth();
select.shouldHaveAriaInvalid(true);
});
(0, vitest_1.test)("has aria-required when hasRequiredMarker is true", () => {
const select = renderSelect({ hasRequiredMarker: true });
select.shouldHaveAriaRequired(true);
});
(0, vitest_1.test)("has aria-activedescendant pointing to focused option when navigating with keyboard", async () => {
const select = renderSelect({ items, value: option1.value });
await select.click();
select.shouldHaveAriaActiveDescendantForOption("Option 1");
await select.pressArrowDown();
select.shouldHaveAriaActiveDescendantForOption("Option 2");
});
(0, vitest_1.test)("does not have aria-activedescendant when dropdown is closed", () => {
const select = renderSelect({ items, value: option1.value });
select.shouldNotHaveAriaActiveDescendant();
});
});
(0, vitest_1.describe)("required validation", () => {
(0, vitest_1.test)("does not show a required error before any change is made, even when empty", () => {
const select = renderSelect({ required: true, value: undefined });
select.shouldNotHaveError("This field is required.");
});
(0, vitest_1.test)("shows a required error once the selected value is cleared", async () => {
const select = renderSelect({ required: true, value: option1.value, allowClear: true });
await select.clickClearButton();
select.shouldHaveError("This field is required.");
});
(0, vitest_1.test)("does not show a required error when a different value is selected", async () => {
const select = renderSelect({ required: true, value: option1.value });
await select.click();
await select.selectOption("Option 2");
select.shouldNotHaveError("This field is required.");
});
});
(0, vitest_1.describe)("clear button", () => {
(0, vitest_1.test)("does not render clear button by default", () => {
const select = renderSelect({ value: option1.value });
select.shouldNotShowClearButton();
});
(0, vitest_1.test)("does not render clear button when no value is selected", () => {
const select = renderSelect({ value: undefined, allowClear: true });
select.shouldNotShowClearButton();
});
(0, vitest_1.test)("renders clear button when value is selected", () => {
const select = renderSelect({ value: option1.value, allowClear: true });
select.shouldShowClearButton();
});
(0, vitest_1.test)("does not render clear button when disabled with a value selected", () => {
const select = renderSelect({ value: option1.value, disabled: true, allowClear: true });
select.shouldNotShowClearButton();
});
(0, vitest_1.test)("does not render clear button when readonly with a value selected", () => {
const select = renderSelect({ value: option1.value, readOnly: true, allowClear: true });
select.shouldNotShowClearButton();
});
(0, vitest_1.test)("calls onChange with undefined when clear button is clicked", async () => {
const select = renderSelect({ value: option2.value, allowClear: true });
await select.clickClearButton();
select.shouldHaveCalledOnChangeWith(undefined);
});
});
(0, vitest_1.describe)("filtering", () => {
(0, vitest_1.test)("shows search input when allowFilter is true", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
select.shouldShowSearchInput();
});
(0, vitest_1.test)("filters options based on search text", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
await select.typeInSearchInput("1");
select.shouldShowOnlyOption("Option 1");
});
(0, vitest_1.test)("filters options case-insensitively", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
await select.typeInSearchInput("OPTION 2");
select.shouldShowOnlyOption("Option 2");
});
(0, vitest_1.test)("shows multiple matching options when filter matches", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
await select.typeInSearchInput("Option");
select.shouldShowAllOptions(items);
});
(0, vitest_1.test)("shows no results message when filter matches nothing", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
await select.typeInSearchInput("xyz");
select.shouldShowNoResultsMessage();
});
(0, vitest_1.test)("clears filter when dropdown closes", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
await select.typeInSearchInput("1");
select.shouldShowOnlyOption("Option 1");
await select.pressEscape();
await select.click();
select.shouldShowAllOptions(items);
});
(0, vitest_1.test)("navigates filtered options with keyboard", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
await select.pressArrowDown();
select.shouldHaveOptionActive("Option 2");
});
(0, vitest_1.test)("selects filtered option with Enter key from search input", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
await select.typeInSearchInput("Option 2");
await select.pressEnter();
select.shouldHaveCalledOnChangeWith(option2.value);
});
(0, vitest_1.test)("jumps to first option with Home key in search input", async () => {
const select = renderSelect({ allowFilter: true, value: option3.value });
await select.click();
await select.pressHome();
select.shouldHaveOptionActive("Option 1");
});
(0, vitest_1.test)("jumps to last option with End key in search input", async () => {
const select = renderSelect({ allowFilter: true });
await select.click();
await select.pressEnd();
select.shouldHaveOptionActive("Option 3");
});
(0, vitest_1.test)("search input has correct aria attributes", async () => {
const select = renderSelect({ label: "Test Select", allowFilter: true });
await select.click();
select.shouldHaveSearchInputWithAriaLabel("Filter Test Select");
});
});
(0, vitest_1.describe)("AsyncList items", () => {
(0, vitest_1.test)("displays selected option from getItemById when not in loadedItems", () => {
const asyncList = createMockAsyncList({
loadedItems: [],
getItemById: (id) => (id === "1" ? option1 : undefined),
});
const select = renderSelect({ items: asyncList, value: option1.value });
select.shouldShowSelectedOption("Option 1");
});
(0, vitest_1.test)("displays loaded items in dropdown", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
});
const select = renderSelect({ items: asyncList });
await select.click();
select.shouldShowAllOptions([option1, option2, option3]);
});
(0, vitest_1.test)("shows a refresh button when the async list exposes refresh", () => {
const asyncList = createMockAsyncList({ loadedItems: [option1], refresh: vitest_1.vi.fn() });
const select = renderSelect({ items: asyncList });
select.shouldShowRefreshButton();
});
(0, vitest_1.test)("does not show a refresh button when the async list has no refresh", () => {
const asyncList = createMockAsyncList({ loadedItems: [option1] });
const select = renderSelect({ items: asyncList });
select.shouldNotShowRefreshButton();
});
(0, vitest_1.test)("calls refresh when the refresh button is clicked", async () => {
const refresh = vitest_1.vi.fn();
const asyncList = createMockAsyncList({ loadedItems: [option1], refresh });
const select = renderSelect({ items: asyncList });
await select.clickRefreshButton();
(0, vitest_1.expect)(refresh).toHaveBeenCalledTimes(1);
});
(0, vitest_1.test)("keeps the refresh button focusable while a refresh is in flight", async () => {
const deferredRefresh = (0, testing_library_1.createControlledPromise)();
const asyncList = createMockAsyncList({ loadedItems: [option1], refresh: () => deferredRefresh.promise });
const select = renderSelect({ items: asyncList });
await select.clickRefreshButton();
// While the reload is in flight the trigger must stay focusable rather than being disabled,
// so keyboard / screen-reader users don't lose their place mid-action.
select.refreshButtonShouldBeEnabled();
select.refreshButtonShouldHaveFocus();
await deferredRefresh.resolve();
});
(0, vitest_1.test)("ignores repeat refresh clicks while one is already in flight", async () => {
const deferredRefresh = (0, testing_library_1.createControlledPromise)();
const refresh = vitest_1.vi.fn(() => deferredRefresh.promise);
const asyncList = createMockAsyncList({ loadedItems: [option1], refresh });
const select = renderSelect({ items: asyncList });
await select.clickRefreshButton();
await select.clickRefreshButton();
(0, vitest_1.expect)(refresh).toHaveBeenCalledTimes(1);
await deferredRefresh.resolve();
});
(0, vitest_1.test)("displays loading indicator when isLoading is true", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1],
isLoading: true,
});
const select = renderSelect({ items: asyncList });
await select.click();
select.shouldShowLoadingIndicator();
});
(0, vitest_1.test)("does not display loading indicator when isLoading is false", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1],
isLoading: false,
});
const select = renderSelect({ items: asyncList });
await select.click();
select.shouldNotShowLoadingIndicator();
});
(0, vitest_1.test)("calls loadMore when scrolling in dropdown with more items available", async () => {
const loadMore = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2],
loadMore,
});
const select = renderSelect({ items: asyncList });
await select.click();
await select.triggerScrollForLoadMore();
(0, vitest_1.expect)(loadMore).toHaveBeenCalled();
});
(0, vitest_1.test)("does not call loadMore when isLoading is true", async () => {
const loadMore = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2],
isLoading: true,
loadMore,
});
const select = renderSelect({ items: asyncList });
await select.click();
await select.triggerScrollForLoadMore();
(0, vitest_1.expect)(loadMore).not.toHaveBeenCalled();
});
(0, vitest_1.test)("only triggers loadMore once when multiple scroll events occur near the end of the list", async () => {
const loadMore = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2],
isLoading: false,
error: null,
loadMore,
});
const select = renderSelect({ items: asyncList });
await select.click();
// Trigger multiple scroll events rapidly
await select.triggerScrollForLoadMore();
await select.triggerScrollForLoadMore();
await select.triggerScrollForLoadMore();
// Should only be called once despite multiple scroll events
(0, vitest_1.expect)(loadMore).toHaveBeenCalledTimes(1);
});
(0, vitest_1.test)("calls loadMore when listbox is initially rendered and already near end of list", async () => {
const restoreScrollDimensions = mockListboxScrollDimensions({ scrollHeight: 150, clientHeight: 100 });
const loadMore = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1],
isLoading: false,
error: null,
loadMore,
});
const select = renderSelect({ items: asyncList });
await select.click();
(0, vitest_1.expect)(loadMore).toHaveBeenCalledTimes(1);
restoreScrollDimensions();
});
(0, vitest_1.test)("does not call loadMore when listbox is near end of list and is already loading", async () => {
const restoreScrollDimensions = mockListboxScrollDimensions({ scrollHeight: 150, clientHeight: 100 });
const loadMore = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
isLoading: true,
error: null,
loadMore,
});
const select = renderSelect({ items: asyncList });
await select.click();
(0, vitest_1.expect)(loadMore).not.toHaveBeenCalled();
restoreScrollDimensions();
});
(0, vitest_1.test)("does not call loadMore when listbox is initially rendered and not near end of list", async () => {
const restoreScrollDimensions = mockListboxScrollDimensions({ scrollHeight: 1000, clientHeight: 100 });
const loadMore = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
isLoading: false,
error: null,
loadMore,
});
const select = renderSelect({ items: asyncList });
await select.click();
(0, vitest_1.expect)(loadMore).not.toHaveBeenCalled();
restoreScrollDimensions();
});
(0, vitest_1.test)("can select option from loaded items", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
});
const select = renderSelect({ items: asyncList, value: option1.value });
await select.click();
await select.selectOption("Option 2");
select.shouldHaveCalledOnChangeWith(option2.value);
});
(0, vitest_1.describe)("with filtering", () => {
(0, vitest_1.test)("shows search input when allowFilter is true with AsyncList", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
});
const select = renderSelect({ items: asyncList, allowFilter: true });
await select.click();
select.shouldShowSearchInput();
});
(0, vitest_1.test)("calls setFilterText when typing in search input with AsyncList", async () => {
const setFilterText = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
setFilterText,
});
const select = renderSelect({ items: asyncList, allowFilter: true });
await select.click();
await select.typeInSearchInput("test");
(0, vitest_1.expect)(setFilterText).toHaveBeenCalledWith("test");
});
(0, vitest_1.test)("displays all loadedItems without client-side filtering when using AsyncList", async () => {
const setFilterText = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1],
setFilterText,
});
const select = renderSelect({ items: asyncList, allowFilter: true });
await select.click();
await select.typeInSearchInput("2");
select.shouldShowOnlyOption("Option 1");
(0, vitest_1.expect)(setFilterText).toHaveBeenCalledWith("2");
});
(0, vitest_1.test)("shows no results view when filtering returns no results", async () => {
const setFilterText = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [],
setFilterText,
});
const select = renderSelect({ items: asyncList, allowFilter: true });
await select.click();
await select.typeInSearchInput("xyz");
select.shouldShowNoResultsMessage();
});
(0, vitest_1.test)("clears filter text and calls setFilterText when dropdown closes", async () => {
const setFilterText = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1],
setFilterText,
});
const select = renderSelect({ items: asyncList, allowFilter: true });
await select.click();
await select.typeInSearchInput("test");
setFilterText.mockClear();
await select.pressEscape();
(0, vitest_1.expect)(setFilterText).toHaveBeenCalledWith("");
});
});
(0, vitest_1.describe)("with error", () => {
(0, vitest_1.test)("shows loaded items when last load has an error", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error(),
});
const select = renderSelect({ items: asyncList });
await select.click();
select.shouldShowAllOptions([option1, option2, option3]);
});
(0, vitest_1.test)("shows error message when last load has an error", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error(),
});
const select = renderSelect({ items: asyncList });
await select.click();
select.shouldHaveError("Failed to load results");
});
(0, vitest_1.test)("shows a load error on the field, without needing to open the dropdown", () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error("Raw server error that should not be shown"),
});
const select = renderSelect({ items: asyncList, label: "Runbook" });
// A fixed message matching the dropdown footer, not the raw error, and visible without opening the dropdown.
select.shouldHaveError("Failed to load results");
});
(0, vitest_1.test)("does not mark the field invalid when a load fails but the value is valid", () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error("load failed"),
});
const select = renderSelect({ items: asyncList });
// A load failure isn't a validation error — the selected value is still valid.
select.shouldHaveAriaInvalid(false);
});
(0, vitest_1.test)("shows error message when there is an error and no items", async () => {
const asyncList = createMockAsyncList({
loadedItems: [],
error: new Error(),
});
const select = renderSelect({ items: asyncList });
await select.click();
select.shouldHaveError("Failed to load results");
});
(0, vitest_1.test)("shows retry button when last load has an error", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error(),
loadMore: vitest_1.vi.fn(),
});
const select = renderSelect({ items: asyncList });
await select.click();
select.shouldHaveRetryButton();
});
(0, vitest_1.test)("does not show a retry button when the list has no loadMore or refresh", async () => {
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error(),
});
const select = renderSelect({ items: asyncList });
await select.click();
select.shouldNotHaveRetryButton();
});
(0, vitest_1.test)("calls loadMore when retry is clicked", async () => {
const loadMore = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error(),
loadMore,
});
const select = renderSelect({ items: asyncList });
await select.click();
await select.clickRetryButton();
(0, vitest_1.expect)(loadMore).toHaveBeenCalled();
});
(0, vitest_1.test)("calls refresh when retry is clicked and the list has no loadMore", async () => {
const refresh = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error(),
refresh,
});
const select = renderSelect({ items: asyncList });
await select.click();
await select.clickRetryButton();
(0, vitest_1.expect)(refresh).toHaveBeenCalledTimes(1);
});
(0, vitest_1.test)("does not call loadMore on scroll when there is an error", async () => {
const loadMore = vitest_1.vi.fn();
const asyncList = createMockAsyncList({
loadedItems: [option1, option2, option3],
error: new Error(),
loadMore,
});
const select = renderSelect({ items: asyncList });
await select.click();
await select.triggerScrollForLoadMore();
(0, vitest_1.expect)(loadMore).not.toHaveBeenCalled();
});
});
});
});
function mockListboxScrollDimensions({ scrollHeight, clientHeight, scrollTop = 0 }) {
const scrollHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight");
const clientHeightDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "clientHeight");
const scrollTopDescriptor = Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollTop");
Object.defineProperty(HTMLElement.prototype, "scrollHeight", { configurable: true, get: () => scrollHeight });
Object.defineProperty(HTMLElement.prototype, "clientHeight", { configurable: true, get: () => clientHeight });
Object.defineProperty(HTMLElement.prototype, "scrollTop", { configurable: true, get: () => scrollTop });
return () => {
if (scrollHeightDescriptor) {
Object.defineProperty(HTMLElement.prototype, "scrollHeight", scrollHeightDescriptor);
}
else {
Reflect.deleteProperty(HTMLElement.prototype, "scrollHeight");
}
if (clientHeightDescriptor) {
Object.defineProperty(HTMLElement.prototype, "clientHeight", clientHeightDescriptor);
}
else {
Reflect.deleteProperty(HTMLElement.prototype, "clientHeight");
}
if (scrollTopDescriptor) {
Object.defineProperty(HTMLElement.prototype, "scrollTop", scrollTopDescriptor);
}
else {
Reflect.deleteProperty(HTMLElement.prototype, "scrollTop");
}
};
}
// Keeps the field genuinely controlled (mirroring how a real consumer feeds onChange back into value), so
// SelectBase's required-validation (which detects a change by comparing the value prop across renders) can
// actually observe a change instead of the value staying fixed at whatever the test passed in initially.
function ControlledSelect({ onChange, value: initialValue, ...props }) {
const [value, setValue] = (0, react_2.useState)(initialValue);
return ((0, jsx_runtime_1.jsx)(Select_1.Select, { ...props, value: value, onChange: (newValue) => {
setValue(newValue);
onChange(newValue);
} }));
}
function renderSelect(opts = {}) {
const { withForm, onSubmit, ...selectProps } = opts;
const onChange = vitest_1.vi.fn();
const props = {
label: "Test Label",
items: items,
getOption: (item) => item,
value: undefined,
onChange,
...selectProps,
};
const component = withForm ? ((0, jsx_runtime_1.jsx)("form", { onSubmit: onSubmit, children: (0, jsx_runtime_1.jsx)(ControlledSelect, { ...props }) })) : ((0, jsx_runtime_1.jsx)(ControlledSelect, { ...props }));
(0, react_1.render)(component);
return { ...getSelectInteractions(props.label, onChange), onSubmit };
}
function getSelectInteractions(label, onChange) {
const select = () => react_1.screen.getByRole("combobox", { name: new RegExp(label) });
return {
...(0, getSharedSelectInteractions_1.getSharedSelectInteractions)(label, onChange),
shouldShowSelectedOption: (optionLabel) => {
testing_library_1.domQueries.getText(optionLabel, { containerElement: select() }).assertIsInTheDocument();
},
shouldHaveCalledOnChangeWith: (value) => {
(0, vitest_1.expect)(onChange).toHaveBeenCalledWith(value);
},
};
}
function createMockAsyncList(options) {
return {
getItemId: options.getItemId ?? ((item) => item.value),
getItemById: options.getItemById ?? (() => undefined),
loadedItems: options.loadedItems ?? [],
isLoading: options.isLoading ?? false,
error: options.error ?? null,
loadMore: options.loadMore,
setFilterText: options.setFilterText ?? vitest_1.vi.fn(),
refresh: options.refresh,
};
}