@bearmenu/ui 0.8.13 → 0.8.15
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 +0 -1
- package/dist/{chunk-66MAPLXS.js → chunk-2ADCZXZJ.js} +1 -1
- package/dist/{chunk-2DEIXTBN.js → chunk-2ZXIXQIU.js} +1 -1
- package/dist/{chunk-YOKHBAIH.js → chunk-4OF4T4AI.js} +12 -11
- package/dist/{chunk-MZL7CGCL.js → chunk-4XDJXMLF.js} +2 -2
- package/dist/{chunk-VAXOQS7W.js → chunk-5FCC5L5F.js} +3 -3
- package/dist/{chunk-YYGB4G4Q.js → chunk-CZK2MRS2.js} +1 -1
- package/dist/chunk-DJD3EMPK.js +126 -0
- package/dist/chunk-EZG2NELX.js +5 -0
- package/dist/{chunk-VG3PND3D.js → chunk-FMTJGO76.js} +1 -1
- package/dist/{chunk-UXWAOISG.js → chunk-FVSKGCK3.js} +2 -2
- package/dist/{chunk-2XCZJWTV.js → chunk-GNCWJWFE.js} +1 -1
- package/dist/{chunk-X3WMRKOW.js → chunk-HDR7V6IZ.js} +1 -1
- package/dist/{chunk-HSIYYA2E.js → chunk-HQU6QVAN.js} +5 -5
- package/dist/chunk-I5V446BX.js +27 -0
- package/dist/{chunk-WCTHZKU7.js → chunk-ICOU5QHT.js} +1 -1
- package/dist/{chunk-G7IFL4Z4.js → chunk-L3VEMP5A.js} +57 -9
- package/dist/chunk-LQWGZIYA.js +5 -0
- package/dist/{chunk-XNA2INVQ.js → chunk-MCOHLWRS.js} +2 -2
- package/dist/{chunk-3GWWZKX7.js → chunk-MMUBH76A.js} +21 -1
- package/dist/{chunk-ZDAHKI3A.js → chunk-MWB7S54O.js} +1 -1
- package/dist/{chunk-D5QHEHXJ.js → chunk-NMEVJT5Q.js} +1 -1
- package/dist/{chunk-3TNPSN4D.js → chunk-NSNEANOQ.js} +2 -2
- package/dist/{chunk-WSRWFA6K.js → chunk-NWZGSLPU.js} +1 -1
- package/dist/{chunk-LEAYBICI.js → chunk-OIV7B44M.js} +1 -1
- package/dist/{chunk-2CXU5WZO.js → chunk-P43B3ONY.js} +1 -1
- package/dist/{chunk-FRPPIXMY.js → chunk-PDTPO6NG.js} +1 -1
- package/dist/{chunk-6XMYEZVR.js → chunk-PX6RICS5.js} +1 -1
- package/dist/{chunk-6TEEY5YA.js → chunk-S33K4RC5.js} +1 -1
- package/dist/{chunk-3VMDCCCX.js → chunk-SC2VUTUI.js} +1 -1
- package/dist/{chunk-3NMG3KNA.js → chunk-SXQZ7DLH.js} +3 -3
- package/dist/{chunk-UDTG3RSE.js → chunk-T6S2ZBV6.js} +2 -2
- package/dist/{chunk-R47NEOSI.js → chunk-V4E333QS.js} +29 -4
- package/dist/{chunk-FKNBZF5T.js → chunk-WYTJR2YU.js} +1 -1
- package/dist/{chunk-QXHMPJ35.js → chunk-XBBEX4SF.js} +1 -1
- package/dist/chunk-XCB3F6SF.js +16 -0
- package/dist/{chunk-7BWTYWM3.js → chunk-ZLMRVBD7.js} +1 -1
- package/dist/{chunk-P7XTW2CG.js → chunk-ZLPWEHZO.js} +1 -1
- package/dist/components/accordion.js +1 -1
- package/dist/components/alert-dialog.js +3 -3
- package/dist/components/alert.js +2 -2
- package/dist/components/aspect-ratio.js +2 -2
- package/dist/components/avatar.js +1 -1
- package/dist/components/badge.js +2 -2
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/button-group.js +1 -1
- package/dist/components/button.d.ts +4 -0
- package/dist/components/button.js +3 -3
- package/dist/components/calendar.js +3 -3
- package/dist/components/card.js +3 -3
- package/dist/components/carousel.js +3 -3
- package/dist/components/category-deck.js +2 -2
- package/dist/components/checkbox.js +2 -2
- package/dist/components/circular-progress.js +1 -1
- package/dist/components/collapsible.js +1 -1
- package/dist/components/combobox.js +6 -6
- package/dist/components/command.js +3 -3
- package/dist/components/concept-card.js +2 -2
- package/dist/components/concept-compare-rail.js +3 -3
- package/dist/components/concept-compare.d.ts +2 -1
- package/dist/components/concept-compare.js +5 -5
- package/dist/components/concept-convergence.js +4 -4
- package/dist/components/convergence.js +3 -3
- package/dist/components/coverflow.js +1 -1
- package/dist/components/currency-input.js +2 -2
- package/dist/components/date-input.js +2 -2
- package/dist/components/day-rail-chip.js +2 -2
- package/dist/components/description-list.js +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/doorway-tiles.d.ts +57 -0
- package/dist/components/doorway-tiles.js +86 -0
- package/dist/components/drawer.js +1 -1
- package/dist/components/drift-rail.js +1 -1
- package/dist/components/dropdown-menu.js +1 -1
- package/dist/components/empty-state.js +4 -4
- package/dist/components/event-card.js +4 -4
- package/dist/components/event-compact-row.js +4 -4
- package/dist/components/event-day-header.js +2 -2
- package/dist/components/event-day-separator.js +3 -3
- package/dist/components/event-detail-panel.js +5 -5
- package/dist/components/event-hub-hero.js +2 -2
- package/dist/components/event-index-row.js +4 -4
- package/dist/components/event-list-row-wide.js +6 -6
- package/dist/components/event-list-row.js +5 -5
- package/dist/components/event-poster-fallback.js +1 -1
- package/dist/components/event-types.js +1 -1
- package/dist/components/events-empty-state.js +1 -1
- package/dist/components/explainer-steps.js +1 -1
- package/dist/components/facet-group.js +2 -2
- package/dist/components/featured-rail-item.js +2 -2
- package/dist/components/field.js +2 -2
- package/dist/components/filter-category-row.js +3 -3
- package/dist/components/form.js +2 -2
- package/dist/components/glass-ground.js +2 -2
- package/dist/components/grade-atom.d.ts +3 -0
- package/dist/components/grade-atom.js +4 -0
- package/dist/components/hour-band.js +4 -4
- package/dist/components/hour-group.js +4 -4
- package/dist/components/hub-surface.js +1 -1
- package/dist/components/hue-chip.js +1 -1
- package/dist/components/icon.js +1 -1
- package/dist/components/input-group.js +1 -1
- package/dist/components/input-otp.js +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/item.js +1 -1
- package/dist/components/kbd.js +1 -1
- package/dist/components/kitchen-dossier.d.ts +2 -1
- package/dist/components/kitchen-dossier.js +4 -4
- package/dist/components/label.js +2 -2
- package/dist/components/lazy-markdown.js +1 -1
- package/dist/components/link-button.js +3 -3
- package/dist/components/markdown-renderer.js +1 -1
- package/dist/components/menu-dead-end.d.ts +2 -1
- package/dist/components/menu-dead-end.js +1 -1
- package/dist/components/menu-family-block.d.ts +2 -1
- package/dist/components/menu-family-block.js +15 -9
- package/dist/components/menu-family-card.d.ts +2 -1
- package/dist/components/menu-family-card.js +1 -1
- package/dist/components/menu-field.js +1 -1
- package/dist/components/menu-filter-chips.d.ts +2 -1
- package/dist/components/menu-filter-chips.js +3 -3
- package/dist/components/menu-group-rows.d.ts +2 -1
- package/dist/components/menu-group-rows.js +2 -2
- package/dist/components/menu-hub-hero.js +1 -1
- package/dist/components/menu-index-header.d.ts +2 -1
- package/dist/components/menu-index-header.js +2 -2
- package/dist/components/menu-index-list.d.ts +2 -1
- package/dist/components/menu-index-list.js +5 -5
- package/dist/components/menu-intent-hero.d.ts +3 -2
- package/dist/components/menu-intent-hero.js +1 -1
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +4 -4
- package/dist/components/menu-kitchen-list.d.ts +2 -1
- package/dist/components/menu-kitchen-list.js +4 -4
- package/dist/components/menu-mocks.d.ts +16 -5
- package/dist/components/menu-mocks.js +23 -2
- package/dist/components/menu-types.d.ts +21 -34
- package/dist/components/motion-icon-actions.d.ts +27 -0
- package/dist/components/motion-icon-actions.js +336 -0
- package/dist/components/motion-icon-controls.js +3 -3
- package/dist/components/motion-icon-set.d.ts +13 -1
- package/dist/components/motion-icon-set.js +197 -6
- package/dist/components/motion-icon.d.ts +15 -1
- package/dist/components/motion-icon.js +2 -2
- package/dist/components/multi-select-combobox.js +8 -8
- package/dist/components/multi-select-menu.js +2 -2
- package/dist/components/native-select.js +1 -1
- package/dist/components/open-now-list.d.ts +2 -1
- package/dist/components/open-now-list.js +2 -2
- package/dist/components/pagination.js +3 -3
- package/dist/components/phone-frame.js +1 -1
- package/dist/components/photo-ground.js +1 -1
- package/dist/components/place-about-tab.d.ts +2 -1
- package/dist/components/place-about-tab.js +6 -6
- package/dist/components/place-card.d.ts +94 -12
- package/dist/components/place-card.js +203 -128
- package/dist/components/place-details-mobile.d.ts +2 -1
- package/dist/components/place-details-mobile.js +7 -7
- package/dist/components/place-schedule-week.d.ts +2 -1
- package/dist/components/place-schedule-week.js +3 -3
- package/dist/components/place-types.d.ts +3 -265
- package/dist/components/popover.js +2 -2
- package/dist/components/poster-stack.js +1 -1
- package/dist/components/poster-tape.js +1 -1
- package/dist/components/poster-wall.d.ts +22 -6
- package/dist/components/poster-wall.js +197 -12
- package/dist/components/progress.js +1 -1
- package/dist/components/qa-block.d.ts +2 -1
- package/dist/components/qa-block.js +1 -1
- package/dist/components/quiet-stretch.js +1 -1
- package/dist/components/radio-group.js +1 -1
- package/dist/components/rating.js +1 -1
- package/dist/components/scroll-area.js +1 -1
- package/dist/components/scroll-fade.js +2 -2
- package/dist/components/search-bar.js +1 -1
- package/dist/components/searchable-select.js +6 -6
- package/dist/components/section-header.js +2 -2
- package/dist/components/segmented-nav.d.ts +24 -4
- package/dist/components/segmented-nav.js +11 -2
- package/dist/components/select.js +1 -1
- package/dist/components/selection-bar.js +1 -1
- package/dist/components/separator.js +2 -2
- package/dist/components/sheet.js +1 -1
- package/dist/components/signature-chip.js +3 -3
- package/dist/components/skeleton-card.js +3 -3
- package/dist/components/skeleton.js +2 -2
- package/dist/components/slider.js +1 -1
- package/dist/components/sliding-panels.js +3 -3
- package/dist/components/snap-rail.js +2 -2
- package/dist/components/sonner.js +1 -1
- package/dist/components/sort-option-list.js +1 -1
- package/dist/components/spinner.js +1 -1
- package/dist/components/stat-banner.js +1 -1
- package/dist/components/stepper.js +1 -1
- package/dist/components/sticky-container.js +1 -1
- package/dist/components/story.js +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.js +2 -2
- package/dist/components/tag-group.js +2 -2
- package/dist/components/tag.js +2 -2
- package/dist/components/text.js +2 -2
- package/dist/components/textarea.js +1 -1
- package/dist/components/timeline-band.js +4 -4
- package/dist/components/timeline-playhead.js +4 -4
- package/dist/components/toggle-group.js +2 -2
- package/dist/components/toggle.js +2 -2
- package/dist/components/tooltip.js +1 -1
- package/dist/components/top-comparison.d.ts +2 -1
- package/dist/components/top-comparison.js +1 -1
- package/dist/components/top-cross-link.d.ts +2 -1
- package/dist/components/top-cross-link.js +1 -1
- package/dist/components/top-ranking-entry.d.ts +2 -1
- package/dist/components/top-ranking-entry.js +6 -6
- package/dist/components/top-ranking-hero.d.ts +2 -1
- package/dist/components/top-ranking-hero.js +1 -1
- package/dist/components/top-ranking-legend.js +1 -1
- package/dist/components/top-types.d.ts +3 -1
- package/dist/components/use-mobile.js +1 -1
- package/dist/components/venue-events-block.js +5 -5
- package/dist/components/venue-grade-dimensions.d.ts +2 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/venue-quotes.d.ts +2 -1
- package/dist/components/venue-quotes.js +1 -1
- package/dist/components/venue-wall.d.ts +2 -1
- package/dist/components/venue-wall.js +1 -1
- package/dist/components/weekday-theme-banner.js +1 -1
- package/dist/components/weekend-panel.js +4 -4
- package/dist/hooks/use-drag-swipe.js +1 -1
- package/dist/hooks/use-haptic.js +2 -2
- package/dist/hooks/use-media-query.js +1 -1
- package/dist/hooks/use-mobile.js +1 -1
- package/dist/hooks/use-mounted-theme.js +1 -1
- package/dist/hooks/use-sticky.js +1 -1
- package/dist/lib/card-hover.js +2 -18
- package/dist/lib/grade.d.ts +54 -0
- package/dist/lib/grade.js +2 -0
- package/dist/lib/icon-depth.d.ts +35 -0
- package/dist/lib/icon-depth.js +2 -0
- package/dist/lib/icon-registry.js +1 -1
- package/dist/lib/motion.js +2 -2
- package/dist/lib/overlay-pill.d.ts +30 -0
- package/dist/lib/overlay-pill.js +2 -0
- package/dist/lib/utils.js +1 -1
- package/dist/place-types-BHdeMMiG.d.ts +317 -0
- package/package.json +1 -1
- package/src/components/button.tsx +5 -1
- package/src/components/colour-dictionary.stories.tsx +268 -82
- package/src/components/doorway-tiles.stories.tsx +83 -0
- package/src/components/doorway-tiles.test.tsx +51 -0
- package/src/components/doorway-tiles.tsx +225 -0
- package/src/components/grade-atom.stories.tsx +81 -0
- package/src/components/grade-atom.tsx +256 -0
- package/src/components/hue-chip.stories.tsx +2 -2
- package/src/components/menu-family-block.stories.tsx +29 -0
- package/src/components/menu-family-block.test.tsx +51 -0
- package/src/components/menu-family-block.tsx +20 -4
- package/src/components/menu-intent-hero.tsx +1 -1
- package/src/components/menu-mocks.ts +39 -3
- package/src/components/menu-types.ts +17 -33
- package/src/components/motion-icon-actions.test.tsx +115 -0
- package/src/components/motion-icon-actions.tsx +580 -0
- package/src/components/motion-icon-controls.test.tsx +1 -0
- package/src/components/motion-icon-controls.tsx +18 -13
- package/src/components/motion-icon-set.stories.tsx +62 -3
- package/src/components/motion-icon-set.tsx +349 -2
- package/src/components/motion-icon.test.tsx +180 -1
- package/src/components/motion-icon.tsx +97 -8
- package/src/components/place-card.stories.tsx +66 -76
- package/src/components/place-card.test.tsx +188 -100
- package/src/components/place-card.tsx +356 -148
- package/src/components/place-types.ts +17 -4
- package/src/components/poster-wall.stories.tsx +84 -0
- package/src/components/poster-wall.test.tsx +94 -3
- package/src/components/poster-wall.tsx +257 -9
- package/src/components/segmented-nav.stories.tsx +72 -1
- package/src/components/segmented-nav.test.tsx +96 -0
- package/src/components/segmented-nav.tsx +36 -4
- package/src/globals.css +26 -0
- package/src/hooks/use-haptic.test.tsx +80 -0
- package/src/hooks/use-haptic.ts +45 -4
- package/src/lib/grade.test.ts +82 -0
- package/src/lib/grade.ts +76 -0
- package/src/lib/icon-depth.ts +35 -0
- package/src/lib/overlay-pill.ts +29 -0
- package/dist/components/haptic-button.d.ts +0 -12
- package/dist/components/haptic-button.js +0 -17
- package/dist/components/menu-family-tiles.d.ts +0 -19
- package/dist/components/menu-family-tiles.js +0 -57
- package/src/components/haptic-button.stories.tsx +0 -41
- package/src/components/haptic-button.tsx +0 -19
- package/src/components/menu-family-tiles.stories.tsx +0 -54
- package/src/components/menu-family-tiles.test.tsx +0 -30
- package/src/components/menu-family-tiles.tsx +0 -132
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
-
import { PlaceCard } from "./place-card";
|
|
3
|
+
import { PlaceCard, PlaceCardSkeleton } from "./place-card";
|
|
4
4
|
import type { PlaceCardLabels, PlacePreview } from "./place-types";
|
|
5
5
|
|
|
6
6
|
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
7
7
|
|
|
8
8
|
const LABELS: PlaceCardLabels = {
|
|
9
|
-
open: "Open",
|
|
10
9
|
closed: "Closed",
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
viewDetails: (name) => `View details for ${name}`,
|
|
11
|
+
priceLevel: (level) => `Price level ${level} of 4`,
|
|
12
|
+
grade: {
|
|
13
|
+
soft: "Worth exploring",
|
|
14
|
+
bands: {
|
|
15
|
+
exceptional: "Exceptional",
|
|
16
|
+
great: "Great",
|
|
17
|
+
good: "Good",
|
|
18
|
+
decent: "Decent",
|
|
19
|
+
weak: "Weak",
|
|
20
|
+
},
|
|
21
|
+
aria: (value) => `Grade ${value} out of 10`,
|
|
22
|
+
capped: "Capped",
|
|
23
|
+
},
|
|
14
24
|
};
|
|
15
25
|
|
|
16
26
|
function makePlace(overrides: Partial<PlacePreview> = {}): PlacePreview {
|
|
@@ -25,163 +35,241 @@ function makePlace(overrides: Partial<PlacePreview> = {}): PlacePreview {
|
|
|
25
35
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
26
36
|
|
|
27
37
|
describe("PlaceCard", () => {
|
|
28
|
-
|
|
38
|
+
it("renders the name and an accessible card label", () => {
|
|
39
|
+
render(<PlaceCard place={makePlace()} labels={LABELS} onOpen={vi.fn()} />);
|
|
40
|
+
expect(screen.getByText("Test Place")).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByRole("button", { name: "View details for Test Place" })).toBeInTheDocument();
|
|
42
|
+
});
|
|
29
43
|
|
|
30
|
-
it("
|
|
31
|
-
render(<PlaceCard place={makePlace()} labels={LABELS} />);
|
|
44
|
+
it("is a plain block — no role, no tab stop — without href or onOpen", () => {
|
|
45
|
+
const { container } = render(<PlaceCard place={makePlace()} labels={LABELS} />);
|
|
32
46
|
expect(screen.getByText("Test Place")).toBeInTheDocument();
|
|
47
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
48
|
+
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
|
49
|
+
expect(container.querySelector("[tabindex]")).not.toBeInTheDocument();
|
|
50
|
+
expect(container.querySelector(".cursor-pointer")).not.toBeInTheDocument();
|
|
33
51
|
});
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
// ─── Headline metric ────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
it("shows the grade over the star when the place has one", () => {
|
|
36
56
|
render(
|
|
37
57
|
<PlaceCard
|
|
38
|
-
place={makePlace({
|
|
58
|
+
place={makePlace({
|
|
59
|
+
rating: { rating: 4.5, count: 120 },
|
|
60
|
+
reviews_digest: { grade: { overall: 8.2 } },
|
|
61
|
+
})}
|
|
39
62
|
labels={LABELS}
|
|
40
63
|
/>
|
|
41
64
|
);
|
|
42
|
-
expect(screen.
|
|
43
|
-
expect(screen.
|
|
65
|
+
expect(screen.getByLabelText("Grade 8.2 out of 10")).toBeInTheDocument();
|
|
66
|
+
expect(screen.queryByText("4.5")).not.toBeInTheDocument();
|
|
44
67
|
});
|
|
45
68
|
|
|
46
|
-
it("
|
|
47
|
-
render(<PlaceCard place={makePlace()} labels={LABELS} />);
|
|
48
|
-
|
|
49
|
-
expect(screen.queryByText(/^\d+\.\d+$/)).not.toBeInTheDocument();
|
|
69
|
+
it("falls back to the star rating when there is no grade", () => {
|
|
70
|
+
render(<PlaceCard place={makePlace({ rating: { rating: 4.5, count: 120 } })} labels={LABELS} />);
|
|
71
|
+
expect(screen.getByText("4.5")).toBeInTheDocument();
|
|
50
72
|
});
|
|
51
73
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
it("falls back to the star when the grade is soft — the xs slot has no room for the sentence", () => {
|
|
75
|
+
render(
|
|
76
|
+
<PlaceCard
|
|
77
|
+
place={makePlace({
|
|
78
|
+
rating: { rating: 4.5, count: 120 },
|
|
79
|
+
reviews_digest: { grade: { overall: 5.1, show_number: false } },
|
|
80
|
+
})}
|
|
81
|
+
labels={LABELS}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByText("4.5")).toBeInTheDocument();
|
|
85
|
+
expect(screen.queryByText("Worth exploring")).not.toBeInTheDocument();
|
|
60
86
|
});
|
|
61
87
|
|
|
62
|
-
it("renders
|
|
88
|
+
it("renders nothing in the metric slot without rating or grade", () => {
|
|
63
89
|
render(<PlaceCard place={makePlace()} labels={LABELS} />);
|
|
64
|
-
expect(screen.
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("fires onClick with the place object when clicked", () => {
|
|
68
|
-
const onClick = vi.fn();
|
|
69
|
-
const place = makePlace({ name: "Clickable Place" });
|
|
70
|
-
render(<PlaceCard place={place} labels={LABELS} onClick={onClick} />);
|
|
71
|
-
fireEvent.click(screen.getByRole("button"));
|
|
72
|
-
expect(onClick).toHaveBeenCalledTimes(1);
|
|
73
|
-
expect(onClick).toHaveBeenCalledWith(place);
|
|
90
|
+
expect(screen.queryByText(/^\d+\.\d+$/)).not.toBeInTheDocument();
|
|
74
91
|
});
|
|
75
92
|
|
|
76
|
-
// ───
|
|
77
|
-
|
|
78
|
-
it("renders UtensilsCrossed icon when thumbnail_url is missing", () => {
|
|
79
|
-
render(<PlaceCard place={makePlace()} labels={LABELS} />);
|
|
80
|
-
// The icon is aria-hidden but we can check that no <img> is rendered
|
|
81
|
-
expect(screen.queryByRole("img")).not.toBeInTheDocument();
|
|
82
|
-
// Verify the container still renders (icon placeholder)
|
|
83
|
-
const heading = screen.getByText("Test Place");
|
|
84
|
-
expect(heading).toBeInTheDocument();
|
|
85
|
-
});
|
|
93
|
+
// ─── Meta rows ──────────────────────────────────────────────────────────────
|
|
86
94
|
|
|
87
|
-
it("renders
|
|
95
|
+
it("renders the first cuisine and the price level", () => {
|
|
88
96
|
render(
|
|
89
97
|
<PlaceCard
|
|
90
|
-
place={makePlace({
|
|
98
|
+
place={makePlace({ cuisines: ["italian", "pizza"], price_dishes: { price_score: 2 } })}
|
|
91
99
|
labels={LABELS}
|
|
92
100
|
/>
|
|
93
101
|
);
|
|
94
|
-
expect(screen.
|
|
102
|
+
expect(screen.getByText("italian")).toBeInTheDocument();
|
|
103
|
+
expect(screen.queryByText("pizza")).not.toBeInTheDocument();
|
|
104
|
+
expect(screen.getByLabelText("Price level 2 of 4")).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("clamps an out-of-range price level instead of throwing", () => {
|
|
108
|
+
render(<PlaceCard place={makePlace({ price_drinks: { price_score: 7 } })} labels={LABELS} />);
|
|
109
|
+
expect(screen.getByLabelText("Price level 7 of 4")).toHaveTextContent("$$$$");
|
|
95
110
|
});
|
|
96
111
|
|
|
97
|
-
it("
|
|
112
|
+
it("prefers distance over the neighbourhood once it is known", () => {
|
|
98
113
|
render(
|
|
99
114
|
<PlaceCard
|
|
100
|
-
place={makePlace({
|
|
115
|
+
place={makePlace({
|
|
116
|
+
areas: [{ name: "Centru" }],
|
|
117
|
+
location: { distance: { meters: 1240 } },
|
|
118
|
+
})}
|
|
101
119
|
labels={LABELS}
|
|
102
120
|
/>
|
|
103
121
|
);
|
|
104
|
-
|
|
105
|
-
expect(
|
|
106
|
-
|
|
107
|
-
// Trigger the error handler
|
|
108
|
-
fireEvent.error(img);
|
|
109
|
-
|
|
110
|
-
// Image should be replaced by the fallback (no <img> in DOM)
|
|
111
|
-
expect(screen.queryByRole("img")).not.toBeInTheDocument();
|
|
122
|
+
expect(screen.getByText("1.2 km")).toBeInTheDocument();
|
|
123
|
+
expect(screen.queryByText("Centru")).not.toBeInTheDocument();
|
|
112
124
|
});
|
|
113
125
|
|
|
114
|
-
|
|
126
|
+
it("shows the neighbourhood without geolocation, and metres under a kilometre", () => {
|
|
127
|
+
const { rerender } = render(
|
|
128
|
+
<PlaceCard place={makePlace({ areas: [{ name: "Centru" }] })} labels={LABELS} />
|
|
129
|
+
);
|
|
130
|
+
expect(screen.getByText("Centru")).toBeInTheDocument();
|
|
115
131
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<PlaceCard place={makePlace({ open_now: true })} labels={LABELS} />
|
|
132
|
+
rerender(
|
|
133
|
+
<PlaceCard place={makePlace({ location: { distance: { meters: 340 } } })} labels={LABELS} />
|
|
119
134
|
);
|
|
120
|
-
expect(screen.getByText("
|
|
135
|
+
expect(screen.getByText("340 m")).toBeInTheDocument();
|
|
121
136
|
});
|
|
122
137
|
|
|
123
|
-
it("shows
|
|
124
|
-
render(
|
|
138
|
+
it("shows the closed pill only when open_now is explicitly false", () => {
|
|
139
|
+
const { rerender } = render(
|
|
125
140
|
<PlaceCard place={makePlace({ open_now: false })} labels={LABELS} />
|
|
126
141
|
);
|
|
127
142
|
expect(screen.getByText("Closed")).toBeInTheDocument();
|
|
143
|
+
|
|
144
|
+
rerender(<PlaceCard place={makePlace()} labels={LABELS} />);
|
|
145
|
+
expect(screen.queryByText("Closed")).not.toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("renders the footer under a separator", () => {
|
|
149
|
+
const { container } = render(
|
|
150
|
+
<PlaceCard place={makePlace()} labels={LABELS} footer={<span>Served as X</span>} />
|
|
151
|
+
);
|
|
152
|
+
expect(screen.getByText("Served as X")).toBeInTheDocument();
|
|
153
|
+
expect(container.querySelector('[data-slot="separator"]')).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ─── Cover image ────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
it("renders the placeholder without a cover, and after the cover fails", () => {
|
|
159
|
+
const { rerender } = render(<PlaceCard place={makePlace()} labels={LABELS} />);
|
|
160
|
+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
|
|
161
|
+
|
|
162
|
+
rerender(
|
|
163
|
+
<PlaceCard place={makePlace({ cover_url: "https://example.com/cover.jpg" })} labels={LABELS} />
|
|
164
|
+
);
|
|
165
|
+
const img = screen.getByRole("img");
|
|
166
|
+
expect(img).toHaveAttribute("alt", "Test Place");
|
|
167
|
+
fireEvent.error(img);
|
|
168
|
+
expect(screen.queryByRole("img")).not.toBeInTheDocument();
|
|
128
169
|
});
|
|
129
170
|
|
|
130
|
-
it("
|
|
171
|
+
it("routes the cover through an injected image component with the sizes and priority hints", () => {
|
|
172
|
+
const ImageComponent = vi.fn(() => <span data-testid="injected-image" />);
|
|
131
173
|
render(
|
|
132
174
|
<PlaceCard
|
|
133
|
-
place={makePlace({
|
|
134
|
-
open_now: true,
|
|
135
|
-
today_schedule: [{ opens_at: 9, closes_at: 22 }],
|
|
136
|
-
})}
|
|
175
|
+
place={makePlace({ cover_url: "place/1/cover.avif" })}
|
|
137
176
|
labels={LABELS}
|
|
177
|
+
imageComponent={ImageComponent}
|
|
178
|
+
imageSizes="220px"
|
|
179
|
+
priority
|
|
138
180
|
/>
|
|
139
181
|
);
|
|
140
|
-
|
|
141
|
-
expect(
|
|
182
|
+
expect(screen.getByTestId("injected-image")).toBeInTheDocument();
|
|
183
|
+
expect(ImageComponent).toHaveBeenCalledWith(
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
src: "place/1/cover.avif",
|
|
186
|
+
alt: "Test Place",
|
|
187
|
+
sizes: "220px",
|
|
188
|
+
priority: true,
|
|
189
|
+
}),
|
|
190
|
+
undefined
|
|
191
|
+
);
|
|
142
192
|
});
|
|
143
193
|
|
|
144
|
-
|
|
194
|
+
// ─── Interaction ────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
it("opens on click, Enter and Space without an href", () => {
|
|
197
|
+
const onOpen = vi.fn();
|
|
198
|
+
const place = makePlace();
|
|
199
|
+
render(<PlaceCard place={place} labels={LABELS} onOpen={onOpen} />);
|
|
200
|
+
const root = screen.getByRole("button");
|
|
201
|
+
fireEvent.click(root);
|
|
202
|
+
fireEvent.keyDown(root, { key: "Enter" });
|
|
203
|
+
fireEvent.keyDown(root, { key: " " });
|
|
204
|
+
expect(onOpen).toHaveBeenCalledTimes(3);
|
|
205
|
+
expect(onOpen).toHaveBeenCalledWith(place);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("renders a real anchor for an href and intercepts only plain left-clicks", () => {
|
|
209
|
+
const onOpen = vi.fn();
|
|
145
210
|
render(
|
|
146
|
-
<PlaceCard
|
|
147
|
-
place={makePlace({
|
|
148
|
-
open_now: false,
|
|
149
|
-
today_schedule: [{ opens_at: 9, closes_at: 22 }],
|
|
150
|
-
})}
|
|
151
|
-
labels={LABELS}
|
|
152
|
-
/>
|
|
211
|
+
<PlaceCard place={makePlace()} labels={LABELS} href="/ro/cluj/places/test-place" onOpen={onOpen} />
|
|
153
212
|
);
|
|
154
|
-
|
|
213
|
+
const link = screen.getByRole("link", { name: "View details for Test Place" });
|
|
214
|
+
expect(link).toHaveAttribute("href", "/ro/cluj/places/test-place");
|
|
215
|
+
|
|
216
|
+
const plain = fireEvent.click(link);
|
|
217
|
+
expect(plain).toBe(false);
|
|
218
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
219
|
+
|
|
220
|
+
const modified = fireEvent.click(link, { metaKey: true });
|
|
221
|
+
expect(modified).toBe(true);
|
|
222
|
+
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
155
223
|
});
|
|
156
224
|
|
|
157
|
-
|
|
225
|
+
it("keeps the linked root mounted across re-renders (motion wrapper is memoised)", () => {
|
|
226
|
+
const place = makePlace({ cover_url: "https://example.com/cover.jpg" });
|
|
227
|
+
const { rerender } = render(<PlaceCard place={place} labels={LABELS} href="/x" />);
|
|
228
|
+
const anchor = screen.getByRole("link");
|
|
229
|
+
const img = screen.getByRole("img");
|
|
158
230
|
|
|
159
|
-
|
|
231
|
+
// A fresh labels object forces a render; an un-memoised `motion.create`
|
|
232
|
+
// would give the root a new element type and React would remount both.
|
|
233
|
+
rerender(<PlaceCard place={place} labels={{ ...LABELS }} href="/x" />);
|
|
234
|
+
expect(screen.getByRole("link")).toBe(anchor);
|
|
235
|
+
expect(screen.getByRole("img")).toBe(img);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("uses the injected link component", () => {
|
|
239
|
+
const LinkComponent = vi.fn(({ href, children }: { href: string; children: React.ReactNode }) => (
|
|
240
|
+
<a href={href} data-testid="injected-link">
|
|
241
|
+
{children}
|
|
242
|
+
</a>
|
|
243
|
+
));
|
|
160
244
|
render(
|
|
161
|
-
<PlaceCard
|
|
162
|
-
place={makePlace({
|
|
163
|
-
open_now: false,
|
|
164
|
-
today_schedule: [{ opens_at: 11, closes_at: 23 }],
|
|
165
|
-
hours: [{ opens_at: 9, closes_at: 22 }],
|
|
166
|
-
})}
|
|
167
|
-
labels={LABELS}
|
|
168
|
-
/>
|
|
245
|
+
<PlaceCard place={makePlace()} labels={LABELS} href="/x" linkComponent={LinkComponent} />
|
|
169
246
|
);
|
|
170
|
-
|
|
171
|
-
expect(screen.getByText(/opens at 11:00/)).toBeInTheDocument();
|
|
172
|
-
expect(screen.queryByText(/09:00/)).not.toBeInTheDocument();
|
|
247
|
+
expect(screen.getByTestId("injected-link")).toHaveAttribute("href", "/x");
|
|
173
248
|
});
|
|
174
249
|
|
|
175
|
-
it("
|
|
250
|
+
it("keeps the photo overlay outside the interactive root", () => {
|
|
176
251
|
render(
|
|
177
252
|
<PlaceCard
|
|
178
|
-
place={makePlace(
|
|
179
|
-
open_now: false,
|
|
180
|
-
hours: [{ opens_at: 9, closes_at: 22 }],
|
|
181
|
-
})}
|
|
253
|
+
place={makePlace()}
|
|
182
254
|
labels={LABELS}
|
|
255
|
+
onOpen={vi.fn()}
|
|
256
|
+
photoOverlay={<button type="button">Trending</button>}
|
|
183
257
|
/>
|
|
184
258
|
);
|
|
185
|
-
|
|
259
|
+
const overlay = screen.getByRole("button", { name: "Trending" });
|
|
260
|
+
const card = screen.getByRole("button", { name: "View details for Test Place" });
|
|
261
|
+
expect(card).not.toContainElement(overlay);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("PlaceCardSkeleton", () => {
|
|
266
|
+
it("is hidden from assistive tech and reserves the footer when asked", () => {
|
|
267
|
+
const { container, rerender } = render(<PlaceCardSkeleton />);
|
|
268
|
+
expect(container.firstChild).toHaveAttribute("aria-hidden", "true");
|
|
269
|
+
expect(container.querySelector('[data-slot="separator"]')).not.toBeInTheDocument();
|
|
270
|
+
|
|
271
|
+
rerender(<PlaceCardSkeleton footer={<span data-testid="footer-skeleton" />} />);
|
|
272
|
+
expect(container.querySelector('[data-slot="separator"]')).toBeInTheDocument();
|
|
273
|
+
expect(screen.getByTestId("footer-skeleton")).toBeInTheDocument();
|
|
186
274
|
});
|
|
187
275
|
});
|