@getmicdrop/svelte-components 2.0.4 → 2.0.6

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.
Files changed (107) hide show
  1. package/dist/components/Alert/Alert.spec.js +170 -170
  2. package/dist/components/Badges/Badge.spec.js +103 -103
  3. package/dist/components/BottomSheet/BottomSheet.spec.js +127 -127
  4. package/dist/components/Breadcrumb/Breadcrumb.spec.js +120 -120
  5. package/dist/components/Button/Button.spec.js +211 -211
  6. package/dist/components/Button/ButtonSaveDemo.spec.js +48 -48
  7. package/dist/components/Calendar/Calendar.spec.js +131 -131
  8. package/dist/components/Calendar/QuarterView.spec.js +394 -394
  9. package/dist/components/Card.spec.js +47 -47
  10. package/dist/components/CropImage/CropImage.spec.js +216 -216
  11. package/dist/components/DarkModeToggle.spec.js +357 -357
  12. package/dist/components/ErrorDisplay.spec.js +69 -69
  13. package/dist/components/FormActions.spec.js +88 -88
  14. package/dist/components/FormValidationSummary.spec.js +203 -203
  15. package/dist/components/Icons/Icon.spec.js +175 -175
  16. package/dist/components/Icons/MoreHori.spec.js +67 -67
  17. package/dist/components/Icons/WarningIcon.spec.js +30 -30
  18. package/dist/components/Input/Input.spec.js +573 -573
  19. package/dist/components/Input/MultiSelect.spec.js +257 -257
  20. package/dist/components/Input/OTPInput.spec.js +238 -238
  21. package/dist/components/Input/Select.spec.js +218 -218
  22. package/dist/components/Layout/BottomNav.spec.js +130 -130
  23. package/dist/components/Layout/Header.spec.js +203 -203
  24. package/dist/components/Modal/ConfirmationModal.spec.js +191 -191
  25. package/dist/components/Modal/Modal.spec.js +95 -95
  26. package/dist/components/Modal/ModalStateManager.spec.js +100 -100
  27. package/dist/components/PageLoader.spec.js +54 -54
  28. package/dist/components/PasswordStrengthIndicator/PasswordStrengthIndicator.spec.js +173 -173
  29. package/dist/components/PlaceAutocomplete/PlaceAutocomplete.spec.js +300 -300
  30. package/dist/components/Spinner/Spinner.spec.js +75 -75
  31. package/dist/components/StatusIndicator/StatusIndicator.spec.js +129 -129
  32. package/dist/components/Toaster/Toaster.stories.svelte +1 -1
  33. package/dist/components/Toggle.spec.js +158 -158
  34. package/dist/components/ValidationError.spec.js +79 -79
  35. package/dist/components/pages/performers/AvailabilityCalendarModal.spec.js +606 -606
  36. package/dist/components/pages/performers/AvailabilityCalendarModal.svelte +4 -4
  37. package/dist/components/pages/performers/ModalShowInfo.spec.js +124 -124
  38. package/dist/components/pages/performers/ModalShowInfo.svelte +1 -1
  39. package/dist/components/pages/performers/PageBackButton.spec.js +89 -89
  40. package/dist/components/pages/performers/SectionHeader.spec.js +75 -75
  41. package/dist/components/pages/performers/ShowDetails.spec.js +166 -166
  42. package/dist/components/pages/performers/ShowItemCard.spec.js +793 -793
  43. package/dist/components/pages/performers/ShowItemCard.svelte +4 -4
  44. package/dist/components/pages/performers/SwitchOption.spec.js +127 -127
  45. package/dist/components/pages/performers/VenueInfo.spec.js +167 -167
  46. package/dist/components/pages/performers/VenueInfo.svelte +1 -1
  47. package/dist/components/pages/performers/VenueItemCard.spec.js +763 -763
  48. package/dist/components/pages/performers/VenueItemCard.svelte +4 -4
  49. package/dist/components/pages/profile/profile-form.spec.js +9 -9
  50. package/dist/components/pages/settings/tabs/CustomImageDropzone.svelte +3 -3
  51. package/dist/components/pages/shows/ShowList.spec.js +33 -33
  52. package/dist/components/pages/shows/TabContent.spec.js +90 -90
  53. package/dist/components/pages/shows/TabNavigation.spec.js +143 -143
  54. package/dist/config.js +5 -5
  55. package/dist/config.spec.js +29 -29
  56. package/dist/constants/formOptions.js +25 -25
  57. package/dist/constants/formOptions.spec.js +88 -88
  58. package/dist/index.js +111 -111
  59. package/dist/stores/auth.d.ts +9 -0
  60. package/dist/stores/auth.d.ts.map +1 -0
  61. package/dist/stores/auth.js +36 -0
  62. package/dist/stores/auth.spec.d.ts +2 -0
  63. package/dist/stores/auth.spec.d.ts.map +1 -0
  64. package/dist/stores/auth.spec.js +139 -0
  65. package/dist/stores/formDataStore.d.ts +17 -0
  66. package/dist/stores/formDataStore.d.ts.map +1 -0
  67. package/dist/stores/formDataStore.js +25 -0
  68. package/dist/stores/formDataStore.spec.d.ts +2 -0
  69. package/dist/stores/formDataStore.spec.d.ts.map +1 -0
  70. package/dist/stores/formDataStore.spec.js +257 -0
  71. package/dist/stores/formSave.d.ts +24 -0
  72. package/dist/stores/formSave.d.ts.map +1 -0
  73. package/dist/stores/formSave.js +132 -0
  74. package/dist/stores/formSave.spec.d.ts +2 -0
  75. package/dist/stores/formSave.spec.d.ts.map +1 -0
  76. package/dist/stores/formSave.spec.js +296 -0
  77. package/dist/stores/index.d.ts +1 -0
  78. package/dist/stores/index.d.ts.map +1 -0
  79. package/dist/stores/index.js +0 -0
  80. package/dist/stores/navigation.d.ts +5 -0
  81. package/dist/stores/navigation.d.ts.map +1 -0
  82. package/dist/stores/navigation.js +12 -0
  83. package/dist/stores/navigation.spec.d.ts +2 -0
  84. package/dist/stores/navigation.spec.d.ts.map +1 -0
  85. package/dist/stores/navigation.spec.js +136 -0
  86. package/dist/stores/toaster.d.ts +4 -0
  87. package/dist/stores/toaster.d.ts.map +1 -0
  88. package/dist/stores/toaster.js +13 -0
  89. package/dist/stores/toaster.spec.d.ts +2 -0
  90. package/dist/stores/toaster.spec.d.ts.map +1 -0
  91. package/dist/stores/toaster.spec.js +59 -0
  92. package/dist/telemetry.js +357 -357
  93. package/dist/telemetry.server.js +211 -211
  94. package/dist/telemetry.server.spec.js +434 -434
  95. package/dist/telemetry.spec.js +660 -660
  96. package/dist/utils/apiConfig.js +49 -49
  97. package/dist/utils/apiConfig.spec.js +118 -118
  98. package/dist/utils/greetings.js +187 -187
  99. package/dist/utils/greetings.spec.js +337 -337
  100. package/dist/utils/imageValidation.js +121 -121
  101. package/dist/utils/imageValidation.spec.js +220 -220
  102. package/dist/utils/portal.js +25 -25
  103. package/dist/utils/portal.spec.js +143 -143
  104. package/dist/utils/utils/utils.js +323 -323
  105. package/dist/utils/utils/utils.spec.js +698 -698
  106. package/dist/utils/utils.spec.js +643 -643
  107. package/package.json +1 -1
@@ -1,763 +1,763 @@
1
- import { describe, it, expect, vi, beforeEach, beforeAll, afterEach } from 'vitest';
2
- import { render, fireEvent, waitFor, cleanup } from '@testing-library/svelte';
3
-
4
- // Mock IntersectionObserver
5
- vi.stubGlobal('IntersectionObserver', vi.fn().mockImplementation(() => ({
6
- observe: vi.fn(),
7
- disconnect: vi.fn(),
8
- unobserve: vi.fn(),
9
- })));
10
-
11
- // Mock $app modules
12
- vi.mock('$app/navigation', () => ({
13
- goto: vi.fn(),
14
- }));
15
-
16
- vi.mock('$app/stores', () => ({
17
- page: {
18
- subscribe: vi.fn((callback) => {
19
- callback({ url: new URL('http://localhost:3000/availability') });
20
- return () => {};
21
- }),
22
- },
23
- }));
24
-
25
- vi.mock('$app/environment', () => ({
26
- browser: true,
27
- }));
28
-
29
- // Mock stores
30
- vi.mock('@/stores/toaster', () => ({
31
- showToast: vi.fn(),
32
- }));
33
-
34
- // Mock utils
35
- vi.mock('@/utils/utils', () => ({
36
- getPerformerToken: vi.fn(() => 'mock-token'),
37
- microphonePlaceholder: '/placeholder.jpg',
38
- parseLocation: vi.fn((location) => ({
39
- street: location ? '123 Test St' : '',
40
- cityStateZip: location ? 'City, ST 12345' : '',
41
- })),
42
- getUserDetails: vi.fn(() => ({ firstName: 'Test', lastName: 'User' })),
43
- classNames: vi.fn((...args) => args.filter(Boolean).join(' ')),
44
- }));
45
-
46
- // Mock Modal to always render its slots (for testing modal content)
47
- // This is scoped to this test file only
48
- vi.mock('@/components/Modal/Modal.svelte', async () => {
49
- const MockModal = await import('../../../__mocks__/Modal.svelte');
50
- return { default: MockModal.default };
51
- });
52
-
53
-
54
- vi.mock('@/utils/utils/utils', () => ({
55
- timeAgo: vi.fn(() => '2 hours ago'),
56
- }));
57
-
58
- vi.mock('@/utils/apiConfig', () => ({
59
- buildApiUrl: vi.fn((endpoint) => `http://localhost:8080${endpoint}`),
60
- API_ENDPOINTS: {
61
- REMOVE_VENUE_FROM_ROSTER: '/api/performer/removeVenueFromRoster',
62
- },
63
- }));
64
-
65
- // Mock fetch globally
66
- global.fetch = vi.fn();
67
-
68
- // Mock window.location.reload
69
- const mockReload = vi.fn();
70
- Object.defineProperty(window, 'location', {
71
- value: { reload: mockReload },
72
- writable: true,
73
- });
74
-
75
- describe('VenueItemCard', () => {
76
- let VenueItemCard;
77
- let showToast;
78
-
79
- const defaultProps = {
80
- venueId: 1,
81
- name: 'Test Venue',
82
- image: '/test-image.jpg',
83
- location: '123 Test St, City',
84
- start: '2025-01-01',
85
- end: '2025-12-31',
86
- doorOpen: '6:00 PM',
87
- email: 'test@venue.com',
88
- phone: '555-1234',
89
- description: 'A great venue',
90
- instagram: '@testvenue',
91
- facebook: 'testvenue',
92
- twitter: '@testvenue',
93
- inviteID: 123,
94
- lastUpdated: '2025-06-01T12:00:00Z',
95
- timeRange: '6:00 PM - 2:00 AM',
96
- dateRange: 'Jan 1 - Dec 31',
97
- status: 'upcoming',
98
- invitationAccepted: true,
99
- hasAvailability: true,
100
- onUpdateClick: vi.fn(),
101
- };
102
-
103
- beforeAll(async () => {
104
- vi.resetModules();
105
- const toasterModule = await import('../../../../stores/toaster');
106
- showToast = toasterModule.showToast;
107
- VenueItemCard = (await import('./VenueItemCard.svelte')).default;
108
- }, 60000);
109
-
110
- beforeEach(() => {
111
- vi.clearAllMocks();
112
- global.fetch.mockReset();
113
- mockReload.mockReset();
114
- });
115
-
116
- afterEach(() => {
117
- cleanup();
118
- });
119
-
120
- describe('Basic Rendering', () => {
121
- it('renders the venue card with basic info', async () => {
122
- const { container } = render(VenueItemCard, { props: defaultProps });
123
- expect(container.querySelector('.rounded-lg')).toBeDefined();
124
- });
125
-
126
- it('displays venue name through VenueInfo component', async () => {
127
- const { container } = render(VenueItemCard, { props: defaultProps });
128
- expect(container).toBeDefined();
129
- });
130
-
131
- it('renders with all props', async () => {
132
- const { container } = render(VenueItemCard, { props: defaultProps });
133
- expect(container.innerHTML).toBeDefined();
134
- });
135
- });
136
-
137
- describe('Status: Upcoming with Availability', () => {
138
- it('shows Update button when status is upcoming and has availability', async () => {
139
- const { container } = render(VenueItemCard, {
140
- props: {
141
- ...defaultProps,
142
- status: 'upcoming',
143
- hasAvailability: true,
144
- invitationAccepted: true,
145
- },
146
- });
147
-
148
- await waitFor(() => {
149
- const updateButton = container.querySelector('button');
150
- expect(updateButton).toBeDefined();
151
- });
152
- });
153
-
154
- it('shows last updated text', async () => {
155
- const { container } = render(VenueItemCard, {
156
- props: {
157
- ...defaultProps,
158
- status: 'upcoming',
159
- hasAvailability: true,
160
- invitationAccepted: true,
161
- },
162
- });
163
-
164
- await waitFor(() => {
165
- expect(container.innerHTML).toContain('Last updated');
166
- });
167
- });
168
-
169
- it('calls onUpdateClick when Update button is clicked', async () => {
170
- const mockUpdateClick = vi.fn();
171
- const { container } = render(VenueItemCard, {
172
- props: {
173
- ...defaultProps,
174
- status: 'upcoming',
175
- hasAvailability: true,
176
- invitationAccepted: true,
177
- onUpdateClick: mockUpdateClick,
178
- },
179
- });
180
-
181
- await waitFor(() => {
182
- const buttons = container.querySelectorAll('button');
183
- const updateButton = Array.from(buttons).find(
184
- (btn) => btn.textContent?.includes('Update')
185
- );
186
- if (updateButton) {
187
- fireEvent.click(updateButton);
188
- }
189
- });
190
-
191
- // The onUpdateClick should have been called
192
- expect(mockUpdateClick).toBeDefined();
193
- });
194
- });
195
-
196
- describe('Status: Upcoming without Availability', () => {
197
- it('shows Set up button when invitation accepted but no availability', async () => {
198
- const { container } = render(VenueItemCard, {
199
- props: {
200
- ...defaultProps,
201
- status: 'upcoming',
202
- hasAvailability: false,
203
- invitationAccepted: true,
204
- },
205
- });
206
-
207
- await waitFor(() => {
208
- expect(container.innerHTML).toContain('Set up');
209
- });
210
- });
211
-
212
- it('shows warning message when needs availability update', async () => {
213
- const { container } = render(VenueItemCard, {
214
- props: {
215
- ...defaultProps,
216
- status: 'upcoming',
217
- hasAvailability: false,
218
- invitationAccepted: true,
219
- },
220
- });
221
-
222
- await waitFor(() => {
223
- expect(container.innerHTML).toContain('Please update your availability');
224
- });
225
- });
226
-
227
- it('calls onUpdateClick when Set up button is clicked', async () => {
228
- const mockUpdateClick = vi.fn();
229
- const { container } = render(VenueItemCard, {
230
- props: {
231
- ...defaultProps,
232
- status: 'upcoming',
233
- hasAvailability: false,
234
- invitationAccepted: true,
235
- onUpdateClick: mockUpdateClick,
236
- },
237
- });
238
-
239
- await waitFor(() => {
240
- const buttons = container.querySelectorAll('button');
241
- const setupButton = Array.from(buttons).find(
242
- (btn) => btn.textContent?.includes('Set up')
243
- );
244
- if (setupButton) {
245
- fireEvent.click(setupButton);
246
- }
247
- });
248
- });
249
- });
250
-
251
- describe('Status: Other States', () => {
252
- it('does not show buttons when status is not upcoming', async () => {
253
- const { container } = render(VenueItemCard, {
254
- props: {
255
- ...defaultProps,
256
- status: 'past',
257
- hasAvailability: true,
258
- invitationAccepted: true,
259
- },
260
- });
261
-
262
- await waitFor(() => {
263
- // Should not show the Update or Set up sections
264
- expect(container.innerHTML).not.toContain('Last updated');
265
- });
266
- });
267
-
268
- it('does not show buttons when invitation not accepted', async () => {
269
- const { container } = render(VenueItemCard, {
270
- props: {
271
- ...defaultProps,
272
- status: 'upcoming',
273
- hasAvailability: false,
274
- invitationAccepted: false,
275
- },
276
- });
277
-
278
- await waitFor(() => {
279
- // Should not show the availability sections
280
- expect(container.innerHTML).not.toContain('Please update your availability');
281
- });
282
- });
283
- });
284
-
285
- describe('Leave Venue Modal', () => {
286
- it('renders the dropdown trigger icon', async () => {
287
- const { container } = render(VenueItemCard, {
288
- props: {
289
- ...defaultProps,
290
- status: 'upcoming',
291
- hasAvailability: true,
292
- invitationAccepted: true,
293
- },
294
- });
295
-
296
- // The dropdown trigger is present
297
- expect(container.querySelector('[data-testid="dropdown-menu"]')).toBeDefined();
298
- });
299
-
300
- it('has Update button visible', async () => {
301
- const { container } = render(VenueItemCard, {
302
- props: {
303
- ...defaultProps,
304
- status: 'upcoming',
305
- hasAvailability: true,
306
- invitationAccepted: true,
307
- },
308
- });
309
-
310
- expect(container.innerHTML).toContain('Update');
311
- });
312
-
313
- it('has more-menu class on dropdown trigger', async () => {
314
- const { container } = render(VenueItemCard, {
315
- props: {
316
- ...defaultProps,
317
- status: 'upcoming',
318
- hasAvailability: true,
319
- invitationAccepted: true,
320
- },
321
- });
322
-
323
- const menuTrigger = container.querySelector('.more-menu');
324
- expect(menuTrigger).toBeDefined();
325
- });
326
- });
327
-
328
- describe('Leave Venue API', () => {
329
- it('calls API when leave is confirmed - success', async () => {
330
- global.fetch.mockResolvedValueOnce({
331
- ok: true,
332
- json: async () => ({ success: true }),
333
- });
334
-
335
- const { container } = render(VenueItemCard, {
336
- props: {
337
- ...defaultProps,
338
- status: 'upcoming',
339
- hasAvailability: true,
340
- invitationAccepted: true,
341
- },
342
- });
343
-
344
- // Find and click Leave button (the one in the modal footer)
345
- const buttons = container.querySelectorAll('button');
346
- const leaveButton = Array.from(buttons).find(
347
- (btn) => btn.textContent?.trim() === 'Leave'
348
- );
349
-
350
- if (leaveButton) {
351
- await fireEvent.click(leaveButton);
352
-
353
- await waitFor(() => {
354
- expect(global.fetch).toHaveBeenCalledWith(
355
- expect.stringContaining('/api/performer/removeVenueFromRoster'),
356
- expect.objectContaining({
357
- method: 'DELETE',
358
- headers: expect.objectContaining({
359
- 'Content-Type': 'application/json',
360
- }),
361
- })
362
- );
363
- });
364
- }
365
- });
366
-
367
- it('shows success toast on successful leave', async () => {
368
- global.fetch.mockResolvedValueOnce({
369
- ok: true,
370
- json: async () => ({ success: true }),
371
- });
372
-
373
- const { container } = render(VenueItemCard, {
374
- props: {
375
- ...defaultProps,
376
- status: 'upcoming',
377
- hasAvailability: true,
378
- invitationAccepted: true,
379
- },
380
- });
381
-
382
- const buttons = container.querySelectorAll('button');
383
- const leaveButton = Array.from(buttons).find(
384
- (btn) => btn.textContent?.trim() === 'Leave'
385
- );
386
-
387
- if (leaveButton) {
388
- await fireEvent.click(leaveButton);
389
-
390
- await waitFor(() => {
391
- expect(showToast).toHaveBeenCalledWith(
392
- expect.stringContaining('You have left'),
393
- 'success'
394
- );
395
- });
396
- }
397
- });
398
-
399
- it('shows error toast on failed leave', async () => {
400
- global.fetch.mockResolvedValueOnce({
401
- ok: false,
402
- json: async () => ({ error: 'Failed' }),
403
- });
404
-
405
- const { container } = render(VenueItemCard, {
406
- props: {
407
- ...defaultProps,
408
- status: 'upcoming',
409
- hasAvailability: true,
410
- invitationAccepted: true,
411
- },
412
- });
413
-
414
- const buttons = container.querySelectorAll('button');
415
- const leaveButton = Array.from(buttons).find(
416
- (btn) => btn.textContent?.trim() === 'Leave'
417
- );
418
-
419
- if (leaveButton) {
420
- await fireEvent.click(leaveButton);
421
-
422
- await waitFor(() => {
423
- expect(showToast).toHaveBeenCalledWith(
424
- expect.stringContaining('Failed to leave'),
425
- 'error'
426
- );
427
- });
428
- }
429
- });
430
-
431
- it('reloads page after successful leave', async () => {
432
- global.fetch.mockResolvedValueOnce({
433
- ok: true,
434
- json: async () => ({ success: true }),
435
- });
436
-
437
- const { container } = render(VenueItemCard, {
438
- props: {
439
- ...defaultProps,
440
- status: 'upcoming',
441
- hasAvailability: true,
442
- invitationAccepted: true,
443
- },
444
- });
445
-
446
- const buttons = container.querySelectorAll('button');
447
- const leaveButton = Array.from(buttons).find(
448
- (btn) => btn.textContent?.trim() === 'Leave'
449
- );
450
-
451
- if (leaveButton) {
452
- await fireEvent.click(leaveButton);
453
-
454
- await waitFor(() => {
455
- expect(mockReload).toHaveBeenCalled();
456
- });
457
- }
458
- });
459
- });
460
-
461
- describe('Card Structure', () => {
462
- it('has main container with border and rounded corners', async () => {
463
- const { container } = render(VenueItemCard, {
464
- props: {
465
- ...defaultProps,
466
- status: 'upcoming',
467
- hasAvailability: true,
468
- invitationAccepted: true,
469
- },
470
- });
471
-
472
- const mainDiv = container.querySelector('.rounded-lg');
473
- expect(mainDiv).toBeDefined();
474
- });
475
- });
476
-
477
- describe('Props Handling', () => {
478
- it('renders with minimal required props', async () => {
479
- const minimalProps = {
480
- venueId: 1,
481
- name: 'Minimal Venue',
482
- status: 'upcoming',
483
- invitationAccepted: false,
484
- hasAvailability: false,
485
- };
486
-
487
- const { container } = render(VenueItemCard, { props: minimalProps });
488
- expect(container).toBeDefined();
489
- });
490
-
491
- it('handles undefined optional props', async () => {
492
- const propsWithUndefined = {
493
- venueId: 1,
494
- name: 'Test Venue',
495
- image: undefined,
496
- location: undefined,
497
- status: 'upcoming',
498
- invitationAccepted: true,
499
- hasAvailability: true,
500
- };
501
-
502
- const { container } = render(VenueItemCard, { props: propsWithUndefined });
503
- expect(container).toBeDefined();
504
- });
505
-
506
- it('handles null optional props', async () => {
507
- const propsWithNull = {
508
- venueId: 1,
509
- name: 'Test Venue',
510
- image: null,
511
- location: null,
512
- email: null,
513
- phone: null,
514
- status: 'upcoming',
515
- invitationAccepted: true,
516
- hasAvailability: true,
517
- };
518
-
519
- const { container } = render(VenueItemCard, { props: propsWithNull });
520
- expect(container).toBeDefined();
521
- });
522
-
523
- it('handles empty string props', async () => {
524
- const propsWithEmpty = {
525
- venueId: 1,
526
- name: '',
527
- image: '',
528
- location: '',
529
- description: '',
530
- status: 'upcoming',
531
- invitationAccepted: true,
532
- hasAvailability: true,
533
- };
534
-
535
- const { container } = render(VenueItemCard, { props: propsWithEmpty });
536
- expect(container).toBeDefined();
537
- });
538
- });
539
-
540
- describe('Dropdown Menu', () => {
541
- it('renders dropdown trigger icon', async () => {
542
- const { container } = render(VenueItemCard, {
543
- props: {
544
- ...defaultProps,
545
- status: 'upcoming',
546
- hasAvailability: true,
547
- invitationAccepted: true,
548
- },
549
- });
550
-
551
- const dropdownTrigger = container.querySelector('[data-testid="dropdown-menu"]');
552
- expect(dropdownTrigger).toBeDefined();
553
- });
554
-
555
- it('renders dropdown icon with correct classes', async () => {
556
- const { container } = render(VenueItemCard, {
557
- props: {
558
- ...defaultProps,
559
- status: 'upcoming',
560
- hasAvailability: true,
561
- invitationAccepted: true,
562
- },
563
- });
564
-
565
- const menuTrigger = container.querySelector('.more-menu');
566
- expect(menuTrigger).not.toBeNull();
567
- });
568
- });
569
-
570
- describe('Without Token', () => {
571
- it('handles API call without token', async () => {
572
- const { getPerformerToken } = await import('../../../../utils/utils');
573
- getPerformerToken.mockReturnValueOnce(null);
574
-
575
- global.fetch.mockResolvedValueOnce({
576
- ok: true,
577
- json: async () => ({ success: true }),
578
- });
579
-
580
- const { container } = render(VenueItemCard, {
581
- props: {
582
- ...defaultProps,
583
- status: 'upcoming',
584
- hasAvailability: true,
585
- invitationAccepted: true,
586
- },
587
- });
588
-
589
- const buttons = container.querySelectorAll('button');
590
- const leaveButton = Array.from(buttons).find(
591
- (btn) => btn.textContent?.trim() === 'Leave'
592
- );
593
-
594
- // With the Modal mock, the Leave button is rendered
595
- expect(leaveButton).toBeDefined();
596
-
597
- if (leaveButton) {
598
- await fireEvent.click(leaveButton);
599
- // Give time for async operations
600
- await new Promise(resolve => setTimeout(resolve, 100));
601
- }
602
- });
603
- });
604
-
605
- describe('Modal Interactions', () => {
606
- it('Cancel button closes the modal', async () => {
607
- const { container } = render(VenueItemCard, {
608
- props: {
609
- ...defaultProps,
610
- status: 'upcoming',
611
- hasAvailability: true,
612
- invitationAccepted: true,
613
- },
614
- });
615
-
616
- // Find Cancel button in the modal footer
617
- const buttons = container.querySelectorAll('button');
618
- const cancelButton = Array.from(buttons).find(
619
- (btn) => btn.textContent?.trim() === 'Cancel'
620
- );
621
-
622
- if (cancelButton) {
623
- await fireEvent.click(cancelButton);
624
- // Modal should close after cancel
625
- expect(container).toBeDefined();
626
- }
627
- });
628
-
629
- it('handles note input in leave venue modal', async () => {
630
- const { container } = render(VenueItemCard, {
631
- props: {
632
- ...defaultProps,
633
- status: 'upcoming',
634
- hasAvailability: true,
635
- invitationAccepted: true,
636
- },
637
- });
638
-
639
- // Find textarea for notes
640
- const textarea = container.querySelector('textarea');
641
- if (textarea) {
642
- await fireEvent.input(textarea, { target: { value: 'Test note for leaving' } });
643
- expect(textarea.value).toBe('Test note for leaving');
644
- }
645
- });
646
-
647
- it('Leave button triggers API call from modal', async () => {
648
- global.fetch.mockResolvedValueOnce({
649
- ok: true,
650
- json: async () => ({ success: true }),
651
- });
652
-
653
- const { container } = render(VenueItemCard, {
654
- props: {
655
- ...defaultProps,
656
- status: 'upcoming',
657
- hasAvailability: true,
658
- invitationAccepted: true,
659
- },
660
- });
661
-
662
- // Modal only shows Leave button when showLeaveVenue is true
663
- // Look for modal buttons that might be in the DOM
664
- await waitFor(() => {
665
- const buttons = container.querySelectorAll('button');
666
- const leaveButton = Array.from(buttons).find(
667
- (btn) => btn.textContent?.trim() === 'Leave'
668
- );
669
-
670
- if (leaveButton) {
671
- fireEvent.click(leaveButton);
672
- }
673
- });
674
- });
675
- });
676
-
677
- describe('Dropdown Menu Interactions', () => {
678
- it('dropdown icon is clickable', async () => {
679
- const { container } = render(VenueItemCard, {
680
- props: {
681
- ...defaultProps,
682
- status: 'upcoming',
683
- hasAvailability: true,
684
- invitationAccepted: true,
685
- },
686
- });
687
-
688
- const dropdownIcon = container.querySelector('[data-testid="dropdown-menu"]');
689
- expect(dropdownIcon).toBeDefined();
690
- if (dropdownIcon) {
691
- await fireEvent.click(dropdownIcon);
692
- // Dropdown should be rendered
693
- expect(container).toBeDefined();
694
- }
695
- });
696
-
697
- it('shows dropdown for red warning state', async () => {
698
- const { container } = render(VenueItemCard, {
699
- props: {
700
- ...defaultProps,
701
- status: 'upcoming',
702
- hasAvailability: false,
703
- invitationAccepted: true,
704
- },
705
- });
706
-
707
- const dropdownIcon = container.querySelector('[data-testid="dropdown-menu"]');
708
- expect(dropdownIcon).toBeDefined();
709
- });
710
-
711
- it('shows dropdown for normal update state', async () => {
712
- const { container } = render(VenueItemCard, {
713
- props: {
714
- ...defaultProps,
715
- status: 'upcoming',
716
- hasAvailability: true,
717
- invitationAccepted: true,
718
- },
719
- });
720
-
721
- const dropdownIcon = container.querySelector('[data-testid="dropdown-menu"]');
722
- expect(dropdownIcon).toBeDefined();
723
- });
724
- });
725
-
726
- describe('Toggle Modal Function', () => {
727
- it('handles non-leave modal name', async () => {
728
- const { container } = render(VenueItemCard, {
729
- props: {
730
- ...defaultProps,
731
- status: 'upcoming',
732
- hasAvailability: true,
733
- invitationAccepted: true,
734
- },
735
- });
736
-
737
- // The toggleModal function should handle unknown modal names gracefully
738
- expect(container).toBeDefined();
739
- });
740
- });
741
-
742
- describe('Styling', () => {
743
- it('has correct card container classes', async () => {
744
- const { container } = render(VenueItemCard, { props: defaultProps });
745
-
746
- const card = container.querySelector('.rounded-lg');
747
- expect(card).toBeDefined();
748
- });
749
-
750
- it('applies red styling when needs availability update', async () => {
751
- const { container } = render(VenueItemCard, {
752
- props: {
753
- ...defaultProps,
754
- status: 'upcoming',
755
- hasAvailability: false,
756
- invitationAccepted: true,
757
- },
758
- });
759
-
760
- expect(container.innerHTML).toContain('red-50');
761
- });
762
- });
763
- });
1
+ import { describe, it, expect, vi, beforeEach, beforeAll, afterEach } from 'vitest';
2
+ import { render, fireEvent, waitFor, cleanup } from '@testing-library/svelte';
3
+
4
+ // Mock IntersectionObserver
5
+ vi.stubGlobal('IntersectionObserver', vi.fn().mockImplementation(() => ({
6
+ observe: vi.fn(),
7
+ disconnect: vi.fn(),
8
+ unobserve: vi.fn(),
9
+ })));
10
+
11
+ // Mock $app modules
12
+ vi.mock('$app/navigation', () => ({
13
+ goto: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('$app/stores', () => ({
17
+ page: {
18
+ subscribe: vi.fn((callback) => {
19
+ callback({ url: new URL('http://localhost:3000/availability') });
20
+ return () => {};
21
+ }),
22
+ },
23
+ }));
24
+
25
+ vi.mock('$app/environment', () => ({
26
+ browser: true,
27
+ }));
28
+
29
+ // Mock stores
30
+ vi.mock('@/stores/toaster', () => ({
31
+ showToast: vi.fn(),
32
+ }));
33
+
34
+ // Mock utils
35
+ vi.mock('@/utils/utils', () => ({
36
+ getPerformerToken: vi.fn(() => 'mock-token'),
37
+ microphonePlaceholder: '/placeholder.jpg',
38
+ parseLocation: vi.fn((location) => ({
39
+ street: location ? '123 Test St' : '',
40
+ cityStateZip: location ? 'City, ST 12345' : '',
41
+ })),
42
+ getUserDetails: vi.fn(() => ({ firstName: 'Test', lastName: 'User' })),
43
+ classNames: vi.fn((...args) => args.filter(Boolean).join(' ')),
44
+ }));
45
+
46
+ // Mock Modal to always render its slots (for testing modal content)
47
+ // This is scoped to this test file only
48
+ vi.mock('@/components/Modal/Modal.svelte', async () => {
49
+ const MockModal = await import('../../../__mocks__/Modal.svelte');
50
+ return { default: MockModal.default };
51
+ });
52
+
53
+
54
+ vi.mock('@/utils/utils/utils', () => ({
55
+ timeAgo: vi.fn(() => '2 hours ago'),
56
+ }));
57
+
58
+ vi.mock('@/utils/apiConfig', () => ({
59
+ buildApiUrl: vi.fn((endpoint) => `http://localhost:8080${endpoint}`),
60
+ API_ENDPOINTS: {
61
+ REMOVE_VENUE_FROM_ROSTER: '/api/performer/removeVenueFromRoster',
62
+ },
63
+ }));
64
+
65
+ // Mock fetch globally
66
+ global.fetch = vi.fn();
67
+
68
+ // Mock window.location.reload
69
+ const mockReload = vi.fn();
70
+ Object.defineProperty(window, 'location', {
71
+ value: { reload: mockReload },
72
+ writable: true,
73
+ });
74
+
75
+ describe('VenueItemCard', () => {
76
+ let VenueItemCard;
77
+ let showToast;
78
+
79
+ const defaultProps = {
80
+ venueId: 1,
81
+ name: 'Test Venue',
82
+ image: '/test-image.jpg',
83
+ location: '123 Test St, City',
84
+ start: '2025-01-01',
85
+ end: '2025-12-31',
86
+ doorOpen: '6:00 PM',
87
+ email: 'test@venue.com',
88
+ phone: '555-1234',
89
+ description: 'A great venue',
90
+ instagram: '@testvenue',
91
+ facebook: 'testvenue',
92
+ twitter: '@testvenue',
93
+ inviteID: 123,
94
+ lastUpdated: '2025-06-01T12:00:00Z',
95
+ timeRange: '6:00 PM - 2:00 AM',
96
+ dateRange: 'Jan 1 - Dec 31',
97
+ status: 'upcoming',
98
+ invitationAccepted: true,
99
+ hasAvailability: true,
100
+ onUpdateClick: vi.fn(),
101
+ };
102
+
103
+ beforeAll(async () => {
104
+ vi.resetModules();
105
+ const toasterModule = await import('../../../../stores/toaster');
106
+ showToast = toasterModule.showToast;
107
+ VenueItemCard = (await import('./VenueItemCard.svelte')).default;
108
+ }, 60000);
109
+
110
+ beforeEach(() => {
111
+ vi.clearAllMocks();
112
+ global.fetch.mockReset();
113
+ mockReload.mockReset();
114
+ });
115
+
116
+ afterEach(() => {
117
+ cleanup();
118
+ });
119
+
120
+ describe('Basic Rendering', () => {
121
+ it('renders the venue card with basic info', async () => {
122
+ const { container } = render(VenueItemCard, { props: defaultProps });
123
+ expect(container.querySelector('.rounded-lg')).toBeDefined();
124
+ });
125
+
126
+ it('displays venue name through VenueInfo component', async () => {
127
+ const { container } = render(VenueItemCard, { props: defaultProps });
128
+ expect(container).toBeDefined();
129
+ });
130
+
131
+ it('renders with all props', async () => {
132
+ const { container } = render(VenueItemCard, { props: defaultProps });
133
+ expect(container.innerHTML).toBeDefined();
134
+ });
135
+ });
136
+
137
+ describe('Status: Upcoming with Availability', () => {
138
+ it('shows Update button when status is upcoming and has availability', async () => {
139
+ const { container } = render(VenueItemCard, {
140
+ props: {
141
+ ...defaultProps,
142
+ status: 'upcoming',
143
+ hasAvailability: true,
144
+ invitationAccepted: true,
145
+ },
146
+ });
147
+
148
+ await waitFor(() => {
149
+ const updateButton = container.querySelector('button');
150
+ expect(updateButton).toBeDefined();
151
+ });
152
+ });
153
+
154
+ it('shows last updated text', async () => {
155
+ const { container } = render(VenueItemCard, {
156
+ props: {
157
+ ...defaultProps,
158
+ status: 'upcoming',
159
+ hasAvailability: true,
160
+ invitationAccepted: true,
161
+ },
162
+ });
163
+
164
+ await waitFor(() => {
165
+ expect(container.innerHTML).toContain('Last updated');
166
+ });
167
+ });
168
+
169
+ it('calls onUpdateClick when Update button is clicked', async () => {
170
+ const mockUpdateClick = vi.fn();
171
+ const { container } = render(VenueItemCard, {
172
+ props: {
173
+ ...defaultProps,
174
+ status: 'upcoming',
175
+ hasAvailability: true,
176
+ invitationAccepted: true,
177
+ onUpdateClick: mockUpdateClick,
178
+ },
179
+ });
180
+
181
+ await waitFor(() => {
182
+ const buttons = container.querySelectorAll('button');
183
+ const updateButton = Array.from(buttons).find(
184
+ (btn) => btn.textContent?.includes('Update')
185
+ );
186
+ if (updateButton) {
187
+ fireEvent.click(updateButton);
188
+ }
189
+ });
190
+
191
+ // The onUpdateClick should have been called
192
+ expect(mockUpdateClick).toBeDefined();
193
+ });
194
+ });
195
+
196
+ describe('Status: Upcoming without Availability', () => {
197
+ it('shows Set up button when invitation accepted but no availability', async () => {
198
+ const { container } = render(VenueItemCard, {
199
+ props: {
200
+ ...defaultProps,
201
+ status: 'upcoming',
202
+ hasAvailability: false,
203
+ invitationAccepted: true,
204
+ },
205
+ });
206
+
207
+ await waitFor(() => {
208
+ expect(container.innerHTML).toContain('Set up');
209
+ });
210
+ });
211
+
212
+ it('shows warning message when needs availability update', async () => {
213
+ const { container } = render(VenueItemCard, {
214
+ props: {
215
+ ...defaultProps,
216
+ status: 'upcoming',
217
+ hasAvailability: false,
218
+ invitationAccepted: true,
219
+ },
220
+ });
221
+
222
+ await waitFor(() => {
223
+ expect(container.innerHTML).toContain('Please update your availability');
224
+ });
225
+ });
226
+
227
+ it('calls onUpdateClick when Set up button is clicked', async () => {
228
+ const mockUpdateClick = vi.fn();
229
+ const { container } = render(VenueItemCard, {
230
+ props: {
231
+ ...defaultProps,
232
+ status: 'upcoming',
233
+ hasAvailability: false,
234
+ invitationAccepted: true,
235
+ onUpdateClick: mockUpdateClick,
236
+ },
237
+ });
238
+
239
+ await waitFor(() => {
240
+ const buttons = container.querySelectorAll('button');
241
+ const setupButton = Array.from(buttons).find(
242
+ (btn) => btn.textContent?.includes('Set up')
243
+ );
244
+ if (setupButton) {
245
+ fireEvent.click(setupButton);
246
+ }
247
+ });
248
+ });
249
+ });
250
+
251
+ describe('Status: Other States', () => {
252
+ it('does not show buttons when status is not upcoming', async () => {
253
+ const { container } = render(VenueItemCard, {
254
+ props: {
255
+ ...defaultProps,
256
+ status: 'past',
257
+ hasAvailability: true,
258
+ invitationAccepted: true,
259
+ },
260
+ });
261
+
262
+ await waitFor(() => {
263
+ // Should not show the Update or Set up sections
264
+ expect(container.innerHTML).not.toContain('Last updated');
265
+ });
266
+ });
267
+
268
+ it('does not show buttons when invitation not accepted', async () => {
269
+ const { container } = render(VenueItemCard, {
270
+ props: {
271
+ ...defaultProps,
272
+ status: 'upcoming',
273
+ hasAvailability: false,
274
+ invitationAccepted: false,
275
+ },
276
+ });
277
+
278
+ await waitFor(() => {
279
+ // Should not show the availability sections
280
+ expect(container.innerHTML).not.toContain('Please update your availability');
281
+ });
282
+ });
283
+ });
284
+
285
+ describe('Leave Venue Modal', () => {
286
+ it('renders the dropdown trigger icon', async () => {
287
+ const { container } = render(VenueItemCard, {
288
+ props: {
289
+ ...defaultProps,
290
+ status: 'upcoming',
291
+ hasAvailability: true,
292
+ invitationAccepted: true,
293
+ },
294
+ });
295
+
296
+ // The dropdown trigger is present
297
+ expect(container.querySelector('[data-testid="dropdown-menu"]')).toBeDefined();
298
+ });
299
+
300
+ it('has Update button visible', async () => {
301
+ const { container } = render(VenueItemCard, {
302
+ props: {
303
+ ...defaultProps,
304
+ status: 'upcoming',
305
+ hasAvailability: true,
306
+ invitationAccepted: true,
307
+ },
308
+ });
309
+
310
+ expect(container.innerHTML).toContain('Update');
311
+ });
312
+
313
+ it('has more-menu class on dropdown trigger', async () => {
314
+ const { container } = render(VenueItemCard, {
315
+ props: {
316
+ ...defaultProps,
317
+ status: 'upcoming',
318
+ hasAvailability: true,
319
+ invitationAccepted: true,
320
+ },
321
+ });
322
+
323
+ const menuTrigger = container.querySelector('.more-menu');
324
+ expect(menuTrigger).toBeDefined();
325
+ });
326
+ });
327
+
328
+ describe('Leave Venue API', () => {
329
+ it('calls API when leave is confirmed - success', async () => {
330
+ global.fetch.mockResolvedValueOnce({
331
+ ok: true,
332
+ json: async () => ({ success: true }),
333
+ });
334
+
335
+ const { container } = render(VenueItemCard, {
336
+ props: {
337
+ ...defaultProps,
338
+ status: 'upcoming',
339
+ hasAvailability: true,
340
+ invitationAccepted: true,
341
+ },
342
+ });
343
+
344
+ // Find and click Leave button (the one in the modal footer)
345
+ const buttons = container.querySelectorAll('button');
346
+ const leaveButton = Array.from(buttons).find(
347
+ (btn) => btn.textContent?.trim() === 'Leave'
348
+ );
349
+
350
+ if (leaveButton) {
351
+ await fireEvent.click(leaveButton);
352
+
353
+ await waitFor(() => {
354
+ expect(global.fetch).toHaveBeenCalledWith(
355
+ expect.stringContaining('/api/performer/removeVenueFromRoster'),
356
+ expect.objectContaining({
357
+ method: 'DELETE',
358
+ headers: expect.objectContaining({
359
+ 'Content-Type': 'application/json',
360
+ }),
361
+ })
362
+ );
363
+ });
364
+ }
365
+ });
366
+
367
+ it('shows success toast on successful leave', async () => {
368
+ global.fetch.mockResolvedValueOnce({
369
+ ok: true,
370
+ json: async () => ({ success: true }),
371
+ });
372
+
373
+ const { container } = render(VenueItemCard, {
374
+ props: {
375
+ ...defaultProps,
376
+ status: 'upcoming',
377
+ hasAvailability: true,
378
+ invitationAccepted: true,
379
+ },
380
+ });
381
+
382
+ const buttons = container.querySelectorAll('button');
383
+ const leaveButton = Array.from(buttons).find(
384
+ (btn) => btn.textContent?.trim() === 'Leave'
385
+ );
386
+
387
+ if (leaveButton) {
388
+ await fireEvent.click(leaveButton);
389
+
390
+ await waitFor(() => {
391
+ expect(showToast).toHaveBeenCalledWith(
392
+ expect.stringContaining('You have left'),
393
+ 'success'
394
+ );
395
+ });
396
+ }
397
+ });
398
+
399
+ it('shows error toast on failed leave', async () => {
400
+ global.fetch.mockResolvedValueOnce({
401
+ ok: false,
402
+ json: async () => ({ error: 'Failed' }),
403
+ });
404
+
405
+ const { container } = render(VenueItemCard, {
406
+ props: {
407
+ ...defaultProps,
408
+ status: 'upcoming',
409
+ hasAvailability: true,
410
+ invitationAccepted: true,
411
+ },
412
+ });
413
+
414
+ const buttons = container.querySelectorAll('button');
415
+ const leaveButton = Array.from(buttons).find(
416
+ (btn) => btn.textContent?.trim() === 'Leave'
417
+ );
418
+
419
+ if (leaveButton) {
420
+ await fireEvent.click(leaveButton);
421
+
422
+ await waitFor(() => {
423
+ expect(showToast).toHaveBeenCalledWith(
424
+ expect.stringContaining('Failed to leave'),
425
+ 'error'
426
+ );
427
+ });
428
+ }
429
+ });
430
+
431
+ it('reloads page after successful leave', async () => {
432
+ global.fetch.mockResolvedValueOnce({
433
+ ok: true,
434
+ json: async () => ({ success: true }),
435
+ });
436
+
437
+ const { container } = render(VenueItemCard, {
438
+ props: {
439
+ ...defaultProps,
440
+ status: 'upcoming',
441
+ hasAvailability: true,
442
+ invitationAccepted: true,
443
+ },
444
+ });
445
+
446
+ const buttons = container.querySelectorAll('button');
447
+ const leaveButton = Array.from(buttons).find(
448
+ (btn) => btn.textContent?.trim() === 'Leave'
449
+ );
450
+
451
+ if (leaveButton) {
452
+ await fireEvent.click(leaveButton);
453
+
454
+ await waitFor(() => {
455
+ expect(mockReload).toHaveBeenCalled();
456
+ });
457
+ }
458
+ });
459
+ });
460
+
461
+ describe('Card Structure', () => {
462
+ it('has main container with border and rounded corners', async () => {
463
+ const { container } = render(VenueItemCard, {
464
+ props: {
465
+ ...defaultProps,
466
+ status: 'upcoming',
467
+ hasAvailability: true,
468
+ invitationAccepted: true,
469
+ },
470
+ });
471
+
472
+ const mainDiv = container.querySelector('.rounded-lg');
473
+ expect(mainDiv).toBeDefined();
474
+ });
475
+ });
476
+
477
+ describe('Props Handling', () => {
478
+ it('renders with minimal required props', async () => {
479
+ const minimalProps = {
480
+ venueId: 1,
481
+ name: 'Minimal Venue',
482
+ status: 'upcoming',
483
+ invitationAccepted: false,
484
+ hasAvailability: false,
485
+ };
486
+
487
+ const { container } = render(VenueItemCard, { props: minimalProps });
488
+ expect(container).toBeDefined();
489
+ });
490
+
491
+ it('handles undefined optional props', async () => {
492
+ const propsWithUndefined = {
493
+ venueId: 1,
494
+ name: 'Test Venue',
495
+ image: undefined,
496
+ location: undefined,
497
+ status: 'upcoming',
498
+ invitationAccepted: true,
499
+ hasAvailability: true,
500
+ };
501
+
502
+ const { container } = render(VenueItemCard, { props: propsWithUndefined });
503
+ expect(container).toBeDefined();
504
+ });
505
+
506
+ it('handles null optional props', async () => {
507
+ const propsWithNull = {
508
+ venueId: 1,
509
+ name: 'Test Venue',
510
+ image: null,
511
+ location: null,
512
+ email: null,
513
+ phone: null,
514
+ status: 'upcoming',
515
+ invitationAccepted: true,
516
+ hasAvailability: true,
517
+ };
518
+
519
+ const { container } = render(VenueItemCard, { props: propsWithNull });
520
+ expect(container).toBeDefined();
521
+ });
522
+
523
+ it('handles empty string props', async () => {
524
+ const propsWithEmpty = {
525
+ venueId: 1,
526
+ name: '',
527
+ image: '',
528
+ location: '',
529
+ description: '',
530
+ status: 'upcoming',
531
+ invitationAccepted: true,
532
+ hasAvailability: true,
533
+ };
534
+
535
+ const { container } = render(VenueItemCard, { props: propsWithEmpty });
536
+ expect(container).toBeDefined();
537
+ });
538
+ });
539
+
540
+ describe('Dropdown Menu', () => {
541
+ it('renders dropdown trigger icon', async () => {
542
+ const { container } = render(VenueItemCard, {
543
+ props: {
544
+ ...defaultProps,
545
+ status: 'upcoming',
546
+ hasAvailability: true,
547
+ invitationAccepted: true,
548
+ },
549
+ });
550
+
551
+ const dropdownTrigger = container.querySelector('[data-testid="dropdown-menu"]');
552
+ expect(dropdownTrigger).toBeDefined();
553
+ });
554
+
555
+ it('renders dropdown icon with correct classes', async () => {
556
+ const { container } = render(VenueItemCard, {
557
+ props: {
558
+ ...defaultProps,
559
+ status: 'upcoming',
560
+ hasAvailability: true,
561
+ invitationAccepted: true,
562
+ },
563
+ });
564
+
565
+ const menuTrigger = container.querySelector('.more-menu');
566
+ expect(menuTrigger).not.toBeNull();
567
+ });
568
+ });
569
+
570
+ describe('Without Token', () => {
571
+ it('handles API call without token', async () => {
572
+ const { getPerformerToken } = await import('../../../../utils/utils');
573
+ getPerformerToken.mockReturnValueOnce(null);
574
+
575
+ global.fetch.mockResolvedValueOnce({
576
+ ok: true,
577
+ json: async () => ({ success: true }),
578
+ });
579
+
580
+ const { container } = render(VenueItemCard, {
581
+ props: {
582
+ ...defaultProps,
583
+ status: 'upcoming',
584
+ hasAvailability: true,
585
+ invitationAccepted: true,
586
+ },
587
+ });
588
+
589
+ const buttons = container.querySelectorAll('button');
590
+ const leaveButton = Array.from(buttons).find(
591
+ (btn) => btn.textContent?.trim() === 'Leave'
592
+ );
593
+
594
+ // With the Modal mock, the Leave button is rendered
595
+ expect(leaveButton).toBeDefined();
596
+
597
+ if (leaveButton) {
598
+ await fireEvent.click(leaveButton);
599
+ // Give time for async operations
600
+ await new Promise(resolve => setTimeout(resolve, 100));
601
+ }
602
+ });
603
+ });
604
+
605
+ describe('Modal Interactions', () => {
606
+ it('Cancel button closes the modal', async () => {
607
+ const { container } = render(VenueItemCard, {
608
+ props: {
609
+ ...defaultProps,
610
+ status: 'upcoming',
611
+ hasAvailability: true,
612
+ invitationAccepted: true,
613
+ },
614
+ });
615
+
616
+ // Find Cancel button in the modal footer
617
+ const buttons = container.querySelectorAll('button');
618
+ const cancelButton = Array.from(buttons).find(
619
+ (btn) => btn.textContent?.trim() === 'Cancel'
620
+ );
621
+
622
+ if (cancelButton) {
623
+ await fireEvent.click(cancelButton);
624
+ // Modal should close after cancel
625
+ expect(container).toBeDefined();
626
+ }
627
+ });
628
+
629
+ it('handles note input in leave venue modal', async () => {
630
+ const { container } = render(VenueItemCard, {
631
+ props: {
632
+ ...defaultProps,
633
+ status: 'upcoming',
634
+ hasAvailability: true,
635
+ invitationAccepted: true,
636
+ },
637
+ });
638
+
639
+ // Find textarea for notes
640
+ const textarea = container.querySelector('textarea');
641
+ if (textarea) {
642
+ await fireEvent.input(textarea, { target: { value: 'Test note for leaving' } });
643
+ expect(textarea.value).toBe('Test note for leaving');
644
+ }
645
+ });
646
+
647
+ it('Leave button triggers API call from modal', async () => {
648
+ global.fetch.mockResolvedValueOnce({
649
+ ok: true,
650
+ json: async () => ({ success: true }),
651
+ });
652
+
653
+ const { container } = render(VenueItemCard, {
654
+ props: {
655
+ ...defaultProps,
656
+ status: 'upcoming',
657
+ hasAvailability: true,
658
+ invitationAccepted: true,
659
+ },
660
+ });
661
+
662
+ // Modal only shows Leave button when showLeaveVenue is true
663
+ // Look for modal buttons that might be in the DOM
664
+ await waitFor(() => {
665
+ const buttons = container.querySelectorAll('button');
666
+ const leaveButton = Array.from(buttons).find(
667
+ (btn) => btn.textContent?.trim() === 'Leave'
668
+ );
669
+
670
+ if (leaveButton) {
671
+ fireEvent.click(leaveButton);
672
+ }
673
+ });
674
+ });
675
+ });
676
+
677
+ describe('Dropdown Menu Interactions', () => {
678
+ it('dropdown icon is clickable', async () => {
679
+ const { container } = render(VenueItemCard, {
680
+ props: {
681
+ ...defaultProps,
682
+ status: 'upcoming',
683
+ hasAvailability: true,
684
+ invitationAccepted: true,
685
+ },
686
+ });
687
+
688
+ const dropdownIcon = container.querySelector('[data-testid="dropdown-menu"]');
689
+ expect(dropdownIcon).toBeDefined();
690
+ if (dropdownIcon) {
691
+ await fireEvent.click(dropdownIcon);
692
+ // Dropdown should be rendered
693
+ expect(container).toBeDefined();
694
+ }
695
+ });
696
+
697
+ it('shows dropdown for red warning state', async () => {
698
+ const { container } = render(VenueItemCard, {
699
+ props: {
700
+ ...defaultProps,
701
+ status: 'upcoming',
702
+ hasAvailability: false,
703
+ invitationAccepted: true,
704
+ },
705
+ });
706
+
707
+ const dropdownIcon = container.querySelector('[data-testid="dropdown-menu"]');
708
+ expect(dropdownIcon).toBeDefined();
709
+ });
710
+
711
+ it('shows dropdown for normal update state', async () => {
712
+ const { container } = render(VenueItemCard, {
713
+ props: {
714
+ ...defaultProps,
715
+ status: 'upcoming',
716
+ hasAvailability: true,
717
+ invitationAccepted: true,
718
+ },
719
+ });
720
+
721
+ const dropdownIcon = container.querySelector('[data-testid="dropdown-menu"]');
722
+ expect(dropdownIcon).toBeDefined();
723
+ });
724
+ });
725
+
726
+ describe('Toggle Modal Function', () => {
727
+ it('handles non-leave modal name', async () => {
728
+ const { container } = render(VenueItemCard, {
729
+ props: {
730
+ ...defaultProps,
731
+ status: 'upcoming',
732
+ hasAvailability: true,
733
+ invitationAccepted: true,
734
+ },
735
+ });
736
+
737
+ // The toggleModal function should handle unknown modal names gracefully
738
+ expect(container).toBeDefined();
739
+ });
740
+ });
741
+
742
+ describe('Styling', () => {
743
+ it('has correct card container classes', async () => {
744
+ const { container } = render(VenueItemCard, { props: defaultProps });
745
+
746
+ const card = container.querySelector('.rounded-lg');
747
+ expect(card).toBeDefined();
748
+ });
749
+
750
+ it('applies red styling when needs availability update', async () => {
751
+ const { container } = render(VenueItemCard, {
752
+ props: {
753
+ ...defaultProps,
754
+ status: 'upcoming',
755
+ hasAvailability: false,
756
+ invitationAccepted: true,
757
+ },
758
+ });
759
+
760
+ expect(container.innerHTML).toContain('red-50');
761
+ });
762
+ });
763
+ });