diff --git a/package-lock.json b/package-lock.json
index 4e8b9ccc..a7a284ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,8 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
"@types/classnames": "^2.2.10",
"@types/markdown-it": "^12.2.3",
"@types/node": "^22.19.9",
@@ -40,10 +42,45 @@
"eslint-config-next": "^15.5.0",
"feed": "^4.1.0",
"globals": "^16.0.0",
+ "happy-dom": "^20.5.0",
"typescript": "^5.9.0",
"vitest": "^4.0.18"
}
},
+ "node_modules/@adobe/css-tools": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
@@ -1677,6 +1714,101 @@
"tslib": "^2.8.0"
}
},
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/dom/node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -1797,6 +1929,23 @@
"@types/node": "*"
}
},
+ "node_modules/@types/whatwg-mimetype": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
+ "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
@@ -2222,6 +2371,31 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2790,6 +2964,13 @@
"node": ">=8.0.0"
}
},
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/csstype": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
@@ -2969,6 +3150,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -2990,6 +3182,14 @@
"node": ">=8"
}
},
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -4178,6 +4378,37 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/happy-dom": {
+ "version": "20.5.0",
+ "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.5.0.tgz",
+ "integrity": "sha512-VQe+Q5CYiGOgcCERXhcfNsbnrN92FDEKciMH/x6LppU9dd0j4aTjCTlqONFOIMcAm/5JxS3+utowbXV1OoFr+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">=20.0.0",
+ "@types/whatwg-mimetype": "^3.0.2",
+ "@types/ws": "^8.18.1",
+ "entities": "^4.5.0",
+ "whatwg-mimetype": "^3.0.0",
+ "ws": "^8.18.3"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/happy-dom/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -4316,6 +4547,16 @@
"node": ">=0.8.19"
}
},
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -4940,6 +5181,17 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5022,6 +5274,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -5569,6 +5831,30 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -5735,6 +6021,20 @@
"node": ">= 6"
}
},
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz",
@@ -6399,6 +6699,19 @@
"node": ">=4"
}
},
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -7052,6 +7365,16 @@
}
}
},
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7183,6 +7506,28 @@
"resolved": "http://storage.mds.yandex.net/get-npm/69187/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
diff --git a/package.json b/package.json
index 04f99b38..0e4d3874 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,8 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
"@types/classnames": "^2.2.10",
"@types/markdown-it": "^12.2.3",
"@types/node": "^22.19.9",
@@ -52,6 +54,7 @@
"eslint-config-next": "^15.5.0",
"feed": "^4.1.0",
"globals": "^16.0.0",
+ "happy-dom": "^20.5.0",
"typescript": "^5.9.0",
"vitest": "^4.0.18"
}
diff --git a/src/components/Breadcrumbs/Breadcrumbs.test.tsx b/src/components/Breadcrumbs/Breadcrumbs.test.tsx
new file mode 100644
index 00000000..17396c5d
--- /dev/null
+++ b/src/components/Breadcrumbs/Breadcrumbs.test.tsx
@@ -0,0 +1,34 @@
+import { render, within } from "@testing-library/react"
+import { describe, it, expect } from "vitest"
+import { Breadcrumbs } from "."
+
+describe("Breadcrumbs", () => {
+ const items = [
+ { name: "Home", href: "/" },
+ { name: "About", href: "/about" },
+ { name: "Team", href: "/about/team" },
+ ]
+
+ it("renders breadcrumb items with separators", () => {
+ const { container } = render()
+ const nav = within(container.querySelector("nav")!)
+
+ expect(nav.getByText("Home")).toBeInTheDocument()
+ expect(nav.getByText("About")).toBeInTheDocument()
+ expect(nav.getByText("Team")).toBeInTheDocument()
+
+ const separators = nav.getAllByText("/")
+ expect(separators).toHaveLength(2)
+ })
+
+ it("disables the button matching the current path", () => {
+ const { container } = render()
+ const nav = within(container.querySelector("nav")!)
+
+ const aboutLink = nav.getByText("About").closest("a")!
+ expect(aboutLink.className).toMatch(/disabled/)
+
+ const homeLink = nav.getByText("Home").closest("a")!
+ expect(homeLink.className).not.toMatch(/disabled/)
+ })
+})
diff --git a/src/ui/Box/Box.test.tsx b/src/ui/Box/Box.test.tsx
new file mode 100644
index 00000000..cfb01995
--- /dev/null
+++ b/src/ui/Box/Box.test.tsx
@@ -0,0 +1,44 @@
+import { render, screen } from "@testing-library/react"
+import { describe, it, expect } from "vitest"
+import { Box } from "."
+
+describe("Box", () => {
+ it("renders children", () => {
+ render(hello)
+ expect(screen.getByText("hello")).toBeInTheDocument()
+ })
+
+ it("defaults to a div tag", () => {
+ render(content)
+ expect(screen.getByText("content").tagName).toBe("DIV")
+ })
+
+ it("renders as a custom tag via the as prop", () => {
+ render(section content)
+ expect(screen.getByText("section content").tagName).toBe("SECTION")
+ })
+
+ it("applies vertical class", () => {
+ const { container } = render(v)
+ const el = container.firstElementChild!
+ expect(el.className).toMatch(/vertical/)
+ })
+
+ it("applies center class", () => {
+ const { container } = render(c)
+ const el = container.firstElementChild!
+ expect(el.className).toMatch(/center/)
+ })
+
+ it("applies wrap class", () => {
+ const { container } = render(w)
+ const el = container.firstElementChild!
+ expect(el.className).toMatch(/wrap/)
+ })
+
+ it("passes gap to inline style", () => {
+ const { container } = render(g)
+ const el = container.firstElementChild!
+ expect(el).toHaveStyle({ gap: "8px" })
+ })
+})
diff --git a/src/ui/Button/Button.test.tsx b/src/ui/Button/Button.test.tsx
new file mode 100644
index 00000000..85a1e33e
--- /dev/null
+++ b/src/ui/Button/Button.test.tsx
@@ -0,0 +1,32 @@
+import { render, screen, fireEvent } from "@testing-library/react"
+import { describe, it, expect, vi } from "vitest"
+import { Button } from "."
+
+describe("Button", () => {
+ it("renders children in a button element", () => {
+ render()
+ const button = screen.getByRole("button", { name: "Click me" })
+ expect(button).toBeInTheDocument()
+ expect(button.tagName).toBe("BUTTON")
+ })
+
+ it("renders as a link when href is provided", () => {
+ render()
+ const link = screen.getByRole("link", { name: "About" })
+ expect(link).toBeInTheDocument()
+ expect(link).toHaveAttribute("href", "/about")
+ })
+
+ it("calls onClick with value when clicked", () => {
+ const onClick = vi.fn()
+ render()
+ fireEvent.click(screen.getByRole("button", { name: "Click" }))
+ expect(onClick).toHaveBeenCalledOnce()
+ expect(onClick).toHaveBeenCalledWith("test-value", expect.anything())
+ })
+
+ it("applies disabled class when disabled is true", () => {
+ const { container } = render()
+ expect(container.firstElementChild!.className).toMatch(/disabled/)
+ })
+})
diff --git a/src/ui/Panel/Panel.test.tsx b/src/ui/Panel/Panel.test.tsx
new file mode 100644
index 00000000..d0c9829f
--- /dev/null
+++ b/src/ui/Panel/Panel.test.tsx
@@ -0,0 +1,23 @@
+import { render } from "@testing-library/react"
+import { describe, it, expect } from "vitest"
+import { Panel } from "."
+
+describe("Panel", () => {
+ it("renders children", () => {
+ const { container } = render(hello)
+ expect(container).toHaveTextContent("hello")
+ })
+
+ it("applies the default variant class", () => {
+ const { container } = render(content)
+ const el = container.firstElementChild!
+ expect(el.className).toMatch(/default/)
+ })
+
+ it("applies the ghost variant class when ghost is true", () => {
+ const { container } = render(content)
+ const el = container.firstElementChild!
+ expect(el.className).toMatch(/ghost/)
+ expect(el.className).not.toMatch(/default/)
+ })
+})
diff --git a/vitest.config.ts b/vitest.config.ts
index 2afc56f9..b0229e80 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -2,6 +2,9 @@ import { defineConfig } from "vitest/config"
import path from "path"
export default defineConfig({
+ esbuild: {
+ jsx: "automatic",
+ },
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
@@ -10,5 +13,8 @@ export default defineConfig({
},
test: {
include: ["src/**/*.test.{ts,tsx}"],
+ environment: "happy-dom",
+ setupFiles: ["./vitest.setup.tsx"],
+ css: true,
},
})
diff --git a/vitest.setup.tsx b/vitest.setup.tsx
new file mode 100644
index 00000000..f2895e39
--- /dev/null
+++ b/vitest.setup.tsx
@@ -0,0 +1,9 @@
+import "@testing-library/jest-dom/vitest"
+import { vi } from "vitest"
+
+vi.mock("next/link", () => ({
+ __esModule: true,
+ default: ({ href, children, ...props }: any) => (
+ {children}
+ ),
+}))