diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml
new file mode 100644
index 00000000..4b2bcf0a
--- /dev/null
+++ b/.github/workflows/web.yml
@@ -0,0 +1,45 @@
+name: web-tests
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ unit:
+ name: Web unit tests
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: apps/web
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ name: Install pnpm
+ with:
+ version: 10
+ run_install: false
+
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'pnpm'
+ cache-dependency-path: apps/web/pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Run playwright install
+ run: pnpm exec playwright install
+
+ - name: Run tests
+ run: pnpm test
+
diff --git a/apps/web/package.json b/apps/web/package.json
index 325e7242..acd1fd29 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -7,6 +7,8 @@
"dev": "vite --host 0.0.0.0",
"build": "tsc -b && vite build",
"lint": "eslint .",
+ "test": "vitest run",
+ "test-cover": "vitest run --coverage",
"preview": "vite preview",
"prepare": "cd ../.. && husky ./apps/web/.husky",
"format": "prettier --write . && git add --all",
@@ -46,6 +48,8 @@
"@storybook/react-vite": "^8.6.12",
"@storybook/test": "^8.6.12",
"@tanstack/router-plugin": "^1.120.2",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.3.0",
"@types/node": "^22.15.15",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
@@ -58,6 +62,7 @@
"eslint-plugin-storybook": "^0.12.0",
"globals": "^16.0.0",
"husky": "^9.1.7",
+ "jsdom": "^26.1.0",
"lint-staged": "^15.5.2",
"playwright": "^1.52.0",
"prettier": "3.5.3",
diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml
index 419ac828..9bca726a 100644
--- a/apps/web/pnpm-lock.yaml
+++ b/apps/web/pnpm-lock.yaml
@@ -71,6 +71,12 @@ importers:
"@tanstack/router-plugin":
specifier: ^1.120.2
version: 1.120.2(@tanstack/react-router@1.120.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(vite@6.3.5(@types/node@22.15.15)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))(webpack@5.99.8(esbuild@0.25.4))
+ "@testing-library/jest-dom":
+ specifier: ^6.6.3
+ version: 6.6.3
+ "@testing-library/react":
+ specifier: ^16.3.0
+ version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
"@types/node":
specifier: ^22.15.15
version: 22.15.15
@@ -107,6 +113,9 @@ importers:
husky:
specifier: ^9.1.7
version: 9.1.7
+ jsdom:
+ specifier: ^26.1.0
+ version: 26.1.0
lint-staged:
specifier: ^15.5.2
version: 15.5.2
@@ -130,7 +139,7 @@ importers:
version: 6.3.5(@types/node@22.15.15)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
vitest:
specifier: ^3.1.3
- version: 3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
+ version: 3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
packages:
"@adobe/css-tools@4.4.2":
@@ -146,6 +155,12 @@ packages:
}
engines: { node: ">=6.0.0" }
+ "@asamuzakjp/css-color@3.1.7":
+ resolution:
+ {
+ integrity: sha512-Ok5fYhtwdyJQmU1PpEv6Si7Y+A4cYb8yNM9oiIJC9TzXPMuN9fvdonKJqcnz9TbFqV6bQ8z0giRq0iaOpGZV2g==,
+ }
+
"@babel/code-frame@7.27.1":
resolution:
{
@@ -320,6 +335,49 @@ packages:
peerDependencies:
storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0
+ "@csstools/color-helpers@5.0.2":
+ resolution:
+ {
+ integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==,
+ }
+ engines: { node: ">=18" }
+
+ "@csstools/css-calc@2.1.3":
+ resolution:
+ {
+ integrity: sha512-XBG3talrhid44BY1x3MHzUx/aTG8+x/Zi57M4aTKK9RFB4aLlF3TTSzfzn8nWVHWL3FgAXAxmupmDd6VWww+pw==,
+ }
+ engines: { node: ">=18" }
+ peerDependencies:
+ "@csstools/css-parser-algorithms": ^3.0.4
+ "@csstools/css-tokenizer": ^3.0.3
+
+ "@csstools/css-color-parser@3.0.9":
+ resolution:
+ {
+ integrity: sha512-wILs5Zk7BU86UArYBJTPy/FMPPKVKHMj1ycCEyf3VUptol0JNRLFU/BZsJ4aiIHJEbSLiizzRrw8Pc1uAEDrXw==,
+ }
+ engines: { node: ">=18" }
+ peerDependencies:
+ "@csstools/css-parser-algorithms": ^3.0.4
+ "@csstools/css-tokenizer": ^3.0.3
+
+ "@csstools/css-parser-algorithms@3.0.4":
+ resolution:
+ {
+ integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==,
+ }
+ engines: { node: ">=18" }
+ peerDependencies:
+ "@csstools/css-tokenizer": ^3.0.3
+
+ "@csstools/css-tokenizer@3.0.3":
+ resolution:
+ {
+ integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==,
+ }
+ engines: { node: ">=18" }
+
"@esbuild/aix-ppc64@0.25.4":
resolution:
{
@@ -2433,6 +2491,31 @@ packages:
}
engines: { node: ">=14", npm: ">=6", yarn: ">=1" }
+ "@testing-library/jest-dom@6.6.3":
+ resolution:
+ {
+ integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==,
+ }
+ engines: { node: ">=14", npm: ">=6", yarn: ">=1" }
+
+ "@testing-library/react@16.3.0":
+ resolution:
+ {
+ integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==,
+ }
+ 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
+
"@testing-library/user-event@14.5.2":
resolution:
{
@@ -2870,6 +2953,13 @@ packages:
engines: { node: ">=0.4.0" }
hasBin: true
+ agent-base@7.1.3:
+ resolution:
+ {
+ integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==,
+ }
+ engines: { node: ">= 14" }
+
ajv-formats@2.1.1:
resolution:
{
@@ -3319,12 +3409,26 @@ packages:
integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==,
}
+ cssstyle@4.3.1:
+ resolution:
+ {
+ integrity: sha512-ZgW+Jgdd7i52AaLYCriF8Mxqft0gD/R9i9wi6RWBhs1pqdPEzPjym7rvRKi397WmQFf3SlyUsszhw+VVCbx79Q==,
+ }
+ engines: { node: ">=18" }
+
csstype@3.1.3:
resolution:
{
integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==,
}
+ data-urls@5.0.0:
+ resolution:
+ {
+ integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==,
+ }
+ engines: { node: ">=18" }
+
debug@4.4.0:
resolution:
{
@@ -3481,6 +3585,13 @@ packages:
}
engines: { node: ">=10.13.0" }
+ entities@6.0.0:
+ resolution:
+ {
+ integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==,
+ }
+ engines: { node: ">=0.12" }
+
environment@1.1.0:
resolution:
{
@@ -4058,6 +4169,13 @@ packages:
}
engines: { node: ">= 0.4" }
+ html-encoding-sniffer@4.0.0:
+ resolution:
+ {
+ integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==,
+ }
+ engines: { node: ">=18" }
+
html-escaper@2.0.2:
resolution:
{
@@ -4071,6 +4189,20 @@ packages:
}
engines: { node: ">= 0.8" }
+ http-proxy-agent@7.0.2:
+ resolution:
+ {
+ integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==,
+ }
+ engines: { node: ">= 14" }
+
+ https-proxy-agent@7.0.6:
+ resolution:
+ {
+ integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==,
+ }
+ engines: { node: ">= 14" }
+
human-signals@5.0.0:
resolution:
{
@@ -4225,6 +4357,12 @@ packages:
}
engines: { node: ">=0.12.0" }
+ is-potential-custom-element-name@1.0.1:
+ resolution:
+ {
+ integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==,
+ }
+
is-promise@4.0.0:
resolution:
{
@@ -4333,6 +4471,18 @@ packages:
}
engines: { node: ">=12.0.0" }
+ jsdom@26.1.0:
+ resolution:
+ {
+ integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==,
+ }
+ engines: { node: ">=18" }
+ peerDependencies:
+ canvas: ^3.0.0
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
jsesc@3.1.0:
resolution:
{
@@ -4796,6 +4946,12 @@ packages:
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
+ nwsapi@2.2.20:
+ resolution:
+ {
+ integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==,
+ }
+
object-assign@4.1.1:
resolution:
{
@@ -4878,6 +5034,12 @@ packages:
}
engines: { node: ">=6" }
+ parse5@7.3.0:
+ resolution:
+ {
+ integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==,
+ }
+
parseurl@1.3.3:
resolution:
{
@@ -5262,6 +5424,12 @@ packages:
}
engines: { node: ">= 18" }
+ rrweb-cssom@0.8.0:
+ resolution:
+ {
+ integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==,
+ }
+
run-parallel@1.2.0:
resolution:
{
@@ -5287,6 +5455,13 @@ packages:
integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==,
}
+ saxes@6.0.0:
+ resolution:
+ {
+ integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==,
+ }
+ engines: { node: ">=v12.22.7" }
+
scheduler@0.26.0:
resolution:
{
@@ -5579,6 +5754,12 @@ packages:
}
engines: { node: ">= 0.4" }
+ symbol-tree@3.2.4:
+ resolution:
+ {
+ integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==,
+ }
+
tailwind-merge@3.0.2:
resolution:
{
@@ -5708,6 +5889,19 @@ packages:
}
engines: { node: ">=14.0.0" }
+ tldts-core@6.1.86:
+ resolution:
+ {
+ integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==,
+ }
+
+ tldts@6.1.86:
+ resolution:
+ {
+ integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==,
+ }
+ hasBin: true
+
to-regex-range@5.0.1:
resolution:
{
@@ -5729,6 +5923,20 @@ packages:
}
engines: { node: ">=6" }
+ tough-cookie@5.1.2:
+ resolution:
+ {
+ integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==,
+ }
+ engines: { node: ">=16" }
+
+ tr46@5.1.1:
+ resolution:
+ {
+ integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==,
+ }
+ engines: { node: ">=18" }
+
ts-api-utils@2.1.0:
resolution:
{
@@ -5970,6 +6178,13 @@ packages:
jsdom:
optional: true
+ w3c-xmlserializer@5.0.0:
+ resolution:
+ {
+ integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==,
+ }
+ engines: { node: ">=18" }
+
watchpack@2.4.2:
resolution:
{
@@ -5977,6 +6192,13 @@ packages:
}
engines: { node: ">=10.13.0" }
+ webidl-conversions@7.0.0:
+ resolution:
+ {
+ integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==,
+ }
+ engines: { node: ">=12" }
+
webpack-sources@3.2.3:
resolution:
{
@@ -6003,6 +6225,27 @@ packages:
webpack-cli:
optional: true
+ whatwg-encoding@3.1.1:
+ resolution:
+ {
+ integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==,
+ }
+ engines: { node: ">=18" }
+
+ whatwg-mimetype@4.0.0:
+ resolution:
+ {
+ integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==,
+ }
+ engines: { node: ">=18" }
+
+ whatwg-url@14.2.0:
+ resolution:
+ {
+ integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==,
+ }
+ engines: { node: ">=18" }
+
which-typed-array@1.1.19:
resolution:
{
@@ -6075,6 +6318,19 @@ packages:
utf-8-validate:
optional: true
+ xml-name-validator@5.0.0:
+ resolution:
+ {
+ integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==,
+ }
+ engines: { node: ">=18" }
+
+ xmlchars@2.2.0:
+ resolution:
+ {
+ integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==,
+ }
+
yallist@3.1.1:
resolution:
{
@@ -6118,6 +6374,14 @@ snapshots:
"@jridgewell/gen-mapping": 0.3.8
"@jridgewell/trace-mapping": 0.3.25
+ "@asamuzakjp/css-color@3.1.7":
+ dependencies:
+ "@csstools/css-calc": 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
+ "@csstools/css-color-parser": 3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
+ "@csstools/css-parser-algorithms": 3.0.4(@csstools/css-tokenizer@3.0.3)
+ "@csstools/css-tokenizer": 3.0.3
+ lru-cache: 10.4.3
+
"@babel/code-frame@7.27.1":
dependencies:
"@babel/helper-validator-identifier": 7.27.1
@@ -6255,6 +6519,26 @@ snapshots:
- "@chromatic-com/playwright"
- react
+ "@csstools/color-helpers@5.0.2": {}
+
+ "@csstools/css-calc@2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)":
+ dependencies:
+ "@csstools/css-parser-algorithms": 3.0.4(@csstools/css-tokenizer@3.0.3)
+ "@csstools/css-tokenizer": 3.0.3
+
+ "@csstools/css-color-parser@3.0.9(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)":
+ dependencies:
+ "@csstools/color-helpers": 5.0.2
+ "@csstools/css-calc": 2.1.3(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
+ "@csstools/css-parser-algorithms": 3.0.4(@csstools/css-tokenizer@3.0.3)
+ "@csstools/css-tokenizer": 3.0.3
+
+ "@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)":
+ dependencies:
+ "@csstools/css-tokenizer": 3.0.3
+
+ "@csstools/css-tokenizer@3.0.3": {}
+
"@esbuild/aix-ppc64@0.25.4":
optional: true
@@ -7773,7 +8057,7 @@ snapshots:
optionalDependencies:
"@vitest/browser": 3.1.3(playwright@1.52.0)(vite@6.3.5(@types/node@22.15.15)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))(vitest@3.1.3)
"@vitest/runner": 3.1.3
- vitest: 3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
+ vitest: 3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
transitivePeerDependencies:
- react
- react-dom
@@ -8030,6 +8314,26 @@ snapshots:
lodash: 4.17.21
redent: 3.0.0
+ "@testing-library/jest-dom@6.6.3":
+ dependencies:
+ "@adobe/css-tools": 4.4.2
+ aria-query: 5.3.2
+ chalk: 3.0.0
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ lodash: 4.17.21
+ redent: 3.0.0
+
+ "@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)":
+ dependencies:
+ "@babel/runtime": 7.27.1
+ "@testing-library/dom": 10.4.0
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ "@types/react": 19.1.3
+ "@types/react-dom": 19.1.3(@types/react@19.1.3)
+
"@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)":
dependencies:
"@testing-library/dom": 10.4.0
@@ -8192,7 +8496,7 @@ snapshots:
magic-string: 0.30.17
sirv: 3.0.1
tinyrainbow: 2.0.0
- vitest: 3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
+ vitest: 3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
ws: 8.18.2
optionalDependencies:
playwright: 1.52.0
@@ -8216,7 +8520,7 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
- vitest: 3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
+ vitest: 3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1)
optionalDependencies:
"@vitest/browser": 3.1.3(playwright@1.52.0)(vite@6.3.5(@types/node@22.15.15)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))(vitest@3.1.3)
transitivePeerDependencies:
@@ -8385,6 +8689,8 @@ snapshots:
acorn@8.14.1: {}
+ agent-base@7.1.3: {}
+
ajv-formats@2.1.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
@@ -8633,8 +8939,18 @@ snapshots:
css.escape@1.5.1: {}
+ cssstyle@4.3.1:
+ dependencies:
+ "@asamuzakjp/css-color": 3.1.7
+ rrweb-cssom: 0.8.0
+
csstype@3.1.3: {}
+ data-urls@5.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+
debug@4.4.0:
dependencies:
ms: 2.1.3
@@ -8696,6 +9012,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.2.1
+ entities@6.0.0: {}
+
environment@1.1.0: {}
es-define-property@1.0.1: {}
@@ -9084,6 +9402,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
html-escaper@2.0.2: {}
http-errors@2.0.0:
@@ -9094,6 +9416,20 @@ snapshots:
statuses: 2.0.1
toidentifier: 1.0.1
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.3
+ debug: 4.4.0
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.3
+ debug: 4.4.0
+ transitivePeerDependencies:
+ - supports-color
+
human-signals@5.0.0: {}
husky@9.1.7: {}
@@ -9164,6 +9500,8 @@ snapshots:
is-number@7.0.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-promise@4.0.0: {}
is-regex@1.2.1:
@@ -9228,6 +9566,33 @@ snapshots:
jsdoc-type-pratt-parser@4.1.0: {}
+ jsdom@26.1.0:
+ dependencies:
+ cssstyle: 4.3.1
+ data-urls: 5.0.0
+ decimal.js: 10.5.0
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ is-potential-custom-element-name: 1.0.1
+ nwsapi: 2.2.20
+ parse5: 7.3.0
+ rrweb-cssom: 0.8.0
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 5.1.2
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+ ws: 8.18.2
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
@@ -9447,6 +9812,8 @@ snapshots:
dependencies:
path-key: 4.0.0
+ nwsapi@2.2.20: {}
+
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -9496,6 +9863,10 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.0
+
parseurl@1.3.3: {}
path-exists@4.0.0: {}
@@ -9811,6 +10182,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ rrweb-cssom@0.8.0: {}
+
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -9825,6 +10198,10 @@ snapshots:
safer-buffer@2.1.2: {}
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.26.0: {}
schema-utils@4.3.2:
@@ -10011,6 +10388,8 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
+ symbol-tree@3.2.4: {}
+
tailwind-merge@3.0.2: {}
tailwind-variants@1.0.0(tailwindcss@4.1.5):
@@ -10071,6 +10450,12 @@ snapshots:
tinyspy@3.0.2: {}
+ tldts-core@6.1.86: {}
+
+ tldts@6.1.86:
+ dependencies:
+ tldts-core: 6.1.86
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -10079,6 +10464,14 @@ snapshots:
totalist@3.0.1: {}
+ tough-cookie@5.1.2:
+ dependencies:
+ tldts: 6.1.86
+
+ tr46@5.1.1:
+ dependencies:
+ punycode: 2.3.1
+
ts-api-utils@2.1.0(typescript@5.8.3):
dependencies:
typescript: 5.8.3
@@ -10207,7 +10600,7 @@ snapshots:
tsx: 4.19.4
yaml: 2.7.1
- vitest@3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1):
+ vitest@3.1.3(@types/node@22.15.15)(@vitest/browser@3.1.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1):
dependencies:
"@vitest/expect": 3.1.3
"@vitest/mocker": 3.1.3(vite@6.3.5(@types/node@22.15.15)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))
@@ -10233,6 +10626,7 @@ snapshots:
optionalDependencies:
"@types/node": 22.15.15
"@vitest/browser": 3.1.3(playwright@1.52.0)(vite@6.3.5(@types/node@22.15.15)(jiti@2.4.2)(lightningcss@1.29.2)(terser@5.39.0)(tsx@4.19.4)(yaml@2.7.1))(vitest@3.1.3)
+ jsdom: 26.1.0
transitivePeerDependencies:
- jiti
- less
@@ -10247,11 +10641,17 @@ snapshots:
- tsx
- yaml
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
watchpack@2.4.2:
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
+ webidl-conversions@7.0.0: {}
+
webpack-sources@3.2.3: {}
webpack-virtual-modules@0.6.2: {}
@@ -10287,6 +10687,17 @@ snapshots:
- esbuild
- uglify-js
+ whatwg-encoding@3.1.1:
+ dependencies:
+ iconv-lite: 0.6.3
+
+ whatwg-mimetype@4.0.0: {}
+
+ whatwg-url@14.2.0:
+ dependencies:
+ tr46: 5.1.1
+ webidl-conversions: 7.0.0
+
which-typed-array@1.1.19:
dependencies:
available-typed-arrays: 1.0.7
@@ -10330,6 +10741,10 @@ snapshots:
ws@8.18.2: {}
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
yallist@3.1.1: {}
yaml@2.7.1: {}
diff --git a/apps/web/src/components/ui/Button/Button.test.tsx b/apps/web/src/components/ui/Button/Button.test.tsx
new file mode 100644
index 00000000..06d0316f
--- /dev/null
+++ b/apps/web/src/components/ui/Button/Button.test.tsx
@@ -0,0 +1,12 @@
+// src/components/Button.test.tsx
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+import { Button } from "./Button";
+
+describe("Button component", () => {
+ it("renders with correct text", () => {
+ render();
+ const button = screen.getByText(/Click Me!/i);
+ expect(button).toBeInTheDocument();
+ });
+});
diff --git a/apps/web/src/components/ui/Button/Button.types.ts b/apps/web/src/components/ui/Button/Button.types.ts
deleted file mode 100644
index 553f3293..00000000
--- a/apps/web/src/components/ui/Button/Button.types.ts
+++ /dev/null
@@ -1 +0,0 @@
-// This will be where button types and constants go
diff --git a/apps/web/src/setupTests.ts b/apps/web/src/setupTests.ts
new file mode 100644
index 00000000..bf210c79
--- /dev/null
+++ b/apps/web/src/setupTests.ts
@@ -0,0 +1,11 @@
+// src/setupTests.js (or .ts)
+import "@testing-library/jest-dom";
+import { cleanup } from "@testing-library/react";
+import { afterEach } from "vitest";
+
+// This is often implicitly handled by @testing-library/react with Vitest,
+// but explicitly calling cleanup can be a good practice to ensure isolation
+// between tests in case of specific scenarios or custom renderers.
+afterEach(() => {
+ cleanup();
+});
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 1ffef600..4d73ec11 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -3,5 +3,8 @@
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
- ]
+ ],
+ "compilerOptions": {
+ "types": ["vitest/globals", "@testing-library/jest-dom"]
+ }
}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 743c393c..9d3cc68d 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,3 +1,4 @@
+///
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
@@ -11,6 +12,11 @@ export default defineConfig({
react(),
tailwindcss(),
],
+ test: {
+ environment: "jsdom",
+ setupFiles: ["./src/setupTests.ts"],
+ globals: true,
+ },
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
diff --git a/apps/web/vitest.workspace.ts b/apps/web/vitest.workspace.ts
index c56bdfe5..1da3a007 100644
--- a/apps/web/vitest.workspace.ts
+++ b/apps/web/vitest.workspace.ts
@@ -13,6 +13,15 @@ const dirname =
// More info at: https://storybook.js.org/docs/writing-tests/test-addon
export default defineWorkspace([
"vite.config.ts",
+ {
+ extends: "vite.config.ts",
+ test: {
+ name: "unit",
+ environment: "jsdom",
+ setupFiles: ["./src/setupTests.ts"],
+ globals: true,
+ },
+ },
{
extends: "vite.config.ts",
plugins: [