Skip to content

Commit

Permalink
feat: display departure times and search time in CET, regardless of w…
Browse files Browse the repository at this point in the history
…hich timezone the users is located in (#234)

* Added functionality to display departure times and the time in the search time selectore in CTE, regardless of which time zone the user is located in.

* Added functinonality to set the correct searchTimeParam when navigating from detailed view back to search results. + some minor updates in the code.

* Fixed bug in search time selector and setTimezoneIfNeeded().

* Fixed small blunder when using wrong function.

* Add playwrigth tests.

* Added mores tests.
  • Loading branch information
jonasbrunvoll authored Mar 1, 2024
1 parent 92c7edd commit 333ab75
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 12 deletions.
145 changes: 145 additions & 0 deletions e2e-tests/assistant-search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { test, expect } from '@playwright/test';

const JST = 'Asia/Tokyo';
const EET = 'Europe/Helsinki';
const CTU = 'Europe/Oslo';
const UTC = 'Europe/London';
const PST = 'America/Los_Angeles';

const fromTextbox = 'Kristiansund';
const fromOption = 'Kristiansund trafikkterminal';
const toTextbox = 'Molde';
const toOption = 'Molde trafikkterminal';
const searchTime = '15:30';
const expectedDeparture = '16:00 -';

test.describe('Trip search from different timezones - detailed view.', () => {
test.beforeEach(async ({ page }) => {
await page.goto(process.env.E2E_URL ?? 'http://localhost:3000');
});

test.use({
timezoneId: JST,
});
test(JST + ' - (UTC +0900)', async ({ page }) => {
await page.getByTestId('searchTimeSelector-departBy').click();
await page.getByTestId('searchTimeSelector-time').click();
await page.getByTestId('searchTimeSelector-time').fill(searchTime);
await page.getByRole('textbox', { name: 'From' }).click();
await page.getByRole('textbox', { name: 'From' }).fill(fromTextbox);
await page.getByRole('option', { name: fromOption }).click();
await page.getByRole('textbox', { name: 'To' }).click();
await page.getByRole('textbox', { name: 'To' }).fill(toTextbox);
await page
.getByRole('option', {
name: toOption,
})
.click();
await page.getByTestId('tripPattern-0-0').click();
const elementText = await page
.getByTestId('detailsHeader-duration')
.textContent();

expect(elementText?.includes(expectedDeparture));
});

test.use({
timezoneId: EET,
});
test(EET + ' - (UTC +0200)', async ({ page }) => {
await page.getByTestId('searchTimeSelector-departBy').click();
await page.getByTestId('searchTimeSelector-time').click();
await page.getByTestId('searchTimeSelector-time').fill(searchTime);
await page.getByRole('textbox', { name: 'From' }).click();
await page.getByRole('textbox', { name: 'From' }).fill(fromTextbox);
await page.getByRole('option', { name: fromOption }).click();
await page.getByRole('textbox', { name: 'To' }).click();
await page.getByRole('textbox', { name: 'To' }).fill(toTextbox);
await page
.getByRole('option', {
name: toOption,
})
.click();
await page.getByTestId('tripPattern-0-0').click();
const elementText = await page
.getByTestId('detailsHeader-duration')
.textContent();

expect(elementText?.includes(expectedDeparture));
});

test.use({
timezoneId: CTU,
});
test(CTU + ' - (UTC +0100)', async ({ page }) => {
await page.getByTestId('searchTimeSelector-departBy').click();
await page.getByTestId('searchTimeSelector-time').click();
await page.getByTestId('searchTimeSelector-time').fill(searchTime);
await page.getByRole('textbox', { name: 'From' }).click();
await page.getByRole('textbox', { name: 'From' }).fill(fromTextbox);
await page.getByRole('option', { name: fromOption }).click();
await page.getByRole('textbox', { name: 'To' }).click();
await page.getByRole('textbox', { name: 'To' }).fill(toTextbox);
await page
.getByRole('option', {
name: toOption,
})
.click();
await page.getByTestId('tripPattern-0-0').click();
const elementText = await page
.getByTestId('detailsHeader-duration')
.textContent();

expect(elementText?.includes(expectedDeparture));
});

test.use({
timezoneId: UTC,
});
test(UTC + ' - (UTC +0000)', async ({ page }) => {
await page.getByTestId('searchTimeSelector-departBy').click();
await page.getByTestId('searchTimeSelector-time').click();
await page.getByTestId('searchTimeSelector-time').fill(searchTime);
await page.getByRole('textbox', { name: 'From' }).click();
await page.getByRole('textbox', { name: 'From' }).fill(fromTextbox);
await page.getByRole('option', { name: fromOption }).click();
await page.getByRole('textbox', { name: 'To' }).click();
await page.getByRole('textbox', { name: 'To' }).fill(toTextbox);
await page
.getByRole('option', {
name: toOption,
})
.click();
await page.getByTestId('tripPattern-0-0').click();
const elementText = await page
.getByTestId('detailsHeader-duration')
.textContent();

expect(elementText?.includes(expectedDeparture));
});

test.use({
timezoneId: PST,
});
test(PST + ' - (UTC -0800)', async ({ page }) => {
await page.getByTestId('searchTimeSelector-departBy').click();
await page.getByTestId('searchTimeSelector-time').click();
await page.getByTestId('searchTimeSelector-time').fill(searchTime);
await page.getByRole('textbox', { name: 'From' }).click();
await page.getByRole('textbox', { name: 'From' }).fill(fromTextbox);
await page.getByRole('option', { name: fromOption }).click();
await page.getByRole('textbox', { name: 'To' }).click();
await page.getByRole('textbox', { name: 'To' }).fill(toTextbox);
await page
.getByRole('option', {
name: toOption,
})
.click();
await page.getByTestId('tripPattern-0-0').click();
const elementText = await page
.getByTestId('detailsHeader-duration')
.textContent();

expect(elementText?.includes(expectedDeparture));
});
});
36 changes: 30 additions & 6 deletions src/modules/search-time/selector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@atb/translations';
import { SEARCH_MODES, SearchMode, SearchTime } from '../types';
import style from './selector.module.css';
import { formatLocalTimeToCET, setTimezone } from '@atb/utils/date';

type SearchTimeSelectorProps = {
onChange: (state: SearchTime) => void;
Expand All @@ -22,8 +23,12 @@ export default function SearchTimeSelector({
}: SearchTimeSelectorProps) {
const { t } = useTranslation();
const [selectedMode, setSelectedMode] = useState<SearchTime>(initialState);
const initialDate =
'dateTime' in initialState ? new Date(initialState.dateTime) : new Date();
const initialDate = setTimezone(
'dateTime' in initialState
? new Date(formatLocalTimeToCET(initialState.dateTime))
: new Date(),
) as Date;

const [selectedDate, setSelectedDate] = useState(initialDate);
const [selectedTime, setSelectedTime] = useState(() =>
format(initialDate, 'HH:mm'),
Expand All @@ -47,11 +52,23 @@ export default function SearchTimeSelector({
};

const resetToCurrentTime = () => {
setSelectedTime(() => format(new Date(), 'HH:mm'));
const newState = {
mode: selectedMode.mode,
dateTime: setTimezone(new Date()).getTime(),
};
setSelectedTime(() => format(newState.dateTime, 'HH:mm'));
setSelectedMode(newState);
onChange(newState);
};

const resetToCurrentDate = () => {
setSelectedDate(new Date());
const newState = {
mode: selectedMode.mode,
dateTime: setTimezone(new Date()).getTime(),
};
setSelectedDate(new Date(newState.dateTime));
setSelectedMode(newState);
onChange(newState);
};

const isPastDate = (selectedDate: string) => {
Expand Down Expand Up @@ -102,7 +119,11 @@ export default function SearchTimeSelector({
style={{ '--number-of-options': options.length } as CSSProperties}
>
{options.map((state) => (
<label key={state} className={style.option}>
<label
key={state}
className={style.option}
data-testid={'searchTimeSelector-' + state}
>
<input
type="radio"
name="searchTimeSelector"
Expand Down Expand Up @@ -155,7 +176,10 @@ export default function SearchTimeSelector({
/>
</div>
<div className={style.timeSelector}>
<label htmlFor="searchTimeSelector-time">
<label
htmlFor="searchTimeSelector-time"
data-testid="searchTimeSelector-time"
>
{t(ModuleText.SearchTime.time)}
</label>

Expand Down
13 changes: 12 additions & 1 deletion src/page-modules/assistant/client/journey-planner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { swrFetcher } from '@atb/modules/api-browser';
import useSWRInfinite from 'swr/infinite';
import { createTripQuery, tripQueryToQueryString } from '../../utils';
import { useEffect, useState } from 'react';
import { formatLocalTimeToCET } from '@atb/utils/date';

const MAX_NUMBER_OF_INITIAL_SEARCH_ATTEMPTS = 3;
const INITIAL_NUMBER_OF_WANTED_TRIP_PATTERNS = 6;
Expand Down Expand Up @@ -37,7 +38,17 @@ export function useTripPatterns(
fallback?: TripData,
) {
const [numberOfTripPatterns, setNumberOfTripPatterns] = useState(0);
const query = createTripQuery(tripQuery);
const query = createTripQuery(
tripQuery.searchTime.mode === 'now'
? tripQuery
: {
...tripQuery,
searchTime: {
...tripQuery.searchTime,
dateTime: formatLocalTimeToCET(tripQuery.searchTime.dateTime),
},
},
);
const { data, error, isLoading, isValidating, size, setSize } =
useSWRInfinite<TripApiReturnType>(
createKeyGetterOfQuery(query),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export default function DetailsHeader({ tripPattern }: DetailsHeaderProps) {
{weekdayAndDate}
</Typo.p>
</div>
<div className={style.duration}>
<div className={style.duration} data-testid={'detailsHeader-duration'}>
<MonoIcon icon="time/Duration" />
<Typo.p
textType={isCancelled ? 'body__primary--strike' : 'body__primary'}
Expand Down
5 changes: 4 additions & 1 deletion src/page-modules/assistant/details/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatCETToLocalTime } from '@atb/utils/date';
import { parseTripQueryString } from '../server/journey-planner';

export function formatQuayName(quayName?: string, publicCode?: string | null) {
Expand Down Expand Up @@ -51,7 +52,9 @@ export function tripQueryStringToQueryParams(
const searchMode = arriveBy ? 'arriveBy' : 'departBy';
const fromLayer = from.place?.includes('StopPlace') ? 'venue' : 'address';
const toLayer = to.place?.includes('StopPlace') ? 'venue' : 'address';
const searchTime = String(new Date(originalSearchTime).getTime());
const searchTime = String(
formatCETToLocalTime(new Date(originalSearchTime).getTime()),
);

const params = {
searchMode,
Expand Down
5 changes: 4 additions & 1 deletion src/page-modules/assistant/trip/trip-pattern/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ export default function TripPattern({
</div>
)}

<div className={style.timeStartContainer}>
<div
className={style.timeStartContainer}
data-testid={`timeStartContainer-${i}`}
>
{secondsBetween(leg.aimedStartTime, leg.expectedStartTime) >
DEFAULT_THRESHOLD_AIMED_EXPECTED_IN_SECONDS ? (
<>
Expand Down
26 changes: 24 additions & 2 deletions src/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ import humanizeDuration from 'humanize-duration';
import { DEFAULT_LANGUAGE, Language } from '../translations';
import dictionary from '@atb/translations/dictionary';
import { TFunc } from '@leile/lobo-t';
import { FALLBACK_LANGUAGE } from '@atb/translations/commons';

const humanizer = humanizeDuration.humanizer({});
const CET = 'Europe/Oslo';
const ONE_HOUR = 3600000;

export function parseIfNeeded(a: string | Date): Date {
return a instanceof Date ? a : parseISO(a);
Expand Down Expand Up @@ -148,8 +151,9 @@ export function formatToClock(
showSeconds?: boolean,
) {
const parsed = parseIfNeeded(isoDate);
const rounded = !showSeconds ? roundMinute(parsed, roundingMethod) : parsed;
const seconds = showSeconds ? ':' + format(parsed, 'ss') : '';
const cet = setTimezone(parsed);
const rounded = !showSeconds ? roundMinute(cet, roundingMethod) : cet;
const seconds = showSeconds ? ':' + format(cet, 'ss') : '';

return formatLocaleTime(rounded, language) + seconds;
}
Expand Down Expand Up @@ -352,3 +356,21 @@ export function formatTripDuration(

return { duration, departure, arrival };
}

export function setTimezone(date: Date): Date {
return new Date(date.toLocaleString(FALLBACK_LANGUAGE, { timeZone: CET }));
}

export function formatLocalTimeToCET(localTime: number) {
const offset = getOffsetTimezone();
return localTime + ONE_HOUR * (offset - 1);
}

export function formatCETToLocalTime(cet: number) {
const offset = getOffsetTimezone();
return cet - ONE_HOUR * (offset - 1);
}

function getOffsetTimezone() {
return (-1 * new Date().getTimezoneOffset()) / 60;
}

0 comments on commit 333ab75

Please sign in to comment.