import { HTML, innerText } from "@interactors/html";
import { isDefined } from "./helpers";
function isDisabled(element: Element) {
return element.className.includes("Mui-disabled");
}
export type SliderOrientation = "horizontal" | "vertical";
function getThumbs(element: HTMLElement) {
return Array.from(element.querySelectorAll('[class*="MuiSlider-thumb"]'));
}
function getThumb(element: HTMLElement) {
return getThumbs(element)[0] ?? null;
}
function parseValue(stringValue?: string | null) {
let value = stringValue ? parseInt(stringValue) : NaN;
return Number.isNaN(value) ? undefined : value;
}
export const Thumb = HTML.extend("MUISliderThumb")
.locator((element) => element.getAttribute("aria-label") ?? element.getAttribute("aria-valuetext") ?? "")
.selector('[role="slider"][class*="MuiSlider-thumb"]')
.filters({
// NOTE: Do we need other filters like, min/max value, orientation? They are the same for each thumb in Slider
index: (element) => parseValue(element.getAttribute("data-index")),
value: (element) => parseValue(element.getAttribute("aria-valuenow")),
textValue: (element) => element.getAttribute("aria-valuetext"),
min: (element) =>
parseValue(element.getAttribute("aria-valuenow")) == parseValue(element.getAttribute("aria-valuemin")),
max: (element) =>
parseValue(element.getAttribute("aria-valuenow")) == parseValue(element.getAttribute("aria-valuemax")),
disabled: {
// NOTE: The Slider doesn't have `aria-disabled` attribute https://github.com/mui-org/material-ui/issues/27031
apply: isDisabled,
default: false,
},
})
.actions({
decrease: ({ perform }, steps = 1) =>
perform((element) => {
if (isDisabled(element)) return;
element.focus();
Array.from({ length: steps }).forEach(() =>
document.activeElement?.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true, cancelable: true })
)
);
}),
increase: ({ perform }, steps = 1) =>
perform((element) => {
if (isDisabled(element)) return;
element.focus();
Array.from({ length: steps }).forEach(() =>
document.activeElement?.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true, cancelable: true })
)
);
}),
setMin: ({ perform }) =>
perform((element) => {
if (isDisabled(element)) return;
element.dispatchEvent(new KeyboardEvent("keydown", { key: "Home", bubbles: true, cancelable: true }));
}),
setMax: ({ perform }) =>
perform((element) => {
if (isDisabled(element)) return;
element.dispatchEvent(new KeyboardEvent("keydown", { key: "End", bubbles: true, cancelable: true }));
}),
setValue: ({ description, perform }, value: number) =>
perform((element) => {
if (isDisabled(element)) return;
let minValue = parseValue(element.getAttribute("aria-valuemin"));
let maxValue = parseValue(element.getAttribute("aria-valuemax"));
let orientation = element.getAttribute("aria-orientation") as SliderOrientation | null;
if (!minValue || !maxValue || !orientation) throw new Error(`Can't set value ${value} for ${description}`);
let valuePercent = Math.min(1, Math.max(0, (value - minValue) / (maxValue - minValue)));
let rail = element.parentElement?.querySelector('[class*="MuiSlider-rail"]');
let { left = 0, top = 0 } = rail?.getBoundingClientRect() ?? {};
let thumbRect = element.getBoundingClientRect();
rail?.dispatchEvent(
new MouseEvent("mousedown", {
clientX: Math.round(thumbRect.left + thumbRect.width / 2),
clientY: Math.round(thumbRect.top + thumbRect.height / 2),
bubbles: true,
cancelable: true,
})
);
rail?.dispatchEvent(
new MouseEvent("mousemove", {
...(orientation == "horizontal"
? { clientX: Math.round(left + valuePercent * rail.clientWidth) }
: { clientY: Math.round(top + (1 - valuePercent) * rail.clientHeight) }),
bubbles: true,
cancelable: true,
})
);
rail?.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
}),
});
const SliderInteractor = HTML.extend("MUISlider")
.selector('[class*="MuiSlider-root"]')
.locator((element) => {
let thumb = getThumb(element);
let labelId = thumb?.getAttribute("aria-labelledby");
let label = labelId ? document.getElementById(labelId) : null;
return label ? innerText(label) : thumb.getAttribute("aria-label") ?? "";
})
.filters({
orientation: (element) => getThumb(element)?.getAttribute("aria-orientation") as SliderOrientation | null,
horizontal: (element) => getThumb(element)?.getAttribute("aria-orientation") == "horizontal",
vertical: (element) => getThumb(element)?.getAttribute("aria-orientation") == "vertical",
minValue: (element) => parseValue(getThumb(element)?.getAttribute("aria-valuemin")),
maxValue: (element) => parseValue(getThumb(element)?.getAttribute("aria-valuemax")),
value: (element) => {
let thumbs = getThumbs(element);
if (thumbs.length == 0) return;
if (thumbs.length == 1) return parseValue(thumbs[0].getAttribute("aria-valuenow"));
return thumbs.map((thumb) => parseValue(thumb.getAttribute("aria-valuenow"))).filter(isDefined);
},
textValue: (element) => {
let thumbs = getThumbs(element);
if (thumbs.length == 0) return;
if (thumbs.length == 1) return thumbs[0].getAttribute("aria-valuetext");
return thumbs.map((thumb) => thumb.getAttribute("aria-valuetext")).filter(isDefined);
},
min: (element) =>
getThumbs(element).every(
(thumb) => parseValue(thumb.getAttribute("aria-valuenow")) == parseValue(thumb.getAttribute("aria-valuemin"))
),
max: (element) =>
getThumbs(element).every(
(thumb) => parseValue(thumb.getAttribute("aria-valuenow")) == parseValue(thumb.getAttribute("aria-valuemax"))
),
disabled: {
// NOTE: The Slider doesn't have `aria-disabled` attribute https://github.com/mui-org/material-ui/issues/27031
apply: isDisabled,
default: false,
},
})
.actions({
decrease: ({ find }, steps = 1) => find(Thumb()).decrease(steps),
increase: ({ find }, steps = 1) => find(Thumb()).increase(steps),
setMin: ({ find }) => find(Thumb()).setMin(),
setMax: ({ find }) => find(Thumb()).setMax(),
setValue: ({ find }, value: number) => find(Thumb()).setValue(value),
});
/**
* Call this {@link InteractorConstructor} to initialize a slider {@link Interactor}.
* The slider interactor can be used to interact with sliders on the page and
* to assert on their state.
*
* The slider is located by the label or the `aria-label` attribute.
*
* ### Example
*
* ``` typescript
* await Slider('Submit').click();
* await Slider('Submit').is({ disabled: true });
* await Slider({ id: 'submit-slider', disabled: true }).exists();
* ```
*
* ### Filters
*
* - `id`: *string* – Filter by id
* - `visible`: *boolean* – Filter by visibility. Defaults to `true`. See {@link isVisible}.
* - `disabled`: *boolean* – Filter by whether the slider is disabled. Defaults to `false`.
* - `orientation`: *horizontal | vertical* - Filter by orientation
* - `horizontal`: *boolean* - Filter by whether the slider has horizontal orientation
* - `vertical`: *boolean* - Filter by whether the slider has vertical orientation
* - `minValue`: *number* - Filter by the least possible value
* - `maxValue`: *number* - Filter by the greatest possible value
* - `value`: *number* - Filter by the current value
* - `textValue`: *string* - Filter by the text representation of current value
* - `min`: *boolean* - Filter by whether the slider value is equal `minValue`
* - `max`: *boolean* - Filter by whether the slider value is equal `maxValue`
*
* ### Actions
*
* - `focus()`: *{@link Interaction}* – Move focus to the slider
* - `blur()`: *{@link Interaction}* – Move focus away from the slider
* - `decrease(steps: number = 1)`: *{@link Interaction}* – Decrease slider value by specific steps
* - `increase(steps: number = 1)`: *{@link Interaction}* – Increase slider value by specific steps
* - `setMin()`: *{@link Interaction}* - Set value to the least possible
* - `setMax()`: *{@link Interaction}* – Set value to the greatest possible
* - `setValue(value: number)`: *{@link Interaction}* – Set current value
*
* @category Interactor
*/
export const Slider = SliderInteractor;