@bearmenu/ui 0.1.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-2CUFS5LF.js → chunk-2BMJRXQX.js} +1 -1
- package/dist/chunk-HLQ2IRE7.js +279 -0
- package/dist/chunk-IQASCH2V.js +25 -0
- package/dist/{chunk-UXAYXRFN.js → chunk-QJ54Z7TC.js} +15 -1
- package/dist/{chunk-X6GATUI2.js → chunk-UISA4CIG.js} +1 -0
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.d.ts +9 -7
- package/dist/components/calendar.js +153 -39
- package/dist/components/card.d.ts +2 -1
- package/dist/components/card.js +1 -1
- package/dist/components/carousel.js +1 -1
- package/dist/components/checkbox.js +3 -25
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/empty-state.js +1 -1
- package/dist/components/haptic-button.js +1 -1
- package/dist/components/link-button.js +1 -1
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/multi-select-menu.d.ts +26 -0
- package/dist/components/multi-select-menu.js +71 -0
- package/dist/components/pagination.js +1 -1
- package/dist/components/phone-frame.d.ts +17 -0
- package/dist/components/phone-frame.js +41 -0
- package/dist/components/place-about-tab.d.ts +17 -0
- package/dist/components/place-about-tab.js +6 -0
- package/dist/components/place-card.d.ts +17 -0
- package/dist/components/place-card.js +145 -0
- package/dist/components/place-details-mobile.d.ts +18 -0
- package/dist/components/place-details-mobile.js +187 -0
- package/dist/components/place-types.d.ts +145 -0
- package/dist/components/place-types.js +1 -0
- package/dist/components/search-bar.js +1 -1
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.js +1 -1
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/sonner.js +11 -2
- package/dist/components/tabs.js +1 -1
- package/dist/components/toaster.js +1 -1
- package/package.json +28 -9
- package/src/components/accordion.stories.tsx +51 -0
- package/src/components/alert-dialog.stories.tsx +45 -0
- package/src/components/alert.stories.tsx +85 -0
- package/src/components/aspect-ratio.stories.tsx +47 -0
- package/src/components/avatar.stories.tsx +66 -0
- package/src/components/badge.stories.tsx +60 -0
- package/src/components/breadcrumb.stories.tsx +63 -0
- package/src/components/button.stories.tsx +115 -0
- package/src/components/button.tsx +1 -1
- package/src/components/calendar.stories.tsx +47 -0
- package/src/components/calendar.tsx +160 -40
- package/src/components/card.stories.tsx +72 -0
- package/src/components/card.tsx +13 -1
- package/src/components/carousel.stories.tsx +62 -0
- package/src/components/checkbox.stories.tsx +54 -0
- package/src/components/checkbox.tsx +1 -1
- package/src/components/collapsible.stories.tsx +34 -0
- package/src/components/command.stories.tsx +64 -0
- package/src/components/currency-input.stories.tsx +50 -0
- package/src/components/dialog.stories.tsx +75 -0
- package/src/components/drawer.stories.tsx +47 -0
- package/src/components/dropdown-menu.stories.tsx +64 -0
- package/src/components/dropdown-menu.tsx +1 -1
- package/src/components/empty-state.stories.tsx +67 -0
- package/src/components/form.stories.tsx +73 -0
- package/src/components/haptic-button.stories.tsx +41 -0
- package/src/components/input-otp.stories.tsx +42 -0
- package/src/components/input.stories.tsx +41 -0
- package/src/components/label.stories.tsx +35 -0
- package/src/components/markdown-renderer.stories.tsx +45 -0
- package/src/components/multi-select-combobox.stories.tsx +78 -0
- package/src/components/multi-select-menu.tsx +96 -0
- package/src/components/pagination.stories.tsx +48 -0
- package/src/components/phone-frame.stories.tsx +51 -0
- package/src/components/phone-frame.test.tsx +82 -0
- package/src/components/phone-frame.tsx +59 -0
- package/src/components/place-about-tab.stories.tsx +164 -0
- package/src/components/place-about-tab.test.tsx +325 -0
- package/src/components/place-about-tab.tsx +474 -0
- package/src/components/place-card.stories.tsx +120 -0
- package/src/components/place-card.test.tsx +187 -0
- package/src/components/place-card.tsx +187 -0
- package/src/components/place-details-mobile.tsx +251 -0
- package/src/components/place-types.ts +127 -0
- package/src/components/popover.stories.tsx +42 -0
- package/src/components/progress.stories.tsx +35 -0
- package/src/components/radio-group.stories.tsx +46 -0
- package/src/components/rating.stories.tsx +45 -0
- package/src/components/scroll-area.stories.tsx +48 -0
- package/src/components/search-bar.stories.tsx +92 -0
- package/src/components/searchable-select.stories.tsx +55 -0
- package/src/components/select.stories.tsx +92 -0
- package/src/components/separator.stories.tsx +33 -0
- package/src/components/sheet.stories.tsx +95 -0
- package/src/components/skeleton-card.stories.tsx +53 -0
- package/src/components/skeleton.stories.tsx +42 -0
- package/src/components/skeleton.tsx +1 -0
- package/src/components/slider.stories.tsx +27 -0
- package/src/components/sliding-panels.stories.tsx +68 -0
- package/src/components/sonner.tsx +17 -2
- package/src/components/sticky-container.stories.tsx +33 -0
- package/src/components/switch.stories.tsx +35 -0
- package/src/components/table.stories.tsx +60 -0
- package/src/components/tabs.stories.tsx +53 -0
- package/src/components/tabs.tsx +1 -1
- package/src/components/text.stories.tsx +82 -0
- package/src/components/textarea.stories.tsx +35 -0
- package/src/components/toast.stories.tsx +63 -0
- package/src/components/toggle-group.stories.tsx +60 -0
- package/src/components/toggle.stories.tsx +48 -0
- package/src/components/tooltip.stories.tsx +71 -0
- package/src/globals.css +14 -2
- package/src/test/setup.ts +9 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import { PlaceAboutTab } from "./place-about-tab";
|
|
4
|
+
import type { PlaceAboutTabLabels, PlacePreview } from "./place-types";
|
|
5
|
+
|
|
6
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const LABELS: PlaceAboutTabLabels = {
|
|
9
|
+
overview: "Overview",
|
|
10
|
+
description: "Description",
|
|
11
|
+
whatPeopleSay: "What people say",
|
|
12
|
+
contactAndLocation: "Contact & Location",
|
|
13
|
+
address: "Address",
|
|
14
|
+
instagram: "Instagram",
|
|
15
|
+
facebook: "Facebook",
|
|
16
|
+
todayHours: "Today's hours",
|
|
17
|
+
statusOpen: "Open now",
|
|
18
|
+
statusClosed: "Closed",
|
|
19
|
+
typicalBusyness: "Typical busyness",
|
|
20
|
+
cuisines: "Cuisines",
|
|
21
|
+
amenities: "Amenities",
|
|
22
|
+
paymentOptions: "Payment options",
|
|
23
|
+
accessibility: "Accessibility",
|
|
24
|
+
parking: "Parking",
|
|
25
|
+
nearbyLandmarks: "Nearby landmarks",
|
|
26
|
+
neighborhood: "Neighborhood",
|
|
27
|
+
businessInformation: "Business information",
|
|
28
|
+
type: "Type",
|
|
29
|
+
status: "Status",
|
|
30
|
+
amenityLabels: { wifi: "Free Wi-Fi" },
|
|
31
|
+
paymentLabels: {},
|
|
32
|
+
accessibilityLabels: {},
|
|
33
|
+
parkingLabels: {},
|
|
34
|
+
typeLabels: {},
|
|
35
|
+
statusLabels: {},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function makePlace(overrides: Partial<PlacePreview> = {}): PlacePreview {
|
|
39
|
+
return {
|
|
40
|
+
id: "1",
|
|
41
|
+
slug: "test-place",
|
|
42
|
+
name: "Test Place",
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe("PlaceAboutTab", () => {
|
|
50
|
+
// ─── Contact section: each field conditionally rendered ─────────────────────
|
|
51
|
+
|
|
52
|
+
it("renders phone link only when place.phone is provided", () => {
|
|
53
|
+
const { rerender } = render(
|
|
54
|
+
<PlaceAboutTab place={makePlace({ phone: "+40700000000" })} labels={LABELS} />
|
|
55
|
+
);
|
|
56
|
+
expect(screen.getByText("+40700000000")).toBeInTheDocument();
|
|
57
|
+
|
|
58
|
+
rerender(<PlaceAboutTab place={makePlace()} labels={LABELS} />);
|
|
59
|
+
expect(screen.queryByText("+40700000000")).not.toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders website link only when place.website_url is provided", () => {
|
|
63
|
+
render(
|
|
64
|
+
<PlaceAboutTab
|
|
65
|
+
place={makePlace({ website_url: "https://example.com" })}
|
|
66
|
+
labels={LABELS}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
expect(screen.getByText("https://example.com")).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not render website link when website_url is absent", () => {
|
|
73
|
+
render(<PlaceAboutTab place={makePlace()} labels={LABELS} />);
|
|
74
|
+
expect(screen.queryByRole("link", { name: /http/ })).not.toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("renders Instagram link only when place.socials.instagram_url is provided", () => {
|
|
78
|
+
render(
|
|
79
|
+
<PlaceAboutTab
|
|
80
|
+
place={makePlace({ socials: { instagram_url: "https://instagram.com/test" } })}
|
|
81
|
+
labels={LABELS}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByText("Instagram")).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("does not render Instagram link when socials.instagram_url is absent", () => {
|
|
88
|
+
render(<PlaceAboutTab place={makePlace()} labels={LABELS} />);
|
|
89
|
+
expect(screen.queryByText("Instagram")).not.toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders Facebook link only when place.socials.facebook_url is provided", () => {
|
|
93
|
+
render(
|
|
94
|
+
<PlaceAboutTab
|
|
95
|
+
place={makePlace({ socials: { facebook_url: "https://facebook.com/test" } })}
|
|
96
|
+
labels={LABELS}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
expect(screen.getByText("Facebook")).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not render Facebook link when socials.facebook_url is absent", () => {
|
|
103
|
+
render(<PlaceAboutTab place={makePlace()} labels={LABELS} />);
|
|
104
|
+
expect(screen.queryByText("Facebook")).not.toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─── mapSlot ────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
it("renders mapSlot when prop is passed (regardless of busyness data)", () => {
|
|
110
|
+
render(
|
|
111
|
+
<PlaceAboutTab
|
|
112
|
+
place={makePlace()}
|
|
113
|
+
labels={LABELS}
|
|
114
|
+
mapSlot={<div data-testid="map-slot">Map</div>}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
expect(screen.getByTestId("map-slot")).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("does not render mapSlot content when prop is omitted", () => {
|
|
121
|
+
render(<PlaceAboutTab place={makePlace()} labels={LABELS} />);
|
|
122
|
+
expect(screen.queryByTestId("map-slot")).not.toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ─── busynessSlot ───────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
it("renders busynessSlot when both the prop and place.busyness entries are present", () => {
|
|
128
|
+
render(
|
|
129
|
+
<PlaceAboutTab
|
|
130
|
+
place={makePlace({ busyness: [{ hour: 12, score: 80 }] })}
|
|
131
|
+
labels={LABELS}
|
|
132
|
+
busynessSlot={<div data-testid="busyness-slot">Chart</div>}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
expect(screen.getByTestId("busyness-slot")).toBeInTheDocument();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("does NOT render busynessSlot when prop is present but place.busyness is empty", () => {
|
|
139
|
+
render(
|
|
140
|
+
<PlaceAboutTab
|
|
141
|
+
place={makePlace({ busyness: [] })}
|
|
142
|
+
labels={LABELS}
|
|
143
|
+
busynessSlot={<div data-testid="busyness-slot">Chart</div>}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
expect(screen.queryByTestId("busyness-slot")).not.toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("does NOT render busynessSlot when prop is present but place.busyness is absent", () => {
|
|
150
|
+
render(
|
|
151
|
+
<PlaceAboutTab
|
|
152
|
+
place={makePlace()}
|
|
153
|
+
labels={LABELS}
|
|
154
|
+
busynessSlot={<div data-testid="busyness-slot">Chart</div>}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
expect(screen.queryByTestId("busyness-slot")).not.toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("does NOT render busynessSlot when the prop is omitted even if busyness data exists", () => {
|
|
161
|
+
render(
|
|
162
|
+
<PlaceAboutTab
|
|
163
|
+
place={makePlace({ busyness: [{ hour: 12, score: 80 }] })}
|
|
164
|
+
labels={LABELS}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
expect(screen.queryByTestId("busyness-slot")).not.toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── onLinkClick ────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
it("fires onLinkClick with kind='phone' and correct url on phone click", () => {
|
|
173
|
+
const onLinkClick = vi.fn();
|
|
174
|
+
render(
|
|
175
|
+
<PlaceAboutTab
|
|
176
|
+
place={makePlace({ phone: "+40700000000" })}
|
|
177
|
+
labels={LABELS}
|
|
178
|
+
onLinkClick={onLinkClick}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
fireEvent.click(screen.getByText("+40700000000"));
|
|
182
|
+
expect(onLinkClick).toHaveBeenCalledWith("phone", "+40700000000");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("fires onLinkClick with kind='website' on website click", () => {
|
|
186
|
+
const onLinkClick = vi.fn();
|
|
187
|
+
render(
|
|
188
|
+
<PlaceAboutTab
|
|
189
|
+
place={makePlace({ website_url: "https://example.com" })}
|
|
190
|
+
labels={LABELS}
|
|
191
|
+
onLinkClick={onLinkClick}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
fireEvent.click(screen.getByText("https://example.com"));
|
|
195
|
+
expect(onLinkClick).toHaveBeenCalledWith("website", "https://example.com");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("fires onLinkClick with kind='instagram' on instagram click", () => {
|
|
199
|
+
const onLinkClick = vi.fn();
|
|
200
|
+
render(
|
|
201
|
+
<PlaceAboutTab
|
|
202
|
+
place={makePlace({ socials: { instagram_url: "https://instagram.com/test" } })}
|
|
203
|
+
labels={LABELS}
|
|
204
|
+
onLinkClick={onLinkClick}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
fireEvent.click(screen.getByText("Instagram"));
|
|
208
|
+
expect(onLinkClick).toHaveBeenCalledWith(
|
|
209
|
+
"instagram",
|
|
210
|
+
"https://instagram.com/test"
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("fires onLinkClick with kind='facebook' on facebook click", () => {
|
|
215
|
+
const onLinkClick = vi.fn();
|
|
216
|
+
render(
|
|
217
|
+
<PlaceAboutTab
|
|
218
|
+
place={makePlace({ socials: { facebook_url: "https://facebook.com/test" } })}
|
|
219
|
+
labels={LABELS}
|
|
220
|
+
onLinkClick={onLinkClick}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
fireEvent.click(screen.getByText("Facebook"));
|
|
224
|
+
expect(onLinkClick).toHaveBeenCalledWith(
|
|
225
|
+
"facebook",
|
|
226
|
+
"https://facebook.com/test"
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ─── Today's hours card ──────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
it("renders today's hours card when place.hours has entries", () => {
|
|
233
|
+
render(
|
|
234
|
+
<PlaceAboutTab
|
|
235
|
+
place={makePlace({ hours: [{ opens_at: 9, closes_at: 22 }] })}
|
|
236
|
+
labels={LABELS}
|
|
237
|
+
/>
|
|
238
|
+
);
|
|
239
|
+
expect(screen.getByText("Today's hours")).toBeInTheDocument();
|
|
240
|
+
expect(screen.getByText("09:00 - 22:00")).toBeInTheDocument();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("does NOT render today's hours card when place.hours is empty", () => {
|
|
244
|
+
render(
|
|
245
|
+
<PlaceAboutTab place={makePlace({ hours: [] })} labels={LABELS} />
|
|
246
|
+
);
|
|
247
|
+
expect(screen.queryByText("Today's hours")).not.toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("does NOT render today's hours card when place.hours is absent", () => {
|
|
251
|
+
render(<PlaceAboutTab place={makePlace()} labels={LABELS} />);
|
|
252
|
+
expect(screen.queryByText("Today's hours")).not.toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ─── amenityLabels fallback to humanizeEnum ──────────────────────────────────
|
|
256
|
+
|
|
257
|
+
it("uses the amenityLabels map when the key is present", () => {
|
|
258
|
+
render(
|
|
259
|
+
<PlaceAboutTab
|
|
260
|
+
place={makePlace({ amenities: ["wifi"] })}
|
|
261
|
+
labels={LABELS}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
// LABELS.amenityLabels.wifi = "Free Wi-Fi"
|
|
265
|
+
expect(screen.getByText("Free Wi-Fi")).toBeInTheDocument();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("falls back to humanizeEnum (underscores → spaces) when key missing from amenityLabels", () => {
|
|
269
|
+
render(
|
|
270
|
+
<PlaceAboutTab
|
|
271
|
+
place={makePlace({ amenities: ["outdoor_seating"] })}
|
|
272
|
+
labels={LABELS}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
// humanizeEnum("outdoor_seating") → "outdoor seating"
|
|
276
|
+
expect(screen.getByText("outdoor seating")).toBeInTheDocument();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ─── Description card visibility ────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
it("renders description card when description is set", () => {
|
|
282
|
+
render(
|
|
283
|
+
<PlaceAboutTab
|
|
284
|
+
place={makePlace({ description: "A great place to eat." })}
|
|
285
|
+
labels={LABELS}
|
|
286
|
+
/>
|
|
287
|
+
);
|
|
288
|
+
expect(screen.getByText("A great place to eat.")).toBeInTheDocument();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("renders description card when generative_summary is set", () => {
|
|
292
|
+
render(
|
|
293
|
+
<PlaceAboutTab
|
|
294
|
+
place={makePlace({ generative_summary: "AI summary here." })}
|
|
295
|
+
labels={LABELS}
|
|
296
|
+
/>
|
|
297
|
+
);
|
|
298
|
+
expect(screen.getByText("AI summary here.")).toBeInTheDocument();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("renders description card when review_summary is set", () => {
|
|
302
|
+
render(
|
|
303
|
+
<PlaceAboutTab
|
|
304
|
+
place={makePlace({ review_summary: "People love it." })}
|
|
305
|
+
labels={LABELS}
|
|
306
|
+
/>
|
|
307
|
+
);
|
|
308
|
+
expect(screen.getByText("People love it.")).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("hides description card when none of description/generative_summary/review_summary are set", () => {
|
|
312
|
+
render(<PlaceAboutTab place={makePlace()} labels={LABELS} />);
|
|
313
|
+
// The card header title is "Description" which is the same as labels.description.
|
|
314
|
+
// The description card renders labels.description as its CardTitle.
|
|
315
|
+
// Use a more specific query: look for the card header content.
|
|
316
|
+
// Since labels.contactAndLocation is always rendered, we need to count occurrences.
|
|
317
|
+
// "Description" appears in labels.description; if no card exists it should not appear at all.
|
|
318
|
+
expect(screen.queryByText("A great place to eat.")).not.toBeInTheDocument();
|
|
319
|
+
// Verify no card title "Description" is rendered (description card uses labels.description as title)
|
|
320
|
+
// Note: "Description" also equals labels.description label inside the card — use getAllByText and check count
|
|
321
|
+
const descTexts = screen.queryAllByText("Description");
|
|
322
|
+
// When description card is hidden, "Description" label should not appear
|
|
323
|
+
expect(descTexts).toHaveLength(0);
|
|
324
|
+
});
|
|
325
|
+
});
|