Skip to content

Commit

Permalink
chore(react-datepicker-compat): Add cypress and unit tests for DatePi…
Browse files Browse the repository at this point in the history
…cker (#27209)

* adding some of the tests and setting up cypress

* sync

* finish implementing tests

* restoring text input story

* adding requested changes

* adding requested changeS
  • Loading branch information
sopranopillow authored Mar 16, 2023
1 parent 4292289 commit a97f513
Show file tree
Hide file tree
Showing 21 changed files with 472 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { baseConfig } from '@fluentui/scripts-cypress';

export default baseConfig;
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,6 @@ export type DatePickerProps = ComponentProps<Partial<DatePickerSlots>> & {
label?: string;
isRequired?: boolean;
disabled?: boolean;
ariaLabel?: string;
underlined?: boolean;
pickerAriaLabel?: string;
isMonthPickerVisible?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"build": "just-scripts build",
"clean": "just-scripts clean",
"code-style": "just-scripts code-style",
"e2e": "cypress run --component",
"e2e:local": "cypress open --component",
"just": "just-scripts",
"lint": "just-scripts lint",
"test": "jest --passWithNoTests",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ export const CalendarDayGrid: React.FunctionComponent<CalendarDayGridProps> = pr
week={weeks[0]}
weekIndex={-1}
rowClassName={classNames.firstTransitionWeek}
ariaRole="presentation"
ariaHidden={true}
aria-role="presentation"
aria-hidden={true}
/>
{weeks!.slice(1, weeks!.length - 1).map((week: DayInfo[], weekIndex: number) => (
<CalendarGridRow
Expand All @@ -166,8 +166,8 @@ export const CalendarDayGrid: React.FunctionComponent<CalendarDayGridProps> = pr
week={weeks![weeks!.length - 1]}
weekIndex={-2}
rowClassName={classNames.lastTransitionWeek}
ariaRole="presentation"
ariaHidden={true}
aria-role="presentation"
aria-hidden={true}
/>
</tbody>
</table>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as React from 'react';
import { mount as mountBase } from '@cypress/react';

import { FluentProvider } from '@fluentui/react-provider';
import { teamsLightTheme } from '@fluentui/react-theme';

import { DatePicker } from './DatePicker';

const mount = (element: JSX.Element) => {
mountBase(<FluentProvider theme={teamsLightTheme}>{element}</FluentProvider>);
};

const inputSelector = '[role="combobox"]';
const popoverSelector = '[role="dialog"]';
const fieldErrorMessageSelector = '[role=alert]';

describe('DatePicker', () => {
it('opens a default datepicker', () => {
mount(<DatePicker />);
cy.get(inputSelector).click().get(popoverSelector).should('be.visible');
});

it('should not open a datepicker when disabled', () => {
mount(<DatePicker disabled />);
// Force is needed because otherwise Cypress throws an error
cy.get(inputSelector).click({ force: true }).get(popoverSelector).should('not.exist');
});

it('should render DatePicker and calloutId must exist in the DOM when isDatePickerShown is set', () => {
mount(<DatePicker />);
cy.get(inputSelector).click();

cy.get('body').find('[aria-owns]').should('exist');
});

it('should clear error message when required input has date text and allowTextInput is true', () => {
mount(<DatePicker isRequired allowTextInput />);

// Open DatePicker and dismiss
cy.get(inputSelector).click().get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('exist');

// Type a date and dismiss
cy.get(inputSelector).click().click().type('Jan 1 2030').get('body').click('bottomRight');

cy.get(fieldErrorMessageSelector).should('not.exist');
});

it('clears error message when required input has date selected from calendar and allowTextInput is true', () => {
mount(<DatePicker isRequired allowTextInput />);

// Open picker and dismiss to show error message
cy.get(inputSelector).click().get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('exist');

// Select a date from calendar, we choose 10 since the first 0-6 days in the grid are not really dates, and dismiss
cy.get(inputSelector).click().get('[role="gridcell"]').its(10).click().get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('not.exist');
});

it('should not clear initial error when datepicker is opened', () => {
mount(<DatePicker isRequired allowTextInput maxDate={new Date('2020-04-01')} value={new Date('2020-04-02')} />);

cy.get(fieldErrorMessageSelector).should('exist');

// open and dismiss picker
cy.get(inputSelector).click().get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('exist');
});

it('should reset status message after selecting a valid date', () => {
mount(<DatePicker allowTextInput initialPickerDate={new Date('2021-04-15')} />);

cy.get(fieldErrorMessageSelector).should('not.exist');
cy.get(inputSelector).click().click().type('test').get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('exist');
cy.get(inputSelector).click().get('[role="gridcell"]').its(10).click().get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('not.exist');
});
});

describe('When boundaries are specified', () => {
const defaultDate = new Date('Dec 15 2017');
const minDate = new Date('Jan 1 2017');
const maxDate = new Date('Dec 31 2017');
const strings = {
months: [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
shortDays: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
goToToday: 'Go to today',
isOutOfBoundsErrorMessage: 'out of bounds',
};

beforeEach(() => {
mount(<DatePicker allowTextInput minDate={minDate} maxDate={maxDate} value={defaultDate} strings={strings} />);
});

it('should throw validation error for date outside boundary', () => {
// Before min date
cy.get(inputSelector).click().click().clear().type('Jan 1 2010{enter}').get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('exist').should('have.text', 'out of bounds');

// After max date
cy.get(inputSelector).click().click().clear().type('Jan 1 2020{enter}').get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('exist').should('have.text', 'out of bounds');
});

it('should not throw validation error for date inside boundary', () => {
// In boundary
cy.get(inputSelector).click().click().clear().type('Dec 16 2017{enter}').get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('not.exist');

// In boundary
cy.get(inputSelector).click().click().clear().type('Jan 1 2017{enter}').get('body').click('bottomRight');
cy.get(fieldErrorMessageSelector).should('not.exist');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import * as React from 'react';
import { fireEvent, render, RenderResult } from '@testing-library/react';
import { DatePicker } from './DatePicker';
import { isConformant } from '../../testing/isConformant';
import { datePickerClassNames } from './useDatePickerStyles';
import { resetIdsForTests } from '@fluentui/react-utilities';

// testing-library's queryByRole function doesn't look inside portals
function queryByRoleDialog(result: RenderResult) {
const dialogs = result.baseElement.querySelectorAll('*[role="dialog"]');
if (!dialogs?.length) {
return null;
} else {
expect(dialogs.length).toBe(1);
return dialogs.item(0) as HTMLElement;
}
}

const getDatepickerPopoverElement = (result: RenderResult) => {
result.getByRole('combobox').click();
const dialog = queryByRoleDialog(result);
expect(dialog).not.toBeNull();
return dialog!;
};

describe('DatePicker', () => {
beforeEach(() => {
resetIdsForTests();
});

isConformant({
Component: DatePicker,
displayName: 'DatePicker',
disabledTests: ['consistent-callback-args'],
testOptions: {
'has-static-classnames': [
{
props: {},
expectedClassNames: {
root: datePickerClassNames.root,
inputField: datePickerClassNames.inputField,
wrapper: datePickerClassNames.wrapper,
popoverSurface: datePickerClassNames.popoverSurface,
input: datePickerClassNames.input,
calendar: datePickerClassNames.calendar,
},
getPortalElement: getDatepickerPopoverElement,
},
],
},
});

it('can add an id to the container', () => {
const result = render(<DatePicker id="test-id" />);
expect(result.findByTestId('test-id')).toBeTruthy();
});

it('should not render DatePicker when isDatePickerShown is not set', () => {
const result = render(<DatePicker />);
expect(result).toMatchSnapshot();
});

it('renders a normal input when allowTextInput is true', () => {
const result = render(<DatePicker allowTextInput />);
expect(result.getByRole('combobox').getAttribute('readonly')).toBeNull();
});

it('renders a readonly input when allowTextInput is false', () => {
const result = render(<DatePicker />);
expect(result.getByRole('combobox').getAttribute('readonly')).not.toBeNull();
});

it('should call onSelectDate even when required input is empty when allowTextInput is true', () => {
const onSelectDate = jest.fn();
const result = render(<DatePicker isRequired allowTextInput onSelectDate={onSelectDate} />);
const input = result.getByRole('combobox');

fireEvent.change(input, { target: { value: 'Jan 1 2030' } });
fireEvent.blur(input);

fireEvent.change(input, { target: { value: '' } });
fireEvent.blur(input);

expect(onSelectDate).toHaveBeenCalledTimes(2);
});

it('should call onSelectDate only once when allowTextInput is true and popup is used to select the value', () => {
const onSelectDate = jest.fn();
const result = render(<DatePicker allowTextInput onSelectDate={onSelectDate} />);

fireEvent.click(result.getByRole('combobox'));
result.getAllByRole('gridcell')[10].click();

expect(onSelectDate).toHaveBeenCalledTimes(1);
});

it('should set "Calendar" as the Callout\'s aria-label', () => {
const result = render(<DatePicker />);
const input = result.getByRole('combobox');

fireEvent.click(input);
fireEvent.blur(input);

expect(result.getByRole('dialog').getAttribute('aria-label')).toBe('Calendar');
});

it('should reflect the correct date in the input field when selecting a value', () => {
const today = new Date('January 15, 2020');
const initiallySelectedDate = new Date('January 10, 2020');
const result = render(<DatePicker allowTextInput today={today} initialPickerDate={initiallySelectedDate} />);

const input = result.getByRole('combobox');

fireEvent.click(input);
result.getByText('15').click();

expect(input.getAttribute('value')).toBe('Wed Jan 15 2020');
});

it('reflects the correct date in the input field when selecting a value and a different format is given', () => {
const today = new Date('January 15, 2020');
const initiallySelectedDate = new Date('January 10, 2020');
const onFormatDate = (date?: Date): string => {
return date ? date.getDate() + '/' + (date.getMonth() + 1) + '/' + (date.getFullYear() % 100) : '';
};

const result = render(
<DatePicker
allowTextInput={true}
today={today}
formatDate={onFormatDate}
initialPickerDate={initiallySelectedDate}
/>,
);
const input = result.getByRole('combobox');

fireEvent.click(input);
result.getByText('15').click();

expect(input.getAttribute('value')).toBe('15/1/20');
});
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,6 @@ export type DatePickerProps = ComponentProps<Partial<DatePickerSlots>> & {
*/
disabled?: boolean;

/**
* Aria Label for TextField of the DatePicker for screen reader users.
*/
ariaLabel?: string;

/**
* Whether or not the Textfield of the DatePicker is underlined.
* @defaultvalue false
Expand Down
Loading

0 comments on commit a97f513

Please sign in to comment.