sunpeak 0.8.6 → 0.8.8
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/bin/commands/build.mjs +3 -3
- package/bin/commands/pull.mjs +2 -2
- package/bin/sunpeak.js +4 -6
- package/dist/mcp/entry.cjs.map +1 -1
- package/dist/mcp/entry.js.map +1 -1
- package/dist/style.css +0 -37
- package/package.json +1 -1
- package/template/.sunpeak/dev.tsx +2 -2
- package/template/README.md +5 -5
- package/template/dist/albums.js +1 -1
- package/template/dist/albums.json +1 -1
- package/template/dist/carousel.js +1 -1
- package/template/dist/carousel.json +1 -1
- package/template/dist/map.js +1 -1
- package/template/dist/map.json +1 -1
- package/template/dist/review.js +1 -1
- package/template/dist/review.json +1 -1
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Button.js +3 -3
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_SegmentedControl.js +4 -4
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +20 -20
- package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Textarea.js +3 -3
- package/template/node_modules/.vite/deps/_metadata.json +35 -35
- package/template/node_modules/.vite/deps/{chunk-SPYXUHEY.js → chunk-N6DVYEXK.js} +8 -8
- package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/template/src/resources/index.ts +4 -4
- package/template/src/resources/map-resource.test.tsx +95 -0
- package/template/src/resources/review-resource.test.tsx +538 -0
- package/template/dist/counter.js +0 -49
- package/template/dist/counter.json +0 -15
- package/template/src/resources/counter-resource.json +0 -12
- package/template/src/resources/counter-resource.test.tsx +0 -116
- package/template/src/resources/counter-resource.tsx +0 -101
- package/template/src/simulations/counter-show-simulation.json +0 -20
- /package/template/node_modules/.vite/deps/{chunk-SPYXUHEY.js.map → chunk-N6DVYEXK.js.map} +0 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { ReviewResource } from './review-resource';
|
|
4
|
+
|
|
5
|
+
// Mock sunpeak hooks
|
|
6
|
+
const mockSetWidgetState = vi.fn();
|
|
7
|
+
const mockRequestDisplayMode = vi.fn();
|
|
8
|
+
|
|
9
|
+
let mockWidgetData: Record<string, unknown> = { title: 'Test Review' };
|
|
10
|
+
let mockWidgetState: Record<string, unknown> = { decision: null, decidedAt: null };
|
|
11
|
+
let mockSafeArea: { insets: { top: number; bottom: number; left: number; right: number } } | null =
|
|
12
|
+
{
|
|
13
|
+
insets: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
14
|
+
};
|
|
15
|
+
let mockMaxHeight: number | null = 600;
|
|
16
|
+
let mockUserAgent: {
|
|
17
|
+
device: { type: 'desktop' | 'mobile' | 'tablet' | 'unknown' };
|
|
18
|
+
capabilities: { hover: boolean; touch: boolean };
|
|
19
|
+
} | null = {
|
|
20
|
+
device: { type: 'desktop' },
|
|
21
|
+
capabilities: { hover: true, touch: false },
|
|
22
|
+
};
|
|
23
|
+
let mockDisplayMode: 'inline' | 'fullscreen' = 'inline';
|
|
24
|
+
|
|
25
|
+
vi.mock('sunpeak', () => ({
|
|
26
|
+
useWidgetProps: (defaultFn: () => Record<string, unknown>) => {
|
|
27
|
+
const defaults = defaultFn();
|
|
28
|
+
return { ...defaults, ...mockWidgetData };
|
|
29
|
+
},
|
|
30
|
+
useWidgetState: () => [mockWidgetState, mockSetWidgetState],
|
|
31
|
+
useSafeArea: () => mockSafeArea,
|
|
32
|
+
useMaxHeight: () => mockMaxHeight,
|
|
33
|
+
useUserAgent: () => mockUserAgent,
|
|
34
|
+
useDisplayMode: () => mockDisplayMode,
|
|
35
|
+
useWidgetAPI: () => ({ requestDisplayMode: mockRequestDisplayMode }),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Mock Button component
|
|
39
|
+
vi.mock('@openai/apps-sdk-ui/components/Button', () => ({
|
|
40
|
+
Button: ({
|
|
41
|
+
children,
|
|
42
|
+
onClick,
|
|
43
|
+
variant,
|
|
44
|
+
color,
|
|
45
|
+
size,
|
|
46
|
+
className,
|
|
47
|
+
'aria-label': ariaLabel,
|
|
48
|
+
}: {
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
onClick?: () => void;
|
|
51
|
+
variant?: string;
|
|
52
|
+
color?: string;
|
|
53
|
+
size?: string;
|
|
54
|
+
className?: string;
|
|
55
|
+
'aria-label'?: string;
|
|
56
|
+
}) => (
|
|
57
|
+
<button
|
|
58
|
+
onClick={onClick}
|
|
59
|
+
data-variant={variant}
|
|
60
|
+
data-color={color}
|
|
61
|
+
data-size={size}
|
|
62
|
+
className={className}
|
|
63
|
+
aria-label={ariaLabel}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</button>
|
|
67
|
+
),
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Mock Icon component
|
|
71
|
+
vi.mock('@openai/apps-sdk-ui/components/Icon', () => ({
|
|
72
|
+
ExpandLg: ({ className }: { className?: string }) => (
|
|
73
|
+
<span data-testid="expand-icon" className={className}>
|
|
74
|
+
Expand
|
|
75
|
+
</span>
|
|
76
|
+
),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
describe('ReviewResource', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.clearAllMocks();
|
|
82
|
+
mockWidgetData = { title: 'Test Review' };
|
|
83
|
+
mockWidgetState = { decision: null, decidedAt: null };
|
|
84
|
+
mockSafeArea = { insets: { top: 0, bottom: 0, left: 0, right: 0 } };
|
|
85
|
+
mockMaxHeight = 600;
|
|
86
|
+
mockUserAgent = { device: { type: 'desktop' }, capabilities: { hover: true, touch: false } };
|
|
87
|
+
mockDisplayMode = 'inline';
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('Basic Rendering', () => {
|
|
91
|
+
it('renders with title', () => {
|
|
92
|
+
mockWidgetData = { title: 'Confirm Purchase' };
|
|
93
|
+
|
|
94
|
+
render(<ReviewResource />);
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText('Confirm Purchase')).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('renders with description', () => {
|
|
100
|
+
mockWidgetData = {
|
|
101
|
+
title: 'Test',
|
|
102
|
+
description: 'Please review the following items',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
render(<ReviewResource />);
|
|
106
|
+
|
|
107
|
+
expect(screen.getByText('Please review the following items')).toBeInTheDocument();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('renders empty state when no sections', () => {
|
|
111
|
+
mockWidgetData = { title: 'Test', sections: [] };
|
|
112
|
+
|
|
113
|
+
render(<ReviewResource />);
|
|
114
|
+
|
|
115
|
+
expect(screen.getByText('Nothing to confirm')).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('has the correct displayName', () => {
|
|
119
|
+
expect(ReviewResource.displayName).toBe('ReviewResource');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('Action Buttons', () => {
|
|
124
|
+
it('renders default button labels', () => {
|
|
125
|
+
render(<ReviewResource />);
|
|
126
|
+
|
|
127
|
+
expect(screen.getByText('Confirm')).toBeInTheDocument();
|
|
128
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('renders custom button labels', () => {
|
|
132
|
+
mockWidgetData = {
|
|
133
|
+
title: 'Test',
|
|
134
|
+
acceptLabel: 'Approve',
|
|
135
|
+
rejectLabel: 'Decline',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
render(<ReviewResource />);
|
|
139
|
+
|
|
140
|
+
expect(screen.getByText('Approve')).toBeInTheDocument();
|
|
141
|
+
expect(screen.getByText('Decline')).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('calls setWidgetState with accepted decision when accept clicked', () => {
|
|
145
|
+
render(<ReviewResource />);
|
|
146
|
+
|
|
147
|
+
const acceptButton = screen.getByText('Confirm');
|
|
148
|
+
fireEvent.click(acceptButton);
|
|
149
|
+
|
|
150
|
+
expect(mockSetWidgetState).toHaveBeenCalledWith(
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
decision: 'accepted',
|
|
153
|
+
decidedAt: expect.any(String),
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('calls setWidgetState with rejected decision when reject clicked', () => {
|
|
159
|
+
render(<ReviewResource />);
|
|
160
|
+
|
|
161
|
+
const rejectButton = screen.getByText('Cancel');
|
|
162
|
+
fireEvent.click(rejectButton);
|
|
163
|
+
|
|
164
|
+
expect(mockSetWidgetState).toHaveBeenCalledWith(
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
decision: 'rejected',
|
|
167
|
+
decidedAt: expect.any(String),
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('renders danger styling for accept button when acceptDanger is true', () => {
|
|
173
|
+
mockWidgetData = { title: 'Test', acceptDanger: true };
|
|
174
|
+
|
|
175
|
+
render(<ReviewResource />);
|
|
176
|
+
|
|
177
|
+
const acceptButton = screen.getByText('Confirm');
|
|
178
|
+
expect(acceptButton).toHaveAttribute('data-color', 'danger');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('renders primary styling for accept button by default', () => {
|
|
182
|
+
render(<ReviewResource />);
|
|
183
|
+
|
|
184
|
+
const acceptButton = screen.getByText('Confirm');
|
|
185
|
+
expect(acceptButton).toHaveAttribute('data-color', 'primary');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Decision State', () => {
|
|
190
|
+
it('shows accepted message after accepting', () => {
|
|
191
|
+
mockWidgetState = { decision: 'accepted', decidedAt: '2024-01-01T00:00:00.000Z' };
|
|
192
|
+
|
|
193
|
+
render(<ReviewResource />);
|
|
194
|
+
|
|
195
|
+
expect(screen.getByText('Confirmed')).toBeInTheDocument();
|
|
196
|
+
expect(screen.queryByText('Confirm')).not.toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('shows rejected message after rejecting', () => {
|
|
200
|
+
mockWidgetState = { decision: 'rejected', decidedAt: '2024-01-01T00:00:00.000Z' };
|
|
201
|
+
|
|
202
|
+
render(<ReviewResource />);
|
|
203
|
+
|
|
204
|
+
expect(screen.getByText('Cancelled')).toBeInTheDocument();
|
|
205
|
+
expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('shows custom accepted message', () => {
|
|
209
|
+
mockWidgetData = { title: 'Test', acceptedMessage: 'Order Placed!' };
|
|
210
|
+
mockWidgetState = { decision: 'accepted', decidedAt: '2024-01-01T00:00:00.000Z' };
|
|
211
|
+
|
|
212
|
+
render(<ReviewResource />);
|
|
213
|
+
|
|
214
|
+
expect(screen.getByText('Order Placed!')).toBeInTheDocument();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('shows custom rejected message', () => {
|
|
218
|
+
mockWidgetData = { title: 'Test', rejectedMessage: 'Order Cancelled' };
|
|
219
|
+
mockWidgetState = { decision: 'rejected', decidedAt: '2024-01-01T00:00:00.000Z' };
|
|
220
|
+
|
|
221
|
+
render(<ReviewResource />);
|
|
222
|
+
|
|
223
|
+
expect(screen.getByText('Order Cancelled')).toBeInTheDocument();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('shows decidedAt timestamp', () => {
|
|
227
|
+
mockWidgetState = { decision: 'accepted', decidedAt: '2024-01-15T10:30:00.000Z' };
|
|
228
|
+
|
|
229
|
+
render(<ReviewResource />);
|
|
230
|
+
|
|
231
|
+
// The timestamp should be displayed
|
|
232
|
+
const timestampElement = screen.getByText(/2024/);
|
|
233
|
+
expect(timestampElement).toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('Sections', () => {
|
|
238
|
+
it('renders details section', () => {
|
|
239
|
+
mockWidgetData = {
|
|
240
|
+
title: 'Test',
|
|
241
|
+
sections: [
|
|
242
|
+
{
|
|
243
|
+
title: 'Order Details',
|
|
244
|
+
type: 'details',
|
|
245
|
+
content: [
|
|
246
|
+
{ label: 'Item', value: 'Widget' },
|
|
247
|
+
{ label: 'Price', value: '$10.00' },
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
render(<ReviewResource />);
|
|
254
|
+
|
|
255
|
+
expect(screen.getByText('Order Details')).toBeInTheDocument();
|
|
256
|
+
expect(screen.getByText('Item')).toBeInTheDocument();
|
|
257
|
+
expect(screen.getByText('Widget')).toBeInTheDocument();
|
|
258
|
+
expect(screen.getByText('Price')).toBeInTheDocument();
|
|
259
|
+
expect(screen.getByText('$10.00')).toBeInTheDocument();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('renders items section', () => {
|
|
263
|
+
mockWidgetData = {
|
|
264
|
+
title: 'Test',
|
|
265
|
+
sections: [
|
|
266
|
+
{
|
|
267
|
+
title: 'Cart Items',
|
|
268
|
+
type: 'items',
|
|
269
|
+
content: [
|
|
270
|
+
{ id: '1', title: 'Product A', subtitle: 'Small', value: '$5.00' },
|
|
271
|
+
{ id: '2', title: 'Product B', badge: 'Sale', value: '$15.00' },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
render(<ReviewResource />);
|
|
278
|
+
|
|
279
|
+
expect(screen.getByText('Cart Items')).toBeInTheDocument();
|
|
280
|
+
expect(screen.getByText('Product A')).toBeInTheDocument();
|
|
281
|
+
expect(screen.getByText('Small')).toBeInTheDocument();
|
|
282
|
+
expect(screen.getByText('Product B')).toBeInTheDocument();
|
|
283
|
+
expect(screen.getByText('Sale')).toBeInTheDocument();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('renders changes section', () => {
|
|
287
|
+
mockWidgetData = {
|
|
288
|
+
title: 'Test',
|
|
289
|
+
sections: [
|
|
290
|
+
{
|
|
291
|
+
title: 'File Changes',
|
|
292
|
+
type: 'changes',
|
|
293
|
+
content: [
|
|
294
|
+
{ id: '1', type: 'create', path: 'src/new.ts', description: 'New file' },
|
|
295
|
+
{ id: '2', type: 'modify', path: 'src/old.ts', description: 'Updated imports' },
|
|
296
|
+
{ id: '3', type: 'delete', path: 'src/deprecated.ts', description: 'Removed' },
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
render(<ReviewResource />);
|
|
303
|
+
|
|
304
|
+
expect(screen.getByText('File Changes')).toBeInTheDocument();
|
|
305
|
+
expect(screen.getByText('src/new.ts')).toBeInTheDocument();
|
|
306
|
+
expect(screen.getByText('New file')).toBeInTheDocument();
|
|
307
|
+
expect(screen.getByText('src/old.ts')).toBeInTheDocument();
|
|
308
|
+
expect(screen.getByText('Updated imports')).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('renders preview section', () => {
|
|
312
|
+
mockWidgetData = {
|
|
313
|
+
title: 'Test',
|
|
314
|
+
sections: [
|
|
315
|
+
{
|
|
316
|
+
title: 'Preview',
|
|
317
|
+
type: 'preview',
|
|
318
|
+
content: 'This is the preview content that will be displayed.',
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
render(<ReviewResource />);
|
|
324
|
+
|
|
325
|
+
expect(screen.getByText('Preview')).toBeInTheDocument();
|
|
326
|
+
expect(
|
|
327
|
+
screen.getByText('This is the preview content that will be displayed.')
|
|
328
|
+
).toBeInTheDocument();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('renders summary section', () => {
|
|
332
|
+
mockWidgetData = {
|
|
333
|
+
title: 'Test',
|
|
334
|
+
sections: [
|
|
335
|
+
{
|
|
336
|
+
type: 'summary',
|
|
337
|
+
content: [
|
|
338
|
+
{ label: 'Subtotal', value: '$20.00' },
|
|
339
|
+
{ label: 'Total', value: '$25.00', emphasis: true },
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
render(<ReviewResource />);
|
|
346
|
+
|
|
347
|
+
expect(screen.getByText('Subtotal')).toBeInTheDocument();
|
|
348
|
+
expect(screen.getByText('$20.00')).toBeInTheDocument();
|
|
349
|
+
expect(screen.getByText('Total')).toBeInTheDocument();
|
|
350
|
+
expect(screen.getByText('$25.00')).toBeInTheDocument();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('Alerts', () => {
|
|
355
|
+
it('renders info alert', () => {
|
|
356
|
+
mockWidgetData = {
|
|
357
|
+
title: 'Test',
|
|
358
|
+
alerts: [{ type: 'info', message: 'This is informational' }],
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
render(<ReviewResource />);
|
|
362
|
+
|
|
363
|
+
expect(screen.getByText('This is informational')).toBeInTheDocument();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('renders warning alert', () => {
|
|
367
|
+
mockWidgetData = {
|
|
368
|
+
title: 'Test',
|
|
369
|
+
alerts: [{ type: 'warning', message: 'Please review carefully' }],
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
render(<ReviewResource />);
|
|
373
|
+
|
|
374
|
+
expect(screen.getByText('Please review carefully')).toBeInTheDocument();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('renders error alert', () => {
|
|
378
|
+
mockWidgetData = {
|
|
379
|
+
title: 'Test',
|
|
380
|
+
alerts: [{ type: 'error', message: 'Something went wrong' }],
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
render(<ReviewResource />);
|
|
384
|
+
|
|
385
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('renders success alert', () => {
|
|
389
|
+
mockWidgetData = {
|
|
390
|
+
title: 'Test',
|
|
391
|
+
alerts: [{ type: 'success', message: 'All checks passed' }],
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
render(<ReviewResource />);
|
|
395
|
+
|
|
396
|
+
expect(screen.getByText('All checks passed')).toBeInTheDocument();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('renders multiple alerts', () => {
|
|
400
|
+
mockWidgetData = {
|
|
401
|
+
title: 'Test',
|
|
402
|
+
alerts: [
|
|
403
|
+
{ type: 'warning', message: 'Warning message' },
|
|
404
|
+
{ type: 'info', message: 'Info message' },
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
render(<ReviewResource />);
|
|
409
|
+
|
|
410
|
+
expect(screen.getByText('Warning message')).toBeInTheDocument();
|
|
411
|
+
expect(screen.getByText('Info message')).toBeInTheDocument();
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('Safe Area and Layout', () => {
|
|
416
|
+
it('respects safe area insets', () => {
|
|
417
|
+
mockSafeArea = { insets: { top: 20, bottom: 30, left: 10, right: 15 } };
|
|
418
|
+
|
|
419
|
+
const { container } = render(<ReviewResource />);
|
|
420
|
+
const mainDiv = container.firstChild as HTMLElement;
|
|
421
|
+
|
|
422
|
+
expect(mainDiv).toHaveStyle({
|
|
423
|
+
paddingTop: '20px',
|
|
424
|
+
paddingBottom: '30px',
|
|
425
|
+
paddingLeft: '10px',
|
|
426
|
+
paddingRight: '15px',
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('respects maxHeight constraint', () => {
|
|
431
|
+
mockMaxHeight = 400;
|
|
432
|
+
|
|
433
|
+
const { container } = render(<ReviewResource />);
|
|
434
|
+
const mainDiv = container.firstChild as HTMLElement;
|
|
435
|
+
|
|
436
|
+
expect(mainDiv).toHaveStyle({
|
|
437
|
+
maxHeight: '400px',
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('handles null safe area', () => {
|
|
442
|
+
mockSafeArea = null;
|
|
443
|
+
|
|
444
|
+
const { container } = render(<ReviewResource />);
|
|
445
|
+
const mainDiv = container.firstChild as HTMLElement;
|
|
446
|
+
|
|
447
|
+
expect(mainDiv).toHaveStyle({
|
|
448
|
+
paddingTop: '0px',
|
|
449
|
+
paddingBottom: '0px',
|
|
450
|
+
paddingLeft: '0px',
|
|
451
|
+
paddingRight: '0px',
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('handles null maxHeight', () => {
|
|
456
|
+
mockMaxHeight = null;
|
|
457
|
+
|
|
458
|
+
const { container } = render(<ReviewResource />);
|
|
459
|
+
const mainDiv = container.firstChild as HTMLElement;
|
|
460
|
+
|
|
461
|
+
expect(mainDiv.style.maxHeight).toBe('');
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('Touch Device Support', () => {
|
|
466
|
+
it('renders larger buttons for touch devices', () => {
|
|
467
|
+
mockUserAgent = { device: { type: 'mobile' }, capabilities: { hover: false, touch: true } };
|
|
468
|
+
|
|
469
|
+
render(<ReviewResource />);
|
|
470
|
+
|
|
471
|
+
const acceptButton = screen.getByText('Confirm');
|
|
472
|
+
const rejectButton = screen.getByText('Cancel');
|
|
473
|
+
|
|
474
|
+
expect(acceptButton).toHaveAttribute('data-size', 'lg');
|
|
475
|
+
expect(rejectButton).toHaveAttribute('data-size', 'lg');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('renders standard buttons for non-touch devices', () => {
|
|
479
|
+
mockUserAgent = { device: { type: 'desktop' }, capabilities: { hover: true, touch: false } };
|
|
480
|
+
|
|
481
|
+
render(<ReviewResource />);
|
|
482
|
+
|
|
483
|
+
const acceptButton = screen.getByText('Confirm');
|
|
484
|
+
const rejectButton = screen.getByText('Cancel');
|
|
485
|
+
|
|
486
|
+
expect(acceptButton).toHaveAttribute('data-size', 'md');
|
|
487
|
+
expect(rejectButton).toHaveAttribute('data-size', 'md');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('handles null userAgent gracefully', () => {
|
|
491
|
+
mockUserAgent = null;
|
|
492
|
+
|
|
493
|
+
render(<ReviewResource />);
|
|
494
|
+
|
|
495
|
+
const acceptButton = screen.getByText('Confirm');
|
|
496
|
+
expect(acceptButton).toHaveAttribute('data-size', 'md');
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('Fullscreen Mode', () => {
|
|
501
|
+
it('shows expand button when not in fullscreen mode', () => {
|
|
502
|
+
mockDisplayMode = 'inline';
|
|
503
|
+
|
|
504
|
+
render(<ReviewResource />);
|
|
505
|
+
|
|
506
|
+
expect(screen.getByTestId('expand-icon')).toBeInTheDocument();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('hides expand button when in fullscreen mode', () => {
|
|
510
|
+
mockDisplayMode = 'fullscreen';
|
|
511
|
+
|
|
512
|
+
render(<ReviewResource />);
|
|
513
|
+
|
|
514
|
+
expect(screen.queryByTestId('expand-icon')).not.toBeInTheDocument();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('calls requestDisplayMode when expand button clicked', () => {
|
|
518
|
+
mockDisplayMode = 'inline';
|
|
519
|
+
|
|
520
|
+
render(<ReviewResource />);
|
|
521
|
+
|
|
522
|
+
const expandButton = screen.getByLabelText('Enter fullscreen');
|
|
523
|
+
fireEvent.click(expandButton);
|
|
524
|
+
|
|
525
|
+
expect(mockRequestDisplayMode).toHaveBeenCalledWith({ mode: 'fullscreen' });
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
describe('Ref Forwarding', () => {
|
|
530
|
+
it('forwards ref to the container div', () => {
|
|
531
|
+
const ref = vi.fn();
|
|
532
|
+
render(<ReviewResource ref={ref} />);
|
|
533
|
+
|
|
534
|
+
expect(ref).toHaveBeenCalled();
|
|
535
|
+
expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLDivElement);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
});
|