From 3f9ebd4c189bc927df31bad454fe02f50dc9054a Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 20:10:29 +0200 Subject: [PATCH 01/11] feat: unit test utils --- package.json | 2 + pnpm-lock.yaml | 153 +++++++++++++++---- src/common/ui/utils/context.ts | 4 +- src/common/ui/utils/shadcn.test.ts | 39 +++++ src/tags/data/collections/tags-collection.ts | 3 + test/utils/setup.ts | 9 ++ vitest.config.mts | 2 + 7 files changed, 178 insertions(+), 34 deletions(-) create mode 100644 src/common/ui/utils/shadcn.test.ts create mode 100644 test/utils/setup.ts diff --git a/package.json b/package.json index d05eebca..f841ae10 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,8 @@ "devDependencies": { "@next/env": "^14.2.4", "@playwright/test": "^1.45.0", + "@testing-library/dom": "^10.2.0", + "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^15.0.7", "@types/dompurify": "^3.0.5", "@types/jsdom": "^21.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c3c6320..27b9f165 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,6 +186,12 @@ importers: '@playwright/test': specifier: ^1.45.0 version: 1.45.0 + '@testing-library/dom': + specifier: ^10.2.0 + version: 10.2.0 + '@testing-library/jest-dom': + specifier: ^6.4.6 + version: 6.4.6(vitest@1.6.0(@types/node@20.14.8)(jsdom@24.1.0)(terser@5.31.1)) '@testing-library/react': specifier: ^15.0.7 version: 15.0.7(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -249,6 +255,9 @@ importers: packages: + '@adobe/css-tools@4.4.0': + resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -2427,10 +2436,31 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} - '@testing-library/dom@10.1.0': - resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} + '@testing-library/dom@10.2.0': + resolution: {integrity: sha512-CytIvb6tVOADRngTHGWNxH8LPgO/3hi/BdCEHOf7Qd2GvZVClhVP0Wo/QHzWhpki49Bk0b4VT6xpt3fx8HTSIw==} engines: {node: '>=18'} + '@testing-library/jest-dom@6.4.6': + resolution: {integrity: sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@jest/globals': '>= 28' + '@types/bun': latest + '@types/jest': '>= 28' + jest: '>= 28' + vitest: '>= 0.32' + peerDependenciesMeta: + '@jest/globals': + optional: true + '@types/bun': + optional: true + '@types/jest': + optional: true + jest: + optional: true + vitest: + optional: true + '@testing-library/react@15.0.7': resolution: {integrity: sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==} engines: {node: '>=18'} @@ -3272,6 +3302,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -3396,6 +3429,9 @@ packages: dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -3930,6 +3966,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -4207,6 +4247,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4283,6 +4326,10 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4917,6 +4964,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -5139,6 +5190,10 @@ packages: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -5638,6 +5693,8 @@ packages: snapshots: + '@adobe/css-tools@4.4.0': {} + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -5697,10 +5754,10 @@ snapshots: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.600.0(@aws-sdk/client-sts@3.600.0) - '@aws-sdk/client-sts': 3.600.0 + '@aws-sdk/client-sso-oidc': 3.600.0 + '@aws-sdk/client-sts': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0) '@aws-sdk/core': 3.598.0 - '@aws-sdk/credential-provider-node': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0))(@aws-sdk/client-sts@3.600.0) + '@aws-sdk/credential-provider-node': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0)(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)) '@aws-sdk/middleware-bucket-endpoint': 3.598.0 '@aws-sdk/middleware-expect-continue': 3.598.0 '@aws-sdk/middleware-flexible-checksums': 3.598.0 @@ -5755,13 +5812,13 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0)': + '@aws-sdk/client-sso-oidc@3.600.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sts': 3.600.0 + '@aws-sdk/client-sts': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0) '@aws-sdk/core': 3.598.0 - '@aws-sdk/credential-provider-node': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0))(@aws-sdk/client-sts@3.600.0) + '@aws-sdk/credential-provider-node': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0)(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)) '@aws-sdk/middleware-host-header': 3.598.0 '@aws-sdk/middleware-logger': 3.598.0 '@aws-sdk/middleware-recursion-detection': 3.598.0 @@ -5798,7 +5855,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 transitivePeerDependencies: - - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso@3.598.0': @@ -5844,13 +5900,13 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.600.0': + '@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.600.0(@aws-sdk/client-sts@3.600.0) + '@aws-sdk/client-sso-oidc': 3.600.0 '@aws-sdk/core': 3.598.0 - '@aws-sdk/credential-provider-node': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0))(@aws-sdk/client-sts@3.600.0) + '@aws-sdk/credential-provider-node': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0)(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)) '@aws-sdk/middleware-host-header': 3.598.0 '@aws-sdk/middleware-logger': 3.598.0 '@aws-sdk/middleware-recursion-detection': 3.598.0 @@ -5887,6 +5943,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/core@3.598.0': @@ -5918,14 +5975,14 @@ snapshots: '@smithy/util-stream': 3.0.4 tslib: 2.6.2 - '@aws-sdk/credential-provider-ini@3.598.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0))(@aws-sdk/client-sts@3.600.0)': + '@aws-sdk/credential-provider-ini@3.598.0(@aws-sdk/client-sso-oidc@3.600.0)(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))': dependencies: - '@aws-sdk/client-sts': 3.600.0 + '@aws-sdk/client-sts': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0) '@aws-sdk/credential-provider-env': 3.598.0 '@aws-sdk/credential-provider-http': 3.598.0 '@aws-sdk/credential-provider-process': 3.598.0 - '@aws-sdk/credential-provider-sso': 3.598.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0)) - '@aws-sdk/credential-provider-web-identity': 3.598.0(@aws-sdk/client-sts@3.600.0) + '@aws-sdk/credential-provider-sso': 3.598.0(@aws-sdk/client-sso-oidc@3.600.0) + '@aws-sdk/credential-provider-web-identity': 3.598.0(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)) '@aws-sdk/types': 3.598.0 '@smithy/credential-provider-imds': 3.1.2 '@smithy/property-provider': 3.1.2 @@ -5936,14 +5993,14 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-node@3.600.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0))(@aws-sdk/client-sts@3.600.0)': + '@aws-sdk/credential-provider-node@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))': dependencies: '@aws-sdk/credential-provider-env': 3.598.0 '@aws-sdk/credential-provider-http': 3.598.0 - '@aws-sdk/credential-provider-ini': 3.598.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0))(@aws-sdk/client-sts@3.600.0) + '@aws-sdk/credential-provider-ini': 3.598.0(@aws-sdk/client-sso-oidc@3.600.0)(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)) '@aws-sdk/credential-provider-process': 3.598.0 - '@aws-sdk/credential-provider-sso': 3.598.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0)) - '@aws-sdk/credential-provider-web-identity': 3.598.0(@aws-sdk/client-sts@3.600.0) + '@aws-sdk/credential-provider-sso': 3.598.0(@aws-sdk/client-sso-oidc@3.600.0) + '@aws-sdk/credential-provider-web-identity': 3.598.0(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0)) '@aws-sdk/types': 3.598.0 '@smithy/credential-provider-imds': 3.1.2 '@smithy/property-provider': 3.1.2 @@ -5963,10 +6020,10 @@ snapshots: '@smithy/types': 3.2.0 tslib: 2.6.2 - '@aws-sdk/credential-provider-sso@3.598.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0))': + '@aws-sdk/credential-provider-sso@3.598.0(@aws-sdk/client-sso-oidc@3.600.0)': dependencies: '@aws-sdk/client-sso': 3.598.0 - '@aws-sdk/token-providers': 3.598.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0)) + '@aws-sdk/token-providers': 3.598.0(@aws-sdk/client-sso-oidc@3.600.0) '@aws-sdk/types': 3.598.0 '@smithy/property-provider': 3.1.2 '@smithy/shared-ini-file-loader': 3.1.2 @@ -5976,9 +6033,9 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-web-identity@3.598.0(@aws-sdk/client-sts@3.600.0)': + '@aws-sdk/credential-provider-web-identity@3.598.0(@aws-sdk/client-sts@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))': dependencies: - '@aws-sdk/client-sts': 3.600.0 + '@aws-sdk/client-sts': 3.600.0(@aws-sdk/client-sso-oidc@3.600.0) '@aws-sdk/types': 3.598.0 '@smithy/property-provider': 3.1.2 '@smithy/types': 3.2.0 @@ -6106,9 +6163,9 @@ snapshots: '@smithy/types': 3.2.0 tslib: 2.6.2 - '@aws-sdk/token-providers@3.598.0(@aws-sdk/client-sso-oidc@3.600.0(@aws-sdk/client-sts@3.600.0))': + '@aws-sdk/token-providers@3.598.0(@aws-sdk/client-sso-oidc@3.600.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.600.0(@aws-sdk/client-sts@3.600.0) + '@aws-sdk/client-sso-oidc': 3.600.0 '@aws-sdk/types': 3.598.0 '@smithy/property-provider': 3.1.2 '@smithy/shared-ini-file-loader': 3.1.2 @@ -6381,7 +6438,7 @@ snapshots: '@babel/template@7.24.6': dependencies: - '@babel/code-frame': 7.24.6 + '@babel/code-frame': 7.24.7 '@babel/parser': 7.24.6 '@babel/types': 7.24.6 @@ -6393,7 +6450,7 @@ snapshots: '@babel/traverse@7.24.6': dependencies: - '@babel/code-frame': 7.24.6 + '@babel/code-frame': 7.24.7 '@babel/generator': 7.24.6 '@babel/helper-environment-visitor': 7.24.6 '@babel/helper-function-name': 7.24.6 @@ -8319,9 +8376,9 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.6.3 - '@testing-library/dom@10.1.0': + '@testing-library/dom@10.2.0': dependencies: - '@babel/code-frame': 7.24.6 + '@babel/code-frame': 7.24.7 '@babel/runtime': 7.24.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -8330,10 +8387,23 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 + '@testing-library/jest-dom@6.4.6(vitest@1.6.0(@types/node@20.14.8)(jsdom@24.1.0)(terser@5.31.1))': + dependencies: + '@adobe/css-tools': 4.4.0 + '@babel/runtime': 7.24.6 + aria-query: 5.3.0 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + optionalDependencies: + vitest: 1.6.0(@types/node@20.14.8)(jsdom@24.1.0)(terser@5.31.1) + '@testing-library/react@15.0.7(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@testing-library/dom': 10.1.0 + '@testing-library/dom': 10.2.0 '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -9330,6 +9400,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css.escape@1.5.1: {} + cssesc@3.0.0: {} cssstyle@4.0.1: @@ -9429,6 +9501,8 @@ snapshots: dom-accessibility-api@0.5.16: {} + dom-accessibility-api@0.6.3: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.24.6 @@ -10174,6 +10248,8 @@ snapshots: imurmurhash@0.1.4: {} + indent-string@4.0.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -10453,6 +10529,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.17.21: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -10527,6 +10605,8 @@ snapshots: mimic-fn@4.0.0: {} + min-indent@1.0.1: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -10755,7 +10835,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.24.6 + '@babel/code-frame': 7.24.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -11129,6 +11209,11 @@ snapshots: dependencies: picomatch: 2.3.1 + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -11390,6 +11475,10 @@ snapshots: strip-final-newline@3.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@3.1.1: {} strip-literal@2.1.0: diff --git a/src/common/ui/utils/context.ts b/src/common/ui/utils/context.ts index fc72ba66..8cf89d43 100644 --- a/src/common/ui/utils/context.ts +++ b/src/common/ui/utils/context.ts @@ -1,13 +1,13 @@ -import { CourseDoesNotExistError } from "@/src/courses/domain/models/course-errors"; import type { Context } from "react"; import { createContext, useContext } from "react"; +import { ReactContextNotFoundError } from "../models/context-errors"; export const createNullContext = () => createContext(null); export function createContextHook(context: Context) { return () => { const value = useContext(context); - if (!value) throw new CourseDoesNotExistError(); + if (!value) throw new ReactContextNotFoundError(); return value; }; } diff --git a/src/common/ui/utils/shadcn.test.ts b/src/common/ui/utils/shadcn.test.ts new file mode 100644 index 00000000..24a3cb84 --- /dev/null +++ b/src/common/ui/utils/shadcn.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "vitest"; +import { cn } from "./shadcn"; + +describe("cn", () => { + test("does nothing when receives a single class", () => { + expect(cn("flex-col")).toBe("flex-col"); + }); + + test("merges two classes and adds a space in between", () => { + expect(cn("btn", "btn-primary")).toBe("btn btn-primary"); + }); + + test("handles conditional class inputs", () => { + let isActive = true; + expect(cn("btn", isActive && "active")).toBe("btn active"); + isActive = false; + expect(cn("btn", isActive && "active")).toBe("btn"); + }); + + test("removes duplicate classes", () => { + expect(cn("text-red", "text-red")).toBe("text-red"); + }); + + test("merges TailwindCSS utility classes correctly", () => { + expect(cn("text-center", "text-center md:text-left")).toBe( + "text-center md:text-left", + ); + }); + + test("handles array inputs", () => { + expect(cn(["btn", "btn-primary"])).toBe("btn btn-primary"); + }); + + test("handles object inputs", () => { + expect(cn({ flex: true, "btn-primary": false, active: true })).toBe( + "flex active", + ); + }); +}); diff --git a/src/tags/data/collections/tags-collection.ts b/src/tags/data/collections/tags-collection.ts index 3d7bc224..8faaa88b 100644 --- a/src/tags/data/collections/tags-collection.ts +++ b/src/tags/data/collections/tags-collection.ts @@ -1,5 +1,8 @@ import { collection } from "@/src/common/data/utils/mongo"; +/** + * Data of a tag as it is stored in the database. + */ interface TagDoc { name: string; } diff --git a/test/utils/setup.ts b/test/utils/setup.ts new file mode 100644 index 00000000..3d7c85ed --- /dev/null +++ b/test/utils/setup.ts @@ -0,0 +1,9 @@ +import { expect, afterEach } from 'vitest'; +import { cleanup } from '@testing-library/react'; +import * as matchers from "@testing-library/jest-dom/matchers" + +expect.extend(matchers); + +afterEach(() => { + cleanup(); +}); \ No newline at end of file diff --git a/vitest.config.mts b/vitest.config.mts index 03aa87e9..5b13cb2b 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -10,12 +10,14 @@ export default defineConfig(() => { // @ts-ignore plugins: [react(), tsconfigPaths()], test: { + globals: true, environment: "jsdom", include: [ "./app/**/*.{test,spec}.{ts,tsx}", "./src/**/*.{test,spec}.{ts,tsx}", "./test/**/*.{test,spec}.{ts,tsx}", ], + setupFiles: "./test/utils/setup.ts", }, }); }); From f97b239c2b1b9a91a32b65e7be50e16f3f2589e5 Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 20:26:31 +0200 Subject: [PATCH 02/11] feat: react utilstests --- src/common/ui/utils/context.test.tsx | 38 ++++++++++++++++++++++++++++ test/types/vitest.d.ts | 10 ++++++++ test/utils/mock-console.ts | 4 +++ test/utils/setup.ts | 8 +++--- tsconfig.json | 3 ++- 5 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 src/common/ui/utils/context.test.tsx create mode 100644 test/types/vitest.d.ts create mode 100644 test/utils/mock-console.ts diff --git a/src/common/ui/utils/context.test.tsx b/src/common/ui/utils/context.test.tsx new file mode 100644 index 00000000..6019b3c3 --- /dev/null +++ b/src/common/ui/utils/context.test.tsx @@ -0,0 +1,38 @@ +import { mockConsoleError } from "@/test/utils/mock-console"; +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { ReactContextNotFoundError } from "../models/context-errors"; +import { createContextHook, createNullContext } from "./context"; // Adjust the import path as necessary + +describe("createContextHook", () => { + it("throws CourseDoesNotExistError when context value is null", () => { + const TestContext = createNullContext(); + const useTestContext = createContextHook(TestContext); + const TestComponent = () => { + const value = useTestContext(); + return
{value}
; + }; + + const mock = mockConsoleError(); + expect(() => render()).toThrow(ReactContextNotFoundError); + mock.mockRestore(); + }); + + it("returns the context value when it is not null", () => { + const TestContext = createNullContext(); + const useTestContext = createContextHook(TestContext); + const TestComponent = () => { + const value = useTestContext(); + return
{value}
; + }; + + render( + + + , + ); + + expect(screen.getByText("Test Value")).toBeInTheDocument(); + }); +}); diff --git a/test/types/vitest.d.ts b/test/types/vitest.d.ts new file mode 100644 index 00000000..6f2e8541 --- /dev/null +++ b/test/types/vitest.d.ts @@ -0,0 +1,10 @@ +import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers"; +import "vitest"; + +declare module "vitest" { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + interface Assertion extends TestingLibraryMatchers {} + interface AsymmetricMatchersContaining + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extends TestingLibraryMatchers {} +} diff --git a/test/utils/mock-console.ts b/test/utils/mock-console.ts new file mode 100644 index 00000000..7cac708d --- /dev/null +++ b/test/utils/mock-console.ts @@ -0,0 +1,4 @@ +import { vi } from "vitest"; + +export const mockConsoleError = () => + vi.spyOn(console, "error").mockImplementation(() => undefined); diff --git a/test/utils/setup.ts b/test/utils/setup.ts index 3d7c85ed..971941d9 100644 --- a/test/utils/setup.ts +++ b/test/utils/setup.ts @@ -1,9 +1,9 @@ -import { expect, afterEach } from 'vitest'; -import { cleanup } from '@testing-library/react'; -import * as matchers from "@testing-library/jest-dom/matchers" +import { expect, afterEach } from "vitest"; +import { cleanup } from "@testing-library/react"; +import * as matchers from "@testing-library/jest-dom/matchers"; expect.extend(matchers); afterEach(() => { cleanup(); -}); \ No newline at end of file +}); diff --git a/tsconfig.json b/tsconfig.json index 4543081e..6f81f7c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,8 @@ "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", - "vitest.config.mts" + "vitest.config.mts", + "test/types/vitest.d.ts" ], "exclude": ["node_modules"] } From c48256ab2b0f6a6e8231009d77fc0f93f98ecf33 Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 20:31:37 +0200 Subject: [PATCH 03/11] fix: imports order --- test/utils/setup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/setup.ts b/test/utils/setup.ts index 971941d9..5bf8c89f 100644 --- a/test/utils/setup.ts +++ b/test/utils/setup.ts @@ -1,6 +1,6 @@ -import { expect, afterEach } from "vitest"; -import { cleanup } from "@testing-library/react"; import * as matchers from "@testing-library/jest-dom/matchers"; +import { cleanup } from "@testing-library/react"; +import { afterEach, expect } from "vitest"; expect.extend(matchers); From 4f4f8a1ba99851c80b2a166db6dd66488c17a2c6 Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 20:34:52 +0200 Subject: [PATCH 04/11] feat: tests --- src/common/data/utils/mongo.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/common/data/utils/mongo.test.ts diff --git a/src/common/data/utils/mongo.test.ts b/src/common/data/utils/mongo.test.ts new file mode 100644 index 00000000..05787d8a --- /dev/null +++ b/src/common/data/utils/mongo.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { collection } from "./mongo"; + +interface MockDoc { + foo: string; + bar: number; +} + +describe("collection", () => { + it("should return an object with the correct name property", () => { + const collectionName = "testCollection"; + const result = collection(collectionName); + expect(result.name).toBe(collectionName); + }); +}); From dc69e08a8e653640f4e5d0f5fff3a510393a9c05 Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 20:38:24 +0200 Subject: [PATCH 05/11] feat: util test --- src/common/domain/utils/array.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/common/domain/utils/array.test.ts diff --git a/src/common/domain/utils/array.test.ts b/src/common/domain/utils/array.test.ts new file mode 100644 index 00000000..41fa9443 --- /dev/null +++ b/src/common/domain/utils/array.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { shuffle } from "./array"; + +const createArray = (length: number) => Array.from({ length }, (_, i) => i); + +describe("shuffle", () => { + it("should return an array of the same length", () => { + const array = createArray(10); + const shuffledArray = shuffle(array); + expect(shuffledArray).toHaveLength(array.length); + }); + + it("should contain the same elements", () => { + const array = createArray(42); + const shuffledArray = shuffle(array); + expect(shuffledArray.sort()).toEqual(array.sort()); + }); +}); From 722c557333c9f240fb8ff2d1c9f1dd8b937af893 Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 20:43:37 +0200 Subject: [PATCH 06/11] feat: test util function --- ...mail-verification-codes-repository-impl.ts | 2 +- .../components/forgot-password-form.tsx | 4 ++-- .../components/reset-password-form.tsx | 4 ++-- .../actions/login-with-password-action.ts | 2 +- src/auth/ui/login/components/login-form.tsx | 8 ++++---- src/auth/ui/signup/components/signup-form.tsx | 2 +- .../components/verify-email-form.tsx | 6 +++--- src/common/domain/utils/promise.test.ts | 20 +++++++++++++++++++ .../domain/utils/{promises.ts => promise.ts} | 0 .../profile-courses-results-section.tsx | 2 +- .../components/discover-results-section.tsx | 2 +- .../components/course-notes-loaded.tsx | 2 +- .../ui/contexts/task-queue-context.tsx | 2 +- .../ui/actions/change-password-action.ts | 2 +- src/settings/ui/actions/delete-user-action.ts | 2 +- 15 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 src/common/domain/utils/promise.test.ts rename src/common/domain/utils/{promises.ts => promise.ts} (100%) diff --git a/src/auth/data/repositories/email-verification-codes-repository-impl.ts b/src/auth/data/repositories/email-verification-codes-repository-impl.ts index 094e4907..816123b9 100644 --- a/src/auth/data/repositories/email-verification-codes-repository-impl.ts +++ b/src/auth/data/repositories/email-verification-codes-repository-impl.ts @@ -1,5 +1,5 @@ import type { DatabaseService } from "@/src/common/domain/interfaces/database-service"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { ObjectId } from "mongodb"; import { TimeSpan, createDate } from "oslo"; import { alphabet, generateRandomString } from "oslo/crypto"; diff --git a/src/auth/ui/forgot-password/components/forgot-password-form.tsx b/src/auth/ui/forgot-password/components/forgot-password-form.tsx index b40bdf02..ced584d1 100644 --- a/src/auth/ui/forgot-password/components/forgot-password-form.tsx +++ b/src/auth/ui/forgot-password/components/forgot-password-form.tsx @@ -1,9 +1,10 @@ "use client"; import { clientLocator } from "@/src/common/di/client-locator"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { FormGlobalErrorMessage } from "@/src/common/ui/components/form/form-global-error-message"; import { FormSubmitButton } from "@/src/common/ui/components/form/form-submit-button"; +import { InputFormField } from "@/src/common/ui/components/form/input-form-field"; import { Button } from "@/src/common/ui/components/shadcn/ui/button"; import { FormResponseHandler } from "@/src/common/ui/models/server-form-errors"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -14,7 +15,6 @@ import { FormProvider, useForm } from "react-hook-form"; import { forgotPasswordAction } from "../actions/forgot-password-action"; import type { ForgotPasswordActionModel } from "../schemas/forgot-password-action-schema"; import { ForgotPasswordActionSchema } from "../schemas/forgot-password-action-schema"; -import { InputFormField } from "@/src/common/ui/components/form/input-form-field"; const ForgotPasswordConfirmDialog = dynamic(() => import("./forgot-password-confirm-dialog").then( diff --git a/src/auth/ui/forgot-password/components/reset-password-form.tsx b/src/auth/ui/forgot-password/components/reset-password-form.tsx index b8de4305..6f8d4d41 100644 --- a/src/auth/ui/forgot-password/components/reset-password-form.tsx +++ b/src/auth/ui/forgot-password/components/reset-password-form.tsx @@ -6,8 +6,9 @@ import { FormSubmitButton } from "@/src/common/ui/components/form/form-submit-bu import { Button } from "@/src/common/ui/components/shadcn/ui/button"; import { clientLocator } from "@/src/common/di/client-locator"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { PasswordSchema } from "@/src/common/schemas/password-schema"; +import { PasswordInputFormField } from "@/src/common/ui/components/form/password-input-form-field"; import { FormResponseHandler } from "@/src/common/ui/models/server-form-errors"; import { zodResolver } from "@hookform/resolvers/zod"; import dynamic from "next/dynamic"; @@ -15,7 +16,6 @@ import Link from "next/link"; import { useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { resetPasswordAction } from "../actions/reset-password-action"; -import { PasswordInputFormField } from "@/src/common/ui/components/form/password-input-form-field"; const ResetPasswordConfirmDialog = dynamic(() => import("./reset-password-confirm-dialog").then( diff --git a/src/auth/ui/login/actions/login-with-password-action.ts b/src/auth/ui/login/actions/login-with-password-action.ts index 8d742c8c..b91a328b 100644 --- a/src/auth/ui/login/actions/login-with-password-action.ts +++ b/src/auth/ui/login/actions/login-with-password-action.ts @@ -4,7 +4,7 @@ import { IncorrectPasswordError, UserDoesNotExistError, } from "@/src/auth/domain/errors/auth-errors"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { ActionErrorHandler } from "@/src/common/ui/actions/action-error-handler"; import { ActionResponse } from "@/src/common/ui/models/server-form-errors"; import { redirect } from "next/navigation"; diff --git a/src/auth/ui/login/components/login-form.tsx b/src/auth/ui/login/components/login-form.tsx index 1e7bb9b4..c71ecee8 100644 --- a/src/auth/ui/login/components/login-form.tsx +++ b/src/auth/ui/login/components/login-form.tsx @@ -3,9 +3,12 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { FormProvider, useForm } from "react-hook-form"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { clientLocator } from "@/src/common/di/client-locator"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { FormGlobalErrorMessage } from "@/src/common/ui/components/form/form-global-error-message"; import { FormSubmitButton } from "@/src/common/ui/components/form/form-submit-button"; +import { InputFormField } from "@/src/common/ui/components/form/input-form-field"; +import { PasswordInputFormField } from "@/src/common/ui/components/form/password-input-form-field"; import { Button } from "@/src/common/ui/components/shadcn/ui/button"; import { FormResponseHandler } from "@/src/common/ui/models/server-form-errors"; import { textStyles } from "@/src/common/ui/styles/text-styles"; @@ -14,9 +17,6 @@ import Link from "next/link"; import { loginWithPasswordAction } from "../actions/login-with-password-action"; import type { LoginWithPasswordActionModel } from "../schemas/login-with-password-action-schema"; import { LoginWithPasswordActionSchema } from "../schemas/login-with-password-action-schema"; -import { clientLocator } from "@/src/common/di/client-locator"; -import { InputFormField } from "@/src/common/ui/components/form/input-form-field"; -import { PasswordInputFormField } from "@/src/common/ui/components/form/password-input-form-field"; export function LoginForm() { const form = useForm({ diff --git a/src/auth/ui/signup/components/signup-form.tsx b/src/auth/ui/signup/components/signup-form.tsx index e00d177d..e1cd6886 100644 --- a/src/auth/ui/signup/components/signup-form.tsx +++ b/src/auth/ui/signup/components/signup-form.tsx @@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { FormProvider, useForm } from "react-hook-form"; import { clientLocator } from "@/src/common/di/client-locator"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { EmailSchema } from "@/src/common/schemas/email-schema"; import { PasswordSchema } from "@/src/common/schemas/password-schema"; import { CheckboxFormField } from "@/src/common/ui/components/form/checkbox-form-field"; diff --git a/src/auth/ui/verify-email/components/verify-email-form.tsx b/src/auth/ui/verify-email/components/verify-email-form.tsx index 8ebe6ce4..d599fb4c 100644 --- a/src/auth/ui/verify-email/components/verify-email-form.tsx +++ b/src/auth/ui/verify-email/components/verify-email-form.tsx @@ -4,16 +4,16 @@ import { z } from "@/i18n/zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { FormProvider, useForm } from "react-hook-form"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { clientLocator } from "@/src/common/di/client-locator"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { AsyncButton } from "@/src/common/ui/components/button/async-button"; import { FormGlobalErrorMessage } from "@/src/common/ui/components/form/form-global-error-message"; import { FormSubmitButton } from "@/src/common/ui/components/form/form-submit-button"; +import { InputOtpFormField } from "@/src/common/ui/components/form/input-otp-form-field"; import { FormResponseHandler } from "@/src/common/ui/models/server-form-errors"; import { useEffect, useRef } from "react"; import { logoutAction } from "../../actions/logout-action"; import { verifyEmailAction } from "../actions/verify-email-action"; -import { clientLocator } from "@/src/common/di/client-locator"; -import { InputOtpFormField } from "@/src/common/ui/components/form/input-otp-form-field"; const FormSchema = z.object({ code: z.string().length(6), diff --git a/src/common/domain/utils/promise.test.ts b/src/common/domain/utils/promise.test.ts new file mode 100644 index 00000000..41b4c8e4 --- /dev/null +++ b/src/common/domain/utils/promise.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it, vi } from "vitest"; +import { waitMilliseconds } from "./promise"; + +describe("waitMilliseconds", () => { + it("should call setTimeout with the correct number of milliseconds", async () => { + const mock = vi.spyOn(global, "setTimeout"); + const ms = 100; + await waitMilliseconds(ms); + expect(mock).toHaveBeenCalledWith(expect.anything(), ms); + mock.mockRestore(); + }); + + it("should wait for at least the specified number of milliseconds", async () => { + const start = performance.now(); + const ms = 80; + await waitMilliseconds(ms); + const end = performance.now(); + expect(end - start).toBeGreaterThanOrEqual(ms); + }); +}); diff --git a/src/common/domain/utils/promises.ts b/src/common/domain/utils/promise.ts similarity index 100% rename from src/common/domain/utils/promises.ts rename to src/common/domain/utils/promise.ts diff --git a/src/courses/ui/profile-courses/components/profile-courses-results-section.tsx b/src/courses/ui/profile-courses/components/profile-courses-results-section.tsx index d5928874..5ad05af5 100644 --- a/src/courses/ui/profile-courses/components/profile-courses-results-section.tsx +++ b/src/courses/ui/profile-courses/components/profile-courses-results-section.tsx @@ -2,7 +2,7 @@ import type { TokenPaginationModelData } from "@/src/common/domain/models/token-pagination-model"; import { TokenPaginationModel } from "@/src/common/domain/models/token-pagination-model"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { PaginationEmptyState } from "@/src/common/ui/components/empty-state/pagination-empty-state"; import { Skeleton } from "@/src/common/ui/components/shadcn/ui/skeleton"; import { FormResponseHandler } from "@/src/common/ui/models/server-form-errors"; diff --git a/src/discover/ui/components/discover-results-section.tsx b/src/discover/ui/components/discover-results-section.tsx index 3891b3cf..56c37864 100644 --- a/src/discover/ui/components/discover-results-section.tsx +++ b/src/discover/ui/components/discover-results-section.tsx @@ -2,7 +2,7 @@ import type { TokenPaginationModelData } from "@/src/common/domain/models/token-pagination-model"; import { TokenPaginationModel } from "@/src/common/domain/models/token-pagination-model"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { PaginationEmptyState } from "@/src/common/ui/components/empty-state/pagination-empty-state"; import { Skeleton } from "@/src/common/ui/components/shadcn/ui/skeleton"; import { FormResponseHandler } from "@/src/common/ui/models/server-form-errors"; diff --git a/src/notes/ui/course-notes/components/course-notes-loaded.tsx b/src/notes/ui/course-notes/components/course-notes-loaded.tsx index bfb2b112..68c55374 100644 --- a/src/notes/ui/course-notes/components/course-notes-loaded.tsx +++ b/src/notes/ui/course-notes/components/course-notes-loaded.tsx @@ -2,7 +2,7 @@ import type { PaginationModelData } from "@/src/common/domain/models/pagination-model"; import { PaginationModel } from "@/src/common/domain/models/pagination-model"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { PaginationEmptyState } from "@/src/common/ui/components/empty-state/pagination-empty-state"; import { Skeleton } from "@/src/common/ui/components/shadcn/ui/skeleton"; import { FormResponseHandler } from "@/src/common/ui/models/server-form-errors"; diff --git a/src/practice/ui/contexts/task-queue-context.tsx b/src/practice/ui/contexts/task-queue-context.tsx index a0a28035..02fba041 100644 --- a/src/practice/ui/contexts/task-queue-context.tsx +++ b/src/practice/ui/contexts/task-queue-context.tsx @@ -1,6 +1,6 @@ "use client"; import { clientLocator } from "@/src/common/di/client-locator"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { createContextHook, createNullContext, diff --git a/src/settings/ui/actions/change-password-action.ts b/src/settings/ui/actions/change-password-action.ts index 5bea38a2..b5251d5f 100644 --- a/src/settings/ui/actions/change-password-action.ts +++ b/src/settings/ui/actions/change-password-action.ts @@ -1,7 +1,7 @@ "use server"; import { authLocator } from "@/src/auth/auth-locator"; import { IncorrectPasswordError } from "@/src/auth/domain/errors/auth-errors"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { ActionErrorHandler } from "@/src/common/ui/actions/action-error-handler"; import { ActionResponse } from "@/src/common/ui/models/server-form-errors"; import type { ChangePasswordActionModel } from "../schemas/change-password-action-schema"; diff --git a/src/settings/ui/actions/delete-user-action.ts b/src/settings/ui/actions/delete-user-action.ts index 54b430f8..0aba1ce1 100644 --- a/src/settings/ui/actions/delete-user-action.ts +++ b/src/settings/ui/actions/delete-user-action.ts @@ -4,7 +4,7 @@ import { IncorrectPasswordError, InvalidConfirmationError, } from "@/src/auth/domain/errors/auth-errors"; -import { waitMilliseconds } from "@/src/common/domain/utils/promises"; +import { waitMilliseconds } from "@/src/common/domain/utils/promise"; import { ActionErrorHandler } from "@/src/common/ui/actions/action-error-handler"; import { ActionResponse } from "@/src/common/ui/models/server-form-errors"; import type { DeleteUserActionModel } from "../schemas/delete-user-action-schema"; From 1834030e2b4cd9d997c9175eaac9074f199e6393 Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 23:07:20 +0200 Subject: [PATCH 07/11] feat: remove unused dependency and add tests to utils --- package.json | 1 - pnpm-lock.yaml | 3 --- src/common/di/locator-utils.test.ts | 30 +++++++++++++++++++++++++++++ src/common/di/locator-utils.ts | 16 +++++++++++---- 4 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 src/common/di/locator-utils.test.ts diff --git a/package.json b/package.json index f841ae10..f90a75ea 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "jsdom": "^24.1.0", "lucia": "^3.2.0", "lucide-react": "^0.396.0", - "memoize-one": "^6.0.0", "mongodb": "^6.7.0", "next": "^14.2.4", "next-themes": "^0.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27b9f165..2cb7d1c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,9 +119,6 @@ importers: lucide-react: specifier: ^0.396.0 version: 0.396.0(react@18.3.1) - memoize-one: - specifier: ^6.0.0 - version: 6.0.0 mongodb: specifier: ^6.7.0 version: 6.7.0 diff --git a/src/common/di/locator-utils.test.ts b/src/common/di/locator-utils.test.ts new file mode 100644 index 00000000..fd8248e9 --- /dev/null +++ b/src/common/di/locator-utils.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { singleton } from "./locator-utils"; + +describe("singleton", () => { + it("should always return the same result", () => { + const memoizedFn = singleton(() => "test1"); + const result1 = memoizedFn(); + const result2 = memoizedFn(); + const result3 = memoizedFn(); + + expect(result1).toBe("test1"); + expect(result1).toBe(result2); + expect(result2).toBe(result3); + + const memoizedFn2 = singleton(() => Symbol("test1")); + const result4 = memoizedFn2(); + const result5 = memoizedFn2(); + expect(result4).toBe(result5); + }); + + it("should always return the same result when the function is asynchronous", async () => { + const memoizedFn = singleton(async () => Symbol("test2")); + const result1 = await memoizedFn(); + const result2 = await memoizedFn(); + const result3 = await memoizedFn(); + + expect(result1).toBe(result2); + expect(result2).toBe(result3); + }); +}); diff --git a/src/common/di/locator-utils.ts b/src/common/di/locator-utils.ts index fb40ee47..6e6469f8 100644 --- a/src/common/di/locator-utils.ts +++ b/src/common/di/locator-utils.ts @@ -1,8 +1,16 @@ -import memoizeOne from "memoize-one"; - /** * Memoizes the result of a function. - * @param resultFn The function to memoize. + * @param fn The function to memoize. * @returns The memoized function. */ -export const singleton = memoizeOne; +export function singleton(fn: () => T ) { + let isCached = false; + let cachedResult: T; + return () => { + if (!isCached) { + isCached = true; + cachedResult = fn(); + } + return cachedResult; + } +} \ No newline at end of file From fbfe0156f2d9382f84e1c3172900a8e13d1f5dec Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 23:07:44 +0200 Subject: [PATCH 08/11] chore: format --- src/common/di/locator-utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/di/locator-utils.ts b/src/common/di/locator-utils.ts index 6e6469f8..69a44b06 100644 --- a/src/common/di/locator-utils.ts +++ b/src/common/di/locator-utils.ts @@ -3,7 +3,7 @@ * @param fn The function to memoize. * @returns The memoized function. */ -export function singleton(fn: () => T ) { +export function singleton(fn: () => T) { let isCached = false; let cachedResult: T; return () => { @@ -12,5 +12,5 @@ export function singleton(fn: () => T ) { cachedResult = fn(); } return cachedResult; - } -} \ No newline at end of file + }; +} From 0407330d5c03d2d2bffeac7fad2a4a272249d94b Mon Sep 17 00:00:00 2001 From: Arnau Date: Wed, 26 Jun 2024 23:24:34 +0200 Subject: [PATCH 09/11] fix: test utility function --- .../utils/handle-promise-errors.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/common/utils/handle-promise-errors.test.ts diff --git a/src/common/utils/handle-promise-errors.test.ts b/src/common/utils/handle-promise-errors.test.ts new file mode 100644 index 00000000..a2c2721d --- /dev/null +++ b/src/common/utils/handle-promise-errors.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it, vi } from "vitest"; +import { handlePromiseError } from "./handle-promise-error"; + +const captureError = vi.fn(); +vi.mock("../di/client-locator", () => ({ + clientLocator: { + ErrorTrackingService: () => ({ + captureError, + }), + }, +})); + +describe("handlePromiseError", () => { + it("returns the resolved value of the promise", async () => { + const mockValue = "test1"; + const promise = Promise.resolve(mockValue); + const result = await handlePromiseError(promise); + expect(result).toBe(mockValue); + expect(captureError).not.toHaveBeenCalled(); + captureError.mockClear(); + }); + + it("returns undefined when the promise rejects with an error and captures the error with the ErrorTrackingService", async () => { + const mockError = new Error("Test Error"); + const promise = Promise.reject(mockError); + const result = await handlePromiseError(promise); + expect(result).toBeUndefined(); + expect(captureError).toHaveBeenCalled(); + captureError.mockClear(); + }); +}); From af918c1bc508d17b1f4908e38498d3721c4f4881 Mon Sep 17 00:00:00 2001 From: Arnau Date: Thu, 27 Jun 2024 08:57:38 +0200 Subject: [PATCH 10/11] feat: course model tests --- src/auth/domain/models/user-model.test.ts | 44 ++++++ src/auth/domain/models/user-model.ts | 4 +- .../domain/models/course-model.test.ts | 134 ++++++++++++++++++ 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/auth/domain/models/user-model.test.ts create mode 100644 src/courses/domain/models/course-model.test.ts diff --git a/src/auth/domain/models/user-model.test.ts b/src/auth/domain/models/user-model.test.ts new file mode 100644 index 00000000..dc6993b7 --- /dev/null +++ b/src/auth/domain/models/user-model.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { AuthTypeModel } from "./auth-type-model"; +import type { UserModelData } from "./user-model"; +import { UserModel } from "./user-model"; + +describe("UserModel", () => { + const mockData: UserModelData = { + id: "user-123", + email: "test@example.com", + authTypes: [AuthTypeModel.email], + isEmailVerified: true, + }; + + it("should instantiate correctly with provided data", () => { + const user = new UserModel(mockData); + expect(user).toBeInstanceOf(UserModel); + }); + + it("should have getters id, email and authTypes and isEmailVerified that match the values of the constructor argument", () => { + const user = new UserModel(mockData); + expect(user.id).toBe(mockData.id); + expect(user.email).toBe(mockData.email); + expect(user.authTypes).toEqual(mockData.authTypes); + expect(user.isEmailVerified).toBe(mockData.isEmailVerified); + }); + + it("isEmailVerified should be false if the data value is undefined", () => { + const userDataWithoutEmailVerification = { + ...mockData, + isEmailVerified: undefined, + }; + const user = new UserModel(userDataWithoutEmailVerification); + expect(user.isEmailVerified).toBe(false); + }); + + it("isEmailVerified should be false if the data value is false", () => { + const userDataWithEmailVerificationFalse = { + ...mockData, + isEmailVerified: false, + }; + const user = new UserModel(userDataWithEmailVerificationFalse); + expect(user.isEmailVerified).toBe(false); + }); +}); diff --git a/src/auth/domain/models/user-model.ts b/src/auth/domain/models/user-model.ts index 3d838104..2f9911cc 100644 --- a/src/auth/domain/models/user-model.ts +++ b/src/auth/domain/models/user-model.ts @@ -1,6 +1,6 @@ import type { AuthTypeModel } from "./auth-type-model"; -interface UserModelData { +export interface UserModelData { id: string; email: string; authTypes: AuthTypeModel[]; @@ -36,6 +36,6 @@ export class UserModel { * Whether the user has verified their email address */ get isEmailVerified() { - return this.data.isEmailVerified; + return this.data.isEmailVerified ?? false; } } diff --git a/src/courses/domain/models/course-model.test.ts b/src/courses/domain/models/course-model.test.ts new file mode 100644 index 00000000..ffbbf246 --- /dev/null +++ b/src/courses/domain/models/course-model.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from "vitest"; +import type { CourseModelData } from "./course-model"; +import { CourseModel } from "./course-model"; +import { CoursePermissionTypeModel } from "./course-permission-type-model"; + +describe("CourseModel", () => { + const mockData: CourseModelData = { + id: "course-123", + name: "Test Course", + description: "A course for testing", + picture: "https://example.com/picture.jpg", + isPublic: true, + permissionType: CoursePermissionTypeModel.own, + enrollment: null, + tags: ["maths", "science", "english"], + }; + + it("should have getters that match the data values", () => { + const course = new CourseModel(mockData); + expect(course.id).toBe(mockData.id); + expect(course.name).toBe(mockData.name); + expect(course.description).toBe(mockData.description); + expect(course.picture).toBe(mockData.picture); + expect(course.isPublic).toBe(mockData.isPublic); + expect(course.permissionType).toBe(mockData.permissionType); + expect(course.tags).toEqual(mockData.tags); + expect(course.canView).toBe(true); + expect(course.canEdit).toBe(true); + expect(course.canDelete).toBe(true); + expect(course.isOwner).toBe(true); + expect(course.enrollment).toBeNull(); + expect(course.isEnrolled).toBe(false); + }); + + it("canDelete is true only if permissionType is own", () => { + const courseOwner = new CourseModel({ + ...mockData, + permissionType: CoursePermissionTypeModel.own, + }); + expect(courseOwner.canDelete).toBe(true); + + const course2 = new CourseModel({ + ...mockData, + permissionType: CoursePermissionTypeModel.edit, + }); + expect(course2.canDelete).toBe(false); + + const course3 = new CourseModel({ + ...mockData, + permissionType: CoursePermissionTypeModel.edit, + }); + expect(course3.canDelete).toBe(false); + }); + + it("canView is true if course is public or if permissionType is not undefined", () => { + const course1 = new CourseModel({ + ...mockData, + permissionType: undefined, + isPublic: true, + }); + expect(course1.canView).toBe(true); + + const course2 = new CourseModel({ + ...mockData, + isPublic: false, + permissionType: CoursePermissionTypeModel.own, + }); + expect(course2.canView).toBe(true); + + const course3 = new CourseModel({ + ...mockData, + isPublic: false, + permissionType: CoursePermissionTypeModel.edit, + }); + expect(course3.canView).toBe(true); + + const course4 = new CourseModel({ + ...mockData, + isPublic: false, + permissionType: CoursePermissionTypeModel.view, + }); + expect(course4.canView).toBe(true); + + const course5 = new CourseModel({ + ...mockData, + isPublic: false, + permissionType: undefined, + }); + expect(course5.canView).toBe(false); + }); + + it("canEdit is true only if permissionType is edit or own", () => { + const courseWithOwnPermission = new CourseModel({ + ...mockData, + permissionType: CoursePermissionTypeModel.own, + }); + expect(courseWithOwnPermission.canEdit).toBe(true); + + const courseWithEditPermission = new CourseModel({ + ...mockData, + permissionType: CoursePermissionTypeModel.edit, + }); + expect(courseWithEditPermission.canEdit).toBe(true); + + const courseWithViewPermission = new CourseModel({ + ...mockData, + permissionType: CoursePermissionTypeModel.view, + }); + expect(courseWithViewPermission.canEdit).toBe(false); + + const courseWithNoPermission = new CourseModel({ + ...mockData, + permissionType: undefined, + }); + expect(courseWithNoPermission.canEdit).toBe(false); + }); + + it("isEnrolled is true if course has enrollment", () => { + const notEnrolledCourse = new CourseModel(mockData); + expect(notEnrolledCourse.isEnrolled).toBe(false); + + const enrolledData: CourseModelData = { + ...mockData, + enrollment: { + id: "", + profileId: "", + courseId: "course-123", + isFavorite: false, + }, + }; + const enrolledCourse = new CourseModel(enrolledData); + expect(enrolledCourse.isEnrolled).toBe(true); + }); +}); From 01e8b9ba039a39adcd12b876ccde74ad46a13f12 Mon Sep 17 00:00:00 2001 From: Arnau Date: Thu, 27 Jun 2024 09:00:58 +0200 Subject: [PATCH 11/11] fix: types error --- src/courses/domain/models/course-model.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/courses/domain/models/course-model.test.ts b/src/courses/domain/models/course-model.test.ts index ffbbf246..3a9b0134 100644 --- a/src/courses/domain/models/course-model.test.ts +++ b/src/courses/domain/models/course-model.test.ts @@ -55,8 +55,8 @@ describe("CourseModel", () => { it("canView is true if course is public or if permissionType is not undefined", () => { const course1 = new CourseModel({ ...mockData, - permissionType: undefined, isPublic: true, + permissionType: null, }); expect(course1.canView).toBe(true); @@ -84,7 +84,7 @@ describe("CourseModel", () => { const course5 = new CourseModel({ ...mockData, isPublic: false, - permissionType: undefined, + permissionType: null, }); expect(course5.canView).toBe(false); }); @@ -110,7 +110,7 @@ describe("CourseModel", () => { const courseWithNoPermission = new CourseModel({ ...mockData, - permissionType: undefined, + permissionType: null, }); expect(courseWithNoPermission.canEdit).toBe(false); });