diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 3ed7aff..f6fcad1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -32,6 +32,46 @@ jobs:
- name: Build
run: go build ./...
+ web:
+ name: Web checks
+ runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: postgres:16-alpine
+ env:
+ POSTGRES_PASSWORD: devpass
+ POSTGRES_DB: f3redirect
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd "pg_isready -U postgres"
+ --health-interval 5s --health-timeout 5s --health-retries 10
+ defaults:
+ run:
+ working-directory: web
+ env:
+ DATABASE_URL: postgres://postgres:devpass@localhost:5432/f3redirect
+ TEST_DATABASE_URL: postgres://postgres:devpass@localhost:5432/f3redirect
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 9.12.0
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+ cache-dependency-path: web/pnpm-lock.yaml
+ - run: pnpm install --frozen-lockfile
+ - name: Apply schema to the CI Postgres
+ run: pnpm db:push
+ - name: Typecheck
+ run: pnpm typecheck
+ - name: Unit + integration + component tests (coverage gate)
+ run: pnpm test:cov
+ # NB: Playwright e2e runs locally (browser-heavy); unit/integration/
+ # component tests run here against the Postgres service.
+
docker:
name: Docker build
runs-on: ubuntu-latest
diff --git a/web/package.json b/web/package.json
index e32bea1..87cae34 100644
--- a/web/package.json
+++ b/web/package.json
@@ -30,11 +30,15 @@
},
"devDependencies": {
"@playwright/test": "^1.49.1",
+ "@testing-library/jest-dom": "^6",
+ "@testing-library/react": "^16",
+ "@testing-library/user-event": "^14",
"@types/node": "^22.10.7",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@vitest/coverage-v8": "^3.0.5",
"drizzle-kit": "^0.31.4",
+ "jsdom": "^25",
"typescript": "^5.7.3",
"vitest": "^3.0.5"
}
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 0251c39..d43db42 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -10,13 +10,13 @@ importers:
dependencies:
'@better-auth/passkey':
specifier: ^1.6.11
- version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(tsx@4.22.3)))(better-call@1.3.5(zod@4.4.3))(nanostores@1.3.0)
+ version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3)))(better-call@1.3.5(zod@4.4.3))(nanostores@1.3.0)
'@google-cloud/storage':
specifier: ^7.14.0
version: 7.19.0
better-auth:
specifier: ^1.6.11
- version: 1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(tsx@4.22.3))
+ version: 1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3))
drizzle-orm:
specifier: ^0.45.2
version: 0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9)
@@ -42,6 +42,15 @@ importers:
'@playwright/test':
specifier: ^1.49.1
version: 1.60.0
+ '@testing-library/jest-dom':
+ specifier: ^6
+ version: 6.9.1
+ '@testing-library/react':
+ specifier: ^16
+ version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ '@testing-library/user-event':
+ specifier: ^14
+ version: 14.6.1(@testing-library/dom@10.4.1)
'@types/node':
specifier: ^22.10.7
version: 22.19.19
@@ -53,23 +62,36 @@ importers:
version: 19.2.3(@types/react@19.2.15)
'@vitest/coverage-v8':
specifier: ^3.0.5
- version: 3.2.4(vitest@3.2.4(@types/node@22.19.19)(tsx@4.22.3))
+ version: 3.2.4(vitest@3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3))
drizzle-kit:
specifier: ^0.31.4
version: 0.31.10
+ jsdom:
+ specifier: ^25
+ version: 25.0.1
typescript:
specifier: ^5.7.3
version: 5.9.3
vitest:
specifier: ^3.0.5
- version: 3.2.4(@types/node@22.19.19)(tsx@4.22.3)
+ version: 3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3)
packages:
+ '@adobe/css-tools@4.5.0':
+ resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
+
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
+ '@asamuzakjp/css-color@3.2.0':
+ resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
+
+ '@babel/code-frame@7.29.0':
+ resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
@@ -83,6 +105,10 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
+ '@babel/runtime@7.29.2':
+ resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
+ engines: {node: '>=6.9.0'}
+
'@babel/types@7.29.0':
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
@@ -180,6 +206,34 @@ packages:
'@better-fetch/fetch@1.1.21':
resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==}
+ '@csstools/color-helpers@5.1.0':
+ resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-calc@2.1.4':
+ resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-color-parser@3.1.0':
+ resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5':
+ resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-tokenizer@3.0.4':
+ resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+ engines: {node: '>=18'}
+
'@drizzle-team/brocli@0.10.2':
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
@@ -1230,10 +1284,42 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+ '@testing-library/dom@10.4.1':
+ resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
+ engines: {node: '>=18'}
+
+ '@testing-library/jest-dom@6.9.1':
+ resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
+ engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+
+ '@testing-library/react@16.3.2':
+ resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
+ 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.6.1':
+ resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==}
+ engines: {node: '>=12', npm: '>=6'}
+ peerDependencies:
+ '@testing-library/dom': '>=7.21.4'
+
'@tootallnate/once@2.0.1':
resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==}
engines: {node: '>= 10'}
+ '@types/aria-query@5.0.4':
+ resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
+
'@types/caseless@0.12.5':
resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==}
@@ -1328,10 +1414,21 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
+ aria-query@5.3.0:
+ resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
arrify@2.0.1:
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
engines: {node: '>=8'}
@@ -1486,9 +1583,20 @@ packages:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ css.escape@1.5.1:
+ resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
+
+ cssstyle@4.6.0:
+ resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
+ engines: {node: '>=18'}
+
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ data-urls@5.0.0:
+ resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
+ engines: {node: '>=18'}
+
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -1498,6 +1606,9 @@ packages:
supports-color:
optional: true
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
@@ -1509,10 +1620,20 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
+ dom-accessibility-api@0.5.16:
+ resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
+
+ dom-accessibility-api@0.6.3:
+ resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+
drizzle-kit@0.31.10:
resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
hasBin: true
@@ -1631,6 +1752,10 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
env-paths@3.0.0:
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -1712,6 +1837,10 @@ packages:
resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==}
engines: {node: '>= 0.12'}
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1786,6 +1915,10 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
+ html-encoding-sniffer@4.0.0:
+ resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+ engines: {node: '>=18'}
+
html-entities@2.6.0:
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
@@ -1796,6 +1929,10 @@ packages:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
@@ -1804,6 +1941,14 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -1811,6 +1956,9 @@ packages:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
@@ -1847,9 +1995,21 @@ packages:
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+ jsdom@25.0.1:
+ resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ canvas: ^2.11.2
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
json-bigint@1.0.0:
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
@@ -1869,6 +2029,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+ lz-string@1.5.0:
+ resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
+ hasBin: true
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1896,6 +2060,10 @@ packages:
engines: {node: '>=10.0.0'}
hasBin: true
+ min-indent@1.0.1:
+ resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
+ engines: {node: '>=4'}
+
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
@@ -1950,6 +2118,9 @@ packages:
encoding:
optional: true
+ nwsapi@2.2.23:
+ resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==}
+
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -1960,6 +2131,9 @@ packages:
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
path-expression-matcher@1.5.0:
resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==}
engines: {node: '>=14.0.0'}
@@ -2008,6 +2182,14 @@ packages:
resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==}
engines: {node: '>=12'}
+ pretty-format@27.5.1:
+ resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
+ engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
pvtsutils@1.3.6:
resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==}
@@ -2020,6 +2202,9 @@ packages:
peerDependencies:
react: ^19.2.6
+ react-is@17.0.2:
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+
react@19.2.6:
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
engines: {node: '>=0.10.0'}
@@ -2028,6 +2213,10 @@ packages:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
+ redent@3.0.0:
+ resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
+ engines: {node: '>=8'}
+
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
@@ -2050,9 +2239,22 @@ packages:
rou3@0.7.12:
resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==}
+ rrweb-cssom@0.7.1:
+ resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==}
+
+ rrweb-cssom@0.8.0:
+ resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
+
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -2129,6 +2331,10 @@ packages:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
+ strip-indent@3.0.0:
+ resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
+ engines: {node: '>=8'}
+
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
@@ -2155,6 +2361,9 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
teeny-request@9.0.0:
resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==}
engines: {node: '>=14'}
@@ -2192,9 +2401,17 @@ packages:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true
+ tough-cookie@5.1.2:
+ resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
+ engines: {node: '>=16'}
+
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ tr46@5.1.1:
+ resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
+ engines: {node: '>=18'}
+
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
@@ -2304,9 +2521,30 @@ packages:
jsdom:
optional: true
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
+
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+ deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
+
+ 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'}
+
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -2336,10 +2574,29 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ ws@8.21.0:
+ resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
+ 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
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
xml-naming@0.1.0:
resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==}
engines: {node: '>=16.0.0'}
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -2349,11 +2606,27 @@ packages:
snapshots:
+ '@adobe/css-tools@4.5.0': {}
+
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
+ '@asamuzakjp/css-color@3.2.0':
+ dependencies:
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+ lru-cache: 10.4.3
+
+ '@babel/code-frame@7.29.0':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.28.5
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
@@ -2362,6 +2635,8 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
+ '@babel/runtime@7.29.2': {}
+
'@babel/types@7.29.0':
dependencies:
'@babel/helper-string-parser': 7.27.1
@@ -2405,14 +2680,14 @@ snapshots:
'@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0)
'@better-auth/utils': 0.4.0
- '@better-auth/passkey@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(tsx@4.22.3)))(better-call@1.3.5(zod@4.4.3))(nanostores@1.3.0)':
+ '@better-auth/passkey@1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3)))(better-call@1.3.5(zod@4.4.3))(nanostores@1.3.0)':
dependencies:
'@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0)
'@better-auth/utils': 0.4.0
'@better-fetch/fetch': 1.1.21
'@simplewebauthn/browser': 13.3.0
'@simplewebauthn/server': 13.3.0
- better-auth: 1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(tsx@4.22.3))
+ better-auth: 1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3))
better-call: 1.3.5(zod@4.4.3)
nanostores: 1.3.0
zod: 4.4.3
@@ -2434,6 +2709,26 @@ snapshots:
'@better-fetch/fetch@1.1.21': {}
+ '@csstools/color-helpers@5.1.0': {}
+
+ '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/color-helpers': 5.1.0
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-tokenizer@3.0.4': {}
+
'@drizzle-team/brocli@0.10.2': {}
'@emnapi/runtime@1.10.0':
@@ -3145,8 +3440,44 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@testing-library/dom@10.4.1':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/runtime': 7.29.2
+ '@types/aria-query': 5.0.4
+ aria-query: 5.3.0
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ picocolors: 1.1.1
+ pretty-format: 27.5.1
+
+ '@testing-library/jest-dom@6.9.1':
+ dependencies:
+ '@adobe/css-tools': 4.5.0
+ aria-query: 5.3.2
+ css.escape: 1.5.1
+ dom-accessibility-api: 0.6.3
+ picocolors: 1.1.1
+ redent: 3.0.0
+
+ '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
+ dependencies:
+ '@babel/runtime': 7.29.2
+ '@testing-library/dom': 10.4.1
+ react: 19.2.6
+ react-dom: 19.2.6(react@19.2.6)
+ optionalDependencies:
+ '@types/react': 19.2.15
+ '@types/react-dom': 19.2.3(@types/react@19.2.15)
+
+ '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
+ dependencies:
+ '@testing-library/dom': 10.4.1
+
'@tootallnate/once@2.0.1': {}
+ '@types/aria-query@5.0.4': {}
+
'@types/caseless@0.12.5': {}
'@types/chai@5.2.3':
@@ -3181,7 +3512,7 @@ snapshots:
'@types/tough-cookie@4.0.5': {}
- '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.19)(tsx@4.22.3))':
+ '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -3196,7 +3527,7 @@ snapshots:
std-env: 3.10.0
test-exclude: 7.0.2
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/node@22.19.19)(tsx@4.22.3)
+ vitest: 3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3)
transitivePeerDependencies:
- supports-color
@@ -3262,8 +3593,16 @@ snapshots:
dependencies:
color-convert: 2.0.1
+ ansi-styles@5.2.0: {}
+
ansi-styles@6.2.3: {}
+ aria-query@5.3.0:
+ dependencies:
+ dequal: 2.0.3
+
+ aria-query@5.3.2: {}
+
arrify@2.0.1: {}
asn1js@3.0.10:
@@ -3292,7 +3631,7 @@ snapshots:
base64-js@1.5.1: {}
- better-auth@1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(tsx@4.22.3)):
+ better-auth@1.6.11(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))(next@15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3)):
dependencies:
'@better-auth/core': 1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0)
'@better-auth/drizzle-adapter': 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(drizzle-orm@0.45.2(gel@2.2.0)(kysely@0.28.17)(postgres@3.4.9))
@@ -3317,7 +3656,7 @@ snapshots:
next: 15.5.18(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
- vitest: 3.2.4(@types/node@22.19.19)(tsx@4.22.3)
+ vitest: 3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3)
transitivePeerDependencies:
- '@cloudflare/workers-types'
- '@opentelemetry/api'
@@ -3382,21 +3721,41 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
+ css.escape@1.5.1: {}
+
+ cssstyle@4.6.0:
+ dependencies:
+ '@asamuzakjp/css-color': 3.2.0
+ rrweb-cssom: 0.8.0
+
csstype@3.2.3: {}
+ data-urls@5.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+
debug@4.4.3:
dependencies:
ms: 2.1.3
+ decimal.js@10.6.0: {}
+
deep-eql@5.0.2: {}
defu@6.1.7: {}
delayed-stream@1.0.0: {}
+ dequal@2.0.3: {}
+
detect-libc@2.1.2:
optional: true
+ dom-accessibility-api@0.5.16: {}
+
+ dom-accessibility-api@0.6.3: {}
+
drizzle-kit@0.31.10:
dependencies:
'@drizzle-team/brocli': 0.10.2
@@ -3437,6 +3796,8 @@ snapshots:
dependencies:
once: 1.4.0
+ entities@6.0.1: {}
+
env-paths@3.0.0:
optional: true
@@ -3610,6 +3971,14 @@ snapshots:
mime-types: 2.1.35
safe-buffer: 5.2.1
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.3
+ mime-types: 2.1.35
+
fsevents@2.3.2:
optional: true
@@ -3717,6 +4086,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
html-entities@2.6.0: {}
html-escaper@2.0.2: {}
@@ -3729,6 +4102,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
@@ -3743,10 +4123,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ indent-string@4.0.0: {}
+
inherits@2.0.4: {}
is-fullwidth-code-point@3.0.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-stream@2.0.1: {}
isexe@2.0.0: {}
@@ -3785,8 +4173,38 @@ snapshots:
js-tokens@10.0.0: {}
+ js-tokens@4.0.0: {}
+
js-tokens@9.0.1: {}
+ jsdom@25.0.1:
+ dependencies:
+ cssstyle: 4.6.0
+ data-urls: 5.0.0
+ decimal.js: 10.6.0
+ form-data: 4.0.5
+ 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.23
+ parse5: 7.3.0
+ rrweb-cssom: 0.7.1
+ 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.21.0
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.3.1
@@ -3808,6 +4226,8 @@ snapshots:
lru-cache@10.4.3: {}
+ lz-string@1.5.0: {}
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3832,6 +4252,8 @@ snapshots:
mime@3.0.0: {}
+ min-indent@1.0.1: {}
+
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.6
@@ -3876,6 +4298,8 @@ snapshots:
dependencies:
whatwg-url: 5.0.0
+ nwsapi@2.2.23: {}
+
once@1.4.0:
dependencies:
wrappy: 1.0.2
@@ -3886,6 +4310,10 @@ snapshots:
package-json-from-dist@1.0.1: {}
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
path-expression-matcher@1.5.0: {}
path-key@3.1.1: {}
@@ -3925,6 +4353,14 @@ snapshots:
postgres@3.4.9: {}
+ pretty-format@27.5.1:
+ dependencies:
+ ansi-regex: 5.0.1
+ ansi-styles: 5.2.0
+ react-is: 17.0.2
+
+ punycode@2.3.1: {}
+
pvtsutils@1.3.6:
dependencies:
tslib: 2.8.1
@@ -3936,6 +4372,8 @@ snapshots:
react: 19.2.6
scheduler: 0.27.0
+ react-is@17.0.2: {}
+
react@19.2.6: {}
readable-stream@3.6.2:
@@ -3944,6 +4382,11 @@ snapshots:
string_decoder: 1.3.0
util-deprecate: 1.0.2
+ redent@3.0.0:
+ dependencies:
+ indent-string: 4.0.0
+ strip-indent: 3.0.0
+
reflect-metadata@0.2.2: {}
resolve-pkg-maps@1.0.0: {}
@@ -3992,8 +4435,18 @@ snapshots:
rou3@0.7.12: {}
+ rrweb-cssom@0.7.1: {}
+
+ rrweb-cssom@0.8.0: {}
+
safe-buffer@5.2.1: {}
+ safer-buffer@2.1.2: {}
+
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
scheduler@0.27.0: {}
semver@7.8.1: {}
@@ -4088,6 +4541,10 @@ snapshots:
dependencies:
ansi-regex: 6.2.2
+ strip-indent@3.0.0:
+ dependencies:
+ min-indent: 1.0.1
+
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
@@ -4105,6 +4562,8 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ symbol-tree@3.2.4: {}
+
teeny-request@9.0.0:
dependencies:
http-proxy-agent: 5.0.0
@@ -4143,8 +4602,16 @@ snapshots:
dependencies:
tldts-core: 6.1.86
+ tough-cookie@5.1.2:
+ dependencies:
+ tldts: 6.1.86
+
tr46@0.0.3: {}
+ tr46@5.1.1:
+ dependencies:
+ punycode: 2.3.1
+
tslib@1.14.1: {}
tslib@2.8.1: {}
@@ -4203,7 +4670,7 @@ snapshots:
fsevents: 2.3.3
tsx: 4.22.3
- vitest@3.2.4(@types/node@22.19.19)(tsx@4.22.3):
+ vitest@3.2.4(@types/node@22.19.19)(jsdom@25.0.1)(tsx@4.22.3):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
@@ -4230,6 +4697,7 @@ snapshots:
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.19.19
+ jsdom: 25.0.1
transitivePeerDependencies:
- jiti
- less
@@ -4244,8 +4712,25 @@ snapshots:
- tsx
- yaml
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
webidl-conversions@3.0.1: {}
+ webidl-conversions@7.0.0: {}
+
+ 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
+
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
@@ -4279,8 +4764,14 @@ snapshots:
wrappy@1.0.2: {}
+ ws@8.21.0: {}
+
+ xml-name-validator@5.0.0: {}
+
xml-naming@0.1.0: {}
+ xmlchars@2.2.0: {}
+
yocto-queue@0.1.0: {}
zod@4.4.3: {}
diff --git a/web/src/components/AuthForm.test.tsx b/web/src/components/AuthForm.test.tsx
new file mode 100644
index 0000000..5936737
--- /dev/null
+++ b/web/src/components/AuthForm.test.tsx
@@ -0,0 +1,71 @@
+// @vitest-environment jsdom
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+// Stub the boundaries (auth-client + router); assert OUR component behavior.
+const h = vi.hoisted(() => ({
+ push: vi.fn(),
+ refresh: vi.fn(),
+ signInEmail: vi.fn(),
+ signUpEmail: vi.fn(),
+ signInPasskey: vi.fn(),
+}));
+vi.mock("next/navigation", () => ({ useRouter: () => ({ push: h.push, refresh: h.refresh }) }));
+vi.mock("@/lib/auth-client", () => ({
+ signIn: { email: h.signInEmail, passkey: h.signInPasskey },
+ signUp: { email: h.signUpEmail },
+}));
+
+import { AuthForm } from "./AuthForm";
+
+beforeEach(() => {
+ Object.values(h).forEach((fn) => fn.mockReset());
+});
+
+describe("AuthForm", () => {
+ it("signs in with email+password and routes to the dashboard", async () => {
+ h.signInEmail.mockResolvedValue({});
+ render();
+ await userEvent.type(screen.getByLabelText("Email"), "a@b.com");
+ await userEvent.type(screen.getByLabelText("Password"), "pw123456");
+ await userEvent.click(screen.getByRole("button", { name: "Sign in" }));
+
+ expect(h.signInEmail).toHaveBeenCalledWith({ email: "a@b.com", password: "pw123456" });
+ expect(h.push).toHaveBeenCalledWith("/dashboard");
+ });
+
+ it("surfaces the server error and does NOT navigate on failure", async () => {
+ h.signInEmail.mockResolvedValue({ error: { message: "invalid credentials" } });
+ render();
+ await userEvent.type(screen.getByLabelText("Email"), "a@b.com");
+ await userEvent.type(screen.getByLabelText("Password"), "wrong");
+ await userEvent.click(screen.getByRole("button", { name: "Sign in" }));
+
+ expect(await screen.findByText("invalid credentials")).toBeInTheDocument();
+ expect(h.push).not.toHaveBeenCalled();
+ });
+
+ it("creates an account in signup mode", async () => {
+ h.signUpEmail.mockResolvedValue({});
+ render();
+ await userEvent.click(screen.getByText("new here? create an account"));
+ await userEvent.type(screen.getByLabelText("Email"), "new@b.com");
+ await userEvent.type(screen.getByLabelText("Password"), "pw123456");
+ await userEvent.click(screen.getByRole("button", { name: "Create account" }));
+
+ expect(h.signUpEmail).toHaveBeenCalledWith(
+ expect.objectContaining({ email: "new@b.com", password: "pw123456" }),
+ );
+ expect(h.signInEmail).not.toHaveBeenCalled();
+ });
+
+ it("signs in with a passkey", async () => {
+ h.signInPasskey.mockResolvedValue({});
+ render();
+ await userEvent.click(screen.getByRole("button", { name: "Sign in with a passkey" }));
+
+ expect(h.signInPasskey).toHaveBeenCalled();
+ expect(h.push).toHaveBeenCalledWith("/dashboard");
+ });
+});
diff --git a/web/src/components/Dashboard.test.tsx b/web/src/components/Dashboard.test.tsx
new file mode 100644
index 0000000..6c4c3ef
--- /dev/null
+++ b/web/src/components/Dashboard.test.tsx
@@ -0,0 +1,117 @@
+// @vitest-environment jsdom
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const h = vi.hoisted(() => ({
+ push: vi.fn(),
+ refresh: vi.fn(),
+ signOut: vi.fn(),
+ addPasskey: vi.fn(),
+}));
+vi.mock("next/navigation", () => ({ useRouter: () => ({ push: h.push, refresh: h.refresh }) }));
+vi.mock("@/lib/auth-client", () => ({ signOut: h.signOut, passkey: { addPasskey: h.addPasskey } }));
+
+import { Dashboard } from "./Dashboard";
+
+const apexDns = [
+ { type: "A", name: "x.com", value: "34.172.36.60", note: "required", optional: false },
+];
+
+function mockFetchOnce(status: number, body: unknown) {
+ (global.fetch as ReturnType).mockResolvedValueOnce({
+ ok: status >= 200 && status < 300,
+ status,
+ json: async () => body,
+ });
+}
+
+beforeEach(() => {
+ h.push.mockReset();
+ h.addPasskey.mockReset();
+ global.fetch = vi.fn();
+});
+
+describe("Dashboard", () => {
+ it("registers a domain (POST) and renders the new card", async () => {
+ render();
+ mockFetchOnce(201, {
+ domain: { id: "1", hostname: "x.com", destination: "https://y.com", dns: apexDns },
+ });
+
+ await userEvent.type(screen.getByLabelText("Custom domain"), "x.com");
+ await userEvent.type(screen.getByLabelText("Redirect destination"), "https://y.com");
+ await userEvent.click(screen.getByRole("button", { name: "Register domain" }));
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ "/api/domains",
+ expect.objectContaining({ method: "POST" }),
+ );
+ // Assert on the destination (unique to the card; the hostname also appears
+ // in the always-rendered DNS-sheet markup).
+ expect(await screen.findByText("https://y.com", { exact: false })).toBeInTheDocument();
+ });
+
+ it("surfaces the 'already claimed' error from the API", async () => {
+ render();
+ mockFetchOnce(409, { error: "this domain is already claimed by another account" });
+
+ await userEvent.type(screen.getByLabelText("Custom domain"), "x.com");
+ await userEvent.type(screen.getByLabelText("Redirect destination"), "https://y.com");
+ await userEvent.click(screen.getByRole("button", { name: "Register domain" }));
+
+ expect(await screen.findByText(/already claimed by another account/i)).toBeInTheDocument();
+ });
+
+ it("edits a destination in place (PUT) and shows the new value", async () => {
+ render(
+ ,
+ );
+ await userEvent.click(screen.getByRole("button", { name: "Edit destination" }));
+ const input = screen.getByDisplayValue("https://old.com"); // the card's edit input
+ await userEvent.clear(input);
+ await userEvent.type(input, "https://new.com");
+
+ mockFetchOnce(200, {
+ domain: { id: "1", hostname: "x.com", destination: "https://new.com", dns: apexDns },
+ });
+ await userEvent.click(screen.getByRole("button", { name: "Save" }));
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ "/api/domains/1",
+ expect.objectContaining({ method: "PUT" }),
+ );
+ expect(await screen.findByText("https://new.com", { exact: false })).toBeInTheDocument();
+ });
+
+ it("removes a domain (DELETE) and the card disappears", async () => {
+ render(
+ ,
+ );
+ mockFetchOnce(200, { ok: true });
+
+ await userEvent.click(screen.getByRole("button", { name: "Remove" }));
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ "/api/domains/1",
+ expect.objectContaining({ method: "DELETE" }),
+ );
+ await waitFor(() => expect(screen.queryByText("gone.com")).not.toBeInTheDocument());
+ });
+
+ it("adds a passkey and confirms", async () => {
+ render();
+ h.addPasskey.mockResolvedValue({});
+
+ await userEvent.click(screen.getByRole("button", { name: "Add a passkey" }));
+
+ expect(h.addPasskey).toHaveBeenCalled();
+ expect(await screen.findByText(/passkey added/i)).toBeInTheDocument();
+ });
+});
diff --git a/web/src/test-setup.ts b/web/src/test-setup.ts
new file mode 100644
index 0000000..b2583eb
--- /dev/null
+++ b/web/src/test-setup.ts
@@ -0,0 +1,6 @@
+import "@testing-library/jest-dom/vitest";
+import { cleanup } from "@testing-library/react";
+import { afterEach } from "vitest";
+
+// Unmount React trees between tests so the jsdom DOM doesn't leak across cases.
+afterEach(() => cleanup());
diff --git a/web/vitest.config.ts b/web/vitest.config.ts
index 3a8f986..1ef46e7 100644
--- a/web/vitest.config.ts
+++ b/web/vitest.config.ts
@@ -2,12 +2,16 @@ import path from "node:path";
import { defineConfig } from "vitest/config";
export default defineConfig({
+ // Use the automatic JSX runtime so .tsx (tests + components) don't need a
+ // React import, matching Next.js.
+ esbuild: { jsx: "automatic" },
resolve: {
alias: { "@": path.resolve(__dirname, "src") },
},
test: {
environment: "node",
- include: ["src/**/*.test.ts"],
+ include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
+ setupFiles: ["./src/test-setup.ts"],
env: {
// Integration tests run against the local dockerized Postgres and write
// the export to a temp file (never GCS).
@@ -19,7 +23,7 @@ export default defineConfig({
},
coverage: {
provider: "v8",
- include: ["src/lib/**", "src/app/api/**"],
+ include: ["src/lib/**", "src/app/api/**", "src/components/**"],
reportsDirectory: "./coverage",
thresholds: { lines: 70, functions: 70, statements: 70, branches: 60 },
},