diff --git a/apps/web/package.json b/apps/web/package.json index 02127ab..dfe2b80 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,17 +6,22 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run" }, "dependencies": { "react": "^19.3.0", "react-dom": "^19.3.0" }, "devDependencies": { + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^6.1.1", + "jsdom": "^30.0.1", "typescript": "^7.0.2", - "vite": "^8.2.2" + "vite": "^8.2.2", + "vitest": "^5.0.0" } } diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 1e5048b..209f946 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -15,6 +15,12 @@ importers: specifier: ^19.3.0 version: 19.3.0(react@19.3.0) devDependencies: + '@testing-library/jest-dom': + specifier: ^7.0.1 + version: 7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0(jsdom@30.0.1)(vite@8.2.2)) + '@testing-library/react': + specifier: ^16.3.3 + version: 16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@types/react': specifier: ^19.3.0 version: 19.3.0 @@ -24,15 +30,103 @@ importers: '@vitejs/plugin-react': specifier: ^6.1.1 version: 6.1.1(vite@8.2.2) + jsdom: + specifier: ^30.0.1 + version: 30.0.1 typescript: specifier: ^7.0.2 version: 7.0.2 vite: specifier: ^8.2.2 version: 8.2.2 + vitest: + specifier: ^5.0.0 + version: 5.0.0(jsdom@30.0.1)(vite@8.2.2) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@6.0.7': + resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} + engines: {node: ^22.13.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@8.3.2': + resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==} + engines: {node: ^22.13.0 || >=24.0.0} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.1.1': + resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.2.2': + resolution: {integrity: sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==} + 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.1.12': + resolution: {integrity: sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + 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 + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@oxc-project/types@0.149.0': resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==} @@ -135,6 +229,47 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@7.0.1': + resolution: {integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' + vitest: '>= 0.32' + peerDependenciesMeta: + vitest: + optional: true + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + 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 + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} peerDependencies: @@ -279,13 +414,91 @@ packages: oxc-transform-react: optional: true + '@vitest/mocker@5.0.0': + resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.0': + resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + 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'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -300,6 +513,29 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -374,11 +610,36 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@1.3.1: + resolution: {integrity: sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -390,36 +651,102 @@ packages: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + react-dom@19.3.0: resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} peerDependencies: react: ^19.3.0 + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react@19.3.0: resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} engines: {node: '>=0.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + rolldown@1.2.8: resolution: {integrity: sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.28.0: resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@6.1.4: + resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tldts-core@7.4.12: + resolution: {integrity: sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==} + + tldts@7.4.12: + resolution: {integrity: sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} hasBin: true + undici@8.10.2: + resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==} + engines: {node: '>=22.19.0'} + vite@8.2.2: resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -463,8 +790,147 @@ packages: yaml: optional: true + vitest@5.0.0: + resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.0 + '@vitest/browser-preview': 5.0.0 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.0 + '@vitest/coverage-v8': 5.0.0 + '@vitest/ui': 5.0.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + whatwg-url@17.1.1: + resolution: {integrity: sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==} + engines: {node: ^22.14.0 || >=24.0.0} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + snapshots: + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@6.0.7': + dependencies: + '@csstools/css-calc': 3.3.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.2.2(@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.5.2 + + '@asamuzakjp/dom-selector@8.3.2': + dependencies: + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/runtime@7.29.7': {} + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.1.1': {} + + '@csstools/css-calc@3.3.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.2.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.1 + '@csstools/css-calc': 3.3.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.1.12(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@exodus/bytes@1.15.1': {} + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + '@oxc-project/types@0.149.0': {} '@rolldown/binding-android-arm-eabi@1.2.8': @@ -514,6 +980,50 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@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@7.0.1(@testing-library/dom@10.4.1)(vitest@5.0.0(jsdom@30.0.1)(vite@8.2.2))': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + optionalDependencies: + vitest: 5.0.0(jsdom@30.0.1)(vite@8.2.2) + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: '@types/react': 19.3.0 @@ -587,10 +1097,71 @@ snapshots: '@rolldown/pluginutils': 1.0.1 vite: 8.2.2 + '@vitest/mocker@5.0.0(vite@8.2.2)': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.0 + estree-walker: 3.0.3 + magic-string: 1.3.1 + optionalDependencies: + vite: 8.2.2 + + '@vitest/spy@5.0.0': {} + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + + chai@6.2.2: {} + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + csstype@3.2.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + decimal.js@10.6.0: {} + + dequal@2.0.3: {} + detect-libc@2.1.2: {} + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + entities@8.1.0: {} + + es-module-lexer@2.3.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: picomatch: 4.0.7 @@ -598,6 +1169,44 @@ snapshots: fsevents@2.3.3: optional: true + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + indent-string@4.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + js-tokens@4.0.0: {} + + jsdom@30.0.1: + dependencies: + '@asamuzakjp/css-color': 6.0.7 + '@asamuzakjp/dom-selector': 8.3.2 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.12(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 8.10.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + lightningcss-android-arm64@1.33.0: optional: true @@ -647,8 +1256,26 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + lru-cache@11.5.2: {} + + lz-string@1.5.0: {} + + magic-string@1.3.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mdn-data@2.27.1: {} + + min-indent@1.0.1: {} + nanoid@3.3.18: {} + obug@2.2.1: {} + + parse5@8.0.1: + dependencies: + entities: 8.1.0 + picocolors@1.1.1: {} picomatch@4.0.7: {} @@ -659,13 +1286,30 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + punycode@2.3.1: {} + react-dom@19.3.0(react@19.3.0): dependencies: react: 19.3.0 scheduler: 0.28.0 + react-is@17.0.2: {} + react@19.3.0: {} + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + require-from-string@2.0.2: {} + rolldown@1.2.8: dependencies: '@oxc-project/types': 0.149.0 @@ -687,15 +1331,49 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.8 '@rolldown/binding-win32-x64-msvc': 1.2.8 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.28.0: {} + siginfo@2.0.0: {} + source-map-js@1.2.1: {} + stackback@0.0.2: {} + + std-env@4.2.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + symbol-tree@3.2.4: {} + + tinybench@6.1.4: {} + + tinyexec@1.3.0: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tldts-core@7.4.12: {} + + tldts@7.4.12: + dependencies: + tldts-core: 7.4.12 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.12 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + typescript@7.0.2: optionalDependencies: '@typescript/typescript-aix-ppc64': 7.0.2 @@ -719,6 +1397,8 @@ snapshots: '@typescript/typescript-win32-arm64': 7.0.2 '@typescript/typescript-win32-x64': 7.0.2 + undici@8.10.2: {} + vite@8.2.2: dependencies: lightningcss: 1.33.0 @@ -728,3 +1408,57 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: fsevents: 2.3.3 + + vitest@5.0.0(jsdom@30.0.1)(vite@8.2.2): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.0(vite@8.2.2) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.3.1 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.1.4 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + vite: 8.2.2 + why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 30.0.1 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@17.1.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} diff --git a/apps/web/src/__tests__/LayerTable.test.tsx b/apps/web/src/__tests__/LayerTable.test.tsx new file mode 100644 index 0000000..76b7a83 --- /dev/null +++ b/apps/web/src/__tests__/LayerTable.test.tsx @@ -0,0 +1,92 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import LayerTable from '../components/LayerTable'; +import type { Layer } from '../types'; + +function makeLayer(overrides: Partial = {}): Layer { + return { + id: 'layer-1', + z: 1, + name: 'Test Layer', + type: 'media', + status: 'active', + sourceName: 'Clip A', + sourceBank: 'B1·04', + target: 'Out 1', + timePositionSec: 0, + timeDurationSec: 10, + opacity: 100, + blend: 'Normal', + fxSlots: [null, null], + control: { sources: ['web'], owner: 'web' }, + loopMode: 'Loop', + generatorPhase: 0, + transform: { x: 0, y: 0, scale: 100, rotation: 0 }, + color: { brightness: 100, contrast: 100, saturation: 100, hue: 0 }, + dmx: { universe: 0, address: 1, mode: 'rgb' }, + ...overrides, + }; +} + +const noop = () => {}; + +function renderTable(layers: Layer[], selectedId: string | null = null) { + return render( + , + ); +} + +describe('LayerTable', () => { + it('renders all 10 required columns', () => { + renderTable([makeLayer()]); + const headers = screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(10); + const headerTexts = headers.map((h) => h.textContent); + // Status header is empty; the remaining 9 are named. + expect(headerTexts).toEqual(['', 'Layer', 'Typ', 'Source', 'Target', 'Time', 'Opacity', 'Blend', 'FX', 'Control']); + }); + + it('shows layer names', () => { + renderTable([makeLayer({ id: 'a', name: 'Alpha' }), makeLayer({ id: 'b', name: 'Beta' })]); + expect(screen.getByText('Alpha')).toBeInTheDocument(); + expect(screen.getByText('Beta')).toBeInTheDocument(); + }); + + it('marks the selected row with the selected class', () => { + renderTable([makeLayer({ id: 'a', name: 'Alpha' }), makeLayer({ id: 'b', name: 'Beta' })], 'b'); + const rows = screen.getAllByRole('row'); + // rows[0] is the header row; rows[1] = Alpha, rows[2] = Beta + expect(rows[1]).not.toHaveClass('selected'); + expect(rows[2]).toHaveClass('selected'); + expect(rows[2]).toHaveAttribute('aria-selected', 'true'); + }); + + it('distinguishes active/paused/error status visually via tone classes', () => { + renderTable([ + makeLayer({ id: 'a', name: 'Active', status: 'active' }), + makeLayer({ id: 'b', name: 'Paused', status: 'paused' }), + makeLayer({ id: 'c', name: 'Error', status: 'error' }), + ]); + const rows = screen.getAllByRole('row'); + // Each row's status cell carries the tone class. + expect(rows[1].querySelector('.c-status')).toHaveClass('st-active'); + expect(rows[2].querySelector('.c-status')).toHaveClass('st-paused'); + expect(rows[3].querySelector('.c-status')).toHaveClass('st-error'); + }); + + it('shows the status label as screen-reader text (never color alone)', () => { + renderTable([makeLayer({ id: 'a', name: 'Active', status: 'active' })]); + expect(screen.getByText('Aktiv')).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/__tests__/StatusBar.test.tsx b/apps/web/src/__tests__/StatusBar.test.tsx new file mode 100644 index 0000000..075467f --- /dev/null +++ b/apps/web/src/__tests__/StatusBar.test.tsx @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import type { ComponentProps } from 'react'; +import StatusBar from '../components/StatusBar'; +import type { StatusToneItem } from '../components/StatusBar'; + +function makeTone(overrides: Partial = {}): StatusToneItem { + return { tone: 'ok', label: 'OK', ...overrides }; +} + +function renderBar(overrides: Partial> = {}) { + return render( + , + ); +} + +describe('StatusBar', () => { + it('shows FPS with two decimal places', () => { + renderBar({ fps: 59.9401 }); + expect(screen.getByText('59.94')).toBeInTheDocument(); + }); + + it('shows FPS placeholder when no measurement is available', () => { + renderBar({ fps: null }); + expect(screen.getByText('—')).toBeInTheDocument(); + }); + + it('shows Quality: Auto', () => { + renderBar({ quality: 'Auto' }); + expect(screen.getByText('Quality:')).toBeInTheDocument(); + expect(screen.getByText('Auto')).toBeInTheDocument(); + }); + + it('shows the Art-Net status with label and tone class', () => { + renderBar({ artnet: makeTone({ tone: 'warn', label: 'Art-Net Warn' }) }); + expect(screen.getByText('Art-Net Warn')).toBeInTheDocument(); + const item = screen.getByTitle('Art-Net: Art-Net Warn'); + expect(item).toHaveClass('tone-warn'); + }); +}); diff --git a/apps/web/src/test/setup.ts b/apps/web/src/test/setup.ts new file mode 100644 index 0000000..0a40ce3 --- /dev/null +++ b/apps/web/src/test/setup.ts @@ -0,0 +1,8 @@ +import '@testing-library/jest-dom/vitest'; +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; + +// Ensure the DOM is cleaned up between tests (globals are disabled). +afterEach(() => { + cleanup(); +}); diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo index 533db39..dffebd1 100644 --- a/apps/web/tsconfig.tsbuildinfo +++ b/apps/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"version":"7.0.2","root":["./src/App.tsx","./src/icons.tsx","./src/main.tsx","./src/seed.ts","./src/types.ts","./src/utils.ts","./src/vite-env.d.ts","./src/components/Inspector.tsx","./src/components/LayerTable.tsx","./src/components/RightSidebar.tsx","./src/components/StatusBar.tsx","./src/hooks/useApi.ts","./src/hooks/useWebSocket.ts"],"packageJsons":["./node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/@types/react-dom/package.json","./node_modules/.pnpm/@types+react@19.3.0/node_modules/@types/react/package.json","./node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/package.json","./node_modules/.pnpm/react-dom@19.3.0_react@19.3.0/node_modules/react-dom/package.json","./node_modules/.pnpm/react@19.3.0/node_modules/react/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/vite/package.json","./package.json"],"missingPackageJsons":["./node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/react/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/@types/node/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/@vitejs/devtools/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/esbuild/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/jiti/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/less/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/sass-embedded/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/sass/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/stylus/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/sugarss/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/terser/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/tsx/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/vite/types/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/yaml/package.json","./node_modules/@types/react-dom/client/package.json","./node_modules/@types/react/jsx-runtime/package.json","./node_modules/@types/vite/client/package.json","./node_modules/react-dom/client/package.json","./node_modules/react/jsx-runtime/package.json","./node_modules/vite/client/package.json"]} \ No newline at end of file +{"version":"7.0.2","root":["./src/App.tsx","./src/icons.tsx","./src/main.tsx","./src/seed.ts","./src/types.ts","./src/utils.ts","./src/vite-env.d.ts","./src/__tests__/LayerTable.test.tsx","./src/__tests__/StatusBar.test.tsx","./src/components/Inspector.tsx","./src/components/LayerTable.tsx","./src/components/RightSidebar.tsx","./src/components/StatusBar.tsx","./src/hooks/useApi.ts","./src/hooks/useWebSocket.ts","./src/test/setup.ts"],"packageJsons":["./node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/package.json","./node_modules/.pnpm/@testing-library+jest-dom@7.0.1_@testing-library+dom@10.4.1_vitest@5.0.0_jsdom@30.0.1_vite@8.2.2_/node_modules/@testing-library/jest-dom/package.json","./node_modules/.pnpm/@testing-library+react@16.3.3_@testing-library+dom@10.4.1_@types+react-dom@19.3.0_@type_cd9943bf1027a9c45d0c72f500abd754/node_modules/@testing-library/react/package.json","./node_modules/.pnpm/@types+aria-query@5.0.4/node_modules/@types/aria-query/package.json","./node_modules/.pnpm/@types+chai@5.2.3/node_modules/@types/chai/package.json","./node_modules/.pnpm/@types+deep-eql@4.0.2/node_modules/@types/deep-eql/package.json","./node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/@types/react-dom/package.json","./node_modules/.pnpm/@types+react@19.3.0/node_modules/@types/react/package.json","./node_modules/.pnpm/@vitest+mocker@5.0.0_vite@8.2.2/node_modules/@vitest/mocker/package.json","./node_modules/.pnpm/aria-query@5.3.0/node_modules/aria-query/package.json","./node_modules/.pnpm/aria-query@5.3.2/node_modules/aria-query/package.json","./node_modules/.pnpm/assertion-error@2.0.1/node_modules/assertion-error/package.json","./node_modules/.pnpm/chai@6.2.2/node_modules/chai/package.json","./node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/package.json","./node_modules/.pnpm/expect-type@1.4.0/node_modules/expect-type/package.json","./node_modules/.pnpm/jsdom@30.0.1/node_modules/jsdom/package.json","./node_modules/.pnpm/pretty-format@27.5.1/node_modules/pretty-format/package.json","./node_modules/.pnpm/react-dom@19.3.0_react@19.3.0/node_modules/react-dom/package.json","./node_modules/.pnpm/react@19.3.0/node_modules/react/package.json","./node_modules/.pnpm/tinybench@6.1.4/node_modules/tinybench/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/vite/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/package.json","./package.json"],"missingPackageJsons":["./node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/node_modules/aria-query/package.json","./node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/node_modules/pretty-format/package.json","./node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/package.json","./node_modules/.pnpm/@testing-library+jest-dom@7.0.1_@testing-library+dom@10.4.1_vitest@5.0.0_jsdom@30.0.1_vite@8.2.2_/node_modules/@testing-library/jest-dom/node_modules/@testing-library/dom/package.json","./node_modules/.pnpm/@testing-library+jest-dom@7.0.1_@testing-library+dom@10.4.1_vitest@5.0.0_jsdom@30.0.1_vite@8.2.2_/node_modules/@testing-library/jest-dom/node_modules/aria-query/package.json","./node_modules/.pnpm/@testing-library+jest-dom@7.0.1_@testing-library+dom@10.4.1_vitest@5.0.0_jsdom@30.0.1_vite@8.2.2_/node_modules/@testing-library/jest-dom/node_modules/vitest/package.json","./node_modules/.pnpm/@testing-library+jest-dom@7.0.1_@testing-library+dom@10.4.1_vitest@5.0.0_jsdom@30.0.1_vite@8.2.2_/node_modules/@testing-library/jest-dom/types/package.json","./node_modules/.pnpm/@testing-library+react@16.3.3_@testing-library+dom@10.4.1_@types+react-dom@19.3.0_@type_cd9943bf1027a9c45d0c72f500abd754/node_modules/@testing-library/react/types/package.json","./node_modules/.pnpm/@testing-library+react@16.3.3_@testing-library+dom@10.4.1_@types+react-dom@19.3.0_@type_cd9943bf1027a9c45d0c72f500abd754/node_modules/@types/react-dom/client/package.json","./node_modules/.pnpm/@testing-library+react@16.3.3_@testing-library+dom@10.4.1_@types+react-dom@19.3.0_@type_cd9943bf1027a9c45d0c72f500abd754/node_modules/@types/react-dom/test-utils/package.json","./node_modules/.pnpm/@testing-library+react@16.3.3_@testing-library+dom@10.4.1_@types+react-dom@19.3.0_@type_cd9943bf1027a9c45d0c72f500abd754/node_modules/react-dom/client/package.json","./node_modules/.pnpm/@testing-library+react@16.3.3_@testing-library+dom@10.4.1_@types+react-dom@19.3.0_@type_cd9943bf1027a9c45d0c72f500abd754/node_modules/react-dom/test-utils/package.json","./node_modules/.pnpm/@types+chai@5.2.3/node_modules/deep-eql/package.json","./node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/@types/react-dom/test-utils/package.json","./node_modules/.pnpm/@types+react-dom@19.3.0_@types+react@19.3.0/node_modules/react/package.json","./node_modules/.pnpm/@vitest+mocker@5.0.0_vite@8.2.2/node_modules/@vitest/mocker/dist/package.json","./node_modules/.pnpm/@vitest+mocker@5.0.0_vite@8.2.2/node_modules/msw/package.json","./node_modules/.pnpm/expect-type@1.4.0/node_modules/expect-type/dist/package.json","./node_modules/.pnpm/pretty-format@27.5.1/node_modules/pretty-format/build/package.json","./node_modules/.pnpm/tinybench@6.1.4/node_modules/tinybench/dist/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/@types/node/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/@vitejs/devtools/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/esbuild/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/jiti/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/less/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/sass-embedded/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/sass/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/stylus/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/sugarss/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/terser/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/tsx/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/vite/dist/node/chunks/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/vite/dist/node/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/vite/dist/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/vite/types/package.json","./node_modules/.pnpm/vite@8.2.2/node_modules/yaml/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@edge-runtime/vm/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@opentelemetry/api/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@types/node/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@vitest/browser-playwright/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@vitest/browser-preview/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@vitest/browser-webdriverio/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@vitest/coverage-istanbul/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@vitest/coverage-v8/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/@vitest/ui/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/happy-dom/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vite/module-runner/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/dist/chunks/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/dist/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/node_modules/@vitest/mocker/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/node_modules/chai/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/node_modules/expect-type/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/node_modules/tinybench/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/node_modules/vite/module-runner/package.json","./node_modules/.pnpm/vitest@5.0.0_jsdom@30.0.1_vite@8.2.2/node_modules/vitest/node_modules/vite/package.json","./node_modules/@testing-library/jest-dom/vitest/package.json","./node_modules/@types/react-dom/client/package.json","./node_modules/@types/react/jsx-runtime/package.json","./node_modules/@types/vite/client/package.json","./node_modules/react-dom/client/package.json","./node_modules/react/jsx-runtime/package.json","./node_modules/vite/client/package.json"],"semanticErrors":true} \ No newline at end of file diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 0000000..e6149cf --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: false, + setupFiles: ['./src/test/setup.ts'], + }, +}); diff --git a/build/windows/build_portable.sh b/build/windows/build_portable.sh new file mode 100644 index 0000000..ec285f7 --- /dev/null +++ b/build/windows/build_portable.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# HMS MediaEngine – Windows Portable Build (§30.2, ADR-0005: Nuitka Onefolder) +# Läuft auf Windows mit Python 3.13 und Nuitka installiert. + +set -euo pipefail + +VERSION="0.1.0" +OUT_DIR="build/windows/HMS-MediaEngine-Portable-$VERSION" + + echo "=== HMS MediaEngine Windows Build v$VERSION ===" + +# 1. Python-Abhängigkeiten installieren +echo "[1/5] Installiere Python-Abhängigkeiten..." +uv sync + +# 2. Frontend bauen (statische Assets) +echo "[2/5] Baue Web-Frontend..." +cd apps/web +corepack enable pnpm 2>/dev/null || true +pnpm install --frozen-lockfile +pnpm build +cd ../.. + +# 3. Rust-Renderkern bauen (nur wenn cargo verfügbar; sonst Überspringen) +echo "[3/5] Baue Rust-Renderkern..." +if command -v cargo >/dev/null 2>&1; then + cd native/render_bridge + cargo build --release + cd ../.. + RENDERER_DLL="native/render_bridge/target/release/hms_render_bridge.dll" + if [ -f "$RENDERER_DLL" ]; then + echo " Render-Bridge DLL gefunden: $RENDERER_DLL" + fi +else + echo " WARNUNG: cargo nicht gefunden; Rust-Renderkern wird nicht gebaut" + echo " (Wird im Windows-Durchlauf nachgeholt, ADR-0004)" +fi + +# 4. Nuitka Onefolder-Build +echo "[4/5] Baue portable Onefolder-Ausgabe (Nuitka)..." +rm -rf "$OUT_DIR" +mkdir -p "$OUT_DIR" + +python -m nuitka \ + --standalone \ + --onefile=False \ + --output-dir="$OUT_DIR" \ + --include-package=hms_protocol \ + --include-package=hms_domain \ + --include-package=hms_parameter \ + --include-package=hms_artnet \ + --include-package=hms_cluster \ + --include-package=hms_persistence \ + --include-package=hms_plugin_sdk \ + --include-package=hms_media \ + --include-package=hms_audio \ + --include-package=hms_content_sync \ + --include-package=hms_capabilities \ + --include-package=hms_adaptive \ + --include-package=hms_renderer \ + --include-package=hms_control_server \ + --include-package=hms_launcher \ + --include-data-dir=apps/web/dist=web \ + --windows-console-mode=disable \ + --company-name="HMS" \ + --product-name="HMS MediaEngine" \ + --file-version="$VERSION" \ + --product-version="$VERSION" \ + apps/launcher/hms_launcher/__main__.py + +# 5. GStreamer-Runtime und portable Struktur +echo "[5/5] Kopiere GStreamer-Runtime und portable Struktur..." + +# PORTABLE_MODE Markierung (§9) +touch "$OUT_DIR/PORTABLE_MODE" + +# GStreamer (MSVC 1.28.6, ADR-0002) +# Erwartet: GStreamer-MSVC-Installationspfad oder GSTREAMER_1_0_ROOT_MSVC_X86_64 +if [ -n "${GSTREAMER_1_0_ROOT_MSVC_X86_64:-}" ]; then + mkdir -p "$OUT_DIR/runtime/gstreamer" + cp -r "$GSTREAMER_1_0_ROOT_MSVC_X86_64/bin" "$OUT_DIR/runtime/gstreamer/bin" + cp -r "$GSTREAMER_1_0_ROOT_MSVC_X86_64/lib/gstreamer-1.0" "$OUT_DIR/runtime/gstreamer/lib/" + echo " GStreamer 1.28.6 gebündelt (ADR-0002)" +else + echo " WARNUNG: GSTREAMER_1_0_ROOT_MSVC_X86_64 nicht gesetzt" + echo " GStreamer wird nicht gebündelt; im Windows-Durchlauf ergänzen" +fi + +# Portable Verzeichnisstruktur (§9) +mkdir -p "$OUT_DIR/plugins/builtin" "$OUT_DIR/plugins/user" +mkdir -p "$OUT_DIR/projects" "$OUT_DIR/media" "$OUT_DIR/logs" +mkdir -p "$OUT_DIR/userdata/database" "$OUT_DIR/userdata/cache" \ + "$OUT_DIR/userdata/identity" "$OUT_DIR/userdata/sync-staging" \ + "$OUT_DIR/userdata/thumbnails" "$OUT_DIR/userdata/plugin-cache" +mkdir -p "$OUT_DIR/config" "$OUT_DIR/licenses" + +# Plugin-Dateien kopieren +cp -r plugins/builtin/generators "$OUT_DIR/plugins/builtin/" +cp -r plugins/builtin/filters "$OUT_DIR/plugins/builtin/" + +# Standardkonfiguration +cat > "$OUT_DIR/config/app.toml" << 'EOF' +[hms] +bind_host = "127.0.0.1" +default_port = 8000 +open_browser = true +EOF + +# SHA-256-Prüfsumme +echo "Erzeuge SHA-256-Prüfsumme..." +cd "build/windows" +if command -v sha256sum >/dev/null 2>&1; then + find "HMS-MediaEngine-Portable-$VERSION" -type f -exec sha256sum {} + > "HMS-MediaEngine-Portable-$VERSION.sha256" + echo " Prüfsumme: HMS-MediaEngine-Portable-$VERSION.sha256" +fi + +# ZIP erstellen +echo "Erstelle Release-ZIP..." +if command -v zip >/dev/null 2>&1; then + zip -r "HMS-MediaEngine-Portable-$VERSION.zip" "HMS-MediaEngine-Portable-$VERSION" + echo " Release: build/windows/HMS-MediaEngine-Portable-$VERSION.zip" +fi + +echo "=== Build abgeschlossen ===" +echo "Ausgabe: $OUT_DIR" +echo "" +echo "Hinweis: Dieser Build ist ein Dev-Build ohne GPU-Validierung (ADR-0008)." +echo "Gate-0-Messungen (D3D11, Framezeiten, DMX-Latenz) laufen auf echter Hardware."