From e2e06be86e39f77c8eff2b635744620bf9ebd4f5 Mon Sep 17 00:00:00 2001 From: Moh Date: Thu, 6 Aug 2026 02:27:20 +0200 Subject: [PATCH] test: add comprehensive automated test coverage - Vitest multi-project setup (unit / integration / component) - Real-Postgres integration harness via in-process PGlite (TEST_DATABASE_URL override), migrations applied per worker; production code untouched - Unit: routing, locale, validation/Zod schemas, metadata, mail, site-theme, marquee, media, portfolio helpers, plus architecture-rule tests - Integration: Prisma data layer, API routes, and all server actions - Component: UI primitives and form components (jsdom + Testing Library) - 371 tests passing --- package-lock.json | 966 +++++++++++++++++- package.json | 8 + tests/TEST_PLAN.md | 154 +++ tests/component/_smoke.test.tsx | 11 + tests/component/admin-flash.test.tsx | 36 + tests/component/dashboard-cards.test.tsx | 37 + .../component/marquee-settings-form.test.tsx | 28 + .../portfolio-category-filter.test.tsx | 72 ++ tests/component/ui-primitives.test.tsx | 149 +++ tests/helpers/component-setup.ts | 133 +++ tests/helpers/factories.ts | 117 +++ tests/helpers/fs-capability.ts | 23 + tests/helpers/global-db-setup.ts | 35 + tests/helpers/integration-setup.ts | 69 ++ tests/helpers/next-mocks.ts | 67 ++ tests/integration/_smoke.test.ts | 40 + tests/integration/actions-contact.test.ts | 92 ++ tests/integration/actions-maintenance.test.ts | 41 + tests/integration/actions-marquee.test.ts | 51 + tests/integration/actions-media.test.ts | 76 ++ tests/integration/actions-portfolio.test.ts | 222 ++++ .../integration/actions-site-settings.test.ts | 104 ++ tests/integration/actions-smtp.test.ts | 91 ++ tests/integration/admin-auth.test.ts | 81 ++ tests/integration/api-routes.test.ts | 52 + tests/integration/app-config.test.ts | 142 +++ tests/integration/media-service.test.ts | 120 +++ tests/integration/media.test.ts | 137 +++ tests/integration/portfolio.test.ts | 141 +++ tests/portfolio-storage.test.ts | 3 +- tests/unit/admin-feedback.test.ts | 36 + tests/unit/admin-navigation.test.ts | 88 ++ tests/unit/admin-routing.test.ts | 147 +++ tests/unit/architecture.test.ts | 122 +++ tests/unit/form-data.test.ts | 20 + tests/unit/locale.test.ts | 103 ++ tests/unit/mail.test.ts | 114 +++ tests/unit/marquee-settings.test.ts | 85 ++ tests/unit/media-service.test.ts | 52 + tests/unit/media-storage.test.ts | 85 ++ tests/unit/media-validation.test.ts | 64 ++ tests/unit/metadata.test.ts | 113 ++ tests/unit/portfolio-form-progress.test.ts | 134 +++ tests/unit/portfolio-validation.test.ts | 205 ++++ tests/unit/portfolio.test.ts | 42 + tests/unit/site-icons.test.ts | 60 ++ tests/unit/site-settings.test.ts | 106 ++ tests/unit/site-theme.test.ts | 53 + tests/unit/utils.test.ts | 26 + vitest.config.ts | 48 +- 50 files changed, 4975 insertions(+), 26 deletions(-) create mode 100644 tests/TEST_PLAN.md create mode 100644 tests/component/_smoke.test.tsx create mode 100644 tests/component/admin-flash.test.tsx create mode 100644 tests/component/dashboard-cards.test.tsx create mode 100644 tests/component/marquee-settings-form.test.tsx create mode 100644 tests/component/portfolio-category-filter.test.tsx create mode 100644 tests/component/ui-primitives.test.tsx create mode 100644 tests/helpers/component-setup.ts create mode 100644 tests/helpers/factories.ts create mode 100644 tests/helpers/fs-capability.ts create mode 100644 tests/helpers/global-db-setup.ts create mode 100644 tests/helpers/integration-setup.ts create mode 100644 tests/helpers/next-mocks.ts create mode 100644 tests/integration/_smoke.test.ts create mode 100644 tests/integration/actions-contact.test.ts create mode 100644 tests/integration/actions-maintenance.test.ts create mode 100644 tests/integration/actions-marquee.test.ts create mode 100644 tests/integration/actions-media.test.ts create mode 100644 tests/integration/actions-portfolio.test.ts create mode 100644 tests/integration/actions-site-settings.test.ts create mode 100644 tests/integration/actions-smtp.test.ts create mode 100644 tests/integration/admin-auth.test.ts create mode 100644 tests/integration/api-routes.test.ts create mode 100644 tests/integration/app-config.test.ts create mode 100644 tests/integration/media-service.test.ts create mode 100644 tests/integration/media.test.ts create mode 100644 tests/integration/portfolio.test.ts create mode 100644 tests/unit/admin-feedback.test.ts create mode 100644 tests/unit/admin-navigation.test.ts create mode 100644 tests/unit/admin-routing.test.ts create mode 100644 tests/unit/architecture.test.ts create mode 100644 tests/unit/form-data.test.ts create mode 100644 tests/unit/locale.test.ts create mode 100644 tests/unit/mail.test.ts create mode 100644 tests/unit/marquee-settings.test.ts create mode 100644 tests/unit/media-service.test.ts create mode 100644 tests/unit/media-storage.test.ts create mode 100644 tests/unit/media-validation.test.ts create mode 100644 tests/unit/metadata.test.ts create mode 100644 tests/unit/portfolio-form-progress.test.ts create mode 100644 tests/unit/portfolio-validation.test.ts create mode 100644 tests/unit/portfolio.test.ts create mode 100644 tests/unit/site-icons.test.ts create mode 100644 tests/unit/site-settings.test.ts create mode 100644 tests/unit/site-theme.test.ts create mode 100644 tests/unit/utils.test.ts diff --git a/package-lock.json b/package-lock.json index c88a942..4f20a3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,12 +34,20 @@ "zod": "^4.3.6" }, "devDependencies": { + "@electric-sql/pglite": "^0.5.4", + "@electric-sql/pglite-socket": "^0.2.7", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.3", "@types/node": "^20", "@types/pg": "^8.18.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "eslint": "^9.39.4", "eslint-config-next": "^16.1.6", + "jsdom": "^30.0.1", + "pglite-prisma-adapter": "^0.7.2", "postcss": "^8", "prisma": "^7.4.2", "tailwindcss": "^3.4.1", @@ -48,6 +56,13 @@ "vitest": "^3.2.4" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -61,6 +76,59 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz", + "integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.2.1", + "@csstools/css-color-parser": "^4.1.9", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -286,6 +354,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -334,6 +412,19 @@ "node": ">=6.9.0" } }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@chevrotain/cst-dts-gen": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", @@ -371,34 +462,248 @@ "devOptional": true, "license": "Apache-2.0" }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@electric-sql/pglite": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", - "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", - "devOptional": true, + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.4.tgz", + "integrity": "sha512-yYZUyyXrHU7tPlCjwZQJ6hIG9DscdCCn7Uk0mYKwC1FeHX286AbcmFveMiRBEak8e9iPupjsoVImN3yJZVed2g==", + "dev": true, "license": "Apache-2.0" }, + "node_modules/@electric-sql/pglite-age": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-age/-/pglite-age-0.0.5.tgz", + "integrity": "sha512-0PoZu8c5tkvqvVrS77Wcz3hK+nSGw8u8omX8cOWzrg7P95lvS3t4ie9tg5RUPSoFS3xkGJFMBwrqWm5BHTDjiQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@electric-sql/pglite": "0.5.4" + } + }, + "node_modules/@electric-sql/pglite-pg_hashids": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pg_hashids/-/pglite-pg_hashids-0.0.5.tgz", + "integrity": "sha512-z8iR0ui18LiK8QBCQfq4vrzMIBEUROdYPWJSG5f91v8JHv0lbCc5B/qw5pSKnoht1pXlH0yPcvLF/+GuMYSRdQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@electric-sql/pglite": "0.5.4" + } + }, + "node_modules/@electric-sql/pglite-pg_ivm": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pg_ivm/-/pglite-pg_ivm-0.0.5.tgz", + "integrity": "sha512-xx9IjXcRmbHivVLB9DbaA9MsMx8vJGk+cqN2N592oCoKqe+k7SXcUuRh/PSglS7jaHPXM/KvE7KDOh+lip3LfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@electric-sql/pglite": "0.5.4" + } + }, + "node_modules/@electric-sql/pglite-pg_textsearch": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pg_textsearch/-/pglite-pg_textsearch-0.0.6.tgz", + "integrity": "sha512-n5lTjivQn/ZvVFdMoW3Rn9u30S0LeAh/sOmjKptnF0nsTa1zIZxa0mAuynzIKrMoUQ0wOFHyB3mj1lIK2UWHUA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@electric-sql/pglite": "0.5.4" + } + }, + "node_modules/@electric-sql/pglite-pg_uuidv7": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pg_uuidv7/-/pglite-pg_uuidv7-0.0.5.tgz", + "integrity": "sha512-uAatun9hKF6TKYZBc83SaU+snWsg6v2cRatzmty+3e6ai4yDpcOMJBjmA+I73ec0Dk6tykEDsgcsJ0+Eg8af+Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@electric-sql/pglite": "0.5.4" + } + }, + "node_modules/@electric-sql/pglite-pgtap": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pgtap/-/pglite-pgtap-0.0.5.tgz", + "integrity": "sha512-Bf2L2mcsN8AjqZJVXKoUOIaXljtrapclfgrgx4MN2UMLxJtzoPju0KR9aD89mi09eJEMZuKIDsKrtEdXUPuzLQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@electric-sql/pglite": "0.5.4" + } + }, + "node_modules/@electric-sql/pglite-pgvector": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pgvector/-/pglite-pgvector-0.0.5.tgz", + "integrity": "sha512-KEPAkD/3FN6WlDM6XPi2Ko1Xk0ecjOU9OfzCoclVkiRoEVNRYwKo1iO+tlkelMgp07SszmSC4JdSBueH2p7uOQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "@electric-sql/pglite": "0.5.4" + } + }, "node_modules/@electric-sql/pglite-socket": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", - "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", - "devOptional": true, + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.2.7.tgz", + "integrity": "sha512-lhYx1OOVueiWJRa2Uj1QiIjCWmpRnYLNs5rRH/g47EqrYXP/Qt3VvcXGGum4uIouKKMVlPlkXb6E5Uw0aHRymw==", + "dev": true, "license": "Apache-2.0", "bin": { "pglite-server": "dist/scripts/server.js" }, "peerDependencies": { - "@electric-sql/pglite": "0.3.15" - } - }, - "node_modules/@electric-sql/pglite-tools": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", - "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", - "devOptional": true, - "license": "Apache-2.0", - "peerDependencies": { - "@electric-sql/pglite": "0.3.15" + "@electric-sql/pglite": "0.5.4", + "@electric-sql/pglite-age": "0.0.5", + "@electric-sql/pglite-pg_hashids": "0.0.5", + "@electric-sql/pglite-pg_ivm": "0.0.5", + "@electric-sql/pglite-pg_textsearch": "0.0.6", + "@electric-sql/pglite-pg_uuidv7": "0.0.5", + "@electric-sql/pglite-pgtap": "0.0.5", + "@electric-sql/pglite-pgvector": "0.0.5" } }, "node_modules/@emnapi/core": { @@ -1082,6 +1387,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "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 + } + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -2405,6 +2728,36 @@ "zeptomatch": "2.1.0" } }, + "node_modules/@prisma/dev/node_modules/@electric-sql/pglite": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", + "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev/node_modules/@electric-sql/pglite-socket": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", + "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, + "node_modules/@prisma/dev/node_modules/@electric-sql/pglite-tools": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", + "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.15" + } + }, "node_modules/@prisma/driver-adapter-utils": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.4.2.tgz", @@ -3894,6 +4247,108 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz", + "integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "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 + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.3.tgz", + "integrity": "sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -3905,6 +4360,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -4661,6 +5123,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4989,6 +5461,16 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5441,6 +5923,27 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -5468,6 +5971,35 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5626,6 +6158,16 @@ "node": ">=0.10" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -5675,6 +6217,13 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -5738,6 +6287,19 @@ "node": ">=14" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.1", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", @@ -7132,6 +7694,19 @@ "node": ">=16.9.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/http-status-codes": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", @@ -7208,6 +7783,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -7503,6 +8088,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -7717,6 +8309,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@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.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7936,6 +8579,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -7956,6 +8609,13 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7980,6 +8640,16 @@ "node": ">=8.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -8616,6 +9286,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8756,6 +9439,38 @@ "node": ">=4" } }, + "node_modules/pglite-prisma-adapter": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/pglite-prisma-adapter/-/pglite-prisma-adapter-0.7.2.tgz", + "integrity": "sha512-NqLXIUguFbgEHpIoyCvAbpEurbK+AUukiv96gIogVnNYWBdhAU0idfTXcJrsS7OdWenYa8QxEQM2ND1cTQrU4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@prisma/driver-adapter-utils": "7.2.0", + "postgres-array": "^3.0.4" + }, + "peerDependencies": { + "@electric-sql/pglite": ">=0.2.0", + "@prisma/client": ">= 7.1.0" + } + }, + "node_modules/pglite-prisma-adapter/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/pglite-prisma-adapter/node_modules/@prisma/driver-adapter-utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz", + "integrity": "sha512-gzrUcbI9VmHS24Uf+0+7DNzdIw7keglJsD5m/MHxQOU68OhGVzlphQRobLiDMn8CHNA2XN8uugwKjudVtnfMVQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, "node_modules/pgpass": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", @@ -9058,6 +9773,41 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/prisma": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.2.tgz", @@ -9318,6 +10068,20 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -9379,6 +10143,16 @@ "url": "https://github.com/sponsors/remeda" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -9572,6 +10346,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -9996,6 +10783,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10101,6 +10901,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -10277,6 +11084,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10290,6 +11117,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -10477,6 +11330,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -10867,6 +11730,54 @@ "dev": true, "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10999,6 +11910,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index c368843..07a0228 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,20 @@ "zod": "^4.3.6" }, "devDependencies": { + "@electric-sql/pglite": "^0.5.4", + "@electric-sql/pglite-socket": "^0.2.7", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.3", "@types/node": "^20", "@types/pg": "^8.18.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "eslint": "^9.39.4", "eslint-config-next": "^16.1.6", + "jsdom": "^30.0.1", + "pglite-prisma-adapter": "^0.7.2", "postcss": "^8", "prisma": "^7.4.2", "tailwindcss": "^3.4.1", diff --git a/tests/TEST_PLAN.md b/tests/TEST_PLAN.md new file mode 100644 index 0000000..b5a97d5 --- /dev/null +++ b/tests/TEST_PLAN.md @@ -0,0 +1,154 @@ +# Test Plan — mohfarawati.de + +Comprehensive automated test coverage for the multilingual Next.js portfolio + admin +workspace. Focus: **code correctness**. Scope excludes Playwright / browser E2E (per request). + +## 1. Goals & principles + +- Cover every application module: pure helpers, validation/schemas, data layer (Prisma), + server actions, API routes, middleware, forms, components, and architecture rules. +- **Do not change production behaviour.** Tests observe the code as-is. If a test reveals a + real bug or an architecture-rule violation, it is flagged for the owner — production code is + not changed without approval. +- Deterministic and self-contained: no external network, no reliance on a running app server. +- Real Postgres for the data layer (not mocks). See §3. + +## 2. Test taxonomy & runner layout + +Vitest with three **projects** (isolated environments), selected by file location: + +| Project | Env | Location | Parallel | Purpose | +|---|---|---|---|---| +| `unit` | node | `tests/unit/**` | yes | Pure functions, schemas, formatting, mapping logic | +| `integration` | node | `tests/integration/**` | serial | Prisma data layer, server actions, API routes, middleware | +| `component` | jsdom | `tests/component/**` | yes | React components & forms (RTL) | + +Legacy flat `tests/*.test.ts` files are folded into the new structure (kept passing). + +### Tooling added (dev-only) +- `@testing-library/react`, `@testing-library/dom`, `@testing-library/jest-dom`, + `@testing-library/user-event`, `jsdom`, `@vitejs/plugin-react` — component tests. +- `@electric-sql/pglite`, `pglite-prisma-adapter` — an **embedded real Postgres** + (Postgres compiled to WASM) that runs the project's actual migrations and Prisma queries + in-process, via a Prisma driver adapter. + +### Database strategy (real Postgres) +`tests/helpers/integration-setup.ts` + `tests/helpers/global-db-setup.ts`: +- If `TEST_DATABASE_URL` is set → the real `lib/prisma` singleton is used unchanged, pointed + at that Postgres (e.g. the Docker instance). The global setup resets the schema and applies + every `prisma/migrations/*/migration.sql` once before the run. +- Otherwise → `lib/prisma` is mocked (test-only) with a Prisma client backed by an in-process + PGlite database. Each worker gets its own isolated database with the migrations applied — + real Postgres semantics, no external server, no shared-state races. **Production code is + never modified.** +- `resetDb()` (TRUNCATE all tables, restart identities) runs in `beforeEach`. +- The integration project runs **serially** (`fileParallelism: false`); each file gets a fresh + database connection. + +### Next.js runtime mocks (integration) +Server actions/middleware depend on the Next runtime. `tests/helpers/next-mocks.ts` provides: +- `next/navigation` → `redirect()` throws a catchable `NEXT_REDIRECT` carrying the URL. +- `next/dist/client/components/redirect-error` → `isRedirectError()` recognises the above. +- `next/cache` → `revalidatePath()` spy (no-op, asserted). +- `next/headers` → controllable `cookies()` / `headers()` stores. +- `@/lib/admin-auth` `isAdminAuthenticated` → toggled per test (auth guard tests). +- `nodemailer` → captured transport (no real SMTP). + +## 3. Coverage matrix + +### 3.1 Unit — pure lib + +| Module | Cases | +|---|---| +| `admin-routing` | host resolution (forwarded/comma/port), `isAdminHost`, `hasDedicatedAdminHost`, legacy/dev/internal path predicates, `toInternalAdminPath`, `fromDevelopmentAdminPath`, `getAdminAppPath` dev vs prod, `buildAdminUrl`/`buildSiteUrl`, env overrides, normalization edge cases | +| `admin-feedback` | `withFlash` (success/error/both/none, encoding), `readFlash` | +| `admin-navigation` | tree shape, `active`/`expanded` flags for each section, portfolio child mapping, href de-dup filter | +| `form-data` | `isCheckedFormValue` truthy/falsey set | +| `locale` | `isSupportedLocale`, `resolveLocale`, `getDirection` (rtl for ar), `stripLocalePrefix`, `getLocalizedPath(WithDefault)` incl. prefix stripping/rebuilding | +| `utils` | `cn` merge/dedupe/conditional | +| `site-theme` | `buildSiteThemeTokens` (hex→hsl, derive dark/secondary, clamps), `buildSiteThemeStyleText` structure, invalid hex fallback | +| `marquee-settings` | defaults, `parseMarqueeSettingsValue` (invalid json, partial, trims), `syncMarqueeSettingsToGermanSource`, `splitMarqueeRowItems` (newline/comma/blank) | +| `site-settings` | `normalizeSiteDefaultLocale`, `normalizeSitePrimaryColor`, `buildDefaultSiteSettings`, `parseSiteSettingsValue` (merge, invalid json, legacy title, invalid locale/color) | +| `site-icons` | `buildSiteIconUrls` (version, relative vs absolute favicon url, name fallback), `buildSiteIconResponse` transparent fallback for non-managed paths | +| `media-storage` (pure) | `sanitizeBaseName`, `getExtensionForMimeType` (all mimes + unknown), `isManagedMediaFilePath`, `resolveMediaUploadPath` (root confinement + traversal guard) | +| `media-validation` | `mediaFieldInputSchema`: library needs assetId, external needs url, url format rule, upload mode, kind enum, trimming | +| `media-service` (pure) | `inferMediaKindFromMimeType`, `inferMediaKindFromFileName`, `getKindFromUploadFile` | +| `portfolio` (pure) | `resolvePortfolioProjectViewMode`, `getLocalizedValue` (direct/fallback/any) | +| `portfolio-form-progress` | slug/year/sortOrder validators, section readiness per type, asset readiness, wizard progress, first incomplete step | +| `portfolio-validation` | category/section/asset/project schemas: required fields, slug regex, coercions, view modes, section superRefine per type, media refinements, url rules | +| `metadata` | `applyTitleTemplateFn`, `buildLocaleAlternates`, `buildAppMetadataFromConfig`, `buildLocalizedMetadataFromConfig` (title template skip, description fallback, og/twitter, icons) | +| `mail` | `createSmtpTransport` (required host/user/pass errors, port/secure), `sendMail` (from with/without name), `sendContactMessage` (recipient + fallback, body fields), `sendTestEmail` (recipient fallback, transport reject) | + +### 3.2 Integration — data layer (real DB) + +| Module | Cases | +|---|---| +| `app-config` | maintenance get/set, site settings get (fallback name from `siteName` key) / update roundtrip, mail settings get/update, marquee get/update (german sync), `getSiteSettingsMediaBindings` (mediaUsage → bindings per field) | +| `media` | create asset, get by id (+usages), list, `getMediaOptions` kind filter, `replaceEntityMediaUsages` (transactional replace, unique constraint), `deleteEntityMediaUsages`, `getPortfolioMediaBindings` routing by usageType, `countMediaUsageReferences` | +| `portfolio` (queries) | admin categories (+project counts), active categories/by-slug, admin projects (status/category filters + ordering), published projects/by-slug, by-id with media bindings, localized mapping, `onDelete` Restrict/Cascade behaviour | +| `media-service.resolveMediaSelection` | library (found/missing), external (creates asset, filename from url), missing+required error, not-required empty | +| `admin-auth` (lockout) | `registerFailedAdminAttempt` increments & locks at threshold, `getAdminLockState`, `resetAdminFailedAttempts`, IP hashing via mocked headers; token `isPasswordValid`/verify with env | + +### 3.3 Integration — API routes & middleware + +| Target | Cases | +|---|---| +| `GET /api/health` | 200 + `database: up`; 503 + `database: down` when query throws | +| `GET /api/site/default-locale` | returns runtime `defaultLocale` + `maintenanceEnabled`, no-store header | +| `proxy` (middleware) | runtime default-locale passthrough, safe fallback on fetch failure, maintenance redirect, `SITE_RUNTIME_ORIGIN`, admin host rewrite → internal, dev `/root` handling, legacy 404 in prod, internal path 404 for non-admin in prod, basic-auth challenge/valid | + +### 3.4 Integration — server actions + +| Action file | Cases | +|---|---| +| `contact/actions` | valid → sends mail + redirect `/success`; invalid (short name/bad email/short message) → redirect with error; locale resolution | +| `maintenance/actions` | unauth → redirect to admin root; enable/disable toggles config + revalidates + success flash | +| `marquee/actions` | unauth guard; empty german rows throw per-row error → error flash; valid → saves (german-synced) + success flash | +| `smtp/actions` | `parseMailSettingsFormData` (port parse error, password retention when blank), save → success; `sendTestEmailAction` success + failure | +| `site-settings/actions` | brand save (primary color normalize, media selection + usage wiring, cleanup on error), localization save (siteName required, title template must contain `{pageTitle}`), `parseJsonObject` guard | +| `media/actions` | create (kind image/document, missing file error), delete (not found, in-use guard, managed-file removal) | +| `portfolio/actions` | `upsertCategoryAction` create/update + P2002 unique message; `deleteCategoryAction` blocks when projects exist; `saveProjectAction` create + update, sections/assets replace, `publishedAt` first-publish logic, media usage wiring, validation + error cleanup of created media; `deleteProjectAction` not-found + cascade + usage cleanup | + +### 3.5 Component (jsdom + RTL) + +Global mocks: `framer-motion`, `gsap`, `next/link`, `next/image`, `next-intl`, `next/navigation`. + +| Component | Cases | +|---|---| +| `ui/badge` | variant classes, custom className merge, passthrough props | +| `ui/input` | renders, forwardRef, type/placeholder/disabled, className merge | +| `ui/textarea`, `ui/label`, `ui/card`*, `ui/app-card`, `ui/separator`, `ui/table`* | render, props, ref, composition | +| `admin/admin-flash` | null when empty, success `role=status`, error `role=alert`, both, className | +| `admin/marquee-settings-form` | renders 4 rows, default values, submit wiring to action | +| `site/portfolio-category-filter` | "all" link + per-category links, active state, localized labels, hrefs | +| `dashboard/dashboard-card`, `dashboard/stats-card` | presentational render, props | +| `layout/container`, `layout/hero-badge`, `home/section-heading`, `home/bento-card` | presentational render, children, className | + +(*multi-part primitives tested for sub-component composition.) + +### 3.6 Architecture-rule tests (`tests/integration/architecture`) + +Enforced from `CLAUDE.md`: +1. **No Prisma in client components** — no file containing `"use client"` imports `lib/prisma`. +2. **Business logic out of UI** — components don't import server-only data modules directly + (allow-list of pure `lib/*` view/format helpers). +3. **Server actions are guarded** — every exported action in `app/**/actions.ts` calls an auth + guard (`ensureAdmin`/`requireAdminAuth`) except the public contact action. +4. **`"use server"` directive** — every `actions.ts` begins with `"use server"`. +5. **`lib/*` does not import from `app/`** — dependency direction. +6. **Admin mirror parity** — every `page.tsx` under `app/_admin/**` has matching re-export + stubs under `app/root/**` and `app/admin-internal/**` pointing back to `_admin`. +7. **No browser storage in components** — no `localStorage`/`sessionStorage` usage. + +## 4. Deliverables & running + +```bash +npm test # all projects +npx vitest run --project unit +npx vitest run --project integration +npx vitest run --project component +TEST_DATABASE_URL=postgres://… # optional: run integration against real Postgres +``` + +Findings (real bugs / rule violations) are reported to the owner; production code is only +changed after approval. diff --git a/tests/component/_smoke.test.tsx b/tests/component/_smoke.test.tsx new file mode 100644 index 0000000..214b581 --- /dev/null +++ b/tests/component/_smoke.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { Badge } from "@/components/ui/badge"; + +describe("component harness smoke test", () => { + it("renders a component into jsdom", () => { + render(Hello); + expect(screen.getByText("Hello")).toBeInTheDocument(); + }); +}); diff --git a/tests/component/admin-flash.test.tsx b/tests/component/admin-flash.test.tsx new file mode 100644 index 0000000..e76d230 --- /dev/null +++ b/tests/component/admin-flash.test.tsx @@ -0,0 +1,36 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { AdminFlash } from "@/components/admin/admin-flash"; + +describe("AdminFlash", () => { + it("renders nothing when there are no messages", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("renders a success message with a status role", () => { + render(); + const status = screen.getByRole("status"); + expect(status).toHaveTextContent("Saved."); + expect(screen.queryByRole("alert")).toBeNull(); + }); + + it("renders an error message with an alert role", () => { + render(); + const alert = screen.getByRole("alert"); + expect(alert).toHaveTextContent("Failed."); + expect(screen.queryByRole("status")).toBeNull(); + }); + + it("renders both a success and error message together", () => { + render(); + expect(screen.getByRole("status")).toHaveTextContent("Yes"); + expect(screen.getByRole("alert")).toHaveTextContent("No"); + }); + + it("applies a custom className to the wrapper", () => { + const { container } = render(); + expect(container.firstChild).toHaveClass("mb-4"); + }); +}); diff --git a/tests/component/dashboard-cards.test.tsx b/tests/component/dashboard-cards.test.tsx new file mode 100644 index 0000000..c1b5374 --- /dev/null +++ b/tests/component/dashboard-cards.test.tsx @@ -0,0 +1,37 @@ +import { Activity } from "lucide-react"; + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { DashboardCard } from "@/components/dashboard/dashboard-card"; +import { StatsCard } from "@/components/dashboard/stats-card"; + +describe("StatsCard", () => { + it("renders the title and value", () => { + render(); + expect(screen.getByText("Visitors")).toBeInTheDocument(); + expect(screen.getByText("1,234")).toBeInTheDocument(); + }); + + it("renders an optional description and footer", () => { + render( + footer} />, + ); + expect(screen.getByText("Up 5%")).toBeInTheDocument(); + expect(screen.getByText("footer")).toBeInTheDocument(); + }); + + it("omits the icon container when no icon is provided", () => { + const { container } = render(); + expect(container.querySelector("svg")).toBeNull(); + }); +}); + +describe("DashboardCard", () => { + it("passes its props through to a StatsCard", () => { + render(); + expect(screen.getByText("Sales")).toBeInTheDocument(); + expect(screen.getByText("42")).toBeInTheDocument(); + expect(screen.getByText("today")).toBeInTheDocument(); + }); +}); diff --git a/tests/component/marquee-settings-form.test.tsx b/tests/component/marquee-settings-form.test.tsx new file mode 100644 index 0000000..73bda5e --- /dev/null +++ b/tests/component/marquee-settings-form.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { MarqueeSettingsForm } from "@/components/admin/marquee-settings-form"; +import { buildDefaultMarqueeSettings } from "@/lib/marquee-settings"; + +describe("MarqueeSettingsForm", () => { + it("renders a textarea per row with german default values", () => { + const settings = buildDefaultMarqueeSettings(); + settings.locales.de.row1 = "First row value"; + render(); + + for (const name of ["row1-de", "row2-de", "row3-de", "row4-de"]) { + const field = document.querySelector(`textarea[name="${name}"]`); + expect(field).not.toBeNull(); + } + expect((document.querySelector('textarea[name="row1-de"]') as HTMLTextAreaElement).value).toBe( + "First row value", + ); + }); + + it("wires the server action onto the form and renders a submit button", () => { + const action = vi.fn(); + render(); + expect(document.querySelector("form#marquee-settings-form")).not.toBeNull(); + expect(screen.getByRole("button", { name: /save marquee/i })).toBeInTheDocument(); + }); +}); diff --git a/tests/component/portfolio-category-filter.test.tsx b/tests/component/portfolio-category-filter.test.tsx new file mode 100644 index 0000000..615772d --- /dev/null +++ b/tests/component/portfolio-category-filter.test.tsx @@ -0,0 +1,72 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { PortfolioCategoryFilter } from "@/components/site/portfolio-category-filter"; +import type { PortfolioCategoryView } from "@/lib/portfolio"; + +function category(overrides: Partial = {}): PortfolioCategoryView { + return { + id: overrides.id ?? "c1", + slug: overrides.slug ?? "branding", + name: overrides.name ?? { ar: "الهوية", en: "Branding", de: "Branding" }, + description: overrides.description ?? { ar: "", en: "", de: "" }, + sortOrder: overrides.sortOrder ?? 0, + isActive: overrides.isActive ?? true, + }; +} + +describe("PortfolioCategoryFilter", () => { + const categories = [ + category({ id: "c1", slug: "branding", name: { ar: "الهوية", en: "Branding", de: "Marke" } }), + category({ id: "c2", slug: "web", name: { ar: "ويب", en: "Web", de: "Web" } }), + ]; + + it("renders the all-projects link and one link per category", () => { + render( + , + ); + const links = screen.getAllByRole("link"); + expect(links).toHaveLength(3); + expect(screen.getByRole("link", { name: "All" })).toHaveAttribute("href", "/en/portfolio"); + expect(screen.getByRole("link", { name: "Branding" })).toHaveAttribute( + "href", + "/en/portfolio/category/branding", + ); + }); + + it("uses localized labels for the active locale", () => { + render( + , + ); + // German locale on the default locale -> unprefixed paths and German labels + expect(screen.getByRole("link", { name: "Marke" })).toHaveAttribute( + "href", + "/portfolio/category/branding", + ); + expect(screen.getByRole("link", { name: "Alle" })).toHaveAttribute("href", "/portfolio"); + }); + + it("highlights the active category", () => { + render( + , + ); + const active = screen.getByRole("link", { name: "Web" }); + expect(active).toHaveClass("bg-primary"); + }); +}); diff --git a/tests/component/ui-primitives.test.tsx b/tests/component/ui-primitives.test.tsx new file mode 100644 index 0000000..ac9c865 --- /dev/null +++ b/tests/component/ui-primitives.test.tsx @@ -0,0 +1,149 @@ +import { createRef } from "react"; + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { AppCard } from "@/components/ui/app-card"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; +import { Container } from "@/components/layout/container"; +import { HeroBadge } from "@/components/layout/hero-badge"; +import { SectionHeading } from "@/components/home/section-heading"; + +describe("Badge", () => { + it("renders children and default variant classes", () => { + render(New); + const badge = screen.getByText("New"); + expect(badge).toHaveClass("bg-primary"); + }); + + it("applies a chosen variant and merges custom classNames", () => { + render(OK); + const badge = screen.getByText("OK"); + expect(badge).toHaveClass("bg-status-success-soft"); + expect(badge).toHaveClass("custom-class"); + }); + + it("passes through arbitrary props", () => { + render(X); + expect(screen.getByTestId("b")).toHaveAttribute("title", "tip"); + }); +}); + +describe("Input", () => { + it("forwards the ref and renders attributes", () => { + const ref = createRef(); + render(); + const input = screen.getByPlaceholderText("you@example.com"); + expect(ref.current).toBe(input); + expect(input).toHaveAttribute("type", "email"); + expect(input).toBeDisabled(); + }); + + it("merges custom classes", () => { + render(); + expect(screen.getByLabelText("field")).toHaveClass("w-20"); + }); +}); + +describe("Textarea", () => { + it("forwards the ref and renders a default value", () => { + const ref = createRef(); + render(