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

This commit is contained in:
Jesse Yang 2022-03-10 18:09:20 -08:00 committed by GitHub
parent 158396fb6c
commit 3a78165d13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 115 additions and 78 deletions

View File

@ -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)');
// 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');
});
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);
});

View File

@ -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';
@ -92,43 +92,40 @@ const TIMEZONE_OPTIONS = TIMEZONES.map(zone => ({
timezoneName: zone.name,
}));
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 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();
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],
TIMEZONE_OPTIONS.sort(TIMEZONE_OPTIONS_SORT_COMPARATOR);
const matchTimezoneToOptions = (timezone: string) =>
TIMEZONE_OPTIONS.find(option => option.offsets === getOffsetKey(timezone))
?.value || DEFAULT_TIMEZONE.value;
const TimezoneSelector = ({ onTimezoneChange, timezone }: TimezoneProps) => {
const validTimezone = useMemo(
() => matchTimezoneToOptions(timezone || moment.tz.guess()),
[timezone],
);
// 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}
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}
/>
);
};