cronofy-elements 1.42.1 → 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.
@@ -64,10 +64,83 @@
64
64
  return output.utc().format();
65
65
  };
66
66
 
67
+ const sequencedAvailabilityQuery = {
68
+ sequence: [
69
+ {
70
+ sequence_id: "test",
71
+ ordinal: 1,
72
+ participants: [
73
+ {
74
+ required: "all",
75
+ members: [
76
+ {
77
+ sub: "<%= sub %>",
78
+ // managed_availability: true,
79
+ // availability_rule_ids: ["weekly_work_hours"]
80
+ }
81
+ ]
82
+ }
83
+ ],
84
+ required_duration: { minutes: 5 },
85
+ },
86
+ {
87
+ sequence_id: "test-1",
88
+ ordinal: 2,
89
+ participants: [
90
+ {
91
+ required: "all",
92
+ members: [
93
+ {
94
+ sub: "<%= sub %>",
95
+ // managed_availability: true,
96
+ // availability_rule_ids: ["weekly_work_hours"]
97
+ }
98
+ ]
99
+ }
100
+ ],
101
+ required_duration: { minutes: 45 },
102
+ start_interval: { minutes: 5 },
103
+ buffer: {
104
+ before: {
105
+ minimum: { minutes: 15 }
106
+ }
107
+ }
108
+ },
109
+ {
110
+ sequence_id: "test-2",
111
+ ordinal: 3,
112
+ participants: [
113
+ {
114
+ required: "all",
115
+ members: [
116
+ {
117
+ sub: "<%= sub %>",
118
+ // managed_availability: true,
119
+ // availability_rule_ids: ["weekly_work_hours"]
120
+ }
121
+ ]
122
+ }
123
+ ],
124
+ required_duration: { minutes: 30 },
125
+ start_interval: { minutes: 10 },
126
+ buffer: {
127
+ before: {
128
+ minimum: { minutes: 15 }
129
+ }
130
+ }
131
+ },
132
+
133
+ ],
134
+ query_periods: [
135
+ { start: slotTimes(01,"08:00"), end: slotTimes(31,"17:00") }
136
+ ]
137
+ }
138
+
67
139
  CronofyElements.DateTimePicker({
68
140
  element_token: "<%= date_time_picker_token %>",
69
141
  target_id: 'cronofy-date-time-picker',
70
142
  api_domain:"<%= api_domain %>",
143
+ //availability_query: sequencedAvailabilityQuery,
71
144
  availability_query: {
72
145
  // response_format: "slots",
73
146
  participants: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cronofy-elements",
3
- "version": "1.42.1",
3
+ "version": "1.43.0",
4
4
  "description": "Fast track scheduling with Cronofy's embeddable UI Elements",
5
5
  "main": "build/npm/CronofyElements.js",
6
6
  "scripts": {
@@ -6,6 +6,7 @@ import {
6
6
  parseTzList,
7
7
  getInitialSelectedTzid,
8
8
  getMonthsLoadingFromQuery,
9
+ getDurationFromQuery,
9
10
  } from "./utils/slots";
10
11
  import { getMonthsInDisplay, parseTimeSlots } from "./utils/calendar";
11
12
 
@@ -30,6 +31,8 @@ const DateTimePicker = ({ options }) => {
30
31
  selectedDateObject?.format("YYYY-MM")
31
32
  );
32
33
 
34
+ const duration = getDurationFromQuery(options.query);
35
+
33
36
  const endDateObject = moment(options.config.endDate, "YYYY-MM-DD");
34
37
  const startDateObject = moment(options.config.startDate, "YYYY-MM-DD");
35
38
  const currentMonthObject = selectedDateObject ?? startDateObject;
@@ -61,6 +64,7 @@ const DateTimePicker = ({ options }) => {
61
64
  : () => options.log.info("No `callback` option has been provided"),
62
65
  columnView: "loading", // loading | error | slots | confirm | no-slots
63
66
  daySlots: [],
67
+ duration,
64
68
  locale: options.locale,
65
69
  mode: options.config.mode, // confirm (default) | no_confirm
66
70
  query: options.query,
@@ -75,6 +79,7 @@ const DateTimePicker = ({ options }) => {
75
79
  endDateObject,
76
80
  focusedSlot: false,
77
81
  availableDays: [],
82
+ sequenced_availability: options.query.sequence ? true : false,
78
83
  slots: {},
79
84
  slotFetchCount: 0,
80
85
  slotInjectionPoint: undefined,
@@ -17,9 +17,11 @@ const Details = ({ duration, locale }) => {
17
17
  <TimeZoneSelector locale={locale} />
18
18
  </div>
19
19
  <div className={theme.classBuilder("details--duration")}>
20
- <p>
21
- <strong>{i18n.t("duration_label")}:</strong> {duration} {i18n.t("minutes")}
22
- </p>
20
+ {duration && (
21
+ <p>
22
+ <strong>{i18n.t("duration_label")}:</strong> {duration} {i18n.t("minutes")}
23
+ </p>
24
+ )}
23
25
  </div>
24
26
  </div>
25
27
  );
@@ -0,0 +1,71 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import moment from "moment-timezone";
3
+
4
+ import { useI18n } from "../../contexts/i18n-context";
5
+ import { useStatus } from "./contexts/status-context";
6
+ import { useTheme } from "./contexts/theme-context";
7
+ import { useTz } from "./contexts/tz-context";
8
+
9
+ const SequencedSlotButton = ({ slot }) => {
10
+ const i18n = useI18n();
11
+ const theme = useTheme();
12
+ const [status, dispatchStatus] = useStatus();
13
+ const [tz] = useTz();
14
+
15
+ const slotButtonRef = useRef();
16
+
17
+ const startArray = slot.map(a => a.start);
18
+ const endArray = slot.map(a => a.end);
19
+
20
+ const start = startArray.reduce((prev, current) => {
21
+ return prev < current ? prev : current;
22
+ });
23
+
24
+ const end = endArray.reduce((prev, current) => {
25
+ return prev > current ? prev : current;
26
+ });
27
+
28
+ const handleSlotSelection = slot => {
29
+ const selectedSlot = {
30
+ start,
31
+ end,
32
+ sequence: slot,
33
+ };
34
+ dispatchStatus({ type: "SELECT_SLOT", slot: selectedSlot, tzid: tz.selectedTzid.tzid });
35
+ };
36
+
37
+ useEffect(() => {
38
+ if (status.focusedSlot === start) {
39
+ slotButtonRef.current.focus();
40
+ }
41
+ }, [status.focusedSlot]);
42
+
43
+ let classStub = "time-slot";
44
+ if (status.selected.start === start) classStub = classStub + " time-slot--selected";
45
+
46
+ return (
47
+ <button
48
+ className={theme.classBuilder(classStub)}
49
+ onClick={() => handleSlotSelection(slot)}
50
+ ref={slotButtonRef}
51
+ >
52
+ <span className={theme.classBuilder("visually-hidden")}>
53
+ {i18n.t("select_time_slot")}
54
+ </span>
55
+ {i18n.customFormatedTimeZone(
56
+ moment(start, "YYYY-MM-DDTHH:mm:00Z"),
57
+ tz.selectedTzid.tzid,
58
+ "LT"
59
+ )}
60
+ {" - "}
61
+ {i18n.customFormatedTimeZone(
62
+ moment(end, "YYYY-MM-DDTHH:mm:00Z"),
63
+ tz.selectedTzid.tzid,
64
+ "LT"
65
+ )}{" "}
66
+ {`(${moment(start, "YYYY-MM-DDTHH:mm:00Z").tz(tz.selectedTzid.tzid).format("z")})`}
67
+ </button>
68
+ );
69
+ };
70
+
71
+ export default SequencedSlotButton;
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
 
3
3
  import SlotButton from "./SlotButton";
4
+ import SequencedSlotButton from "./SequencedSlotButton";
4
5
 
5
6
  import { useI18n } from "../../contexts/i18n-context";
6
7
  import { useStatus } from "./contexts/status-context";
@@ -14,7 +15,11 @@ const SlotsList = () => {
14
15
  const renderSlots = status.daySlots.map((slot, key) => {
15
16
  return (
16
17
  <li key={key} className={theme.classBuilder("slot-list--item")}>
17
- <SlotButton slot={slot} />
18
+ {status.sequenced_availability ? (
19
+ <SequencedSlotButton slot={slot} />
20
+ ) : (
21
+ <SlotButton slot={slot} />
22
+ )}
18
23
  </li>
19
24
  );
20
25
  });
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useRef, useState } from "react";
2
2
  import moment from "moment";
3
3
 
4
- import { getSlots } from "./utils/slots";
4
+ import { getSequencedSlots, getSlots } from "./utils/slots";
5
5
 
6
6
  import Calendar from "./Calendar";
7
7
  import Confirm from "./Confirm";
@@ -31,26 +31,20 @@ const Wrapper = () => {
31
31
  }
32
32
 
33
33
  const fetchMonthSlots = (query, month) => {
34
- return getSlots({
34
+ const fetch = status.sequenced_availability ? getSequencedSlots : getSlots;
35
+
36
+ return fetch({
35
37
  query,
36
38
  auth: status.auth,
37
39
  tzid: status.tzid,
38
- })
39
- .then(res => {
40
- dispatchStatus({
41
- type: "SET_SLOTS",
42
- slots: res,
43
- tzid: tz.selectedTzid.tzid,
44
- month: month,
45
- });
46
- })
47
- .catch(res => {
48
- dispatchStatus({
49
- type: "ERROR_LOADING_SLOTS",
50
- error: res,
51
- month: month,
52
- });
40
+ }).then(res => {
41
+ dispatchStatus({
42
+ type: "SET_SLOTS",
43
+ slots: res,
44
+ tzid: tz.selectedTzid.tzid,
45
+ month: month,
53
46
  });
47
+ });
54
48
  };
55
49
 
56
50
  const currentMonth = status.months.find(m => m.current);
@@ -60,7 +54,15 @@ const Wrapper = () => {
60
54
  .then(() => {
61
55
  // Get slots for remianing months
62
56
  const remainingMonths = status.months.filter(month => !month.current);
63
- remainingMonths.forEach(month => fetchMonthSlots(month.query, month.month));
57
+ remainingMonths.forEach(month =>
58
+ fetchMonthSlots(month.query, month.month).catch(res => {
59
+ dispatchStatus({
60
+ type: "ERROR_LOADING_SLOTS",
61
+ error: res,
62
+ month: month.month,
63
+ });
64
+ })
65
+ );
64
66
  })
65
67
  .catch(error => {
66
68
  dispatchStatus({ type: "ERROR_GETTING_SLOTS", error });
@@ -172,7 +174,7 @@ const Wrapper = () => {
172
174
 
173
175
  return (
174
176
  <section className={theme.classBuilder()} style={theme.customProperties}>
175
- <Details duration={status.query.required_duration.minutes} locale={status.locale} />
177
+ <Details duration={status.duration} locale={status.locale} />
176
178
  <div className={theme.classBuilder("wrapper")}>
177
179
  <div
178
180
  className={
@@ -6,6 +6,7 @@ import {
6
6
  getAvailableDays,
7
7
  getLocalDayFromUtc,
8
8
  removeMonthFromLoading,
9
+ addSequencedSlotsToObject,
9
10
  } from "../utils/slots";
10
11
  import { getMonthsInDisplay, parseTimeSlots } from "../utils/calendar";
11
12
 
@@ -43,6 +44,7 @@ export const statusReducer = (state, action) => {
43
44
  notification: {
44
45
  type: "error",
45
46
  message: "There was an error getting the slots",
47
+ body: action.error.body,
46
48
  },
47
49
  };
48
50
  state.callback(notification);
@@ -99,18 +101,23 @@ export const statusReducer = (state, action) => {
99
101
  };
100
102
  }
101
103
 
102
- const slotsObject = addSlotsToObject(state.slots, action.slots);
103
- const availableDays = getAvailableDays(addSlotsToObject({}, action.slots), action.tzid);
104
- const availableDaysSet = new Set([...state.availableDays, ...availableDays]);
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);
105
108
  const monthsLoading = removeMonthFromLoading(state.monthsLoading, action.month);
106
109
 
110
+ const injectionPoint = state.sequenced_availability
111
+ ? getLocalDayFromUtc(action.slots[0].sequence[0].start, action.tzid)
112
+ : getLocalDayFromUtc(action.slots[0].start, action.tzid);
113
+
107
114
  return {
108
115
  ...state,
109
- availableDays: [...availableDaysSet].sort(),
116
+ availableDays,
110
117
  monthsLoading,
111
118
  slots: slotsObject,
112
119
  slotFetchCount: state.slotFetchCount + 1,
113
- slotInjectionPoint: getLocalDayFromUtc(action.slots[0].start, action.tzid),
120
+ slotInjectionPoint: injectionPoint,
114
121
  };
115
122
  }
116
123
 
@@ -123,13 +130,17 @@ export const statusReducer = (state, action) => {
123
130
 
124
131
  case "SELECT_DAY": {
125
132
  const daySlots = getSlotsByDay(state.slots, action.day, action.tzid);
126
- const focusedSlot = daySlots[0];
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;
127
138
 
128
139
  return {
129
140
  ...state,
130
141
  selectedDay: action.day,
131
142
  focusedDay: action.day,
132
- focusedSlot: focusedSlot?.start,
143
+ focusedSlot: focusedSlot,
133
144
  columnView: "slots",
134
145
  daySlots,
135
146
  populated: true,
@@ -1,6 +1,6 @@
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
5
  import { errorMessages } from "../../../helpers/logging";
6
6
 
@@ -33,6 +33,12 @@ export const getMonthsFromQuery = (periods = [], tzid) => {
33
33
  return uniqueMonths;
34
34
  };
35
35
 
36
+ export const getDurationFromQuery = query => {
37
+ const duration = query.sequence ? false : query.required_duration.minutes;
38
+
39
+ return duration;
40
+ };
41
+
36
42
  export const getMonthsLoadingFromQuery = (query, tzid) => {
37
43
  if (!query.query_periods || !query.query_periods.length) {
38
44
  return [
@@ -104,7 +110,7 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
104
110
  tzid,
105
111
  auth.demo
106
112
  ).then(res => {
107
- if (res.errors) {
113
+ if (res.status === 422) {
108
114
  throw {
109
115
  type: 422,
110
116
  message: errorMessages[422].message,
@@ -142,6 +148,53 @@ export const getSlots = ({ query, auth, tzid, slots = [] }) =>
142
148
  });
143
149
  });
144
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
+
145
198
  export const parseQuery = query => {
146
199
  if (!query.bookable_events) {
147
200
  return {
@@ -156,6 +209,8 @@ export const parseSlotsResult = res => {
156
209
  let returnedSlots;
157
210
  if (typeof res.available_bookable_events !== "undefined") {
158
211
  returnedSlots = res.available_bookable_events;
212
+ } else if (typeof res.sequences !== "undefined") {
213
+ returnedSlots = res.sequences;
159
214
  } else {
160
215
  returnedSlots = res.available_slots;
161
216
 
@@ -209,6 +264,17 @@ export const addSlotsToObject = (slotsObject, newSlotsArray) => {
209
264
  return slotsObject;
210
265
  };
211
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
+
212
278
  export const getSlotsByDay = (slots, day, tzid) => {
213
279
  const slotKeys = Object.keys(slots);
214
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,
@@ -72,6 +72,7 @@ export const parseDateTimePickerOptions = (options = {}) => {
72
72
  const target = parseTarget(options, "date-time-picker", log);
73
73
 
74
74
  const isBookableEventsQuery = options.availability_query.bookable_events ? true : false;
75
+ const isSequencedAvailabilityQuery = options.availability_query.sequence ? true : false;
75
76
 
76
77
  let query;
77
78
  if (options.demo) {
@@ -79,6 +80,9 @@ export const parseDateTimePickerOptions = (options = {}) => {
79
80
  } else if (isBookableEventsQuery) {
80
81
  query = options.availability_query;
81
82
  query = parseWithOverlappingSlots(query);
83
+ } else if (isSequencedAvailabilityQuery) {
84
+ query = options.availability_query;
85
+ query = parseWithOverlappingSlots(query);
82
86
  } else {
83
87
  query = parseQuery({ options, elementSlug: "date-time-picker", log });
84
88
  query = parseWithOverlappingSlots(query);
@@ -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
+ });