@jusankar/moon 0.1.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.
Files changed (41) hide show
  1. package/README.md +44 -0
  2. package/dist/icons.d.ts +2 -0
  3. package/dist/icons.js +30051 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +3389 -0
  6. package/dist/src/components/alert/alert.d.ts +11 -0
  7. package/dist/src/components/alert/alert.story.d.ts +9 -0
  8. package/dist/src/components/alert/alert.test.d.ts +1 -0
  9. package/dist/src/components/alert/index.d.ts +1 -0
  10. package/dist/src/components/badge/badge.d.ts +10 -0
  11. package/dist/src/components/badge/badge.story.d.ts +10 -0
  12. package/dist/src/components/badge/badge.test.d.ts +1 -0
  13. package/dist/src/components/badge/index.d.ts +1 -0
  14. package/dist/src/components/card/card.d.ts +11 -0
  15. package/dist/src/components/card/card.story.d.ts +9 -0
  16. package/dist/src/components/card/card.test.d.ts +1 -0
  17. package/dist/src/components/card/index.d.ts +1 -0
  18. package/dist/src/icons.d.ts +1 -0
  19. package/dist/src/index.d.ts +3 -0
  20. package/dist/src/tests/vitest.setup.d.ts +7 -0
  21. package/dist/src/utils.d.ts +2 -0
  22. package/dist/vite.config.d.ts +3 -0
  23. package/package.json +132 -0
  24. package/src/components/alert/alert.story.tsx +58 -0
  25. package/src/components/alert/alert.test.tsx +299 -0
  26. package/src/components/alert/alert.tsx +65 -0
  27. package/src/components/alert/index.ts +1 -0
  28. package/src/components/badge/badge.story.tsx +82 -0
  29. package/src/components/badge/badge.test.tsx +189 -0
  30. package/src/components/badge/badge.tsx +43 -0
  31. package/src/components/badge/index.ts +1 -0
  32. package/src/components/card/card.story.tsx +123 -0
  33. package/src/components/card/card.test.tsx +231 -0
  34. package/src/components/card/card.tsx +85 -0
  35. package/src/components/card/index.ts +1 -0
  36. package/src/icons.ts +1 -0
  37. package/src/index.ts +3 -0
  38. package/src/styles/index.css +123 -0
  39. package/src/styles/storybook-only.css +20 -0
  40. package/src/tests/vitest.setup.ts +76 -0
  41. package/src/utils.ts +6 -0
@@ -0,0 +1 @@
1
+ export * from './alert'
@@ -0,0 +1,82 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import { Badge } from '~/src/components/badge'
3
+ import { BadgeCheck, BookmarkIcon, ArrowUpRightIcon } from 'lucide-react'
4
+
5
+ const meta: Meta<typeof Badge> = {
6
+ title: 'Components/Badge',
7
+ component: Badge,
8
+ }
9
+ export default meta
10
+ type Story = StoryObj<typeof Badge>
11
+
12
+ export const Variants: Story = {
13
+ render: () => (
14
+ <div className="flex flex-wrap gap-2">
15
+ <Badge>Default</Badge>
16
+ <Badge variant="secondary">Secondary</Badge>
17
+ <Badge variant="destructive">Destructive</Badge>
18
+ <Badge variant="outline">Outline</Badge>
19
+ <Badge variant="ghost">Ghost</Badge>
20
+ </div>
21
+ ),
22
+ }
23
+
24
+ export const WithIcon: Story = {
25
+ render: () => (
26
+ <div className="flex flex-wrap gap-2">
27
+ <Badge variant="secondary">
28
+ <BadgeCheck data-icon="inline-start" />
29
+ Verified
30
+ </Badge>
31
+ <Badge variant="outline">
32
+ Bookmark
33
+ <BookmarkIcon data-icon="inline-end" />
34
+ </Badge>
35
+ </div>
36
+ ),
37
+ }
38
+
39
+ export const WithSpinner: Story = {
40
+ render: () => (
41
+ <div className="flex flex-wrap gap-2">
42
+ <Badge variant="destructive">
43
+ Deleting
44
+ </Badge>
45
+ <Badge variant="secondary">
46
+ Generating
47
+ </Badge>
48
+ </div>
49
+ ),
50
+ }
51
+
52
+ export const AsLink: Story = {
53
+ render: () => (
54
+ <Badge asChild>
55
+ <a href="#link">
56
+ Open Link <ArrowUpRightIcon data-icon="inline-end" />
57
+ </a>
58
+ </Badge>
59
+ ),
60
+ }
61
+
62
+ export const CustomColors: Story = {
63
+ render: () => (
64
+ <div className="flex flex-wrap gap-2">
65
+ <Badge className="bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
66
+ Blue
67
+ </Badge>
68
+ <Badge className="bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300">
69
+ Green
70
+ </Badge>
71
+ <Badge className="bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300">
72
+ Sky
73
+ </Badge>
74
+ <Badge className="bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300">
75
+ Purple
76
+ </Badge>
77
+ <Badge className="bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300">
78
+ Red
79
+ </Badge>
80
+ </div>
81
+ ),
82
+ }
@@ -0,0 +1,189 @@
1
+ import React from "react"
2
+ import { render, screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+ import { axe } from "vitest-axe"
5
+ import { describe, expect, it } from "vitest"
6
+
7
+ import { Badge } from "./badge"
8
+
9
+ // Dummy icons for testing purposes inline with usage examples
10
+ const DummyIconStart = () => <svg data-icon="inline-start" aria-hidden="true"></svg>
11
+ const DummyIconEnd = () => <svg data-icon="inline-end" aria-hidden="true"></svg>
12
+
13
+ describe("Badge", () => {
14
+ describe("rendering", () => {
15
+ it("renders the badge with default content", () => {
16
+ render(<Badge>Default Badge</Badge>)
17
+ expect(screen.getByText("Default Badge")).toBeInTheDocument()
18
+ })
19
+
20
+ it("renders the badge container element", () => {
21
+ const { container } = render(<Badge>Badge Content</Badge>)
22
+ expect(container.firstChild).toBeInstanceOf(HTMLElement)
23
+ })
24
+ })
25
+
26
+ describe("components", () => {
27
+ it("renders children content correctly", () => {
28
+ render(<Badge>Children Content</Badge>)
29
+ expect(screen.getByText("Children Content")).toBeVisible()
30
+ })
31
+ })
32
+
33
+ describe("variants", () => {
34
+ it("applies 'default' variant by default", () => {
35
+ const { container } = render(<Badge>Default Variant</Badge>)
36
+ expect(container.firstChild).toHaveClass("rounded") // Assuming default styling includes rounded
37
+ })
38
+
39
+ it("applies the 'secondary' variant class", () => {
40
+ const { container } = render(<Badge variant="secondary">Secondary</Badge>)
41
+ expect(container.firstChild).toHaveClass("secondary")
42
+ })
43
+
44
+ it("applies the 'destructive' variant class", () => {
45
+ const { container } = render(<Badge variant="destructive">Destructive</Badge>)
46
+ expect(container.firstChild).toHaveClass("destructive")
47
+ })
48
+
49
+ it("applies the 'outline' variant class", () => {
50
+ const { container } = render(<Badge variant="outline">Outline</Badge>)
51
+ expect(container.firstChild).toHaveClass("outline")
52
+ })
53
+
54
+ it("applies the 'ghost' variant class", () => {
55
+ const { container } = render(<Badge variant="ghost">Ghost</Badge>)
56
+ expect(container.firstChild).toHaveClass("ghost")
57
+ })
58
+
59
+ it("applies the 'link' variant class", () => {
60
+ const { container } = render(<Badge variant="link">Link</Badge>)
61
+ expect(container.firstChild).toHaveClass("link")
62
+ })
63
+ })
64
+
65
+ describe("size", () => {
66
+ // Badge component does not inherently define explicit sizes by prop
67
+ it("does not apply any size-related class by default", () => {
68
+ const { container } = render(<Badge>Size Test</Badge>)
69
+ expect(container.firstChild).not.toHaveClass(/^size-/)
70
+ })
71
+ })
72
+
73
+ describe("subcomponents", () => {
74
+ it("renders icon with data-icon='inline-start' attribute", () => {
75
+ const { container } = render(
76
+ <Badge>
77
+ <DummyIconStart />
78
+ Text
79
+ </Badge>
80
+ )
81
+ const icon = container.querySelector('[data-icon="inline-start"]')
82
+ expect(icon).toBeInTheDocument()
83
+ })
84
+
85
+ it("renders icon with data-icon='inline-end' attribute", () => {
86
+ const { container } = render(
87
+ <Badge>
88
+ Text
89
+ <DummyIconEnd />
90
+ </Badge>
91
+ )
92
+ const icon = container.querySelector('[data-icon="inline-end"]')
93
+ expect(icon).toBeInTheDocument()
94
+ })
95
+ })
96
+
97
+ describe("state", () => {
98
+ // No explicit state props or controlling state for Badge component
99
+ it("renders static Badge without state changes", () => {
100
+ render(<Badge>Static State</Badge>)
101
+ expect(screen.getByText("Static State")).toBeVisible()
102
+ })
103
+ })
104
+
105
+ describe("props", () => {
106
+ it("accepts and applies a custom className prop", () => {
107
+ const { container } = render(
108
+ <Badge className="custom-class">Custom Class</Badge>
109
+ )
110
+ expect(container.firstChild).toHaveClass("custom-class")
111
+ })
112
+
113
+ it("accepts an empty className prop without error", () => {
114
+ const { container } = render(<Badge className="">Empty Class</Badge>)
115
+ expect(container.firstChild).toBeInTheDocument()
116
+ })
117
+
118
+ it("renders correctly with minimal required props", () => {
119
+ render(<Badge>Minimal</Badge>)
120
+ expect(screen.getByText("Minimal")).toBeInTheDocument()
121
+ })
122
+
123
+ it("can render as a child element when 'asChild' prop is true", () => {
124
+ // We cannot fully implement this behavior test as 'asChild' likely uses Radix Polymorphic behavior.
125
+ // Instead, we'll at least verify the container renders child anchor correctly.
126
+ render(
127
+ <Badge asChild>
128
+ <a href="#test">Link Badge</a>
129
+ </Badge>
130
+ )
131
+ expect(screen.getByRole("link", { name: /Link Badge/i })).toBeInTheDocument()
132
+ })
133
+ })
134
+
135
+ describe("interactions", () => {
136
+ // Badge is non-interactive by default, no user input interaction handlers tested.
137
+ it("does not support user interactions inherently", () => {
138
+ render(<Badge>Non interactive</Badge>)
139
+ expect(screen.getByText("Non interactive")).toBeInTheDocument()
140
+ })
141
+ })
142
+
143
+ describe("accessibility", () => {
144
+ it("should have no accessibility violations", async () => {
145
+ const { container } = render(<Badge>Accessible Badge</Badge>)
146
+ const results = await axe(container)
147
+ expect(results).toHaveNoViolations()
148
+ })
149
+
150
+ it("renders icon slots with appropriate aria-hidden attribute", () => {
151
+ const { container } = render(
152
+ <Badge>
153
+ <DummyIconStart />
154
+ Text
155
+ <DummyIconEnd />
156
+ </Badge>
157
+ )
158
+ const iconStart = container.querySelector('[data-icon="inline-start"]')
159
+ const iconEnd = container.querySelector('[data-icon="inline-end"]')
160
+ expect(iconStart).toHaveAttribute("aria-hidden", "true")
161
+ expect(iconEnd).toHaveAttribute("aria-hidden", "true")
162
+ })
163
+ })
164
+
165
+ describe("error handling", () => {
166
+ it("renders gracefully when no children are provided", () => {
167
+ const { container } = render(<Badge>{null}</Badge>)
168
+ expect(container.firstChild).toBeInTheDocument()
169
+ })
170
+
171
+ it("renders gracefully when given undefined children", () => {
172
+ const { container } = render(<Badge>{undefined}</Badge>)
173
+ expect(container.firstChild).toBeInTheDocument()
174
+ })
175
+
176
+ it("renders gracefully when variant prop is invalid", () => {
177
+ // @ts-expect-error testing resilience with invalid variant prop
178
+ const { container } = render(<Badge variant="invalid">Invalid Variant</Badge>)
179
+ expect(container.firstChild).toBeInTheDocument()
180
+ })
181
+ })
182
+
183
+ describe("exports", () => {
184
+ it("should have named export: Badge", () => {
185
+ expect(Badge).toBeDefined()
186
+ expect(typeof Badge).toBe("function")
187
+ })
188
+ })
189
+ })
@@ -0,0 +1,43 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { cn } from "../../utils"
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
11
+ secondary:
12
+ "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
13
+ destructive:
14
+ "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
17
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
18
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "default",
23
+ },
24
+ }
25
+ )
26
+ function Badge({
27
+ className,
28
+ variant = "default",
29
+ asChild = false,
30
+ ...props
31
+ }: React.ComponentProps<"span"> &
32
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
33
+ const Comp = asChild ? Slot : "span"
34
+ return (
35
+ <Comp
36
+ data-slot="badge"
37
+ data-variant={variant}
38
+ className={cn(badgeVariants({ variant }), className)}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+ export { Badge, badgeVariants }
@@ -0,0 +1 @@
1
+ export * from './badge'
@@ -0,0 +1,123 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+ import {
3
+ Card,
4
+ CardAction,
5
+ CardContent,
6
+ CardDescription,
7
+ CardFooter,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from '~/src/components/card'
11
+ import { Badge } from '~/src/components/badge'
12
+
13
+ const meta: Meta<typeof Card> = {
14
+ title: 'Components/Card',
15
+ component: Card,
16
+ }
17
+ export default meta
18
+ type Story = StoryObj<typeof Card>
19
+
20
+ export const Usage: Story = {
21
+ render: () => (
22
+ <Card>
23
+ <CardHeader>
24
+ <CardTitle>Card Title</CardTitle>
25
+ <CardDescription>Card Description</CardDescription>
26
+ <CardAction>Card Action</CardAction>
27
+ </CardHeader>
28
+ <CardContent>
29
+ <p>Card Content</p>
30
+ </CardContent>
31
+ <CardFooter>
32
+ <p>Card Footer</p>
33
+ </CardFooter>
34
+ </Card>
35
+ ),
36
+ }
37
+
38
+ export const Size: Story = {
39
+ render: () => (
40
+ <Card size="sm" className="mx-auto w-full max-w-sm">
41
+ <CardHeader>
42
+ <CardTitle>Small Card</CardTitle>
43
+ <CardDescription>
44
+ This card uses the small size variant.
45
+ </CardDescription>
46
+ </CardHeader>
47
+ <CardContent>
48
+ <p>
49
+ The card component supports a size prop that can be set to
50
+ &quot;sm&quot; for a more compact appearance.
51
+ </p>
52
+ </CardContent>
53
+ <CardFooter>
54
+ Action
55
+ </CardFooter>
56
+ </Card>
57
+ ),
58
+ }
59
+
60
+ export const Image: Story = {
61
+ render: () => (
62
+ <Card className="relative mx-auto w-full max-w-sm pt-0">
63
+ <div className="absolute inset-0 z-30 aspect-video bg-black/35" />
64
+ <img
65
+ src="https://avatar.vercel.sh/shadcn1"
66
+ alt="Event cover"
67
+ className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale dark:brightness-40"
68
+ />
69
+ <CardHeader>
70
+ <CardAction>
71
+ <Badge variant="secondary">Featured</Badge>
72
+ </CardAction>
73
+ <CardTitle>Design systems meetup</CardTitle>
74
+ <CardDescription>
75
+ A practical talk on component APIs, accessibility, and shipping
76
+ faster.
77
+ </CardDescription>
78
+ </CardHeader>
79
+ <CardFooter>
80
+ View Event
81
+ </CardFooter>
82
+ </Card>
83
+ ),
84
+ }
85
+
86
+ export const Demo: Story = {
87
+ render: () => (
88
+ <Card className="w-full max-w-sm">
89
+ <CardHeader>
90
+ <CardTitle>Login to your account</CardTitle>
91
+ <CardDescription>
92
+ Enter your email below to login to your account
93
+ </CardDescription>
94
+ <CardAction>
95
+ Sign up
96
+ </CardAction>
97
+ </CardHeader>
98
+ <CardContent>
99
+ <form>
100
+ <div className="flex flex-col gap-6">
101
+ <div className="grid gap-2">
102
+ </div>
103
+ <div className="grid gap-2">
104
+ <div className="flex items-center">
105
+ Password
106
+ <a
107
+ href="#"
108
+ className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
109
+ >
110
+ Forgot your password?
111
+ </a>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </form>
116
+ </CardContent>
117
+ <CardFooter className="flex-col gap-2">
118
+ Login
119
+ Login with Google
120
+ </CardFooter>
121
+ </Card>
122
+ ),
123
+ }
@@ -0,0 +1,231 @@
1
+ import React from "react"
2
+ import { render, screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+ import { axe } from "vitest-axe"
5
+ import {
6
+ Card,
7
+ CardAction,
8
+ CardContent,
9
+ CardDescription,
10
+ CardFooter,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "~/src/components/card"
14
+
15
+ describe("Card", () => {
16
+ describe("rendering", () => {
17
+ it("renders the Card container", () => {
18
+ const { container } = render(<Card>Test card</Card>)
19
+ expect(container.firstChild).toBeInTheDocument()
20
+ })
21
+
22
+ it("renders CardHeader, CardContent and CardFooter when provided", () => {
23
+ render(
24
+ <Card>
25
+ <CardHeader>Header</CardHeader>
26
+ <CardContent>Content</CardContent>
27
+ <CardFooter>Footer</CardFooter>
28
+ </Card>
29
+ )
30
+ expect(screen.getByText("Header")).toBeVisible()
31
+ expect(screen.getByText("Content")).toBeVisible()
32
+ expect(screen.getByText("Footer")).toBeVisible()
33
+ })
34
+
35
+ it("renders CardTitle and CardDescription inside CardHeader", () => {
36
+ render(
37
+ <CardHeader>
38
+ <CardTitle>Title</CardTitle>
39
+ <CardDescription>Description</CardDescription>
40
+ </CardHeader>
41
+ )
42
+ expect(screen.getByText("Title")).toBeVisible()
43
+ expect(screen.getByText("Description")).toBeVisible()
44
+ })
45
+
46
+ it("renders CardAction inside CardHeader", () => {
47
+ render(
48
+ <CardHeader>
49
+ <CardAction>Action</CardAction>
50
+ </CardHeader>
51
+ )
52
+ expect(screen.getByText("Action")).toBeVisible()
53
+ })
54
+ })
55
+
56
+ describe("components", () => {
57
+ it("CardHeader supports className and renders correctly", () => {
58
+ const { container } = render(
59
+ <CardHeader className="custom-header">Header</CardHeader>
60
+ )
61
+ const header = container.querySelector(".custom-header")
62
+ expect(header).toBeInTheDocument()
63
+ expect(header).toHaveTextContent("Header")
64
+ })
65
+
66
+ it("CardTitle supports className and renders text", () => {
67
+ const { container } = render(
68
+ <CardTitle className="custom-title">My Title</CardTitle>
69
+ )
70
+ const title = container.querySelector(".custom-title")
71
+ expect(title).toBeInTheDocument()
72
+ expect(title).toHaveTextContent("My Title")
73
+ })
74
+
75
+ it("CardDescription supports className and renders text", () => {
76
+ const { container } = render(
77
+ <CardDescription className="custom-description">Desc</CardDescription>
78
+ )
79
+ const description = container.querySelector(".custom-description")
80
+ expect(description).toBeInTheDocument()
81
+ expect(description).toHaveTextContent("Desc")
82
+ })
83
+
84
+ it("CardAction supports className and renders children", () => {
85
+ const { container } = render(
86
+ <CardAction className="custom-action">
87
+ Click
88
+ </CardAction>
89
+ )
90
+ const action = container.querySelector(".custom-action")
91
+ expect(action).toBeInTheDocument()
92
+ expect(screen.getByRole("button", { name: "Click" })).toBeVisible()
93
+ })
94
+
95
+ it("CardContent supports className and renders children", () => {
96
+ const { container } = render(
97
+ <CardContent className="custom-content">
98
+ <p>Content text</p>
99
+ </CardContent>
100
+ )
101
+ const content = container.querySelector(".custom-content")
102
+ expect(content).toBeInTheDocument()
103
+ expect(screen.getByText("Content text")).toBeVisible()
104
+ })
105
+
106
+ it("CardFooter supports className and renders children", () => {
107
+ const { container } = render(
108
+ <CardFooter className="custom-footer">Footer content</CardFooter>
109
+ )
110
+ const footer = container.querySelector(".custom-footer")
111
+ expect(footer).toBeInTheDocument()
112
+ expect(screen.getByText("Footer content")).toBeVisible()
113
+ })
114
+ })
115
+
116
+ describe("variants", () => {
117
+ it("does not apply any special variant classes by default", () => {
118
+ const { container } = render(<Card>Default</Card>)
119
+ expect(container.firstChild).not.toHaveClass(/size-/)
120
+ })
121
+ })
122
+
123
+ describe("size", () => {
124
+ it("applies small size class when size='sm' is set", () => {
125
+ const { container } = render(<Card size="sm">Small Card</Card>)
126
+ expect(container.firstChild).toHaveClass("sm")
127
+ })
128
+
129
+ it("defaults to normal size when size is not set", () => {
130
+ const { container } = render(<Card>Default Size</Card>)
131
+ // No "sm" class expected on root container for default
132
+ expect(container.firstChild).not.toHaveClass("sm")
133
+ })
134
+ })
135
+
136
+ describe("subcomponents", () => {
137
+ // Subcomponents covered individually
138
+ it("CardTitle renders as heading by default", () => {
139
+ render(<CardTitle>Heading Title</CardTitle>)
140
+ const heading = screen.getByText("Heading Title")
141
+ expect(heading.tagName.toLowerCase()).toMatch(/h[1-6]/)
142
+ })
143
+ })
144
+
145
+ describe("state", () => {
146
+ // No explicit state props or stateful behavior found in the component
147
+ })
148
+
149
+ describe("props", () => {
150
+ it("forwards className to root Card container", () => {
151
+ const { container } = render(<Card className="my-card">Content</Card>)
152
+ expect(container.firstChild).toHaveClass("my-card")
153
+ })
154
+
155
+ it("forwards other props to the root Card container", () => {
156
+ const { container } = render(
157
+ <Card id="card-id" data-testid="card-test">
158
+ Content
159
+ </Card>
160
+ )
161
+ expect(container.firstChild).toHaveAttribute("id", "card-id")
162
+ expect(container.firstChild).toHaveAttribute("data-testid", "card-test")
163
+ })
164
+ })
165
+
166
+ describe("interactions", () => {
167
+ // No user input interactions handled directly by Card components
168
+ })
169
+
170
+ describe("accessibility", () => {
171
+ it("has no accessibility violations when rendering full Card", async () => {
172
+ const { container } = render(
173
+ <Card>
174
+ <CardHeader>
175
+ <CardTitle>Title</CardTitle>
176
+ <CardDescription>Description</CardDescription>
177
+ <CardAction>
178
+ </CardAction>
179
+ </CardHeader>
180
+ <CardContent>
181
+ <p>This is the main content</p>
182
+ </CardContent>
183
+ <CardFooter>
184
+ </CardFooter>
185
+ </Card>
186
+ )
187
+ const results = await axe(container)
188
+ expect(results).toHaveNoViolations()
189
+ })
190
+
191
+ it("renders CardTitle as a heading element for semantic accessibility", () => {
192
+ render(<CardTitle>Accessible Title</CardTitle>)
193
+ const title = screen.getByText("Accessible Title")
194
+ // Heading tags h1-h6
195
+ expect(title.tagName).toMatch(/H[1-6]/)
196
+ })
197
+ })
198
+
199
+ describe("error handling", () => {
200
+ it("renders without crashing when no children are provided", () => {
201
+ const { container } = render(<Card />)
202
+ expect(container.firstChild).toBeInTheDocument()
203
+ })
204
+
205
+ it("renders subcomponents gracefully with minimal or empty props", () => {
206
+ const { container } = render(
207
+ <Card>
208
+ <CardHeader className="">
209
+ <CardTitle />
210
+ <CardDescription />
211
+ </CardHeader>
212
+ <CardContent />
213
+ <CardFooter />
214
+ </Card>
215
+ )
216
+ expect(container.firstChild).toBeInTheDocument()
217
+ })
218
+ })
219
+
220
+ describe("exports", () => {
221
+ it("exports all subcomponents as named exports", () => {
222
+ expect(Card).toBeDefined()
223
+ expect(CardHeader).toBeDefined()
224
+ expect(CardTitle).toBeDefined()
225
+ expect(CardDescription).toBeDefined()
226
+ expect(CardAction).toBeDefined()
227
+ expect(CardContent).toBeDefined()
228
+ expect(CardFooter).toBeDefined()
229
+ })
230
+ })
231
+ })