mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
test: add a browser Vitest project for interactive components
Run *.browser.test.tsx in headless Chromium through the Playwright provider. The shared helper in tests/browser/ mounts the app's own providers, stubs only the fetch boundary, and shims next/navigation onto the page's real location and history.
This commit is contained in:
1 parent
3eb5aa9364
commit
3d1692204b
9 files changed
+736
-32
No files matched your search
+3
-1
@@ -120,6 +120,7 @@
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@vitest/browser-playwright": "^5.0.5",
|
||||
"@vitest/coverage-v8": "^5.0.5",
|
||||
"ajv": "^8.20.0",
|
||||
"eslint": "^9",
|
||||
@@ -136,6 +137,7 @@
|
||||
"testcontainers": "^12.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^6",
|
||||
"vitest": "^5.0.5"
|
||||
"vitest": "^5.0.5",
|
||||
"vitest-browser-react": "^2.3.0"
|
||||
}
|
||||
}
|
||||
Generated
+148
-7
@@ -241,9 +241,12 @@ importers:
|
||||
'@types/web-push':
|
||||
specifier: ^3.6.4
|
||||
version: 3.6.4
|
||||
'@vitest/browser-playwright':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(playwright@1.64.1)(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))(vitest@5.0.5)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5(vitest@5.0.5)
|
||||
version: 5.0.5(@vitest/browser@5.0.5)(vitest@5.0.5)
|
||||
ajv:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
@@ -291,7 +294,10 @@ importers:
|
||||
version: 6.0.3
|
||||
vitest:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
version: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/browser-playwright@5.0.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
vitest-browser-react:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -708,6 +714,9 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@blazediff/core@1.10.0':
|
||||
resolution: {integrity: sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==}
|
||||
|
||||
'@cfworker/json-schema@4.1.1':
|
||||
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
|
||||
|
||||
@@ -3407,6 +3416,17 @@ packages:
|
||||
'@visx/vendor@4.0.0-alpha.0':
|
||||
resolution: {integrity: sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==}
|
||||
|
||||
'@vitest/browser-playwright@5.0.5':
|
||||
resolution: {integrity: sha512-lcdThFUUcMV9mGGV1SUiZXPxfjgW4CssA7o3STxzh8WK0tT2r526kc12tZIBRZfoLMwkhxjLmMXkWXONzdsDQw==}
|
||||
peerDependencies:
|
||||
playwright: '*'
|
||||
vitest: 5.0.5
|
||||
|
||||
'@vitest/browser@5.0.5':
|
||||
resolution: {integrity: sha512-IDMoZX/iZ8rkilw7weaSgN8tJ7iZ9hUv0wYoKXyiyjWjWFwLSWAY707DcVTgxTYOudWC1E3HA9LYn7kVa9a7uQ==}
|
||||
peerDependencies:
|
||||
vitest: 5.0.5
|
||||
|
||||
'@vitest/coverage-v8@5.0.5':
|
||||
resolution: {integrity: sha512-4KpkGdIrSqBihx/JyP/e+S+IKB98etg1XDQiLAxksLfcShnwi1VReGeOT9NI43hLHbknosKp/1CCLn9Zgcs8mg==}
|
||||
peerDependencies:
|
||||
@@ -3435,9 +3455,20 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@5.0.5':
|
||||
resolution: {integrity: sha512-24sSdIlMA9DVPXnYiB9siwU0bTd887Y9jX3+vabztCSsvHwo7ipvJ/a2L3Rljw8+2rK1f5fCj3h5U9I/Jsqgbw==}
|
||||
|
||||
'@vitest/spy@5.0.5':
|
||||
resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==}
|
||||
|
||||
'@vitest/ui@5.0.5':
|
||||
resolution: {integrity: sha512-dCoGUOdgmCaSB2g+WQ/spPCBkytAWdNUkFSXY4z+eYp2d8JEOY8v0uDRaJTG9qf/kTlTVZ4CuYMYMz/f1HS/qQ==}
|
||||
peerDependencies:
|
||||
vitest: 5.0.5
|
||||
|
||||
'@vitest/utils@5.0.5':
|
||||
resolution: {integrity: sha512-/rBu7gzLDQI/SC79Tn98b5DwOgaHU/Gt4I9OVeP3YofzNaYtbImRCOJQmT1S6vOJg9gEIlWC+r3QxKkjuSa1nQ==}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
@@ -5981,6 +6012,10 @@ packages:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
pngjs@7.0.0:
|
||||
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
|
||||
engines: {node: '>=14.19.0'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6529,6 +6564,10 @@ packages:
|
||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
sirv@3.0.2:
|
||||
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
@@ -7065,6 +7104,20 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest-browser-react@2.3.0:
|
||||
resolution: {integrity: sha512-S5vdGHHA93j2Z6dbmIJI1wtxT3AXPTRlmC1Meo+ekG011yNnEutMAnHrwyj8qFlGhgBYA8ENGK6+o1Q+vnFz6A==}
|
||||
peerDependencies:
|
||||
'@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
|
||||
vitest: ^4.0.0 || ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
vitest@5.0.5:
|
||||
resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==}
|
||||
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
|
||||
@@ -7197,6 +7250,18 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
ws@8.22.0:
|
||||
resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==}
|
||||
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
|
||||
|
||||
wsl-utils@1.0.1:
|
||||
resolution: {integrity: sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -7885,6 +7950,8 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@blazediff/core@1.10.0': {}
|
||||
|
||||
'@cfworker/json-schema@4.1.1':
|
||||
optional: true
|
||||
|
||||
@@ -10055,7 +10122,7 @@ snapshots:
|
||||
'@stryker-mutator/util': 10.0.0
|
||||
semver: 7.8.5
|
||||
tslib: 2.8.1
|
||||
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/browser-playwright@5.0.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
|
||||
'@swc/helpers@0.5.23':
|
||||
dependencies:
|
||||
@@ -10562,7 +10629,38 @@ snapshots:
|
||||
d3-time-format: 4.1.0
|
||||
internmap: 2.0.3
|
||||
|
||||
'@vitest/coverage-v8@5.0.5(vitest@5.0.5)':
|
||||
'@vitest/browser-playwright@5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(playwright@1.64.1)(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))(vitest@5.0.5)':
|
||||
dependencies:
|
||||
'@vitest/browser': 5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))(vitest@5.0.5)
|
||||
'@vitest/mocker': 5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
playwright: 1.64.1
|
||||
tinyrainbow: 3.2.0
|
||||
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/browser-playwright@5.0.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@vitest/browser@5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))(vitest@5.0.5)':
|
||||
dependencies:
|
||||
'@blazediff/core': 1.10.0
|
||||
'@vitest/mocker': 5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
'@vitest/ui': 5.0.5(vitest@5.0.5)
|
||||
'@vitest/utils': 5.0.5
|
||||
magic-string: 1.4.3
|
||||
pngjs: 7.0.0
|
||||
sirv: 3.0.2
|
||||
tinyrainbow: 3.2.0
|
||||
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/browser-playwright@5.0.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
ws: 8.22.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@vitest/coverage-v8@5.0.5(@vitest/browser@5.0.5)(vitest@5.0.5)':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/istanbul-lib-coverage': 1.0.2
|
||||
@@ -10572,7 +10670,9 @@ snapshots:
|
||||
obug: 3.0.0
|
||||
std-env: 4.3.0
|
||||
tinyrainbow: 3.2.0
|
||||
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/browser-playwright@5.0.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
optionalDependencies:
|
||||
'@vitest/browser': 5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))(vitest@5.0.5)
|
||||
|
||||
'@vitest/istanbul-lib-coverage@1.0.2': {}
|
||||
|
||||
@@ -10590,8 +10690,28 @@ snapshots:
|
||||
msw: 2.14.6(@types/node@26.6.5)(typescript@6.0.3)
|
||||
vite: 8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1)
|
||||
|
||||
'@vitest/pretty-format@5.0.5':
|
||||
dependencies:
|
||||
tinyrainbow: 3.2.0
|
||||
|
||||
'@vitest/spy@5.0.5': {}
|
||||
|
||||
'@vitest/ui@5.0.5(vitest@5.0.5)':
|
||||
dependencies:
|
||||
'@vitest/utils': 5.0.5
|
||||
fflate: 0.8.3
|
||||
flatted: 3.4.4
|
||||
pathe: 2.0.3
|
||||
sirv: 3.0.2
|
||||
tinyrainbow: 3.2.0
|
||||
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/browser-playwright@5.0.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
|
||||
'@vitest/utils@5.0.5':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 5.0.5
|
||||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.2.0
|
||||
|
||||
abort-controller@3.0.0:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
@@ -13216,6 +13336,8 @@ snapshots:
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
pngjs@7.0.0: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-selector-parser@7.1.6:
|
||||
@@ -13897,6 +14019,12 @@ snapshots:
|
||||
mrmime: 2.0.1
|
||||
totalist: 3.0.1
|
||||
|
||||
sirv@3.0.2:
|
||||
dependencies:
|
||||
'@polka/url': 1.0.0-next.29
|
||||
mrmime: 2.0.1
|
||||
totalist: 3.0.1
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
smart-buffer@4.2.0: {}
|
||||
@@ -14483,7 +14611,16 @@ snapshots:
|
||||
jiti: 2.7.0
|
||||
yaml: 2.9.1
|
||||
|
||||
vitest@5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1)):
|
||||
vitest-browser-react@2.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5):
|
||||
dependencies:
|
||||
react: 19.3.1
|
||||
react-dom: 19.3.1(react@19.3.1)
|
||||
vitest: 5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/browser-playwright@5.0.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
optionalDependencies:
|
||||
'@types/react': 19.3.0
|
||||
'@types/react-dom': 19.3.0(@types/react@19.3.0)
|
||||
|
||||
vitest@5.0.5(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/browser-playwright@5.0.5)(@vitest/coverage-v8@5.0.5)(@vitest/ui@5.0.5)(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1)):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/mocker': 5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))
|
||||
@@ -14502,7 +14639,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
'@types/node': 26.6.5
|
||||
'@vitest/coverage-v8': 5.0.5(vitest@5.0.5)
|
||||
'@vitest/browser-playwright': 5.0.5(msw@2.14.6(@types/node@26.6.5)(typescript@6.0.3))(playwright@1.64.1)(vite@8.1.6(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.1))(vitest@5.0.5)
|
||||
'@vitest/coverage-v8': 5.0.5(@vitest/browser@5.0.5)(vitest@5.0.5)
|
||||
'@vitest/ui': 5.0.5(vitest@5.0.5)
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
@@ -14625,6 +14764,8 @@ snapshots:
|
||||
|
||||
ws@7.5.13: {}
|
||||
|
||||
ws@8.22.0: {}
|
||||
|
||||
wsl-utils@1.0.1:
|
||||
dependencies:
|
||||
is-wsl: 3.1.1
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { userEvent } from "vitest/browser";
|
||||
|
||||
import { renderWithApp } from "../../../../tests/browser/render";
|
||||
import { Segmented } from "../segmented";
|
||||
|
||||
const OPTIONS = [
|
||||
{ value: "day", label: "Day" },
|
||||
{ value: "week", label: "Week" },
|
||||
{ value: "month", label: "Month" },
|
||||
] as const;
|
||||
|
||||
describe("<Segmented> smoke", () => {
|
||||
it("moves with the arrow key", async () => {
|
||||
const onChange = vi.fn();
|
||||
const { screen } = await renderWithApp(
|
||||
<Segmented
|
||||
options={OPTIONS}
|
||||
value="day"
|
||||
onChange={onChange}
|
||||
label="Zoom"
|
||||
/>,
|
||||
);
|
||||
await screen.getByRole("radio", { name: "Day" }).click();
|
||||
await userEvent.keyboard("{ArrowRight}");
|
||||
expect(onChange).toHaveBeenLastCalledWith("week");
|
||||
await expect
|
||||
.element(screen.getByRole("radio", { name: "Week" }))
|
||||
.toHaveFocus();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* The network boundary for browser tests.
|
||||
*
|
||||
* Components talk to the server through `@/lib/api/api-fetch`, which calls
|
||||
* the global `fetch`. Replacing `fetch` and nothing else keeps everything
|
||||
* above it real: the envelope unwrapping, the idempotency header, TanStack
|
||||
* Query's caching and retries, the component's own pending and error states.
|
||||
* Never mock `@tanstack/react-query` or the API wrapper in this project; a
|
||||
* test that does asserts its own stand-in.
|
||||
*
|
||||
* Every request is recorded with its parsed JSON body, so a form test asserts
|
||||
* the payload the server would receive. A route can answer later than the
|
||||
* request (`deferred`), which is how a test holds a form in its pending
|
||||
* state. A request no route matches fails with a 404 envelope and is recorded
|
||||
* too, so an unexpected call is visible rather than hanging.
|
||||
*
|
||||
* `setup.ts` restores the real `fetch` after every test.
|
||||
*/
|
||||
export interface RecordedRequest {
|
||||
method: string;
|
||||
/** Path and query, without the origin. */
|
||||
path: string;
|
||||
headers: Headers;
|
||||
/** The parsed JSON body, or the raw text when it is not JSON. */
|
||||
body: unknown;
|
||||
}
|
||||
|
||||
export interface StubResponse {
|
||||
status?: number;
|
||||
/** The envelope's `data`. Ignored when `error` is set. */
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
type Responder = (
|
||||
request: RecordedRequest,
|
||||
) => StubResponse | Promise<StubResponse>;
|
||||
|
||||
interface Route {
|
||||
method: string;
|
||||
path: string | RegExp;
|
||||
respond: Responder;
|
||||
}
|
||||
|
||||
export interface FetchStub {
|
||||
/** Answer `method path` (exact path, or a pattern) with `response`. */
|
||||
on(
|
||||
method: string,
|
||||
path: string | RegExp,
|
||||
response: StubResponse | Responder,
|
||||
): FetchStub;
|
||||
/**
|
||||
* Answer `method path` only when `release` is called, for asserting the
|
||||
* pending state in between.
|
||||
*/
|
||||
deferred(
|
||||
method: string,
|
||||
path: string | RegExp,
|
||||
): { release: (response?: StubResponse) => void };
|
||||
/** Every request so far, in order. */
|
||||
requests: RecordedRequest[];
|
||||
/** The requests that hit `method path`. */
|
||||
calls(method: string, path: string | RegExp): RecordedRequest[];
|
||||
}
|
||||
|
||||
let realFetch: typeof fetch | null = null;
|
||||
|
||||
function matches(route: Route, method: string, path: string): boolean {
|
||||
if (route.method !== method) return false;
|
||||
const bare = path.split("?")[0];
|
||||
return typeof route.path === "string"
|
||||
? route.path === bare
|
||||
: route.path.test(path);
|
||||
}
|
||||
|
||||
function envelope({ status = 200, data = null, error }: StubResponse) {
|
||||
const body = error ? { data: null, error } : { data, error: null };
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: error && status === 200 ? 400 : status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
async function record(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
): Promise<RecordedRequest> {
|
||||
const request = new Request(input, init);
|
||||
const url = new URL(request.url, window.location.href);
|
||||
const text = request.method === "GET" ? "" : await request.text();
|
||||
let body: unknown = text === "" ? undefined : text;
|
||||
try {
|
||||
if (text !== "") body = JSON.parse(text);
|
||||
} catch {
|
||||
// Keep the raw text.
|
||||
}
|
||||
return {
|
||||
method: request.method,
|
||||
path: `${url.pathname}${url.search}`,
|
||||
headers: request.headers,
|
||||
body,
|
||||
};
|
||||
}
|
||||
|
||||
/** Replace `fetch` for the rest of the test. */
|
||||
export function stubFetch(): FetchStub {
|
||||
realFetch ??= window.fetch;
|
||||
const routes: Route[] = [];
|
||||
const requests: RecordedRequest[] = [];
|
||||
|
||||
window.fetch = async (input, init) => {
|
||||
const request = await record(input, init);
|
||||
requests.push(request);
|
||||
// The newest route wins, so a test can override a default it set up.
|
||||
const route = [...routes]
|
||||
.reverse()
|
||||
.find((r) => matches(r, request.method, request.path));
|
||||
if (!route) {
|
||||
return envelope({
|
||||
status: 404,
|
||||
error: `no stubbed route for ${request.method} ${request.path}`,
|
||||
});
|
||||
}
|
||||
return envelope(await route.respond(request));
|
||||
};
|
||||
|
||||
const stub: FetchStub = {
|
||||
requests,
|
||||
on(method, path, response) {
|
||||
routes.push({
|
||||
method,
|
||||
path,
|
||||
respond: typeof response === "function" ? response : () => response,
|
||||
});
|
||||
return stub;
|
||||
},
|
||||
deferred(method, path) {
|
||||
let release: (response?: StubResponse) => void = () => {};
|
||||
const answer = new Promise<StubResponse>((resolve) => {
|
||||
release = (response = {}) => resolve(response);
|
||||
});
|
||||
routes.push({ method, path, respond: () => answer });
|
||||
return { release: (response) => release(response) };
|
||||
},
|
||||
calls(method, path) {
|
||||
return requests.filter((r) =>
|
||||
matches({ method, path, respond: () => ({}) }, r.method, r.path),
|
||||
);
|
||||
},
|
||||
};
|
||||
return stub;
|
||||
}
|
||||
|
||||
/** Called by `setup.ts` after each test. */
|
||||
export function restoreFetch(): void {
|
||||
if (realFetch) window.fetch = realFetch;
|
||||
realFetch = null;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* `next/navigation` for the browser project, aliased in `vitest.config.mts`.
|
||||
*
|
||||
* The App Router's hooks throw unless a router is mounted, and mounting one
|
||||
* means booting Next. What a component needs from them is smaller: the
|
||||
* current path and query, and a way to change them. This module answers both
|
||||
* from the page's real `location` and `history`, so `@/lib/url-state` writes
|
||||
* through the same `pushState` / `replaceState` it uses in the app.
|
||||
*
|
||||
* `useSearchParams` follows the address bar the way the router does: it
|
||||
* re-reads on `popstate` and on the URL-state event, one task after the write
|
||||
* rather than synchronously, which is the "adopted a render later" gap the
|
||||
* helper's queue exists for. A test that only asserts what the page shows
|
||||
* never sees the gap; one that writes twice in a row exercises it.
|
||||
*
|
||||
* `redirect` and `notFound` throw like Next's do, so a component that calls
|
||||
* one fails its test loudly instead of rendering past it.
|
||||
*/
|
||||
import { useMemo, useSyncExternalStore } from "react";
|
||||
|
||||
import { URL_STATE_EVENT } from "@/lib/url-state";
|
||||
|
||||
function subscribe(onChange: () => void): () => void {
|
||||
// The router adopts a write after the event loop turns, not inside the
|
||||
// dispatch that announced it.
|
||||
const deferred = () => setTimeout(onChange, 0);
|
||||
window.addEventListener("popstate", deferred);
|
||||
window.addEventListener(URL_STATE_EVENT, deferred);
|
||||
return () => {
|
||||
window.removeEventListener("popstate", deferred);
|
||||
window.removeEventListener(URL_STATE_EVENT, deferred);
|
||||
};
|
||||
}
|
||||
|
||||
const readSearch = () => window.location.search;
|
||||
const readPathname = () => window.location.pathname;
|
||||
|
||||
export function useSearchParams(): URLSearchParams {
|
||||
const search = useSyncExternalStore(subscribe, readSearch, readSearch);
|
||||
return useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
export function usePathname(): string {
|
||||
return useSyncExternalStore(subscribe, readPathname, readPathname);
|
||||
}
|
||||
|
||||
function navigate(mode: "push" | "replace", href: string): void {
|
||||
const url = new URL(href, window.location.href);
|
||||
if (mode === "push") window.history.pushState(null, "", url);
|
||||
else window.history.replaceState(null, "", url);
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
}
|
||||
|
||||
const router = {
|
||||
push: (href: string) => navigate("push", href),
|
||||
replace: (href: string) => navigate("replace", href),
|
||||
back: () => window.history.back(),
|
||||
forward: () => window.history.forward(),
|
||||
refresh: () => {},
|
||||
prefetch: () => {},
|
||||
};
|
||||
|
||||
export function useRouter(): typeof router {
|
||||
return router;
|
||||
}
|
||||
|
||||
export function useParams(): Record<string, string> {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function useSelectedLayoutSegment(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useSelectedLayoutSegments(): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export function redirect(href: string): never {
|
||||
throw new Error(`redirect(${href}) called in a browser test`);
|
||||
}
|
||||
|
||||
export function permanentRedirect(href: string): never {
|
||||
throw new Error(`permanentRedirect(${href}) called in a browser test`);
|
||||
}
|
||||
|
||||
export function notFound(): never {
|
||||
throw new Error("notFound() called in a browser test");
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Render a component the way the app mounts it, in a real browser.
|
||||
*
|
||||
* The providers are the app's own: `I18nProvider` with a real catalog and a
|
||||
* real TanStack `QueryClient`. The signed-in account is seeded into that
|
||||
* client under `queryKeys.authMe()`, exactly where `useAuth` reads it once
|
||||
* `/api/auth/me` has answered, so a component that asks for the account gets
|
||||
* it without a request. Everything else a component fetches goes through the
|
||||
* network and is answered by `stubFetch()` from `./fetch-stub`.
|
||||
*
|
||||
* The app stylesheet is loaded by `setup.ts` for every browser test, so
|
||||
* `hidden`, `pointer-events-none`, popper placement and the breakpoints lay
|
||||
* out as they do in the app.
|
||||
*
|
||||
* const { screen, queryClient } = await renderWithApp(<MeasurementForm />, {
|
||||
* user: { unitPreference: "imperial" },
|
||||
* });
|
||||
* await screen.getByLabelText("Value (lb)").fill("210");
|
||||
*/
|
||||
import type { ReactNode } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, type RenderResult } from "vitest-browser-react";
|
||||
|
||||
import type { AuthUser } from "@/hooks/use-auth";
|
||||
import type { Locale } from "@/lib/i18n/config";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { loadMessages } from "@/lib/i18n/load-locale";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
/** A signed-in account with every preference at its default. */
|
||||
export function authUser(overrides: Partial<AuthUser> = {}): AuthUser {
|
||||
return {
|
||||
id: "user-1",
|
||||
username: "user",
|
||||
email: null,
|
||||
role: "USER",
|
||||
heightCm: null,
|
||||
dateOfBirth: null,
|
||||
gender: null,
|
||||
timezone: "UTC",
|
||||
onboardingCompletedAt: "2026-01-01T00:00:00.000Z",
|
||||
onboardingTourCompleted: true,
|
||||
onboardingTourProgress: null,
|
||||
avatarUrl: null,
|
||||
glucoseUnit: null,
|
||||
unitPreference: "metric",
|
||||
timeFormat: "AUTO",
|
||||
dateFormat: "AUTO",
|
||||
disableCoach: false,
|
||||
fullName: null,
|
||||
insurerName: null,
|
||||
insurerIkNumber: null,
|
||||
insuranceNumber: null,
|
||||
lastReportPracticeName: null,
|
||||
reportSelection: null,
|
||||
cycleTrackingEnabled: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export interface RenderWithAppOptions {
|
||||
/** The reader's language. Its catalog is loaded before the first render. */
|
||||
locale?: Locale;
|
||||
/**
|
||||
* The signed-in account, merged over `authUser()`. `null` leaves the
|
||||
* account unanswered, as before `/api/auth/me` returns.
|
||||
*/
|
||||
user?: Partial<AuthUser> | null;
|
||||
/** Bring a client to seed more reads before rendering. */
|
||||
queryClient?: QueryClient;
|
||||
}
|
||||
|
||||
export interface AppRender {
|
||||
screen: RenderResult;
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
|
||||
/** A client configured like a test should want it: no retries, no refetch. */
|
||||
export function testQueryClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, refetchOnWindowFocus: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function renderWithApp(
|
||||
ui: ReactNode,
|
||||
{ locale = "en", user = {}, queryClient }: RenderWithAppOptions = {},
|
||||
): Promise<AppRender> {
|
||||
const client = queryClient ?? testQueryClient();
|
||||
if (user !== null) client.setQueryData(queryKeys.authMe(), authUser(user));
|
||||
if (locale !== "en") await loadMessages(locale);
|
||||
const screen = await render(
|
||||
<QueryClientProvider client={client}>
|
||||
<I18nProvider initialLocale={locale}>{ui}</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return { screen, queryClient: client };
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Setup for the `browser` project (`*.browser.test.tsx`, headless Chromium).
|
||||
*
|
||||
* - The app stylesheet, compiled by Vite through the project's PostCSS
|
||||
* config, so components lay out and hide as they do in the app.
|
||||
* - The EN catalog, the fallback floor of every `t()` chain, loaded through
|
||||
* the production loader. `renderWithApp` loads any other locale it is
|
||||
* asked for.
|
||||
* - After every test: the real `fetch` and the page's starting address come
|
||||
* back, so a stub or a URL write never reaches the next test. Rendered
|
||||
* trees are unmounted by `vitest-browser-react` itself.
|
||||
*/
|
||||
import "@/app/globals.css";
|
||||
|
||||
import { afterEach, beforeAll, beforeEach } from "vitest";
|
||||
|
||||
import { loadMessages } from "@/lib/i18n/load-locale";
|
||||
|
||||
import { restoreFetch } from "./fetch-stub";
|
||||
import { rememberAddress, restoreAddress } from "./url";
|
||||
|
||||
beforeAll(async () => {
|
||||
await loadMessages("en");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
rememberAddress();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreFetch();
|
||||
restoreAddress();
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* The page's address, for URL-state tests in the browser project.
|
||||
*
|
||||
* Each test file runs in its own page, and `setup.ts` puts the address the
|
||||
* page started on back after every test, so a test may write the URL freely.
|
||||
* Only the query and hash are ever changed: the path is the test runner's
|
||||
* own, and a component that writes state keeps the path it found.
|
||||
*/
|
||||
import { resetUrlStateForTests } from "@/lib/url-state";
|
||||
|
||||
export interface HistoryRecord {
|
||||
/** Every `pushState` and `replaceState` since `recordHistory()`, in order. */
|
||||
writes: Array<{ mode: "push" | "replace"; search: string }>;
|
||||
pushes: () => string[];
|
||||
replaces: () => string[];
|
||||
}
|
||||
|
||||
let restoreHistory: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Record history writes without changing them: the real methods still run,
|
||||
* so the page's location moves exactly as it would in the app.
|
||||
*/
|
||||
export function recordHistory(): HistoryRecord {
|
||||
restoreHistory?.();
|
||||
const { history } = window;
|
||||
const push = history.pushState.bind(history);
|
||||
const replace = history.replaceState.bind(history);
|
||||
const writes: HistoryRecord["writes"] = [];
|
||||
const searchOf = (url: string | URL | null | undefined) =>
|
||||
url == null
|
||||
? window.location.search
|
||||
: new URL(url, window.location.href).search;
|
||||
history.pushState = (data, unused, url) => {
|
||||
push(data, unused, url);
|
||||
writes.push({ mode: "push", search: searchOf(url) });
|
||||
};
|
||||
history.replaceState = (data, unused, url) => {
|
||||
replace(data, unused, url);
|
||||
writes.push({ mode: "replace", search: searchOf(url) });
|
||||
};
|
||||
restoreHistory = () => {
|
||||
history.pushState = push;
|
||||
history.replaceState = replace;
|
||||
restoreHistory = null;
|
||||
};
|
||||
return {
|
||||
writes,
|
||||
pushes: () => writes.filter((w) => w.mode === "push").map((w) => w.search),
|
||||
replaces: () =>
|
||||
writes.filter((w) => w.mode === "replace").map((w) => w.search),
|
||||
};
|
||||
}
|
||||
|
||||
/** Start the test on `search` (`""` for a bare path). */
|
||||
export function setSearch(search: string): void {
|
||||
const query = search === "" || search.startsWith("?") ? search : `?${search}`;
|
||||
window.history.replaceState(null, "", `${window.location.pathname}${query}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the query the way the person would from outside the component:
|
||||
* Back, Forward or an edited address. The router hears `popstate`.
|
||||
*/
|
||||
export function navigateExternally(search: string): void {
|
||||
setSearch(search);
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
}
|
||||
|
||||
/** The current query string, `?`-prefixed or empty. */
|
||||
export function currentSearch(): string {
|
||||
return window.location.search;
|
||||
}
|
||||
|
||||
let initialHref: string | null = null;
|
||||
|
||||
/** Called by `setup.ts` before each test. */
|
||||
export function rememberAddress(): void {
|
||||
initialHref = window.location.href;
|
||||
}
|
||||
|
||||
/** Called by `setup.ts` after each test. */
|
||||
export function restoreAddress(): void {
|
||||
restoreHistory?.();
|
||||
resetUrlStateForTests();
|
||||
if (initialHref !== null && window.location.href !== initialHref) {
|
||||
window.history.replaceState(null, "", initialHref);
|
||||
}
|
||||
}
|
||||
+84
-24
@@ -1,3 +1,4 @@
|
||||
import { playwright } from "@vitest/browser-playwright";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { globSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
@@ -10,12 +11,17 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
/**
|
||||
* Three projects, decided per file from its source (`tests/unit-projects.ts`):
|
||||
* `guards` and `unit` share a module graph inside a worker (`isolate:
|
||||
* false`), because their files neither mock nor stub anything; `isolated`
|
||||
* keeps one fresh graph per file for the ones that do. Run one with
|
||||
* `pnpm test --project guards`.
|
||||
* Four projects. Three run in Node and are decided per file from its source
|
||||
* (`tests/unit-projects.ts`): `guards` and `unit` share a module graph inside
|
||||
* a worker (`isolate: false`), because their files neither mock nor stub
|
||||
* anything; `isolated` keeps one fresh graph per file for the ones that do.
|
||||
* The fourth, `browser`, runs `*.browser.test.tsx` in headless Chromium for
|
||||
* components whose behaviour is interaction (see `tests/browser/`). Run one
|
||||
* with `pnpm test --project guards`.
|
||||
*/
|
||||
/** Files the `browser` project owns; no Node project collects them. */
|
||||
const BROWSER_TEST = /\.browser\.test\.tsx$/;
|
||||
|
||||
const projectFiles = classifyUnitFiles(
|
||||
__dirname,
|
||||
globSync(
|
||||
@@ -28,28 +34,34 @@ const projectFiles = classifyUnitFiles(
|
||||
{
|
||||
cwd: __dirname,
|
||||
exclude: (path) =>
|
||||
path.startsWith("tests/integration") || path.includes("node_modules"),
|
||||
path.startsWith("tests/integration") ||
|
||||
path.includes("node_modules") ||
|
||||
BROWSER_TEST.test(path),
|
||||
},
|
||||
).sort(),
|
||||
);
|
||||
|
||||
/** What the three Node projects share; the browser project sets its own. */
|
||||
const nodeTest = {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
// Pin the host timezone. CI runs UTC; without this a contributor in any
|
||||
// other zone sees a different suite than the gate does — verified: a
|
||||
// UTC+14 host fails five tests on an otherwise green tree. Pinning makes
|
||||
// local and CI agree, and makes "which zone does this test mean?" an
|
||||
// explicit choice the fixture has to state rather than a property of the
|
||||
// machine it runs on. Tests that need a different HOST zone set
|
||||
// `process.env.TZ` themselves and restore it afterwards (see
|
||||
// `next-due-day-label.test.ts`).
|
||||
env: { TZ: "UTC" },
|
||||
// Primes the i18n locale cache so provider mounts resolve every
|
||||
// locale synchronously (production gets the active bundle as an RSC
|
||||
// prop instead — see src/lib/i18n/load-locale.ts).
|
||||
setupFiles: ["./vitest.setup.ts"],
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
// Pin the host timezone. CI runs UTC; without this a contributor in any
|
||||
// other zone sees a different suite than the gate does — verified: a
|
||||
// UTC+14 host fails five tests on an otherwise green tree. Pinning makes
|
||||
// local and CI agree, and makes "which zone does this test mean?" an
|
||||
// explicit choice the fixture has to state rather than a property of the
|
||||
// machine it runs on. Tests that need a different HOST zone set
|
||||
// `process.env.TZ` themselves and restore it afterwards (see
|
||||
// `next-due-day-label.test.ts`).
|
||||
env: { TZ: "UTC" },
|
||||
// Primes the i18n locale cache so provider mounts resolve every
|
||||
// locale synchronously (production gets the active bundle as an RSC
|
||||
// prop instead — see src/lib/i18n/load-locale.ts).
|
||||
setupFiles: ["./vitest.setup.ts"],
|
||||
// `--shard` splits by measured duration rather than file count, and files
|
||||
// start longest first. See `tests/durations/sequencer.ts`.
|
||||
sequence: {
|
||||
@@ -77,15 +89,63 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: { name: "guards", include: projectFiles.guards, isolate: false },
|
||||
test: {
|
||||
...nodeTest,
|
||||
name: "guards",
|
||||
include: projectFiles.guards,
|
||||
isolate: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: { name: "unit", include: projectFiles.shared, isolate: false },
|
||||
test: {
|
||||
...nodeTest,
|
||||
name: "unit",
|
||||
include: projectFiles.shared,
|
||||
isolate: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: true,
|
||||
test: { name: "isolated", include: projectFiles.isolated },
|
||||
test: {
|
||||
...nodeTest,
|
||||
name: "isolated",
|
||||
include: projectFiles.isolated,
|
||||
},
|
||||
},
|
||||
// Not `extends: true` and none of `nodeTest`: the Node setup file reads
|
||||
// the file system and mocks the write meter. Each file gets its own
|
||||
// page, so a stubbed `fetch` or a rewritten URL cannot leak into the
|
||||
// next file.
|
||||
{
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
// The App Router's hooks throw outside a mounted router. The shim
|
||||
// reads and writes the page's real location instead.
|
||||
"next/navigation": resolve(
|
||||
__dirname,
|
||||
"tests/browser/next-navigation.ts",
|
||||
),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
name: "browser",
|
||||
include: ["src/**/*.browser.test.tsx"],
|
||||
globals: true,
|
||||
setupFiles: ["./tests/browser/setup.ts"],
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
// Pinned like the Node projects' `TZ`, so a date the form
|
||||
// derives from the clock means the same instant everywhere.
|
||||
provider: playwright({
|
||||
contextOptions: { timezoneId: "UTC", locale: "en-US" },
|
||||
}),
|
||||
instances: [{ browser: "chromium" }],
|
||||
screenshotFailures: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
coverage: {
|
||||
|
||||
Reference in new issue
Block a user