import { click, HTML, createInteractor, Interactor, innerText } from "@interactors/html";
import { createFormFieldFilters } from "./form-field-filters";
import { isDefined, isHTMLElement, delay, dispatchMouseDown, getInputLabel, isDisabled } from "./helpers";
export const SelectOption = HTML.extend("MUISelectOption")
.selector('li[role="option"]')
.filters({
selected: (element) => element.getAttribute("aria-selected") == "true",
disabled: {
apply: isDisabled,
default: false,
},
})
.actions({
choose: ({ perform }) => perform((element) => click(element)),
});
const SelectOptionsList = createInteractor("MUISelectOptionsList")
.selector("ul")
.locator((element) => element.getAttribute("aria-labelledby") ?? "");
async function closeSelectOptionsList(labelId: string) {
await SelectOptionsList(labelId).perform((element) => {
let popover = element.parentElement?.parentElement;
if (popover?.getAttribute("role") == "presentation" && popover.classList.toString().includes("MuiPopover-root")) {
let popoverTrap = popover.firstElementChild;
if (isHTMLElement(popoverTrap)) popoverTrap.click();
}
});
}
function getValueText(element: HTMLInputElement) {
let select = element.previousElementSibling;
return isHTMLElement(select) ? innerText(select) : "";
}
function getChipLabels(element: HTMLInputElement) {
return Array.from(
element.previousElementSibling?.querySelectorAll('[class*="MuiChip-root"] > [class*="MuiChip-label"]') ?? []
)
.map((chip) => (isHTMLElement(chip) ? innerText(chip) : null))
.filter(isDefined);
}
async function clearSelection(labelId: string) {
let selected: string[] = [];
await SelectOptionsList(labelId).perform(
(element) =>
(selected = Array.from(element.querySelectorAll("li"))
.filter((option) => option.getAttribute("aria-selected") == "true")
.map((option) => innerText(option)))
);
for (let option of selected) {
await SelectOptionsList(labelId).find(SelectOption(option)).choose();
}
}
async function openSelectOptionsList>(interactor: Interactor) {
let labelId = "";
await interactor.perform((element) => (labelId = getInputLabel(element)?.id ?? ""));
await interactor.perform((element) => {
let select = element.previousElementSibling;
if (isHTMLElement(select)) dispatchMouseDown(select);
});
await delay(100);
return labelId;
}
const BaseSelect = createInteractor("MUIBaseSelect")
.selector('[class*="MuiSelect-root"] + input[class*="MuiSelect-nativeInput"]')
.locator((element) => innerText(getInputLabel(element)))
.filters({
...createFormFieldFilters(),
id: (element) => element.previousElementSibling?.id,
className: (element) => element.parentElement?.className ?? "",
classList: (element) => Array.from(element.parentElement?.classList ?? []),
valid: (element) => !element.previousElementSibling?.classList.toString().includes("Mui-error"),
disabled: {
apply: (element) => isDisabled(element.previousElementSibling),
default: false,
},
})
.actions({
open: ({ perform }) =>
perform((element) => {
let selectElement = element.previousElementSibling;
if (isHTMLElement(selectElement)) dispatchMouseDown(selectElement);
}),
});
const SelectInteractor = BaseSelect.extend("MUISelect")
.filters({ value: getValueText })
.actions({
choose: async (interactor, value: string) => {
if ((await interactor.value()) == value) return;
let labelId = await openSelectOptionsList(interactor);
await SelectOptionsList(labelId).find(SelectOption(value)).choose();
},
});
const MultiSelectInteractor = BaseSelect.extend("MUIMultiSelect")
.filters({ values: getChipLabels })
.actions({
choose: async (interactor, value: string) => {
let selected = await interactor.values();
if (selected.length == 1 && selected[0] == value) return;
let labelId = await openSelectOptionsList(interactor);
await clearSelection(labelId);
await SelectOptionsList(labelId).find(SelectOption(value)).choose();
await closeSelectOptionsList(labelId);
},
select: async (interactor, value: string) => {
let selected = await interactor.values();
if (selected.includes(value)) return;
let labelId = await openSelectOptionsList(interactor);
await SelectOptionsList(labelId).find(SelectOption(value)).choose();
await closeSelectOptionsList(labelId);
},
deselect: async (interactor, value: string) => {
let selected = await interactor.values();
if (!selected.includes(value)) return;
let labelId = await openSelectOptionsList(interactor);
await SelectOptionsList(labelId).find(SelectOption(value)).choose();
await closeSelectOptionsList(labelId);
},
});
/**
* Call this {@link InteractorConstructor} to initialize a select {@link Interactor}.
* The select interactor can be used to interact with selects and to assert on their state.
*
* For interacting with multiple selects, see {@link MultiSelect}.
*
* The select is located by the text of its label.
*
* ### Example
*
* ``` typescript
* await Select('Language').select('English');
* await Select('Language').has({ value: 'English' });
* ```
*
* ### Filters
*
* - `id`: *string* – Filter by id
* - `valid`: *boolean* – Filter by whether the select is valid.
* - `required`: *boolean* – Filter by whether the select is required.
* - `description`: *string* – Filter by description.
* - `value`: *string* – Filter by the text of the selected option.
* - `disabled`: *boolean* – Filter by whether the select is disabled. Defaults to `false`.
*
* ### Actions
*
* - `click()`: *{@link Interaction}* – Click on the select
* - `focus()`: *{@link Interaction}* – Move focus to the select
* - `blur()`: *{@link Interaction}* – Move focus away from the select
* - `choose(text: string)`: *{@link Interaction}* – Choose the option with the given text from the select.
*
* @category Interactor
*/
export const Select = SelectInteractor;
/**
* Call this {@link InteractorConstructor} to initialize a select {@link Interactor}.
* The select interactor can be used to interact with selects with the `multiple` attribute
* and to assert on their state.
*
* See {@link Select} for an interactor for single select.
*
* The multi select is located by the text of its label.
*
* ### Example
*
* ``` typescript
* await MultiSelect('Language').select('English');
* await MultiSelect('Language').select('German');
* await MultiSelect('Language').deselect('Swedish');
* await MultiSelect('Language').has({ values: ['English', 'German'] });
* ```
*
* ### Filters
*
* - `id`: *string* – Filter by id
* - `valid`: *boolean* – Filter by whether the select is valid.
* - `required`: *boolean* – Filter by whether the select is required.
* - `description`: *string* – Filter by description.
* - `value`: *string* – Filter by the text of the selected option.
* - `disabled`: *boolean* – Filter by whether the select is disabled. Defaults to `false`.
*
* ### Actions
*
* - `click()`: *{@link Interaction}* – Click on the multi select
* - `focus()`: *{@link Interaction}* – Move focus to the multi select
* - `blur()`: *{@link Interaction}* – Move focus away from the multi select
* - `choose(text: string)`: *{@link Interaction}* – Choose the option with the given text from the multi select. Will deselect all other selected options.
* - `select(text: string)`: *{@link Interaction}* – Add the option with the given text to the selection.
* - `deselect(text: string)`: *{@link Interaction}* – Remove the option with the given text from the selection.
*
* @category Interactor
*/
export const MultiSelect = MultiSelectInteractor;