cronofy-elements 1.40.2 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,14 @@
1
+ import moment from "moment-timezone";
2
+
1
3
  import {
2
4
  addSlotsToObject,
3
- getFirstAvailableDay,
4
5
  getSlotsByDay,
5
6
  getAvailableDays,
6
7
  getLocalDayFromUtc,
8
+ removeMonthFromLoading,
9
+ addSequencedSlotsToObject,
7
10
  } from "../utils/slots";
8
- import { parseTimeSlots } from "../utils/calendar";
11
+ import { getMonthsInDisplay, parseTimeSlots } from "../utils/calendar";
9
12
 
10
13
  export const statusReducer = (state, action) => {
11
14
  const { type, ...actionBody } = action;
@@ -41,12 +44,29 @@ export const statusReducer = (state, action) => {
41
44
  notification: {
42
45
  type: "error",
43
46
  message: "There was an error getting the slots",
47
+ body: action.error.body,
44
48
  },
45
49
  };
46
50
  state.callback(notification);
47
51
  return { ...state, error: action.error, columnView: "error" };
48
52
  }
49
53
 
54
+ case "ERROR_LOADING_SLOTS": {
55
+ const notification = {
56
+ notification: {
57
+ type: "error",
58
+ message:
59
+ "There was a problem with your availability query. Slots for the month of " +
60
+ action.month +
61
+ " could not be loaded.",
62
+ body: action.error.body,
63
+ },
64
+ };
65
+ const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
66
+ state.callback(notification);
67
+ return { ...state, monthsLoading };
68
+ }
69
+
50
70
  case "NO_SLOTS_FOUND": {
51
71
  return { ...state, columnView: "no-slots" };
52
72
  }
@@ -54,6 +74,9 @@ export const statusReducer = (state, action) => {
54
74
  case "RECALCULATE_MONTH_VIEW": {
55
75
  const monthlyView = {
56
76
  month: state.monthlyView.month,
77
+ hasNext: state.monthlyView.hasNext,
78
+ hasPrev: state.monthlyView.hasPrev,
79
+ monthsInView: state.monthlyView.monthsInView,
57
80
  days: parseTimeSlots({
58
81
  slots: state.slots,
59
82
  month: state.monthlyView.month,
@@ -70,38 +93,31 @@ export const statusReducer = (state, action) => {
70
93
 
71
94
  case "SET_SLOTS": {
72
95
  if (!action.slots.length > 0) {
73
- return state;
96
+ const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
97
+ return {
98
+ ...state,
99
+ monthsLoading,
100
+ slotFetchCount: state.slotFetchCount + 1,
101
+ };
74
102
  }
75
103
 
76
- const slotsObject = addSlotsToObject(state.slots, action.slots);
77
-
78
- let daySlots = state.daySlots;
79
- let columnView = state.columnView;
80
- let focusedDay = state.focusedDay;
81
- let selectedDay = state.selectedDay;
82
- let availableDays = getAvailableDays(addSlotsToObject({}, action.slots), action.tzid);
104
+ const slotsObject = state.sequenced_availability
105
+ ? addSequencedSlotsToObject(state.slots, action.slots)
106
+ : addSlotsToObject(state.slots, action.slots);
107
+ const availableDays = getAvailableDays(slotsObject, action.tzid);
108
+ const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
83
109
 
84
- const availableDaysSet = new Set([...state.availableDays, ...availableDays]);
85
- availableDays = [...availableDaysSet].sort();
86
-
87
- if (!state.selectedDay) {
88
- columnView = "slots";
89
- focusedDay = availableDays[0];
90
- selectedDay = availableDays[0];
91
- daySlots = getSlotsByDay(slotsObject, selectedDay, action.tzid);
92
- }
110
+ const injectionPoint = state.sequenced_availability
111
+ ? getLocalDayFromUtc(action.slots[0].sequence[0].start, action.tzid)
112
+ : getLocalDayFromUtc(action.slots[0].start, action.tzid);
93
113
 
94
114
  return {
95
115
  ...state,
96
- daySlots,
97
- columnView,
98
- focusedDay,
99
- selectedDay,
100
116
  availableDays,
101
-
117
+ monthsLoading,
102
118
  slots: slotsObject,
103
119
  slotFetchCount: state.slotFetchCount + 1,
104
- slotInjectionPoint: getLocalDayFromUtc(action.slots[0].start, action.tzid),
120
+ slotInjectionPoint: injectionPoint,
105
121
  };
106
122
  }
107
123
 
@@ -114,26 +130,46 @@ export const statusReducer = (state, action) => {
114
130
 
115
131
  case "SELECT_DAY": {
116
132
  const daySlots = getSlotsByDay(state.slots, action.day, action.tzid);
133
+ const focusedSlot = state.sequenced_availability
134
+ ? daySlots[0]?.reduce((prev, current) => {
135
+ return prev.start < current.start ? prev.start : current.start;
136
+ })
137
+ : daySlots[0]?.start;
138
+
117
139
  return {
118
140
  ...state,
119
141
  selectedDay: action.day,
120
142
  focusedDay: action.day,
121
- focusedSlot: daySlots[0].start,
143
+ focusedSlot: focusedSlot,
122
144
  columnView: "slots",
123
145
  daySlots,
124
146
  populated: true,
125
147
  };
126
148
  }
127
149
  case "SELECT_MONTH": {
150
+ const actionMonthObject = moment(action.month, "YYYY-MM");
151
+
152
+ if (
153
+ !actionMonthObject.isBetween(
154
+ state.startDateObject,
155
+ state.endDateObject,
156
+ "month",
157
+ "[]"
158
+ )
159
+ ) {
160
+ return state;
161
+ }
162
+
128
163
  const months = state.months.map(month => ({
129
164
  ...month,
130
165
  current: month.month === action.month,
131
166
  }));
132
167
 
133
- let focusedDay = action.focusedDay ?? false;
134
-
135
168
  const monthlyView = {
136
169
  month: action.month,
170
+ hasNext: actionMonthObject.isBefore(state.endDateObject, "month"),
171
+ hasPrev: actionMonthObject.isAfter(state.startDateObject, "month"),
172
+ monthsInView: getMonthsInDisplay(action.month, state.startDay),
137
173
  days: parseTimeSlots({
138
174
  tzid: action.tzid,
139
175
  slots: state.slots,
@@ -142,6 +178,8 @@ export const statusReducer = (state, action) => {
142
178
  }),
143
179
  };
144
180
 
181
+ let focusedDay = action.focusedDay ?? false;
182
+
145
183
  return {
146
184
  ...state,
147
185
  months,
@@ -170,6 +208,9 @@ export const statusReducer = (state, action) => {
170
208
 
171
209
  const monthlyView = {
172
210
  month: state.monthlyView.month,
211
+ hasNext: state.monthlyView.hasNext,
212
+ hasPrev: state.monthlyView.hasPrev,
213
+ monthsInView: state.monthlyView.monthsInView,
173
214
  days: parseTimeSlots({
174
215
  slots: state.slots,
175
216
  month: state.monthlyView.month,
@@ -24,3 +24,13 @@
24
24
  animation: spin infinite linear 1s;
25
25
  }
26
26
  }
27
+
28
+ .DTP__calendar-loading {
29
+ margin-top: 1rem;
30
+ svg {
31
+ width: 1em;
32
+ height: 1em;
33
+ animation: spin infinite linear 1s;
34
+ margin-right: 1rem;
35
+ }
36
+ }
@@ -92,6 +92,29 @@ export const getDatesInMonthDisplay = (month, availableDays, startDay) => {
92
92
  return rows;
93
93
  };
94
94
 
95
+ export const getMonthsInDisplay = (month, startDay) => {
96
+ const monthObject = moment(month, "YYYY-MM");
97
+ const daysInMonth = monthObject.daysInMonth();
98
+ const offset = weekDayOffset(monthObject, startDay);
99
+
100
+ const result = [month];
101
+ const daysLength = daysInMonth + offset;
102
+
103
+ //Add prev month if in display...
104
+ if (offset > 0) {
105
+ const prevMonth = monthObject.clone().add(-1, "months").format("YYYY-MM");
106
+ result.push(prevMonth);
107
+ }
108
+
109
+ // Add next month if in display
110
+ if (daysLength < 42) {
111
+ const nextMonth = monthObject.clone().add(1, "months").format("YYYY-MM");
112
+ result.push(nextMonth);
113
+ }
114
+
115
+ return result;
116
+ };
117
+
95
118
  export const parseTimeSlots = ({ slots, month, tzid, startDay }) => {
96
119
  //Get array of available days
97
120
  const availableDays = getAvailableDays(slots, tzid);
@@ -1,7 +1,8 @@
1
1
  import moment from "moment-timezone";
2
2
 
3
- import { getAvailability } from "../../../helpers/connections";
3
+ import { getAvailability, getSequencedAvailability } from "../../../helpers/connections";
4
4
  import { uniqueItems, humanizeTzName } from "../../../helpers/utils";
5
+ import { errorMessages } from "../../../helpers/logging";
5
6
 
6
7
  import { defaultTimeZones } from "./tz-list";
7
8
 
@@ -21,7 +22,7 @@ export const getMonthsCoveredByPeriod = (period, tzid) => {
21
22
  return months;
22
23
  };
23
24
 
24
- export const getMonthsFromQuery = (periods, tzid) => {
25
+ export const getMonthsFromQuery = (periods = [], tzid) => {
25
26
  const months = periods
26
27
  .map(period => getMonthsCoveredByPeriod(period, tzid))
27
28
  // getMonthsCoveredByPeriod returns an array, so flatten them...
@@ -32,6 +33,40 @@ export const getMonthsFromQuery = (periods, tzid) => {
32
33
  return uniqueMonths;
33
34
  };
34
35
 
36
+ export const getDurationFromQuery = query => {
37
+ const duration = query.sequence ? false : query.required_duration.minutes;
38
+
39
+ return duration;
40
+ };
41
+
42
+ export const getMonthsLoadingFromQuery = (query, tzid) => {
43
+ if (!query.query_periods || !query.query_periods.length) {
44
+ return [
45
+ {
46
+ month: moment.tz().format("YYYY-MM"),
47
+ loading: true,
48
+ },
49
+ ];
50
+ }
51
+ const monthStrings = getMonthsFromQuery(query.query_periods, tzid);
52
+ const monthObjects = monthStrings.map(month => {
53
+ return {
54
+ month,
55
+ loading: true,
56
+ };
57
+ });
58
+ return monthObjects;
59
+ };
60
+
61
+ export const removeMonthFromLoading = (monthsLoading, month) => {
62
+ const monthPos = monthsLoading.findIndex(m => m.month === month);
63
+ const newMonthsLoading = [...monthsLoading];
64
+ if (monthsLoading && monthPos >= 0) {
65
+ newMonthsLoading[monthPos]["loading"] = false;
66
+ }
67
+ return newMonthsLoading;
68
+ };
69
+
35
70
  export const getCurrentMonth = query => ({
36
71
  month: moment.tz().format("YYYY-MM"),
37
72
  current: true,
@@ -42,9 +77,17 @@ export const getMonthObjectsFromQuery = (query, tzid, current = false) => {
42
77
  if (!query.query_periods || !query.query_periods.length) {
43
78
  return [getCurrentMonth()];
44
79
  }
80
+
45
81
  const monthStrings = getMonthsFromQuery(query.query_periods, tzid);
46
- const currentMonth = current ? current : monthStrings[0];
47
- const monthObjects = monthStrings.map(month => {
82
+ const startMonth = moment(monthStrings[0], "YYYY-MM");
83
+ const endMonth = moment(monthStrings[monthStrings.length - 1], "YYYY-MM");
84
+
85
+ const currentMonth =
86
+ current && moment(current, "YYYY-MM").isBetween(startMonth, endMonth, "month", "[]")
87
+ ? current
88
+ : monthStrings[0];
89
+
90
+ return monthStrings.map(month => {
48
91
  const croppedQueryPeriods = cropPeriodsByMonth(query.query_periods, month, tzid);
49
92
  return {
50
93
  month,
@@ -56,7 +99,6 @@ export const getMonthObjectsFromQuery = (query, tzid, current = false) => {
56
99
  },
57
100
  };
58
101
  });
59
- return monthObjects;
60
102
  };
61
103
 
62
104
  export const getSlots = ({ query, auth, tzid, slots = [] }) =>
@@ -68,6 +110,14 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
68
110
  tzid,
69
111
  auth.demo
70
112
  ).then(res => {
113
+ if (res.status === 422) {
114
+ throw {
115
+ type: 422,
116
+ message: errorMessages[422].message,
117
+ body: res.errors,
118
+ docsSlug: errorMessages[422].docsSlug,
119
+ };
120
+ }
71
121
  // This will intentionally throw an error if the
72
122
  // result is not in the correct format:
73
123
  const returnedSlots = parseSlotsResult(res);
@@ -98,6 +148,53 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
98
148
  });
99
149
  });
100
150
 
151
+ export const getSequencedSlots = ({ query, auth, tzid, slots = [] }) =>
152
+ getSequencedAvailability(
153
+ auth.token,
154
+ auth.domains.apiDomain,
155
+ query,
156
+ "DateTimePicker",
157
+ tzid,
158
+ auth.demo
159
+ ).then(res => {
160
+ if (res.errors) {
161
+ throw {
162
+ type: 422,
163
+ message: errorMessages[422].message,
164
+ body: res.errors,
165
+ docsSlug: errorMessages[422].docsSlug,
166
+ };
167
+ }
168
+ // This will intentionally throw an error if the
169
+ // result is not in the correct format:
170
+ const returnedSlots = parseSlotsResult(res);
171
+
172
+ const allSlots = [...slots, ...returnedSlots];
173
+
174
+ if (returnedSlots.length < 512) return [...slots, ...returnedSlots];
175
+
176
+ // If we get here, the API has returned the maximum number
177
+ // of slots allowed, so we need to crop the query and try
178
+ // again to ensure we haven't missed any slots.
179
+
180
+ const startOfLastSlot = returnedSlots[returnedSlots.length - 1].start;
181
+ const endOfLastPeriod = query.query_periods[query.query_periods.length - 1].end;
182
+ const boundsForCropping = {
183
+ start: startOfLastSlot,
184
+ end: endOfLastPeriod,
185
+ };
186
+ const croppedPeriods = cropPeriodsArbitrarily(query.query_periods, boundsForCropping);
187
+ const croppedQuery = { ...query, query_periods: croppedPeriods };
188
+
189
+ // Rerun the query
190
+ return getSequencedSlots({
191
+ query: croppedQuery,
192
+ auth,
193
+ tzid,
194
+ slots: allSlots,
195
+ });
196
+ });
197
+
101
198
  export const parseQuery = query => {
102
199
  if (!query.bookable_events) {
103
200
  return {
@@ -112,6 +209,8 @@ export const parseSlotsResult = res => {
112
209
  let returnedSlots;
113
210
  if (typeof res.available_bookable_events !== "undefined") {
114
211
  returnedSlots = res.available_bookable_events;
212
+ } else if (typeof res.sequences !== "undefined") {
213
+ returnedSlots = res.sequences;
115
214
  } else {
116
215
  returnedSlots = res.available_slots;
117
216
 
@@ -165,6 +264,17 @@ export const addSlotsToObject = (slotsObject, newSlotsArray) => {
165
264
  return slotsObject;
166
265
  };
167
266
 
267
+ export const addSequencedSlotsToObject = (slotsObject, newSlotsArray) => {
268
+ newSlotsArray.forEach(slot => {
269
+ const startArray = slot.sequence.map(a => a.start);
270
+ const start = startArray.reduce((prev, current) => {
271
+ return prev < current ? prev : current;
272
+ });
273
+ slotsObject[start] = slot.sequence;
274
+ });
275
+ return slotsObject;
276
+ };
277
+
168
278
  export const getSlotsByDay = (slots, day, tzid) => {
169
279
  const slotKeys = Object.keys(slots);
170
280
  const dayObject = moment.tz(day, "YYYY-MM-DD", tzid);
@@ -268,6 +268,44 @@ export const getAvailability = (
268
268
  .catch(catchAndRethrowErrors);
269
269
  };
270
270
 
271
+ export const getSequencedAvailability = (
272
+ token,
273
+ api_domain,
274
+ params = {},
275
+ element = "sequenced-availability",
276
+ tzid = "Etc/UTC",
277
+ mock = false
278
+ ) => {
279
+ if (
280
+ (mock && params.response_format === "slots") ||
281
+ (mock && typeof params.start_interval === "undefined")
282
+ ) {
283
+ return mocks.availabilitySlots;
284
+ }
285
+ if (mock && params.response_format === "overlapping_slots") {
286
+ return mocks.availabilityOverlappingSlots(
287
+ params.start_interval.minutes,
288
+ params.required_duration.minutes,
289
+ params.query_periods,
290
+ tzid
291
+ );
292
+ }
293
+ if (mock) return mocks.availability;
294
+
295
+ return fetch(`${api_domain}/v1/sequenced_availability?et=${token}`, {
296
+ method: "POST",
297
+ headers: {
298
+ "Cronofy-Element": `v${packageDetails.version}, ${element}`,
299
+ "Content-Type": "application/json; charset=utf-8",
300
+ },
301
+ body: JSON.stringify(params),
302
+ })
303
+ .then(handleInvalidResponses)
304
+ .then(res => res.json())
305
+ .then(handle422Responses)
306
+ .catch(catchAndRethrowErrors);
307
+ };
308
+
271
309
  export const getAvailabilityRules = ({
272
310
  token,
273
311
  api_domain,
@@ -1,3 +1,5 @@
1
+ import moment from "moment-timezone";
2
+
1
3
  import {
2
4
  parseConnectionDomains,
3
5
  parseQuery,
@@ -8,6 +10,12 @@ import {
8
10
  validateLocaleModifiers,
9
11
  } from "./init";
10
12
  import { logConstructor } from "./logging";
13
+ import { queryForDateTimePicker } from "./mocks";
14
+
15
+ import {
16
+ getMonthsFromQuery,
17
+ parseQuery as parseWithOverlappingSlots,
18
+ } from "../components/DateTimePicker/utils/slots";
11
19
 
12
20
  export const parseDateTimePickerOptions = (options = {}) => {
13
21
  const config = typeof options.config === "undefined" ? {} : options.config;
@@ -64,12 +72,20 @@ export const parseDateTimePickerOptions = (options = {}) => {
64
72
  const target = parseTarget(options, "date-time-picker", log);
65
73
 
66
74
  const isBookableEventsQuery = options.availability_query.bookable_events ? true : false;
75
+ const isSequencedAvailabilityQuery = options.availability_query.sequence ? true : false;
67
76
 
68
77
  let query;
69
- if (isBookableEventsQuery) {
78
+ if (options.demo) {
79
+ query = queryForDateTimePicker;
80
+ } else if (isBookableEventsQuery) {
81
+ query = options.availability_query;
82
+ query = parseWithOverlappingSlots(query);
83
+ } else if (isSequencedAvailabilityQuery) {
70
84
  query = options.availability_query;
85
+ query = parseWithOverlappingSlots(query);
71
86
  } else {
72
87
  query = parseQuery({ options, elementSlug: "date-time-picker", log });
88
+ query = parseWithOverlappingSlots(query);
73
89
  }
74
90
 
75
91
  const tzid = parseTimezone(options.tzid, "date-time-picker", log);
@@ -93,6 +109,44 @@ export const parseDateTimePickerOptions = (options = {}) => {
93
109
  });
94
110
  }
95
111
 
112
+ let selectedDate = config.selected_date;
113
+ if (typeof selectedDate !== "undefined") {
114
+ const validDate = moment(selectedDate, "YYYY-MM-DD", true).isValid();
115
+
116
+ if (!validDate) {
117
+ log.warn(
118
+ `The provided date ${selectedDate} is not valid. Please ensure it's formatted like "YYYY-MM-DD". Picking the first available date as starting date.`,
119
+ {
120
+ docsSlug: "#config.start_date",
121
+ }
122
+ );
123
+
124
+ selectedDate = undefined;
125
+ }
126
+ }
127
+
128
+ const months = getMonthsFromQuery(query?.query_periods, tzid.tzid);
129
+ const selectedDateMoment = selectedDate && moment(selectedDate, "YYYY-MM-DD");
130
+
131
+ let startDateMoment = months.length
132
+ ? moment(months[0], "YYYY-MM").startOf("month")
133
+ : moment().startOf("month");
134
+
135
+ if (selectedDateMoment?.isBefore(startDate)) {
136
+ startDateMoment = selectedDateMoment;
137
+ }
138
+
139
+ let endDateMoment = months.length
140
+ ? moment(months[months.length - 1], "YYYY-MM").endOf("month")
141
+ : moment().endOf("month");
142
+
143
+ if (selectedDateMoment?.isAfter(endDateMoment)) {
144
+ endDateMoment = selectedDateMoment;
145
+ }
146
+
147
+ const startDate = startDateMoment.format("YYYY-MM-DD");
148
+ const endDate = endDateMoment.format("YYYY-MM-DD");
149
+
96
150
  delete options.availability_query;
97
151
  delete options.element_token;
98
152
  delete options.target_id;
@@ -107,7 +161,16 @@ export const parseDateTimePickerOptions = (options = {}) => {
107
161
  token,
108
162
  domains,
109
163
  query,
110
- config: { ...config, mode, logs, startDay, tzList },
164
+ config: {
165
+ ...config,
166
+ mode,
167
+ logs,
168
+ startDay,
169
+ selectedDate,
170
+ startDate,
171
+ endDate,
172
+ tzList,
173
+ },
111
174
  translations,
112
175
  log,
113
176
  };
@@ -4,6 +4,7 @@
4
4
  "confirm": "Confirm",
5
5
  "confirm_slot_title": "Confirm time",
6
6
  "duration_label": "Duration",
7
+ "loading_calendar": "Loading Available Days",
7
8
  "minutes": "minutes",
8
9
  "nav_previous_month": "navigate to previous month",
9
10
  "nav_next_month": "navigate to next month",
@@ -0,0 +1,130 @@
1
+ import React from "react";
2
+ import { render, fireEvent } from "@testing-library/react";
3
+ import "@testing-library/jest-dom";
4
+
5
+ import SequencedSlotButton from "../../src/js/components/DateTimePicker/SequencedSlotButton";
6
+
7
+ import { I18nProvider } from "../../src/js/contexts/i18n-context";
8
+ import { ThemeProvider } from "../../src/js/components/DateTimePicker/contexts/theme-context";
9
+ import { StatusProvider } from "../../src/js/components/DateTimePicker/contexts/status-context";
10
+ import { TzProvider } from "../../src/js/components/DateTimePicker/contexts/tz-context";
11
+
12
+ const wrapper = ({ children, status }) => (
13
+ <ThemeProvider options={{ name: "DTP" }}>
14
+ <I18nProvider
15
+ options={{
16
+ locale: "en",
17
+ slug: "date_time_picker",
18
+ tzid: "Europe/London",
19
+ }}
20
+ >
21
+ <TzProvider
22
+ options={{
23
+ selectedTzid: {
24
+ tzid: "Europe/London",
25
+ offset: "+01:00",
26
+ offsetMins: 60,
27
+ name: "London",
28
+ abbr: "BST",
29
+ },
30
+ list: [],
31
+ }}
32
+ >
33
+ <StatusProvider
34
+ options={{
35
+ selected: false,
36
+ ...status,
37
+ }}
38
+ >
39
+ {children}
40
+ </StatusProvider>
41
+ </TzProvider>
42
+ </I18nProvider>
43
+ </ThemeProvider>
44
+ );
45
+
46
+ describe("SequencedSlotButton", () => {
47
+ const slot = [
48
+ { sequence_id: "test-1", start: "2022-10-05T09:30:00Z", end: "2022-10-05T10:00:00Z" },
49
+ { sequence_id: "test-2", start: "2022-10-05T10:30:00Z", end: "2022-10-05T11:00:00Z" },
50
+ ];
51
+
52
+ it("displays slot button with correct start and end time", () => {
53
+ const { container } = render(<SequencedSlotButton slot={slot} />, { wrapper });
54
+
55
+ const button = container.querySelector(".DTP__time-slot");
56
+
57
+ expect(button).toBeInTheDocument();
58
+ expect(button.innerHTML).toEqual(expect.stringContaining("10:30 AM - 12:00 PM (BST)"));
59
+ });
60
+
61
+ it("displays slot button with time in BST", () => {
62
+ const { container } = render(<SequencedSlotButton slot={slot} />, { wrapper });
63
+
64
+ const button = container.querySelector(".DTP__time-slot");
65
+
66
+ expect(button).toBeInTheDocument();
67
+ expect(button.innerHTML).toEqual(expect.stringContaining("10:30 AM - 12:00 PM (BST)"));
68
+ });
69
+
70
+ it("displays slot button with time in GMT", () => {
71
+ const slot = [
72
+ { sequence_id: "test-1", start: "2022-11-05T09:30:00Z", end: "2022-11-05T10:00:00Z" },
73
+ { sequence_id: "test-2", start: "2022-11-05T10:30:00Z", end: "2022-11-05T11:00:00Z" },
74
+ ];
75
+ const { container } = render(<SequencedSlotButton slot={slot} />, { wrapper });
76
+
77
+ const button = container.querySelector(".DTP__time-slot");
78
+
79
+ expect(button).toBeInTheDocument();
80
+ expect(button.innerHTML).toEqual(expect.stringContaining("9:30 AM - 11:00 AM (GMT)"));
81
+ });
82
+
83
+ it("focuses if slot start time matches status.focusedSlot", () => {
84
+ const status = {
85
+ focusedSlot: "2022-10-05T09:30:00Z",
86
+ selected: false,
87
+ };
88
+
89
+ const { container } = render(<SequencedSlotButton slot={slot} />, {
90
+ wrapper: e => wrapper({ ...e, status }),
91
+ });
92
+
93
+ const button = container.querySelector("button");
94
+
95
+ expect(button).toHaveFocus();
96
+ });
97
+
98
+ it("sets the slot to selected when button is clicked", () => {
99
+ const { container } = render(<SequencedSlotButton slot={slot} />, { wrapper });
100
+
101
+ const button = container.querySelector("button");
102
+
103
+ expect(button).not.toHaveClass("DTP_time-slot--selected");
104
+
105
+ fireEvent.click(button);
106
+
107
+ expect(button).toHaveFocus();
108
+ expect(button.classList).toContain("DTP__time-slot--selected");
109
+ });
110
+
111
+ it("shows selected className if preselected slot exists", () => {
112
+ const status = {
113
+ focusedSlot: "2022-10-05T09:30:00Z",
114
+ selected: {
115
+ start: "2022-10-05T09:30:00Z",
116
+ end: "2022-10-05T11:00:00Z",
117
+ sequence: slot,
118
+ },
119
+ };
120
+
121
+ const { container } = render(<SequencedSlotButton slot={slot} />, {
122
+ wrapper: e => wrapper({ ...e, status }),
123
+ });
124
+
125
+ const button = container.querySelector("button");
126
+
127
+ expect(button).toHaveFocus();
128
+ expect(button).toHaveClass("DTP__time-slot--selected");
129
+ });
130
+ });