import { click, HTML, createInteractor, including, Interaction, Interactor, not, innerText } from "@interactors/html"; import { delay, isHTMLElement } from "./helpers"; import { DatePickerUtils } from "./types"; function getHeaderElement(element: HTMLElement) { let header = element.parentElement?.querySelector('[class*="MuiPickersCalendarHeader-switchHeader"]'); return isHTMLElement(header) ? header : null; } function getTitleElement(element: HTMLElement) { let header = element.parentElement?.querySelector('[class*="MuiPickersCalendarHeader-transitionContainer"]'); return isHTMLElement(header) ? header : null; } function getWeekDaysElement(element: HTMLElement) { let daysHeader = element.parentElement?.querySelector('[class*="MuiPickersCalendarHeader-daysHeader"]'); return isHTMLElement(daysHeader) ? daysHeader : null; } function getSelectedElement(element: HTMLElement) { let dayButton = element.querySelector('[class*="MuiPickersDay-daySelected"]'); return isHTMLElement(dayButton) ? dayButton : null; } function calendarLocator(element: HTMLElement) { let header = innerText(getTitleElement(element)); let selectedDay = innerText(getSelectedElement(element)); return [selectedDay, header].filter(Boolean).join(" "); } export const getDay = (element: HTMLElement): number | undefined => { let text = innerText(getSelectedElement(element)); let day = text ? parseInt(text) : NaN; return Number.isNaN(day) ? undefined : day; }; export const getMonth = (element: HTMLElement): string | undefined => innerText(getTitleElement(element)).replace(/\s[0-9]{4}$/, ""); export const getYear = (element: HTMLElement): number | undefined => { let yearString = innerText(getTitleElement(element)).replace(/.*\s([0-9]{4})$/, "$1"); let year = yearString ? parseInt(yearString) : NaN; return Number.isNaN(year) ? undefined : year; }; async function goToMonth( interactor: ReturnType, targetMonth: string, directionStep: () => Interaction, currentYear?: number ) { let currentMonth = await interactor.month(); if (!currentMonth || !currentYear) throw new Error("Can't get current month and year"); if (currentMonth == targetMonth) return; let targetYear = currentYear; while (currentYear != targetYear || currentMonth != targetMonth) { await directionStep(); await delay(1000); let prevMonth: string | undefined = currentMonth; currentMonth = await interactor.month(); currentYear = await interactor.year(); if (currentYear != targetYear || currentMonth == prevMonth) throw new Error( `Can't set '${targetMonth}' month. It might happened because of 'minDate/maxDate' or 'disableFuture/disablePast' props` ); } } async function goToDay>(interactor: Interactor, day: number) { // NOTE: We can't find day if user has custom day render let dayInteractor = interactor.find( HTML.selector('[class*="MuiPickersCalendar-week"] > [role="presentation"]')(String(day)) ); try { await dayInteractor.has({ className: not(including("MuiPickersDay-dayDisabled")) }); } catch (_) { throw new Error(`Can't select ${day} day because it's disabled`); } return dayInteractor.click(); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function createCalendar(utils: DatePickerUtils) { return Calendar.filters({ date: (element) => { let header = innerText(getTitleElement(element)); let selectedDay = innerText(getSelectedElement(element)); let monthAndYear = header ? utils.parse(header, "MMMM yyyy") ?? new Date() : new Date(); let day = selectedDay ? parseInt(selectedDay) : NaN; let month = monthAndYear.getMonth() + 1; let year = monthAndYear.getFullYear(); // NOTE: Date.toISOString() depends on local timezone and returns different results of local machine and CI return Number.isNaN(day) ? new Date() : new Date( `${year}-${String(month).replace(/^(\d)$/, "0$1")}-${String(day).replace(/^(\d)$/, "0$1")}T00:00:00.000Z` ); }, }).actions({ setMonth: async (interactor, targetMonth: string) => { let currentMonth = await interactor.month(); if (!currentMonth) throw new Error("Can't get current month"); let currentMonthNumber = utils.getMonth(utils.parse(currentMonth, "MMMM")); let targetMonthNumber = utils.getMonth(utils.parse(targetMonth, "MMMM")); if (currentMonthNumber == targetMonthNumber) return; await goToMonth( interactor, targetMonth, currentMonthNumber < targetMonthNumber ? () => interactor.nextMonth() : () => interactor.prevMonth(), await interactor.year() ); }, }); } const CalendarInteractor = createInteractor("MUICalendar") .selector('[class*="MuiPickersCalendar-transitionContainer"]') .locator(calendarLocator) .filters({ year: getYear, month: getMonth, day: getDay, title: (element) => innerText(getTitleElement(element)), weekDay: (element) => { let rootDayElement = getSelectedElement(element)?.parentElement; let weekIndex = rootDayElement ? Array.from(rootDayElement?.parentElement?.children ?? []).indexOf(rootDayElement) : -1; let weekDayElement = weekIndex != -1 ? getWeekDaysElement(element)?.children.item(weekIndex) : null; return isHTMLElement(weekDayElement) ? innerText(weekDayElement) : undefined; }, }) .actions({ nextMonth: ({ perform }) => { return perform((element) => { // NOTE: We can't go upwards by using `Interactor().find(...)` let nextMonthElement = getHeaderElement(element)?.lastElementChild; if (isHTMLElement(nextMonthElement)) click(nextMonthElement); }); }, prevMonth: ({ perform }) => { return perform((element) => { // NOTE: We can't go upwards by using `Interactor().find(...)` let prevMonthElement = getHeaderElement(element)?.firstElementChild; if (isHTMLElement(prevMonthElement)) click(prevMonthElement); }); }, }) .actions({ setYear: async (interactor, targetYear: number) => { let currentMonth = await interactor.month(); let currentYear = await interactor.year(); if (!currentMonth || !currentYear) throw new Error("Can't get current month and year"); if (currentYear == targetYear) return; let step = currentYear < targetYear ? () => interactor.nextMonth() : () => interactor.prevMonth(); let targetMonth = currentMonth; while (currentYear != targetYear || currentMonth != targetMonth) { await step(); await delay(1000); let prevMonth: string | undefined = currentMonth; currentMonth = await interactor.month(); currentYear = await interactor.year(); if (prevMonth == currentMonth) throw new Error( `Can't set '${targetYear}' year. It might happened because of 'minDate/maxDate' or 'disableFuture/disablePast' props` ); } }, setMonth: async (interactor, targetMonth: string) => { let currentYear = await interactor.year(); let directions = [() => interactor.prevMonth(), () => interactor.nextMonth()]; directions = Math.round(Math.random()) ? directions : directions.reverse(); let directionStep = directions.shift() as () => Interaction; try { await goToMonth(interactor as ReturnType, targetMonth, directionStep, currentYear); } catch (_) { directionStep = directions.shift() as () => Interaction; await goToMonth(interactor as ReturnType, targetMonth, directionStep, currentYear); } }, setDay: goToDay, }); /** * Call this {@link InteractorConstructor} to initialize a calendar {@link Interactor}. * The calendar interactor can be used to interact with calendars on the page and * to assert on their state. * * The calendar is located by selected day and title text divided by space (ex. '18 August 2014'). * * ### Example * * ``` typescript * await Calendar('18 August 2014').setDay(13); * await Calendar('18 August 2014').has({ weekDay: 'Mo' }); * await Calendar({ title: 'August 2014', day: 18 }).exists(); * ``` * * ### Filters * * - `title`: *string* – Filter by title * - `year`: *number* – Filter by year * - `month`: *string* – Filter by month * - `day`: *number* – Filter by day * - `weekDay`: *string* – Filter by weekDay * * ### Actions * * - `nextMonth()`: *{@link Interaction}* – Switch calendar view to the next month * - `prevMonth()`: *{@link Interaction}* – Switch calendar view to the previous month * - `setYear(value: number)`: *{@link Interaction}* – Switch calendar view to the target year * - `setMonth(value: string)`: *{@link Interaction}* – Switch calendar view to the target month * - `setDay(value: number)`: *{@link Interaction}* – Set a new date for calendar with previously selected year and month * * @category Interactor */ export const Calendar = CalendarInteractor;