@arquimedes.co/eureka-forms 3.0.72 → 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.
@@ -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 → date → time. */
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 date + time. */
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 requester is sent back to the hours with the reason. */
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 entry card stays disabled and offers "Reintentar". */
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 → date → time. */
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 date + time. */
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 requester is sent back to the hours with the reason. */
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 entry card stays disabled and offers "Reintentar". */
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
- await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
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.findByRole('heading', { name: 'Elige un día' });
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 user.click(screen.getByRole('button', { name: 'Siguiente' }));
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: 'Revisa y reserva tu horario' });
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 el resumen compacto con opción de cambiar
39
- await waitFor(() => expect(screen.queryByRole('heading', { name: 'Revisa y reserva tu horario' })).not.toBeInTheDocument());
40
- expect(screen.getByText(/09:00–09:50 · con María González/)).toBeInTheDocument();
41
- expect(screen.getByText(/Envía el formulario para confirmar la cita/)).toBeInTheDocument();
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 agendar
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: 'Agendar cita' })).toBeInTheDocument();
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; the first one of the day also reads "Primera disponible". */
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: 'Agendar cita' }));
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 screen.findByRole('heading', { name: 'Revisa y reserva tu horario' });
58
+ await onStep('Reservar');
48
59
  }
49
60
  describe('ErkBookingPicker (wizard)', () => {
50
- it('drives the full flow: person → day → hour → reserve, asking for name and email', async () => {
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
- await user.click(screen.getByRole('button', { name: 'Agendar cita' }));
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
- expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
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
- // Grouped by part of the day, with the first one flagged; nothing is shown as taken.
75
- expect(screen.getByRole('heading', { name: 'Mañana' })).toBeInTheDocument();
76
- expect(screen.getByRole('heading', { name: 'Tarde' })).toBeInTheDocument();
77
- expect(await hour('09:00')).toHaveTextContent('Primera disponible');
78
- expect(screen.queryByText(/Tachados/)).not.toBeInTheDocument();
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
- expect(await screen.findByRole('heading', { name: 'Revisa y reserva tu horario' })).toBeInTheDocument();
82
- expect(screen.getByText(/La cita se confirma cuando envíes el formulario/)).toBeInTheDocument();
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: 'Agendar cita' }));
142
- expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
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: 'Agendar cita' }));
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
- expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
157
- await user.click(screen.getByRole('button', { name: /Persona/ }));
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: 'Agendar cita' }));
182
- expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
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
- await user.click(screen.getByRole('button', { name: 'Siguiente' }));
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(screen.getByRole('heading', { name: 'Elige una hora' })).toBeInTheDocument();
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: 'Agendar cita' }));
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: 'Agendar cita' }));
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: 'Agendar cita' }));
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: 'Agendar cita' }));
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: 'Agendar cita' }));
329
+ await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
310
330
  await user.click(clickableToday());
311
- await user.click(screen.getByRole('button', { name: 'Siguiente' }));
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.getAllByText(new RegExp(`\\(${localTime} en tu hora\\)`)).length).toBeGreaterThan(0);
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: 'Agendar cita' }));
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: 'Agendar cita' }));
336
- expect(await screen.findByRole('heading', { name: 'Elige una hora' })).toBeInTheDocument();
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(/11:00–11:50 · con María González/)).toBeInTheDocument();
356
- expect(screen.getByText(/Reservado hasta las 10:30\. Envía el formulario para confirmar la cita\./)).toBeInTheDocument();
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: 'Agendar cita' }));
393
+ await user.click(screen.getByRole('button', { name: 'Elegir horario' }));
374
394
  expect(await screen.findByRole('heading', { name: '¿Qué necesitas?' })).toBeInTheDocument();
375
- expect(screen.getByRole('button', { name: /^1Tipo/ })).toBeInTheDocument();
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
- expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
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 user.click(screen.getByRole('button', { name: 'Siguiente' }));
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: 'Agendar cita' }));
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
- expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
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: 'Agendar cita' }));
416
- expect(await screen.findByRole('heading', { name: 'Elige un día' })).toBeInTheDocument();
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.72",
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",