@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.
- package/README.md +44 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +30051 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3389 -0
- package/dist/src/components/alert/alert.d.ts +11 -0
- package/dist/src/components/alert/alert.story.d.ts +9 -0
- package/dist/src/components/alert/alert.test.d.ts +1 -0
- package/dist/src/components/alert/index.d.ts +1 -0
- package/dist/src/components/badge/badge.d.ts +10 -0
- package/dist/src/components/badge/badge.story.d.ts +10 -0
- package/dist/src/components/badge/badge.test.d.ts +1 -0
- package/dist/src/components/badge/index.d.ts +1 -0
- package/dist/src/components/card/card.d.ts +11 -0
- package/dist/src/components/card/card.story.d.ts +9 -0
- package/dist/src/components/card/card.test.d.ts +1 -0
- package/dist/src/components/card/index.d.ts +1 -0
- package/dist/src/icons.d.ts +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/tests/vitest.setup.d.ts +7 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/vite.config.d.ts +3 -0
- package/package.json +132 -0
- package/src/components/alert/alert.story.tsx +58 -0
- package/src/components/alert/alert.test.tsx +299 -0
- package/src/components/alert/alert.tsx +65 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/badge/badge.story.tsx +82 -0
- package/src/components/badge/badge.test.tsx +189 -0
- package/src/components/badge/badge.tsx +43 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/card/card.story.tsx +123 -0
- package/src/components/card/card.test.tsx +231 -0
- package/src/components/card/card.tsx +85 -0
- package/src/components/card/index.ts +1 -0
- package/src/icons.ts +1 -0
- package/src/index.ts +3 -0
- package/src/styles/index.css +123 -0
- package/src/styles/storybook-only.css +20 -0
- package/src/tests/vitest.setup.ts +76 -0
- 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
|
+
"sm" 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
|
+
})
|