@reqord/web 0.1.0 → 0.3.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 (46) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/components/dashboard/critical-path-display.test.tsx +61 -0
  3. package/src/__tests__/components/dashboard/progress-bar.test.tsx +63 -0
  4. package/src/__tests__/components/dashboard/project-health.test.tsx +21 -7
  5. package/src/__tests__/components/dashboard/status-card.test.tsx +86 -0
  6. package/src/__tests__/components/dashboard/warning-alert.test.tsx +6 -6
  7. package/src/__tests__/components/feedback/feedback-filters-improved.test.tsx +33 -0
  8. package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +12 -0
  9. package/src/__tests__/components/graph/edge-styles.test.ts +6 -6
  10. package/src/__tests__/components/graph/issue-node.test.tsx +25 -6
  11. package/src/__tests__/components/graph/requirement-node.test.tsx +45 -0
  12. package/src/__tests__/components/graph/specification-node.test.tsx +27 -14
  13. package/src/__tests__/components/requirement/requirement-table.test.tsx +165 -0
  14. package/src/__tests__/components/specification/specification-table.test.tsx +189 -0
  15. package/src/__tests__/components/ui/badge.test.tsx +98 -0
  16. package/src/__tests__/components/ui/button.test.tsx +98 -0
  17. package/src/__tests__/components/ui/card.test.tsx +58 -0
  18. package/src/__tests__/components/ui/nav.test.tsx +91 -0
  19. package/src/__tests__/components/ui/tabs.test.tsx +53 -0
  20. package/src/__tests__/lib/drilldown-graph-data.test.ts +45 -3
  21. package/src/app/dashboard/page.tsx +29 -21
  22. package/src/app/globals.css +46 -0
  23. package/src/app/layout.tsx +4 -1
  24. package/src/app/requirements/loading.tsx +30 -5
  25. package/src/app/specifications/loading.tsx +29 -5
  26. package/src/components/dashboard/critical-path-display.tsx +30 -15
  27. package/src/components/dashboard/progress-bar.tsx +2 -4
  28. package/src/components/dashboard/project-health.tsx +9 -10
  29. package/src/components/dashboard/status-card.tsx +20 -9
  30. package/src/components/dashboard/warning-alert.tsx +57 -5
  31. package/src/components/feedback/feedback-filters.tsx +41 -12
  32. package/src/components/graph/drilldown-breadcrumb.tsx +1 -1
  33. package/src/components/graph/drilldown-graph.tsx +3 -1
  34. package/src/components/graph/edge-styles.ts +3 -3
  35. package/src/components/graph/issue-node.tsx +7 -7
  36. package/src/components/graph/multi-level-graph.tsx +2 -2
  37. package/src/components/graph/requirement-node.tsx +5 -5
  38. package/src/components/graph/specification-node.tsx +12 -9
  39. package/src/components/requirement/requirement-table.tsx +62 -18
  40. package/src/components/specification/specification-table.tsx +59 -17
  41. package/src/components/ui/badge.tsx +4 -4
  42. package/src/components/ui/button.tsx +43 -0
  43. package/src/components/ui/card.tsx +25 -0
  44. package/src/components/ui/nav.tsx +35 -35
  45. package/src/components/ui/tabs.tsx +2 -0
  46. package/src/lib/drilldown-graph-data.ts +23 -4
@@ -0,0 +1,165 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, vi, afterEach } from "vitest";
4
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { RequirementTable } from "../../../components/requirement/requirement-table";
7
+ import type { Requirement } from "@reqord/shared";
8
+
9
+ vi.mock("next/link", () => ({
10
+ default: ({ href, children, className }: { href: string; children: React.ReactNode; className?: string }) => (
11
+ <a href={href} className={className}>{children}</a>
12
+ ),
13
+ }));
14
+
15
+ const makeReq = (id: string, overrides: Partial<Requirement> = {}): Requirement => ({
16
+ id,
17
+ title: `Requirement ${id}`,
18
+ status: "draft",
19
+ priority: "medium",
20
+ estimatedComplexity: "small",
21
+ version: "1.0",
22
+ format: { type: "user-story", userStory: { as: "user", iWant: "feature", soThat: "benefit" } },
23
+ updatedAt: "2026-01-01T00:00:00Z",
24
+ createdAt: "2026-01-01T00:00:00Z",
25
+ dependencies: { blockedBy: [], blocks: [], relatedTo: [] },
26
+ files: { description: "description.md", supplementary: [] },
27
+ successCriteria: [],
28
+ versionHistory: [],
29
+ ...overrides,
30
+ });
31
+
32
+ const requirements: Requirement[] = [
33
+ makeReq("req-000001", { status: "approved", priority: "high" }),
34
+ makeReq("req-000002", { status: "implemented", priority: "low" }),
35
+ makeReq("req-000003", { status: "draft", priority: "medium" }),
36
+ ];
37
+
38
+ describe("RequirementTable - ソートヘッダのアクセシビリティ", () => {
39
+ afterEach(() => cleanup());
40
+
41
+ it("ソートボタンがbutton要素として描画される(キーボード操作可能)", () => {
42
+ render(<RequirementTable requirements={requirements} />);
43
+
44
+ const sortButtons = screen.getAllByRole("button");
45
+ expect(sortButtons.length).toBeGreaterThanOrEqual(5);
46
+ });
47
+
48
+ it("アクティブなソート列のthにaria-sort='ascending'が設定される", () => {
49
+ render(<RequirementTable requirements={requirements} />);
50
+
51
+ const idHeader = screen.getByRole("columnheader", { name: /id/i });
52
+ expect(idHeader).toHaveAttribute("aria-sort", "ascending");
53
+ });
54
+
55
+ it("ソートボタンクリックで降順になるとthのaria-sort='descending'になる", () => {
56
+ render(<RequirementTable requirements={requirements} />);
57
+
58
+ const idButton = screen.getByRole("button", { name: /id/i });
59
+ fireEvent.click(idButton);
60
+
61
+ const idHeader = screen.getByRole("columnheader", { name: /id/i });
62
+ expect(idHeader).toHaveAttribute("aria-sort", "descending");
63
+ });
64
+
65
+ it("非アクティブな列のthにaria-sort属性がない", () => {
66
+ render(<RequirementTable requirements={requirements} />);
67
+
68
+ const titleHeader = screen.getByRole("columnheader", { name: /title/i });
69
+ expect(titleHeader).not.toHaveAttribute("aria-sort");
70
+ });
71
+
72
+ it("Enterキーでソートが実行できる", () => {
73
+ render(<RequirementTable requirements={requirements} />);
74
+
75
+ const titleButton = screen.getByRole("button", { name: /title/i });
76
+ fireEvent.keyDown(titleButton, { key: "Enter", code: "Enter" });
77
+ fireEvent.click(titleButton);
78
+
79
+ const titleHeader = screen.getByRole("columnheader", { name: /title/i });
80
+ expect(titleHeader).toHaveAttribute("aria-sort", "ascending");
81
+ });
82
+ });
83
+
84
+ describe("RequirementTable - フォーム要素のアクセシビリティ", () => {
85
+ afterEach(() => cleanup());
86
+
87
+ it("検索inputにaria-labelが設定されている", () => {
88
+ render(<RequirementTable requirements={requirements} />);
89
+
90
+ const searchInput = screen.getByRole("textbox");
91
+ expect(searchInput).toHaveAttribute("aria-label");
92
+ });
93
+
94
+ it("ステータスselectにaria-labelが設定されている", () => {
95
+ render(<RequirementTable requirements={requirements} />);
96
+
97
+ const selects = screen.getAllByRole("combobox");
98
+ const statusSelect = selects.find(s =>
99
+ s.getAttribute("aria-label")?.toLowerCase().includes("status")
100
+ );
101
+ expect(statusSelect).toBeDefined();
102
+ });
103
+
104
+ it("プライオリティselectにaria-labelが設定されている", () => {
105
+ render(<RequirementTable requirements={requirements} />);
106
+
107
+ const selects = screen.getAllByRole("combobox");
108
+ const prioritySelect = selects.find(s =>
109
+ s.getAttribute("aria-label")?.toLowerCase().includes("priority")
110
+ );
111
+ expect(prioritySelect).toBeDefined();
112
+ });
113
+ });
114
+
115
+ describe("RequirementTable - 空状態UI", () => {
116
+ afterEach(() => cleanup());
117
+
118
+ it("フィルタ結果が0件のとき空状態UIが表示される", () => {
119
+ render(<RequirementTable requirements={requirements} />);
120
+
121
+ const searchInput = screen.getByRole("textbox");
122
+ fireEvent.change(searchInput, { target: { value: "nonexistent-xyz-abc" } });
123
+
124
+ expect(screen.getByText("No requirements found")).toBeInTheDocument();
125
+ });
126
+
127
+ it("フィルタ適用中に空状態UIのリセットボタンが表示される", () => {
128
+ render(<RequirementTable requirements={requirements} />);
129
+
130
+ const searchInput = screen.getByRole("textbox");
131
+ fireEvent.change(searchInput, { target: { value: "nonexistent-xyz-abc" } });
132
+
133
+ expect(screen.getByText("Clear all filters")).toBeInTheDocument();
134
+ });
135
+
136
+ it("リセットボタンクリックでフィルタが解除され全件表示に戻る", () => {
137
+ render(<RequirementTable requirements={requirements} />);
138
+
139
+ const searchInput = screen.getByRole("textbox");
140
+ fireEvent.change(searchInput, { target: { value: "nonexistent-xyz-abc" } });
141
+
142
+ const clearButton = screen.getByText("Clear all filters");
143
+ fireEvent.click(clearButton);
144
+
145
+ expect(screen.queryByText("No requirements found")).not.toBeInTheDocument();
146
+ expect(screen.getByText("Requirement req-000001")).toBeInTheDocument();
147
+ });
148
+
149
+ it("フィルタなし・0件の場合はリセットボタンが表示されない", () => {
150
+ render(<RequirementTable requirements={[]} />);
151
+
152
+ expect(screen.queryByText("Clear all filters")).not.toBeInTheDocument();
153
+ });
154
+ });
155
+
156
+ describe("RequirementTable - テーブル可読性", () => {
157
+ afterEach(() => cleanup());
158
+
159
+ it("テーブルヘッダにborder-b-2クラスが適用されている", () => {
160
+ const { container } = render(<RequirementTable requirements={requirements} />);
161
+
162
+ const thead = container.querySelector("thead");
163
+ expect(thead).toHaveClass("border-b-2");
164
+ });
165
+ });
@@ -0,0 +1,189 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, vi, afterEach } from "vitest";
4
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { SpecificationTable } from "../../../components/specification/specification-table";
7
+ import type { Specification } from "@reqord/shared";
8
+
9
+ vi.mock("next/link", () => ({
10
+ default: ({ href, children, className }: { href: string; children: React.ReactNode; className?: string }) => (
11
+ <a href={href} className={className}>{children}</a>
12
+ ),
13
+ }));
14
+
15
+ const makeSpec = (id: string, overrides: Partial<Specification> = {}): Specification => ({
16
+ id,
17
+ requirementId: "req-000001",
18
+ title: `Specification ${id}`,
19
+ status: "draft",
20
+ version: "1.0",
21
+ updatedAt: "2026-01-01T00:00:00Z",
22
+ createdAt: "2026-01-01T00:00:00Z",
23
+ files: { design: "design.md", supplementary: [] },
24
+ versionHistory: [],
25
+ ...overrides,
26
+ });
27
+
28
+ const specifications: Specification[] = [
29
+ makeSpec("spec-000001", { status: "approved", requirementId: "req-000001" }),
30
+ makeSpec("spec-000002", { status: "implemented", requirementId: "req-000002" }),
31
+ makeSpec("spec-000003", { status: "draft", requirementId: "req-000001" }),
32
+ ];
33
+
34
+ const requirementTitleMap: Record<string, string> = {
35
+ "req-000001": "Auth Requirement",
36
+ "req-000002": "Dashboard Requirement",
37
+ };
38
+
39
+ describe("SpecificationTable - ソートヘッダのアクセシビリティ", () => {
40
+ afterEach(() => cleanup());
41
+
42
+ it("ソートボタンがbutton要素として描画される(キーボード操作可能)", () => {
43
+ render(
44
+ <SpecificationTable
45
+ specifications={specifications}
46
+ requirementTitleMap={requirementTitleMap}
47
+ />
48
+ );
49
+
50
+ const sortButtons = screen.getAllByRole("button");
51
+ expect(sortButtons.length).toBeGreaterThanOrEqual(6);
52
+ });
53
+
54
+ it("アクティブなソート列のthにaria-sort='ascending'が設定される", () => {
55
+ render(
56
+ <SpecificationTable
57
+ specifications={specifications}
58
+ requirementTitleMap={requirementTitleMap}
59
+ />
60
+ );
61
+
62
+ const idHeader = screen.getByRole("columnheader", { name: /id/i });
63
+ expect(idHeader).toHaveAttribute("aria-sort", "ascending");
64
+ });
65
+
66
+ it("ソートボタンクリックで降順になるとthのaria-sort='descending'になる", () => {
67
+ render(
68
+ <SpecificationTable
69
+ specifications={specifications}
70
+ requirementTitleMap={requirementTitleMap}
71
+ />
72
+ );
73
+
74
+ const idButton = screen.getByRole("button", { name: /id/i });
75
+ fireEvent.click(idButton);
76
+
77
+ const idHeader = screen.getByRole("columnheader", { name: /id/i });
78
+ expect(idHeader).toHaveAttribute("aria-sort", "descending");
79
+ });
80
+
81
+ it("非アクティブな列のthにaria-sort属性がない", () => {
82
+ render(
83
+ <SpecificationTable
84
+ specifications={specifications}
85
+ requirementTitleMap={requirementTitleMap}
86
+ />
87
+ );
88
+
89
+ const titleHeader = screen.getByRole("columnheader", { name: /title/i });
90
+ expect(titleHeader).not.toHaveAttribute("aria-sort");
91
+ });
92
+ });
93
+
94
+ describe("SpecificationTable - フォーム要素のアクセシビリティ", () => {
95
+ afterEach(() => cleanup());
96
+
97
+ it("検索inputにaria-labelが設定されている", () => {
98
+ render(
99
+ <SpecificationTable
100
+ specifications={specifications}
101
+ requirementTitleMap={requirementTitleMap}
102
+ />
103
+ );
104
+
105
+ const searchInput = screen.getByRole("textbox");
106
+ expect(searchInput).toHaveAttribute("aria-label");
107
+ });
108
+
109
+ it("ステータスselectにaria-labelが設定されている", () => {
110
+ render(
111
+ <SpecificationTable
112
+ specifications={specifications}
113
+ requirementTitleMap={requirementTitleMap}
114
+ />
115
+ );
116
+
117
+ const selects = screen.getAllByRole("combobox");
118
+ const statusSelect = selects.find(s =>
119
+ s.getAttribute("aria-label")?.toLowerCase().includes("status")
120
+ );
121
+ expect(statusSelect).toBeDefined();
122
+ });
123
+ });
124
+
125
+ describe("SpecificationTable - 空状態UI", () => {
126
+ afterEach(() => cleanup());
127
+
128
+ it("フィルタ結果が0件のとき空状態UIが表示される", () => {
129
+ render(
130
+ <SpecificationTable
131
+ specifications={specifications}
132
+ requirementTitleMap={requirementTitleMap}
133
+ />
134
+ );
135
+
136
+ const searchInput = screen.getByRole("textbox");
137
+ fireEvent.change(searchInput, { target: { value: "nonexistent-xyz-abc" } });
138
+
139
+ expect(screen.getByText("No specifications found")).toBeInTheDocument();
140
+ });
141
+
142
+ it("フィルタ適用中に空状態UIのリセットボタンが表示される", () => {
143
+ render(
144
+ <SpecificationTable
145
+ specifications={specifications}
146
+ requirementTitleMap={requirementTitleMap}
147
+ />
148
+ );
149
+
150
+ const searchInput = screen.getByRole("textbox");
151
+ fireEvent.change(searchInput, { target: { value: "nonexistent-xyz-abc" } });
152
+
153
+ expect(screen.getByText("Clear all filters")).toBeInTheDocument();
154
+ });
155
+
156
+ it("リセットボタンクリックでフィルタが解除され全件表示に戻る", () => {
157
+ render(
158
+ <SpecificationTable
159
+ specifications={specifications}
160
+ requirementTitleMap={requirementTitleMap}
161
+ />
162
+ );
163
+
164
+ const searchInput = screen.getByRole("textbox");
165
+ fireEvent.change(searchInput, { target: { value: "nonexistent-xyz-abc" } });
166
+
167
+ const clearButton = screen.getByText("Clear all filters");
168
+ fireEvent.click(clearButton);
169
+
170
+ expect(screen.queryByText("No specifications found")).not.toBeInTheDocument();
171
+ expect(screen.getByText("Specification spec-000001")).toBeInTheDocument();
172
+ });
173
+ });
174
+
175
+ describe("SpecificationTable - テーブル可読性", () => {
176
+ afterEach(() => cleanup());
177
+
178
+ it("テーブルヘッダにborder-b-2クラスが適用されている", () => {
179
+ const { container } = render(
180
+ <SpecificationTable
181
+ specifications={specifications}
182
+ requirementTitleMap={requirementTitleMap}
183
+ />
184
+ );
185
+
186
+ const thead = container.querySelector("thead");
187
+ expect(thead).toHaveClass("border-b-2");
188
+ });
189
+ });
@@ -0,0 +1,98 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { StatusBadge, PriorityBadge, ComplexityBadge } from "../../../components/ui/badge";
7
+
8
+ describe("StatusBadge", () => {
9
+ afterEach(() => cleanup());
10
+
11
+ it("draftステータスに正しいクラスが適用される", () => {
12
+ const { container } = render(<StatusBadge status="draft" />);
13
+ const badge = container.firstChild as HTMLElement;
14
+ expect(badge).toHaveTextContent("Draft");
15
+ expect(badge.className).toContain("bg-gray-100");
16
+ expect(badge.className).toContain("text-gray-600");
17
+ });
18
+
19
+ it("approvedステータスにblue系クラスが適用される", () => {
20
+ const { container } = render(<StatusBadge status="approved" />);
21
+ const badge = container.firstChild as HTMLElement;
22
+ expect(badge).toHaveTextContent("Approved");
23
+ expect(badge.className).toContain("bg-blue-50");
24
+ expect(badge.className).toContain("text-blue-700");
25
+ });
26
+
27
+ it("implementedステータスにemerald系クラスが適用される", () => {
28
+ const { container } = render(<StatusBadge status="implemented" />);
29
+ const badge = container.firstChild as HTMLElement;
30
+ expect(badge).toHaveTextContent("Implemented");
31
+ expect(badge.className).toContain("bg-emerald-50");
32
+ expect(badge.className).toContain("text-emerald-700");
33
+ });
34
+
35
+ it("deprecatedステータスにred系クラスが適用される", () => {
36
+ const { container } = render(<StatusBadge status="deprecated" />);
37
+ const badge = container.firstChild as HTMLElement;
38
+ expect(badge).toHaveTextContent("Deprecated");
39
+ expect(badge.className).toContain("bg-red-50");
40
+ expect(badge.className).toContain("text-red-700");
41
+ });
42
+
43
+ it("ring-1 ring-insetクラスで輪郭が明確化されている", () => {
44
+ const { container } = render(<StatusBadge status="draft" />);
45
+ const badge = container.firstChild as HTMLElement;
46
+ expect(badge.className).toContain("ring-1");
47
+ expect(badge.className).toContain("ring-inset");
48
+ });
49
+ });
50
+
51
+ describe("PriorityBadge", () => {
52
+ afterEach(() => cleanup());
53
+
54
+ it("highプライオリティに正しいクラスが適用される", () => {
55
+ const { container } = render(<PriorityBadge priority="high" />);
56
+ const badge = container.firstChild as HTMLElement;
57
+ expect(badge).toHaveTextContent("High");
58
+ expect(badge.className).toContain("bg-red-100");
59
+ });
60
+
61
+ it("mediumプライオリティに正しいクラスが適用される", () => {
62
+ const { container } = render(<PriorityBadge priority="medium" />);
63
+ const badge = container.firstChild as HTMLElement;
64
+ expect(badge).toHaveTextContent("Medium");
65
+ expect(badge.className).toContain("bg-orange-100");
66
+ });
67
+
68
+ it("lowプライオリティに正しいクラスが適用される", () => {
69
+ const { container } = render(<PriorityBadge priority="low" />);
70
+ const badge = container.firstChild as HTMLElement;
71
+ expect(badge).toHaveTextContent("Low");
72
+ expect(badge.className).toContain("bg-blue-100");
73
+ });
74
+ });
75
+
76
+ describe("ComplexityBadge", () => {
77
+ afterEach(() => cleanup());
78
+
79
+ it("smallに正しいラベルが表示される", () => {
80
+ render(<ComplexityBadge complexity="small" />);
81
+ expect(screen.getByText("S")).toBeInTheDocument();
82
+ });
83
+
84
+ it("mediumに正しいラベルが表示される", () => {
85
+ render(<ComplexityBadge complexity="medium" />);
86
+ expect(screen.getByText("M")).toBeInTheDocument();
87
+ });
88
+
89
+ it("largeに正しいラベルが表示される", () => {
90
+ render(<ComplexityBadge complexity="large" />);
91
+ expect(screen.getByText("L")).toBeInTheDocument();
92
+ });
93
+
94
+ it("xlargeに正しいラベルが表示される", () => {
95
+ render(<ComplexityBadge complexity="xlarge" />);
96
+ expect(screen.getByText("XL")).toBeInTheDocument();
97
+ });
98
+ });
@@ -0,0 +1,98 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { Button } from "../../../components/ui/button";
7
+
8
+ describe("Button", () => {
9
+ afterEach(() => {
10
+ cleanup();
11
+ });
12
+
13
+ describe("variant", () => {
14
+ it("renders primary variant with warm-800 background by default", () => {
15
+ render(<Button>Click me</Button>);
16
+
17
+ const button = screen.getByRole("button", { name: "Click me" });
18
+ expect(button).toHaveClass("bg-warm-800");
19
+ expect(button).toHaveClass("text-white");
20
+ });
21
+
22
+ it("renders secondary variant with gray outline", () => {
23
+ render(<Button variant="secondary">Secondary</Button>);
24
+
25
+ const button = screen.getByRole("button", { name: "Secondary" });
26
+ expect(button).toHaveClass("border");
27
+ expect(button).toHaveClass("border-warm-300");
28
+ expect(button).toHaveClass("bg-white");
29
+ expect(button).toHaveClass("text-warm-700");
30
+ });
31
+
32
+ it("renders danger variant with red background", () => {
33
+ render(<Button variant="danger">Delete</Button>);
34
+
35
+ const button = screen.getByRole("button", { name: "Delete" });
36
+ expect(button).toHaveClass("bg-accent");
37
+ expect(button).toHaveClass("text-white");
38
+ });
39
+
40
+ it("renders ghost variant with transparent background", () => {
41
+ render(<Button variant="ghost">Ghost</Button>);
42
+
43
+ const button = screen.getByRole("button", { name: "Ghost" });
44
+ expect(button).toHaveClass("text-warm-700");
45
+ expect(button).not.toHaveClass("bg-warm-800");
46
+ expect(button).not.toHaveClass("bg-white");
47
+ });
48
+ });
49
+
50
+ describe("size", () => {
51
+ it("renders md size by default", () => {
52
+ render(<Button>Medium</Button>);
53
+
54
+ const button = screen.getByRole("button", { name: "Medium" });
55
+ expect(button).toHaveClass("px-4");
56
+ expect(button).toHaveClass("py-2");
57
+ expect(button).toHaveClass("text-sm");
58
+ });
59
+
60
+ it("renders sm size with smaller padding", () => {
61
+ render(<Button size="sm">Small</Button>);
62
+
63
+ const button = screen.getByRole("button", { name: "Small" });
64
+ expect(button).toHaveClass("px-3");
65
+ expect(button).toHaveClass("py-1.5");
66
+ expect(button).toHaveClass("text-xs");
67
+ });
68
+
69
+ it("renders lg size with larger padding", () => {
70
+ render(<Button size="lg">Large</Button>);
71
+
72
+ const button = screen.getByRole("button", { name: "Large" });
73
+ expect(button).toHaveClass("px-6");
74
+ expect(button).toHaveClass("py-3");
75
+ expect(button).toHaveClass("text-base");
76
+ });
77
+ });
78
+
79
+ it("passes additional className prop", () => {
80
+ render(<Button className="custom-class">Click</Button>);
81
+
82
+ const button = screen.getByRole("button", { name: "Click" });
83
+ expect(button).toHaveClass("custom-class");
84
+ });
85
+
86
+ it("passes native button props like disabled", () => {
87
+ render(<Button disabled>Disabled</Button>);
88
+
89
+ const button = screen.getByRole("button", { name: "Disabled" });
90
+ expect(button).toBeDisabled();
91
+ });
92
+
93
+ it("renders children correctly", () => {
94
+ render(<Button>Submit</Button>);
95
+
96
+ expect(screen.getByText("Submit")).toBeInTheDocument();
97
+ });
98
+ });
@@ -0,0 +1,58 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { Card } from "../../../components/ui/card";
7
+
8
+ describe("Card", () => {
9
+ afterEach(() => {
10
+ cleanup();
11
+ });
12
+
13
+ it("renders children correctly", () => {
14
+ render(<Card>Card content</Card>);
15
+
16
+ expect(screen.getByText("Card content")).toBeInTheDocument();
17
+ });
18
+
19
+ it("renders default variant with standard styles", () => {
20
+ render(<Card data-testid="card">Content</Card>);
21
+
22
+ const card = screen.getByTestId("card");
23
+ expect(card).toHaveClass("rounded-xl");
24
+ expect(card).toHaveClass("border");
25
+ expect(card).toHaveClass("border-warm-200");
26
+ expect(card).toHaveClass("bg-white");
27
+ expect(card).toHaveClass("p-6");
28
+ expect(card).toHaveClass("shadow-sm");
29
+ });
30
+
31
+ it("renders hero variant with gradient background", () => {
32
+ render(<Card variant="hero" data-testid="hero-card">Hero content</Card>);
33
+
34
+ const card = screen.getByTestId("hero-card");
35
+ expect(card).toHaveClass("bg-warm-100");
36
+ });
37
+
38
+ it("hero variant does not apply bg-white", () => {
39
+ render(<Card variant="hero" data-testid="hero-card">Hero content</Card>);
40
+
41
+ const card = screen.getByTestId("hero-card");
42
+ expect(card).not.toHaveClass("bg-white");
43
+ });
44
+
45
+ it("passes additional className prop", () => {
46
+ render(<Card className="custom-class" data-testid="card">Content</Card>);
47
+
48
+ const card = screen.getByTestId("card");
49
+ expect(card).toHaveClass("custom-class");
50
+ });
51
+
52
+ it("renders as a div element", () => {
53
+ render(<Card data-testid="card">Content</Card>);
54
+
55
+ const card = screen.getByTestId("card");
56
+ expect(card.tagName).toBe("DIV");
57
+ });
58
+ });