@fpkit/acss 0.5.12 → 0.5.13

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 (264) hide show
  1. package/README.md +89 -0
  2. package/libs/{chunk-DV56L5YX.cjs → chunk-2LTJ7HHX.cjs} +4 -4
  3. package/libs/{chunk-EQ67LF46.js → chunk-2Y7W75TT.js} +3 -3
  4. package/libs/{chunk-KKLTUJFB.cjs → chunk-3MKLDCKQ.cjs} +5 -5
  5. package/libs/chunk-3MKLDCKQ.cjs.map +1 -0
  6. package/libs/{chunk-X3EVB7VS.cjs → chunk-5S4ORA4C.cjs} +3 -3
  7. package/libs/{chunk-O6QZBB6G.js → chunk-772NRB75.js} +5 -5
  8. package/libs/chunk-772NRB75.js.map +1 -0
  9. package/libs/{chunk-6BVXFW7U.cjs → chunk-AHDJGCG5.cjs} +3 -3
  10. package/libs/{chunk-E3XP6BEX.cjs → chunk-B7F5FS6D.cjs} +3 -3
  11. package/libs/chunk-D4YLRWAO.cjs +18 -0
  12. package/libs/chunk-D4YLRWAO.cjs.map +1 -0
  13. package/libs/chunk-ETFLFC2S.js +10 -0
  14. package/libs/chunk-ETFLFC2S.js.map +1 -0
  15. package/libs/chunk-GZ4QFPRY.js +9 -0
  16. package/libs/chunk-GZ4QFPRY.js.map +1 -0
  17. package/libs/{chunk-LHVJKDMA.cjs → chunk-J32EZPYD.cjs} +3 -3
  18. package/libs/chunk-JJ43O4Y5.js +8 -0
  19. package/libs/chunk-JJ43O4Y5.js.map +1 -0
  20. package/libs/chunk-KUKIVRC2.js +7 -0
  21. package/libs/chunk-KUKIVRC2.js.map +1 -0
  22. package/libs/chunk-L75OQKEI.cjs +13 -0
  23. package/libs/chunk-L75OQKEI.cjs.map +1 -0
  24. package/libs/{chunk-LL7HTLMS.cjs → chunk-M5RRNTVX.cjs} +3 -3
  25. package/libs/{chunk-LIQJ7ZZR.js → chunk-NGTJDDFO.js} +2 -2
  26. package/libs/chunk-OK5QEIMD.cjs +17 -0
  27. package/libs/chunk-OK5QEIMD.cjs.map +1 -0
  28. package/libs/chunk-P2DC76ZZ.cjs +18 -0
  29. package/libs/chunk-P2DC76ZZ.cjs.map +1 -0
  30. package/libs/chunk-PQ2K3BM6.cjs +17 -0
  31. package/libs/chunk-PQ2K3BM6.cjs.map +1 -0
  32. package/libs/{chunk-QCMV4VQZ.js → chunk-QLZWHAMK.js} +2 -2
  33. package/libs/{chunk-BIP2NY53.js → chunk-RIVUMPOG.js} +2 -2
  34. package/libs/{chunk-ICCKQ2GC.cjs → chunk-ROZI23GS.cjs} +4 -4
  35. package/libs/{chunk-NHYXGV3L.js → chunk-SMYRLO3E.js} +2 -2
  36. package/libs/{chunk-5ZM4XL44.js → chunk-TYRCEX2L.js} +2 -2
  37. package/libs/chunk-VUH3FXGJ.js +11 -0
  38. package/libs/chunk-VUH3FXGJ.js.map +1 -0
  39. package/libs/{chunk-PPOOBUOS.js → chunk-XBA562WW.js} +2 -2
  40. package/libs/{chunk-QVV34QEH.cjs → chunk-XTQKWY7W.cjs} +3 -3
  41. package/libs/{chunk-YWOYVRFT.js → chunk-ZANSFMTD.js} +3 -3
  42. package/libs/components/alert/alert.css +1 -1
  43. package/libs/components/alert/alert.css.map +1 -1
  44. package/libs/components/alert/alert.min.css +2 -2
  45. package/libs/components/badge/badge.css +1 -1
  46. package/libs/components/badge/badge.css.map +1 -1
  47. package/libs/components/badge/badge.min.css +2 -2
  48. package/libs/components/breadcrumbs/breadcrumb.cjs +9 -5
  49. package/libs/components/breadcrumbs/breadcrumb.d.cts +271 -32
  50. package/libs/components/breadcrumbs/breadcrumb.d.ts +271 -32
  51. package/libs/components/breadcrumbs/breadcrumb.js +3 -3
  52. package/libs/components/button.cjs +4 -4
  53. package/libs/components/button.d.cts +2 -2
  54. package/libs/components/button.d.ts +2 -2
  55. package/libs/components/button.js +2 -2
  56. package/libs/components/buttons/button.css +1 -1
  57. package/libs/components/buttons/button.css.map +1 -1
  58. package/libs/components/buttons/button.min.css +2 -2
  59. package/libs/components/card.cjs +7 -7
  60. package/libs/components/card.d.cts +277 -33
  61. package/libs/components/card.d.ts +277 -33
  62. package/libs/components/card.js +2 -2
  63. package/libs/components/cards/card.css +1 -1
  64. package/libs/components/cards/card.css.map +1 -1
  65. package/libs/components/cards/card.min.css +2 -2
  66. package/libs/components/details/details.css +1 -1
  67. package/libs/components/details/details.css.map +1 -1
  68. package/libs/components/details/details.min.css +2 -2
  69. package/libs/components/dialog/dialog.cjs +7 -7
  70. package/libs/components/dialog/dialog.css +1 -1
  71. package/libs/components/dialog/dialog.css.map +1 -1
  72. package/libs/components/dialog/dialog.d.cts +88 -34
  73. package/libs/components/dialog/dialog.d.ts +88 -34
  74. package/libs/components/dialog/dialog.js +5 -5
  75. package/libs/components/dialog/dialog.min.css +2 -2
  76. package/libs/components/form/fields.cjs +4 -4
  77. package/libs/components/form/fields.d.cts +2 -2
  78. package/libs/components/form/fields.d.ts +2 -2
  79. package/libs/components/form/fields.js +2 -2
  80. package/libs/components/form/textarea.cjs +4 -4
  81. package/libs/components/form/textarea.d.cts +2 -2
  82. package/libs/components/form/textarea.d.ts +2 -2
  83. package/libs/components/form/textarea.js +2 -2
  84. package/libs/components/heading/heading.cjs +3 -3
  85. package/libs/components/heading/heading.d.cts +3 -14
  86. package/libs/components/heading/heading.d.ts +3 -14
  87. package/libs/components/heading/heading.js +2 -2
  88. package/libs/components/icons/icon.cjs +4 -4
  89. package/libs/components/icons/icon.d.cts +148 -4
  90. package/libs/components/icons/icon.d.ts +148 -4
  91. package/libs/components/icons/icon.js +2 -2
  92. package/libs/components/images/img.css +1 -1
  93. package/libs/components/images/img.css.map +1 -1
  94. package/libs/components/images/img.min.css +2 -2
  95. package/libs/components/link/link.cjs +4 -4
  96. package/libs/components/link/link.d.cts +2 -2
  97. package/libs/components/link/link.d.ts +2 -2
  98. package/libs/components/link/link.js +2 -2
  99. package/libs/components/list/list.cjs +5 -5
  100. package/libs/components/list/list.d.cts +3 -3
  101. package/libs/components/list/list.d.ts +3 -3
  102. package/libs/components/list/list.js +2 -2
  103. package/libs/components/modal.cjs +4 -4
  104. package/libs/components/modal.js +3 -3
  105. package/libs/components/nav/nav.cjs +7 -7
  106. package/libs/components/nav/nav.d.cts +2 -2
  107. package/libs/components/nav/nav.d.ts +2 -2
  108. package/libs/components/nav/nav.js +3 -3
  109. package/libs/components/text/text.cjs +5 -5
  110. package/libs/components/text/text.d.cts +2 -2
  111. package/libs/components/text/text.d.ts +2 -2
  112. package/libs/components/text/text.js +2 -2
  113. package/libs/heading-3648c538.d.ts +250 -0
  114. package/libs/hooks.cjs +7 -0
  115. package/libs/hooks.d.cts +5 -0
  116. package/libs/hooks.d.ts +5 -0
  117. package/libs/hooks.js +3 -0
  118. package/libs/icons.cjs +3 -3
  119. package/libs/icons.d.cts +1 -1
  120. package/libs/icons.d.ts +1 -1
  121. package/libs/icons.js +2 -2
  122. package/libs/index.cjs +112 -91
  123. package/libs/index.cjs.map +1 -1
  124. package/libs/index.css +1 -1
  125. package/libs/index.css.map +1 -1
  126. package/libs/index.d.cts +515 -31
  127. package/libs/index.d.ts +515 -31
  128. package/libs/index.js +31 -19
  129. package/libs/index.js.map +1 -1
  130. package/libs/ui-645f95b5.d.ts +285 -0
  131. package/package.json +2 -83
  132. package/src/components/README-UI.mdx +416 -0
  133. package/src/components/alert/ACCESSIBILITY.md +319 -0
  134. package/src/components/alert/README.mdx +475 -19
  135. package/src/components/alert/alert.scss +113 -6
  136. package/src/components/alert/alert.stories.tsx +372 -0
  137. package/src/components/alert/alert.test.tsx +762 -0
  138. package/src/components/alert/alert.tsx +331 -66
  139. package/src/components/alert/views/alert-actions.tsx +13 -0
  140. package/src/components/alert/views/alert-content.tsx +17 -0
  141. package/src/components/alert/views/alert-icon.tsx +53 -0
  142. package/src/components/alert/views/alert-screen-reader-text.tsx +30 -0
  143. package/src/components/alert/views/alert-title.tsx +23 -0
  144. package/src/components/alert/views/alert-view.tsx +158 -0
  145. package/src/components/alert/views/index.ts +12 -0
  146. package/src/components/badge/badge.mdx +186 -49
  147. package/src/components/badge/badge.scss +20 -2
  148. package/src/components/badge/badge.stories.tsx +160 -14
  149. package/src/components/badge/badge.test.tsx +179 -0
  150. package/src/components/badge/badge.tsx +97 -4
  151. package/src/components/breadcrumbs/README.mdx +364 -45
  152. package/src/components/breadcrumbs/__snapshots__/breadcrumb.test.tsx.snap +152 -0
  153. package/src/components/breadcrumbs/breadcrumb.stories.tsx +7 -3
  154. package/src/components/breadcrumbs/breadcrumb.test.tsx +490 -0
  155. package/src/components/breadcrumbs/breadcrumb.tsx +427 -170
  156. package/src/components/buttons/button.scss +34 -31
  157. package/src/components/buttons/button.stories.tsx +35 -0
  158. package/src/components/cards/README.mdx +657 -0
  159. package/src/components/cards/card.scss +22 -0
  160. package/src/components/cards/card.stories.tsx +167 -5
  161. package/src/components/cards/card.test.tsx +360 -20
  162. package/src/components/cards/card.tsx +200 -79
  163. package/src/components/cards/card.types.ts +135 -0
  164. package/src/components/cards/card.utils.ts +79 -0
  165. package/src/components/details/ACCESSIBILITY-REVIEW-LIVE.md +1050 -0
  166. package/src/components/details/ACCESSIBILITY-REVIEW.md +502 -0
  167. package/src/components/details/README.mdx +437 -69
  168. package/src/components/details/details.scss +16 -7
  169. package/src/components/details/details.test.tsx +385 -0
  170. package/src/components/details/details.tsx +101 -69
  171. package/src/components/details/details.types.ts +76 -0
  172. package/src/components/dialog/README.mdx +513 -110
  173. package/src/components/dialog/dialog-modal.tsx +79 -56
  174. package/src/components/dialog/dialog.scss +53 -3
  175. package/src/components/dialog/dialog.stories.tsx +10 -7
  176. package/src/components/dialog/dialog.test.tsx +450 -0
  177. package/src/components/dialog/dialog.tsx +69 -59
  178. package/src/components/dialog/dialog.types.ts +133 -0
  179. package/src/components/dialog/views/dialog-footer.tsx +54 -11
  180. package/src/components/dialog/views/dialog-header.tsx +20 -15
  181. package/src/components/heading/heading.stories.tsx +44 -4
  182. package/src/components/heading/heading.tsx +89 -23
  183. package/src/components/icons/README.mdx +332 -0
  184. package/src/components/icons/icon.stories.tsx +74 -1
  185. package/src/components/icons/icon.tsx +89 -1
  186. package/src/components/icons/types.ts +47 -0
  187. package/src/components/images/README.mdx +340 -24
  188. package/src/components/images/img.scss +19 -3
  189. package/src/components/images/img.stories.tsx +424 -15
  190. package/src/components/images/img.test.tsx +354 -25
  191. package/src/components/images/img.tsx +186 -63
  192. package/src/components/images/img.types.ts +211 -0
  193. package/src/components/title/MIGRATION.md +199 -0
  194. package/src/components/title/README.md +326 -0
  195. package/src/components/title/README.mdx +452 -0
  196. package/src/components/title/title.stories.tsx +393 -0
  197. package/src/components/title/title.test.tsx +251 -0
  198. package/src/components/title/title.tsx +219 -0
  199. package/src/components/ui.stories.tsx +894 -0
  200. package/src/components/ui.test.tsx +559 -0
  201. package/src/components/ui.tsx +266 -15
  202. package/src/components/word-count/README.md +240 -0
  203. package/src/hooks.ts +1 -0
  204. package/src/index.ts +10 -2
  205. package/src/sass/_properties.scss +1 -0
  206. package/src/styles/alert/alert.css +94 -4
  207. package/src/styles/alert/alert.css.map +1 -1
  208. package/src/styles/badge/badge.css +20 -2
  209. package/src/styles/badge/badge.css.map +1 -1
  210. package/src/styles/buttons/button.css +31 -31
  211. package/src/styles/buttons/button.css.map +1 -1
  212. package/src/styles/cards/card.css +16 -0
  213. package/src/styles/cards/card.css.map +1 -1
  214. package/src/styles/details/details.css +19 -8
  215. package/src/styles/details/details.css.map +1 -1
  216. package/src/styles/dialog/dialog.css +43 -2
  217. package/src/styles/dialog/dialog.css.map +1 -1
  218. package/src/styles/images/img.css +15 -3
  219. package/src/styles/images/img.css.map +1 -1
  220. package/src/styles/index.css +240 -51
  221. package/src/styles/index.css.map +1 -1
  222. package/src/test/setup.d.ts +9 -0
  223. package/src/test/setup.ts +53 -1
  224. package/libs/chunk-6TE5QEVE.cjs +0 -13
  225. package/libs/chunk-6TE5QEVE.cjs.map +0 -1
  226. package/libs/chunk-7K76RW2A.cjs +0 -18
  227. package/libs/chunk-7K76RW2A.cjs.map +0 -1
  228. package/libs/chunk-BSPKFLO4.js +0 -8
  229. package/libs/chunk-BSPKFLO4.js.map +0 -1
  230. package/libs/chunk-BV5CLH44.cjs +0 -18
  231. package/libs/chunk-BV5CLH44.cjs.map +0 -1
  232. package/libs/chunk-DKGJHKGW.js +0 -9
  233. package/libs/chunk-DKGJHKGW.js.map +0 -1
  234. package/libs/chunk-ECLD37WN.cjs +0 -16
  235. package/libs/chunk-ECLD37WN.cjs.map +0 -1
  236. package/libs/chunk-HYBZBN4G.js +0 -8
  237. package/libs/chunk-HYBZBN4G.js.map +0 -1
  238. package/libs/chunk-KKLTUJFB.cjs.map +0 -1
  239. package/libs/chunk-M5QL5TAE.cjs +0 -14
  240. package/libs/chunk-M5QL5TAE.cjs.map +0 -1
  241. package/libs/chunk-NE6YXTMC.js +0 -7
  242. package/libs/chunk-NE6YXTMC.js.map +0 -1
  243. package/libs/chunk-O6QZBB6G.js.map +0 -1
  244. package/libs/chunk-SXVZSWX6.js +0 -11
  245. package/libs/chunk-SXVZSWX6.js.map +0 -1
  246. package/libs/ui-9a6f9f8d.d.ts +0 -24
  247. package/src/components/cards/README.md +0 -80
  248. package/src/components/dialog/hooks/useClickOutside.ts +0 -33
  249. /package/libs/{chunk-DV56L5YX.cjs.map → chunk-2LTJ7HHX.cjs.map} +0 -0
  250. /package/libs/{chunk-EQ67LF46.js.map → chunk-2Y7W75TT.js.map} +0 -0
  251. /package/libs/{chunk-X3EVB7VS.cjs.map → chunk-5S4ORA4C.cjs.map} +0 -0
  252. /package/libs/{chunk-6BVXFW7U.cjs.map → chunk-AHDJGCG5.cjs.map} +0 -0
  253. /package/libs/{chunk-E3XP6BEX.cjs.map → chunk-B7F5FS6D.cjs.map} +0 -0
  254. /package/libs/{chunk-LHVJKDMA.cjs.map → chunk-J32EZPYD.cjs.map} +0 -0
  255. /package/libs/{chunk-LL7HTLMS.cjs.map → chunk-M5RRNTVX.cjs.map} +0 -0
  256. /package/libs/{chunk-LIQJ7ZZR.js.map → chunk-NGTJDDFO.js.map} +0 -0
  257. /package/libs/{chunk-QCMV4VQZ.js.map → chunk-QLZWHAMK.js.map} +0 -0
  258. /package/libs/{chunk-BIP2NY53.js.map → chunk-RIVUMPOG.js.map} +0 -0
  259. /package/libs/{chunk-ICCKQ2GC.cjs.map → chunk-ROZI23GS.cjs.map} +0 -0
  260. /package/libs/{chunk-NHYXGV3L.js.map → chunk-SMYRLO3E.js.map} +0 -0
  261. /package/libs/{chunk-5ZM4XL44.js.map → chunk-TYRCEX2L.js.map} +0 -0
  262. /package/libs/{chunk-PPOOBUOS.js.map → chunk-XBA562WW.js.map} +0 -0
  263. /package/libs/{chunk-QVV34QEH.cjs.map → chunk-XTQKWY7W.cjs.map} +0 -0
  264. /package/libs/{chunk-YWOYVRFT.js.map → chunk-ZANSFMTD.js.map} +0 -0
@@ -0,0 +1,490 @@
1
+ import React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, it, expect, vi } from "vitest";
4
+ import { Breadcrumb, CustomRoute } from "./breadcrumb";
5
+
6
+ describe("Breadcrumb", () => {
7
+ // ============================================================================
8
+ // CORE FUNCTIONALITY TESTS
9
+ // ============================================================================
10
+
11
+ describe("Core Functionality", () => {
12
+ it("renders a navigation element with breadcrumb list", () => {
13
+ render(<Breadcrumb currentRoute="/products/shirts" />);
14
+ const nav = screen.getByRole("navigation");
15
+ expect(nav).toBeInTheDocument();
16
+
17
+ const list = nav.querySelector("ol");
18
+ expect(list).toBeInTheDocument();
19
+ });
20
+
21
+ it("returns null when currentRoute is undefined", () => {
22
+ const { container } = render(<Breadcrumb currentRoute={undefined} />);
23
+ expect(container.firstChild).toBeNull();
24
+ });
25
+
26
+ it("returns null when currentRoute is empty string", () => {
27
+ const { container } = render(<Breadcrumb currentRoute="" />);
28
+ expect(container.firstChild).toBeNull();
29
+ });
30
+
31
+ it("renders home/start route link correctly", () => {
32
+ render(<Breadcrumb currentRoute="/products" />);
33
+ const homeLink = screen.getByRole("link", { name: "Home" });
34
+ expect(homeLink).toBeInTheDocument();
35
+ expect(homeLink).toHaveAttribute("href", "/");
36
+ });
37
+
38
+ it("parses path segments correctly from currentRoute", () => {
39
+ render(<Breadcrumb currentRoute="/products/shirts/blue-shirt" />);
40
+
41
+ expect(screen.getByText("products")).toBeInTheDocument();
42
+ expect(screen.getByText("shirts")).toBeInTheDocument();
43
+ expect(screen.getByText("blue-shirt")).toBeInTheDocument();
44
+ });
45
+
46
+ it("handles paths with leading slashes", () => {
47
+ render(<Breadcrumb currentRoute="/products" />);
48
+ expect(screen.getByText("products")).toBeInTheDocument();
49
+ });
50
+
51
+ it("handles paths with trailing slashes", () => {
52
+ render(<Breadcrumb currentRoute="/products/shirts/" />);
53
+ expect(screen.getByText("products")).toBeInTheDocument();
54
+ expect(screen.getByText("shirts")).toBeInTheDocument();
55
+ });
56
+
57
+ it("handles encoded URI components correctly", () => {
58
+ render(<Breadcrumb currentRoute="/products/learning%20in%20public" />);
59
+
60
+ expect(screen.getByText("products")).toBeInTheDocument();
61
+
62
+ // Text is truncated, so use aria-label to verify full decoded text
63
+ const element = screen.getByText(/learning in pub/);
64
+ expect(element).toHaveAttribute("aria-label", "learning in public");
65
+ });
66
+ });
67
+
68
+ // ============================================================================
69
+ // CUSTOM ROUTES TESTS
70
+ // ============================================================================
71
+
72
+ describe("Custom Routes", () => {
73
+ const customRoutes: CustomRoute[] = [
74
+ {
75
+ path: "products",
76
+ name: "All Products",
77
+ url: "/products",
78
+ },
79
+ {
80
+ path: "shirts",
81
+ name: "Shirts & Tops",
82
+ url: "/products/shirts",
83
+ },
84
+ ];
85
+
86
+ it("maps path segments to custom route names", () => {
87
+ render(
88
+ <Breadcrumb
89
+ currentRoute="/products/shirts"
90
+ routes={customRoutes}
91
+ />
92
+ );
93
+
94
+ expect(screen.getByText("All Products")).toBeInTheDocument();
95
+ expect(screen.getByText("Shirts & Tops")).toBeInTheDocument();
96
+ });
97
+
98
+ it("uses custom URLs when provided", () => {
99
+ render(
100
+ <Breadcrumb
101
+ currentRoute="/products/shirts"
102
+ routes={customRoutes}
103
+ />
104
+ );
105
+
106
+ const productsLink = screen.getByRole("link", { name: "All Products" });
107
+ expect(productsLink).toHaveAttribute("href", "/products");
108
+
109
+ const shirtsSpan = screen.getByText("Shirts & Tops");
110
+ expect(shirtsSpan).toBeInTheDocument();
111
+ });
112
+
113
+ it("falls back to path segment when no custom route found", () => {
114
+ render(
115
+ <Breadcrumb
116
+ currentRoute="/products/pants"
117
+ routes={customRoutes}
118
+ />
119
+ );
120
+
121
+ expect(screen.getByText("All Products")).toBeInTheDocument();
122
+ expect(screen.getByText("pants")).toBeInTheDocument(); // No custom route
123
+ });
124
+
125
+ it("handles partial custom route mappings", () => {
126
+ const partialRoutes: CustomRoute[] = [
127
+ {
128
+ path: "products",
129
+ name: "Products",
130
+ url: "/products",
131
+ },
132
+ ];
133
+
134
+ render(
135
+ <Breadcrumb
136
+ currentRoute="/products/shirts/item-123"
137
+ routes={partialRoutes}
138
+ />
139
+ );
140
+
141
+ expect(screen.getByText("Products")).toBeInTheDocument();
142
+ expect(screen.getByText("shirts")).toBeInTheDocument();
143
+ expect(screen.getByText("item-123")).toBeInTheDocument();
144
+ });
145
+ });
146
+
147
+ // ============================================================================
148
+ // ACCESSIBILITY TESTS
149
+ // ============================================================================
150
+
151
+ describe("Accessibility", () => {
152
+ it("renders semantic nav element with proper aria-label", () => {
153
+ render(
154
+ <Breadcrumb
155
+ currentRoute="/products"
156
+ ariaLabel="Page navigation"
157
+ />
158
+ );
159
+
160
+ const nav = screen.getByRole("navigation", { name: "Page navigation" });
161
+ expect(nav).toBeInTheDocument();
162
+ });
163
+
164
+ it("uses default aria-label of 'Breadcrumb' when not provided", () => {
165
+ render(<Breadcrumb currentRoute="/products" />);
166
+
167
+ const nav = screen.getByRole("navigation", { name: "Breadcrumb" });
168
+ expect(nav).toBeInTheDocument();
169
+ });
170
+
171
+ it("uses ordered list (ol) for breadcrumb list", () => {
172
+ render(<Breadcrumb currentRoute="/products/shirts" />);
173
+
174
+ const nav = screen.getByRole("navigation");
175
+ const list = nav.querySelector("ol");
176
+ expect(list).toBeInTheDocument();
177
+ });
178
+
179
+ it("marks last segment with aria-current='page'", () => {
180
+ render(<Breadcrumb currentRoute="/products/shirts" />);
181
+
182
+ const currentPage = screen.getByText("shirts");
183
+ expect(currentPage).toHaveAttribute("aria-current", "page");
184
+ });
185
+
186
+ it("hides spacers from screen readers with aria-hidden", () => {
187
+ render(<Breadcrumb currentRoute="/products/shirts" />);
188
+
189
+ const nav = screen.getByRole("navigation");
190
+ const spacers = nav.querySelectorAll('[aria-hidden="true"]');
191
+
192
+ // Should have at least 2 spacers (one for each segment)
193
+ expect(spacers.length).toBeGreaterThan(0);
194
+ });
195
+
196
+ it("does not render anchor tags with href='#'", () => {
197
+ render(<Breadcrumb currentRoute="/products/shirts" />);
198
+
199
+ const nav = screen.getByRole("navigation");
200
+ const invalidLinks = nav.querySelectorAll('a[href="#"]');
201
+
202
+ expect(invalidLinks.length).toBe(0);
203
+ });
204
+
205
+ it("provides full text in aria-label when truncated", () => {
206
+ const longName = "this-is-a-very-long-product-name";
207
+ render(
208
+ <Breadcrumb
209
+ currentRoute={`/products/${longName}`}
210
+ truncateLength={10}
211
+ />
212
+ );
213
+
214
+ const truncatedElement = screen.getByText(/this-is-a-/);
215
+ expect(truncatedElement).toHaveAttribute("aria-label", longName);
216
+ });
217
+
218
+ it("does not add aria-label when text is not truncated", () => {
219
+ render(
220
+ <Breadcrumb
221
+ currentRoute="/products/shirt"
222
+ truncateLength={20}
223
+ />
224
+ );
225
+
226
+ const element = screen.getByText("shirt");
227
+ expect(element).not.toHaveAttribute("aria-label");
228
+ });
229
+ });
230
+
231
+ // ============================================================================
232
+ // TRUNCATION TESTS
233
+ // ============================================================================
234
+
235
+ describe("Text Truncation", () => {
236
+ it("truncates text beyond default truncateLength (15)", () => {
237
+ const longName = "verylongproductname";
238
+ render(<Breadcrumb currentRoute={`/products/${longName}`} />);
239
+
240
+ // Should be truncated to 15 chars + "..."
241
+ expect(screen.getByText("verylongproduct...")).toBeInTheDocument();
242
+ });
243
+
244
+ it("respects custom truncateLength prop", () => {
245
+ const longName = "verylongname";
246
+ render(
247
+ <Breadcrumb
248
+ currentRoute={`/products/${longName}`}
249
+ truncateLength={5}
250
+ />
251
+ );
252
+
253
+ expect(screen.getByText("veryl...")).toBeInTheDocument();
254
+ });
255
+
256
+ it("does not truncate text shorter than truncateLength", () => {
257
+ render(
258
+ <Breadcrumb
259
+ currentRoute="/products/shirt"
260
+ truncateLength={15}
261
+ />
262
+ );
263
+
264
+ expect(screen.getByText("shirt")).toBeInTheDocument();
265
+ });
266
+
267
+ it("truncates both intermediate and current page segments", () => {
268
+ render(
269
+ <Breadcrumb
270
+ currentRoute="/verylongfirstsegment/verylongsecondsegment"
271
+ truncateLength={10}
272
+ />
273
+ );
274
+
275
+ expect(screen.getByText("verylongfi...")).toBeInTheDocument();
276
+ expect(screen.getByText("verylongse...")).toBeInTheDocument();
277
+ });
278
+ });
279
+
280
+ // ============================================================================
281
+ // EDGE CASES TESTS
282
+ // ============================================================================
283
+
284
+ describe("Edge Cases", () => {
285
+ it("skips last segment if length <= 3 characters", () => {
286
+ render(<Breadcrumb currentRoute="/products/shirts/abc" />);
287
+
288
+ // "abc" should be skipped (last segment with length 3)
289
+ expect(screen.queryByText("abc")).not.toBeInTheDocument();
290
+ expect(screen.getByText("products")).toBeInTheDocument();
291
+ expect(screen.getByText("shirts")).toBeInTheDocument();
292
+ });
293
+
294
+ it("skips duplicate consecutive segments", () => {
295
+ const { container } = render(
296
+ <Breadcrumb currentRoute="/products/products" />
297
+ );
298
+
299
+ const nav = container.querySelector("nav");
300
+ const productLinks = nav?.querySelectorAll('a[href="products"]');
301
+
302
+ // Should only have one "products" link (first occurrence)
303
+ expect(productLinks?.length).toBeLessThanOrEqual(1);
304
+ });
305
+
306
+ it("handles single segment path", () => {
307
+ render(<Breadcrumb currentRoute="/about" />);
308
+
309
+ expect(screen.getByRole("link", { name: "Home" })).toBeInTheDocument();
310
+ expect(screen.getByText("about")).toBeInTheDocument();
311
+ });
312
+
313
+ it("handles deep nesting (many segments)", () => {
314
+ render(
315
+ <Breadcrumb currentRoute="/level1/level2/level3/level4/level5" />
316
+ );
317
+
318
+ expect(screen.getByText("level1")).toBeInTheDocument();
319
+ expect(screen.getByText("level2")).toBeInTheDocument();
320
+ expect(screen.getByText("level3")).toBeInTheDocument();
321
+ expect(screen.getByText("level4")).toBeInTheDocument();
322
+ expect(screen.getByText("level5")).toBeInTheDocument();
323
+ });
324
+
325
+ it("handles special characters in segments", () => {
326
+ render(<Breadcrumb currentRoute="/products/t-shirts/blue" />);
327
+
328
+ expect(screen.getByText("t-shirts")).toBeInTheDocument();
329
+ });
330
+ });
331
+
332
+ // ============================================================================
333
+ // INTEGRATION TESTS
334
+ // ============================================================================
335
+
336
+ describe("Props Integration", () => {
337
+ it("spreads linkProps to Link components", () => {
338
+ const handleClick = vi.fn();
339
+
340
+ render(
341
+ <Breadcrumb
342
+ currentRoute="/products/shirts"
343
+ linkProps={{
344
+ onClick: handleClick,
345
+ "data-testid": "breadcrumb-link",
346
+ }}
347
+ />
348
+ );
349
+
350
+ const links = screen.getAllByTestId("breadcrumb-link");
351
+ expect(links.length).toBeGreaterThan(0);
352
+
353
+ // Click first link
354
+ links[0].click();
355
+ expect(handleClick).toHaveBeenCalled();
356
+ });
357
+
358
+ it("uses custom startRoute and startRouteUrl", () => {
359
+ render(
360
+ <Breadcrumb
361
+ currentRoute="/products"
362
+ startRoute="Dashboard"
363
+ startRouteUrl="/dashboard"
364
+ />
365
+ );
366
+
367
+ const startLink = screen.getByRole("link", { name: "Dashboard" });
368
+ expect(startLink).toBeInTheDocument();
369
+ expect(startLink).toHaveAttribute("href", "/dashboard");
370
+ });
371
+
372
+ it("renders custom spacer element", () => {
373
+ render(
374
+ <Breadcrumb
375
+ currentRoute="/products/shirts"
376
+ spacer={<span data-testid="custom-spacer">→</span>}
377
+ />
378
+ );
379
+
380
+ const spacers = screen.getAllByTestId("custom-spacer");
381
+ expect(spacers.length).toBeGreaterThan(0);
382
+ expect(spacers[0]).toHaveTextContent("→");
383
+ });
384
+
385
+ it("applies custom id to nav element", () => {
386
+ render(
387
+ <Breadcrumb
388
+ currentRoute="/products"
389
+ id="custom-breadcrumb"
390
+ />
391
+ );
392
+
393
+ const nav = screen.getByRole("navigation");
394
+ expect(nav).toHaveAttribute("id", "custom-breadcrumb");
395
+ });
396
+
397
+ it("applies custom className via classes prop", () => {
398
+ render(
399
+ <Breadcrumb
400
+ currentRoute="/products"
401
+ classes="custom-breadcrumb-class"
402
+ />
403
+ );
404
+
405
+ const nav = screen.getByRole("navigation");
406
+ expect(nav).toHaveClass("custom-breadcrumb-class");
407
+ });
408
+
409
+ it("applies custom inline styles", () => {
410
+ render(
411
+ <Breadcrumb
412
+ currentRoute="/products"
413
+ styles={{ padding: "1rem", backgroundColor: "lightgray" }}
414
+ />
415
+ );
416
+
417
+ const nav = screen.getByRole("navigation");
418
+ expect(nav).toHaveStyle({ padding: "1rem" });
419
+ });
420
+ });
421
+
422
+ // ============================================================================
423
+ // SUB-COMPONENT EXPORTS TESTS
424
+ // ============================================================================
425
+
426
+ describe("Sub-component Exports", () => {
427
+ it("exports Nav sub-component", () => {
428
+ expect(Breadcrumb.Nav).toBeDefined();
429
+ });
430
+
431
+ it("exports List sub-component", () => {
432
+ expect(Breadcrumb.List).toBeDefined();
433
+ });
434
+
435
+ it("exports Item sub-component", () => {
436
+ expect(Breadcrumb.Item).toBeDefined();
437
+ });
438
+
439
+ it("allows custom composition with sub-components", () => {
440
+ render(
441
+ <Breadcrumb.Nav aria-label="Custom breadcrumb">
442
+ <Breadcrumb.Item>
443
+ <a href="/">Home</a>
444
+ </Breadcrumb.Item>
445
+ <Breadcrumb.Item>
446
+ <span>Current Page</span>
447
+ </Breadcrumb.Item>
448
+ </Breadcrumb.Nav>
449
+ );
450
+
451
+ expect(screen.getByRole("navigation")).toBeInTheDocument();
452
+ expect(screen.getByText("Home")).toBeInTheDocument();
453
+ expect(screen.getByText("Current Page")).toBeInTheDocument();
454
+ });
455
+ });
456
+
457
+ // ============================================================================
458
+ // SNAPSHOT TESTS
459
+ // ============================================================================
460
+
461
+ describe("Snapshot Tests", () => {
462
+ it("matches snapshot for simple breadcrumb", () => {
463
+ const { container } = render(
464
+ <Breadcrumb currentRoute="/products/shirts" />
465
+ );
466
+ expect(container).toMatchSnapshot();
467
+ });
468
+
469
+ it("matches snapshot with custom routes", () => {
470
+ const routes: CustomRoute[] = [
471
+ { path: "products", name: "All Products", url: "/products" },
472
+ ];
473
+
474
+ const { container } = render(
475
+ <Breadcrumb currentRoute="/products/shirts" routes={routes} />
476
+ );
477
+ expect(container).toMatchSnapshot();
478
+ });
479
+
480
+ it("matches snapshot with truncation", () => {
481
+ const { container } = render(
482
+ <Breadcrumb
483
+ currentRoute="/products/verylongproductname"
484
+ truncateLength={10}
485
+ />
486
+ );
487
+ expect(container).toMatchSnapshot();
488
+ });
489
+ });
490
+ });