From 01db47c4e342b90a47c03e7432267d72efcd1e3e Mon Sep 17 00:00:00 2001 From: Darsh Patel Date: Mon, 17 Mar 2025 19:17:11 -0700 Subject: [PATCH] feat: tests --- README.md | 24 ++ eslint.config.js => eslint.config.mjs | 7 +- package.json | 8 +- pnpm-lock.yaml | 340 ++++++++++++++++++ .../__snapshots__/integration.test.ts.snap | 197 ++++++++++ src/__tests__/fixtures/test-documents.graphql | 63 ++++ src/__tests__/fixtures/test-schema.graphql | 33 ++ src/__tests__/integration.test.ts | 92 +++++ src/utils/__tests__/fragments.test.ts | 184 ++++++++++ src/utils/__tests__/transforms.test.ts | 132 +++++++ vitest.config.ts | 13 + 11 files changed, 1087 insertions(+), 6 deletions(-) rename eslint.config.js => eslint.config.mjs (86%) create mode 100644 src/__tests__/__snapshots__/integration.test.ts.snap create mode 100644 src/__tests__/fixtures/test-documents.graphql create mode 100644 src/__tests__/fixtures/test-schema.graphql create mode 100644 src/__tests__/integration.test.ts create mode 100644 src/utils/__tests__/fragments.test.ts create mode 100644 src/utils/__tests__/transforms.test.ts create mode 100644 vitest.config.ts diff --git a/README.md b/README.md index 765a557..24a7b06 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,30 @@ The plugin's workflow is straightforward but powerful: All generated hashes use the algorithm you specify (default: `sha256`). You can enable the "Prefixed Document Identifier" format (e.g., `sha256:abc123...`) for compliance with the [GraphQL over HTTP specification](https://github.com/graphql/graphql-over-http/blob/52d56fb36d51c17e08a920510a23bdc2f6a720be/spec/Appendix%20A%20--%20Persisted%20Documents.md#sha256-hex-document-identifier) by setting `includeAlgorithmPrefix: true`. +## Development + +### Setup + +```bash +# Install dependencies +pnpm install + +# Watch mode for development +pnpm dev +``` + +### Testing + +The plugin includes a comprehensive test suite built with Vitest: + +```bash +# Run tests +pnpm test + +# Run tests in watch mode +pnpm test:watch +``` + ## License MIT \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.mjs similarity index 86% rename from eslint.config.js rename to eslint.config.mjs index d569f0f..2c0a9d9 100644 --- a/eslint.config.js +++ b/eslint.config.mjs @@ -3,6 +3,9 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( eslint.configs.recommended, + { + ignores: ["dist/**/*", "eslint.config.mjs"], + }, ...tseslint.configs.strict, ...tseslint.configs.stylistic, { @@ -12,8 +15,4 @@ export default tseslint.config( }, }, }, - { - ignores: ["dist/**/*"], - files: ["**/*.ts"], - }, ); \ No newline at end of file diff --git a/package.json b/package.json index 9a9ee1e..5789ff0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "typecheck": "tsc --noEmit", "lint": "eslint . --ext .ts", "clean": "rm -rf dist", - "prepare": "npm run build" + "prepare": "npm run build", + "test": "vitest run", + "test:watch": "vitest" }, "repository": { "type": "git", @@ -51,6 +53,8 @@ "eslint-plugin-prettier": "^5.2.3", "graphql": "^16.8.1", "prettier": "^3.5.3", - "tsup": "^8.4.0" + "tsup": "^8.4.0", + "typescript-eslint": "^8.26.1", + "vitest": "^3.0.9" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7732267..4e158f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,12 @@ devDependencies: tsup: specifier: ^8.4.0 version: 8.4.0(typescript@5.8.2) + typescript-eslint: + specifier: ^8.26.1 + version: 8.26.1(eslint@9.22.0)(typescript@5.8.2) + vitest: + specifier: ^3.0.9 + version: 3.0.9(@types/node@22.13.10) packages: @@ -790,6 +796,67 @@ packages: eslint-visitor-keys: 4.2.0 dev: true + /@vitest/expect@3.0.9: + resolution: {integrity: sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==} + dependencies: + '@vitest/spy': 3.0.9 + '@vitest/utils': 3.0.9 + chai: 5.2.0 + tinyrainbow: 2.0.0 + dev: true + + /@vitest/mocker@3.0.9(vite@6.2.2): + resolution: {integrity: sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + dependencies: + '@vitest/spy': 3.0.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + vite: 6.2.2(@types/node@22.13.10) + dev: true + + /@vitest/pretty-format@3.0.9: + resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} + dependencies: + tinyrainbow: 2.0.0 + dev: true + + /@vitest/runner@3.0.9: + resolution: {integrity: sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==} + dependencies: + '@vitest/utils': 3.0.9 + pathe: 2.0.3 + dev: true + + /@vitest/snapshot@3.0.9: + resolution: {integrity: sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==} + dependencies: + '@vitest/pretty-format': 3.0.9 + magic-string: 0.30.17 + pathe: 2.0.3 + dev: true + + /@vitest/spy@3.0.9: + resolution: {integrity: sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==} + dependencies: + tinyspy: 3.0.2 + dev: true + + /@vitest/utils@3.0.9: + resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==} + dependencies: + '@vitest/pretty-format': 3.0.9 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + dev: true + /@whatwg-node/promise-helpers@1.3.0: resolution: {integrity: sha512-486CouizxHXucj8Ky153DDragfkMcHtVEToF5Pn/fInhUUSiCmt9Q4JVBa6UK5q4RammFBtGQ4C9qhGlXU9YbA==} engines: {node: '>=16.0.0'} @@ -850,6 +917,11 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + dev: true + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -909,6 +981,17 @@ packages: upper-case-first: 2.0.2 dev: true + /chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.3 + pathval: 2.0.0 + dev: true + /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -949,6 +1032,11 @@ packages: tslib: 2.8.1 dev: true + /check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + dev: true + /chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1022,6 +1110,11 @@ packages: ms: 2.1.3 dev: true + /deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + dev: true + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -1050,6 +1143,10 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true + /es-module-lexer@1.6.0: + resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} + dev: true + /esbuild@0.25.1: resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} engines: {node: '>=18'} @@ -1213,11 +1310,22 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + dependencies: + '@types/estree': 1.0.6 + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} dev: true + /expect-type@1.2.0: + resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} + engines: {node: '>=12.0.0'} + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -1504,6 +1612,10 @@ packages: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true + /loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + dev: true + /lower-case-first@2.0.2: resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} dependencies: @@ -1520,6 +1632,12 @@ packages: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} dev: true + /magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1563,6 +1681,12 @@ packages: thenify-all: 1.6.0 dev: true + /nanoid@3.3.10: + resolution: {integrity: sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -1655,6 +1779,15 @@ packages: minipass: 7.1.2 dev: true + /pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + dev: true + + /pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + dev: true + /picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} dev: true @@ -1695,6 +1828,15 @@ packages: lilconfig: 3.1.3 dev: true + /postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.10 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1803,6 +1945,10 @@ packages: engines: {node: '>=8'} dev: true + /siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + dev: true + /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -1815,6 +1961,11 @@ packages: tslib: 2.8.1 dev: true + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + dev: true + /source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} @@ -1828,6 +1979,14 @@ packages: tslib: 2.8.1 dev: true + /stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + dev: true + + /std-env@3.8.1: + resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} + dev: true + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1913,6 +2072,10 @@ packages: any-promise: 1.3.0 dev: true + /tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + dev: true + /tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} dev: true @@ -1925,6 +2088,21 @@ packages: picomatch: 4.0.2 dev: true + /tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + dev: true + + /tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + dev: true + + /tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + dev: true + /title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} dependencies: @@ -2020,6 +2198,22 @@ packages: prelude-ls: 1.2.1 dev: true + /typescript-eslint@8.26.1(eslint@9.22.0)(typescript@5.8.2): + resolution: {integrity: sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + dependencies: + '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1)(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 + typescript: 5.8.2 + transitivePeerDependencies: + - supports-color + dev: true + /typescript@5.8.2: resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} @@ -2048,6 +2242,143 @@ packages: punycode: 2.3.1 dev: true + /vite-node@3.0.9(@types/node@22.13.10): + resolution: {integrity: sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 2.0.3 + vite: 6.2.2(@types/node@22.13.10) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + dev: true + + /vite@6.2.2(@types/node@22.13.10): + resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + dependencies: + '@types/node': 22.13.10 + esbuild: 0.25.1 + postcss: 8.5.3 + rollup: 4.36.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@3.0.9(@types/node@22.13.10): + resolution: {integrity: sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.9 + '@vitest/ui': 3.0.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 22.13.10 + '@vitest/expect': 3.0.9 + '@vitest/mocker': 3.0.9(vite@6.2.2) + '@vitest/pretty-format': 3.0.9 + '@vitest/runner': 3.0.9 + '@vitest/snapshot': 3.0.9 + '@vitest/spy': 3.0.9 + '@vitest/utils': 3.0.9 + chai: 5.2.0 + debug: 4.4.0 + expect-type: 1.2.0 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.8.1 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 6.2.2(@types/node@22.13.10) + vite-node: 3.0.9(@types/node@22.13.10) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + dev: true + /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true @@ -2068,6 +2399,15 @@ packages: isexe: 2.0.0 dev: true + /why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + /word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} diff --git a/src/__tests__/__snapshots__/integration.test.ts.snap b/src/__tests__/__snapshots__/integration.test.ts.snap new file mode 100644 index 0000000..c24a60c --- /dev/null +++ b/src/__tests__/__snapshots__/integration.test.ts.snap @@ -0,0 +1,197 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Persisted Query Plugin Integration Tests > Client Manifest Generation > generates valid client manifest with algorithm prefix 1`] = ` +{ + "CreateUser": "sha256:ba9e16c83890d5d9ecabcb415610364fee2932887b2bc393c7143b71aab269b3", + "GetPost": "sha256:3c6302a47f6243d487fac04c0af0f5f95e0187c17cb1a27e06c7a7c9f0488fd4", + "GetPosts": "sha256:ae215a6ca2480a8c5c721801d9f943cbb54055ce3bdf855ef21bd255b6bbfdde", + "GetUser": "sha256:3ac4d5cb7197d3811eedb53feb1db0615ce67aaa91fe1f8d9175e12d37eb6239", + "GetUsers": "sha256:85306f39b57686912f3e429274c2448e2b4a94f39293875f42d674a7363574ca", + "HelloQuery": "sha256:789ccee2f24e6bec795f6d345529a1fc195e282453956a69eef4209b06b347a9", + "OnUserCreated": "sha256:3af4547b737d2086c4a7f6f74f61e310c1c9d1a54c6218f34edfa592a43bf7b5", + "UpdateUser": "sha256:2ed387ebd9ffea9ea9b42fb1c4c2d149d3c5c45c2c8603ba4ffc755d296dc794", +} +`; + +exports[`Persisted Query Plugin Integration Tests > Client Manifest Generation > generates valid client manifest with custom algorithm 1`] = ` +{ + "CreateUser": "08382963981d862dd2fd2bfe0e9cd207", + "GetPost": "79c0b9239ad347d5710bf729c8fdfe66", + "GetPosts": "69714bf6547e845a36de493b21f754c8", + "GetUser": "0b03e148029a42a67ed0f9c84b96c1ad", + "GetUsers": "b584a3ef5982a75fa6ea872c867c4a53", + "HelloQuery": "7a65e58a5f2108e855f55246308323a3", + "OnUserCreated": "5cdc0f7772d519dd6543d8c1858f0a79", + "UpdateUser": "fdec2e64f7bea659f50032796d8e84a6", +} +`; + +exports[`Persisted Query Plugin Integration Tests > Client Manifest Generation > generates valid client manifest with default options 1`] = ` +{ + "CreateUser": "ba9e16c83890d5d9ecabcb415610364fee2932887b2bc393c7143b71aab269b3", + "GetPost": "3c6302a47f6243d487fac04c0af0f5f95e0187c17cb1a27e06c7a7c9f0488fd4", + "GetPosts": "ae215a6ca2480a8c5c721801d9f943cbb54055ce3bdf855ef21bd255b6bbfdde", + "GetUser": "3ac4d5cb7197d3811eedb53feb1db0615ce67aaa91fe1f8d9175e12d37eb6239", + "GetUsers": "85306f39b57686912f3e429274c2448e2b4a94f39293875f42d674a7363574ca", + "HelloQuery": "789ccee2f24e6bec795f6d345529a1fc195e282453956a69eef4209b06b347a9", + "OnUserCreated": "3af4547b737d2086c4a7f6f74f61e310c1c9d1a54c6218f34edfa592a43bf7b5", + "UpdateUser": "2ed387ebd9ffea9ea9b42fb1c4c2d149d3c5c45c2c8603ba4ffc755d296dc794", +} +`; + +exports[`Persisted Query Plugin Integration Tests > Server Manifest Generation > generates valid server manifest with default options 1`] = ` +{ + "format": "apollo-persisted-query-manifest", + "operations": { + "2ed387ebd9ffea9ea9b42fb1c4c2d149d3c5c45c2c8603ba4ffc755d296dc794": { + "body": "mutation UpdateUser($id: ID!, $name: String, $email: String) { + updateUser(id: $id, name: $name, email: $email) { + ...UserFields + __typename + } +} + +fragment UserFields on User { + id + name + email + __typename +}", + "name": "UpdateUser", + "type": "mutation", + }, + "3ac4d5cb7197d3811eedb53feb1db0615ce67aaa91fe1f8d9175e12d37eb6239": { + "body": "query GetUser($id: ID!) { + user(id: $id) { + ...UserFields + posts { + ...PostFields + __typename + } + __typename + } +} + +fragment UserFields on User { + id + name + email + __typename +} + +fragment PostFields on Post { + id + title + content + __typename +}", + "name": "GetUser", + "type": "query", + }, + "3af4547b737d2086c4a7f6f74f61e310c1c9d1a54c6218f34edfa592a43bf7b5": { + "body": "subscription OnUserCreated { + userCreated { + ...UserFields + __typename + } +} + +fragment UserFields on User { + id + name + email + __typename +}", + "name": "OnUserCreated", + "type": "subscription", + }, + "3c6302a47f6243d487fac04c0af0f5f95e0187c17cb1a27e06c7a7c9f0488fd4": { + "body": "query GetPost($id: ID!) { + post(id: $id) { + ...PostFields + author { + ...UserFields + __typename + } + __typename + } +} + +fragment PostFields on Post { + id + title + content + __typename +} + +fragment UserFields on User { + id + name + email + __typename +}", + "name": "GetPost", + "type": "query", + }, + "789ccee2f24e6bec795f6d345529a1fc195e282453956a69eef4209b06b347a9": { + "body": "query HelloQuery { + hello +}", + "name": "HelloQuery", + "type": "query", + }, + "85306f39b57686912f3e429274c2448e2b4a94f39293875f42d674a7363574ca": { + "body": "query GetUsers { + users { + ...UserFields + __typename + } +} + +fragment UserFields on User { + id + name + email + __typename +}", + "name": "GetUsers", + "type": "query", + }, + "ae215a6ca2480a8c5c721801d9f943cbb54055ce3bdf855ef21bd255b6bbfdde": { + "body": "query GetPosts { + posts { + ...PostFields + __typename + } +} + +fragment PostFields on Post { + id + title + content + __typename +}", + "name": "GetPosts", + "type": "query", + }, + "ba9e16c83890d5d9ecabcb415610364fee2932887b2bc393c7143b71aab269b3": { + "body": "mutation CreateUser($name: String!, $email: String) { + createUser(name: $name, email: $email) { + ...UserFields + __typename + } +} + +fragment UserFields on User { + id + name + email + __typename +}", + "name": "CreateUser", + "type": "mutation", + }, + }, + "version": 1, +} +`; diff --git a/src/__tests__/fixtures/test-documents.graphql b/src/__tests__/fixtures/test-documents.graphql new file mode 100644 index 0000000..bb15702 --- /dev/null +++ b/src/__tests__/fixtures/test-documents.graphql @@ -0,0 +1,63 @@ +fragment UserFields on User { + id + name + email +} + +fragment PostFields on Post { + id + title + content +} + +query HelloQuery { + hello +} + +query GetUser($id: ID!) { + user(id: $id) { + ...UserFields + posts { + ...PostFields + } + } +} + +query GetUsers { + users { + ...UserFields + } +} + +query GetPost($id: ID!) { + post(id: $id) { + ...PostFields + author { + ...UserFields + } + } +} + +query GetPosts { + posts { + ...PostFields + } +} + +mutation CreateUser($name: String!, $email: String) { + createUser(name: $name, email: $email) { + ...UserFields + } +} + +mutation UpdateUser($id: ID!, $name: String, $email: String) { + updateUser(id: $id, name: $name, email: $email) { + ...UserFields + } +} + +subscription OnUserCreated { + userCreated { + ...UserFields + } +} \ No newline at end of file diff --git a/src/__tests__/fixtures/test-schema.graphql b/src/__tests__/fixtures/test-schema.graphql new file mode 100644 index 0000000..50ee29b --- /dev/null +++ b/src/__tests__/fixtures/test-schema.graphql @@ -0,0 +1,33 @@ +type User { + id: ID! + name: String! + email: String + posts: [Post!] +} + +type Post { + id: ID! + title: String! + content: String + author: User! +} + +type Query { + hello: String! + user(id: ID!): User + users: [User!]! + post(id: ID!): Post + posts: [Post!]! +} + +type Mutation { + createUser(name: String!, email: String): User! + updateUser(id: ID!, name: String, email: String): User + deleteUser(id: ID!): Boolean! + createPost(title: String!, content: String, authorId: ID!): Post! +} + +type Subscription { + userCreated: User! + postCreated: Post! +} \ No newline at end of file diff --git a/src/__tests__/integration.test.ts b/src/__tests__/integration.test.ts new file mode 100644 index 0000000..1a3580d --- /dev/null +++ b/src/__tests__/integration.test.ts @@ -0,0 +1,92 @@ +import { describe, test, expect } from 'vitest'; +import { plugin } from '../core/plugin'; +import { parse, buildSchema } from 'graphql'; +import { Types } from '@graphql-codegen/plugin-helpers'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +describe('Persisted Query Plugin Integration Tests', () => { + // Load test schema and documents + const schemaPath = join(__dirname, 'fixtures/test-schema.graphql'); + const documentsPath = join(__dirname, 'fixtures/test-documents.graphql'); + + const schemaDoc = readFileSync(schemaPath, 'utf8'); + const documentsDoc = readFileSync(documentsPath, 'utf8'); + + const schema = buildSchema(schemaDoc); + const documentNode = parse(documentsDoc); + + const documents: Types.DocumentFile[] = [ + { + document: documentNode, + location: documentsPath, + }, + ]; + + describe('Client Manifest Generation', () => { + test('generates valid client manifest with default options', () => { + const result = plugin(schema, documents, { output: 'client' }); + // We know result is a string, but TypeScript sees it as Promisable + const manifest = JSON.parse(result as string); + + // Use snapshot for client manifest + expect(manifest).toMatchSnapshot(); + }); + + test('generates valid client manifest with algorithm prefix', () => { + const result = plugin(schema, documents, { + output: 'client', + includeAlgorithmPrefix: true + }); + // We know result is a string, but TypeScript sees it as Promisable + const manifest = JSON.parse(result as string); + + // Use snapshot for client manifest with algorithm prefix + expect(manifest).toMatchSnapshot(); + }); + + test('generates valid client manifest with custom algorithm', () => { + const result = plugin(schema, documents, { + output: 'client', + algorithm: 'md5' + }); + // We know result is a string, but TypeScript sees it as Promisable + const manifest = JSON.parse(result as string); + + // Use snapshot for client manifest with custom algorithm + expect(manifest).toMatchSnapshot(); + }); + }); + + describe('Server Manifest Generation', () => { + test('generates valid server manifest with default options', () => { + const result = plugin(schema, documents, { output: 'server' }); + // We know result is a string, but TypeScript sees it as Promisable + const manifest = JSON.parse(result as string); + + // Use snapshot for server manifest + expect(manifest).toMatchSnapshot(); + }); + }); + + describe('Error Handling', () => { + test('throws error on missing operation names', () => { + const docWithUnnamedOperation = parse(` + query { + hello + } + `); + + const docs: Types.DocumentFile[] = [ + { + document: docWithUnnamedOperation, + location: 'test.graphql', + }, + ]; + + expect(() => + plugin(schema, docs, { output: 'client' }) + ).toThrow('OperationDefinition missing name'); + }); + }); +}); \ No newline at end of file diff --git a/src/utils/__tests__/fragments.test.ts b/src/utils/__tests__/fragments.test.ts new file mode 100644 index 0000000..6cc54e5 --- /dev/null +++ b/src/utils/__tests__/fragments.test.ts @@ -0,0 +1,184 @@ +import { describe, test, expect } from 'vitest'; +import { findFragments, findUsedFragments } from '../fragments'; +import { parse } from 'graphql'; + +describe('Fragment Utilities', () => { + describe('findFragments', () => { + test('finds all fragments in documents', () => { + const doc = parse(` + fragment UserFragment on User { + id + name + } + + fragment PostFragment on Post { + id + title + } + + query GetUser { + user { + ...UserFragment + } + } + `); + + const fragments = findFragments([doc]); + + expect(fragments.size).toBe(2); + expect(fragments.has('UserFragment')).toBe(true); + expect(fragments.has('PostFragment')).toBe(true); + expect(fragments.has('NonExistentFragment')).toBe(false); + }); + + test('returns empty map when no fragments are found', () => { + const doc = parse(` + query SimpleQuery { + hello + } + `); + + const fragments = findFragments([doc]); + + expect(fragments.size).toBe(0); + }); + + test('finds fragments across multiple documents', () => { + const doc1 = parse(` + fragment UserFragment on User { + id + name + } + `); + + const doc2 = parse(` + fragment PostFragment on Post { + id + title + } + `); + + const fragments = findFragments([doc1, doc2]); + + expect(fragments.size).toBe(2); + expect(fragments.has('UserFragment')).toBe(true); + expect(fragments.has('PostFragment')).toBe(true); + }); + }); + + describe('findUsedFragments', () => { + test('finds directly used fragments', () => { + const doc = parse(` + fragment UserFragment on User { + id + name + } + + fragment PostFragment on Post { + id + title + } + + query GetUser { + user { + ...UserFragment + } + } + `); + + const fragments = findFragments([doc]); + const operation = doc.definitions.find( + def => def.kind === 'OperationDefinition' + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const usedFragments = findUsedFragments(operation as any, fragments); + + expect(usedFragments.size).toBe(1); + expect(usedFragments.has('UserFragment')).toBe(true); + expect(usedFragments.has('PostFragment')).toBe(false); + }); + + test('finds nested fragments', () => { + const doc = parse(` + fragment NameFragment on User { + firstName + lastName + } + + fragment UserFragment on User { + id + ...NameFragment + } + + query GetUser { + user { + ...UserFragment + } + } + `); + + const fragments = findFragments([doc]); + const operation = doc.definitions.find( + def => def.kind === 'OperationDefinition' + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const usedFragments = findUsedFragments(operation as any, fragments); + + expect(usedFragments.size).toBe(2); + expect(usedFragments.has('UserFragment')).toBe(true); + expect(usedFragments.has('NameFragment')).toBe(true); + }); + + test('throws error on unknown fragment', () => { + const doc = parse(` + query GetUser { + user { + ...UnknownFragment + } + } + `); + + const fragments = new Map(); + const operation = doc.definitions.find( + def => def.kind === 'OperationDefinition' + ); + + expect(() => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + findUsedFragments(operation as any, fragments) + ).toThrow('Unknown fragment: UnknownFragment'); + }); + + test('handles circular fragment references', () => { + const doc = parse(` + fragment Fragment1 on Type { + field1 + ...Fragment2 + } + + fragment Fragment2 on Type { + field2 + ...Fragment1 + } + + query Test { + ...Fragment1 + } + `); + + const fragments = findFragments([doc]); + const operation = doc.definitions.find( + def => def.kind === 'OperationDefinition' + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const usedFragments = findUsedFragments(operation as any, fragments); + + expect(usedFragments.size).toBe(2); + expect(usedFragments.has('Fragment1')).toBe(true); + expect(usedFragments.has('Fragment2')).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/src/utils/__tests__/transforms.test.ts b/src/utils/__tests__/transforms.test.ts new file mode 100644 index 0000000..709f8d1 --- /dev/null +++ b/src/utils/__tests__/transforms.test.ts @@ -0,0 +1,132 @@ +import { describe, test, expect } from 'vitest'; +import { printDefinitions, addTypenameToDocument } from '../transforms'; +import { parse } from 'graphql'; +import { Definition } from '../../types'; + +describe('Transform Utilities', () => { + describe('printDefinitions', () => { + test('prints a single definition', () => { + const doc = parse(` + query TestQuery { + hello + } + `); + + const output = printDefinitions([doc.definitions[0] as Definition]); + + expect(output).toContain('query TestQuery'); + expect(output).toContain('hello'); + }); + + test('prints multiple definitions with spacing', () => { + const doc = parse(` + query TestQuery { + hello + } + + fragment UserFragment on User { + id + name + } + `); + + const output = printDefinitions([ + doc.definitions[0] as Definition, + doc.definitions[1] as Definition, + ]); + + expect(output).toContain('query TestQuery'); + expect(output).toContain('fragment UserFragment on User'); + expect(output.split('\n\n').length).toBe(2); // Definitions separated by 2 newlines + }); + }); + + describe('addTypenameToDocument', () => { + test('adds __typename to a simple query', () => { + const doc = parse(` + query TestQuery { + user { + id + name + } + } + `); + + const result = addTypenameToDocument(doc); + const printed = printDefinitions([result]); + + expect(printed).toContain('__typename'); + expect(printed).toMatch(/user\s*{\s*id\s*name\s*__typename\s*}/s); + }); + + test('does not add __typename to operation definitions', () => { + const doc = parse(` + query TestQuery { + hello + } + `); + + const result = addTypenameToDocument(doc); + const printed = printDefinitions([result]); + + // __typename should not be at the top level + expect(printed).not.toMatch(/query TestQuery\s*{\s*hello\s*__typename\s*}/s); + }); + + test('does not add duplicate __typename fields', () => { + const doc = parse(` + query TestQuery { + user { + id + name + __typename + } + } + `); + + const result = addTypenameToDocument(doc); + const printed = printDefinitions([result]); + + // Check that there's exactly one __typename + const matches = printed.match(/__typename/g); + expect(matches).toHaveLength(1); + }); + + test('does not add __typename to empty selection sets', () => { + const doc = parse(` + query TestQuery { + scalar + } + `); + + const result = addTypenameToDocument(doc); + const printed = printDefinitions([result]); + + expect(printed).not.toContain('__typename'); + }); + + test('handles introspection fields correctly', () => { + const doc = parse(` + query IntrospectionQuery { + __schema { + types { + name + } + } + } + `); + + const result = addTypenameToDocument(doc); + const printed = printDefinitions([result]); + + // The implementation treats introspection fields special - but still adds + // __typename to selection sets inside them. + // This is the current implementation behavior which we're documenting with this test. + expect(printed).toContain('__typename'); + + // Verify that it doesn't add __typename at the operation level + // (already checked by another test, but double-checking here) + expect(printed).not.toContain('query IntrospectionQuery { __typename'); + }); + }); +}); \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..0f570ab --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/__tests__/**/*.test.ts', 'src/**/*.test.ts'], + coverage: { + reporter: ['text', 'json', 'html'], + exclude: ['node_modules/'], + }, + }, +}); \ No newline at end of file