sunpeak 0.6.6 → 0.6.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunpeak",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "The ChatGPT App framework. Quickstart, build, & test your ChatGPT App locally!",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -2,13 +2,13 @@ import {
2
2
  Button,
3
3
  ButtonLink,
4
4
  CopyButton
5
- } from "./chunk-LR7NKCX5.js";
5
+ } from "./chunk-DQAZDQU3.js";
6
+ import "./chunk-XB525PXG.js";
6
7
  import "./chunk-YOJ6QPGS.js";
7
- import "./chunk-QPJAV452.js";
8
8
  import "./chunk-BAG6OO6S.js";
9
+ import "./chunk-QPJAV452.js";
9
10
  import "./chunk-EGRHWZRV.js";
10
11
  import "./chunk-CNYJBM5F.js";
11
- import "./chunk-XB525PXG.js";
12
12
  import "./chunk-PTVT3RFX.js";
13
13
  import "./chunk-4TLBUCVB.js";
14
14
  import "./chunk-ILHRZGIS.js";
@@ -5,11 +5,11 @@ import {
5
5
  handlePressableMouseEnter,
6
6
  waitForAnimationFrame
7
7
  } from "./chunk-BAG6OO6S.js";
8
- import "./chunk-EGRHWZRV.js";
9
8
  import {
10
9
  dist_exports4 as dist_exports
11
10
  } from "./chunk-SGWD4VEU.js";
12
11
  import "./chunk-KFGKZMLK.js";
12
+ import "./chunk-EGRHWZRV.js";
13
13
  import {
14
14
  clsx_default
15
15
  } from "./chunk-CNYJBM5F.js";
@@ -1,41 +1,41 @@
1
- import {
2
- Input
3
- } from "./chunk-CQ3GYAYB.js";
4
1
  import {
5
2
  Button,
6
3
  LoadingIndicator,
7
4
  TransitionGroup
8
- } from "./chunk-LR7NKCX5.js";
5
+ } from "./chunk-DQAZDQU3.js";
6
+ import {
7
+ Check_default,
8
+ ChevronDownVector_default,
9
+ DropdownVector_default,
10
+ Info_default,
11
+ Search_default,
12
+ X_default
13
+ } from "./chunk-XB525PXG.js";
9
14
  import {
10
15
  useTimeout
11
16
  } from "./chunk-YOJ6QPGS.js";
12
- import {
13
- o
14
- } from "./chunk-QPJAV452.js";
15
17
  import {
16
18
  handlePressableMouseEnter,
17
19
  preventDefaultHandler,
18
20
  toCssVariables,
19
21
  waitForAnimationFrame
20
22
  } from "./chunk-BAG6OO6S.js";
21
- import "./chunk-EGRHWZRV.js";
22
23
  import {
23
24
  dist_exports,
24
25
  dist_exports3 as dist_exports2,
25
26
  dist_exports5 as dist_exports3
26
27
  } from "./chunk-SGWD4VEU.js";
27
28
  import "./chunk-KFGKZMLK.js";
29
+ import {
30
+ Input
31
+ } from "./chunk-CQ3GYAYB.js";
32
+ import {
33
+ o
34
+ } from "./chunk-QPJAV452.js";
35
+ import "./chunk-EGRHWZRV.js";
28
36
  import {
29
37
  clsx_default
30
38
  } from "./chunk-CNYJBM5F.js";
31
- import {
32
- Check_default,
33
- ChevronDownVector_default,
34
- DropdownVector_default,
35
- Info_default,
36
- Search_default,
37
- X_default
38
- } from "./chunk-XB525PXG.js";
39
39
  import {
40
40
  require_jsx_runtime
41
41
  } from "./chunk-PTVT3RFX.js";
@@ -1,9 +1,9 @@
1
- import {
2
- o
3
- } from "./chunk-QPJAV452.js";
4
1
  import {
5
2
  toCssVariables
6
3
  } from "./chunk-BAG6OO6S.js";
4
+ import {
5
+ o
6
+ } from "./chunk-QPJAV452.js";
7
7
  import "./chunk-EGRHWZRV.js";
8
8
  import {
9
9
  clsx_default
@@ -7,149 +7,149 @@
7
7
  "react": {
8
8
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/index.js",
9
9
  "file": "react.js",
10
- "fileHash": "9756d72d",
10
+ "fileHash": "63a1aabd",
11
11
  "needsInterop": true
12
12
  },
13
13
  "react-dom": {
14
14
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.0_react@19.2.0/node_modules/react-dom/index.js",
15
15
  "file": "react-dom.js",
16
- "fileHash": "a63da1f0",
16
+ "fileHash": "39a93d3f",
17
17
  "needsInterop": true
18
18
  },
19
19
  "react/jsx-dev-runtime": {
20
20
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/jsx-dev-runtime.js",
21
21
  "file": "react_jsx-dev-runtime.js",
22
- "fileHash": "5d910eda",
22
+ "fileHash": "23777ec9",
23
23
  "needsInterop": true
24
24
  },
25
25
  "react/jsx-runtime": {
26
26
  "src": "../../../../node_modules/.pnpm/react@19.2.0/node_modules/react/jsx-runtime.js",
27
27
  "file": "react_jsx-runtime.js",
28
- "fileHash": "8b4092f6",
28
+ "fileHash": "22021338",
29
29
  "needsInterop": true
30
30
  },
31
31
  "@openai/apps-sdk-ui/components/Avatar": {
32
32
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Avatar/index.js",
33
33
  "file": "@openai_apps-sdk-ui_components_Avatar.js",
34
- "fileHash": "c160e014",
34
+ "fileHash": "acc568e6",
35
35
  "needsInterop": false
36
36
  },
37
37
  "@openai/apps-sdk-ui/components/Button": {
38
38
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Button/index.js",
39
39
  "file": "@openai_apps-sdk-ui_components_Button.js",
40
- "fileHash": "67f50423",
40
+ "fileHash": "4d606162",
41
41
  "needsInterop": false
42
42
  },
43
43
  "@openai/apps-sdk-ui/components/Checkbox": {
44
44
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Checkbox/index.js",
45
45
  "file": "@openai_apps-sdk-ui_components_Checkbox.js",
46
- "fileHash": "29b22e1b",
46
+ "fileHash": "fe130d50",
47
47
  "needsInterop": false
48
48
  },
49
49
  "@openai/apps-sdk-ui/components/Icon": {
50
50
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Icon/index.js",
51
51
  "file": "@openai_apps-sdk-ui_components_Icon.js",
52
- "fileHash": "a65a89dc",
52
+ "fileHash": "9c6ad541",
53
53
  "needsInterop": false
54
54
  },
55
55
  "@openai/apps-sdk-ui/components/Input": {
56
56
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Input/index.js",
57
57
  "file": "@openai_apps-sdk-ui_components_Input.js",
58
- "fileHash": "efcd04ba",
58
+ "fileHash": "6843ec9f",
59
59
  "needsInterop": false
60
60
  },
61
61
  "@openai/apps-sdk-ui/components/SegmentedControl": {
62
62
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/SegmentedControl/index.js",
63
63
  "file": "@openai_apps-sdk-ui_components_SegmentedControl.js",
64
- "fileHash": "b7c8e52c",
64
+ "fileHash": "65fb785b",
65
65
  "needsInterop": false
66
66
  },
67
67
  "@openai/apps-sdk-ui/components/Select": {
68
68
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Select/index.js",
69
69
  "file": "@openai_apps-sdk-ui_components_Select.js",
70
- "fileHash": "b43c4297",
70
+ "fileHash": "e4d331f8",
71
71
  "needsInterop": false
72
72
  },
73
73
  "@openai/apps-sdk-ui/components/Textarea": {
74
74
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/components/Textarea/index.js",
75
75
  "file": "@openai_apps-sdk-ui_components_Textarea.js",
76
- "fileHash": "e6d46958",
76
+ "fileHash": "733d2750",
77
77
  "needsInterop": false
78
78
  },
79
79
  "@openai/apps-sdk-ui/theme": {
80
80
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.0_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._60630c8dcc43ec213b3e346c9e26579b/node_modules/@openai/apps-sdk-ui/dist/es/lib/theme.js",
81
81
  "file": "@openai_apps-sdk-ui_theme.js",
82
- "fileHash": "8f9b13b1",
82
+ "fileHash": "86ab9241",
83
83
  "needsInterop": false
84
84
  },
85
85
  "clsx": {
86
86
  "src": "../../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs",
87
87
  "file": "clsx.js",
88
- "fileHash": "b1cc3b7f",
88
+ "fileHash": "ea30a216",
89
89
  "needsInterop": false
90
90
  },
91
91
  "embla-carousel-react": {
92
92
  "src": "../../../../node_modules/.pnpm/embla-carousel-react@8.6.0_react@19.2.0/node_modules/embla-carousel-react/esm/embla-carousel-react.esm.js",
93
93
  "file": "embla-carousel-react.js",
94
- "fileHash": "62a93251",
94
+ "fileHash": "bbac0c24",
95
95
  "needsInterop": false
96
96
  },
97
97
  "embla-carousel-wheel-gestures": {
98
98
  "src": "../../../../node_modules/.pnpm/embla-carousel-wheel-gestures@8.1.0_embla-carousel@8.6.0/node_modules/embla-carousel-wheel-gestures/dist/embla-carousel-wheel-gestures.esm.js",
99
99
  "file": "embla-carousel-wheel-gestures.js",
100
- "fileHash": "368bd1a3",
100
+ "fileHash": "aea80d6b",
101
101
  "needsInterop": false
102
102
  },
103
103
  "mapbox-gl": {
104
104
  "src": "../../../../node_modules/.pnpm/mapbox-gl@3.17.0/node_modules/mapbox-gl/dist/mapbox-gl.js",
105
105
  "file": "mapbox-gl.js",
106
- "fileHash": "e7ac33e5",
106
+ "fileHash": "94c5be64",
107
107
  "needsInterop": true
108
108
  },
109
109
  "react-dom/client": {
110
110
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.0_react@19.2.0/node_modules/react-dom/client.js",
111
111
  "file": "react-dom_client.js",
112
- "fileHash": "400e60e6",
112
+ "fileHash": "8795efae",
113
113
  "needsInterop": true
114
114
  },
115
115
  "tailwind-merge": {
116
116
  "src": "../../../../node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.mjs",
117
117
  "file": "tailwind-merge.js",
118
- "fileHash": "0e85aebb",
118
+ "fileHash": "4f935315",
119
119
  "needsInterop": false
120
120
  }
121
121
  },
122
122
  "chunks": {
123
- "chunk-CQ3GYAYB": {
124
- "file": "chunk-CQ3GYAYB.js"
123
+ "chunk-DQAZDQU3": {
124
+ "file": "chunk-DQAZDQU3.js"
125
125
  },
126
- "chunk-LR7NKCX5": {
127
- "file": "chunk-LR7NKCX5.js"
126
+ "chunk-XB525PXG": {
127
+ "file": "chunk-XB525PXG.js"
128
128
  },
129
129
  "chunk-YOJ6QPGS": {
130
130
  "file": "chunk-YOJ6QPGS.js"
131
131
  },
132
- "chunk-QPJAV452": {
133
- "file": "chunk-QPJAV452.js"
134
- },
135
132
  "chunk-BAG6OO6S": {
136
133
  "file": "chunk-BAG6OO6S.js"
137
134
  },
138
- "chunk-EGRHWZRV": {
139
- "file": "chunk-EGRHWZRV.js"
140
- },
141
135
  "chunk-SGWD4VEU": {
142
136
  "file": "chunk-SGWD4VEU.js"
143
137
  },
144
138
  "chunk-KFGKZMLK": {
145
139
  "file": "chunk-KFGKZMLK.js"
146
140
  },
141
+ "chunk-CQ3GYAYB": {
142
+ "file": "chunk-CQ3GYAYB.js"
143
+ },
144
+ "chunk-QPJAV452": {
145
+ "file": "chunk-QPJAV452.js"
146
+ },
147
+ "chunk-EGRHWZRV": {
148
+ "file": "chunk-EGRHWZRV.js"
149
+ },
147
150
  "chunk-CNYJBM5F": {
148
151
  "file": "chunk-CNYJBM5F.js"
149
152
  },
150
- "chunk-XB525PXG": {
151
- "file": "chunk-XB525PXG.js"
152
- },
153
153
  "chunk-PTVT3RFX": {
154
154
  "file": "chunk-PTVT3RFX.js"
155
155
  },
@@ -1,9 +1,10 @@
1
+ import {
2
+ Check_default,
3
+ Copy_default
4
+ } from "./chunk-XB525PXG.js";
1
5
  import {
2
6
  useTimeout
3
7
  } from "./chunk-YOJ6QPGS.js";
4
- import {
5
- o
6
- } from "./chunk-QPJAV452.js";
7
8
  import {
8
9
  handlePressableMouseEnter,
9
10
  isDev,
@@ -15,13 +16,12 @@ import {
15
16
  toTransformProperty,
16
17
  waitForAnimationFrame
17
18
  } from "./chunk-BAG6OO6S.js";
19
+ import {
20
+ o
21
+ } from "./chunk-QPJAV452.js";
18
22
  import {
19
23
  clsx_default
20
24
  } from "./chunk-CNYJBM5F.js";
21
- import {
22
- Check_default,
23
- Copy_default
24
- } from "./chunk-XB525PXG.js";
25
25
  import {
26
26
  require_jsx_runtime
27
27
  } from "./chunk-PTVT3RFX.js";
@@ -625,4 +625,4 @@ export {
625
625
  ButtonLink,
626
626
  CopyButton
627
627
  };
628
- //# sourceMappingURL=chunk-LR7NKCX5.js.map
628
+ //# sourceMappingURL=chunk-DQAZDQU3.js.map
@@ -1 +1 @@
1
- {"version":"4.0.13","results":[[":src/components/album/albums.test.tsx",{"duration":316.7086179999999,"failed":false}],[":src/resources/counter-resource.test.tsx",{"duration":261.19590900000003,"failed":false}],[":src/resources/carousel-resource.test.tsx",{"duration":254.1855129999999,"failed":false}],[":src/components/album/album-carousel.test.tsx",{"duration":87.29753299999993,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":260.2528709999997,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":90.40763599999991,"failed":false}],[":src/resources/albums-resource.test.tsx",{"duration":251.03367500000013,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":483.4843430000001,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":253.79688199999987,"failed":false}],[":src/components/carousel/card.test.tsx",{"duration":57.731027999999924,"failed":false}]]}
1
+ {"version":"4.0.13","results":[[":src/resources/carousel-resource.test.tsx",{"duration":257.4971499999999,"failed":false}],[":src/components/album/albums.test.tsx",{"duration":338.6859280000001,"failed":false}],[":src/components/map/map-view.test.tsx",{"duration":68.25615300000004,"failed":false}],[":src/resources/counter-resource.test.tsx",{"duration":346.021925,"failed":false}],[":src/components/map/place-inspector.test.tsx",{"duration":415.60394399999996,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":205.53456199999982,"failed":false}],[":src/components/map/place-list.test.tsx",{"duration":166.04964100000007,"failed":false}],[":src/components/map/place-card.test.tsx",{"duration":333.05097,"failed":false}],[":src/components/map/place-carousel.test.tsx",{"duration":443.04913099999976,"failed":false}],[":src/components/album/album-carousel.test.tsx",{"duration":87.34626800000001,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":86.42935499999999,"failed":false}],[":src/resources/albums-resource.test.tsx",{"duration":254.2782340000001,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":451.0885719999999,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":332.125442,"failed":false}],[":src/components/carousel/card.test.tsx",{"duration":91.10827399999994,"failed":false}]]}
@@ -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
+ });