diff --git a/astro.config.mjs b/astro.config.mjs index 7d5c233..21521de 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -11,7 +11,7 @@ export default defineConfig({ vite: { ssr: { noExternal: ["@patternfly/*", "react-dropzone"], - external: ["node:fs", "node:path", "fs/promises", "path"] + external: ["fs", "node:fs", "node:path", "path", "fs/promises"] }, server: { fs: { @@ -19,5 +19,7 @@ export default defineConfig({ } }, }, - adapter: cloudflare() + adapter: cloudflare({ + imageService: 'compile' + }) }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eb6c533..93a1640 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,16 +14,16 @@ "@astrojs/node": "^9.4.3", "@astrojs/react": "^4.3.0", "@nanostores/react": "^0.8.4", - "@patternfly/ast-helpers": "1.4.0-alpha.190", - "@patternfly/patternfly": "^6.0.0", - "@patternfly/quickstarts": "^6.0.0", - "@patternfly/react-code-editor": "^6.2.2", - "@patternfly/react-core": "^6.0.0", - "@patternfly/react-drag-drop": "^6.0.0", - "@patternfly/react-icons": "^6.0.0", - "@patternfly/react-styles": "^6.0.0", - "@patternfly/react-table": "^6.0.0", - "@patternfly/react-tokens": "^6.0.0", + "@patternfly/ast-helpers": "1.4.0-alpha.381", + "@patternfly/patternfly": "^6.5.2", + "@patternfly/quickstarts": "^6.5.0", + "@patternfly/react-code-editor": "^6.5.1", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-drag-drop": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", + "@patternfly/react-table": "^6.5.1", + "@patternfly/react-tokens": "^6.5.1", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "astro": "^5.15.9", @@ -46,8 +46,8 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@eslint/js": "^9.16.0", - "@patternfly/react-data-view": "^6.0.0", - "@patternfly/react-user-feedback": "^6.0.0", + "@patternfly/react-data-view": "^6.5.0", + "@patternfly/react-user-feedback": "^6.3.0", "@semantic-release/git": "^10.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", @@ -4032,31 +4032,31 @@ } }, "node_modules/@patternfly/ast-helpers": { - "version": "1.4.0-alpha.190", - "resolved": "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.4.0-alpha.190.tgz", - "integrity": "sha512-NEVd593xs/qvjodvUQmyBretUeMkI4AX0dLe8flQnHYig5rhL65Bpfo+T/Nljv3M/HDV0CrRduJCMtnvpEDiNQ==", + "version": "1.4.0-alpha.381", + "resolved": "https://registry.npmjs.org/@patternfly/ast-helpers/-/ast-helpers-1.4.0-alpha.381.tgz", + "integrity": "sha512-i9Y+zdGZePR5Ko8b2pI5TjeG73oEMRqqH+Jbqn16NKLp3uVSXgP+NtxFX/8vmlWJzc6Nn9gDxWvX2OIBPfZL/g==", "license": "MIT", "dependencies": { - "acorn": "^8.4.1", + "acorn": "^8.16.0", "acorn-class-fields": "^1.0.0", "acorn-jsx": "^5.3.2", "acorn-static-class-features": "^1.0.0", - "astring": "^1.7.5" + "astring": "^1.9.0" } }, "node_modules/@patternfly/patternfly": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.2.3.tgz", - "integrity": "sha512-FR027W7JygcQpvlRU/Iom936Vm0apzfi2o5lvtlcWW6IaeZCCTtTaDxehoYuELHlemzkLziQAgu6LuCJEVayjw==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.5.2.tgz", + "integrity": "sha512-yZ71+1gt1VGzUN5amjDNd9NvttTnSOm9M0JeBL0YX1KaWXW1bmDPzTWEM+vQXuC4LVK0msHmR5hKB7KVpamAkA==", "license": "MIT" }, "node_modules/@patternfly/quickstarts": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@patternfly/quickstarts/-/quickstarts-6.3.1.tgz", - "integrity": "sha512-cuQ+m0K90vbGyNo4oR8UToXo1Jw24QDfCaIoAW0pbUkEcYuSPGqVvrOSf7w5hUMJ8jrXqE7g0T7JkcQXElMbHg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@patternfly/quickstarts/-/quickstarts-6.5.0.tgz", + "integrity": "sha512-JYunrOwYF45uIZpcuMNVtoLyUKu1oB+qsUveL7lIduBVcWinw8rBXve+YnQTG7scis5kQONwJ8ZZh8W/5A37aw==", "license": "MIT", "dependencies": { - "dompurify": "^3.2.4", + "dompurify": "^3.3.2", "history": "^5.0.0" }, "peerDependencies": { @@ -4067,21 +4067,21 @@ } }, "node_modules/@patternfly/react-code-editor": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.2.2.tgz", - "integrity": "sha512-KPnkNP769afD2rvoNQtgCx+SYscamM5QSRmw2FJ9QPHVMksarwTsMvrdMxvu+n6Dhs/T40vQLU5UR7X2yPrURg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.5.1.tgz", + "integrity": "sha512-Epd4iAX7/Uzo+Vl+6gX6f1BjhoX06DVV7pnZnQK4N4uknyXbfIGkmOJcE0Eq06z1cxiITV/h/JHI6WqzbOSaGA==", "license": "MIT", "dependencies": { - "@monaco-editor/react": "^4.6.0", - "@patternfly/react-core": "^6.2.2", - "@patternfly/react-icons": "^6.2.2", - "@patternfly/react-styles": "^6.2.2", + "@monaco-editor/react": "^4.7.0", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", "react-dropzone": "14.3.5", "tslib": "^2.8.1" }, "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-component-groups": { @@ -4104,15 +4104,15 @@ } }, "node_modules/@patternfly/react-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.4.0.tgz", - "integrity": "sha512-zMgJmcFohp2FqgAoZHg7EXZS7gnaFESquk0qIavemYI0FsqspVlzV2/PUru7w+86+jXfqebRhgubPRsv1eJwEg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.5.1.tgz", + "integrity": "sha512-fFZ0hcIyHJO27hxbf53W3m2R11l0O9WxR7CusJXuCEaNMP31ULrhf5Pv6ROdTrrs39Kl/yPv+2QuxQfe/4e72g==", "license": "MIT", "dependencies": { - "@patternfly/react-icons": "^6.4.0", - "@patternfly/react-styles": "^6.4.0", - "@patternfly/react-tokens": "^6.4.0", - "focus-trap": "7.6.4", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", + "@patternfly/react-tokens": "^6.5.1", + "focus-trap": "7.6.6", "react-dropzone": "^14.3.5", "tslib": "^2.8.1" }, @@ -4122,9 +4122,9 @@ } }, "node_modules/@patternfly/react-data-view": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.4.0.tgz", - "integrity": "sha512-AYIJvWLSoZaf3askvBjyyFQEvSCiquw5PFzEOiTsNoM2pDYkRagzppjclpI+MRJr44ZrfpljC6ZKE4f5Ni2p+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-6.5.0.tgz", + "integrity": "sha512-QTj8eg/pwchdxSpapqU3a6MZG7syiwzXy1La2hgAJtvQTR9ltkIg/GJJ4ota2O5wHzdSAVzTTdxiNuP33wmvbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4141,28 +4141,28 @@ } }, "node_modules/@patternfly/react-drag-drop": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.2.2.tgz", - "integrity": "sha512-MBPkOz6VitwmRzjJIbS4i8/AML35PqSs5sBXZRk8ryCq/CgZ2j0zV2B5L7Z8UMwHRNdIlNJDIrUvTo+J1A9KsA==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.5.1.tgz", + "integrity": "sha512-10mo7ETrD4voyIUpzjtX8Z+cuQYnXPOHV5vuqhInArgSUiVi7OOqSJFG6iBnn7OCSKAMMakHdHkHRtOLiOO1Mw==", "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", - "@patternfly/react-core": "^6.2.2", - "@patternfly/react-icons": "^6.2.2", - "@patternfly/react-styles": "^6.2.2", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-icons": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.4.0.tgz", - "integrity": "sha512-SPjzatm73NUYv/BL6A/cjRA5sFQ15NkiyPAcT8gmklI7HY+ptd6/eg49uBDFmxTQcSwbb5ISW/R6wwCQBY2M+Q==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.5.1.tgz", + "integrity": "sha512-CnuPvTTs4MMWx8CAUkmnY690ouN1bbHjunsyXu3QxvGOmzbztP+wS4BdiLS8TIXOIH80Yb7KPhnF8VkA+CduOA==", "license": "MIT", "peerDependencies": { "react": "^17 || ^18 || ^19", @@ -4170,22 +4170,22 @@ } }, "node_modules/@patternfly/react-styles": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.4.0.tgz", - "integrity": "sha512-EXmHA67s5sy+Wy/0uxWoUQ52jr9lsH2wV3QcgtvVc5zxpyBX89gShpqv4jfVqaowznHGDoL6fVBBrSe9BYOliQ==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.5.1.tgz", + "integrity": "sha512-yQMzUbbf6qYM/v3JbPvaCJTgxRbOKoEw229XZmnnM8gDvp8ECiI7LqihrAOK/NA6T6M3DDgsRMd2JurUBhPDEw==", "license": "MIT" }, "node_modules/@patternfly/react-table": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.4.0.tgz", - "integrity": "sha512-yv0sFOLGts8a2q9C1xUegjp50ayYyVRe0wKjMf+aMSNIK8sVYu8qu0yfBsCDybsUCldue7+qsYKRLFZosTllWQ==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.5.1.tgz", + "integrity": "sha512-JpX66ctLsg5snRRheDhuF2fHvXDG4UDKYfP5403kCAQ4Dz2dPu3PhlHi4AJol+egEBD2HfS/U37j7noQ1Y7mpA==", "license": "MIT", "dependencies": { - "@patternfly/react-core": "^6.4.0", - "@patternfly/react-icons": "^6.4.0", - "@patternfly/react-styles": "^6.4.0", - "@patternfly/react-tokens": "^6.4.0", - "lodash": "^4.17.21", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", + "@patternfly/react-tokens": "^6.5.1", + "lodash": "^4.18.1", "tslib": "^2.8.1" }, "peerDependencies": { @@ -4193,16 +4193,22 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@patternfly/react-table/node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/@patternfly/react-tokens": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.4.0.tgz", - "integrity": "sha512-iZthBoXSGQ/+PfGTdPFJVulaJZI3rwE+7A/whOXPGp3Jyq3k6X52pr1+5nlO6WHasbZ9FyeZGqXf4fazUZNjbw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.5.1.tgz", + "integrity": "sha512-zwepLsIQTL0Lf4R2/PIBOk1m+pm0hYVT3lktf2H4+Y87eRIifwMRb19c+pr4hj4ckGvHs+WxwjTfTj2Qqwn5rw==", "license": "MIT" }, "node_modules/@patternfly/react-user-feedback": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-user-feedback/-/react-user-feedback-6.2.0.tgz", - "integrity": "sha512-grhaZQwcESNZD2ifpbPoodCJ/NRTt4B24jVNYgDi23EVvQ0oGpyIXAvbUhX3siSkLjdqdYLsY9o5wNRJM78VkA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-user-feedback/-/react-user-feedback-6.3.0.tgz", + "integrity": "sha512-Ff02JISZy//mWu31Y8dIulWJcR7RlniYm0o8VmZ74bZFauxE3eP2Tsfnw+IsLRz90jlGKgmGr/O8yHJHIOHFLg==", "dev": true, "license": "MIT", "dependencies": { @@ -6401,9 +6407,9 @@ "license": "BSD-3-Clause" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -9624,9 +9630,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", + "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -11373,12 +11379,12 @@ } }, "node_modules/focus-trap": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", - "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", + "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "license": "MIT", "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.3.0" } }, "node_modules/fontace": { @@ -23909,9 +23915,9 @@ } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, "node_modules/temp-dir": { diff --git a/package.json b/package.json index cf3fc79..7dec151 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "npm run dev", "start:cli": "npm run build:cli && node ./dist/cli/cli.js start", "start:astro": "astro dev", - "build": "npm run build:cli && node --max-old-space-size=4096 ./dist/cli/cli.js build", + "build": "npm run build:cli && node --max-old-space-size=8192 ./dist/cli/cli.js build", "build:astro": "astro check && astro build", "build:cli": "tsc --build ./cli/tsconfig.json", "build:cli:watch": "tsc --build --watch ./cli/tsconfig.json", @@ -53,16 +53,16 @@ "@astrojs/node": "^9.4.3", "@astrojs/react": "^4.3.0", "@nanostores/react": "^0.8.4", - "@patternfly/ast-helpers": "1.4.0-alpha.190", - "@patternfly/patternfly": "^6.0.0", - "@patternfly/react-code-editor": "^6.2.2", - "@patternfly/react-core": "^6.0.0", - "@patternfly/react-drag-drop": "^6.0.0", - "@patternfly/react-icons": "^6.0.0", - "@patternfly/react-styles": "^6.0.0", - "@patternfly/react-table": "^6.0.0", - "@patternfly/react-tokens": "^6.0.0", - "@patternfly/quickstarts": "^6.0.0", + "@patternfly/ast-helpers": "1.4.0-alpha.381", + "@patternfly/patternfly": "^6.5.2", + "@patternfly/quickstarts": "^6.5.0", + "@patternfly/react-code-editor": "^6.5.1", + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-drag-drop": "^6.5.1", + "@patternfly/react-icons": "^6.5.1", + "@patternfly/react-styles": "^6.5.1", + "@patternfly/react-table": "^6.5.1", + "@patternfly/react-tokens": "^6.5.1", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "astro": "^5.15.9", @@ -82,6 +82,8 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@eslint/js": "^9.16.0", + "@patternfly/react-data-view": "^6.5.0", + "@patternfly/react-user-feedback": "^6.3.0", "@semantic-release/git": "^10.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", @@ -110,9 +112,7 @@ "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript-eslint": "^8.15.0", - "wrangler": "^4.20.0", - "@patternfly/react-user-feedback": "^6.0.0", - "@patternfly/react-data-view": "^6.0.0" + "wrangler": "^4.20.0" }, "config": { "commitizen": { diff --git a/public/_routes.json b/public/_routes.json index 3490329..ffaafc1 100644 --- a/public/_routes.json +++ b/public/_routes.json @@ -17,6 +17,7 @@ "/layouts/*", "/patterns/*", "/utility-classes/*", - "/extensions/*" + "/extensions/*", + "/api/*/iconsets/*" ] } diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts new file mode 100644 index 0000000..6c45c6b --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/icons/[iconName].test.ts @@ -0,0 +1,176 @@ +import { GET } from '../../../../../../pages/api/[version]/icons/[iconName]' + +const mockApiIndex = { + versions: ['v5', 'v6'], + sections: {}, + pages: {}, + tabs: {}, +} + +const mockSvg = '' + +const mockIconSvgs: Record> = { + pf: { CircleIcon: mockSvg }, +} + +const mockIconsIndex = { + icons: [ + { name: 'circle', reactName: 'CircleIcon', usage: '' }, + ], +} + +function createFetchMock(): typeof fetch { + return jest.fn((input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input.toString() + if (url.includes('/iconsIndex.json')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockIconsIndex), + } as Response) + } + const match = url.match(/\/api\/[^/]+\/iconsets\/([^/]+)$/) + if (match) { + const setId = match[1] + const svgs = mockIconSvgs[setId] ?? {} + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(svgs), + } as Response) + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response) + }) as typeof fetch +} + +it('returns SVG markup for valid icon', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v6', iconName: 'CircleIcon' }, + url: new URL('http://localhost:4321/api/v6/icons/CircleIcon'), + } as any) + const body = await response.text() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe( + 'image/svg+xml; charset=utf-8', + ) + expect(body).toBe(mockSvg) + expect(body).toContain(' { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v6', iconName: 'NonExistentIcon' }, + url: new URL('http://localhost:4321/api/v6/icons/NonExistentIcon'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('NonExistentIcon') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 404 when icon name is not in index', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v6', iconName: 'invalid' }, + url: new URL('http://localhost:4321/api/v6/icons/invalid'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('invalid') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 400 when icon name parameter is missing', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Icon name parameter is required') + + jest.restoreAllMocks() +}) + +it('returns 404 for nonexistent version', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v99', iconName: 'CircleIcon' }, + url: new URL('http://localhost:4321/api/v99/icons/CircleIcon'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('v99') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 400 when version parameter is missing', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { iconName: 'CircleIcon' }, + url: new URL('http://localhost:4321/api/icons/CircleIcon'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version parameter is required') + + jest.restoreAllMocks() +}) + +it('returns 500 when fetchApiIndex fails', async () => { + global.fetch = jest.fn((input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input.toString() + if (url.includes('apiIndex.json')) { + return Promise.resolve({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as Response) + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({}), + } as Response) + }) as typeof fetch + + const response = await GET({ + params: { version: 'v6', iconName: 'CircleIcon' }, + url: new URL('http://localhost:4321/api/v6/icons/CircleIcon'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Failed to fetch API index') + + jest.restoreAllMocks() +}) diff --git a/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts new file mode 100644 index 0000000..8a73faa --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/icons/index.test.ts @@ -0,0 +1,184 @@ +import { GET } from '../../../../../../pages/api/[version]/icons/index' + +const mockApiIndex = { + versions: ['v5', 'v6'], + sections: {}, + pages: {}, + tabs: {}, +} + +const mockIcons = [ + { + name: 'circle', + reactName: 'CircleIcon', + usage: "import { CircleIcon } from '@patternfly/react-icons'", + }, + { + name: 'home', + reactName: 'HomeIcon', + usage: "import { HomeIcon } from '@patternfly/react-icons'", + }, + { + name: 'circle-outline', + reactName: 'CircleOutlineIcon', + usage: "import { CircleOutlineIcon } from '@patternfly/react-icons'", + }, +] + +function createFetchMock(): typeof fetch { + return jest.fn((input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input.toString() + const json = () => + Promise.resolve( + url.includes('iconsIndex.json') ? { icons: mockIcons } : mockApiIndex + ) + return Promise.resolve({ ok: true, json } as Response) + }) as typeof fetch +} + +jest.mock('../../../../../../utils/icons/reactIcons', () => ({ + filterIcons: jest.fn((icons: typeof mockIcons, filter: string) => { + if (!filter || !filter.trim()) { + return icons + } + const term = filter.toLowerCase().trim() + return icons.filter( + (icon: (typeof mockIcons)[0]) => + icon.name.toLowerCase().includes(term) || + icon.reactName.toLowerCase().includes(term), + ) + }), +})) + +it('returns all icons with metadata', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe( + 'application/json; charset=utf-8', + ) + expect(body).toHaveProperty('icons') + expect(body).toHaveProperty('total') + expect(Array.isArray(body.icons)).toBe(true) + expect(body.icons).toHaveLength(3) + expect(body.total).toBe(3) + expect(body.icons[0]).toHaveProperty('name') + expect(body.icons[0]).toHaveProperty('reactName') + expect(body.icons[0]).toHaveProperty('usage') + + jest.restoreAllMocks() +}) + +it('filters icons when filter parameter is provided', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons?filter=circle'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.icons).toHaveLength(2) + expect(body.total).toBe(2) + expect(body.filter).toBe('circle') + expect(body.icons.every((i: { name: string }) => i.name.includes('circle'))).toBe(true) + + jest.restoreAllMocks() +}) + +it('filter is case-insensitive', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons?filter=CIRCLE'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.icons.length).toBeGreaterThan(0) + + jest.restoreAllMocks() +}) + +it('returns empty icons array when filter yields no matches', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons?filter=nonexistent'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.icons).toHaveLength(0) + expect(body.total).toBe(0) + + jest.restoreAllMocks() +}) + +it('returns 404 error for nonexistent version', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: { version: 'v99' }, + url: new URL('http://localhost:4321/api/v99/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('v99') + expect(body.error).toContain('not found') + + jest.restoreAllMocks() +}) + +it('returns 400 error when version parameter is missing', async () => { + global.fetch = createFetchMock() + + const response = await GET({ + params: {}, + url: new URL('http://localhost:4321/api/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Version parameter is required') + + jest.restoreAllMocks() +}) + +it('returns 500 error when fetchIconsIndex throws', async () => { + global.fetch = jest.fn((input: RequestInfo | URL) => { + const url = typeof input === 'string' ? input : input.toString() + if (url.includes('iconsIndex.json')) { + return Promise.resolve({ ok: false, status: 500, statusText: 'Internal Server Error' } as Response) + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockApiIndex), + } as Response) + }) as typeof fetch + + const response = await GET({ + params: { version: 'v6' }, + url: new URL('http://localhost:4321/api/v6/icons'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Failed to load icons') + expect(body).toHaveProperty('details') + + jest.restoreAllMocks() +}) diff --git a/src/components/__tests__/__snapshots__/NavEntry.test.tsx.snap b/src/components/__tests__/__snapshots__/NavEntry.test.tsx.snap index bcb40c6..7f49edf 100644 --- a/src/components/__tests__/__snapshots__/NavEntry.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavEntry.test.tsx.snap @@ -4,7 +4,7 @@ exports[`NavEntry matches snapshot 1`] = `
  • diff --git a/src/components/__tests__/__snapshots__/NavSection.test.tsx.snap b/src/components/__tests__/__snapshots__/NavSection.test.tsx.snap index dc408db..974e92b 100644 --- a/src/components/__tests__/__snapshots__/NavSection.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/NavSection.test.tsx.snap @@ -4,7 +4,7 @@ exports[`matches snapshot 1`] = `
  • @@ -26,11 +26,11 @@ exports[`matches snapshot 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" + viewBox="0 0 20 20" width="1em" > @@ -46,7 +46,7 @@ exports[`matches snapshot 1`] = ` >
  • @@ -66,7 +66,7 @@ exports[`matches snapshot 1`] = `
  • @@ -85,7 +85,7 @@ exports[`matches snapshot 1`] = `
  • diff --git a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap index 2550ce9..0af3352 100644 --- a/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Navigation.test.tsx.snap @@ -10,7 +10,7 @@ exports[`matches snapshot 1`] = `