sunpeak 0.8.5 → 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 +6 -8
- 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 +49 -0
- package/template/dist/{confirmation.json → review.json} +4 -4
- package/template/node_modules/.vite/deps/_metadata.json +19 -19
- 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/{confirmation-resource.json → review-resource.json} +3 -3
- package/template/src/resources/review-resource.test.tsx +538 -0
- package/template/src/resources/{confirmation-resource.tsx → review-resource.tsx} +20 -20
- package/template/src/simulations/{confirmation-diff-simulation.json → review-diff-simulation.json} +4 -4
- package/template/src/simulations/{confirmation-post-simulation.json → review-post-simulation.json} +4 -4
- package/template/src/simulations/{confirmation-purchase-simulation.json → review-purchase-simulation.json} +4 -4
- package/template/dist/confirmation.js +0 -49
- 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
|
@@ -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
|
+
});
|
|
@@ -12,14 +12,14 @@ import { Button } from '@openai/apps-sdk-ui/components/Button';
|
|
|
12
12
|
import { ExpandLg } from '@openai/apps-sdk-ui/components/Icon';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Production-ready
|
|
15
|
+
* Production-ready Review Resource
|
|
16
16
|
*
|
|
17
|
-
* A flexible
|
|
18
|
-
* - Purchase
|
|
19
|
-
* - Code change
|
|
20
|
-
* - Social media post
|
|
21
|
-
* - Booking
|
|
22
|
-
* - Generic action
|
|
17
|
+
* A flexible review dialog that adapts to various use cases:
|
|
18
|
+
* - Purchase reviews (items, totals, payment)
|
|
19
|
+
* - Code change reviews (file changes with diffs)
|
|
20
|
+
* - Social media post reviews (content preview)
|
|
21
|
+
* - Booking reviews (details, dates, prices)
|
|
22
|
+
* - Generic action reviews (simple approve/reject)
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
// ============================================================================
|
|
@@ -75,15 +75,15 @@ interface Section {
|
|
|
75
75
|
content: Detail[] | Item[] | Change[] | string;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Tool call configuration for domain-specific
|
|
79
|
-
interface
|
|
78
|
+
/** Tool call configuration for domain-specific review actions */
|
|
79
|
+
interface ReviewTool {
|
|
80
80
|
/** Tool name to call (e.g., "complete_purchase", "publish_post") */
|
|
81
81
|
name: string;
|
|
82
82
|
/** Additional arguments to pass to the tool */
|
|
83
83
|
arguments?: Record<string, unknown>;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
interface
|
|
86
|
+
interface ReviewData extends Record<string, unknown> {
|
|
87
87
|
/** Main title */
|
|
88
88
|
title: string;
|
|
89
89
|
/** Optional description below title */
|
|
@@ -102,11 +102,11 @@ interface ConfirmationData extends Record<string, unknown> {
|
|
|
102
102
|
acceptedMessage?: string;
|
|
103
103
|
/** Message shown after rejecting */
|
|
104
104
|
rejectedMessage?: string;
|
|
105
|
-
/** Domain-specific tool to call on
|
|
106
|
-
|
|
105
|
+
/** Domain-specific tool to call on review */
|
|
106
|
+
reviewTool?: ReviewTool;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
interface
|
|
109
|
+
interface ReviewState extends Record<string, unknown> {
|
|
110
110
|
decision?: 'accepted' | 'rejected' | null;
|
|
111
111
|
decidedAt?: string | null;
|
|
112
112
|
}
|
|
@@ -306,13 +306,13 @@ function AlertBanner({ alert }: { alert: Alert }) {
|
|
|
306
306
|
// Main Component
|
|
307
307
|
// ============================================================================
|
|
308
308
|
|
|
309
|
-
export const
|
|
310
|
-
const data = useWidgetProps<
|
|
311
|
-
title: '
|
|
309
|
+
export const ReviewResource = React.forwardRef<HTMLDivElement>((_props, ref) => {
|
|
310
|
+
const data = useWidgetProps<ReviewData>(() => ({
|
|
311
|
+
title: 'Review',
|
|
312
312
|
sections: [],
|
|
313
313
|
}));
|
|
314
314
|
|
|
315
|
-
const [widgetState, setWidgetState] = useWidgetState<
|
|
315
|
+
const [widgetState, setWidgetState] = useWidgetState<ReviewState>(() => ({
|
|
316
316
|
decision: null,
|
|
317
317
|
decidedAt: null,
|
|
318
318
|
}));
|
|
@@ -338,7 +338,7 @@ export const ConfirmationResource = React.forwardRef<HTMLDivElement>((_props, re
|
|
|
338
338
|
decidedAt,
|
|
339
339
|
});
|
|
340
340
|
|
|
341
|
-
const tool = data.
|
|
341
|
+
const tool = data.reviewTool;
|
|
342
342
|
if (tool) {
|
|
343
343
|
console.log('callTool', {
|
|
344
344
|
name: tool.name,
|
|
@@ -358,7 +358,7 @@ export const ConfirmationResource = React.forwardRef<HTMLDivElement>((_props, re
|
|
|
358
358
|
decidedAt,
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
-
const tool = data.
|
|
361
|
+
const tool = data.reviewTool;
|
|
362
362
|
if (tool) {
|
|
363
363
|
console.log('callTool', {
|
|
364
364
|
name: tool.name,
|
|
@@ -476,4 +476,4 @@ export const ConfirmationResource = React.forwardRef<HTMLDivElement>((_props, re
|
|
|
476
476
|
</div>
|
|
477
477
|
);
|
|
478
478
|
});
|
|
479
|
-
|
|
479
|
+
ReviewResource.displayName = 'ReviewResource';
|
package/template/src/simulations/{confirmation-diff-simulation.json → review-diff-simulation.json}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"userMessage": "I'd like to refactor the authentication module",
|
|
3
3
|
"tool": {
|
|
4
|
-
"name": "diff-
|
|
5
|
-
"description": "Show a
|
|
4
|
+
"name": "diff-review",
|
|
5
|
+
"description": "Show a review dialog for a proposed code diff",
|
|
6
6
|
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
|
|
7
|
-
"title": "Diff
|
|
7
|
+
"title": "Diff Review",
|
|
8
8
|
"annotations": { "readOnlyHint": false },
|
|
9
9
|
"_meta": {
|
|
10
10
|
"openai/toolInvocation/invoking": "Preparing changes",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"rejectLabel": "Cancel",
|
|
63
63
|
"acceptedMessage": "Changes applied",
|
|
64
64
|
"rejectedMessage": "Changes cancelled",
|
|
65
|
-
"
|
|
65
|
+
"reviewTool": {
|
|
66
66
|
"name": "apply_changes",
|
|
67
67
|
"arguments": {
|
|
68
68
|
"changesetId": "cs_789",
|
package/template/src/simulations/{confirmation-post-simulation.json → review-post-simulation.json}
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"userMessage": "Post this to my social media",
|
|
3
3
|
"tool": {
|
|
4
|
-
"name": "
|
|
5
|
-
"description": "
|
|
4
|
+
"name": "review-post",
|
|
5
|
+
"description": "Review a social media post before publishing",
|
|
6
6
|
"inputSchema": { "type": "object", "properties": {}, "additionalProperties": false },
|
|
7
|
-
"title": "
|
|
7
|
+
"title": "Review Post",
|
|
8
8
|
"annotations": { "readOnlyHint": false },
|
|
9
9
|
"_meta": {
|
|
10
10
|
"openai/toolInvocation/invoking": "Preparing post",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"rejectLabel": "Cancel",
|
|
44
44
|
"acceptedMessage": "Post published!",
|
|
45
45
|
"rejectedMessage": "Post cancelled",
|
|
46
|
-
"
|
|
46
|
+
"reviewTool": {
|
|
47
47
|
"name": "publish_post",
|
|
48
48
|
"arguments": {
|
|
49
49
|
"postId": "draft_456",
|