@arquimedes.co/eureka-forms 3.0.72-test → 3.0.73-test
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.
- package/dist/BookingManage/BookingManagePage.test.js +0 -1
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.js +3 -1
- package/dist/FormSteps/BookingStep/MaterialBookingStep/MaterialBookingStep.test.js +20 -14
- package/dist/Icons/CheckIcon.d.ts +3 -0
- package/dist/Icons/CheckIcon.js +7 -0
- package/dist/Icons/VideoIcon.d.ts +3 -0
- package/dist/Icons/VideoIcon.js +7 -0
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.d.ts +19 -16
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.js +204 -132
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.module.css +606 -429
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.d.ts +9 -5
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.js +37 -5
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.stories.test.js +18 -15
- package/dist/Shared/ErkBookingPicker/ErkBookingPicker.test.js +178 -52
- package/package.json +1 -1
|
@@ -14,17 +14,17 @@ declare const meta: {
|
|
|
14
14
|
};
|
|
15
15
|
export default meta;
|
|
16
16
|
type Story = StoryObj<typeof meta>;
|
|
17
|
-
/** Full flow: person →
|
|
17
|
+
/** Full flow: person → day and hour → reserve. */
|
|
18
18
|
export declare const WithPersonSelection: Story;
|
|
19
19
|
/** Optional person — adds a "Cualquiera" option. */
|
|
20
20
|
export declare const PersonOptional: Story;
|
|
21
|
-
/** No person selection → straight to
|
|
21
|
+
/** No person selection → straight to day and hour. */
|
|
22
22
|
export declare const NoPerson: Story;
|
|
23
|
-
/** The email comes from an earlier step of the form: the picker only asks for the name. */
|
|
23
|
+
/** The email comes from an earlier step of the form: the field says where the confirmation goes, the picker only asks for the name. */
|
|
24
24
|
export declare const EmailFromStep: Story;
|
|
25
25
|
/** Full config: name and email asked in the picker, lock notice, in-person mode and the organization clock. */
|
|
26
26
|
export declare const Complete: Story;
|
|
27
|
-
/** A hold that lapsed after it was made: the
|
|
27
|
+
/** A hold that lapsed after it was made: the reason shows under the button and the wizard reopens on day and hour. */
|
|
28
28
|
export declare const Lapsed: Story;
|
|
29
29
|
/** Already booked / read-only summary. */
|
|
30
30
|
export declare const ReadOnly: Story;
|
|
@@ -32,5 +32,9 @@ export declare const ReadOnly: Story;
|
|
|
32
32
|
export declare const PickEventTemplate: Story;
|
|
33
33
|
/** Booked with an event template: the summary names it with its duration. */
|
|
34
34
|
export declare const BookedWithEventTemplate: Story;
|
|
35
|
-
/** The offer did not load: the
|
|
35
|
+
/** The offer did not load: the button stays disabled and the field offers "Reintentar". */
|
|
36
36
|
export declare const OfferFailed: Story;
|
|
37
|
+
/** Required and left empty on submit: button and message in the form's error colour. */
|
|
38
|
+
export declare const RequiredError: Story;
|
|
39
|
+
/** A slot held while the form is being finished: the row says until when, and "Cambiar" gives it back. */
|
|
40
|
+
export declare const Reserved: Story;
|
|
@@ -32,7 +32,7 @@ const meta = {
|
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
34
|
export default meta;
|
|
35
|
-
/** Full flow: person →
|
|
35
|
+
/** Full flow: person → day and hour → reserve. */
|
|
36
36
|
export const WithPersonSelection = {
|
|
37
37
|
args: {
|
|
38
38
|
value: null,
|
|
@@ -56,7 +56,7 @@ export const PersonOptional = {
|
|
|
56
56
|
fetchSlots: mockFetchSlots,
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
|
-
/** No person selection → straight to
|
|
59
|
+
/** No person selection → straight to day and hour. */
|
|
60
60
|
export const NoPerson = {
|
|
61
61
|
args: {
|
|
62
62
|
value: null,
|
|
@@ -67,7 +67,7 @@ export const NoPerson = {
|
|
|
67
67
|
fetchSlots: mockFetchSlots,
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
|
-
/** The email comes from an earlier step of the form: the picker only asks for the name. */
|
|
70
|
+
/** The email comes from an earlier step of the form: the field says where the confirmation goes, the picker only asks for the name. */
|
|
71
71
|
export const EmailFromStep = {
|
|
72
72
|
args: {
|
|
73
73
|
value: null,
|
|
@@ -96,7 +96,7 @@ export const Complete = {
|
|
|
96
96
|
fetchSlots: mockFetchSlots,
|
|
97
97
|
},
|
|
98
98
|
};
|
|
99
|
-
/** A hold that lapsed after it was made: the
|
|
99
|
+
/** A hold that lapsed after it was made: the reason shows under the button and the wizard reopens on day and hour. */
|
|
100
100
|
export const Lapsed = {
|
|
101
101
|
args: {
|
|
102
102
|
value: null,
|
|
@@ -164,7 +164,7 @@ export const BookedWithEventTemplate = {
|
|
|
164
164
|
},
|
|
165
165
|
},
|
|
166
166
|
};
|
|
167
|
-
/** The offer did not load: the
|
|
167
|
+
/** The offer did not load: the button stays disabled and the field offers "Reintentar". */
|
|
168
168
|
export const OfferFailed = {
|
|
169
169
|
args: {
|
|
170
170
|
value: null,
|
|
@@ -175,3 +175,35 @@ export const OfferFailed = {
|
|
|
175
175
|
onRetryLoad: () => undefined,
|
|
176
176
|
},
|
|
177
177
|
};
|
|
178
|
+
/** Required and left empty on submit: button and message in the form's error colour. */
|
|
179
|
+
export const RequiredError = {
|
|
180
|
+
args: {
|
|
181
|
+
value: null,
|
|
182
|
+
label: 'Agendar cita',
|
|
183
|
+
description: 'Elige el día y la hora de tu asesoría.',
|
|
184
|
+
required: true,
|
|
185
|
+
durationMinutes: 60,
|
|
186
|
+
bookingMode: BookingMode.VIRTUAL,
|
|
187
|
+
error: 'Selecciona una cita',
|
|
188
|
+
fetchSlots: mockFetchSlots,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
/** A slot held while the form is being finished: the row says until when, and "Cambiar" gives it back. */
|
|
192
|
+
export const Reserved = {
|
|
193
|
+
args: {
|
|
194
|
+
label: 'Agendar cita',
|
|
195
|
+
required: true,
|
|
196
|
+
durationMinutes: 60,
|
|
197
|
+
timeZone: 'America/Bogota',
|
|
198
|
+
knownContact: { email: 'laura@correo.com' },
|
|
199
|
+
value: {
|
|
200
|
+
type: 'BOOKING_VALUE',
|
|
201
|
+
date: '2026-10-30',
|
|
202
|
+
startTime: '08:00',
|
|
203
|
+
endTime: '09:00',
|
|
204
|
+
personName: 'Ana Rodríguez',
|
|
205
|
+
status: 'PENDING',
|
|
206
|
+
lockedUntil: '2026-10-30T13:15:00.000Z',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|
|
@@ -16,32 +16,35 @@ describe('ErkBookingPicker Complete story', () => {
|
|
|
16
16
|
const user = userEvent.setup();
|
|
17
17
|
render(_jsx(Complete, {}));
|
|
18
18
|
// Trigger → modal con el wizard
|
|
19
|
-
|
|
19
|
+
expect(screen.getByText('Te pediremos nombre y correo al reservar.')).toBeInTheDocument();
|
|
20
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
20
21
|
expect(await screen.findByRole('heading', { name: '¿Con quién quieres agendar?' })).toBeInTheDocument();
|
|
22
|
+
const card = screen.getByRole('dialog').firstElementChild;
|
|
23
|
+
const cardClass = card.className;
|
|
21
24
|
// Persona
|
|
22
25
|
await user.click(screen.getByText('María González'));
|
|
23
26
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
24
|
-
// Día
|
|
25
|
-
await screen.
|
|
27
|
+
// Día y hora, en un solo paso (los slots del mock llegan con delay)
|
|
28
|
+
await waitFor(() => expect(screen.getByRole('button', { current: 'step' })).toHaveTextContent('Día y hora'));
|
|
26
29
|
await user.click(clickableToday());
|
|
27
|
-
await
|
|
28
|
-
// Hora (los slots del mock llegan con delay)
|
|
29
|
-
await screen.findByRole('heading', { name: 'Elige una hora' });
|
|
30
|
-
expect(screen.getByText(/^Horas en hora de Bogotá/)).toBeInTheDocument();
|
|
30
|
+
expect(await screen.findByText(/^Hora de Bogotá/)).toBeInTheDocument();
|
|
31
31
|
await user.click(await screen.findByRole('button', { name: /^09:00/ }));
|
|
32
32
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
33
|
-
// Reservar
|
|
34
|
-
await screen.findByRole('heading', { name: '
|
|
33
|
+
// Reservar: misma tarjeta, ahora con los datos
|
|
34
|
+
expect(await screen.findByRole('heading', { name: 'Tus datos' })).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByRole('dialog').firstElementChild).toBe(card);
|
|
36
|
+
expect(card.className).toBe(cardClass);
|
|
35
37
|
await user.type(screen.getByLabelText(/Nombre/), 'Juan Pérez');
|
|
36
38
|
await user.type(screen.getByLabelText(/Correo/), 'juan@correo.com');
|
|
37
39
|
await user.click(screen.getByRole('button', { name: 'Reservar' }));
|
|
38
|
-
// El modal se cierra y queda
|
|
39
|
-
await waitFor(() => expect(screen.queryByRole('heading', { name: '
|
|
40
|
-
expect(screen.getByText(/09:00–09:50
|
|
41
|
-
expect(screen.getByText(
|
|
40
|
+
// El modal se cierra y queda la fila del horario con opción de cambiar
|
|
41
|
+
await waitFor(() => expect(screen.queryByRole('heading', { name: 'Tus datos' })).not.toBeInTheDocument());
|
|
42
|
+
expect(screen.getByText(/ · 09:00 – 09:50$/)).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText('María González · Horario reservado')).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText(/Te confirmaremos a/)).toHaveTextContent('Te confirmaremos a juan@correo.com cuando envíes el formulario.');
|
|
42
45
|
expect(screen.getByRole('button', { name: 'Cambiar' })).toBeInTheDocument();
|
|
43
|
-
// "Cambiar" limpia el valor y vuelve al botón de
|
|
46
|
+
// "Cambiar" limpia el valor y vuelve al botón de elegir
|
|
44
47
|
await user.click(screen.getByRole('button', { name: 'Cambiar' }));
|
|
45
|
-
expect(screen.getByRole('button', { name: '
|
|
48
|
+
expect(screen.getByRole('button', { name: 'Elegir horario' })).toBeInTheDocument();
|
|
46
49
|
});
|
|
47
50
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
@@ -30,24 +30,35 @@ function dayName(date) {
|
|
|
30
30
|
function clickableToday() {
|
|
31
31
|
return screen.getByRole('button', { name: dayName(new Date()) });
|
|
32
32
|
}
|
|
33
|
-
/** A slot button by its start time;
|
|
33
|
+
/** A slot button by its start time; its accessible name is the whole range ("09:00 – 09:50"). */
|
|
34
34
|
function hour(time) {
|
|
35
35
|
return screen.findByRole('button', { name: new RegExp(`^${time}`) });
|
|
36
36
|
}
|
|
37
|
+
/** The step of the stepper on screen. */
|
|
38
|
+
function currentStep() {
|
|
39
|
+
return screen.getByRole('button', { current: 'step' });
|
|
40
|
+
}
|
|
41
|
+
/** Waits until the wizard is on the step with that label. */
|
|
42
|
+
async function onStep(label) {
|
|
43
|
+
await waitFor(() => expect(currentStep()).toHaveTextContent(label));
|
|
44
|
+
}
|
|
45
|
+
/** The modal's card: the dialog's only child. */
|
|
46
|
+
function card() {
|
|
47
|
+
return screen.getByRole('dialog').firstElementChild;
|
|
48
|
+
}
|
|
37
49
|
function renderPicker(props) {
|
|
38
50
|
render(_jsx(MaterialProviders, { formStyle: InternalFormStyle, children: _jsx(ErkBookingPicker, { value: null, onChange: () => { }, label: "Agendar cita", durationMinutes: 50, personSelectionMode: PersonSelectionMode.NONE, ...props }) }));
|
|
39
51
|
}
|
|
40
52
|
/** Opens the wizard and walks to the confirm step with today's 09:00. */
|
|
41
53
|
async function walkToConfirm(user) {
|
|
42
|
-
await user.click(screen.getByRole('button', { name: '
|
|
54
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
43
55
|
await user.click(clickableToday());
|
|
44
|
-
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
45
56
|
await user.click(await hour('09:00'));
|
|
46
57
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
47
|
-
await
|
|
58
|
+
await onStep('Reservar');
|
|
48
59
|
}
|
|
49
60
|
describe('ErkBookingPicker (wizard)', () => {
|
|
50
|
-
it('drives the full flow: person → day
|
|
61
|
+
it('drives the full flow: person → day and hour → reserve, asking for name and email', async () => {
|
|
51
62
|
const user = userEvent.setup();
|
|
52
63
|
const onChange = vi.fn();
|
|
53
64
|
const fetchSlots = vi.fn(() => Promise.resolve(SLOTS));
|
|
@@ -58,28 +69,34 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
58
69
|
askEmail: true,
|
|
59
70
|
fetchSlots,
|
|
60
71
|
});
|
|
61
|
-
|
|
72
|
+
expect(screen.getByText('Te pediremos nombre y correo al reservar.')).toBeInTheDocument();
|
|
73
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
62
74
|
expect(await screen.findByRole('heading', { name: '¿Con quién quieres agendar?' })).toBeInTheDocument();
|
|
63
75
|
const next = screen.getByRole('button', { name: 'Siguiente' });
|
|
64
76
|
expect(next).toBeDisabled();
|
|
65
77
|
await user.click(screen.getByText('María González'));
|
|
78
|
+
expect(screen.getByRole('button', { name: /María González/ })).toHaveAttribute('aria-pressed', 'true');
|
|
66
79
|
expect(next).toBeEnabled();
|
|
67
80
|
await user.click(next);
|
|
68
|
-
|
|
81
|
+
// Day and hour are one step: the hour column waits for a day.
|
|
82
|
+
await onStep('Día y hora');
|
|
83
|
+
expect(screen.getByText('Elige un día para ver sus horas.')).toBeInTheDocument();
|
|
69
84
|
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeDisabled();
|
|
70
85
|
await user.click(clickableToday());
|
|
71
|
-
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
72
|
-
expect(await screen.findByRole('heading', { name: 'Elige una hora' })).toBeInTheDocument();
|
|
73
86
|
expect(fetchSlots).toHaveBeenCalledWith({ personId: 'p-1', date: format(new Date(), 'yyyy-MM-dd') });
|
|
74
|
-
//
|
|
75
|
-
expect(
|
|
76
|
-
expect(screen.getByRole('
|
|
77
|
-
expect(
|
|
78
|
-
expect(screen.queryByText(/
|
|
87
|
+
// One flat list of hours: no part-of-day groups, no "first available" tag.
|
|
88
|
+
expect(await hour('09:00')).toHaveTextContent(/^09:00$/);
|
|
89
|
+
expect(screen.getByRole('button', { name: /^14:00/ })).toBeInTheDocument();
|
|
90
|
+
expect(screen.queryByText('Mañana')).not.toBeInTheDocument();
|
|
91
|
+
expect(screen.queryByText(/Primera disponible/)).not.toBeInTheDocument();
|
|
92
|
+
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeDisabled();
|
|
79
93
|
await user.click(await hour('09:00'));
|
|
94
|
+
// The footer sums up what is picked.
|
|
95
|
+
expect(screen.getByText('09:00 – 09:50', { selector: 'span' })).toBeInTheDocument();
|
|
80
96
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
81
|
-
|
|
82
|
-
expect(screen.
|
|
97
|
+
await onStep('Reservar');
|
|
98
|
+
expect(screen.getByRole('heading', { name: 'Tus datos' })).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByText(/La cita queda confirmada cuando envíes el formulario/)).toBeInTheDocument();
|
|
83
100
|
const reserve = screen.getByRole('button', { name: 'Reservar' });
|
|
84
101
|
await user.type(screen.getByLabelText(/Nombre/), 'Juan Pérez');
|
|
85
102
|
expect(reserve).toBeDisabled();
|
|
@@ -118,6 +135,12 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
118
135
|
await walkToConfirm(user);
|
|
119
136
|
expect(screen.queryByLabelText(/Nombre/)).not.toBeInTheDocument();
|
|
120
137
|
expect(screen.queryByLabelText(/Correo/)).not.toBeInTheDocument();
|
|
138
|
+
// What the form already has is shown, with where it came from.
|
|
139
|
+
expect(screen.getByRole('heading', { name: 'Confirmación' })).toBeInTheDocument();
|
|
140
|
+
expect(within(card()).getByText('Ana Gómez')).toBeInTheDocument();
|
|
141
|
+
expect(screen.getByText('del campo Nombre')).toBeInTheDocument();
|
|
142
|
+
expect(within(card()).getByText('ana@correo.com')).toBeInTheDocument();
|
|
143
|
+
expect(screen.getByText('del campo Correo')).toBeInTheDocument();
|
|
121
144
|
await user.click(screen.getByRole('button', { name: 'Reservar' }));
|
|
122
145
|
expect(holdSlot).toHaveBeenCalledWith({
|
|
123
146
|
slot: SLOTS[0],
|
|
@@ -138,8 +161,8 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
138
161
|
it('skips the person step when personSelectionMode is NONE', async () => {
|
|
139
162
|
const user = userEvent.setup();
|
|
140
163
|
renderPicker({ durationMinutes: 30, fetchSlots: () => Promise.resolve(SLOTS) });
|
|
141
|
-
await user.click(screen.getByRole('button', { name: '
|
|
142
|
-
|
|
164
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
165
|
+
await onStep('Día y hora');
|
|
143
166
|
expect(screen.queryByText('¿Con quién quieres agendar?')).not.toBeInTheDocument();
|
|
144
167
|
expect(screen.queryByText('Persona')).not.toBeInTheDocument();
|
|
145
168
|
});
|
|
@@ -150,11 +173,11 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
150
173
|
persons: PERSONS,
|
|
151
174
|
fetchSlots: () => Promise.resolve(SLOTS),
|
|
152
175
|
});
|
|
153
|
-
await user.click(screen.getByRole('button', { name: '
|
|
176
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
154
177
|
await user.click(await screen.findByText('Carlos Ruiz'));
|
|
155
178
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
156
|
-
|
|
157
|
-
await user.click(screen.getByRole('button', { name:
|
|
179
|
+
await onStep('Día y hora');
|
|
180
|
+
await user.click(screen.getByRole('button', { name: 'Persona' }));
|
|
158
181
|
expect(await screen.findByRole('heading', { name: '¿Con quién quieres agendar?' })).toBeInTheDocument();
|
|
159
182
|
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeEnabled();
|
|
160
183
|
});
|
|
@@ -178,8 +201,8 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
178
201
|
fetchSlots: () => Promise.resolve(SLOTS),
|
|
179
202
|
holdSlot,
|
|
180
203
|
});
|
|
181
|
-
await user.click(screen.getByRole('button', { name: '
|
|
182
|
-
|
|
204
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
205
|
+
await onStep('Día y hora');
|
|
183
206
|
expect(fetchDays).toHaveBeenCalledWith({ personId: undefined });
|
|
184
207
|
const todayButton = clickableToday();
|
|
185
208
|
const enabledDays = screen
|
|
@@ -187,7 +210,7 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
187
210
|
.filter((b) => /^\d{1,2}$/.test(b.textContent ?? '') && !b.disabled);
|
|
188
211
|
expect(enabledDays).toEqual([todayButton]);
|
|
189
212
|
await user.click(todayButton);
|
|
190
|
-
|
|
213
|
+
expect(todayButton).toHaveAttribute('aria-pressed', 'true');
|
|
191
214
|
await user.click(await hour('09:00'));
|
|
192
215
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
193
216
|
await user.click(await screen.findByRole('button', { name: 'Reservar' }));
|
|
@@ -213,7 +236,7 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
213
236
|
await walkToConfirm(user);
|
|
214
237
|
await user.click(await screen.findByRole('button', { name: 'Reservar' }));
|
|
215
238
|
expect(await screen.findByRole('alert')).toHaveTextContent('Ese horario ya fue reservado por alguien más');
|
|
216
|
-
expect(
|
|
239
|
+
expect(currentStep()).toHaveTextContent('Día y hora');
|
|
217
240
|
expect(fetchSlots).toHaveBeenCalledTimes(2);
|
|
218
241
|
expect(onChange).not.toHaveBeenCalled();
|
|
219
242
|
});
|
|
@@ -230,7 +253,7 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
230
253
|
fetchDays: () => Promise.resolve([todayKey, tomorrowKey]),
|
|
231
254
|
fetchSlots,
|
|
232
255
|
});
|
|
233
|
-
await user.click(screen.getByRole('button', { name: '
|
|
256
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
234
257
|
await user.click(await screen.findByRole('button', { name: dayName(new Date()) }));
|
|
235
258
|
if (format(tomorrow, 'M') !== format(new Date(), 'M')) {
|
|
236
259
|
await user.click(screen.getByRole('button', { name: 'Mes siguiente' }));
|
|
@@ -239,7 +262,6 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
239
262
|
await waitFor(() => expect(resolvers[tomorrowKey]).toBeDefined());
|
|
240
263
|
resolvers[tomorrowKey]([{ id: 't-1', startTime: '15:00', endTime: '15:50', available: true }]);
|
|
241
264
|
resolvers[todayKey]([{ id: 'd-1', startTime: '08:00', endTime: '08:50', available: true }]);
|
|
242
|
-
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
243
265
|
expect(await hour('15:00')).toBeInTheDocument();
|
|
244
266
|
expect(screen.queryByRole('button', { name: /^08:00/ })).not.toBeInTheDocument();
|
|
245
267
|
});
|
|
@@ -250,9 +272,8 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
250
272
|
.mockRejectedValueOnce(new Error('Hay muchas consultas en este momento. Intenta de nuevo en unos segundos.'))
|
|
251
273
|
.mockResolvedValueOnce(SLOTS);
|
|
252
274
|
renderPicker({ fetchSlots });
|
|
253
|
-
await user.click(screen.getByRole('button', { name: '
|
|
275
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
254
276
|
await user.click(clickableToday());
|
|
255
|
-
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
256
277
|
expect(await screen.findByRole('alert')).toHaveTextContent('Hay muchas consultas en este momento');
|
|
257
278
|
expect(screen.queryByText('No hay horarios disponibles ese día.')).not.toBeInTheDocument();
|
|
258
279
|
await user.click(screen.getByRole('button', { name: 'Reintentar' }));
|
|
@@ -261,9 +282,8 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
261
282
|
it('still says a day is empty when it really has no hours', async () => {
|
|
262
283
|
const user = userEvent.setup();
|
|
263
284
|
renderPicker({ fetchSlots: () => Promise.resolve([]) });
|
|
264
|
-
await user.click(screen.getByRole('button', { name: '
|
|
285
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
265
286
|
await user.click(clickableToday());
|
|
266
|
-
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
267
287
|
expect(await screen.findByText('No hay horarios disponibles ese día.')).toBeInTheDocument();
|
|
268
288
|
});
|
|
269
289
|
it('tells a failed load of days apart from a person without days, and retries', async () => {
|
|
@@ -274,7 +294,7 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
274
294
|
.mockRejectedValueOnce(new Error('Esa persona ya no está disponible'))
|
|
275
295
|
.mockResolvedValueOnce([today]);
|
|
276
296
|
renderPicker({ fetchDays, fetchSlots: () => Promise.resolve(SLOTS) });
|
|
277
|
-
await user.click(screen.getByRole('button', { name: '
|
|
297
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
278
298
|
expect(await screen.findByRole('alert')).toHaveTextContent('Esa persona ya no está disponible');
|
|
279
299
|
expect(screen.queryByText('No hay días con horarios disponibles.')).not.toBeInTheDocument();
|
|
280
300
|
expect(clickableToday()).toBeDisabled();
|
|
@@ -306,17 +326,16 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
306
326
|
timeZone: local,
|
|
307
327
|
}).format(start);
|
|
308
328
|
renderPicker({ timeZone: zone, fetchSlots: () => Promise.resolve([slot]) });
|
|
309
|
-
await user.click(screen.getByRole('button', { name: '
|
|
329
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
310
330
|
await user.click(clickableToday());
|
|
311
|
-
await
|
|
312
|
-
expect(await screen.findByText(/^Horas en hora de Tokyo|^Horas en hora de Bogotá/)).toBeInTheDocument();
|
|
331
|
+
expect(await screen.findByText(/^Hora de (Tokyo|Bogotá) \(GMT[+-]\d+\) · citas de 50 min$/)).toBeInTheDocument();
|
|
313
332
|
await user.click(await hour(slot.startTime));
|
|
314
|
-
expect(screen.
|
|
333
|
+
expect(screen.getByText(`${slot.startTime} es ${localTime} en tu hora.`)).toBeInTheDocument();
|
|
315
334
|
});
|
|
316
335
|
it('does not show the buffer, an internal detail', async () => {
|
|
317
336
|
const user = userEvent.setup();
|
|
318
337
|
renderPicker({ fetchSlots: () => Promise.resolve(SLOTS) });
|
|
319
|
-
await user.click(screen.getByRole('button', { name: '
|
|
338
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
320
339
|
expect(screen.queryByText(/buffer/)).not.toBeInTheDocument();
|
|
321
340
|
});
|
|
322
341
|
it('sends the requester back to the hours when a held slot lapses', async () => {
|
|
@@ -332,8 +351,8 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
332
351
|
expect(await screen.findByRole('button', { name: 'Cambiar' })).toBeInTheDocument();
|
|
333
352
|
rerender(_jsx(Host, { lapsed: "Tu reserva venci\u00F3; elige la hora de nuevo." }));
|
|
334
353
|
expect(await screen.findByRole('alert')).toHaveTextContent('Tu reserva venció; elige la hora de nuevo.');
|
|
335
|
-
await user.click(screen.getByRole('button', { name: '
|
|
336
|
-
|
|
354
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
355
|
+
await onStep('Día y hora');
|
|
337
356
|
expect(screen.getByRole('alert')).toHaveTextContent('Tu reserva venció; elige la hora de nuevo.');
|
|
338
357
|
// Fresh hours: the lapsed slot may be gone.
|
|
339
358
|
await waitFor(() => expect(fetchSlots).toHaveBeenCalledTimes(2));
|
|
@@ -352,8 +371,9 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
352
371
|
},
|
|
353
372
|
timeZone: 'America/Bogota',
|
|
354
373
|
});
|
|
355
|
-
expect(screen.getByText(
|
|
356
|
-
expect(screen.getByText(
|
|
374
|
+
expect(screen.getByText('Jueves 2 de julio · 11:00 – 11:50')).toBeInTheDocument();
|
|
375
|
+
expect(screen.getByText('María González · Reservado hasta las 10:30')).toBeInTheDocument();
|
|
376
|
+
expect(screen.getByText('Envía el formulario para confirmar la cita.')).toBeInTheDocument();
|
|
357
377
|
expect(screen.getByRole('button', { name: 'Cambiar' })).toBeInTheDocument();
|
|
358
378
|
});
|
|
359
379
|
it('asks for the event template first and narrows days, hours and the hold to it', async () => {
|
|
@@ -370,21 +390,20 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
370
390
|
fetchSlots,
|
|
371
391
|
holdSlot,
|
|
372
392
|
});
|
|
373
|
-
await user.click(screen.getByRole('button', { name: '
|
|
393
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
374
394
|
expect(await screen.findByRole('heading', { name: '¿Qué necesitas?' })).toBeInTheDocument();
|
|
375
|
-
expect(
|
|
395
|
+
expect(currentStep()).toHaveTextContent(/^1Tipo de atención$/);
|
|
376
396
|
// Nothing is asked of the edge before a type is picked: the days depend on it.
|
|
377
397
|
expect(fetchDays).not.toHaveBeenCalled();
|
|
378
398
|
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeDisabled();
|
|
379
399
|
expect(screen.getByText('Primera visita, sin costo.')).toBeInTheDocument();
|
|
380
400
|
await user.click(screen.getByRole('button', { name: /Asesoría de crédito/ }));
|
|
381
401
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
382
|
-
|
|
402
|
+
await onStep('Día y hora');
|
|
383
403
|
expect(fetchDays).toHaveBeenLastCalledWith({ personId: undefined, eventTemplate: 't-2' });
|
|
384
404
|
expect(screen.getByText('45 min')).toBeInTheDocument();
|
|
385
405
|
await user.click(clickableToday());
|
|
386
|
-
await
|
|
387
|
-
expect(await screen.findByText(/citas de 45 min/)).toBeInTheDocument();
|
|
406
|
+
expect(await screen.findByText(/citas de 45 min/i)).toBeInTheDocument();
|
|
388
407
|
expect(fetchSlots).toHaveBeenLastCalledWith({
|
|
389
408
|
personId: undefined,
|
|
390
409
|
date: format(new Date(), 'yyyy-MM-dd'),
|
|
@@ -399,21 +418,24 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
399
418
|
it('drops the day and hour picked for a type when the requester goes back and picks another', async () => {
|
|
400
419
|
const user = userEvent.setup();
|
|
401
420
|
renderPicker({ eventTemplates: TEMPLATES, fetchSlots: () => Promise.resolve(SLOTS) });
|
|
402
|
-
await user.click(screen.getByRole('button', { name: '
|
|
421
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
403
422
|
await user.click(await screen.findByRole('button', { name: /Valoración/ }));
|
|
404
423
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
405
424
|
await user.click(clickableToday());
|
|
425
|
+
await user.click(await hour('09:00'));
|
|
406
426
|
await user.click(screen.getByRole('button', { name: 'Atrás' }));
|
|
407
427
|
await user.click(screen.getByRole('button', { name: /Asesoría de crédito/ }));
|
|
408
428
|
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
409
|
-
|
|
429
|
+
await onStep('Día y hora');
|
|
430
|
+
expect(clickableToday()).toHaveAttribute('aria-pressed', 'false');
|
|
431
|
+
expect(screen.getByText('Elige un día para ver sus horas.')).toBeInTheDocument();
|
|
410
432
|
expect(screen.getByRole('button', { name: 'Siguiente' })).toBeDisabled();
|
|
411
433
|
});
|
|
412
434
|
it('offers no type step with a single event template', async () => {
|
|
413
435
|
const user = userEvent.setup();
|
|
414
436
|
renderPicker({ eventTemplates: [TEMPLATES[0]], fetchSlots: () => Promise.resolve(SLOTS) });
|
|
415
|
-
await user.click(screen.getByRole('button', { name: '
|
|
416
|
-
|
|
437
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
438
|
+
await onStep('Día y hora');
|
|
417
439
|
expect(screen.queryByRole('heading', { name: '¿Qué necesitas?' })).not.toBeInTheDocument();
|
|
418
440
|
});
|
|
419
441
|
it('shows the picked event template and its duration in the compact summary', () => {
|
|
@@ -427,7 +449,7 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
427
449
|
eventTemplate: { name: 'Asesoría de crédito', durationMinutes: 45 },
|
|
428
450
|
},
|
|
429
451
|
});
|
|
430
|
-
expect(screen.getByText('Asesoría de crédito · 45 min')).toBeInTheDocument();
|
|
452
|
+
expect(screen.getByText('Asesoría de crédito · 45 min · Horario reservado')).toBeInTheDocument();
|
|
431
453
|
});
|
|
432
454
|
it('offers to retry when the offer did not load', async () => {
|
|
433
455
|
const user = userEvent.setup();
|
|
@@ -437,4 +459,108 @@ describe('ErkBookingPicker (wizard)', () => {
|
|
|
437
459
|
await user.click(screen.getByRole('button', { name: 'Reintentar' }));
|
|
438
460
|
expect(onRetryLoad).toHaveBeenCalledTimes(1);
|
|
439
461
|
});
|
|
462
|
+
it('keeps one card of the same class and size on every step, final included', async () => {
|
|
463
|
+
const user = userEvent.setup();
|
|
464
|
+
renderPicker({
|
|
465
|
+
eventTemplates: TEMPLATES,
|
|
466
|
+
personSelectionMode: PersonSelectionMode.REQUIRED,
|
|
467
|
+
persons: PERSONS,
|
|
468
|
+
knownContact: { name: 'Ana' },
|
|
469
|
+
fetchSlots: () => Promise.resolve(SLOTS),
|
|
470
|
+
});
|
|
471
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
472
|
+
const first = card();
|
|
473
|
+
const className = first.className;
|
|
474
|
+
expect(className).not.toMatch(/cardFinal/);
|
|
475
|
+
const seen = [];
|
|
476
|
+
const check = () => {
|
|
477
|
+
expect(card()).toBe(first);
|
|
478
|
+
expect(card().className).toBe(className);
|
|
479
|
+
expect(within(card()).queryByRole('complementary')).not.toBeInTheDocument();
|
|
480
|
+
seen.push(currentStep().textContent ?? '');
|
|
481
|
+
};
|
|
482
|
+
check();
|
|
483
|
+
await user.click(screen.getByRole('button', { name: /Valoración/ }));
|
|
484
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
485
|
+
check();
|
|
486
|
+
await user.click(await screen.findByText('María González'));
|
|
487
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
488
|
+
check();
|
|
489
|
+
await user.click(clickableToday());
|
|
490
|
+
await user.click(await hour('09:00'));
|
|
491
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
492
|
+
await onStep('Reservar');
|
|
493
|
+
check();
|
|
494
|
+
expect(seen).toEqual(['1Tipo de atención', '2Persona', '3Día y hora', '4Reservar']);
|
|
495
|
+
});
|
|
496
|
+
it('jumps back from the summary of the final step to the step that sets each row', async () => {
|
|
497
|
+
const user = userEvent.setup();
|
|
498
|
+
renderPicker({
|
|
499
|
+
personSelectionMode: PersonSelectionMode.REQUIRED,
|
|
500
|
+
persons: PERSONS,
|
|
501
|
+
knownContact: { name: 'Ana' },
|
|
502
|
+
temporaryLockMinutes: 15,
|
|
503
|
+
fetchSlots: () => Promise.resolve(SLOTS),
|
|
504
|
+
});
|
|
505
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
506
|
+
await user.click(await screen.findByText('Carlos Ruiz'));
|
|
507
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
508
|
+
await user.click(clickableToday());
|
|
509
|
+
await user.click(await hour('11:00'));
|
|
510
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
511
|
+
await onStep('Reservar');
|
|
512
|
+
expect(screen.getByText('Tu cita')).toBeInTheDocument();
|
|
513
|
+
expect(screen.getByText('11:00 – 11:50 · 50 min')).toBeInTheDocument();
|
|
514
|
+
expect(screen.getByText(/Reservaremos el horario durante/)).toHaveTextContent('Reservaremos el horario durante 15 min. La cita queda confirmada cuando envíes el formulario.');
|
|
515
|
+
await user.click(screen.getByRole('button', { name: 'Cambiar el día y la hora' }));
|
|
516
|
+
await onStep('Día y hora');
|
|
517
|
+
// The selection survives the trip back.
|
|
518
|
+
expect(screen.getByRole('button', { name: /^11:00/ })).toHaveAttribute('aria-pressed', 'true');
|
|
519
|
+
await user.click(screen.getByRole('button', { name: 'Siguiente' }));
|
|
520
|
+
await user.click(await screen.findByRole('button', { name: 'Cambiar la persona' }));
|
|
521
|
+
await onStep('Persona');
|
|
522
|
+
expect(screen.getByRole('button', { name: /Carlos Ruiz/ })).toHaveAttribute('aria-pressed', 'true');
|
|
523
|
+
});
|
|
524
|
+
it('sums up the choices in the footer, read out politely', async () => {
|
|
525
|
+
const user = userEvent.setup();
|
|
526
|
+
renderPicker({ fetchSlots: () => Promise.resolve(SLOTS) });
|
|
527
|
+
await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
|
|
528
|
+
const footer = screen.getByText('Elige un día.').parentElement;
|
|
529
|
+
expect(footer).toHaveAttribute('aria-live', 'polite');
|
|
530
|
+
await user.click(clickableToday());
|
|
531
|
+
expect(footer).toHaveTextContent('Elige una hora.');
|
|
532
|
+
await user.click(await hour('09:00'));
|
|
533
|
+
expect(footer).toHaveTextContent(`${format(new Date(), 'EEE d MMM', { locale: es })}09:00 – 09:50`.replace(/^./, (c) => c.toUpperCase()));
|
|
534
|
+
});
|
|
535
|
+
it('shows the label as a form field, with the asterisk when required and no card', () => {
|
|
536
|
+
renderPicker({ required: true, description: 'Elige el día que prefieras.' });
|
|
537
|
+
expect(screen.getByText('Agendar cita *')).toBeInTheDocument();
|
|
538
|
+
expect(screen.getByText('Elige el día que prefieras.')).toBeInTheDocument();
|
|
539
|
+
expect(screen.getByRole('button', { name: 'Elegir horario' })).toBeEnabled();
|
|
540
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
541
|
+
});
|
|
542
|
+
it('says where the confirmation goes when the email comes from an earlier step', () => {
|
|
543
|
+
renderPicker({ knownContact: { email: 'laura@correo.com' } });
|
|
544
|
+
expect(screen.getByText(/Te confirmaremos a/)).toHaveTextContent('Te confirmaremos a laura@correo.com cuando envíes el formulario.');
|
|
545
|
+
});
|
|
546
|
+
it('shows the required error under the button', () => {
|
|
547
|
+
renderPicker({ required: true, error: 'Selecciona una cita' });
|
|
548
|
+
expect(screen.getByRole('alert')).toHaveTextContent('Selecciona una cita');
|
|
549
|
+
});
|
|
550
|
+
it('shows a read-only appointment as confirmed, with nothing to change', () => {
|
|
551
|
+
renderPicker({
|
|
552
|
+
readOnly: true,
|
|
553
|
+
value: {
|
|
554
|
+
type: 'BOOKING_VALUE',
|
|
555
|
+
date: '2026-07-02',
|
|
556
|
+
startTime: '11:00',
|
|
557
|
+
endTime: '11:50',
|
|
558
|
+
personName: 'María González',
|
|
559
|
+
status: 'CONFIRMED',
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
expect(screen.getByText('Jueves 2 de julio · 11:00 – 11:50')).toBeInTheDocument();
|
|
563
|
+
expect(screen.getByText('María González · Cita confirmada')).toBeInTheDocument();
|
|
564
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
565
|
+
});
|
|
440
566
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arquimedes.co/eureka-forms",
|
|
3
3
|
"repository": "git://github.com/Arquimede5/Eureka-Forms.git",
|
|
4
|
-
"version":"3.0.
|
|
4
|
+
"version":"3.0.73-test",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"watch": "node node_modules/@typescript/native/bin/tsc --noEmit --watch --project tsconfig.app.json",
|
|
7
7
|
"start": "vite",
|