@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.
- package/dist/components/Alert/Alert.spec.js +170 -170
- package/dist/components/Badges/Badge.spec.js +103 -103
- package/dist/components/BottomSheet/BottomSheet.spec.js +127 -127
- package/dist/components/Breadcrumb/Breadcrumb.spec.js +120 -120
- package/dist/components/Button/Button.spec.js +211 -211
- package/dist/components/Button/ButtonSaveDemo.spec.js +48 -48
- package/dist/components/Calendar/Calendar.spec.js +131 -131
- package/dist/components/Calendar/QuarterView.spec.js +394 -394
- package/dist/components/Card.spec.js +47 -47
- package/dist/components/CropImage/CropImage.spec.js +216 -216
- package/dist/components/DarkModeToggle.spec.js +357 -357
- package/dist/components/ErrorDisplay.spec.js +69 -69
- package/dist/components/FormActions.spec.js +88 -88
- package/dist/components/FormValidationSummary.spec.js +203 -203
- package/dist/components/Icons/Icon.spec.js +175 -175
- package/dist/components/Icons/MoreHori.spec.js +67 -67
- package/dist/components/Icons/WarningIcon.spec.js +30 -30
- package/dist/components/Input/Input.spec.js +573 -573
- package/dist/components/Input/MultiSelect.spec.js +257 -257
- package/dist/components/Input/OTPInput.spec.js +238 -238
- package/dist/components/Input/Select.spec.js +218 -218
- package/dist/components/Layout/BottomNav.spec.js +130 -130
- package/dist/components/Layout/Header.spec.js +203 -203
- package/dist/components/Modal/ConfirmationModal.spec.js +191 -191
- package/dist/components/Modal/Modal.spec.js +95 -95
- package/dist/components/Modal/ModalStateManager.spec.js +100 -100
- package/dist/components/PageLoader.spec.js +54 -54
- package/dist/components/PasswordStrengthIndicator/PasswordStrengthIndicator.spec.js +173 -173
- package/dist/components/PlaceAutocomplete/PlaceAutocomplete.spec.js +300 -300
- package/dist/components/Spinner/Spinner.spec.js +75 -75
- package/dist/components/StatusIndicator/StatusIndicator.spec.js +129 -129
- package/dist/components/Toaster/Toaster.stories.svelte +1 -1
- package/dist/components/Toggle.spec.js +158 -158
- package/dist/components/ValidationError.spec.js +79 -79
- package/dist/components/pages/performers/AvailabilityCalendarModal.spec.js +606 -606
- package/dist/components/pages/performers/AvailabilityCalendarModal.svelte +4 -4
- package/dist/components/pages/performers/ModalShowInfo.spec.js +124 -124
- package/dist/components/pages/performers/ModalShowInfo.svelte +1 -1
- package/dist/components/pages/performers/PageBackButton.spec.js +89 -89
- package/dist/components/pages/performers/SectionHeader.spec.js +75 -75
- package/dist/components/pages/performers/ShowDetails.spec.js +166 -166
- package/dist/components/pages/performers/ShowItemCard.spec.js +793 -793
- package/dist/components/pages/performers/ShowItemCard.svelte +4 -4
- package/dist/components/pages/performers/SwitchOption.spec.js +127 -127
- package/dist/components/pages/performers/VenueInfo.spec.js +167 -167
- package/dist/components/pages/performers/VenueInfo.svelte +1 -1
- package/dist/components/pages/performers/VenueItemCard.spec.js +763 -763
- package/dist/components/pages/performers/VenueItemCard.svelte +4 -4
- package/dist/components/pages/profile/profile-form.spec.js +9 -9
- package/dist/components/pages/settings/tabs/CustomImageDropzone.svelte +3 -3
- package/dist/components/pages/shows/ShowList.spec.js +33 -33
- package/dist/components/pages/shows/TabContent.spec.js +90 -90
- package/dist/components/pages/shows/TabNavigation.spec.js +143 -143
- package/dist/config.js +5 -5
- package/dist/config.spec.js +29 -29
- package/dist/constants/formOptions.js +25 -25
- package/dist/constants/formOptions.spec.js +88 -88
- package/dist/index.js +111 -111
- package/dist/stores/auth.d.ts +9 -0
- package/dist/stores/auth.d.ts.map +1 -0
- package/dist/stores/auth.js +36 -0
- package/dist/stores/auth.spec.d.ts +2 -0
- package/dist/stores/auth.spec.d.ts.map +1 -0
- package/dist/stores/auth.spec.js +139 -0
- package/dist/stores/formDataStore.d.ts +17 -0
- package/dist/stores/formDataStore.d.ts.map +1 -0
- package/dist/stores/formDataStore.js +25 -0
- package/dist/stores/formDataStore.spec.d.ts +2 -0
- package/dist/stores/formDataStore.spec.d.ts.map +1 -0
- package/dist/stores/formDataStore.spec.js +257 -0
- package/dist/stores/formSave.d.ts +24 -0
- package/dist/stores/formSave.d.ts.map +1 -0
- package/dist/stores/formSave.js +132 -0
- package/dist/stores/formSave.spec.d.ts +2 -0
- package/dist/stores/formSave.spec.d.ts.map +1 -0
- package/dist/stores/formSave.spec.js +296 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +0 -0
- package/dist/stores/navigation.d.ts +5 -0
- package/dist/stores/navigation.d.ts.map +1 -0
- package/dist/stores/navigation.js +12 -0
- package/dist/stores/navigation.spec.d.ts +2 -0
- package/dist/stores/navigation.spec.d.ts.map +1 -0
- package/dist/stores/navigation.spec.js +136 -0
- package/dist/stores/toaster.d.ts +4 -0
- package/dist/stores/toaster.d.ts.map +1 -0
- package/dist/stores/toaster.js +13 -0
- package/dist/stores/toaster.spec.d.ts +2 -0
- package/dist/stores/toaster.spec.d.ts.map +1 -0
- package/dist/stores/toaster.spec.js +59 -0
- package/dist/telemetry.js +357 -357
- package/dist/telemetry.server.js +211 -211
- package/dist/telemetry.server.spec.js +434 -434
- package/dist/telemetry.spec.js +660 -660
- package/dist/utils/apiConfig.js +49 -49
- package/dist/utils/apiConfig.spec.js +118 -118
- package/dist/utils/greetings.js +187 -187
- package/dist/utils/greetings.spec.js +337 -337
- package/dist/utils/imageValidation.js +121 -121
- package/dist/utils/imageValidation.spec.js +220 -220
- package/dist/utils/portal.js +25 -25
- package/dist/utils/portal.spec.js +143 -143
- package/dist/utils/utils/utils.js +323 -323
- package/dist/utils/utils/utils.spec.js +698 -698
- package/dist/utils/utils.spec.js +643 -643
- package/package.json +1 -1
|
@@ -1,793 +1,793 @@
|
|
|
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/shows') });
|
|
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
|
-
microphonePlaceholder: '/placeholder.jpg',
|
|
37
|
-
getUserDetails: vi.fn(() => ({ full_name: 'Test User' })),
|
|
38
|
-
classNames: vi.fn((...args) => args.filter(Boolean).join(' ')),
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
vi.mock('@/utils/utils/utils', () => ({
|
|
42
|
-
formatHour: vi.fn((date) => '8:00 PM'),
|
|
43
|
-
formattedDate: vi.fn((date) => 'Jan 15'),
|
|
44
|
-
formattedFullDate: vi.fn((date) => 'January 15, 2025'),
|
|
45
|
-
timeAgo: vi.fn(() => '2 hours ago'),
|
|
46
|
-
}));
|
|
47
|
-
|
|
48
|
-
// Mock ShowService
|
|
49
|
-
vi.mock('@/services/ShowService', () => ({
|
|
50
|
-
acceptInvite: vi.fn().mockResolvedValue({ ok: true }),
|
|
51
|
-
declineInvite: vi.fn().mockResolvedValue({ ok: true }),
|
|
52
|
-
cancelInvite: vi.fn().mockResolvedValue({ ok: true }),
|
|
53
|
-
sendVenueMessage: vi.fn().mockResolvedValue({ ok: true }),
|
|
54
|
-
getEventUrl: vi.fn((venueId) => `https://micdrop.com/events/${venueId}`),
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
describe('ShowItemCard', () => {
|
|
58
|
-
let ShowItemCard;
|
|
59
|
-
let showToast;
|
|
60
|
-
let goto;
|
|
61
|
-
|
|
62
|
-
const defaultProps = {
|
|
63
|
-
venueId: 1,
|
|
64
|
-
title: 'Comedy Night',
|
|
65
|
-
role: 'Headliner',
|
|
66
|
-
startDateTime: '2025-01-15T20:00:00Z',
|
|
67
|
-
doorsOpenTime: '2025-01-15T19:00:00Z',
|
|
68
|
-
spotDuration: 30,
|
|
69
|
-
venueName: 'The Comedy Store',
|
|
70
|
-
location: '8433 Sunset Blvd, Los Angeles, CA',
|
|
71
|
-
status: 'upcoming',
|
|
72
|
-
details: {
|
|
73
|
-
description: 'A great comedy night',
|
|
74
|
-
ticketPrice: 25,
|
|
75
|
-
},
|
|
76
|
-
id: 123,
|
|
77
|
-
invitationAccepted: true,
|
|
78
|
-
hasAvailability: true,
|
|
79
|
-
lastUpdated: '2025-01-10T12:00:00Z',
|
|
80
|
-
image: '/test-image.jpg',
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
beforeAll(async () => {
|
|
84
|
-
vi.resetModules();
|
|
85
|
-
const toasterModule = await import('../../../../stores/toaster');
|
|
86
|
-
showToast = toasterModule.showToast;
|
|
87
|
-
const navModule = await import('$app/navigation');
|
|
88
|
-
goto = navModule.goto;
|
|
89
|
-
ShowItemCard = (await import('./ShowItemCard.svelte')).default;
|
|
90
|
-
}, 60000);
|
|
91
|
-
|
|
92
|
-
beforeEach(() => {
|
|
93
|
-
vi.clearAllMocks();
|
|
94
|
-
// Mock clipboard API
|
|
95
|
-
Object.assign(navigator, {
|
|
96
|
-
clipboard: {
|
|
97
|
-
writeText: vi.fn().mockResolvedValue(undefined),
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
// Mock window.open
|
|
101
|
-
window.open = vi.fn();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
afterEach(() => {
|
|
105
|
-
cleanup();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe('Helper Functions', () => {
|
|
109
|
-
// Test the getRoleVariant function logic
|
|
110
|
-
const getRoleVariant = (role) => role.toLowerCase().replace(/\s+/g, '-');
|
|
111
|
-
|
|
112
|
-
it('converts Headliner to lowercase', () => {
|
|
113
|
-
expect(getRoleVariant('Headliner')).toBe('headliner');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('converts Feature to lowercase', () => {
|
|
117
|
-
expect(getRoleVariant('Feature')).toBe('feature');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('converts Host to lowercase', () => {
|
|
121
|
-
expect(getRoleVariant('Host')).toBe('host');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('converts Special Guest to hyphenated lowercase', () => {
|
|
125
|
-
expect(getRoleVariant('Special Guest')).toBe('special-guest');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('handles multiple spaces', () => {
|
|
129
|
-
expect(getRoleVariant('Super Special Guest')).toBe('super-special-guest');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('handles all caps', () => {
|
|
133
|
-
expect(getRoleVariant('HEADLINER')).toBe('headliner');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('Status Mapping', () => {
|
|
138
|
-
it('status 0 means pending invitation', () => {
|
|
139
|
-
expect(0).toBe(0);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('status 1 means accepted', () => {
|
|
143
|
-
expect(1).toBe(1);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('status 2 means declined', () => {
|
|
147
|
-
expect(2).toBe(2);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
describe('Constants', () => {
|
|
152
|
-
const MIN_LOADING_TIME = 1000;
|
|
153
|
-
|
|
154
|
-
it('minimum loading time is 1000ms', () => {
|
|
155
|
-
expect(MIN_LOADING_TIME).toBe(1000);
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('Basic Rendering', () => {
|
|
160
|
-
it('renders the show card', async () => {
|
|
161
|
-
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
162
|
-
expect(container.querySelector('.rounded-lg')).toBeDefined();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('displays show title', async () => {
|
|
166
|
-
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
167
|
-
expect(container.innerHTML).toContain('Comedy Night');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('displays venue name', async () => {
|
|
171
|
-
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
172
|
-
expect(container.innerHTML).toContain('The Comedy Store');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('displays date and time', async () => {
|
|
176
|
-
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
177
|
-
expect(container.innerHTML).toContain('Jan 15');
|
|
178
|
-
expect(container.innerHTML).toContain('8:00 PM');
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('displays "No date yet" when no startDateTime', async () => {
|
|
182
|
-
const { container } = render(ShowItemCard, {
|
|
183
|
-
props: { ...defaultProps, startDateTime: null },
|
|
184
|
-
});
|
|
185
|
-
expect(container.innerHTML).toContain('No date yet');
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe('Image Handling', () => {
|
|
190
|
-
it('uses full URL for http images', async () => {
|
|
191
|
-
const { container } = render(ShowItemCard, {
|
|
192
|
-
props: { ...defaultProps, image: 'https://example.com/image.jpg' },
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const img = container.querySelector('img');
|
|
196
|
-
expect(img?.src).toContain('https://example.com/image.jpg');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('prepends CDN URL for relative paths', async () => {
|
|
200
|
-
const { container } = render(ShowItemCard, {
|
|
201
|
-
props: { ...defaultProps, image: '/uploads/image.jpg' },
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const img = container.querySelector('img');
|
|
205
|
-
expect(img?.src).toContain('moxy.sfo3.digitaloceanspaces.com');
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('uses placeholder when no image', async () => {
|
|
209
|
-
const { container } = render(ShowItemCard, {
|
|
210
|
-
props: { ...defaultProps, image: null },
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const img = container.querySelector('img');
|
|
214
|
-
expect(img?.src).toContain('placeholder');
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('Status: Invitations', () => {
|
|
219
|
-
it('shows Confirm and Decline buttons for invitations', async () => {
|
|
220
|
-
const { container } = render(ShowItemCard, {
|
|
221
|
-
props: { ...defaultProps, status: 'invitations' },
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
expect(container.innerHTML).toContain('Confirm');
|
|
225
|
-
expect(container.innerHTML).toContain('Decline');
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('triggers accept modal state on Confirm click', async () => {
|
|
229
|
-
const { container } = render(ShowItemCard, {
|
|
230
|
-
props: { ...defaultProps, status: 'invitations' },
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const buttons = container.querySelectorAll('button');
|
|
234
|
-
const confirmButton = Array.from(buttons).find(
|
|
235
|
-
(btn) => btn.textContent?.includes('Confirm')
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
expect(confirmButton).toBeDefined();
|
|
239
|
-
if (confirmButton) {
|
|
240
|
-
await fireEvent.click(confirmButton);
|
|
241
|
-
// Modal state changes - component re-renders
|
|
242
|
-
expect(container).toBeDefined();
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('triggers decline modal state on Decline click', async () => {
|
|
247
|
-
const { container } = render(ShowItemCard, {
|
|
248
|
-
props: { ...defaultProps, status: 'invitations' },
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const buttons = container.querySelectorAll('button');
|
|
252
|
-
const declineButton = Array.from(buttons).find(
|
|
253
|
-
(btn) => btn.textContent?.includes('Decline')
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
expect(declineButton).toBeDefined();
|
|
257
|
-
if (declineButton) {
|
|
258
|
-
await fireEvent.click(declineButton);
|
|
259
|
-
// Modal state changes - component re-renders
|
|
260
|
-
expect(container).toBeDefined();
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
describe('Status: Upcoming with Availability', () => {
|
|
266
|
-
it('shows Update button', async () => {
|
|
267
|
-
const { container } = render(ShowItemCard, {
|
|
268
|
-
props: {
|
|
269
|
-
...defaultProps,
|
|
270
|
-
status: 'upcoming',
|
|
271
|
-
invitationAccepted: true,
|
|
272
|
-
hasAvailability: true,
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
expect(container.innerHTML).toContain('Update');
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('shows last updated text', async () => {
|
|
280
|
-
const { container } = render(ShowItemCard, {
|
|
281
|
-
props: {
|
|
282
|
-
...defaultProps,
|
|
283
|
-
status: 'upcoming',
|
|
284
|
-
invitationAccepted: true,
|
|
285
|
-
hasAvailability: true,
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
expect(container.innerHTML).toContain('Last updated');
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('navigates to availability when Update clicked', async () => {
|
|
293
|
-
const { container } = render(ShowItemCard, {
|
|
294
|
-
props: {
|
|
295
|
-
...defaultProps,
|
|
296
|
-
status: 'upcoming',
|
|
297
|
-
invitationAccepted: true,
|
|
298
|
-
hasAvailability: true,
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
const buttons = container.querySelectorAll('button');
|
|
303
|
-
const updateButton = Array.from(buttons).find(
|
|
304
|
-
(btn) => btn.textContent?.includes('Update')
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
if (updateButton) {
|
|
308
|
-
await fireEvent.click(updateButton);
|
|
309
|
-
|
|
310
|
-
await waitFor(() => {
|
|
311
|
-
expect(goto).toHaveBeenCalledWith('/availability/1');
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
describe('Status: Upcoming without Availability', () => {
|
|
318
|
-
it('shows Set up button when invitation accepted but no availability', async () => {
|
|
319
|
-
const { container } = render(ShowItemCard, {
|
|
320
|
-
props: {
|
|
321
|
-
...defaultProps,
|
|
322
|
-
status: 'upcoming',
|
|
323
|
-
invitationAccepted: true,
|
|
324
|
-
hasAvailability: false,
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
expect(container.innerHTML).toContain('Set up');
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('shows warning message', async () => {
|
|
332
|
-
const { container } = render(ShowItemCard, {
|
|
333
|
-
props: {
|
|
334
|
-
...defaultProps,
|
|
335
|
-
status: 'upcoming',
|
|
336
|
-
invitationAccepted: true,
|
|
337
|
-
hasAvailability: false,
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
expect(container.innerHTML).toContain('Please update your availability');
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
describe('Status: Upcoming without Invitation Accepted', () => {
|
|
346
|
-
it('shows Copy ticket link button', async () => {
|
|
347
|
-
const { container } = render(ShowItemCard, {
|
|
348
|
-
props: {
|
|
349
|
-
...defaultProps,
|
|
350
|
-
status: 'upcoming',
|
|
351
|
-
invitationAccepted: false,
|
|
352
|
-
hasAvailability: false,
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
expect(container.innerHTML).toContain('Copy ticket link');
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('shows Visit event page button', async () => {
|
|
360
|
-
const { container } = render(ShowItemCard, {
|
|
361
|
-
props: {
|
|
362
|
-
...defaultProps,
|
|
363
|
-
status: 'upcoming',
|
|
364
|
-
invitationAccepted: false,
|
|
365
|
-
hasAvailability: false,
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
expect(container.innerHTML).toContain('Visit event page');
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('opens event page in new tab', async () => {
|
|
373
|
-
const { container } = render(ShowItemCard, {
|
|
374
|
-
props: {
|
|
375
|
-
...defaultProps,
|
|
376
|
-
status: 'upcoming',
|
|
377
|
-
invitationAccepted: false,
|
|
378
|
-
hasAvailability: false,
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
const buttons = container.querySelectorAll('button');
|
|
383
|
-
const visitButton = Array.from(buttons).find(
|
|
384
|
-
(btn) => btn.textContent?.includes('Visit event page')
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
if (visitButton) {
|
|
388
|
-
await fireEvent.click(visitButton);
|
|
389
|
-
|
|
390
|
-
await waitFor(() => {
|
|
391
|
-
expect(window.open).toHaveBeenCalledWith(
|
|
392
|
-
expect.stringContaining('micdrop.com/events'),
|
|
393
|
-
'_blank'
|
|
394
|
-
);
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
describe('View Details Toggle', () => {
|
|
401
|
-
it('shows View details button for non-upcoming status', async () => {
|
|
402
|
-
const { container } = render(ShowItemCard, {
|
|
403
|
-
props: { ...defaultProps, status: 'past' },
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
expect(container.innerHTML).toContain('View details');
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it('toggles details visibility on click', async () => {
|
|
410
|
-
const { container } = render(ShowItemCard, {
|
|
411
|
-
props: { ...defaultProps, status: 'past' },
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
const buttons = container.querySelectorAll('button');
|
|
415
|
-
const detailsButton = Array.from(buttons).find(
|
|
416
|
-
(btn) => btn.textContent?.includes('View details')
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
if (detailsButton) {
|
|
420
|
-
await fireEvent.click(detailsButton);
|
|
421
|
-
|
|
422
|
-
await waitFor(() => {
|
|
423
|
-
expect(container.innerHTML).toContain('Close details');
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
describe('Status: Declined', () => {
|
|
430
|
-
it('shows View details for declined status', async () => {
|
|
431
|
-
const { container } = render(ShowItemCard, {
|
|
432
|
-
props: { ...defaultProps, status: 'declined' },
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
expect(container.innerHTML).toContain('View details');
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('does not show Message button for declined status', async () => {
|
|
439
|
-
const { container } = render(ShowItemCard, {
|
|
440
|
-
props: { ...defaultProps, status: 'declined' },
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
const buttons = container.querySelectorAll('button');
|
|
444
|
-
const messageButton = Array.from(buttons).find(
|
|
445
|
-
(btn) => btn.textContent?.includes('Message')
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
expect(messageButton).toBeUndefined();
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
describe('Status: Past', () => {
|
|
453
|
-
it('shows View details for past status', async () => {
|
|
454
|
-
const { container } = render(ShowItemCard, {
|
|
455
|
-
props: { ...defaultProps, status: 'past' },
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
expect(container.innerHTML).toContain('View details');
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it('does not show Message button for past status', async () => {
|
|
462
|
-
const { container } = render(ShowItemCard, {
|
|
463
|
-
props: { ...defaultProps, status: 'past' },
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
const buttons = container.querySelectorAll('button');
|
|
467
|
-
const messageButton = Array.from(buttons).find(
|
|
468
|
-
(btn) => btn.textContent?.includes('Message')
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
expect(messageButton).toBeUndefined();
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
describe('Message Modal', () => {
|
|
476
|
-
it('has Message button for upcoming without invitation', async () => {
|
|
477
|
-
const { container } = render(ShowItemCard, {
|
|
478
|
-
props: {
|
|
479
|
-
...defaultProps,
|
|
480
|
-
status: 'upcoming',
|
|
481
|
-
invitationAccepted: false,
|
|
482
|
-
hasAvailability: false,
|
|
483
|
-
},
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
const buttons = container.querySelectorAll('button');
|
|
487
|
-
const messageButton = Array.from(buttons).find(
|
|
488
|
-
(btn) => btn.textContent?.includes('Message')
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
expect(messageButton).toBeDefined();
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
it('triggers message modal state change on click', async () => {
|
|
495
|
-
const { container } = render(ShowItemCard, {
|
|
496
|
-
props: {
|
|
497
|
-
...defaultProps,
|
|
498
|
-
status: 'upcoming',
|
|
499
|
-
invitationAccepted: false,
|
|
500
|
-
hasAvailability: false,
|
|
501
|
-
},
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
const buttons = container.querySelectorAll('button');
|
|
505
|
-
const messageButton = Array.from(buttons).find(
|
|
506
|
-
(btn) => btn.textContent?.includes('Message')
|
|
507
|
-
);
|
|
508
|
-
|
|
509
|
-
if (messageButton) {
|
|
510
|
-
await fireEvent.click(messageButton);
|
|
511
|
-
// Component re-renders with modal open state
|
|
512
|
-
expect(container).toBeDefined();
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
describe('Copy Ticket Link', () => {
|
|
518
|
-
it('copies link to clipboard', async () => {
|
|
519
|
-
vi.useFakeTimers();
|
|
520
|
-
|
|
521
|
-
const { container } = render(ShowItemCard, {
|
|
522
|
-
props: {
|
|
523
|
-
...defaultProps,
|
|
524
|
-
status: 'upcoming',
|
|
525
|
-
invitationAccepted: false,
|
|
526
|
-
hasAvailability: false,
|
|
527
|
-
},
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
const buttons = container.querySelectorAll('button');
|
|
531
|
-
const copyButton = Array.from(buttons).find(
|
|
532
|
-
(btn) => btn.textContent?.includes('Copy ticket link')
|
|
533
|
-
);
|
|
534
|
-
|
|
535
|
-
if (copyButton) {
|
|
536
|
-
await fireEvent.click(copyButton);
|
|
537
|
-
|
|
538
|
-
await waitFor(() => {
|
|
539
|
-
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
540
|
-
expect.stringContaining('micdrop.com/events')
|
|
541
|
-
);
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
vi.useRealTimers();
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
describe('Leave Venue', () => {
|
|
550
|
-
it('renders dropdown trigger for upcoming with availability', async () => {
|
|
551
|
-
const { container } = render(ShowItemCard, {
|
|
552
|
-
props: {
|
|
553
|
-
...defaultProps,
|
|
554
|
-
status: 'upcoming',
|
|
555
|
-
invitationAccepted: true,
|
|
556
|
-
hasAvailability: true,
|
|
557
|
-
},
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
// The dropdown trigger should be present (dots menu)
|
|
561
|
-
const dropdownTrigger = container.querySelector('[data-testid="dropdown-menu"]');
|
|
562
|
-
expect(dropdownTrigger).toBeDefined();
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
it('handles leave venue flow', async () => {
|
|
566
|
-
const { container } = render(ShowItemCard, {
|
|
567
|
-
props: {
|
|
568
|
-
...defaultProps,
|
|
569
|
-
status: 'upcoming',
|
|
570
|
-
invitationAccepted: true,
|
|
571
|
-
hasAvailability: true,
|
|
572
|
-
},
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
// The component renders with leave functionality available
|
|
576
|
-
expect(container).toBeDefined();
|
|
577
|
-
});
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
describe('Props Handling', () => {
|
|
581
|
-
it('renders with minimal required props', async () => {
|
|
582
|
-
const minimalProps = {
|
|
583
|
-
venueId: 1,
|
|
584
|
-
title: 'Test Show',
|
|
585
|
-
status: 'upcoming',
|
|
586
|
-
venueName: 'Test Venue',
|
|
587
|
-
id: 1,
|
|
588
|
-
invitationAccepted: false,
|
|
589
|
-
hasAvailability: false,
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
const { container } = render(ShowItemCard, { props: minimalProps });
|
|
593
|
-
expect(container).toBeDefined();
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it('handles undefined optional props', async () => {
|
|
597
|
-
const propsWithUndefined = {
|
|
598
|
-
venueId: 1,
|
|
599
|
-
title: 'Test Show',
|
|
600
|
-
status: 'upcoming',
|
|
601
|
-
venueName: 'Test Venue',
|
|
602
|
-
id: 1,
|
|
603
|
-
invitationAccepted: false,
|
|
604
|
-
hasAvailability: false,
|
|
605
|
-
image: undefined,
|
|
606
|
-
location: undefined,
|
|
607
|
-
details: undefined,
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
const { container } = render(ShowItemCard, { props: propsWithUndefined });
|
|
611
|
-
expect(container).toBeDefined();
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it('handles null optional props', async () => {
|
|
615
|
-
const propsWithNull = {
|
|
616
|
-
venueId: 1,
|
|
617
|
-
title: 'Test Show',
|
|
618
|
-
status: 'upcoming',
|
|
619
|
-
venueName: 'Test Venue',
|
|
620
|
-
id: 1,
|
|
621
|
-
invitationAccepted: false,
|
|
622
|
-
hasAvailability: false,
|
|
623
|
-
image: null,
|
|
624
|
-
startDateTime: null,
|
|
625
|
-
doorsOpenTime: null,
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
const { container } = render(ShowItemCard, { props: propsWithNull });
|
|
629
|
-
expect(container).toBeDefined();
|
|
630
|
-
});
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
describe('Dropdown Menu', () => {
|
|
634
|
-
it('renders dropdown trigger for upcoming with availability', async () => {
|
|
635
|
-
const { container } = render(ShowItemCard, {
|
|
636
|
-
props: {
|
|
637
|
-
...defaultProps,
|
|
638
|
-
status: 'upcoming',
|
|
639
|
-
invitationAccepted: true,
|
|
640
|
-
hasAvailability: true,
|
|
641
|
-
},
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
const dropdownTrigger = container.querySelector('[data-testid="dropdown-menu"]');
|
|
645
|
-
expect(dropdownTrigger).toBeDefined();
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
it('renders dropdown trigger for upcoming without availability', async () => {
|
|
649
|
-
const { container } = render(ShowItemCard, {
|
|
650
|
-
props: {
|
|
651
|
-
...defaultProps,
|
|
652
|
-
status: 'upcoming',
|
|
653
|
-
invitationAccepted: true,
|
|
654
|
-
hasAvailability: false,
|
|
655
|
-
},
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
const dropdownTrigger = container.querySelector('[data-testid="dropdown-menu"]');
|
|
659
|
-
expect(dropdownTrigger).toBeDefined();
|
|
660
|
-
});
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
describe('Modal Actions', () => {
|
|
664
|
-
it('has service imports available', async () => {
|
|
665
|
-
const { acceptInvite, declineInvite, sendVenueMessage, cancelInvite } = await import('../../../../services/ShowService');
|
|
666
|
-
|
|
667
|
-
expect(acceptInvite).toBeDefined();
|
|
668
|
-
expect(declineInvite).toBeDefined();
|
|
669
|
-
expect(sendVenueMessage).toBeDefined();
|
|
670
|
-
expect(cancelInvite).toBeDefined();
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
it('opens accept modal on Confirm click', async () => {
|
|
674
|
-
const { container } = render(ShowItemCard, {
|
|
675
|
-
props: { ...defaultProps, status: 'invitations' },
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
// Find and click Confirm button
|
|
679
|
-
const buttons = container.querySelectorAll('button');
|
|
680
|
-
const confirmButton = Array.from(buttons).find(
|
|
681
|
-
(btn) => btn.textContent?.includes('Confirm')
|
|
682
|
-
);
|
|
683
|
-
|
|
684
|
-
expect(confirmButton).toBeDefined();
|
|
685
|
-
if (confirmButton) {
|
|
686
|
-
await fireEvent.click(confirmButton);
|
|
687
|
-
// Modal opens but content may be in portal
|
|
688
|
-
expect(container).toBeDefined();
|
|
689
|
-
}
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
it('opens decline modal on Decline click', async () => {
|
|
693
|
-
const { container } = render(ShowItemCard, {
|
|
694
|
-
props: { ...defaultProps, status: 'invitations' },
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
// Find and click Decline button
|
|
698
|
-
const buttons = container.querySelectorAll('button');
|
|
699
|
-
const declineButton = Array.from(buttons).find(
|
|
700
|
-
(btn) => btn.textContent?.includes('Decline')
|
|
701
|
-
);
|
|
702
|
-
|
|
703
|
-
expect(declineButton).toBeDefined();
|
|
704
|
-
if (declineButton) {
|
|
705
|
-
await fireEvent.click(declineButton);
|
|
706
|
-
expect(container).toBeDefined();
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
it('opens message modal on Message click', async () => {
|
|
711
|
-
const { container } = render(ShowItemCard, {
|
|
712
|
-
props: {
|
|
713
|
-
...defaultProps,
|
|
714
|
-
status: 'upcoming',
|
|
715
|
-
invitationAccepted: false,
|
|
716
|
-
hasAvailability: false,
|
|
717
|
-
},
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
const buttons = container.querySelectorAll('button');
|
|
721
|
-
const messageButton = Array.from(buttons).find(
|
|
722
|
-
(btn) => btn.textContent?.includes('Message')
|
|
723
|
-
);
|
|
724
|
-
|
|
725
|
-
expect(messageButton).toBeDefined();
|
|
726
|
-
if (messageButton) {
|
|
727
|
-
await fireEvent.click(messageButton);
|
|
728
|
-
expect(container).toBeDefined();
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
describe('Styling', () => {
|
|
734
|
-
it('has correct card container classes', async () => {
|
|
735
|
-
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
736
|
-
|
|
737
|
-
const card = container.querySelector('.bg-bg-secondary');
|
|
738
|
-
expect(card).toBeDefined();
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
it('applies red styling when needs availability update', async () => {
|
|
742
|
-
const { container } = render(ShowItemCard, {
|
|
743
|
-
props: {
|
|
744
|
-
...defaultProps,
|
|
745
|
-
status: 'upcoming',
|
|
746
|
-
invitationAccepted: true,
|
|
747
|
-
hasAvailability: false,
|
|
748
|
-
},
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
expect(container.innerHTML).toContain('red-50');
|
|
752
|
-
});
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
describe('Modal States', () => {
|
|
756
|
-
it('toggleModal function opens accept modal', async () => {
|
|
757
|
-
const { container } = render(ShowItemCard, {
|
|
758
|
-
props: { ...defaultProps, status: 'invitations' },
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
// Open accept modal
|
|
762
|
-
const buttons = container.querySelectorAll('button');
|
|
763
|
-
const confirmButton = Array.from(buttons).find(
|
|
764
|
-
(btn) => btn.textContent?.includes('Confirm')
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
expect(confirmButton).toBeDefined();
|
|
768
|
-
if (confirmButton) {
|
|
769
|
-
await fireEvent.click(confirmButton);
|
|
770
|
-
// Modal state should change - component re-renders
|
|
771
|
-
expect(container).toBeDefined();
|
|
772
|
-
}
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
it('toggleModal function opens decline modal', async () => {
|
|
776
|
-
const { container } = render(ShowItemCard, {
|
|
777
|
-
props: { ...defaultProps, status: 'invitations' },
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
// Open decline modal
|
|
781
|
-
const buttons = container.querySelectorAll('button');
|
|
782
|
-
const declineButton = Array.from(buttons).find(
|
|
783
|
-
(btn) => btn.textContent?.includes('Decline')
|
|
784
|
-
);
|
|
785
|
-
|
|
786
|
-
expect(declineButton).toBeDefined();
|
|
787
|
-
if (declineButton) {
|
|
788
|
-
await fireEvent.click(declineButton);
|
|
789
|
-
expect(container).toBeDefined();
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
});
|
|
793
|
-
});
|
|
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/shows') });
|
|
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
|
+
microphonePlaceholder: '/placeholder.jpg',
|
|
37
|
+
getUserDetails: vi.fn(() => ({ full_name: 'Test User' })),
|
|
38
|
+
classNames: vi.fn((...args) => args.filter(Boolean).join(' ')),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock('@/utils/utils/utils', () => ({
|
|
42
|
+
formatHour: vi.fn((date) => '8:00 PM'),
|
|
43
|
+
formattedDate: vi.fn((date) => 'Jan 15'),
|
|
44
|
+
formattedFullDate: vi.fn((date) => 'January 15, 2025'),
|
|
45
|
+
timeAgo: vi.fn(() => '2 hours ago'),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// Mock ShowService
|
|
49
|
+
vi.mock('@/services/ShowService', () => ({
|
|
50
|
+
acceptInvite: vi.fn().mockResolvedValue({ ok: true }),
|
|
51
|
+
declineInvite: vi.fn().mockResolvedValue({ ok: true }),
|
|
52
|
+
cancelInvite: vi.fn().mockResolvedValue({ ok: true }),
|
|
53
|
+
sendVenueMessage: vi.fn().mockResolvedValue({ ok: true }),
|
|
54
|
+
getEventUrl: vi.fn((venueId) => `https://micdrop.com/events/${venueId}`),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
describe('ShowItemCard', () => {
|
|
58
|
+
let ShowItemCard;
|
|
59
|
+
let showToast;
|
|
60
|
+
let goto;
|
|
61
|
+
|
|
62
|
+
const defaultProps = {
|
|
63
|
+
venueId: 1,
|
|
64
|
+
title: 'Comedy Night',
|
|
65
|
+
role: 'Headliner',
|
|
66
|
+
startDateTime: '2025-01-15T20:00:00Z',
|
|
67
|
+
doorsOpenTime: '2025-01-15T19:00:00Z',
|
|
68
|
+
spotDuration: 30,
|
|
69
|
+
venueName: 'The Comedy Store',
|
|
70
|
+
location: '8433 Sunset Blvd, Los Angeles, CA',
|
|
71
|
+
status: 'upcoming',
|
|
72
|
+
details: {
|
|
73
|
+
description: 'A great comedy night',
|
|
74
|
+
ticketPrice: 25,
|
|
75
|
+
},
|
|
76
|
+
id: 123,
|
|
77
|
+
invitationAccepted: true,
|
|
78
|
+
hasAvailability: true,
|
|
79
|
+
lastUpdated: '2025-01-10T12:00:00Z',
|
|
80
|
+
image: '/test-image.jpg',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
vi.resetModules();
|
|
85
|
+
const toasterModule = await import('../../../../stores/toaster');
|
|
86
|
+
showToast = toasterModule.showToast;
|
|
87
|
+
const navModule = await import('$app/navigation');
|
|
88
|
+
goto = navModule.goto;
|
|
89
|
+
ShowItemCard = (await import('./ShowItemCard.svelte')).default;
|
|
90
|
+
}, 60000);
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
vi.clearAllMocks();
|
|
94
|
+
// Mock clipboard API
|
|
95
|
+
Object.assign(navigator, {
|
|
96
|
+
clipboard: {
|
|
97
|
+
writeText: vi.fn().mockResolvedValue(undefined),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
// Mock window.open
|
|
101
|
+
window.open = vi.fn();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
cleanup();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('Helper Functions', () => {
|
|
109
|
+
// Test the getRoleVariant function logic
|
|
110
|
+
const getRoleVariant = (role) => role.toLowerCase().replace(/\s+/g, '-');
|
|
111
|
+
|
|
112
|
+
it('converts Headliner to lowercase', () => {
|
|
113
|
+
expect(getRoleVariant('Headliner')).toBe('headliner');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('converts Feature to lowercase', () => {
|
|
117
|
+
expect(getRoleVariant('Feature')).toBe('feature');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('converts Host to lowercase', () => {
|
|
121
|
+
expect(getRoleVariant('Host')).toBe('host');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('converts Special Guest to hyphenated lowercase', () => {
|
|
125
|
+
expect(getRoleVariant('Special Guest')).toBe('special-guest');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('handles multiple spaces', () => {
|
|
129
|
+
expect(getRoleVariant('Super Special Guest')).toBe('super-special-guest');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('handles all caps', () => {
|
|
133
|
+
expect(getRoleVariant('HEADLINER')).toBe('headliner');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('Status Mapping', () => {
|
|
138
|
+
it('status 0 means pending invitation', () => {
|
|
139
|
+
expect(0).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('status 1 means accepted', () => {
|
|
143
|
+
expect(1).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('status 2 means declined', () => {
|
|
147
|
+
expect(2).toBe(2);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Constants', () => {
|
|
152
|
+
const MIN_LOADING_TIME = 1000;
|
|
153
|
+
|
|
154
|
+
it('minimum loading time is 1000ms', () => {
|
|
155
|
+
expect(MIN_LOADING_TIME).toBe(1000);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('Basic Rendering', () => {
|
|
160
|
+
it('renders the show card', async () => {
|
|
161
|
+
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
162
|
+
expect(container.querySelector('.rounded-lg')).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('displays show title', async () => {
|
|
166
|
+
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
167
|
+
expect(container.innerHTML).toContain('Comedy Night');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('displays venue name', async () => {
|
|
171
|
+
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
172
|
+
expect(container.innerHTML).toContain('The Comedy Store');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('displays date and time', async () => {
|
|
176
|
+
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
177
|
+
expect(container.innerHTML).toContain('Jan 15');
|
|
178
|
+
expect(container.innerHTML).toContain('8:00 PM');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('displays "No date yet" when no startDateTime', async () => {
|
|
182
|
+
const { container } = render(ShowItemCard, {
|
|
183
|
+
props: { ...defaultProps, startDateTime: null },
|
|
184
|
+
});
|
|
185
|
+
expect(container.innerHTML).toContain('No date yet');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Image Handling', () => {
|
|
190
|
+
it('uses full URL for http images', async () => {
|
|
191
|
+
const { container } = render(ShowItemCard, {
|
|
192
|
+
props: { ...defaultProps, image: 'https://example.com/image.jpg' },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const img = container.querySelector('img');
|
|
196
|
+
expect(img?.src).toContain('https://example.com/image.jpg');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('prepends CDN URL for relative paths', async () => {
|
|
200
|
+
const { container } = render(ShowItemCard, {
|
|
201
|
+
props: { ...defaultProps, image: '/uploads/image.jpg' },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const img = container.querySelector('img');
|
|
205
|
+
expect(img?.src).toContain('moxy.sfo3.digitaloceanspaces.com');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('uses placeholder when no image', async () => {
|
|
209
|
+
const { container } = render(ShowItemCard, {
|
|
210
|
+
props: { ...defaultProps, image: null },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const img = container.querySelector('img');
|
|
214
|
+
expect(img?.src).toContain('placeholder');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('Status: Invitations', () => {
|
|
219
|
+
it('shows Confirm and Decline buttons for invitations', async () => {
|
|
220
|
+
const { container } = render(ShowItemCard, {
|
|
221
|
+
props: { ...defaultProps, status: 'invitations' },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(container.innerHTML).toContain('Confirm');
|
|
225
|
+
expect(container.innerHTML).toContain('Decline');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('triggers accept modal state on Confirm click', async () => {
|
|
229
|
+
const { container } = render(ShowItemCard, {
|
|
230
|
+
props: { ...defaultProps, status: 'invitations' },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const buttons = container.querySelectorAll('button');
|
|
234
|
+
const confirmButton = Array.from(buttons).find(
|
|
235
|
+
(btn) => btn.textContent?.includes('Confirm')
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(confirmButton).toBeDefined();
|
|
239
|
+
if (confirmButton) {
|
|
240
|
+
await fireEvent.click(confirmButton);
|
|
241
|
+
// Modal state changes - component re-renders
|
|
242
|
+
expect(container).toBeDefined();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('triggers decline modal state on Decline click', async () => {
|
|
247
|
+
const { container } = render(ShowItemCard, {
|
|
248
|
+
props: { ...defaultProps, status: 'invitations' },
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const buttons = container.querySelectorAll('button');
|
|
252
|
+
const declineButton = Array.from(buttons).find(
|
|
253
|
+
(btn) => btn.textContent?.includes('Decline')
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(declineButton).toBeDefined();
|
|
257
|
+
if (declineButton) {
|
|
258
|
+
await fireEvent.click(declineButton);
|
|
259
|
+
// Modal state changes - component re-renders
|
|
260
|
+
expect(container).toBeDefined();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('Status: Upcoming with Availability', () => {
|
|
266
|
+
it('shows Update button', async () => {
|
|
267
|
+
const { container } = render(ShowItemCard, {
|
|
268
|
+
props: {
|
|
269
|
+
...defaultProps,
|
|
270
|
+
status: 'upcoming',
|
|
271
|
+
invitationAccepted: true,
|
|
272
|
+
hasAvailability: true,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(container.innerHTML).toContain('Update');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('shows last updated text', async () => {
|
|
280
|
+
const { container } = render(ShowItemCard, {
|
|
281
|
+
props: {
|
|
282
|
+
...defaultProps,
|
|
283
|
+
status: 'upcoming',
|
|
284
|
+
invitationAccepted: true,
|
|
285
|
+
hasAvailability: true,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(container.innerHTML).toContain('Last updated');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('navigates to availability when Update clicked', async () => {
|
|
293
|
+
const { container } = render(ShowItemCard, {
|
|
294
|
+
props: {
|
|
295
|
+
...defaultProps,
|
|
296
|
+
status: 'upcoming',
|
|
297
|
+
invitationAccepted: true,
|
|
298
|
+
hasAvailability: true,
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const buttons = container.querySelectorAll('button');
|
|
303
|
+
const updateButton = Array.from(buttons).find(
|
|
304
|
+
(btn) => btn.textContent?.includes('Update')
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (updateButton) {
|
|
308
|
+
await fireEvent.click(updateButton);
|
|
309
|
+
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
expect(goto).toHaveBeenCalledWith('/availability/1');
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe('Status: Upcoming without Availability', () => {
|
|
318
|
+
it('shows Set up button when invitation accepted but no availability', async () => {
|
|
319
|
+
const { container } = render(ShowItemCard, {
|
|
320
|
+
props: {
|
|
321
|
+
...defaultProps,
|
|
322
|
+
status: 'upcoming',
|
|
323
|
+
invitationAccepted: true,
|
|
324
|
+
hasAvailability: false,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(container.innerHTML).toContain('Set up');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('shows warning message', async () => {
|
|
332
|
+
const { container } = render(ShowItemCard, {
|
|
333
|
+
props: {
|
|
334
|
+
...defaultProps,
|
|
335
|
+
status: 'upcoming',
|
|
336
|
+
invitationAccepted: true,
|
|
337
|
+
hasAvailability: false,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(container.innerHTML).toContain('Please update your availability');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('Status: Upcoming without Invitation Accepted', () => {
|
|
346
|
+
it('shows Copy ticket link button', async () => {
|
|
347
|
+
const { container } = render(ShowItemCard, {
|
|
348
|
+
props: {
|
|
349
|
+
...defaultProps,
|
|
350
|
+
status: 'upcoming',
|
|
351
|
+
invitationAccepted: false,
|
|
352
|
+
hasAvailability: false,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(container.innerHTML).toContain('Copy ticket link');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('shows Visit event page button', async () => {
|
|
360
|
+
const { container } = render(ShowItemCard, {
|
|
361
|
+
props: {
|
|
362
|
+
...defaultProps,
|
|
363
|
+
status: 'upcoming',
|
|
364
|
+
invitationAccepted: false,
|
|
365
|
+
hasAvailability: false,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
expect(container.innerHTML).toContain('Visit event page');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('opens event page in new tab', async () => {
|
|
373
|
+
const { container } = render(ShowItemCard, {
|
|
374
|
+
props: {
|
|
375
|
+
...defaultProps,
|
|
376
|
+
status: 'upcoming',
|
|
377
|
+
invitationAccepted: false,
|
|
378
|
+
hasAvailability: false,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const buttons = container.querySelectorAll('button');
|
|
383
|
+
const visitButton = Array.from(buttons).find(
|
|
384
|
+
(btn) => btn.textContent?.includes('Visit event page')
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
if (visitButton) {
|
|
388
|
+
await fireEvent.click(visitButton);
|
|
389
|
+
|
|
390
|
+
await waitFor(() => {
|
|
391
|
+
expect(window.open).toHaveBeenCalledWith(
|
|
392
|
+
expect.stringContaining('micdrop.com/events'),
|
|
393
|
+
'_blank'
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('View Details Toggle', () => {
|
|
401
|
+
it('shows View details button for non-upcoming status', async () => {
|
|
402
|
+
const { container } = render(ShowItemCard, {
|
|
403
|
+
props: { ...defaultProps, status: 'past' },
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(container.innerHTML).toContain('View details');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('toggles details visibility on click', async () => {
|
|
410
|
+
const { container } = render(ShowItemCard, {
|
|
411
|
+
props: { ...defaultProps, status: 'past' },
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const buttons = container.querySelectorAll('button');
|
|
415
|
+
const detailsButton = Array.from(buttons).find(
|
|
416
|
+
(btn) => btn.textContent?.includes('View details')
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
if (detailsButton) {
|
|
420
|
+
await fireEvent.click(detailsButton);
|
|
421
|
+
|
|
422
|
+
await waitFor(() => {
|
|
423
|
+
expect(container.innerHTML).toContain('Close details');
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('Status: Declined', () => {
|
|
430
|
+
it('shows View details for declined status', async () => {
|
|
431
|
+
const { container } = render(ShowItemCard, {
|
|
432
|
+
props: { ...defaultProps, status: 'declined' },
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
expect(container.innerHTML).toContain('View details');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('does not show Message button for declined status', async () => {
|
|
439
|
+
const { container } = render(ShowItemCard, {
|
|
440
|
+
props: { ...defaultProps, status: 'declined' },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const buttons = container.querySelectorAll('button');
|
|
444
|
+
const messageButton = Array.from(buttons).find(
|
|
445
|
+
(btn) => btn.textContent?.includes('Message')
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
expect(messageButton).toBeUndefined();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('Status: Past', () => {
|
|
453
|
+
it('shows View details for past status', async () => {
|
|
454
|
+
const { container } = render(ShowItemCard, {
|
|
455
|
+
props: { ...defaultProps, status: 'past' },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(container.innerHTML).toContain('View details');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('does not show Message button for past status', async () => {
|
|
462
|
+
const { container } = render(ShowItemCard, {
|
|
463
|
+
props: { ...defaultProps, status: 'past' },
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const buttons = container.querySelectorAll('button');
|
|
467
|
+
const messageButton = Array.from(buttons).find(
|
|
468
|
+
(btn) => btn.textContent?.includes('Message')
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
expect(messageButton).toBeUndefined();
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe('Message Modal', () => {
|
|
476
|
+
it('has Message button for upcoming without invitation', async () => {
|
|
477
|
+
const { container } = render(ShowItemCard, {
|
|
478
|
+
props: {
|
|
479
|
+
...defaultProps,
|
|
480
|
+
status: 'upcoming',
|
|
481
|
+
invitationAccepted: false,
|
|
482
|
+
hasAvailability: false,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const buttons = container.querySelectorAll('button');
|
|
487
|
+
const messageButton = Array.from(buttons).find(
|
|
488
|
+
(btn) => btn.textContent?.includes('Message')
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
expect(messageButton).toBeDefined();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('triggers message modal state change on click', async () => {
|
|
495
|
+
const { container } = render(ShowItemCard, {
|
|
496
|
+
props: {
|
|
497
|
+
...defaultProps,
|
|
498
|
+
status: 'upcoming',
|
|
499
|
+
invitationAccepted: false,
|
|
500
|
+
hasAvailability: false,
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const buttons = container.querySelectorAll('button');
|
|
505
|
+
const messageButton = Array.from(buttons).find(
|
|
506
|
+
(btn) => btn.textContent?.includes('Message')
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
if (messageButton) {
|
|
510
|
+
await fireEvent.click(messageButton);
|
|
511
|
+
// Component re-renders with modal open state
|
|
512
|
+
expect(container).toBeDefined();
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe('Copy Ticket Link', () => {
|
|
518
|
+
it('copies link to clipboard', async () => {
|
|
519
|
+
vi.useFakeTimers();
|
|
520
|
+
|
|
521
|
+
const { container } = render(ShowItemCard, {
|
|
522
|
+
props: {
|
|
523
|
+
...defaultProps,
|
|
524
|
+
status: 'upcoming',
|
|
525
|
+
invitationAccepted: false,
|
|
526
|
+
hasAvailability: false,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const buttons = container.querySelectorAll('button');
|
|
531
|
+
const copyButton = Array.from(buttons).find(
|
|
532
|
+
(btn) => btn.textContent?.includes('Copy ticket link')
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
if (copyButton) {
|
|
536
|
+
await fireEvent.click(copyButton);
|
|
537
|
+
|
|
538
|
+
await waitFor(() => {
|
|
539
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
540
|
+
expect.stringContaining('micdrop.com/events')
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
vi.useRealTimers();
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
describe('Leave Venue', () => {
|
|
550
|
+
it('renders dropdown trigger for upcoming with availability', async () => {
|
|
551
|
+
const { container } = render(ShowItemCard, {
|
|
552
|
+
props: {
|
|
553
|
+
...defaultProps,
|
|
554
|
+
status: 'upcoming',
|
|
555
|
+
invitationAccepted: true,
|
|
556
|
+
hasAvailability: true,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// The dropdown trigger should be present (dots menu)
|
|
561
|
+
const dropdownTrigger = container.querySelector('[data-testid="dropdown-menu"]');
|
|
562
|
+
expect(dropdownTrigger).toBeDefined();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('handles leave venue flow', async () => {
|
|
566
|
+
const { container } = render(ShowItemCard, {
|
|
567
|
+
props: {
|
|
568
|
+
...defaultProps,
|
|
569
|
+
status: 'upcoming',
|
|
570
|
+
invitationAccepted: true,
|
|
571
|
+
hasAvailability: true,
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// The component renders with leave functionality available
|
|
576
|
+
expect(container).toBeDefined();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
describe('Props Handling', () => {
|
|
581
|
+
it('renders with minimal required props', async () => {
|
|
582
|
+
const minimalProps = {
|
|
583
|
+
venueId: 1,
|
|
584
|
+
title: 'Test Show',
|
|
585
|
+
status: 'upcoming',
|
|
586
|
+
venueName: 'Test Venue',
|
|
587
|
+
id: 1,
|
|
588
|
+
invitationAccepted: false,
|
|
589
|
+
hasAvailability: false,
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const { container } = render(ShowItemCard, { props: minimalProps });
|
|
593
|
+
expect(container).toBeDefined();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('handles undefined optional props', async () => {
|
|
597
|
+
const propsWithUndefined = {
|
|
598
|
+
venueId: 1,
|
|
599
|
+
title: 'Test Show',
|
|
600
|
+
status: 'upcoming',
|
|
601
|
+
venueName: 'Test Venue',
|
|
602
|
+
id: 1,
|
|
603
|
+
invitationAccepted: false,
|
|
604
|
+
hasAvailability: false,
|
|
605
|
+
image: undefined,
|
|
606
|
+
location: undefined,
|
|
607
|
+
details: undefined,
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const { container } = render(ShowItemCard, { props: propsWithUndefined });
|
|
611
|
+
expect(container).toBeDefined();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('handles null optional props', async () => {
|
|
615
|
+
const propsWithNull = {
|
|
616
|
+
venueId: 1,
|
|
617
|
+
title: 'Test Show',
|
|
618
|
+
status: 'upcoming',
|
|
619
|
+
venueName: 'Test Venue',
|
|
620
|
+
id: 1,
|
|
621
|
+
invitationAccepted: false,
|
|
622
|
+
hasAvailability: false,
|
|
623
|
+
image: null,
|
|
624
|
+
startDateTime: null,
|
|
625
|
+
doorsOpenTime: null,
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const { container } = render(ShowItemCard, { props: propsWithNull });
|
|
629
|
+
expect(container).toBeDefined();
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
describe('Dropdown Menu', () => {
|
|
634
|
+
it('renders dropdown trigger for upcoming with availability', async () => {
|
|
635
|
+
const { container } = render(ShowItemCard, {
|
|
636
|
+
props: {
|
|
637
|
+
...defaultProps,
|
|
638
|
+
status: 'upcoming',
|
|
639
|
+
invitationAccepted: true,
|
|
640
|
+
hasAvailability: true,
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const dropdownTrigger = container.querySelector('[data-testid="dropdown-menu"]');
|
|
645
|
+
expect(dropdownTrigger).toBeDefined();
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('renders dropdown trigger for upcoming without availability', async () => {
|
|
649
|
+
const { container } = render(ShowItemCard, {
|
|
650
|
+
props: {
|
|
651
|
+
...defaultProps,
|
|
652
|
+
status: 'upcoming',
|
|
653
|
+
invitationAccepted: true,
|
|
654
|
+
hasAvailability: false,
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const dropdownTrigger = container.querySelector('[data-testid="dropdown-menu"]');
|
|
659
|
+
expect(dropdownTrigger).toBeDefined();
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
describe('Modal Actions', () => {
|
|
664
|
+
it('has service imports available', async () => {
|
|
665
|
+
const { acceptInvite, declineInvite, sendVenueMessage, cancelInvite } = await import('../../../../services/ShowService');
|
|
666
|
+
|
|
667
|
+
expect(acceptInvite).toBeDefined();
|
|
668
|
+
expect(declineInvite).toBeDefined();
|
|
669
|
+
expect(sendVenueMessage).toBeDefined();
|
|
670
|
+
expect(cancelInvite).toBeDefined();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('opens accept modal on Confirm click', async () => {
|
|
674
|
+
const { container } = render(ShowItemCard, {
|
|
675
|
+
props: { ...defaultProps, status: 'invitations' },
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Find and click Confirm button
|
|
679
|
+
const buttons = container.querySelectorAll('button');
|
|
680
|
+
const confirmButton = Array.from(buttons).find(
|
|
681
|
+
(btn) => btn.textContent?.includes('Confirm')
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
expect(confirmButton).toBeDefined();
|
|
685
|
+
if (confirmButton) {
|
|
686
|
+
await fireEvent.click(confirmButton);
|
|
687
|
+
// Modal opens but content may be in portal
|
|
688
|
+
expect(container).toBeDefined();
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('opens decline modal on Decline click', async () => {
|
|
693
|
+
const { container } = render(ShowItemCard, {
|
|
694
|
+
props: { ...defaultProps, status: 'invitations' },
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Find and click Decline button
|
|
698
|
+
const buttons = container.querySelectorAll('button');
|
|
699
|
+
const declineButton = Array.from(buttons).find(
|
|
700
|
+
(btn) => btn.textContent?.includes('Decline')
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
expect(declineButton).toBeDefined();
|
|
704
|
+
if (declineButton) {
|
|
705
|
+
await fireEvent.click(declineButton);
|
|
706
|
+
expect(container).toBeDefined();
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('opens message modal on Message click', async () => {
|
|
711
|
+
const { container } = render(ShowItemCard, {
|
|
712
|
+
props: {
|
|
713
|
+
...defaultProps,
|
|
714
|
+
status: 'upcoming',
|
|
715
|
+
invitationAccepted: false,
|
|
716
|
+
hasAvailability: false,
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const buttons = container.querySelectorAll('button');
|
|
721
|
+
const messageButton = Array.from(buttons).find(
|
|
722
|
+
(btn) => btn.textContent?.includes('Message')
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
expect(messageButton).toBeDefined();
|
|
726
|
+
if (messageButton) {
|
|
727
|
+
await fireEvent.click(messageButton);
|
|
728
|
+
expect(container).toBeDefined();
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
describe('Styling', () => {
|
|
734
|
+
it('has correct card container classes', async () => {
|
|
735
|
+
const { container } = render(ShowItemCard, { props: defaultProps });
|
|
736
|
+
|
|
737
|
+
const card = container.querySelector('.bg-bg-secondary');
|
|
738
|
+
expect(card).toBeDefined();
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('applies red styling when needs availability update', async () => {
|
|
742
|
+
const { container } = render(ShowItemCard, {
|
|
743
|
+
props: {
|
|
744
|
+
...defaultProps,
|
|
745
|
+
status: 'upcoming',
|
|
746
|
+
invitationAccepted: true,
|
|
747
|
+
hasAvailability: false,
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
expect(container.innerHTML).toContain('red-50');
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
describe('Modal States', () => {
|
|
756
|
+
it('toggleModal function opens accept modal', async () => {
|
|
757
|
+
const { container } = render(ShowItemCard, {
|
|
758
|
+
props: { ...defaultProps, status: 'invitations' },
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// Open accept modal
|
|
762
|
+
const buttons = container.querySelectorAll('button');
|
|
763
|
+
const confirmButton = Array.from(buttons).find(
|
|
764
|
+
(btn) => btn.textContent?.includes('Confirm')
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
expect(confirmButton).toBeDefined();
|
|
768
|
+
if (confirmButton) {
|
|
769
|
+
await fireEvent.click(confirmButton);
|
|
770
|
+
// Modal state should change - component re-renders
|
|
771
|
+
expect(container).toBeDefined();
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('toggleModal function opens decline modal', async () => {
|
|
776
|
+
const { container } = render(ShowItemCard, {
|
|
777
|
+
props: { ...defaultProps, status: 'invitations' },
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Open decline modal
|
|
781
|
+
const buttons = container.querySelectorAll('button');
|
|
782
|
+
const declineButton = Array.from(buttons).find(
|
|
783
|
+
(btn) => btn.textContent?.includes('Decline')
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
expect(declineButton).toBeDefined();
|
|
787
|
+
if (declineButton) {
|
|
788
|
+
await fireEvent.click(declineButton);
|
|
789
|
+
expect(container).toBeDefined();
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
});
|