mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
test: initial frontend testing setup
This commit is contained in:
472
pnpm-lock.yaml
generated
472
pnpm-lock.yaml
generated
@@ -80,7 +80,7 @@ importers:
|
||||
version: 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2))
|
||||
esbuild:
|
||||
specifier: ^0.21.5
|
||||
version: 0.21.5
|
||||
@@ -146,7 +146,7 @@ importers:
|
||||
version: 8.46.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2)
|
||||
|
||||
server:
|
||||
dependencies:
|
||||
@@ -375,7 +375,7 @@ importers:
|
||||
version: 17.0.33
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2))
|
||||
'@yao-pkg/pkg':
|
||||
specifier: ^6.9.0
|
||||
version: 6.9.0
|
||||
@@ -456,7 +456,7 @@ importers:
|
||||
version: 8.46.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2)
|
||||
|
||||
shared:
|
||||
dependencies:
|
||||
@@ -496,7 +496,7 @@ importers:
|
||||
version: 22.10.7
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2))
|
||||
rimraf:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5
|
||||
@@ -514,7 +514,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2)
|
||||
|
||||
types:
|
||||
dependencies:
|
||||
@@ -706,6 +706,15 @@ importers:
|
||||
'@tanstack/router-vite-plugin':
|
||||
specifier: ^1.133.13
|
||||
version: 1.133.13(@tanstack/react-router@1.133.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(vite@7.1.10(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1
|
||||
'@testing-library/react':
|
||||
specifier: ^16.3.2
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.6.1
|
||||
version: 14.6.1(@testing-library/dom@10.4.1)
|
||||
'@types/lodash-es':
|
||||
specifier: 4.17.9
|
||||
version: 4.17.9
|
||||
@@ -745,6 +754,9 @@ importers:
|
||||
eslint-plugin-react-refresh:
|
||||
specifier: ^0.4.16
|
||||
version: 0.4.16(eslint@9.39.2(jiti@2.6.1))
|
||||
jsdom:
|
||||
specifier: ^28.0.0
|
||||
version: 28.0.0(@noble/hashes@1.8.0)
|
||||
make-vfs:
|
||||
specifier: ^1.0.15
|
||||
version: 1.0.15
|
||||
@@ -768,7 +780,7 @@ importers:
|
||||
version: 4.5.0(rollup@4.52.5)(typescript@5.9.3)(vite@7.1.10(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2))
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -776,6 +788,9 @@ packages:
|
||||
resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
'@acemir/cssom@0.9.31':
|
||||
resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
|
||||
|
||||
'@actions/core@2.0.1':
|
||||
resolution: {integrity: sha512-oBfqT3GwkvLlo1fjvhQLQxuwZCGTarTE5OuZ2Wg10hvhBj7LRIlF611WT4aZS6fDhO5ZKlY7lCAZTlpmyaHaeg==}
|
||||
|
||||
@@ -788,6 +803,9 @@ packages:
|
||||
'@actions/io@2.0.0':
|
||||
resolution: {integrity: sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==}
|
||||
|
||||
'@adobe/css-tools@4.4.4':
|
||||
resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -807,6 +825,15 @@ packages:
|
||||
peerDependencies:
|
||||
openapi-types: '>=7'
|
||||
|
||||
'@asamuzakjp/css-color@4.1.2':
|
||||
resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==}
|
||||
|
||||
'@asamuzakjp/dom-selector@6.7.8':
|
||||
resolution: {integrity: sha512-stisC1nULNc9oH5lakAj8MH88ZxeGxzyWNDfbdCxvJSJIvDsHNZqYvscGTgy/ysgXWLJPt6K/4t0/GjvtKcFJQ==}
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9':
|
||||
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
|
||||
|
||||
'@babel/code-frame@7.24.7':
|
||||
resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1154,6 +1181,37 @@ packages:
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@csstools/color-helpers@6.0.1':
|
||||
resolution: {integrity: sha512-NmXRccUJMk2AWA5A7e5a//3bCIMyOu2hAtdRYrhPPHjDxINuCwX1w6rnIZ4xjLcp0ayv6h8Pc3X0eJUGiAAXHQ==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@csstools/css-calc@3.0.0':
|
||||
resolution: {integrity: sha512-q4d82GTl8BIlh/dTnVsWmxnbWJeb3kiU8eUH71UxlxnS+WIaALmtzTL8gR15PkYOexMQYVk0CO4qIG93C1IvPA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^4.0.0
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-color-parser@4.0.1':
|
||||
resolution: {integrity: sha512-vYwO15eRBEkeF6xjAno/KQ61HacNhfQuuU/eGwH67DplL0zD5ZixUa563phQvUelA07yDczIXdtmYojCphKJcw==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-parser-algorithms': ^4.0.0
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-parser-algorithms@4.0.0':
|
||||
resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
peerDependencies:
|
||||
'@csstools/css-tokenizer': ^4.0.0
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.0.26':
|
||||
resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==}
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0':
|
||||
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
'@dotenvx/dotenvx@1.45.1':
|
||||
resolution: {integrity: sha512-wKHPD+/NMMJVBPg3i98uD9jsURDy+Ck6RQRiWf39TlOAzC+Ge1FkmDk3sgeljYZxA3qF6E7SJmvRqC70XQuuVA==}
|
||||
hasBin: true
|
||||
@@ -1862,6 +1920,15 @@ packages:
|
||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
'@exodus/bytes@1.11.0':
|
||||
resolution: {integrity: sha512-wO3vd8nsEHdumsXrjGO/v4p6irbg7hy9kvIeR6i2AwylZSk4HJdWgL0FNaVquW1+AweJcdvU1IEpuIWk/WaPnA==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
'@noble/hashes': ^1.8.0 || ^2.0.0
|
||||
peerDependenciesMeta:
|
||||
'@noble/hashes':
|
||||
optional: true
|
||||
|
||||
'@faker-js/faker@9.9.0':
|
||||
resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==}
|
||||
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
|
||||
@@ -3360,6 +3427,35 @@ packages:
|
||||
'@tanstack/react-router': '>=1.43.2'
|
||||
zod: ^3.23.8
|
||||
|
||||
'@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'
|
||||
|
||||
'@tokenizer/inflate@0.4.1':
|
||||
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3394,6 +3490,9 @@ packages:
|
||||
'@types/argparse@1.0.38':
|
||||
resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==}
|
||||
|
||||
'@types/aria-query@5.0.4':
|
||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||
|
||||
'@types/async-retry@1.4.9':
|
||||
resolution: {integrity: sha512-s1ciZQJzRh3708X/m3vPExr5KJlzlZJvXsKpbtE2luqNcbROr64qU+3KpJsYHqWMeaxI839OvXf9PrUSw1Xtyg==}
|
||||
|
||||
@@ -3937,6 +4036,10 @@ 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.1:
|
||||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -3975,6 +4078,13 @@ packages:
|
||||
argv-formatter@1.0.0:
|
||||
resolution: {integrity: sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==}
|
||||
|
||||
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'}
|
||||
|
||||
arktype@1.0.18-alpha:
|
||||
resolution: {integrity: sha512-7yUOaaeEws1GkDFGvB7IPkurVL5FFcZBbWLNVrqNXWccKbmMz9gozqXfR3pnconHeSbXLEcYUmtGAXTnhRgJrw==}
|
||||
|
||||
@@ -4156,6 +4266,9 @@ packages:
|
||||
better-sqlite3@11.8.1:
|
||||
resolution: {integrity: sha512-9BxNaBkblMjhJW8sMRZxnxVTRgbRmssZW0Oxc1MPBTfiR+WW21e2Mk4qu8CzrcZb1LwPCnFsfDEzq+SNcBU8eg==}
|
||||
|
||||
bidi-js@1.0.3:
|
||||
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||
|
||||
binary-extensions@2.2.0:
|
||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4704,10 +4817,21 @@ packages:
|
||||
css-select@5.2.2:
|
||||
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
|
||||
|
||||
css-tree@3.1.0:
|
||||
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
|
||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
||||
|
||||
css-what@6.2.2:
|
||||
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
css.escape@1.5.1:
|
||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||
|
||||
cssstyle@5.3.7:
|
||||
resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
@@ -4719,6 +4843,10 @@ packages:
|
||||
resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
data-urls@7.0.0:
|
||||
resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
data-view-buffer@1.0.1:
|
||||
resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4796,6 +4924,9 @@ packages:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
decode-named-character-reference@1.0.2:
|
||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||
|
||||
@@ -4899,6 +5030,12 @@ packages:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
dom-accessibility-api@0.5.16:
|
||||
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
|
||||
|
||||
dom-accessibility-api@0.6.3:
|
||||
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
@@ -5954,6 +6091,10 @@ packages:
|
||||
resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
|
||||
html-encoding-sniffer@6.0.0:
|
||||
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
@@ -6279,6 +6420,9 @@ packages:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-potential-custom-element-name@1.0.1:
|
||||
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||
|
||||
is-regex@1.1.4:
|
||||
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6458,6 +6602,15 @@ packages:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
jsdom@28.0.0:
|
||||
resolution: {integrity: sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
peerDependencies:
|
||||
canvas: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
|
||||
jsep@1.4.0:
|
||||
resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==}
|
||||
engines: {node: '>= 10.16.0'}
|
||||
@@ -6725,6 +6878,10 @@ packages:
|
||||
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@11.2.5:
|
||||
resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
@@ -6740,6 +6897,10 @@ packages:
|
||||
resolution: {integrity: sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
|
||||
macos-release@3.4.0:
|
||||
resolution: {integrity: sha512-wpGPwyg/xrSp4H4Db4xYSeAr6+cFQGHfspHzDUdYxswDnUW0L5Ov63UuJiSr8NMSpyaChO4u1n0MXUvVPtrN6A==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
@@ -6820,6 +6981,9 @@ packages:
|
||||
mdast-util-to-string@4.0.0:
|
||||
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
|
||||
|
||||
mdn-data@2.12.2:
|
||||
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
|
||||
|
||||
media-typer@1.1.0:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -7532,6 +7696,9 @@ packages:
|
||||
parse5@7.3.0:
|
||||
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||
|
||||
parse5@8.0.0:
|
||||
resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
|
||||
|
||||
pastable@2.2.1:
|
||||
resolution: {integrity: sha512-K4ClMxRKpgN4sXj6VIPPrvor/TMp2yPNCGtfhvV106C73SwefQ3FuegURsH7AQHpqu0WwbvKXRl1HQxF6qax9w==}
|
||||
engines: {node: '>=14.x'}
|
||||
@@ -7719,6 +7886,10 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
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}
|
||||
|
||||
pretty-ms@9.2.0:
|
||||
resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -7836,6 +8007,9 @@ packages:
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
react-is@19.1.0:
|
||||
resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==}
|
||||
|
||||
@@ -8117,6 +8291,10 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
saxes@6.0.0:
|
||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||
engines: {node: '>=v12.22.7'}
|
||||
|
||||
scheduler@0.23.0:
|
||||
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
||||
|
||||
@@ -8543,6 +8721,9 @@ packages:
|
||||
svg-parser@2.0.4:
|
||||
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
table@6.8.2:
|
||||
resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -8656,6 +8837,13 @@ packages:
|
||||
resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tldts-core@7.0.22:
|
||||
resolution: {integrity: sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw==}
|
||||
|
||||
tldts@7.0.22:
|
||||
resolution: {integrity: sha512-nqpKFC53CgopKPjT6Wfb6tpIcZXHcI6G37hesvikhx0EmUGPkZrujRyAjgnmp1SHNgpQfKVanZ+KfpANFt2Hxw==}
|
||||
hasBin: true
|
||||
|
||||
tmp-promise@3.0.3:
|
||||
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
|
||||
|
||||
@@ -8687,12 +8875,20 @@ packages:
|
||||
resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==}
|
||||
hasBin: true
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
tr46@1.0.1:
|
||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||
|
||||
tr46@6.0.0:
|
||||
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
traverse@0.6.8:
|
||||
resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -8997,6 +9193,10 @@ packages:
|
||||
resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
undici@7.21.0:
|
||||
resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unicode-emoji-modifier-base@1.0.0:
|
||||
resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -9224,6 +9424,10 @@ packages:
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
walk-up-path@4.0.0:
|
||||
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
|
||||
engines: {node: 20 || >=22}
|
||||
@@ -9234,6 +9438,10 @@ packages:
|
||||
webidl-conversions@4.0.2:
|
||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||
|
||||
webidl-conversions@8.0.1:
|
||||
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
@@ -9246,6 +9454,14 @@ packages:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-mimetype@5.0.0:
|
||||
resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
whatwg-url@16.0.0:
|
||||
resolution: {integrity: sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
@@ -9343,6 +9559,13 @@ packages:
|
||||
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
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==}
|
||||
|
||||
xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
@@ -9481,6 +9704,8 @@ snapshots:
|
||||
|
||||
'@aashutoshrathi/word-wrap@1.2.6': {}
|
||||
|
||||
'@acemir/cssom@0.9.31': {}
|
||||
|
||||
'@actions/core@2.0.1':
|
||||
dependencies:
|
||||
'@actions/exec': 2.0.0
|
||||
@@ -9497,6 +9722,8 @@ snapshots:
|
||||
|
||||
'@actions/io@2.0.0': {}
|
||||
|
||||
'@adobe/css-tools@4.4.4': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
@@ -9523,6 +9750,24 @@ snapshots:
|
||||
call-me-maybe: 1.0.2
|
||||
openapi-types: 12.1.3
|
||||
|
||||
'@asamuzakjp/css-color@4.1.2':
|
||||
dependencies:
|
||||
'@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-color-parser': 4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
lru-cache: 11.2.5
|
||||
|
||||
'@asamuzakjp/dom-selector@6.7.8':
|
||||
dependencies:
|
||||
'@asamuzakjp/nwsapi': 2.3.9
|
||||
bidi-js: 1.0.3
|
||||
css-tree: 3.1.0
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
lru-cache: 11.2.5
|
||||
|
||||
'@asamuzakjp/nwsapi@2.3.9': {}
|
||||
|
||||
'@babel/code-frame@7.24.7':
|
||||
dependencies:
|
||||
'@babel/highlight': 7.24.7
|
||||
@@ -9804,8 +10049,7 @@ snapshots:
|
||||
|
||||
'@babel/runtime@7.27.1': {}
|
||||
|
||||
'@babel/runtime@7.28.4':
|
||||
optional: true
|
||||
'@babel/runtime@7.28.4': {}
|
||||
|
||||
'@babel/template@7.24.7':
|
||||
dependencies:
|
||||
@@ -10007,6 +10251,28 @@ snapshots:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
optional: true
|
||||
|
||||
'@csstools/color-helpers@6.0.1': {}
|
||||
|
||||
'@csstools/css-calc@3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-color-parser@4.0.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/color-helpers': 6.0.1
|
||||
'@csstools/css-calc': 3.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
|
||||
dependencies:
|
||||
'@csstools/css-tokenizer': 4.0.0
|
||||
|
||||
'@csstools/css-syntax-patches-for-csstree@1.0.26': {}
|
||||
|
||||
'@csstools/css-tokenizer@4.0.0': {}
|
||||
|
||||
'@dotenvx/dotenvx@1.45.1':
|
||||
dependencies:
|
||||
commander: 11.1.0
|
||||
@@ -10497,6 +10763,10 @@ snapshots:
|
||||
'@eslint/core': 0.17.0
|
||||
levn: 0.4.1
|
||||
|
||||
'@exodus/bytes@1.11.0(@noble/hashes@1.8.0)':
|
||||
optionalDependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
'@faker-js/faker@9.9.0': {}
|
||||
|
||||
'@fastify/accept-negotiator@2.0.0': {}
|
||||
@@ -10948,7 +11218,7 @@ snapshots:
|
||||
|
||||
'@mui/private-theming@7.0.2(@types/react@18.2.15)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
'@mui/utils': 7.0.2(@types/react@18.2.15)(react@18.2.0)
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
@@ -10957,7 +11227,7 @@ snapshots:
|
||||
|
||||
'@mui/styled-engine@7.0.2(@emotion/react@11.14.0(@types/react@18.2.15)(react@18.2.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@18.2.15)(react@18.2.0))(@types/react@18.2.15)(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
'@emotion/cache': 11.14.0
|
||||
'@emotion/serialize': 1.3.3
|
||||
'@emotion/sheet': 1.4.0
|
||||
@@ -12018,6 +12288,40 @@ snapshots:
|
||||
'@tanstack/react-router': 1.133.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
zod: 4.3.6
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
'@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.4.4
|
||||
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@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@testing-library/dom': 10.4.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.15
|
||||
'@types/react-dom': 18.2.7
|
||||
|
||||
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
|
||||
'@tokenizer/inflate@0.4.1':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -12054,6 +12358,8 @@ snapshots:
|
||||
|
||||
'@types/argparse@1.0.38': {}
|
||||
|
||||
'@types/aria-query@5.0.4': {}
|
||||
|
||||
'@types/async-retry@1.4.9':
|
||||
dependencies:
|
||||
'@types/retry': 0.12.5
|
||||
@@ -12458,7 +12764,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@swc/helpers'
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2))':
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
@@ -12473,7 +12779,7 @@ snapshots:
|
||||
std-env: 3.9.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12673,6 +12979,8 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
ansi-styles@6.2.1: {}
|
||||
|
||||
ansis@4.2.0: {}
|
||||
@@ -12717,6 +13025,12 @@ snapshots:
|
||||
|
||||
argv-formatter@1.0.0: {}
|
||||
|
||||
aria-query@5.3.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
aria-query@5.3.2: {}
|
||||
|
||||
arktype@1.0.18-alpha: {}
|
||||
|
||||
array-buffer-byte-length@1.0.1:
|
||||
@@ -12871,7 +13185,7 @@ snapshots:
|
||||
|
||||
babel-plugin-macros@3.1.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
cosmiconfig: 7.1.0
|
||||
resolve: 1.22.10
|
||||
|
||||
@@ -12935,6 +13249,10 @@ snapshots:
|
||||
bindings: 1.5.0
|
||||
prebuild-install: 7.1.3
|
||||
|
||||
bidi-js@1.0.3:
|
||||
dependencies:
|
||||
require-from-string: 2.0.2
|
||||
|
||||
binary-extensions@2.2.0: {}
|
||||
|
||||
bindings@1.5.0:
|
||||
@@ -13573,14 +13891,35 @@ snapshots:
|
||||
domutils: 3.2.2
|
||||
nth-check: 2.1.1
|
||||
|
||||
css-tree@3.1.0:
|
||||
dependencies:
|
||||
mdn-data: 2.12.2
|
||||
source-map-js: 1.2.1
|
||||
|
||||
css-what@6.2.2: {}
|
||||
|
||||
css.escape@1.5.1: {}
|
||||
|
||||
cssstyle@5.3.7:
|
||||
dependencies:
|
||||
'@asamuzakjp/css-color': 4.1.2
|
||||
'@csstools/css-syntax-patches-for-csstree': 1.0.26
|
||||
css-tree: 3.1.0
|
||||
lru-cache: 11.2.4
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
dargs@8.1.0: {}
|
||||
|
||||
data-uri-to-buffer@6.0.2: {}
|
||||
|
||||
data-urls@7.0.0(@noble/hashes@1.8.0):
|
||||
dependencies:
|
||||
whatwg-mimetype: 5.0.0
|
||||
whatwg-url: 16.0.0(@noble/hashes@1.8.0)
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
data-view-buffer@1.0.1:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -13651,6 +13990,8 @@ snapshots:
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
decode-named-character-reference@1.0.2:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
@@ -13747,6 +14088,10 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dom-accessibility-api@0.5.16: {}
|
||||
|
||||
dom-accessibility-api@0.6.3: {}
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
@@ -15067,6 +15412,12 @@ snapshots:
|
||||
dependencies:
|
||||
lru-cache: 11.2.4
|
||||
|
||||
html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0):
|
||||
dependencies:
|
||||
'@exodus/bytes': 1.11.0(@noble/hashes@1.8.0)
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
@@ -15362,6 +15713,8 @@ snapshots:
|
||||
|
||||
is-plain-obj@4.1.0: {}
|
||||
|
||||
is-potential-custom-element-name@1.0.1: {}
|
||||
|
||||
is-regex@1.1.4:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -15539,6 +15892,32 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsdom@28.0.0(@noble/hashes@1.8.0):
|
||||
dependencies:
|
||||
'@acemir/cssom': 0.9.31
|
||||
'@asamuzakjp/dom-selector': 6.7.8
|
||||
'@exodus/bytes': 1.11.0(@noble/hashes@1.8.0)
|
||||
cssstyle: 5.3.7
|
||||
data-urls: 7.0.0(@noble/hashes@1.8.0)
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0)
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
parse5: 8.0.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 6.0.0
|
||||
undici: 7.21.0
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
whatwg-mimetype: 5.0.0
|
||||
whatwg-url: 16.0.0(@noble/hashes@1.8.0)
|
||||
xml-name-validator: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
- supports-color
|
||||
|
||||
jsep@1.4.0: {}
|
||||
|
||||
jsesc@2.5.2: {}
|
||||
@@ -15783,6 +16162,8 @@ snapshots:
|
||||
|
||||
lru-cache@11.2.4: {}
|
||||
|
||||
lru-cache@11.2.5: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
@@ -15795,6 +16176,8 @@ snapshots:
|
||||
|
||||
luxon@3.4.3: {}
|
||||
|
||||
lz-string@1.5.0: {}
|
||||
|
||||
macos-release@3.4.0: {}
|
||||
|
||||
magic-string@0.30.17:
|
||||
@@ -15938,6 +16321,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
||||
mdn-data@2.12.2: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
meilisearch@0.50.0: {}
|
||||
@@ -16714,6 +17099,10 @@ snapshots:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
parse5@8.0.0:
|
||||
dependencies:
|
||||
entities: 6.0.1
|
||||
|
||||
pastable@2.2.1(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/core': 7.24.7
|
||||
@@ -16888,6 +17277,12 @@ snapshots:
|
||||
|
||||
prettier@3.6.2: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 17.0.2
|
||||
|
||||
pretty-ms@9.2.0:
|
||||
dependencies:
|
||||
parse-ms: 4.0.0
|
||||
@@ -17001,6 +17396,8 @@ snapshots:
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
react-is@19.1.0: {}
|
||||
|
||||
react-markdown@9.0.3(@types/react@18.2.15)(react@18.2.0):
|
||||
@@ -17139,7 +17536,7 @@ snapshots:
|
||||
|
||||
redux@4.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.28.4
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
@@ -17434,6 +17831,10 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
saxes@6.0.0:
|
||||
dependencies:
|
||||
xmlchars: 2.2.0
|
||||
|
||||
scheduler@0.23.0:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -17920,6 +18321,8 @@ snapshots:
|
||||
|
||||
svg-parser@2.0.4: {}
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
table@6.8.2:
|
||||
dependencies:
|
||||
ajv: 8.12.0
|
||||
@@ -18059,6 +18462,12 @@ snapshots:
|
||||
|
||||
tinyspy@4.0.3: {}
|
||||
|
||||
tldts-core@7.0.22: {}
|
||||
|
||||
tldts@7.0.22:
|
||||
dependencies:
|
||||
tldts-core: 7.0.22
|
||||
|
||||
tmp-promise@3.0.3:
|
||||
dependencies:
|
||||
tmp: 0.2.5
|
||||
@@ -18088,12 +18497,20 @@ snapshots:
|
||||
dependencies:
|
||||
nopt: 1.0.10
|
||||
|
||||
tough-cookie@6.0.0:
|
||||
dependencies:
|
||||
tldts: 7.0.22
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tr46@1.0.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
tr46@6.0.0:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
traverse@0.6.8: {}
|
||||
|
||||
tree-dump@1.1.0(tslib@2.6.2):
|
||||
@@ -18405,6 +18822,8 @@ snapshots:
|
||||
|
||||
undici@7.16.0: {}
|
||||
|
||||
undici@7.21.0: {}
|
||||
|
||||
unicode-emoji-modifier-base@1.0.0: {}
|
||||
|
||||
unicorn-magic@0.1.0: {}
|
||||
@@ -18608,7 +19027,7 @@ snapshots:
|
||||
tsx: 4.20.6
|
||||
yaml: 2.8.2
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.2):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.7)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(tsx@4.20.6)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
@@ -18636,6 +19055,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 22.10.7
|
||||
jsdom: 28.0.0(@noble/hashes@1.8.0)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
@@ -18650,12 +19070,18 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
w3c-xmlserializer@5.0.0:
|
||||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
walk-up-path@4.0.0: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webidl-conversions@4.0.2: {}
|
||||
|
||||
webidl-conversions@8.0.1: {}
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
@@ -18664,6 +19090,16 @@ snapshots:
|
||||
|
||||
whatwg-mimetype@4.0.0: {}
|
||||
|
||||
whatwg-mimetype@5.0.0: {}
|
||||
|
||||
whatwg-url@16.0.0(@noble/hashes@1.8.0):
|
||||
dependencies:
|
||||
'@exodus/bytes': 1.11.0(@noble/hashes@1.8.0)
|
||||
tr46: 6.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
@@ -18804,6 +19240,10 @@ snapshots:
|
||||
dependencies:
|
||||
is-wsl: 3.1.0
|
||||
|
||||
xml-name-validator@5.0.0: {}
|
||||
|
||||
xmlchars@2.2.0: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
@@ -70,6 +70,9 @@
|
||||
"@tanstack/react-table": "8.19.3",
|
||||
"@tanstack/router-cli": "^1.35.4",
|
||||
"@tanstack/router-vite-plugin": "^1.133.13",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/lodash-es": "4.17.9",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/react": "^18.2.15",
|
||||
@@ -83,6 +86,7 @@
|
||||
"eslint": "catalog:",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"jsdom": "^28.0.0",
|
||||
"make-vfs": "^1.0.15",
|
||||
"nodemon": "^3.0.3",
|
||||
"openapi-zod-client": "^1.14.0",
|
||||
|
||||
129
web/src/components/DeleteConfirmationDialog.test.tsx
Normal file
129
web/src/components/DeleteConfirmationDialog.test.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { renderWithProviders, screen } from '@/test/utils';
|
||||
import { DeleteConfirmationDialog } from './DeleteConfirmationDialog';
|
||||
|
||||
describe('DeleteConfirmationDialog', () => {
|
||||
test('renders title and body', () => {
|
||||
renderWithProviders(
|
||||
<DeleteConfirmationDialog
|
||||
open={true}
|
||||
title="Delete Item"
|
||||
body="Are you sure you want to delete this item?"
|
||||
onConfirm={() => {}}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Delete Item')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Are you sure you want to delete this item?'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('delete button calls onConfirm then onClose', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
|
||||
const { user } = renderWithProviders(
|
||||
<DeleteConfirmationDialog
|
||||
open={true}
|
||||
title="Confirm Delete"
|
||||
body="This action cannot be undone."
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
const deleteButton = screen.getByRole('button', { name: 'Delete' });
|
||||
await user.click(deleteButton);
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
// onConfirm should be called before onClose
|
||||
expect(onConfirm.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
onClose.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
test('cancel button calls onCancel (if provided) then onClose', async () => {
|
||||
const onCancel = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
|
||||
const { user } = renderWithProviders(
|
||||
<DeleteConfirmationDialog
|
||||
open={true}
|
||||
title="Confirm Delete"
|
||||
onConfirm={() => {}}
|
||||
onCancel={onCancel}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('cancel button only calls onClose when onCancel not provided', async () => {
|
||||
const onClose = vi.fn();
|
||||
|
||||
const { user } = renderWithProviders(
|
||||
<DeleteConfirmationDialog
|
||||
open={true}
|
||||
title="Confirm Delete"
|
||||
onConfirm={() => {}}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('body is optional - does not render DialogContent if missing', () => {
|
||||
renderWithProviders(
|
||||
<DeleteConfirmationDialog
|
||||
open={true}
|
||||
title="Delete Without Body"
|
||||
onConfirm={() => {}}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Delete Without Body')).toBeInTheDocument();
|
||||
// Should have title and buttons but no body content
|
||||
expect(screen.queryByRole('paragraph')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render when open is false', () => {
|
||||
renderWithProviders(
|
||||
<DeleteConfirmationDialog
|
||||
open={false}
|
||||
title="Hidden Dialog"
|
||||
body="Should not be visible"
|
||||
onConfirm={() => {}}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Hidden Dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('passes dialogProps to Dialog component', () => {
|
||||
renderWithProviders(
|
||||
<DeleteConfirmationDialog
|
||||
open={true}
|
||||
title="With Props"
|
||||
onConfirm={() => {}}
|
||||
onClose={() => {}}
|
||||
dialogProps={{ maxWidth: 'sm', fullWidth: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('With Props')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
154
web/src/components/programming_controls/AddFlexModal.test.tsx
Normal file
154
web/src/components/programming_controls/AddFlexModal.test.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { renderWithProviders, screen } from '@/test/utils';
|
||||
import AddFlexModal from './AddFlexModal';
|
||||
import type { UIFlexProgram } from '@/types/index';
|
||||
|
||||
// Mock the store actions
|
||||
const mockAddProgramsToCurrentChannel = vi.fn();
|
||||
const mockSetProgramAtIndex = vi.fn();
|
||||
|
||||
vi.mock('../../store/channelEditor/actions.ts', () => ({
|
||||
addProgramsToCurrentChannel: (programs: unknown) =>
|
||||
mockAddProgramsToCurrentChannel(programs),
|
||||
setProgramAtIndex: (program: unknown, index: number) =>
|
||||
mockSetProgramAtIndex(program, index),
|
||||
}));
|
||||
|
||||
describe('AddFlexModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('shows "Add Flex Time" title when no initialProgram', () => {
|
||||
renderWithProviders(<AddFlexModal open={true} onClose={() => {}} />);
|
||||
|
||||
expect(screen.getByText('Add Flex Time')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows "Edit Flex Time" title when initialProgram provided', () => {
|
||||
const initialProgram: UIFlexProgram & { index: number } = {
|
||||
type: 'flex',
|
||||
duration: 300000, // 5 minutes in ms
|
||||
persisted: false,
|
||||
uiIndex: 0,
|
||||
originalIndex: 0,
|
||||
index: 0,
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<AddFlexModal open={true} onClose={() => {}} initialProgram={initialProgram} />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Edit Flex Time')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays validation error for empty input', async () => {
|
||||
const { user } = renderWithProviders(
|
||||
<AddFlexModal open={true} onClose={() => {}} />,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText('Duration (seconds)');
|
||||
|
||||
// Clear the input - component rejects non-numeric input, so clearing shows numeric error
|
||||
await user.clear(input);
|
||||
|
||||
// When input is empty, it shows "must be numeric" error
|
||||
expect(screen.getByText('Duration must be numeric')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('displays validation error for zero value', async () => {
|
||||
const { user } = renderWithProviders(
|
||||
<AddFlexModal open={true} onClose={() => {}} />,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText('Duration (seconds)');
|
||||
await user.clear(input);
|
||||
await user.type(input, '0');
|
||||
|
||||
expect(screen.getByText('Duration must be greater than 0.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows humanized duration in helper text for valid input', () => {
|
||||
renderWithProviders(<AddFlexModal open={true} onClose={() => {}} />);
|
||||
|
||||
// Default is 5 minutes (300 seconds)
|
||||
// dayjs humanizes this as "5 minutes"
|
||||
expect(screen.getByText('5 minutes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls addProgramsToCurrentChannel on save (add mode)', async () => {
|
||||
const onClose = vi.fn();
|
||||
const { user } = renderWithProviders(
|
||||
<AddFlexModal open={true} onClose={onClose} />,
|
||||
);
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: 'Save' });
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(mockAddProgramsToCurrentChannel).toHaveBeenCalledWith([
|
||||
{ type: 'flex', duration: 300000, persisted: false }, // 300 seconds in ms
|
||||
]);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls setProgramAtIndex on save (edit mode)', async () => {
|
||||
const onClose = vi.fn();
|
||||
const initialProgram: UIFlexProgram & { index: number } = {
|
||||
type: 'flex',
|
||||
duration: 600000, // 10 minutes
|
||||
persisted: false,
|
||||
uiIndex: 0,
|
||||
originalIndex: 0,
|
||||
index: 5,
|
||||
};
|
||||
|
||||
const { user } = renderWithProviders(
|
||||
<AddFlexModal open={true} onClose={onClose} initialProgram={initialProgram} />,
|
||||
);
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: 'Save' });
|
||||
await user.click(saveButton);
|
||||
|
||||
expect(mockSetProgramAtIndex).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'flex',
|
||||
duration: 600000,
|
||||
persisted: false,
|
||||
}),
|
||||
5,
|
||||
);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls onClose when cancel clicked', async () => {
|
||||
const onClose = vi.fn();
|
||||
const { user } = renderWithProviders(
|
||||
<AddFlexModal open={true} onClose={onClose} />,
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
expect(mockAddProgramsToCurrentChannel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not render when open is false', () => {
|
||||
renderWithProviders(<AddFlexModal open={false} onClose={() => {}} />);
|
||||
|
||||
expect(screen.queryByText('Add Flex Time')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('updates duration when user types new value', async () => {
|
||||
const { user } = renderWithProviders(
|
||||
<AddFlexModal open={true} onClose={() => {}} />,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText('Duration (seconds)');
|
||||
await user.clear(input);
|
||||
await user.type(input, '3600');
|
||||
|
||||
// 3600 seconds = 1 hour, humanized as "an hour"
|
||||
expect(screen.getByText('an hour')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
146
web/src/components/programming_controls/AddPaddingModal.test.tsx
Normal file
146
web/src/components/programming_controls/AddPaddingModal.test.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { renderWithProviders, screen, within } from '@/test/utils';
|
||||
import AddPaddingModal from './AddPaddingModal';
|
||||
import { StartTimePaddingOptions } from '@/hooks/programming_controls/usePadStartTimes';
|
||||
|
||||
// Mock the usePadStartTimes hook
|
||||
const mockPadStartTimes = vi.fn();
|
||||
|
||||
vi.mock('@/hooks/programming_controls/usePadStartTimes', async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import('@/hooks/programming_controls/usePadStartTimes')
|
||||
>('@/hooks/programming_controls/usePadStartTimes');
|
||||
return {
|
||||
...actual,
|
||||
usePadStartTimes: () => mockPadStartTimes,
|
||||
};
|
||||
});
|
||||
|
||||
describe('AddPaddingModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders with dialog title', () => {
|
||||
renderWithProviders(<AddPaddingModal open={true} onClose={() => {}} />);
|
||||
|
||||
// Use getByRole to specifically get the dialog title
|
||||
expect(screen.getByRole('heading', { name: 'Pad Start Times' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders description text', () => {
|
||||
renderWithProviders(<AddPaddingModal open={true} onClose={() => {}} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Adds Flex breaks after each TV episode or movie/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with select dropdown', () => {
|
||||
renderWithProviders(<AddPaddingModal open={true} onClose={() => {}} />);
|
||||
|
||||
// MUI Select uses a combobox role
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows all padding options when dropdown is opened', async () => {
|
||||
const { user } = renderWithProviders(
|
||||
<AddPaddingModal open={true} onClose={() => {}} />,
|
||||
);
|
||||
|
||||
// Click to open the select dropdown
|
||||
const select = screen.getByRole('combobox');
|
||||
await user.click(select);
|
||||
|
||||
// Check that all options are present
|
||||
const listbox = screen.getByRole('listbox');
|
||||
for (const option of StartTimePaddingOptions) {
|
||||
expect(within(listbox).getByText(option.description)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
test('calls padStartTimes with selected option on save', async () => {
|
||||
const onClose = vi.fn();
|
||||
const { user } = renderWithProviders(
|
||||
<AddPaddingModal open={true} onClose={onClose} />,
|
||||
);
|
||||
|
||||
// Open the select dropdown
|
||||
const select = screen.getByRole('combobox');
|
||||
await user.click(select);
|
||||
|
||||
// Select the 15 minute option (":00, :15, :30, :45")
|
||||
const listbox = screen.getByRole('listbox');
|
||||
const option15min = within(listbox).getByText(':00, :15, :30, :45');
|
||||
await user.click(option15min);
|
||||
|
||||
// Click Add Padding button
|
||||
const addButton = screen.getByRole('button', { name: /Add Padding/i });
|
||||
await user.click(addButton);
|
||||
|
||||
expect(mockPadStartTimes).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
key: 15,
|
||||
mod: 15,
|
||||
description: ':00, :15, :30, :45',
|
||||
}),
|
||||
);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls padStartTimes with null when no option selected', async () => {
|
||||
const onClose = vi.fn();
|
||||
const { user } = renderWithProviders(
|
||||
<AddPaddingModal open={true} onClose={onClose} />,
|
||||
);
|
||||
|
||||
// Click Add Padding without selecting anything
|
||||
const addButton = screen.getByRole('button', { name: /Add Padding/i });
|
||||
await user.click(addButton);
|
||||
|
||||
expect(mockPadStartTimes).toHaveBeenCalledWith(null);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('calls onClose on cancel without calling padStartTimes', async () => {
|
||||
const onClose = vi.fn();
|
||||
const { user } = renderWithProviders(
|
||||
<AddPaddingModal open={true} onClose={onClose} />,
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
expect(mockPadStartTimes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not render when open is false', () => {
|
||||
renderWithProviders(<AddPaddingModal open={false} onClose={() => {}} />);
|
||||
|
||||
expect(screen.queryByRole('heading', { name: 'Pad Start Times' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('allows selecting None option', async () => {
|
||||
const onClose = vi.fn();
|
||||
const { user } = renderWithProviders(
|
||||
<AddPaddingModal open={true} onClose={onClose} />,
|
||||
);
|
||||
|
||||
// Open the select dropdown
|
||||
const select = screen.getByRole('combobox');
|
||||
await user.click(select);
|
||||
|
||||
// Select "None"
|
||||
const listbox = screen.getByRole('listbox');
|
||||
const noneOption = within(listbox).getByText('None');
|
||||
await user.click(noneOption);
|
||||
|
||||
// Click Add Padding
|
||||
const addButton = screen.getByRole('button', { name: /Add Padding/i });
|
||||
await user.click(addButton);
|
||||
|
||||
// When "None" is selected (key: -1), the handler sets padding to null
|
||||
expect(mockPadStartTimes).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
175
web/src/hooks/programming_controls/useAddBreaks.test.ts
Normal file
175
web/src/hooks/programming_controls/useAddBreaks.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import type { ChannelProgram, ContentProgram, FlexProgram } from '@tunarr/types';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { addBreaks, type AddBreaksConfig } from './useAddBreaks';
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
// Mock the random helper
|
||||
vi.mock('../../helpers/random.ts', () => ({
|
||||
random: {
|
||||
integer: vi.fn((min: number, max: number) => min), // Always return min for deterministic tests
|
||||
},
|
||||
}));
|
||||
|
||||
const createContentProgram = (
|
||||
durationMs: number,
|
||||
title = 'Test',
|
||||
): ContentProgram => ({
|
||||
type: 'content',
|
||||
id: `id-${title}-${durationMs}`,
|
||||
persisted: true,
|
||||
subtype: 'movie',
|
||||
title,
|
||||
duration: durationMs,
|
||||
externalIds: [],
|
||||
});
|
||||
|
||||
const createFlexProgram = (durationMs: number): FlexProgram => ({
|
||||
type: 'flex',
|
||||
duration: durationMs,
|
||||
persisted: false,
|
||||
});
|
||||
|
||||
const createConfig = (
|
||||
afterMinutes: number,
|
||||
minMinutes: number,
|
||||
maxMinutes: number,
|
||||
): AddBreaksConfig => ({
|
||||
afterDuration: dayjs.duration(afterMinutes, 'minutes'),
|
||||
minDuration: dayjs.duration(minMinutes, 'minutes'),
|
||||
maxDuration: dayjs.duration(maxMinutes, 'minutes'),
|
||||
});
|
||||
|
||||
describe('addBreaks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('inserts flex break when cumulative duration exceeds threshold', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram(30 * 60 * 1000), // 30 min
|
||||
createContentProgram(20 * 60 * 1000), // 20 min - total 50 min
|
||||
createContentProgram(15 * 60 * 1000), // 15 min - total 65 min, exceeds 60
|
||||
];
|
||||
|
||||
// Break after 60 min, insert 5 min breaks
|
||||
const config = createConfig(60, 5, 10);
|
||||
const result = addBreaks(programs, config);
|
||||
|
||||
// Should have: program1, program2, flex, program3
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result[0].type).toBe('content');
|
||||
expect(result[1].type).toBe('content');
|
||||
expect(result[2].type).toBe('flex');
|
||||
expect(result[3].type).toBe('content');
|
||||
});
|
||||
|
||||
test('resets duration counter when flex is encountered', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram(40 * 60 * 1000), // 40 min
|
||||
createFlexProgram(5 * 60 * 1000), // Existing flex - resets counter
|
||||
createContentProgram(40 * 60 * 1000), // 40 min - counter is 40, not 80
|
||||
createContentProgram(25 * 60 * 1000), // 25 min - total 65, exceeds 60
|
||||
];
|
||||
|
||||
const config = createConfig(60, 5, 10);
|
||||
const result = addBreaks(programs, config);
|
||||
|
||||
// Should have: program1, existing-flex, program2, new-flex, program3
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result[0].type).toBe('content');
|
||||
expect(result[1].type).toBe('flex'); // Original flex
|
||||
expect(result[2].type).toBe('content');
|
||||
expect(result[3].type).toBe('flex'); // New flex inserted
|
||||
expect(result[4].type).toBe('content');
|
||||
});
|
||||
|
||||
test('uses random duration between min and max (mocked to min)', async () => {
|
||||
const { random } = await import('../../helpers/random.ts');
|
||||
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram(70 * 60 * 1000), // 70 min, exceeds 60 min threshold
|
||||
];
|
||||
|
||||
// min 5 min, max 10 min
|
||||
const config = createConfig(60, 5, 10);
|
||||
const result = addBreaks(programs, config);
|
||||
|
||||
expect(random.integer).toHaveBeenCalledWith(
|
||||
5 * 60 * 1000, // min in ms
|
||||
10 * 60 * 1000, // max in ms
|
||||
);
|
||||
|
||||
// Flex should be inserted with min duration (mocked)
|
||||
const flexProgram = result.find((p) => p.type === 'flex') as FlexProgram;
|
||||
expect(flexProgram.duration).toBe(5 * 60 * 1000);
|
||||
});
|
||||
|
||||
test('no breaks inserted when programs are short', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram(10 * 60 * 1000), // 10 min
|
||||
createContentProgram(15 * 60 * 1000), // 15 min - total 25 min
|
||||
createContentProgram(20 * 60 * 1000), // 20 min - total 45 min
|
||||
];
|
||||
|
||||
// Break after 60 min
|
||||
const config = createConfig(60, 5, 10);
|
||||
const result = addBreaks(programs, config);
|
||||
|
||||
// No flex should be inserted since total is 45 min < 60 min
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.every((p) => p.type === 'content')).toBe(true);
|
||||
});
|
||||
|
||||
test('handles empty program list', () => {
|
||||
const programs: ChannelProgram[] = [];
|
||||
const config = createConfig(60, 5, 10);
|
||||
|
||||
const result = addBreaks(programs, config);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('inserts multiple breaks for long lists', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram(40 * 60 * 1000), // 40 min
|
||||
createContentProgram(30 * 60 * 1000), // 30 min - total 70, break
|
||||
createContentProgram(40 * 60 * 1000), // 40 min - counter reset after break
|
||||
createContentProgram(30 * 60 * 1000), // 30 min - total 70, break again
|
||||
];
|
||||
|
||||
const config = createConfig(60, 5, 10);
|
||||
const result = addBreaks(programs, config);
|
||||
|
||||
// Should have: p1, flex, p2, p3, flex, p4
|
||||
expect(result).toHaveLength(6);
|
||||
expect(result[0].type).toBe('content');
|
||||
expect(result[1].type).toBe('flex');
|
||||
expect(result[2].type).toBe('content');
|
||||
expect(result[3].type).toBe('content');
|
||||
expect(result[4].type).toBe('flex');
|
||||
expect(result[5].type).toBe('content');
|
||||
});
|
||||
|
||||
test('break is inserted before the program that exceeds threshold', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram(50 * 60 * 1000, 'First'), // 50 min
|
||||
createContentProgram(20 * 60 * 1000, 'Second'), // Would make 70 min
|
||||
];
|
||||
|
||||
const config = createConfig(60, 5, 10);
|
||||
const result = addBreaks(programs, config);
|
||||
|
||||
// Flex should be inserted before 'Second'
|
||||
expect(result).toHaveLength(3);
|
||||
expect((result[0] as ContentProgram).title).toBe('First');
|
||||
expect(result[1].type).toBe('flex');
|
||||
expect((result[2] as ContentProgram).title).toBe('Second');
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,7 @@ export function useAddBreaks() {
|
||||
};
|
||||
}
|
||||
|
||||
function addBreaks(
|
||||
export function addBreaks(
|
||||
programs: ChannelProgram[],
|
||||
{ afterDuration, minDuration, maxDuration }: AddBreaksConfig,
|
||||
) {
|
||||
|
||||
114
web/src/hooks/programming_controls/useAlphaSort.test.ts
Normal file
114
web/src/hooks/programming_controls/useAlphaSort.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { ChannelProgram, ContentProgram } from '@tunarr/types';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { sortPrograms } from './useAlphaSort';
|
||||
|
||||
const createContentProgram = (
|
||||
title: string,
|
||||
overrides: Partial<ContentProgram> = {},
|
||||
): ContentProgram => ({
|
||||
type: 'content',
|
||||
id: `id-${title}`,
|
||||
persisted: true,
|
||||
subtype: 'movie',
|
||||
title,
|
||||
duration: 3600000,
|
||||
externalIds: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createFlexProgram = (): ChannelProgram => ({
|
||||
type: 'flex',
|
||||
duration: 60000,
|
||||
persisted: false,
|
||||
});
|
||||
|
||||
const createRedirectProgram = (channel: string): ChannelProgram => ({
|
||||
type: 'redirect',
|
||||
channel,
|
||||
duration: 3600000,
|
||||
persisted: false,
|
||||
});
|
||||
|
||||
describe('sortPrograms', () => {
|
||||
test('sorts content programs alphabetically in ascending order', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram('Charlie'),
|
||||
createContentProgram('Alpha'),
|
||||
createContentProgram('Bravo'),
|
||||
];
|
||||
|
||||
const { newProgramSort } = sortPrograms(programs, 'asc');
|
||||
|
||||
expect(newProgramSort.map((p) => (p as ContentProgram).title)).toEqual([
|
||||
'Alpha',
|
||||
'Bravo',
|
||||
'Charlie',
|
||||
]);
|
||||
});
|
||||
|
||||
test('sorts content programs alphabetically in descending order', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram('Alpha'),
|
||||
createContentProgram('Charlie'),
|
||||
createContentProgram('Bravo'),
|
||||
];
|
||||
|
||||
const { newProgramSort } = sortPrograms(programs, 'desc');
|
||||
|
||||
expect(newProgramSort.map((p) => (p as ContentProgram).title)).toEqual([
|
||||
'Charlie',
|
||||
'Bravo',
|
||||
'Alpha',
|
||||
]);
|
||||
});
|
||||
|
||||
test('content programs appear before non-content programs (flex, redirect)', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createFlexProgram(),
|
||||
createContentProgram('Zebra'),
|
||||
createRedirectProgram('channel-1'),
|
||||
createContentProgram('Apple'),
|
||||
];
|
||||
|
||||
const { newProgramSort } = sortPrograms(programs, 'asc');
|
||||
|
||||
// Content programs should come first, sorted alphabetically
|
||||
expect(newProgramSort[0].type).toBe('content');
|
||||
expect((newProgramSort[0] as ContentProgram).title).toBe('Apple');
|
||||
expect(newProgramSort[1].type).toBe('content');
|
||||
expect((newProgramSort[1] as ContentProgram).title).toBe('Zebra');
|
||||
// Non-content programs come after
|
||||
expect(newProgramSort[2].type).not.toBe('content');
|
||||
expect(newProgramSort[3].type).not.toBe('content');
|
||||
});
|
||||
|
||||
test('handles empty array', () => {
|
||||
const programs: ChannelProgram[] = [];
|
||||
|
||||
const { newProgramSort } = sortPrograms(programs, 'asc');
|
||||
|
||||
expect(newProgramSort).toEqual([]);
|
||||
});
|
||||
|
||||
test('handles array with single program', () => {
|
||||
const programs: ChannelProgram[] = [createContentProgram('Only One')];
|
||||
|
||||
const { newProgramSort } = sortPrograms(programs, 'asc');
|
||||
|
||||
expect(newProgramSort).toHaveLength(1);
|
||||
expect((newProgramSort[0] as ContentProgram).title).toBe('Only One');
|
||||
});
|
||||
|
||||
test('handles programs with identical titles', () => {
|
||||
const programs: ChannelProgram[] = [
|
||||
createContentProgram('Same', { id: 'id-1' }),
|
||||
createContentProgram('Same', { id: 'id-2' }),
|
||||
createContentProgram('Same', { id: 'id-3' }),
|
||||
];
|
||||
|
||||
const { newProgramSort } = sortPrograms(programs, 'asc');
|
||||
|
||||
expect(newProgramSort).toHaveLength(3);
|
||||
expect(newProgramSort.every((p) => (p as ContentProgram).title === 'Same')).toBe(true);
|
||||
});
|
||||
});
|
||||
181
web/src/hooks/programming_controls/useRemoveDuplicates.test.ts
Normal file
181
web/src/hooks/programming_controls/useRemoveDuplicates.test.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import type { ContentProgram, CustomProgram, FlexProgram, RedirectProgram } from '@tunarr/types';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import type { UIChannelProgram } from '../../types/index';
|
||||
import { removeDuplicatePrograms } from './useRemoveDuplicates';
|
||||
|
||||
const createContentProgram = (
|
||||
id: string,
|
||||
overrides: Partial<ContentProgram> = {},
|
||||
): UIChannelProgram<ContentProgram> => ({
|
||||
type: 'content',
|
||||
id,
|
||||
persisted: true,
|
||||
subtype: 'movie',
|
||||
title: `Movie ${id}`,
|
||||
duration: 3600000,
|
||||
externalIds: [],
|
||||
uiIndex: 0,
|
||||
originalIndex: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createFlexProgram = (): UIChannelProgram<FlexProgram> => ({
|
||||
type: 'flex',
|
||||
duration: 60000,
|
||||
persisted: false,
|
||||
uiIndex: 0,
|
||||
originalIndex: 0,
|
||||
});
|
||||
|
||||
const createRedirectProgram = (channel: string): UIChannelProgram<RedirectProgram> => ({
|
||||
type: 'redirect',
|
||||
channel,
|
||||
duration: 3600000,
|
||||
persisted: false,
|
||||
uiIndex: 0,
|
||||
originalIndex: 0,
|
||||
});
|
||||
|
||||
const createCustomProgram = (
|
||||
customShowId: string,
|
||||
id: string,
|
||||
): UIChannelProgram<CustomProgram> => ({
|
||||
type: 'custom',
|
||||
customShowId,
|
||||
id,
|
||||
duration: 3600000,
|
||||
persisted: false,
|
||||
uiIndex: 0,
|
||||
originalIndex: 0,
|
||||
});
|
||||
|
||||
describe('removeDuplicatePrograms', () => {
|
||||
test('removes all flex programs', () => {
|
||||
const programs: UIChannelProgram[] = [
|
||||
createContentProgram('1'),
|
||||
createFlexProgram(),
|
||||
createContentProgram('2'),
|
||||
createFlexProgram(),
|
||||
];
|
||||
|
||||
const result = removeDuplicatePrograms(programs);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.every((p) => p.type !== 'flex')).toBe(true);
|
||||
});
|
||||
|
||||
test('deduplicates by persisted id (database ID)', () => {
|
||||
const programs: UIChannelProgram[] = [
|
||||
createContentProgram('db-id-1'),
|
||||
createContentProgram('db-id-2'),
|
||||
createContentProgram('db-id-1'), // Duplicate
|
||||
createContentProgram('db-id-3'),
|
||||
createContentProgram('db-id-2'), // Duplicate
|
||||
];
|
||||
|
||||
const result = removeDuplicatePrograms(programs);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((p) => (p as ContentProgram).id)).toEqual([
|
||||
'db-id-1',
|
||||
'db-id-2',
|
||||
'db-id-3',
|
||||
]);
|
||||
});
|
||||
|
||||
test('deduplicates by external ID (Plex/Jellyfin)', () => {
|
||||
const programWithExternalId = (
|
||||
internalId: string,
|
||||
externalId: string,
|
||||
): UIChannelProgram<ContentProgram> =>
|
||||
createContentProgram(internalId, {
|
||||
persisted: false,
|
||||
externalIds: [
|
||||
{
|
||||
type: 'multi',
|
||||
source: 'plex',
|
||||
sourceId: 'server-1',
|
||||
id: externalId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const programs: UIChannelProgram[] = [
|
||||
programWithExternalId('a', 'plex-123'),
|
||||
programWithExternalId('b', 'plex-456'),
|
||||
programWithExternalId('c', 'plex-123'), // Duplicate external ID
|
||||
];
|
||||
|
||||
const result = removeDuplicatePrograms(programs);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('keeps first occurrence of redirect programs (by channel)', () => {
|
||||
const programs: UIChannelProgram[] = [
|
||||
createRedirectProgram('channel-a'),
|
||||
createRedirectProgram('channel-b'),
|
||||
createRedirectProgram('channel-a'), // Duplicate channel
|
||||
createRedirectProgram('channel-c'),
|
||||
];
|
||||
|
||||
const result = removeDuplicatePrograms(programs);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((p) => (p as RedirectProgram).channel)).toEqual([
|
||||
'channel-a',
|
||||
'channel-b',
|
||||
'channel-c',
|
||||
]);
|
||||
});
|
||||
|
||||
test('keeps first occurrence of custom programs (by customShowId + id)', () => {
|
||||
const programs: UIChannelProgram[] = [
|
||||
createCustomProgram('show-1', 'prog-a'),
|
||||
createCustomProgram('show-1', 'prog-b'),
|
||||
createCustomProgram('show-1', 'prog-a'), // Duplicate
|
||||
createCustomProgram('show-2', 'prog-a'), // Different show, same prog id - not duplicate
|
||||
];
|
||||
|
||||
const result = removeDuplicatePrograms(programs);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('handles mixed program types correctly', () => {
|
||||
const programs: UIChannelProgram[] = [
|
||||
createContentProgram('content-1'),
|
||||
createFlexProgram(),
|
||||
createRedirectProgram('channel-1'),
|
||||
createContentProgram('content-1'), // Duplicate content
|
||||
createCustomProgram('show-1', 'custom-1'),
|
||||
createFlexProgram(), // Flex always removed
|
||||
createRedirectProgram('channel-1'), // Duplicate redirect
|
||||
createCustomProgram('show-1', 'custom-1'), // Duplicate custom
|
||||
];
|
||||
|
||||
const result = removeDuplicatePrograms(programs);
|
||||
|
||||
// content-1 (1), channel-1 redirect (1), custom show-1/custom-1 (1)
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((p) => p.type)).toEqual(['content', 'redirect', 'custom']);
|
||||
});
|
||||
|
||||
test('handles empty array', () => {
|
||||
const result = removeDuplicatePrograms([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('preserves order of first occurrences', () => {
|
||||
const programs: UIChannelProgram[] = [
|
||||
createContentProgram('3'),
|
||||
createContentProgram('1'),
|
||||
createContentProgram('2'),
|
||||
createContentProgram('1'), // Duplicate
|
||||
];
|
||||
|
||||
const result = removeDuplicatePrograms(programs);
|
||||
|
||||
expect(result.map((p) => (p as ContentProgram).id)).toEqual(['3', '1', '2']);
|
||||
});
|
||||
});
|
||||
140
web/src/hooks/useNumberString.test.ts
Normal file
140
web/src/hooks/useNumberString.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { useNumberString } from './useNumberString';
|
||||
|
||||
describe('useNumberString', () => {
|
||||
test('initializes with numeric value as string', () => {
|
||||
const { result } = renderHook(() => useNumberString(42));
|
||||
|
||||
expect(result.current.numValue).toBe(42);
|
||||
expect(result.current.strValue).toBe('42');
|
||||
expect(result.current.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test('initializes with float value as string', () => {
|
||||
const { result } = renderHook(() => useNumberString(3.14, true));
|
||||
|
||||
expect(result.current.numValue).toBe(3.14);
|
||||
expect(result.current.strValue).toBe('3.14');
|
||||
expect(result.current.isValid).toBe(true);
|
||||
});
|
||||
|
||||
test('parses valid integer input', async () => {
|
||||
const { result } = renderHook(() => useNumberString(0));
|
||||
|
||||
act(() => {
|
||||
result.current.setValue('123');
|
||||
});
|
||||
|
||||
expect(result.current.strValue).toBe('123');
|
||||
|
||||
// Wait for debounce
|
||||
await waitFor(() => {
|
||||
expect(result.current.numValue).toBe(123);
|
||||
expect(result.current.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('parses valid float input when isFloat=true', async () => {
|
||||
const { result } = renderHook(() => useNumberString(0, true));
|
||||
|
||||
act(() => {
|
||||
result.current.setValue('3.14159');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.numValue).toBe(3.14159);
|
||||
expect(result.current.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('sets valid=false for non-numeric input', async () => {
|
||||
const { result } = renderHook(() => useNumberString(0));
|
||||
|
||||
act(() => {
|
||||
result.current.setValue('not a number');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('sets valid=false for empty string', async () => {
|
||||
const { result } = renderHook(() => useNumberString(42));
|
||||
|
||||
act(() => {
|
||||
result.current.setValue('');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves last valid numValue when input becomes invalid', async () => {
|
||||
const { result } = renderHook(() => useNumberString(100));
|
||||
|
||||
// First set a valid value
|
||||
act(() => {
|
||||
result.current.setValue('200');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.numValue).toBe(200);
|
||||
});
|
||||
|
||||
// Then set an invalid value
|
||||
act(() => {
|
||||
result.current.setValue('invalid');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isValid).toBe(false);
|
||||
// numValue should still be the last valid value
|
||||
expect(result.current.numValue).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
test('strValue updates immediately while numValue debounces', async () => {
|
||||
const { result } = renderHook(() => useNumberString(0));
|
||||
|
||||
act(() => {
|
||||
result.current.setValue('5');
|
||||
});
|
||||
|
||||
// strValue updates immediately
|
||||
expect(result.current.strValue).toBe('5');
|
||||
|
||||
// numValue updates after debounce
|
||||
await waitFor(() => {
|
||||
expect(result.current.numValue).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
test('handles negative numbers', async () => {
|
||||
const { result } = renderHook(() => useNumberString(0));
|
||||
|
||||
act(() => {
|
||||
result.current.setValue('-50');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.numValue).toBe(-50);
|
||||
expect(result.current.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('handles zero', async () => {
|
||||
const { result } = renderHook(() => useNumberString(100));
|
||||
|
||||
act(() => {
|
||||
result.current.setValue('0');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.numValue).toBe(0);
|
||||
expect(result.current.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
188
web/src/hooks/useProgramSearch.test.ts
Normal file
188
web/src/hooks/useProgramSearch.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { ProgramOrFolder } from '@tunarr/types';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { getChildSearchFilter } from './useProgramSearch';
|
||||
|
||||
const createProgram = (
|
||||
type: ProgramOrFolder['type'],
|
||||
uuid = 'test-uuid',
|
||||
): ProgramOrFolder =>
|
||||
({
|
||||
type,
|
||||
uuid,
|
||||
title: 'Test Program',
|
||||
libraryId: 'lib-1',
|
||||
}) as ProgramOrFolder;
|
||||
|
||||
describe('getChildSearchFilter', () => {
|
||||
describe('leaf types return null', () => {
|
||||
test('returns null for episode', () => {
|
||||
const result = getChildSearchFilter(createProgram('episode'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for movie', () => {
|
||||
const result = getChildSearchFilter(createProgram('movie'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for track', () => {
|
||||
const result = getChildSearchFilter(createProgram('track'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for music_video', () => {
|
||||
const result = getChildSearchFilter(createProgram('music_video'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for other_video', () => {
|
||||
const result = getChildSearchFilter(createProgram('other_video'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for collection', () => {
|
||||
const result = getChildSearchFilter(createProgram('collection'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for folder', () => {
|
||||
const result = getChildSearchFilter(createProgram('folder'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null for playlist', () => {
|
||||
const result = getChildSearchFilter(createProgram('playlist'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parent types return filters for children', () => {
|
||||
test('returns season filter for show', () => {
|
||||
const show = createProgram('show', 'show-uuid-123');
|
||||
const result = getChildSearchFilter(show);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'op',
|
||||
op: 'and',
|
||||
children: [
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'type',
|
||||
name: 'Type',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['season'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'parent.id',
|
||||
name: '',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['show-uuid-123'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('returns episode filter for season', () => {
|
||||
const season = createProgram('season', 'season-uuid-456');
|
||||
const result = getChildSearchFilter(season);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'op',
|
||||
op: 'and',
|
||||
children: [
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'type',
|
||||
name: 'Type',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['episode'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'parent.id',
|
||||
name: '',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['season-uuid-456'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('returns album filter for artist', () => {
|
||||
const artist = createProgram('artist', 'artist-uuid-789');
|
||||
const result = getChildSearchFilter(artist);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'op',
|
||||
op: 'and',
|
||||
children: [
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'type',
|
||||
name: 'Type',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['album'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'parent.id',
|
||||
name: '',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['artist-uuid-789'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('returns track filter for album', () => {
|
||||
const album = createProgram('album', 'album-uuid-012');
|
||||
const result = getChildSearchFilter(album);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'op',
|
||||
op: 'and',
|
||||
children: [
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'type',
|
||||
name: 'Type',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['track'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
fieldSpec: {
|
||||
key: 'parent.id',
|
||||
name: '',
|
||||
op: '=',
|
||||
type: 'string',
|
||||
value: ['album-uuid-012'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
15
web/src/test/setup.ts
Normal file
15
web/src/test/setup.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { afterEach } from 'vitest';
|
||||
|
||||
// Load dayjs plugins needed by components
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
// Automatically cleanup after each test
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
27
web/src/test/utils.test.tsx
Normal file
27
web/src/test/utils.test.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { renderWithProviders, screen } from './utils';
|
||||
|
||||
function TestComponent({ message }: { message: string }) {
|
||||
return <div data-testid="test-component">{message}</div>;
|
||||
}
|
||||
|
||||
describe('Test utilities', () => {
|
||||
test('renderWithProviders renders component with providers', () => {
|
||||
renderWithProviders(<TestComponent message="Hello, World!" />);
|
||||
|
||||
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
||||
expect(screen.getByText('Hello, World!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renderWithProviders returns user for interactions', async () => {
|
||||
const { user } = renderWithProviders(
|
||||
<button onClick={() => console.log('clicked')}>Click me</button>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Click me' });
|
||||
expect(button).toBeInTheDocument();
|
||||
|
||||
// Verify user is available for interactions
|
||||
await user.click(button);
|
||||
});
|
||||
});
|
||||
84
web/src/test/utils.tsx
Normal file
84
web/src/test/utils.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, type RenderOptions } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { ThemeProvider } from '@mui/material';
|
||||
import { Theme } from '@/theme.ts';
|
||||
|
||||
/**
|
||||
* Creates a fresh QueryClient configured for testing.
|
||||
* Uses short retry settings and suppresses error logging.
|
||||
*/
|
||||
function createTestQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface TestProvidersProps {
|
||||
children: ReactNode;
|
||||
queryClient?: QueryClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps children with all necessary providers for testing.
|
||||
*/
|
||||
function TestProviders({ children, queryClient }: TestProvidersProps) {
|
||||
const client = queryClient ?? createTestQueryClient();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={client}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ThemeProvider theme={Theme}>{children}</ThemeProvider>
|
||||
</DndProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
|
||||
queryClient?: QueryClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom render function that wraps the component with all necessary providers.
|
||||
* Use this instead of @testing-library/react's render for component tests.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { renderWithProviders, screen } from '@/test/utils';
|
||||
*
|
||||
* test('renders component', () => {
|
||||
* renderWithProviders(<MyComponent />);
|
||||
* expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function renderWithProviders(
|
||||
ui: ReactElement,
|
||||
options: CustomRenderOptions = {},
|
||||
) {
|
||||
const { queryClient, ...renderOptions } = options;
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<TestProviders queryClient={queryClient}>{children}</TestProviders>
|
||||
);
|
||||
|
||||
return {
|
||||
user: userEvent.setup(),
|
||||
...render(ui, { wrapper: Wrapper, ...renderOptions }),
|
||||
};
|
||||
}
|
||||
|
||||
// Re-export everything from @testing-library/react for convenience
|
||||
export * from '@testing-library/react';
|
||||
export { userEvent };
|
||||
@@ -9,7 +9,9 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
includeSource: ['src/**/*.test.ts'],
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
includeSource: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
|
||||
},
|
||||
define: {
|
||||
'import.meta.vitest': false,
|
||||
|
||||
Reference in New Issue
Block a user