Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(TimezoneSelector): simplify override logics and tests #19090

Merged
merged 1 commit into from
Mar 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,60 +18,100 @@
*/
import React from 'react';
import moment from 'moment-timezone';
import { render, screen } from 'spec/helpers/testing-library';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import TimezoneSelector from './index';

describe('TimezoneSelector', () => {
let timezone: string | undefined;
const onTimezoneChange = jest.fn(zone => {
timezone = zone;
});
beforeEach(() => {
timezone = undefined;
});
it('renders a TimezoneSelector with a default if undefined', () => {
jest.spyOn(moment.tz, 'guess').mockReturnValue('America/New_York');
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone={timezone}
/>,
);
expect(onTimezoneChange).toHaveBeenCalledWith('America/Nassau');
});
it('should properly select values from the offsetsToName map', async () => {
jest.spyOn(moment.tz, 'guess').mockReturnValue('America/New_York');
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone={timezone}
/>,
);
jest.spyOn(moment.tz, 'guess').mockReturnValue('America/New_York');

const select = screen.getByRole('combobox', {
name: 'Timezone selector',
});
expect(select).toBeInTheDocument();
userEvent.click(select);
const getSelectOptions = () =>
waitFor(() => document.querySelectorAll('.ant-select-item-option-content'));

const isDaylight = moment('now').isDST();
let findTitle = 'GMT -07:00 (Mountain Standard Time)';
if (isDaylight) {
findTitle = 'GMT -06:00 (Mountain Daylight Time)';
}
const selection = await screen.findByTitle(findTitle);
expect(selection).toBeInTheDocument();
userEvent.click(selection);
expect(selection).toBeVisible();
});
it('renders a TimezoneSelector with the closest value if passed in', async () => {
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="America/Los_Angeles"
/>,
);
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Vancouver');
it('use the timezone from `moment` if no timezone provided', () => {
const onTimezoneChange = jest.fn();
render(<TimezoneSelector onTimezoneChange={onTimezoneChange} />);
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenCalledWith('America/Nassau');
});

it('update to closest deduped timezone when timezone is provided', async () => {
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="America/Los_Angeles"
/>,
);
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Vancouver');
});

it('use the default timezone when an invalid timezone is provided', async () => {
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector onTimezoneChange={onTimezoneChange} timezone="UTC" />,
);
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('Africa/Abidjan');
});

it('can select a timezone values and returns canonical value', async () => {
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="America/Nassau"
/>,
);

const searchInput = screen.getByRole('combobox', {
name: 'Timezone selector',
});
expect(searchInput).toBeInTheDocument();
userEvent.click(searchInput);
const isDaylight = moment(moment.now()).isDST();

const selectedTimezone = isDaylight
? 'GMT -04:00 (Eastern Daylight Time)'
: 'GMT -05:00 (Eastern Standard Time)';

// selected option ranks first
const options = await getSelectOptions();
expect(options[0]).toHaveTextContent(selectedTimezone);

// others are ranked by offset
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)');
expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)');
expect(options[3]).toHaveTextContent('GMT -10:00 (America/Adak)');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New tests for option ordering.


// search for mountain time
await userEvent.type(searchInput, 'mou', { delay: 10 });

const findTitle = isDaylight
? 'GMT -06:00 (Mountain Daylight Time)'
: 'GMT -07:00 (Mountain Standard Time)';
const selectOption = await screen.findByTitle(findTitle);
expect(selectOption).toBeInTheDocument();
userEvent.click(selectOption);
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Cambridge_Bay');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test the updated timezone value after selecting an option

});

it('can update props and rerender with different values', async () => {
const onTimezoneChange = jest.fn();
const { rerender } = render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="Asia/Dubai"
/>,
);
expect(screen.getByTitle('GMT +04:00 (Asia/Dubai)')).toBeInTheDocument();
rerender(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="Australia/Perth"
/>,
);
expect(screen.getByTitle('GMT +08:00 (Australia/Perth)')).toBeInTheDocument();
expect(onTimezoneChange).toHaveBeenCalledTimes(0);
});
51 changes: 24 additions & 27 deletions superset-frontend/src/components/TimezoneSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import React, { useEffect, useRef, useCallback } from 'react';
import React, { useEffect, useMemo } from 'react';
import moment from 'moment-timezone';
import { t } from '@superset-ui/core';
import { Select } from 'src/components';
Expand Down Expand Up @@ -92,43 +92,40 @@ const TIMEZONE_OPTIONS = TIMEZONES.map(zone => ({
timezoneName: zone.name,
}));

const TIMEZONE_OPTIONS_SORT_COMPARATOR = (
a: typeof TIMEZONE_OPTIONS[number],
b: typeof TIMEZONE_OPTIONS[number],
) =>
moment.tz(currentDate, a.timezoneName).utcOffset() -
moment.tz(currentDate, b.timezoneName).utcOffset();

TIMEZONE_OPTIONS.sort(TIMEZONE_OPTIONS_SORT_COMPARATOR);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#19085 requires select options to be pre-sorted.


const matchTimezoneToOptions = (timezone: string) =>
TIMEZONE_OPTIONS.find(option => option.offsets === getOffsetKey(timezone))
?.value || DEFAULT_TIMEZONE.value;

const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
const prevTimezone = useRef(timezone);
const matchTimezoneToOptions = (timezone: string) =>
TIMEZONE_OPTIONS.find(option => option.offsets === getOffsetKey(timezone))
?.value || DEFAULT_TIMEZONE.value;

const updateTimezone = useCallback(
(tz: string) => {
// update the ref to track changes
prevTimezone.current = tz;
// the parent component contains the state for the value
onTimezoneChange(tz);
},
[onTimezoneChange],
const validTimezone = useMemo(
() => matchTimezoneToOptions(timezone || moment.tz.guess()),
[timezone],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed from useRef to simply triggering a setState in useEffect.

);

// force trigger a timezone update if provided `timezone` is not invalid
useEffect(() => {
const updatedTz = matchTimezoneToOptions(timezone || moment.tz.guess());
if (prevTimezone.current !== updatedTz) {
updateTimezone(updatedTz);
if (timezone !== validTimezone) {
onTimezoneChange(validTimezone);
}
}, [timezone, updateTimezone]);
}, [validTimezone, onTimezoneChange, timezone]);

return (
<Select
ariaLabel={t('Timezone selector')}
css={{ minWidth: MIN_SELECT_WIDTH }} // smallest size for current values
onChange={onTimezoneChange}
value={timezone || DEFAULT_TIMEZONE.value}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure value is a valid timezone. timezone may be different than updatedTz.

onChange={tz => onTimezoneChange(tz as string)}
value={validTimezone}
options={TIMEZONE_OPTIONS}
sortComparator={(
a: typeof TIMEZONE_OPTIONS[number],
b: typeof TIMEZONE_OPTIONS[number],
) =>
moment.tz(currentDate, a.timezoneName).utcOffset() -
moment.tz(currentDate, b.timezoneName).utcOffset()
}
sortComparator={TIMEZONE_OPTIONS_SORT_COMPARATOR}
/>
);
};
Expand Down