sunpeak 0.6.6 → 0.7.9
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/README.md +3 -3
- package/bin/commands/build.mjs +22 -5
- package/bin/commands/deploy.mjs +108 -0
- package/bin/commands/login.mjs +217 -0
- package/bin/commands/logout.mjs +87 -0
- package/bin/commands/pull.mjs +254 -0
- package/bin/commands/push.mjs +347 -0
- package/bin/sunpeak.js +85 -2
- package/dist/mcp/entry.cjs +2 -2
- package/dist/mcp/entry.cjs.map +1 -1
- package/dist/mcp/entry.js +2 -2
- package/dist/mcp/entry.js.map +1 -1
- package/dist/mcp/index.cjs +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/{server-CQGbJWbk.cjs → server-BOYwNazb.cjs} +25 -26
- package/dist/{server-CQGbJWbk.cjs.map → server-BOYwNazb.cjs.map} +1 -1
- package/dist/{server-DGCvp1RA.js → server-C6vMGV6H.js} +25 -26
- package/dist/{server-DGCvp1RA.js.map → server-C6vMGV6H.js.map} +1 -1
- package/package.json +1 -1
- package/template/.sunpeak/dev.tsx +8 -10
- package/template/README.md +4 -4
- package/template/dist/albums.json +15 -0
- package/template/dist/carousel.json +15 -0
- package/template/dist/counter.json +10 -0
- package/template/dist/map.json +19 -0
- package/template/index.html +1 -1
- package/template/node_modules/.vite/deps/_metadata.json +19 -19
- package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/template/src/components/map/map-view.test.tsx +146 -0
- package/template/src/components/map/place-card.test.tsx +76 -0
- package/template/src/components/map/place-carousel.test.tsx +84 -0
- package/template/src/components/map/place-inspector.test.tsx +91 -0
- package/template/src/components/map/place-list.test.tsx +97 -0
- package/template/src/resources/albums-resource.json +12 -0
- package/template/src/resources/carousel-resource.json +12 -0
- package/template/src/resources/counter-resource.json +9 -0
- package/template/src/resources/map-resource.json +13 -0
- package/template/src/simulations/albums-simulation.ts +3 -15
- package/template/src/simulations/carousel-simulation.ts +3 -15
- package/template/src/simulations/counter-simulation.ts +3 -15
- package/template/src/simulations/map-simulation.ts +5 -28
- package/template/src/simulations/widget-config.ts +0 -42
- /package/template/dist/{chatgpt/albums.js → albums.js} +0 -0
- /package/template/dist/{chatgpt/carousel.js → carousel.js} +0 -0
- /package/template/dist/{chatgpt/counter.js → counter.js} +0 -0
- /package/template/dist/{chatgpt/map.js → map.js} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { MapView } from './map-view';
|
|
4
|
+
import type { Place } from '../../simulations/map-simulation';
|
|
5
|
+
|
|
6
|
+
// Mock mapbox-gl
|
|
7
|
+
vi.mock('mapbox-gl', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
accessToken: '',
|
|
10
|
+
Map: class MockMap {
|
|
11
|
+
on = vi.fn();
|
|
12
|
+
once = vi.fn();
|
|
13
|
+
remove = vi.fn();
|
|
14
|
+
resize = vi.fn();
|
|
15
|
+
loaded = vi.fn().mockReturnValue(true);
|
|
16
|
+
flyTo = vi.fn();
|
|
17
|
+
fitBounds = vi.fn();
|
|
18
|
+
},
|
|
19
|
+
Marker: class MockMarker {
|
|
20
|
+
setLngLat = vi.fn().mockReturnThis();
|
|
21
|
+
addTo = vi.fn().mockReturnThis();
|
|
22
|
+
remove = vi.fn();
|
|
23
|
+
getElement = vi.fn().mockReturnValue(document.createElement('div'));
|
|
24
|
+
},
|
|
25
|
+
LngLatBounds: class MockLngLatBounds {
|
|
26
|
+
extend = vi.fn().mockReturnThis();
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// Mock sunpeak hook
|
|
32
|
+
vi.mock('sunpeak', () => ({
|
|
33
|
+
useMaxHeight: vi.fn().mockReturnValue(600),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
describe('MapView', () => {
|
|
37
|
+
const mockPlaces: Place[] = [
|
|
38
|
+
{
|
|
39
|
+
id: 'place-1',
|
|
40
|
+
name: 'First Place',
|
|
41
|
+
coords: [-122.4194, 37.7749],
|
|
42
|
+
description: 'First test place',
|
|
43
|
+
city: 'San Francisco',
|
|
44
|
+
rating: 4.5,
|
|
45
|
+
price: '$$',
|
|
46
|
+
thumbnail: 'https://example.com/1.jpg',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'place-2',
|
|
50
|
+
name: 'Second Place',
|
|
51
|
+
coords: [-122.4094, 37.7849],
|
|
52
|
+
description: 'Second test place',
|
|
53
|
+
city: 'Oakland',
|
|
54
|
+
rating: 4.2,
|
|
55
|
+
price: '$',
|
|
56
|
+
thumbnail: 'https://example.com/2.jpg',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders map container', () => {
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<MapView
|
|
67
|
+
places={mockPlaces}
|
|
68
|
+
selectedPlace={null}
|
|
69
|
+
isFullscreen={false}
|
|
70
|
+
onSelectPlace={vi.fn()}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const mapContainer = container.querySelector('div[class*="absolute"]');
|
|
75
|
+
expect(mapContainer).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('applies different styles for fullscreen mode', () => {
|
|
79
|
+
const { container, rerender } = render(
|
|
80
|
+
<MapView
|
|
81
|
+
places={mockPlaces}
|
|
82
|
+
selectedPlace={null}
|
|
83
|
+
isFullscreen={false}
|
|
84
|
+
onSelectPlace={vi.fn()}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
let mapWrapper = container.firstChild as HTMLElement;
|
|
89
|
+
expect(mapWrapper.className).not.toContain('left-[340px]');
|
|
90
|
+
|
|
91
|
+
rerender(
|
|
92
|
+
<MapView
|
|
93
|
+
places={mockPlaces}
|
|
94
|
+
selectedPlace={null}
|
|
95
|
+
isFullscreen={true}
|
|
96
|
+
onSelectPlace={vi.fn()}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
mapWrapper = container.firstChild as HTMLElement;
|
|
101
|
+
expect(mapWrapper.className).toContain('left-[340px]');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handles empty places array gracefully', () => {
|
|
105
|
+
const { container } = render(
|
|
106
|
+
<MapView places={[]} selectedPlace={null} isFullscreen={false} onSelectPlace={vi.fn()} />
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('handles places with invalid coordinates gracefully', () => {
|
|
113
|
+
const invalidPlaces: Place[] = [
|
|
114
|
+
{
|
|
115
|
+
...mockPlaces[0],
|
|
116
|
+
coords: [] as unknown as [number, number],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const { container } = render(
|
|
121
|
+
<MapView
|
|
122
|
+
places={invalidPlaces}
|
|
123
|
+
selectedPlace={null}
|
|
124
|
+
isFullscreen={false}
|
|
125
|
+
onSelectPlace={vi.fn()}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('accepts custom className prop', () => {
|
|
133
|
+
const { container } = render(
|
|
134
|
+
<MapView
|
|
135
|
+
places={mockPlaces}
|
|
136
|
+
selectedPlace={null}
|
|
137
|
+
isFullscreen={false}
|
|
138
|
+
onSelectPlace={vi.fn()}
|
|
139
|
+
className="custom-class"
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const mapWrapper = container.firstChild as HTMLElement;
|
|
144
|
+
expect(mapWrapper.className).toContain('custom-class');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { PlaceCard } from './place-card';
|
|
4
|
+
import type { Place } from '../../simulations/map-simulation';
|
|
5
|
+
|
|
6
|
+
describe('PlaceCard', () => {
|
|
7
|
+
const mockPlace: Place = {
|
|
8
|
+
id: 'test-place',
|
|
9
|
+
name: 'Test Pizza Place',
|
|
10
|
+
coords: [-122.4194, 37.7749],
|
|
11
|
+
description: 'Delicious test pizza',
|
|
12
|
+
city: 'San Francisco',
|
|
13
|
+
rating: 4.5,
|
|
14
|
+
price: '$$',
|
|
15
|
+
thumbnail: 'https://example.com/test.jpg',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
it('renders place information correctly', () => {
|
|
19
|
+
render(<PlaceCard place={mockPlace} />);
|
|
20
|
+
|
|
21
|
+
expect(screen.getByText('Test Pizza Place')).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText('Delicious test pizza')).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByText('4.5')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('$$', { exact: false })).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders thumbnail with correct attributes', () => {
|
|
28
|
+
render(<PlaceCard place={mockPlace} />);
|
|
29
|
+
|
|
30
|
+
const image = screen.getByRole('img');
|
|
31
|
+
expect(image).toHaveAttribute('src', 'https://example.com/test.jpg');
|
|
32
|
+
expect(image).toHaveAttribute('alt', 'Test Pizza Place');
|
|
33
|
+
expect(image).toHaveAttribute('loading', 'lazy');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('calls onClick handler when clicked', () => {
|
|
37
|
+
const onClick = vi.fn();
|
|
38
|
+
render(<PlaceCard place={mockPlace} onClick={onClick} />);
|
|
39
|
+
|
|
40
|
+
const button = screen.getByRole('button');
|
|
41
|
+
fireEvent.click(button);
|
|
42
|
+
|
|
43
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('applies selected styles when isSelected is true', () => {
|
|
47
|
+
const { container, rerender } = render(<PlaceCard place={mockPlace} isSelected={false} />);
|
|
48
|
+
|
|
49
|
+
let card = container.firstChild as HTMLElement;
|
|
50
|
+
// Should have hover styles but not the active background
|
|
51
|
+
expect(card.className).toContain('hover:bg-black/5');
|
|
52
|
+
// Check that it doesn't have the non-hover background class by looking at the full class string
|
|
53
|
+
const classesWithoutHover = card.className.replace(/hover:[^\s]+/g, '');
|
|
54
|
+
expect(classesWithoutHover).not.toContain('bg-black/5');
|
|
55
|
+
|
|
56
|
+
rerender(<PlaceCard place={mockPlace} isSelected={true} />);
|
|
57
|
+
card = container.firstChild as HTMLElement;
|
|
58
|
+
// When selected, should have both hover and non-hover background classes
|
|
59
|
+
expect(card.className).toContain('bg-black/5 dark:bg-white/5');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('formats rating to one decimal place', () => {
|
|
63
|
+
const placeWithRating = { ...mockPlace, rating: 4.789 };
|
|
64
|
+
render(<PlaceCard place={placeWithRating} />);
|
|
65
|
+
|
|
66
|
+
expect(screen.getByText('4.8')).toBeInTheDocument();
|
|
67
|
+
expect(screen.queryByText('4.789')).not.toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders price when provided', () => {
|
|
71
|
+
const placeWithPrice = { ...mockPlace, price: '$$$' };
|
|
72
|
+
render(<PlaceCard place={placeWithPrice} />);
|
|
73
|
+
|
|
74
|
+
expect(screen.getByText('$$$', { exact: false })).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { PlaceCarousel } from './place-carousel';
|
|
4
|
+
import type { Place } from '../../simulations/map-simulation';
|
|
5
|
+
|
|
6
|
+
describe('PlaceCarousel', () => {
|
|
7
|
+
const mockPlaces: Place[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 'place-1',
|
|
10
|
+
name: 'First Place',
|
|
11
|
+
coords: [-122.4194, 37.7749],
|
|
12
|
+
description: 'First test place',
|
|
13
|
+
city: 'San Francisco',
|
|
14
|
+
rating: 4.5,
|
|
15
|
+
price: '$$',
|
|
16
|
+
thumbnail: 'https://example.com/1.jpg',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'place-2',
|
|
20
|
+
name: 'Second Place',
|
|
21
|
+
coords: [-122.4094, 37.7849],
|
|
22
|
+
description: 'Second test place',
|
|
23
|
+
city: 'Oakland',
|
|
24
|
+
rating: 4.2,
|
|
25
|
+
price: '$',
|
|
26
|
+
thumbnail: 'https://example.com/2.jpg',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
it('renders all places in the carousel', () => {
|
|
31
|
+
render(<PlaceCarousel places={mockPlaces} selectedId={null} onSelect={vi.fn()} />);
|
|
32
|
+
|
|
33
|
+
expect(screen.getByText('First Place')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Second Place')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('calls onSelect with correct place when a place is clicked', () => {
|
|
38
|
+
const onSelect = vi.fn();
|
|
39
|
+
render(<PlaceCarousel places={mockPlaces} selectedId={null} onSelect={onSelect} />);
|
|
40
|
+
|
|
41
|
+
const firstPlace = screen.getByText('First Place');
|
|
42
|
+
fireEvent.click(firstPlace);
|
|
43
|
+
|
|
44
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(onSelect).toHaveBeenCalledWith(mockPlaces[0]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('highlights selected place', () => {
|
|
49
|
+
const { rerender } = render(
|
|
50
|
+
<PlaceCarousel places={mockPlaces} selectedId={null} onSelect={vi.fn()} />
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
rerender(<PlaceCarousel places={mockPlaces} selectedId="place-1" onSelect={vi.fn()} />);
|
|
54
|
+
|
|
55
|
+
const firstPlace = screen.getByText('First Place');
|
|
56
|
+
const card = firstPlace.closest('div[class*="rounded-2xl"]');
|
|
57
|
+
expect(card?.className).toContain('bg-black/5');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders empty carousel when no places provided', () => {
|
|
61
|
+
render(<PlaceCarousel places={[]} selectedId={null} onSelect={vi.fn()} />);
|
|
62
|
+
|
|
63
|
+
expect(screen.queryByText('First Place')).not.toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders each place card with shadow and ring styles', () => {
|
|
67
|
+
const { container } = render(
|
|
68
|
+
<PlaceCarousel places={mockPlaces} selectedId={null} onSelect={vi.fn()} />
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const cards = container.querySelectorAll('div[class*="shadow-xl"]');
|
|
72
|
+
expect(cards.length).toBe(mockPlaces.length);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders carousel at bottom of viewport with correct positioning', () => {
|
|
76
|
+
const { container } = render(
|
|
77
|
+
<PlaceCarousel places={mockPlaces} selectedId={null} onSelect={vi.fn()} />
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const carousel = container.firstChild as HTMLElement;
|
|
81
|
+
expect(carousel.className).toContain('absolute');
|
|
82
|
+
expect(carousel.className).toContain('bottom-0');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { PlaceInspector } from './place-inspector';
|
|
4
|
+
import type { Place } from '../../simulations/map-simulation';
|
|
5
|
+
|
|
6
|
+
describe('PlaceInspector', () => {
|
|
7
|
+
const mockPlace: Place = {
|
|
8
|
+
id: 'test-place',
|
|
9
|
+
name: 'Test Pizza Place',
|
|
10
|
+
coords: [-122.4194, 37.7749],
|
|
11
|
+
description: 'Delicious test pizza',
|
|
12
|
+
city: 'San Francisco',
|
|
13
|
+
rating: 4.5,
|
|
14
|
+
price: '$$',
|
|
15
|
+
thumbnail: 'https://example.com/test.jpg',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
it('renders place details correctly', () => {
|
|
19
|
+
render(<PlaceInspector place={mockPlace} onClose={vi.fn()} />);
|
|
20
|
+
|
|
21
|
+
expect(screen.getByText('Test Pizza Place')).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText('4.5')).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByText('$$', { exact: false })).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('San Francisco', { exact: false })).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders place thumbnail with correct attributes', () => {
|
|
28
|
+
render(<PlaceInspector place={mockPlace} onClose={vi.fn()} />);
|
|
29
|
+
|
|
30
|
+
const image = screen.getByRole('img', { name: 'Test Pizza Place' });
|
|
31
|
+
expect(image).toHaveAttribute('src', 'https://example.com/test.jpg');
|
|
32
|
+
expect(image).toHaveAttribute('alt', 'Test Pizza Place');
|
|
33
|
+
expect(image).toHaveAttribute('loading', 'lazy');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('calls onClose when close button is clicked', () => {
|
|
37
|
+
const onClose = vi.fn();
|
|
38
|
+
render(<PlaceInspector place={mockPlace} onClose={onClose} />);
|
|
39
|
+
|
|
40
|
+
const closeButton = screen.getByLabelText('Close details');
|
|
41
|
+
fireEvent.click(closeButton);
|
|
42
|
+
|
|
43
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('renders action buttons', () => {
|
|
47
|
+
render(<PlaceInspector place={mockPlace} onClose={vi.fn()} />);
|
|
48
|
+
|
|
49
|
+
expect(screen.getByText('Add to favorites')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText('Contact')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders reviews section with multiple reviews', () => {
|
|
54
|
+
render(<PlaceInspector place={mockPlace} onClose={vi.fn()} />);
|
|
55
|
+
|
|
56
|
+
expect(screen.getByText('Reviews')).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText('Leo M.')).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText('Priya S.')).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByText('Maya R.')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('renders review content correctly', () => {
|
|
63
|
+
render(<PlaceInspector place={mockPlace} onClose={vi.fn()} />);
|
|
64
|
+
|
|
65
|
+
expect(
|
|
66
|
+
screen.getByText('Fantastic crust and balanced toppings. The marinara is spot on!')
|
|
67
|
+
).toBeInTheDocument();
|
|
68
|
+
expect(
|
|
69
|
+
screen.getByText('Cozy vibe and friendly staff. Quick service on a Friday night.')
|
|
70
|
+
).toBeInTheDocument();
|
|
71
|
+
expect(
|
|
72
|
+
screen.getByText('Great for sharing. Will definitely come back with friends.')
|
|
73
|
+
).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('renders extended description', () => {
|
|
77
|
+
render(<PlaceInspector place={mockPlace} onClose={vi.fn()} />);
|
|
78
|
+
|
|
79
|
+
expect(
|
|
80
|
+
screen.getByText(/Enjoy a slice at one of SF's favorites/, { exact: false })
|
|
81
|
+
).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('formats rating to one decimal place', () => {
|
|
85
|
+
const placeWithRating = { ...mockPlace, rating: 4.789 };
|
|
86
|
+
render(<PlaceInspector place={placeWithRating} onClose={vi.fn()} />);
|
|
87
|
+
|
|
88
|
+
expect(screen.getByText('4.8')).toBeInTheDocument();
|
|
89
|
+
expect(screen.queryByText('4.789')).not.toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { PlaceList } from './place-list';
|
|
4
|
+
import type { Place } from '../../simulations/map-simulation';
|
|
5
|
+
|
|
6
|
+
describe('PlaceList', () => {
|
|
7
|
+
const mockPlaces: Place[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 'place-1',
|
|
10
|
+
name: 'First Place',
|
|
11
|
+
coords: [-122.4194, 37.7749],
|
|
12
|
+
description: 'First test place',
|
|
13
|
+
city: 'San Francisco',
|
|
14
|
+
rating: 4.5,
|
|
15
|
+
price: '$$',
|
|
16
|
+
thumbnail: 'https://example.com/1.jpg',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'place-2',
|
|
20
|
+
name: 'Second Place',
|
|
21
|
+
coords: [-122.4094, 37.7849],
|
|
22
|
+
description: 'Second test place',
|
|
23
|
+
city: 'Oakland',
|
|
24
|
+
rating: 4.2,
|
|
25
|
+
price: '$',
|
|
26
|
+
thumbnail: 'https://example.com/2.jpg',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'place-3',
|
|
30
|
+
name: 'Third Place',
|
|
31
|
+
coords: [-122.4294, 37.7649],
|
|
32
|
+
description: 'Third test place',
|
|
33
|
+
city: 'Berkeley',
|
|
34
|
+
rating: 4.8,
|
|
35
|
+
price: '$$$',
|
|
36
|
+
thumbnail: 'https://example.com/3.jpg',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
it('renders all places in the list', () => {
|
|
41
|
+
render(<PlaceList places={mockPlaces} selectedId={null} onSelect={vi.fn()} />);
|
|
42
|
+
|
|
43
|
+
expect(screen.getByText('First Place')).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText('Second Place')).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText('Third Place')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('displays correct number of results in header', () => {
|
|
49
|
+
render(<PlaceList places={mockPlaces} selectedId={null} onSelect={vi.fn()} />);
|
|
50
|
+
|
|
51
|
+
expect(screen.getByText('3 results')).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('calls onSelect with correct place when a place is clicked', () => {
|
|
55
|
+
const onSelect = vi.fn();
|
|
56
|
+
render(<PlaceList places={mockPlaces} selectedId={null} onSelect={onSelect} />);
|
|
57
|
+
|
|
58
|
+
const firstPlace = screen.getByText('First Place');
|
|
59
|
+
fireEvent.click(firstPlace);
|
|
60
|
+
|
|
61
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(onSelect).toHaveBeenCalledWith(mockPlaces[0]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('highlights selected place', () => {
|
|
66
|
+
const { rerender } = render(
|
|
67
|
+
<PlaceList places={mockPlaces} selectedId={null} onSelect={vi.fn()} />
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
rerender(<PlaceList places={mockPlaces} selectedId="place-2" onSelect={vi.fn()} />);
|
|
71
|
+
|
|
72
|
+
const secondPlace = screen.getByText('Second Place');
|
|
73
|
+
const card = secondPlace.closest('div[class*="rounded-2xl"]');
|
|
74
|
+
expect(card?.className).toContain('bg-black/5');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('renders empty list when no places provided', () => {
|
|
78
|
+
render(<PlaceList places={[]} selectedId={null} onSelect={vi.fn()} />);
|
|
79
|
+
|
|
80
|
+
expect(screen.getByText('0 results')).toBeInTheDocument();
|
|
81
|
+
expect(screen.queryByText('First Place')).not.toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('maintains selection when places update', () => {
|
|
85
|
+
const onSelect = vi.fn();
|
|
86
|
+
const { rerender } = render(
|
|
87
|
+
<PlaceList places={mockPlaces} selectedId="place-2" onSelect={onSelect} />
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const updatedPlaces = [...mockPlaces].reverse();
|
|
91
|
+
rerender(<PlaceList places={updatedPlaces} selectedId="place-2" onSelect={onSelect} />);
|
|
92
|
+
|
|
93
|
+
const secondPlace = screen.getByText('Second Place');
|
|
94
|
+
const card = secondPlace.closest('div[class*="rounded-2xl"]');
|
|
95
|
+
expect(card?.className).toContain('bg-black/5');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "albums",
|
|
3
|
+
"title": "Albums",
|
|
4
|
+
"description": "Show photo albums widget",
|
|
5
|
+
"mimeType": "text/html+skybridge",
|
|
6
|
+
"_meta": {
|
|
7
|
+
"openai/widgetDomain": "https://sunpeak.ai",
|
|
8
|
+
"openai/widgetCSP": {
|
|
9
|
+
"resource_domains": ["https://*.oaistatic.com"]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "carousel",
|
|
3
|
+
"title": "Carousel",
|
|
4
|
+
"description": "Show popular places to visit widget",
|
|
5
|
+
"mimeType": "text/html+skybridge",
|
|
6
|
+
"_meta": {
|
|
7
|
+
"openai/widgetDomain": "https://sunpeak.ai",
|
|
8
|
+
"openai/widgetCSP": {
|
|
9
|
+
"resource_domains": ["https://images.unsplash.com"]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "map",
|
|
3
|
+
"title": "Map",
|
|
4
|
+
"description": "Pizza restaurant finder widget",
|
|
5
|
+
"mimeType": "text/html+skybridge",
|
|
6
|
+
"_meta": {
|
|
7
|
+
"openai/widgetDomain": "https://sunpeak.ai",
|
|
8
|
+
"openai/widgetCSP": {
|
|
9
|
+
"connect_domains": ["https://api.mapbox.com"],
|
|
10
|
+
"resource_domains": ["https://*.oaistatic.com", "https://api.mapbox.com"]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* This file contains only metadata and doesn't import React components or CSS.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import albumsResourceMeta from '../resources/albums-resource.json';
|
|
7
7
|
|
|
8
8
|
const albumsData = {
|
|
9
9
|
albums: [
|
|
@@ -129,7 +129,6 @@ export const albumsSimulation = {
|
|
|
129
129
|
title: 'Show Albums',
|
|
130
130
|
annotations: { readOnlyHint: true },
|
|
131
131
|
_meta: {
|
|
132
|
-
'openai/outputTemplate': 'ui://AlbumsResource',
|
|
133
132
|
'openai/toolInvocation/invoking': 'Loading albums',
|
|
134
133
|
'openai/toolInvocation/invoked': 'Album loaded',
|
|
135
134
|
'openai/widgetAccessible': true,
|
|
@@ -137,19 +136,8 @@ export const albumsSimulation = {
|
|
|
137
136
|
},
|
|
138
137
|
},
|
|
139
138
|
|
|
140
|
-
// MCP Resource protocol -
|
|
141
|
-
|
|
142
|
-
// resource.title is used as the simulation display label
|
|
143
|
-
resource: {
|
|
144
|
-
uri: 'ui://AlbumsResource',
|
|
145
|
-
name: 'albums',
|
|
146
|
-
title: 'Albums',
|
|
147
|
-
description: 'Show photo albums widget markup',
|
|
148
|
-
mimeType: 'text/html+skybridge',
|
|
149
|
-
_meta: {
|
|
150
|
-
...defaultWidgetMeta,
|
|
151
|
-
},
|
|
152
|
-
},
|
|
139
|
+
// MCP Resource protocol - imported from resource meta file
|
|
140
|
+
resource: albumsResourceMeta,
|
|
153
141
|
|
|
154
142
|
// MCP CallTool protocol - data for CallTool response
|
|
155
143
|
toolCall: {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* This file contains only metadata and doesn't import React components or CSS.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import carouselResourceMeta from '../resources/carousel-resource.json';
|
|
7
7
|
|
|
8
8
|
const placesData = {
|
|
9
9
|
places: [
|
|
@@ -66,7 +66,6 @@ export const carouselSimulation = {
|
|
|
66
66
|
title: 'Show Carousel',
|
|
67
67
|
annotations: { readOnlyHint: true },
|
|
68
68
|
_meta: {
|
|
69
|
-
'openai/outputTemplate': 'ui://CarouselResource',
|
|
70
69
|
'openai/toolInvocation/invoking': 'Loading carousel',
|
|
71
70
|
'openai/toolInvocation/invoked': 'Carousel loaded',
|
|
72
71
|
'openai/widgetAccessible': true,
|
|
@@ -74,19 +73,8 @@ export const carouselSimulation = {
|
|
|
74
73
|
},
|
|
75
74
|
},
|
|
76
75
|
|
|
77
|
-
// MCP Resource protocol -
|
|
78
|
-
|
|
79
|
-
// resource.title is used as the simulation display label
|
|
80
|
-
resource: {
|
|
81
|
-
uri: 'ui://CarouselResource',
|
|
82
|
-
name: 'carousel',
|
|
83
|
-
title: 'Carousel',
|
|
84
|
-
description: 'Show popular places to visit widget markup',
|
|
85
|
-
mimeType: 'text/html+skybridge',
|
|
86
|
-
_meta: {
|
|
87
|
-
...defaultWidgetMeta,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
76
|
+
// MCP Resource protocol - imported from resource meta file
|
|
77
|
+
resource: carouselResourceMeta,
|
|
90
78
|
|
|
91
79
|
// MCP CallTool protocol - data for CallTool response
|
|
92
80
|
toolCall: {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* This file contains only metadata and doesn't import React components or CSS.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import counterResourceMeta from '../resources/counter-resource.json';
|
|
7
7
|
|
|
8
8
|
export const counterSimulation = {
|
|
9
9
|
userMessage: 'Help me count something',
|
|
@@ -16,7 +16,6 @@ export const counterSimulation = {
|
|
|
16
16
|
title: 'Show Counter',
|
|
17
17
|
annotations: { readOnlyHint: true },
|
|
18
18
|
_meta: {
|
|
19
|
-
'openai/outputTemplate': 'ui://CounterResource',
|
|
20
19
|
'openai/toolInvocation/invoking': 'Counting beans',
|
|
21
20
|
'openai/toolInvocation/invoked': 'Beans counted',
|
|
22
21
|
'openai/widgetAccessible': true,
|
|
@@ -24,19 +23,8 @@ export const counterSimulation = {
|
|
|
24
23
|
},
|
|
25
24
|
},
|
|
26
25
|
|
|
27
|
-
// MCP Resource protocol -
|
|
28
|
-
|
|
29
|
-
// resource.title is used as the simulation display label
|
|
30
|
-
resource: {
|
|
31
|
-
uri: 'ui://CounterResource',
|
|
32
|
-
name: 'counter',
|
|
33
|
-
title: 'Counter',
|
|
34
|
-
description: 'Show a simple counter tool widget markup',
|
|
35
|
-
mimeType: 'text/html+skybridge',
|
|
36
|
-
_meta: {
|
|
37
|
-
...defaultWidgetMeta,
|
|
38
|
-
},
|
|
39
|
-
},
|
|
26
|
+
// MCP Resource protocol - imported from resource meta file
|
|
27
|
+
resource: counterResourceMeta,
|
|
40
28
|
|
|
41
29
|
// MCP CallTool protocol - data for CallTool response
|
|
42
30
|
toolCall: {
|