├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── publish.yml │ └── tests_pipeline.yml ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── docs ├── coverage-html.png └── gql-autocomplete-demo.gif ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── codegen-cli │ ├── run-command.ts │ └── setup-codegen-cli.ts ├── coverage-reporter │ ├── coverage-calculation-helpers.ts │ ├── coverageLogger.ts │ ├── coverageStashPath.ts │ ├── gql-client-parser.ts │ ├── html-generator.ts │ ├── index.ts │ ├── report.ts │ └── types.ts ├── gql-generator.d.ts ├── index.ts └── requester.ts ├── tests ├── codegen-cli.test.ts ├── coverage-logger.test.ts ├── reporter.test.ts ├── requester.test.ts └── resources │ ├── clientWithEnum.ts │ ├── clientWithEnumsAsConst.ts │ ├── coverageDir │ ├── group │ └── groups │ ├── gql-fake-server.ts │ ├── graphql.ts │ └── raw-graphql.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. I create $('').... 16 | 2. Actual behaviour VS Expected behaviour. 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Environment (please complete the following information):** 25 | - OS: [e.g. Windows, OSX, Linux] 26 | - Playwright Version [e.g. 1.26.1] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npm 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '16.x' 13 | registry-url: 'https://registry.npmjs.org' 14 | - name: Run npm ci 15 | run: npm ci 16 | - name: Build lib 17 | run: npm run build 18 | - name: Determine version type 19 | id: version_type 20 | run: | 21 | if [[ ${{ github.event.release.tag_name }} == *"-beta"* ]]; then 22 | echo "::set-output name=is_beta::true" 23 | else 24 | echo "::set-output name=is_beta::false" 25 | fi 26 | - name: Publish beta version 27 | if: steps.version_type.outputs.is_beta == 'true' 28 | run: npm publish --tag beta 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | - name: Publish stable version 32 | if: steps.version_type.outputs.is_beta == 'false' 33 | run: npm publish 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/tests_pipeline.yml: -------------------------------------------------------------------------------- 1 | name: TestsPipeline 2 | on: 3 | push: 4 | branches: 5 | - 'experiment/**' 6 | pull_request: 7 | branches: 8 | - main 9 | concurrency: 10 | group: TestsPipeline 11 | cancel-in-progress: true 12 | jobs: 13 | 14 | test: 15 | strategy: 16 | matrix: 17 | platform: [ ubuntu-latest, macos-latest, windows-latest ] 18 | node: [ '22' ] 19 | name: ${{ matrix.platform }} - Node ${{ matrix.node }} 20 | runs-on: ${{ matrix.platform }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node }} 26 | - name: Install dependencies 27 | run: npm ci 28 | - name: Build 29 | run: npm run build 30 | - name: Run tests 31 | run: npm test 32 | timeout-minutes: 5 33 | - run: echo "🍏 This job's status is ${{ job.status }}." 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # custom 133 | .ssh 134 | lib 135 | .idea 136 | 137 | # tests 138 | gql/ 139 | tests/codegen-test/ 140 | playground/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Oleksandr Solomin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playwright-graphql 2 | This library provides Playwright integration with GraphQL and TypeScript for efficient API testing. 3 | It enables you to generate an auto-generated GraphQL API client with autocomplete functionality. 4 | 5 | ![DEMO](docs/gql-autocomplete-demo.gif) 6 | ## 🌟 Features 7 | 8 | - 🚀 Autogenerated GraphQL API client with TypeScript autocomplete 9 | - 📊 Comprehensive schema and operation generation 10 | - 🔍 Flexible request handling and response management 11 | - 📈 Optional GraphQL coverage reporting 12 | 13 | The build-in CLI simplifies code generation process to one simple command. 14 | ```bash 15 | playwright-graphql --schema schema.gql 16 | ``` 17 | 18 | - [Project Setup](#project-setup) 19 | - [Installation](#installation) 20 | - [Generate type safe client](#generate-type-safe-client) 21 | - [Add path to your tsconfig](#add-path-to-your-tsconfig) 22 | - [Create gql fixtures](#create-gql-fixture) 23 | - [Code generation with build in CLI](#code-generation-with-build-in-cli) 24 | - [Raw Response](#return-raw-response-body-instead-of-schema-defined-type) 25 | - [Get Client signature](#get-client-signature) 26 | - [Custom operations](#custom-operations) 27 | - [Graphql explorer](#graphql-explorer) 28 | - [GraphQL API call options](#graphql-api-call-options) 29 | - [Negative Testing](#negative-test-cases) 30 | - [GraphQL Coverage Reporting](#graphql-coverage-reporting) 31 | 32 | 33 | ## Project setup: 34 | 35 | 1. Installation. 36 | 2. Generate type safe client. 37 | 3. Add GraphQL client fixture. 38 | 4. Write GraphQL tests with joy! 39 | 40 | Template project: https://github.com/DanteUkraine/playwright-graphql-example 41 | 42 | ### Installation 43 | To begin, install the playwright-graphql package. 44 | This library integrates GraphQL testing with Playwright and TypeScript, 45 | offering autocomplete and type safety for your API tests. 46 | 47 | - `npm install playwright-graphql` 48 | 49 | or for dev dependency 50 | - `npm install -D playwright-graphql` 51 | 52 | 53 | ### Generate type safe client 54 | 55 | ```bash 56 | playwright-graphql --schema path-to-schema.gql 57 | ``` 58 | 59 | Will generate you next: 60 | ```text 61 | 📁 Project Root 62 | ├── 📄 path-to-schema.gql (existing schema file) 63 | └── 📁 gql (default directory for generated files) 64 | ├──📁 path-to-schema (will become a name for related operation directory) 65 | │ └── 📁 autogenerated-operations (contains all possible GraphQL operations) 66 | │ ├── 📄 mutations.gql 67 | │ └── 📄 queries.gql 68 | └── 📄 graphql.ts (generated TypeScript types and client) 69 | ``` 70 | When you run the command with an existing schema file, the CLI will: 71 | 72 | 1. Generate GraphQL operations based on your schema 73 | 2. Create a codegen configuration file 74 | 3. Generate TypeScript types for type-safe GraphQL operations 75 | 4. Add a client utility function for use with Playwright tests 76 | 77 | The generated graphql.ts file will include a `getClient(apiContext, options?, callback?)` function and 78 | type `GqlAPI` that you can use in your Playwright fixture to return type-safe GraphQL client in tests. 79 | 80 | In case you can not generate schema from GraphQL server directly: 81 | ```bash 82 | playwright-graphql --url http://localhost:4000/api/graphql --schema schema.gql 83 | ``` 84 | 85 | Will generate you next: 86 | ```text 87 | 📁 Project Root 88 | ├── 📄 schema.gql (generated schema file) 89 | └── 📁 gql (default directory for generated files) 90 | ├──📁 schema (each schema will have its own directory with operations) 91 | │ └── 📁 autogenerated-operations (contains all possible GraphQL operations) 92 | │ ├── 📄 mutations.gql 93 | │ └── 📄 queries.gql 94 | └── 📄 graphql.ts (generated TypeScript types and client) 95 | ``` 96 | When you run the command with a GraphQL endpoint URL, the CLI will: 97 | 98 | 1. Fetch the GraphQL schema from the specified URL and save to the specified file (schema.gql). 99 | 2. Save the schema to the specified file (schema.gql) 100 | 3. Generate GraphQL operations based on the fetched schema 101 | 4. Generate GraphQL type-safe client 102 | 103 | This command is useful when you want to generate or update your schema file directly from a GraphQL endpoint. 104 | It combines the schema fetching step with the type-safe client generation, streamlining the setup process 105 | for your Playwright GraphQL tests. 106 | 107 | The generated files and their purposes remain the same as in the previous command, 108 | but now you have the added benefit of automatically fetching and updating your schema file from the live GraphQL endpoint. 109 | 110 | ### Add path to your tsconfig 111 | 112 | To simplify your imports and improve project readability, configure your `tsconfig.json` by adding custom path aliases. 113 | This makes it easier to import your generated GraphQL client across your project: 114 | 115 | Add `"@gql": ["gql/graphql"]` for easy import. 116 | 117 | ```json 118 | { 119 | "compilerOptions": { 120 | "target": "ESNext", 121 | "module": "ESNext", 122 | "moduleResolution": "node", 123 | "resolveJsonModule": true, 124 | "strict": true, 125 | "noUnusedLocals": false, 126 | "noUnusedParameters": false, 127 | "noFallthroughCasesInSwitch": true, 128 | "allowSyntheticDefaultImports": true, 129 | "baseUrl": "./", 130 | "paths": { 131 | "@fixtures/*": ["fixtures/*"], 132 | "@gql": ["gql/graphql"] 133 | } 134 | } 135 | } 136 | ``` 137 | This setup allows you to import your client like this: 138 | ```ts 139 | import { getClient, GqlAPI } from '@gql'; 140 | ``` 141 | 142 | Note that the `@fixtures/*` path alias allows you to import any file from the fixtures directory as a module: 143 | ```ts 144 | import { test, expect } from '@fixtures/gql'; 145 | ``` 146 | Instead of using long relative paths. 147 | 148 | ### Create gql fixture 149 | 150 | The final step creates a fixture for integrating the autogenerated GraphQL client with Playwright tests. 151 | The fixture returns Playwright GraphQl type safe API client into tests. 152 | Create a file (minimalistic example, *fixtures/gql.ts*) with the following content: 153 | 154 | *fixtures/gql.ts* 155 | ```ts 156 | import { test as baseTest, expect, request, APIRequestContext } from '@playwright/test'; 157 | import { getClient, GqlAPI } from '@gql'; 158 | 159 | export { expect }; 160 | 161 | type WorkerFixtures = { 162 | apiContext: APIRequestContext; 163 | gql: GqlAPI; 164 | }; 165 | 166 | export const test = baseTest.extend<{}, WorkerFixtures>({ 167 | apiContext: [ 168 | async ({}, use) => { 169 | const apiContext = await request.newContext({ 170 | baseURL: 'http://localhost:4000' 171 | }); 172 | await use(apiContext); 173 | }, { scope: 'worker' } 174 | ], 175 | gql: [ 176 | async ({ apiContext }, use) => { 177 | await use(getClient(apiContext)); 178 | }, { auto: false, scope: 'worker' } 179 | ] 180 | }); 181 | ``` 182 | This fixture ensures that your tests have a consistent and type-safe GraphQL client available, and it leverages 183 | Playwright’s API request context for efficient testing. 184 | 185 | Full configurability example: 186 | ```ts 187 | import { test as baseTest, expect, request, APIRequestContext } from '@playwright/test'; 188 | import { getClient, GqlAPI, RequesterOptions, RequestHandler } from '@gql'; 189 | 190 | export { expect }; 191 | 192 | const options: RequesterOptions = { 193 | gqlEndpoint: 'api/gql', 194 | rawResponse: true 195 | }; 196 | 197 | // This optional callback allows user to add custom logic to gql api call. 198 | const requestHandlerCallback: RequestHandler = async (request: () => Promise) => { 199 | console.log('Before api call'); 200 | const res = await request(); 201 | console.log(`After api call: ${res.status()}`); 202 | return res; 203 | }; 204 | 205 | type WorkerFixtures = { 206 | apiContext: APIRequestContext; 207 | gql: GqlAPI; 208 | }; 209 | 210 | export const test = baseTest.extend<{}, WorkerFixtures>({ 211 | apiContext: [ 212 | async ({}, use) => { 213 | const apiContext = await request.newContext({ 214 | baseURL: 'http://localhost:4000' 215 | }); 216 | await use(apiContext); 217 | }, { scope: 'worker' } 218 | ], 219 | gql: [ 220 | async ({ apiContext }, use) => { 221 | await use(getClient(apiContext, options, requestHandlerCallback)); 222 | }, { auto: false, scope: 'worker' } 223 | ] 224 | }); 225 | ``` 226 | This full example shows how to customize GraphQL endpoint, type of response, and add custom logic to GraphQL API calls. 227 | 228 | Now, you can write your tests using the fixture. 229 | 230 | #### You are ready to jump into writing tests! 231 | 232 | *tests/example.test*: 233 | ```ts 234 | import { test, expect } from '@fixtures/gql'; 235 | 236 | test('playwright-graphql test', async ({ gql }) => { 237 | const res = await gql.getCityByName({ 238 | name: 'Lviv' 239 | }); 240 | 241 | expect(res.getCityByName).not.toBeNull(); 242 | }) 243 | ``` 244 | ___ 245 | 246 | ## Code generation with build in CLI 247 | 248 | Designed for common workflow, the playwright-graphql CLI tool automates the process of generating 249 | GraphQL schemas, operations, and TypeScript types for your Playwright tests. 250 | 251 | Use cases: 252 | 253 | Basic client generation: 254 | 255 | - `playwright-graphql --url http://localhost:4000/api/graphql` 256 | 257 | Schema generation with authentication: 258 | 259 | - `playwright-graphql --url http://localhost:4000/api/graphql --header "Authorization: Bearer token"` 260 | 261 | Syntax for complex headers: 262 | 263 | - `playwright-graphql --url http://localhost:4000/api/graphql -h "Cookies={'Authorization': 'Bearer token'}"` 264 | 265 | The same header will be added to each schema introspect API call. 266 | 267 | `playwright-graphql -u http://localhost:4000/api/graphql -u http://localhost:4001/api/graphql -h "Authorization: Bearer common-token" -s schema1.gql -s schema2.gql` 268 | 269 | Different headers for each url requires splitting scripts: 270 | 271 | `playwright-graphql -u http://localhost:4000/api/graphql -h "Authorization: Bearer first-token" -s schema1.gql` 272 | 273 | `playwright-graphql -u http://localhost:4001/api/graphql -h "Authorization: Bearer second-token" -s country.gql` 274 | 275 | Custom paths for generated files: 276 | 277 | - `playwright-graphql --url http://localhost:4000/api/graphql --gqlDir generated/test-client --gqlFile my-api-client.ts` 278 | 279 | Using an existing schema file: 280 | 281 | - `playwright-graphql --schema existing-schema.gql` 282 | 283 | Enabling coverage logging: 284 | 285 | - `playwright-graphql --url http://localhost:4000/api/graphql --coverage` 286 | 287 | Custom operations without introspection: 288 | 289 | - `playwright-graphql --url http://localhost:4000/api/graphql --introspect false --document src/operations` 290 | 291 | Using a custom codegen configuration: 292 | 293 | - `playwright-graphql --custom --codegen path/to/codegen.ts` 294 | 295 | You can save `codegen.ts` file for your combination: 296 | 297 | - `playwright-graphql -u http://localhost:4000/api/graphql --saveCodegen` 298 | 299 | Generate multiple clients: 300 | 301 | - `playwright-graphql -u http://localhost:4000/api/graphql -u http://localhost:4000/api/graphql -s schema1.gql -s schema2.gql` 302 | 303 | _Output:_ 304 | ```text 305 | 📁 Project Root 306 | ├── 📄 schema1.gql 307 | ├── 📄 schema2.gql 308 | └── 📁 gql 309 | ├──📁 schema1 310 | │ └── 📁 autogenerated-operations 311 | │ ├── 📄 mutations.gql 312 | │ └── 📄 queries.gql 313 | ├──📁 schema2 314 | │ └── 📁 autogenerated-operations 315 | │ ├── 📄 mutations.gql 316 | │ └── 📄 queries.gql 317 | └── 📄 schema1.ts 318 | └── 📄 schema2.ts 319 | ``` 320 | 321 | Generate multiple clients into custom output dir with custom file names: 322 | 323 | - `playwright-graphql -s schema1.gql -s schema2.gqq -o ./apps/shell/e2e/gql/dgc-operations -d ./generated -f first-schema.ts -f second-schema.ts` 324 | 325 | 326 | When you generate multiple clients and do not specify name for ts file with `-f`, CLI will not reuse default name graphql.ts, 327 | each client name will match schema name to make it clear. 328 | 329 | Specify client names: 330 | - `playwright-graphql -u http://localhost:4000/api/graphql -u http://localhost:4000/api/graphql -s schema1.gql -s schema2.gql -f gqlClient -f gqlApi` 331 | 332 | _Output:_ 333 | ```text 334 | 📁 Project Root 335 | ├── 📄 schema1.gql 336 | ├── 📄 schema2.gql 337 | └── 📁 gql 338 | ├──📁 schema1 339 | │ └── 📁 autogenerated-operations 340 | │ ├── 📄 mutations.gql 341 | │ └── 📄 queries.gql 342 | ├──📁 schema2 343 | │ └── 📁 autogenerated-operations 344 | │ ├── 📄 mutations.gql 345 | │ └── 📄 queries.gql 346 | └── 📄 gqlClient.ts 347 | └── 📄 gqlApi.ts 348 | ``` 349 | 350 | Generate multiple clients with custom operations: 351 | 352 | - `playwright-graphql -u http://localhost:4000/api/graphql -u http://localhost:4000/api/graphql -s schema1.gql -s schema2.gql -o ./schema1-operations, -o ./schema2-operations` 353 | 354 | _Output:_ 355 | ```text 356 | 📁 Project Root 357 | ├── 📄 schema1.gql 358 | ├── 📄 schema2.gql 359 | ├── 📁 schema1-operations 360 | ├── 📁 schema2-operations 361 | └── 📁 gql 362 | ├──📁 schema1 363 | │ └── 📁 autogenerated-operations 364 | │ ├── 📄 mutations.gql 365 | │ └── 📄 queries.gql 366 | ├──📁 schema2 367 | │ └── 📁 autogenerated-operations 368 | │ ├── 📄 mutations.gql 369 | │ └── 📄 queries.gql 370 | └── 📄 schema1.ts // this client will include operations from project root schema1-operations 371 | └── 📄 schema2.ts // this client will include operations from project root schema2-operations 372 | ``` 373 | 374 | In case not all your clients need custom operations, for example only schema2 need custom operations change the order of args. 375 | 376 | - `playwright-graphql -u http://localhost:4000/api/graphql -u http://localhost:4000/api/graphql -s schema2.gql -s schema1.gql -o ./schema2-operations` 377 | 378 | _Output:_ 379 | ```text 380 | 📁 Project Root 381 | ├── 📄 schema1.gql 382 | ├── 📄 schema2.gql 383 | ├── 📁 schema2-operations 384 | └── 📁 gql 385 | ├──📁 schema1 386 | │ └── 📁 autogenerated-operations 387 | │ ├── 📄 mutations.gql 388 | │ └── 📄 queries.gql 389 | ├──📁 schema2 390 | │ └── 📁 autogenerated-operations 391 | │ ├── 📄 mutations.gql 392 | │ └── 📄 queries.gql 393 | └── 📄 schema1.ts 394 | └── 📄 schema2.ts // this client will include operations from project root schema2-operations 395 | ``` 396 | Document params will be added to lists of documents regarding order 397 | 398 | To check details save codegen file and verify output of your command. 399 | 400 | ### CLI Options 401 | The CLI tool accepts several options to customize its behavior. 402 | Below is a summary of the available command-line parameters: 403 | 404 | | Option | Alias | Description | Type | Default | 405 | |-----------------|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------------| 406 | | `--url` | `-u` | Full GraphQL endpoint URL(s) used for schema retrieval. In case this option is not passed, the script will skip schema generation and will look for an existing schema. | array | *optional* | 407 | | `--schema` | `-s` | Path to save the generated GraphQL schema file(s). If the URL option is not provided, the script expects that the schema already exists. | array | [`schema.gql`] | 408 | | `--header` | `-h` | Optional authentication header(s) for schema fetching. Can be passed multiple times. | array | *optional* | 409 | | `--gqlDir` | `-d` | Path to save the auto-generated GraphQL files. | string | `gql` | 410 | | `--gqlFile` | `-f` | Path to save the auto-generated GraphQL type-safe client (you will import this into your code). | array | [`graphql.ts`] | 411 | | `--document` | `-o` | Glob pattern(s) that will be added to documents. | array | *optional* | 412 | | `--depthLimit` | | Defines the maximum depth of nested fields to include in the generated GraphQL queries. | number | 20 | 413 | | `--introspect` | `-i` | Turns off auto-generation of operations. | boolean | true | 414 | | `--raw` | | Generates GraphQL client witch return raw responses. | boolean | false | 415 | | `--enumsAsConst`| | Type safe client will be build with "as const" instead of enum. | boolean | false | 416 | | `--codegen` | `-c` | Path to save the codegen config. | string | `codegen.ts` | 417 | | `--saveCodegen` | | Save generated codegen file. | boolean | false | 418 | | `--custom` | | Generation will be done from your custom codegen file. No autogenerated operation. | boolean | false | 419 | | `--coverage` | | Flag to add coverage logger to auto-generated client. | boolean | false | 420 | | `--silent` | | Suppress all logs. | boolean | false | 421 | | `--version` | | Print version. | | | 422 | | `--help` | | Print all CLI options. | | | 423 | 424 | ### Return raw response body instead of schema defined type. 425 | 426 | You can configure the library to return the raw GraphQL response body instead of the schema-defined types. 427 | This is useful when you need full control over the response payload. 428 | 429 | **Steps to Enable Raw Response:** 430 | 431 | 1. Add `--raw` to your playwright-graphql command. 432 | 433 | 2. Update the GraphQL client in your fixture. 434 | Pass `{ rawResponse: true }` to `getClient`: 435 | 436 | *fixtures/gql.ts* 437 | ```ts 438 | getClient(apiContext, { rawResponse: true }); 439 | ``` 440 | 441 | 3. Use the raw response in your tests. 442 | The raw response will include both data and errors: 443 | 444 | *tests/example.test* 445 | ```ts 446 | import { test, expect } from '@fixtures/gql'; 447 | 448 | test('playwright-graphql test', async ({ gql }) => { 449 | const res = await gql.getCityByName({ 450 | name: 'Lviv' 451 | }); 452 | 453 | expect(res).toHaveProperty('data'); 454 | expect(res).toHaveProperty('errors'); 455 | res.data; // will have type raw schema. 456 | }) 457 | ``` 458 | --- 459 | 460 | ### Get Client signature 461 | 462 | ```ts 463 | getClient( 464 | apiContext: APIRequestContext, 465 | options?: { gqlEndpoint?: string; rawResponse?: boolean }, 466 | requestHandler?: (request: () => Promise) => Promise 467 | ); 468 | 469 | ``` 470 | 471 | #### Options: 472 | 473 | Default values for options: `{ gqlEndpoint: '/api/graphql', rawResponse: false }` 474 | 475 | Set `gqlEndpoint` to customize graphql endpoint. 476 | 477 | Set `rawResponse` to return { errors: any[], body: R } instead of R, R represents autogenerated return type from gql schema. 478 | This parameter can be used only when `rawRequest: true` is included in `codegen.ts`. 479 | 480 | 481 | #### Request Handler Callback 482 | 483 | You can inject custom logic before and after GraphQL API calls using a request handler callback: 484 | 485 | ```typescript 486 | import { getClient, GqlAPI } from '@gql'; 487 | 488 | const customRequestHandler = async (requester: () => Promise) => { 489 | // Custom pre-call logic 490 | const res = await requester(); 491 | // Custom post-call logic 492 | return res; 493 | }; 494 | 495 | const gqlApiClient: GqlAPI = getClient(apiContext, { gqlEndpoint: '/api/graphql' }, customRequestHandler); 496 | ``` 497 | 498 | ### Custom operations 499 | 500 | Collect your custom operations under any directory in your project and pass it to CLI. 501 | 502 | - `playwright-graphql --url http://localhost:4000/api/graphql -o src/operations` 503 | 504 | That will build your client with autogenerated and your custom operations. 505 | 506 | To use only your custom operations add introspect flag `-i false`. 507 | 508 | - `playwright-graphql --url http://localhost:4000/api/graphql -o src/operations -i false` 509 | 510 | ### Graphql explorer 511 | You can use tools like [Apollo Explorer](https://studio.apollographql.com/sandbox/explorer) to build and test custom 512 | queries or mutations interactively before adding them to your project. 513 | 514 | ### GraphQL API call options 515 | 516 | Each generated operation accepts an optional second parameter for additional configuration options. 517 | These options extend Playwright's [post method](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-post) 518 | method with two extra parameters: 519 | 520 | - `returnRawJson` Returns the full JSON payload instead of parsed data. 521 | - `failOnEmptyData` Prevents errors when the response contains no data (useful for error testing). 522 | 523 | Here is how the second parameter type is declared. 524 | ```ts 525 | type PlaywrightRequesterOptions = { 526 | returnRawJson?: boolean; 527 | failOnEmptyData?: boolean; 528 | } & Omit; 529 | ``` 530 | 531 | *Example Usage in Tests:* 532 | ```ts 533 | import { test, expect } from '@fixtures/gql'; 534 | 535 | test('playwright-graphql test with options', async ({ gql }) => { 536 | const res = await gql.getCityByName( 537 | { name: 'Lviv' }, 538 | { returnRawJson: true } 539 | ); 540 | 541 | expect(res).toHaveProperty('data'); 542 | }); 543 | 544 | ``` 545 | ### Negative test cases 546 | GraphQL responses often include an `errors` field instead of returning HTTP error codes like 547 | `400x` or `500x`. To verify errors in such cases, use the option `failOnEmptyData`. 548 | 549 | *Example Negative Test Case:* 550 | ```ts 551 | import { test, expect } from '@fixtures/gql'; 552 | 553 | test('playwright-graphql test negative', async ({ gql }) => { 554 | const res = await gql.getCityByName({ 555 | name: 'Lviv' 556 | }, { failOnEmptyData: false }); 557 | 558 | expect(res).toHaveProperty('errors[0].message'); 559 | }) 560 | ``` 561 | 562 | --- 563 | 564 | ### GraphQL Coverage Reporting 565 | 566 | GraphQL Coverage Reporting helps you track and visualize which GraphQL operations and their 567 | respective arguments are exercised by your tests. This feature not only covers simple queries and mutations but also 568 | handles complex input parameters (including nested types and enums) by presenting them in a clear, human-readable summary. 569 | 570 | Generates a detailed log and an HTML summary report showing the coverage of your GraphQL operations: 571 | ![DEMO](docs/coverage-html.png) 572 | 573 | Playwright config file for the GraphQL coverage reporter: 574 | 1. **graphqlFilePath** (required): Path to your autogenerated GraphQL file with types and getClient function (e.g. './gql/graphql.ts'). 575 | 2. **coverageFilePath** (optional): Path for the coverage log file. Default: `./gql-coverage.log`. 576 | 3. **htmlFilePath** (optional): Path for the HTML summary report. Default: './gql-coverage.html'. 577 | 4. **logUncoveredOperations** (optional): Whether to log uncovered operations. Default: false. 578 | 5. **minCoveragePerOperation** (optional): Minimum coverage percentage required per operation. Default: 100. 579 | 6. **saveGqlCoverageLog** (optional): Whether to save the detailed coverage log. Default: false. 580 | 7. **saveHtmlSummary** (optional): Whether to save the HTML summary report. Default: false. 581 | 582 | Below is a sample Playwright config using these options: 583 | ```ts 584 | import { defineConfig } from '@playwright/test'; 585 | 586 | export default defineConfig({ 587 | reporter: [ 588 | ['list'], 589 | ['playwright-graphql/coverage-reporter', { 590 | graphqlFilePath: './gql/graphql.ts', 591 | coverageFilePath: './coverage/gql-coverage.log', 592 | htmlFilePath: './coverage/gql-coverage.html', 593 | logUncoveredOperations: true, 594 | minCoveragePerOperation: 80, 595 | saveGqlCoverageLog: true, 596 | saveHtmlSummary: true 597 | }] 598 | ], 599 | }); 600 | 601 | ``` 602 | In case you generate multiple clients each client coverage reporter has to be specified separately: 603 | 604 | ```ts 605 | export default defineConfig({ 606 | testDir: 'tests', 607 | reporter: [ 608 | ['playwright-graphql/coverage-reporter', { 609 | graphqlFilePath: './gql/pokemon.ts', 610 | minCoveragePerOperation: 20, 611 | logUncoveredOperations: true, 612 | saveGqlCoverageLog: false, 613 | saveHtmlSummary: true 614 | }], 615 | ['playwright-graphql/coverage-reporter', { 616 | graphqlFilePath: './gql/country.ts', 617 | minCoveragePerOperation: 20, 618 | logUncoveredOperations: true, 619 | saveGqlCoverageLog: false, 620 | saveHtmlSummary: true 621 | }] 622 | ], 623 | }); 624 | ``` 625 | 626 | 627 | ### Integrating the Coverage Logger into Your GraphQL Client 628 | 629 | To enable coverage tracking for your GraphQL requests add `--coverage` to playwright-graphql cli. 630 | 631 | - `playwright-graphql --coverage` 632 | 633 | 634 | Template project: https://github.com/DanteUkraine/playwright-graphql-example -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pages-themes/leap-day@v0.2.0 2 | plugins: 3 | - jekyll-remote-theme -------------------------------------------------------------------------------- /docs/coverage-html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanteUkraine/playwright-graphql/7e4c4bd9fe83ac22f1d0204b09776138c52adbec/docs/coverage-html.png -------------------------------------------------------------------------------- /docs/gql-autocomplete-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DanteUkraine/playwright-graphql/7e4c4bd9fe83ac22f1d0204b09776138c52adbec/docs/gql-autocomplete-demo.gif -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | moduleFileExtensions: ['ts', 'js'], 7 | testMatch: ['**/*.test.ts'], 8 | transform: { 9 | '^.+\\.ts$': 'ts-jest', 10 | }, 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-graphql", 3 | "version": "2.2.0", 4 | "description": "This library provide playwright integration with graphql for efficient API tests.", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "exports": { 11 | ".": { 12 | "require": "./lib/index.js", 13 | "import": "./lib/index.js", 14 | "types": "./lib/index.d.ts" 15 | }, 16 | "./coverage-reporter": { 17 | "require": "./lib/coverage-reporter/index.js", 18 | "import": "./lib/coverage-reporter/index.js", 19 | "types": "./lib/coverage-reporter/index.d.ts" 20 | } 21 | }, 22 | "bin": { 23 | "playwright-graphql": "./lib/codegen-cli/setup-codegen-cli.js" 24 | }, 25 | "scripts": { 26 | "build": "tsc", 27 | "clean": "tsc --build --clean", 28 | "install:deps": "npm install --registry=https://registry.npmjs.org", 29 | "test": "cross-env PW_GQL_TEMP_DIR=./tests/resources/coverageDir jest" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/DanteUkraine/playwright-graphql.git" 34 | }, 35 | "keywords": [ 36 | "playwright", 37 | "graphql" 38 | ], 39 | "author": "Oleksandr Solomin", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/DanteUkraine/playwright-graphql/issues" 43 | }, 44 | "homepage": "https://danteukraine.github.io/playwright-graphql", 45 | "dependencies": { 46 | "@graphql-codegen/cli": "5.0.5", 47 | "@graphql-codegen/typescript": "4.1.5", 48 | "@graphql-codegen/typescript-generic-sdk": "4.0.1", 49 | "@graphql-codegen/typescript-operations": "4.5.1", 50 | "get-graphql-schema": "2.1.2", 51 | "gql-generator": "2.0.0", 52 | "json-bigint-patch": "^0.0.8", 53 | "prettier": "^3.5.3", 54 | "yargs": "^17.7.2" 55 | }, 56 | "devDependencies": { 57 | "@jest/globals": "^29.7.0", 58 | "@types/jest": "^29.5.14", 59 | "@types/node": "^22.10.10", 60 | "cross-env": "^7.0.3", 61 | "jest": "^29.7.0", 62 | "ts-jest": "^29.2.5", 63 | "ts-node": "^10.9.2", 64 | "typescript": "^5.7.3" 65 | }, 66 | "peerDependencies": { 67 | "@playwright/test": ">= 1.44.x" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/codegen-cli/run-command.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | export function runCommand( 4 | command: string, 5 | ): Promise<{ stdout: string; stderr: string }> { 6 | return new Promise((resolve, reject) => { 7 | 8 | const child = spawn(command, { shell: true }); 9 | 10 | const stdout: string[] = []; 11 | const stderr: string[] = []; 12 | 13 | if (child.stdout) { 14 | child.stdout.on('data', (data: Buffer) => { 15 | stdout.push(data.toString()); 16 | }); 17 | } 18 | 19 | if (child.stderr) { 20 | child.stderr.on('data', (data: Buffer) => { 21 | stderr.push(data.toString()); 22 | }); 23 | } 24 | 25 | child.on('error', (error) => { 26 | reject(error); 27 | }); 28 | 29 | child.on('close', (code) => { 30 | if (code === 0) { 31 | resolve({ stdout: stdout.join('\n'), stderr: stderr.join('\n') }); 32 | } else { 33 | reject(new Error(`Command "${command}" exited with code ${code}.\nStderr: ${stderr}`)); 34 | } 35 | }); 36 | }); 37 | } -------------------------------------------------------------------------------- /src/codegen-cli/setup-codegen-cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { runCommand } from './run-command'; 4 | import { existsSync } from 'fs'; 5 | import { dirname, resolve, parse, posix } from 'path'; 6 | import { writeFile, mkdir } from 'fs/promises'; 7 | import { hideBin } from 'yargs/helpers'; 8 | import { generate, loadCodegenConfig, CodegenConfig } from '@graphql-codegen/cli'; 9 | import { getPathToTmpCoverageStash } from '../coverage-reporter/coverageStashPath'; 10 | import gqlg from 'gql-generator'; 11 | import yargs from 'yargs'; 12 | import prettier from 'prettier'; 13 | 14 | export async function configToFile( 15 | config: CodegenConfig, 16 | outputPath = './codegen.ts' 17 | ): Promise { 18 | const configObjectString = inspectConfig(config); 19 | 20 | const fileContent = `/** 21 | * GraphQL Code Generator Configuration 22 | * @see https://the-guild.dev/graphql/codegen/docs/config-reference/codegen-config 23 | */ 24 | import type { CodegenConfig } from '@graphql-codegen/cli'; 25 | 26 | const config: CodegenConfig = ${configObjectString}; 27 | 28 | export default config; 29 | `; 30 | 31 | const prettierOptions = await prettier.resolveConfig(process.cwd()) || {}; 32 | const formattedContent = await prettier.format(fileContent, { 33 | ...prettierOptions, 34 | parser: 'typescript', 35 | }); 36 | 37 | const resolvedPath = resolve(outputPath); 38 | await mkdir(dirname(resolvedPath), { recursive: true }); 39 | await writeFile(resolvedPath, formattedContent, 'utf8'); 40 | } 41 | 42 | function inspectConfig(obj: any, depth = 0): string { 43 | if (depth > 20) return '{/* Depth limit exceeded */}'; 44 | 45 | if (obj === null) return 'null'; 46 | if (obj === undefined) return 'undefined'; 47 | 48 | // Handle functions (hooks, custom scalars, etc.) 49 | if (typeof obj === 'function') { 50 | 51 | return obj.toString(); 52 | } 53 | 54 | if (typeof obj !== 'object') { 55 | 56 | return JSON.stringify(obj); 57 | } 58 | 59 | if (Array.isArray(obj)) { 60 | if (obj.length === 0) return '[]'; 61 | const items = obj.map(item => inspectConfig(item, depth + 1)); 62 | 63 | return `[${items.join(', ')}]`; 64 | } 65 | 66 | if (Object.keys(obj).length === 0) return '{}'; 67 | 68 | const entries = Object.entries(obj).map(([key, value]) => { 69 | const formattedKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) 70 | ? key 71 | : JSON.stringify(key); 72 | 73 | return `${formattedKey}: ${inspectConfig(value, depth + 1)}`; 74 | }); 75 | 76 | return `{ 77 | ${entries.join(',\n ')} 78 | }`; 79 | } 80 | 81 | function buildCodegenConfig( 82 | schemas: string[], 83 | documents: string[][], 84 | gqlClients: string[], 85 | rawRequest: boolean, 86 | enumsAsConst: boolean, 87 | silent: boolean, 88 | ): CodegenConfig { 89 | return { 90 | generates: gqlClients.reduce((acc: any, clientPath: string, currentIndex: number) => { 91 | acc[clientPath] = { 92 | schema: schemas[currentIndex], 93 | documents: documents[currentIndex], 94 | plugins: ['typescript', 'typescript-operations', 'typescript-generic-sdk'], 95 | config: { 96 | rawRequest, 97 | enumsAsConst, 98 | scalars: { 99 | BigInt: 'bigint|number', 100 | Date: 'string', 101 | }, 102 | }, 103 | }; 104 | 105 | return acc; 106 | }, {}), 107 | silent 108 | }; 109 | } 110 | 111 | async function ensureDirectoryExists(filePath: string): Promise { 112 | const directory = parse(filePath).dir; 113 | if (directory.length && !existsSync(directory)) { 114 | await mkdir(directory, { recursive: true }); 115 | } 116 | } 117 | 118 | async function getSchemasFromUrls(url: string[], schema: string[], header: string[] | undefined): Promise { 119 | if (url.length === schema.length) { 120 | const apiCalls = url.map((url, index) => ({ 121 | url, 122 | schema: schema[index] 123 | })); 124 | 125 | await Promise.all(apiCalls.map(async i => { 126 | 127 | await ensureDirectoryExists(i.schema); 128 | 129 | await runCommand( 130 | header ? 131 | `get-graphql-schema ${i.url} > ${i.schema} ${header.map(h => `-h "${h}"`).join(' ')}` : 132 | `get-graphql-schema ${i.url} > ${i.schema}` 133 | ); 134 | log(`Schema generated from "${i.url}" to "${i.schema}".`); 135 | })); 136 | 137 | return true; 138 | } else { 139 | log('Please provide equal count of url and schema parameters.'); 140 | 141 | return false; 142 | } 143 | } 144 | 145 | async function appendCode(gqlFiles: string[], coverage: boolean) { 146 | 147 | const importFragment = coverage ? 'getSdkRequester, coverageLogger' : 'getSdkRequester'; 148 | 149 | const getSdkCoverageFragment = (loggerStash: string) => `coverageLogger(getSdk(getSdkRequester(apiContext, options, requestHandler)), '${loggerStash}')`; 150 | const getSdkFragment = () => 'getSdk(getSdkRequester(apiContext, options, requestHandler))'; 151 | 152 | const graphqlAutogeneratedFileModification = (loggerStash?: string) => ` 153 | 154 | // This additional logic appended by playwright-graphql cli to ensure seamless integration 155 | import { ${importFragment} } from 'playwright-graphql'; 156 | 157 | export type APIRequestContext = Parameters[0]; 158 | export type RequesterOptions = Parameters[1] | string; 159 | export type RequestHandler = Parameters[2]; 160 | 161 | export const getClient = (apiContext: APIRequestContext, options?: RequesterOptions, requestHandler?: RequestHandler) => ${loggerStash ? getSdkCoverageFragment(loggerStash) : getSdkFragment()}; 162 | 163 | export type GqlAPI = ReturnType; 164 | 165 | `; 166 | 167 | await Promise.all( 168 | gqlFiles.map(gqlClientFilePath => writeFile( 169 | gqlClientFilePath, 170 | graphqlAutogeneratedFileModification(coverage ? getPathToTmpCoverageStash(gqlClientFilePath) : undefined), 171 | { flag: 'a' } 172 | ) 173 | ) 174 | ); 175 | 176 | log('Type Script types for Playwright auto generated type safe GQL client generated.'); 177 | } 178 | 179 | const convertToGlob = (path: string) => `${path}/**/*.{gql,graphql}`; 180 | 181 | let isSilent: boolean = false; 182 | const originalLog = console.log; 183 | // mute default gqlg logs, they are not informative 184 | console.log = () => {}; 185 | function log(...args: string[]): void { 186 | if (isSilent) return; 187 | originalLog(...args); 188 | } 189 | 190 | async function main() { 191 | const argv = await yargs(hideBin(process.argv)) 192 | .option('url', { 193 | alias: 'u', 194 | describe: 'Full GraphQL endpoint URL', 195 | type: 'array' 196 | }) 197 | .option('header', { 198 | alias: 'h', 199 | describe: 'Optional authentication header for the get-graphql-schema command.', 200 | type: 'array' 201 | }) 202 | .option('schema', { 203 | alias: 's', 204 | describe: 'Path to save the generated GraphQL schema file.', 205 | type: 'array', 206 | default: ['schema.gql'] 207 | }) 208 | .option('gqlDir', { 209 | alias: 'd', 210 | describe: 'Path to save the auto generated GraphQL files.', 211 | type: 'string', 212 | default: 'gql' 213 | }) 214 | .option('gqlFile', { 215 | alias: 'f', 216 | describe: 'Path to save the auto generated GraphQL queries and mutations and type script types.', 217 | type: 'array', 218 | default: ['graphql.ts'] 219 | }) 220 | .option('document', { 221 | alias: 'o', 222 | describe: 'Glob pattern that will be added to documents.', 223 | type: 'array' 224 | }) 225 | .option('depthLimit', { 226 | describe: 'Defines the maximum depth of nested fields to include in the generated GraphQL queries.', 227 | type: 'number', 228 | default: 20 229 | }) 230 | .option('introspect', { 231 | alias: 'i', 232 | describe: 'Introspect autogenerate operations, set false to turn off.', 233 | type: 'boolean', 234 | default: true 235 | }) 236 | .option('codegen', { 237 | alias: 'c', 238 | describe: 'Path to save the codegen config to for type script types.', 239 | type: 'string', 240 | default: 'codegen.ts', 241 | }) 242 | .option('saveCodegen', { 243 | describe: 'Pass to save codegen file.', 244 | type: 'boolean', 245 | default: false 246 | }) 247 | .option('custom', { 248 | describe: 'Pass to generate client from custom codegen.ts file.', 249 | type: 'boolean', 250 | default: false 251 | }) 252 | .option('raw', { 253 | describe: 'Pass to generate client with not type safe response.', 254 | type: 'boolean', 255 | default: false 256 | }) 257 | .option('enumsAsConst', { 258 | describe: 'Type safe client will be build with "as const" instead of enum.', 259 | type: 'boolean', 260 | default: false 261 | }) 262 | .option('coverage', { 263 | describe: 'Will add coverage logger to auto-generated client.', 264 | type: 'boolean', 265 | default: false 266 | }) 267 | .option('silent', { 268 | type: 'boolean', 269 | description: 'Suppress all logs.', 270 | default: false 271 | }) 272 | .version() 273 | .help() 274 | .argv; 275 | 276 | isSilent = argv.silent; 277 | 278 | if (argv.custom) { 279 | const codegen = await loadCodegenConfig({ configFilePath: argv.codegen }); 280 | 281 | await generate(codegen.config, true); 282 | 283 | const gqlFiles = Object.keys(codegen.config.generates); 284 | 285 | await appendCode(gqlFiles, argv.coverage); 286 | } else { 287 | 288 | const schemas = (argv.schema as string[]).map(schema => (schema.endsWith('.gql') || schema.endsWith('.graphql')) ? schema : `${schema}.gql`); 289 | 290 | if (argv.url) { 291 | const result = await getSchemasFromUrls(argv.url as string[], argv.schema as string[], argv.header as string[]); 292 | 293 | if (!result) return; 294 | } 295 | 296 | for (const schema of schemas) { 297 | if (!existsSync(schema)) { 298 | log(`Schema file: "${argv.schema}" was not found.`); 299 | log('Exit with no generated output.'); 300 | 301 | return; 302 | } 303 | } 304 | 305 | if (!argv.introspect && !argv.document) { 306 | log('Client can not be build without any operations, in case of introspect false set path to custom operations: "-o path/to/folder-with-operations"'); 307 | return; 308 | } 309 | 310 | const operationsPaths: string[][] = []; 311 | 312 | if (argv.document) { 313 | const documents = argv.document as string[]; 314 | documents.forEach((doc, index) => { 315 | if (operationsPaths[index]) { 316 | operationsPaths[index].push(convertToGlob(doc)); 317 | } else { 318 | operationsPaths.push([convertToGlob(doc)]); 319 | } 320 | }); 321 | } 322 | 323 | const buildOperationsPath = (schema: string) => posix.join(argv.gqlDir, parse(schema).name, 'autogenerated-operations'); 324 | 325 | if (argv.introspect) { 326 | if (isNaN(argv.depthLimit)) { 327 | console.error('--depthLimit NaN but should be number'); 328 | return; 329 | } 330 | 331 | for (let i = 0; i < schemas.length; i++) { 332 | const operationsPath = buildOperationsPath(schemas[i]); 333 | 334 | gqlg({ 335 | schemaFilePath: schemas[i], 336 | destDirPath: operationsPath, 337 | depthLimit: argv.depthLimit, 338 | fileExtension: 'gql' 339 | }); 340 | 341 | if (operationsPaths[i]) { 342 | operationsPaths[i].push(convertToGlob(operationsPath)); 343 | } else { 344 | operationsPaths.push([convertToGlob(operationsPath)]); 345 | } 346 | 347 | log(`Operations were generated and saved to "${operationsPath}".`); 348 | } 349 | } 350 | 351 | const gqlFiles: string[] = (schemas.length === operationsPaths.length && operationsPaths.length === (argv.gqlFile as string[]).length) ? 352 | (argv.gqlFile as string[]).map(file => `${argv.gqlDir}/${file.endsWith('.ts') ? file : `${file}.ts`}`) : 353 | schemas.map(schema => posix.join(argv.gqlDir, `${parse(schema).name}.ts`)); 354 | 355 | await generate(buildCodegenConfig(schemas, operationsPaths, gqlFiles, argv.raw, argv.enumsAsConst, argv.silent), true); 356 | 357 | await appendCode(gqlFiles, argv.coverage); 358 | 359 | if (argv.saveCodegen) { 360 | await configToFile(buildCodegenConfig(schemas, operationsPaths, gqlFiles, argv.raw, argv.enumsAsConst, argv.silent), argv.codegen); 361 | log(`File "${argv.codegen}" generated.`); 362 | } 363 | } 364 | } 365 | 366 | main().catch((err) => { 367 | console.error(err); 368 | process.exit(1); 369 | }); 370 | -------------------------------------------------------------------------------- /src/coverage-reporter/coverage-calculation-helpers.ts: -------------------------------------------------------------------------------- 1 | import { OperationSchema, ParsedParameter, ParsedParameters } from "./types"; 2 | 3 | export function floorDecimal(value: number, decimalPlaces: number): number { 4 | //The multiplier is calculated as 10n, where n is the number of decimal places you want to round to. 5 | const multiplier = Math.pow(10, decimalPlaces); 6 | return Math.floor(value * multiplier) / multiplier; 7 | } 8 | 9 | 10 | function incrementCounterForParamRecursively(paramSchema: ParsedParameter, inputParam: any): void { 11 | paramSchema.called++; 12 | 13 | if (paramSchema.subParams && inputParam) { 14 | Object.keys(inputParam).forEach((key: string) => { 15 | const schema = paramSchema.subParams?.find((i) => i.key === key); 16 | if (schema) { 17 | incrementCounterForParamRecursively(schema, inputParam[key]); 18 | } 19 | }); 20 | return; 21 | } 22 | 23 | if (paramSchema.enumValues && inputParam) { 24 | const schema = paramSchema.enumValues.find(i => i.value == inputParam); 25 | if (schema) schema.called++; 26 | } 27 | } 28 | 29 | export function incrementCountersInOperationSchema(schema: OperationSchema, inputParams: any[]): void { 30 | inputParams.forEach((param) => { 31 | if (param) { 32 | Object.keys(param).forEach((key) => { 33 | const paramSchema = schema.inputParams.find(i => i.key === key); 34 | if (paramSchema) { 35 | incrementCounterForParamRecursively(paramSchema, param[key]); 36 | } 37 | }); 38 | } 39 | }); 40 | } 41 | 42 | function putCoverSign(called: number): string { 43 | return called > 0 ? '\u2714' : '\u2718'; // mark(✔) and cross(✘) signs 44 | } 45 | 46 | export function calculateOperationCoverage(params: ParsedParameters): number { 47 | let totalCoverageItems = 0; 48 | let coveredItems = 0; 49 | const incrementation = (i: ParsedParameter) => { 50 | totalCoverageItems++; 51 | if (i.called > 0) coveredItems++; 52 | if (i.subParams) { 53 | i.subParams.forEach(param => { 54 | incrementation(param) 55 | }); 56 | } else if (i.enumValues) { 57 | i.enumValues.forEach(ev => { 58 | totalCoverageItems++; 59 | if (ev.called > 0) { 60 | coveredItems++; 61 | } 62 | }) 63 | } 64 | }; 65 | 66 | params.forEach((i) => incrementation(i)); 67 | 68 | if (totalCoverageItems === 0) { 69 | return 100; // coverage for operations without arguments. 70 | } 71 | if (coveredItems > totalCoverageItems) { 72 | throw new Error('Args coverage calculation has an issue!'); 73 | } 74 | 75 | return floorDecimal((coveredItems / totalCoverageItems) * 100, 2); 76 | } 77 | 78 | export function buildParamCoverageString(param: ParsedParameter, indent: string = ''): string { 79 | const currentIndent = indent + ' '; 80 | let result = `${indent}${param.key} ${putCoverSign(param.called)}`; 81 | if (param.subParams) { 82 | result += `: {\n${param.subParams.map(i => buildParamCoverageString(i, currentIndent)).join(',\n')}\n${indent}}`; 83 | } else if (param.enumValues) { 84 | result += `: [${param.enumValues.map(i => `${i.value} ${putCoverSign(param.called)}`).join(', ')}]`; 85 | } 86 | 87 | return result; 88 | } 89 | 90 | export function calculateTotalArgsCoverage(coverageList: string[]): string { 91 | let totalCoverage = 0; 92 | let count = 0; 93 | 94 | for (const coverage of coverageList) { 95 | const percentageValue = parseFloat(coverage.replace('%', '')); 96 | totalCoverage += percentageValue; 97 | count++; 98 | } 99 | const totalArgsCoverage = count > 0 ? totalCoverage / count : 0; 100 | 101 | return `${totalArgsCoverage.toFixed(2)}%`; 102 | } -------------------------------------------------------------------------------- /src/coverage-reporter/coverageLogger.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises'; 2 | import { join } from 'path'; 3 | 4 | export function coverageLogger(obj: T, loggerStashDir: string): T { 5 | return new Proxy(obj, { 6 | get(target: T, prop: string) { 7 | const originalMethod = (target as any)[prop]; 8 | 9 | if (typeof originalMethod === 'function') { 10 | return async function (...args: any[]) { 11 | const logCoverage = writeFile(join(loggerStashDir, prop), `${JSON.stringify({ name: prop, inputParams: args })},`, { flag: 'a' }); 12 | const gqlResponse = await originalMethod.apply(target, args); 13 | await logCoverage; 14 | return gqlResponse; 15 | }; 16 | } 17 | 18 | return originalMethod; 19 | }, 20 | }); 21 | } -------------------------------------------------------------------------------- /src/coverage-reporter/coverageStashPath.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export function getPathToTmpCoverageStash(path: string): string { 4 | const parts = path.split('/'); 5 | const fileWithExtension = parts[parts.length - 1]; 6 | 7 | return join(process.cwd(), `.${fileWithExtension.replace('.ts', '')}-coverage`); 8 | } -------------------------------------------------------------------------------- /src/coverage-reporter/gql-client-parser.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { ParsedParameters, EnumValues,OperationSchema } from './types'; 3 | 4 | function removeOptionalFromKey(key: string): string { 5 | return key.replace(/\?|\s/g, ''); 6 | } 7 | 8 | function isTypeCustom(typeString: string) { 9 | // all not custom types that can be generated in schema: 10 | // js primitives including bigint https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures 11 | // possible custom gql scalar types https://www.apollographql.com/docs/apollo-server/schema/custom-scalars 12 | // never and function are not in lists but in theory may be generated to I was not able to found any proof of opposite. 13 | return ![ 14 | 'id', 15 | 'null', 16 | 'undefined', 17 | 'string', 18 | 'number', 19 | 'bigint', 20 | 'int', 21 | 'float', 22 | 'boolean', 23 | 'date', 24 | 'function', 25 | 'symbol', 26 | 'never', 27 | 'jsonobject', 28 | 'json', 29 | 'file', 30 | ].includes(typeString.toLowerCase()); 31 | } 32 | 33 | function parseTypeStatement(typeStatement: ts.Statement): ParsedParameters { 34 | return typeStatement.getText() 35 | .replace(/.+=|[\n{}]|\/\**.*\//g, '') 36 | .split(/;/g) 37 | .filter(i => i) 38 | .map((i) => { 39 | const [key, value] = i.trim().split(/:\s/); 40 | const narrowedTypeString = value.replace(/.+<|>/g, '').replace(/[A-Za-z]+\['|'.+/g, ''); 41 | return { 42 | key: removeOptionalFromKey(key), 43 | type: narrowedTypeString, 44 | called: 0 45 | }; 46 | }); 47 | } 48 | 49 | function parseRootInputParamsType(typeString: string): ParsedParameters { 50 | // regexp was written to parse key value pairs from raw arguments string like: 51 | // Exact<{ filters?: CoachingRouterPhoneNumberListFilters; skip: number; sort?: CoachingRouterPhoneNumberSortInput; take: number; }> 52 | // Exact<{ filters: AgentsCallsMetricsListFilters; gradings: CallsGradingsInput; }> 53 | // Exact<{ params: AppointmentsAggregatedByAgentListArgs; params1: AppointmentsKpIsParams; }> 54 | const propertyPattern = /(\w+(\?)?|\[\w+:\s\w+])+:\s\w+/g; 55 | const propStrings = typeString.match(propertyPattern); 56 | return propStrings ? propStrings.map(p => { 57 | const [key, type] = p.split(/:\s(?!\w+])/g); 58 | return { key: removeOptionalFromKey(key), type, called: 0 }; 59 | }) : []; 60 | } 61 | 62 | function isEnum(sourceFile: ts.SourceFile, name: string): boolean { 63 | return !!sourceFile.statements.find((i) => { 64 | return ts.isEnumDeclaration(i) && i.name.text === name; 65 | }); 66 | } 67 | 68 | function parseEnumStatement(sourceFile: ts.SourceFile, enumName: string): EnumValues { 69 | const enumDeclaration = sourceFile.statements.find((i) => { 70 | return ts.isEnumDeclaration(i) && i.name.text === enumName; 71 | }); 72 | 73 | if(!enumDeclaration) return []; 74 | 75 | return enumDeclaration.getText() 76 | .replace(/.+{|}/g, '') 77 | .split(/,/g) 78 | .filter(i => i) 79 | .map((i) => { 80 | const [key, value] = i.trim().split(/=\s/); 81 | return { key: removeOptionalFromKey(key), value: value.replace(/'/g, ''), called: 0 }; 82 | }); 83 | } 84 | 85 | function parseCustomType(sourceFile: ts.SourceFile, name: string): ParsedParameters { 86 | 87 | const typeDeclaration = sourceFile.statements.find((i) => { 88 | return ts.isTypeAliasDeclaration(i) && i.name.text === name; 89 | }); 90 | 91 | if (typeDeclaration) { 92 | const parsedTypes = parseTypeStatement(typeDeclaration); 93 | return parsedTypes.map((i) => { 94 | if (isTypeCustom(i.type)) { 95 | const custom = isEnum(sourceFile, i.type) ? 96 | { enumValues: parseEnumStatement(sourceFile, i.type) } : 97 | isEnumAsConst(sourceFile, i.type) ? 98 | { enumValues: parseEnumAsConstStatement(sourceFile, i.type) } : 99 | { subParams: parseCustomType(sourceFile, i.type) }; 100 | return { 101 | ...i, 102 | ...custom 103 | }; 104 | } 105 | return i; 106 | }); 107 | } 108 | 109 | return []; 110 | } 111 | 112 | function isEnumAsConst(sourceFile: ts.SourceFile, name: string): boolean { 113 | const typeAlias = sourceFile.statements.find(stmt => 114 | ts.isTypeAliasDeclaration(stmt) && 115 | stmt.name.text === name 116 | ); 117 | 118 | if (typeAlias && ts.isTypeAliasDeclaration(typeAlias)) { 119 | const typeText = typeAlias.type.getText(sourceFile); 120 | const pattern = `typeof\\s+${name}\\[keyof typeof\\s+${name}\\]`; 121 | const regex = new RegExp(pattern); 122 | 123 | return regex.test(typeText); 124 | } 125 | 126 | return false; 127 | } 128 | 129 | function parseEnumAsConstStatement(sourceFile: ts.SourceFile, name: string): EnumValues { 130 | const constNameMatch = new RegExp(`(export\\s+)?const\\s+${name}\\s*=\\s*\\{`); 131 | const enumAsConst = sourceFile.statements.find((statement) => { 132 | return ts.isVariableStatement(statement) && constNameMatch.test(statement.getText()); 133 | }); 134 | 135 | if (enumAsConst) { 136 | return enumAsConst.getText() 137 | .replace(/.+{|}\sas\sconst;/g, '') 138 | .split(/,/g) 139 | .filter(i => i) 140 | .map((i) => { 141 | const [key, value] = i.trim().split(/:\s/); 142 | 143 | return { key: removeOptionalFromKey(key), value: value.replace(/'/g, ''), called: 0 }; 144 | }); 145 | } 146 | 147 | return []; 148 | } 149 | 150 | 151 | export function extractOperationsInputParamsSchema(absolutePath: string, sdkFunctionName: string = 'getSdk'): OperationSchema[] { 152 | const program: ts.Program = ts.createProgram([absolutePath], { emitDeclarationOnly: true }); 153 | const sourceFile: ts.SourceFile | undefined = program.getSourceFile(absolutePath); 154 | const typeChecker: ts.TypeChecker = program.getTypeChecker(); 155 | 156 | if (!sourceFile) { 157 | throw new Error(`Source file '${absolutePath}' not found.`); 158 | } 159 | 160 | const sdkFunction: ts.FunctionDeclaration | undefined = sourceFile.statements.find( 161 | (s) => ts.isFunctionDeclaration(s) && s.name?.text === sdkFunctionName 162 | ) as ts.FunctionDeclaration; 163 | 164 | if (!sdkFunction) { 165 | throw new Error(`Function: '${sdkFunctionName}' not found in file: '${absolutePath}'`); 166 | } 167 | 168 | const sdkFunctionType = typeChecker.getTypeAtLocation(sdkFunction); 169 | 170 | const returnType = sdkFunctionType.getCallSignatures()[0]?.getReturnType(); 171 | 172 | if (!returnType) { 173 | throw new Error(`The return type of '${sdkFunctionName}' could not be determined.`); 174 | } 175 | 176 | const operations = typeChecker.getPropertiesOfType(returnType); 177 | const operationsMap: OperationSchema[] = []; 178 | 179 | for (const operation of operations) { // gql sdk properties loop 180 | const operationName = operation.getName(); 181 | const propDeclaration = operation.valueDeclaration; 182 | const operationData: OperationSchema = { name: operationName, inputParams: [] }; 183 | if (propDeclaration) { 184 | const propType = typeChecker.getTypeOfSymbolAtLocation(operation, propDeclaration); 185 | const signatures = propType.getCallSignatures(); 186 | 187 | for (const signature of signatures) { // operations signature extraction loop 188 | const parameters = signature.getParameters(); 189 | for (const param of parameters) { // operation input parameters loop 190 | if (param.getName() !== 'options') { 191 | const inputParamsType = typeChecker.getTypeOfSymbolAtLocation(param, param.valueDeclaration!); 192 | const parsedRootParameters = parseRootInputParamsType(typeChecker.typeToString(inputParamsType)); 193 | 194 | parsedRootParameters.forEach(i => { 195 | if (isTypeCustom(i.type)) { 196 | const parsedParams = isEnum(sourceFile, i.type) ? 197 | { enumValues: parseEnumStatement(sourceFile, i.type) }: 198 | isEnumAsConst(sourceFile, i.type) ? 199 | { enumValues: parseEnumAsConstStatement(sourceFile, i.type) } : 200 | { subParams: parseCustomType(sourceFile, i.type) }; 201 | operationData.inputParams?.push({ 202 | ...i, 203 | ...parsedParams, 204 | }); 205 | } else if (i.type !== 'never') { 206 | operationData.inputParams?.push({ 207 | ...i, 208 | }); 209 | } 210 | }); 211 | } 212 | } 213 | } 214 | } 215 | operationsMap.push(operationData); 216 | } 217 | 218 | return operationsMap; 219 | } -------------------------------------------------------------------------------- /src/coverage-reporter/html-generator.ts: -------------------------------------------------------------------------------- 1 | import { Summary } from './report'; 2 | import { OperationSchema } from './types'; 3 | import { buildParamCoverageString } from './coverage-calculation-helpers'; 4 | 5 | export function generateHtmlReport(summary: Summary, operationsSchema: OperationSchema[]): string { 6 | return ` 7 | 8 | 9 | 10 | 11 | 12 | GraphQL Coverage Report 13 | 100 | 101 | 102 |
103 |
104 |

GraphQL Coverage Report

105 |
106 | 107 |
108 |
Coverage:
${summary.coverage}
109 |
Total Operations:
${summary.coverageTotal}
110 |
Covered:
${summary.covered}
111 |
Total Args Coverage:
${summary.operationsCoverageSummary}
112 |
113 | 114 |
115 | ${operationsSchema.map(operation => { 116 | const coverageInfo = summary.operationsArgCoverage 117 | .find(op => op.name === operation.name); 118 | 119 | const isCovered = coverageInfo?.covered; 120 | 121 | const argsMap = `(${operation.inputParams.map(param => buildParamCoverageString(param)).join(',\n')});\n`; 122 | return ` 123 |
124 |

${operation.name} 125 | 126 | ${coverageInfo?.argsCoverage || 'N/A'} 127 | 128 |

129 |
${argsMap.trim() || 'No arguments provided'}
130 |
`; 131 | }).join('')} 132 |
133 |
134 | 135 | `; 136 | } -------------------------------------------------------------------------------- /src/coverage-reporter/index.ts: -------------------------------------------------------------------------------- 1 | import GraphqlCoverageReport from './report'; 2 | 3 | export default GraphqlCoverageReport; 4 | -------------------------------------------------------------------------------- /src/coverage-reporter/report.ts: -------------------------------------------------------------------------------- 1 | import type { Reporter } from '@playwright/test/reporter'; 2 | import { existsSync } from 'fs'; 3 | import { access, rm, writeFile, mkdir, readdir, readFile } from 'fs/promises'; 4 | import { generateHtmlReport } from './html-generator'; 5 | import { extractOperationsInputParamsSchema } from './gql-client-parser'; 6 | import { join, resolve } from 'path'; 7 | import { OperationSchema } from './types'; 8 | import { 9 | incrementCountersInOperationSchema, 10 | calculateOperationCoverage, 11 | buildParamCoverageString, 12 | calculateTotalArgsCoverage, 13 | floorDecimal 14 | } from './coverage-calculation-helpers'; 15 | import { getPathToTmpCoverageStash } from './coverageStashPath'; 16 | 17 | function isFileExists(path: string): Promise { 18 | return access(path).then(() => true, () => false); 19 | } 20 | 21 | export type Summary = { 22 | coverage?: string, 23 | coverageTotal?: string, 24 | covered?: string, 25 | operationsCoverageSummary?: string, 26 | operationsArgCoverage: { name: string, argsCoverage: string, covered: boolean }[], 27 | }; 28 | 29 | export default class GraphqlCoverageReport implements Reporter { 30 | 31 | private readonly coverageDir: string; 32 | private readonly apiClientFileName: string; 33 | private readonly operationsSchema: OperationSchema[]; 34 | private readonly logUncoveredOperations: boolean = false; 35 | private readonly coverageFilePath: string; 36 | private readonly htmlFilePath: string; 37 | private readonly minCoveragePerOperation: number; 38 | private readonly saveGqlCoverageLog: boolean; 39 | private readonly saveHtmlSummary: boolean; 40 | private summary: Summary = { operationsArgCoverage: [] }; 41 | 42 | constructor(options: { 43 | graphqlFilePath: string, 44 | coverageFilePath?: string, 45 | htmlFilePath?: string, 46 | logUncoveredOperations?: boolean, 47 | minCoveragePerOperation?: number, 48 | saveGqlCoverageLog?: boolean, 49 | saveHtmlSummary?: boolean, 50 | }) { 51 | const absolutePath = resolve(options.graphqlFilePath); 52 | if (!existsSync(absolutePath)) { 53 | throw new Error(`Source file '${absolutePath}' does not exist.`); 54 | } 55 | this.apiClientFileName = options.graphqlFilePath.split('/').at(-1) as string; 56 | this.coverageDir = getPathToTmpCoverageStash(options.graphqlFilePath); 57 | this.operationsSchema = extractOperationsInputParamsSchema(absolutePath); 58 | this.logUncoveredOperations = options.logUncoveredOperations ?? false; 59 | this.minCoveragePerOperation = options.minCoveragePerOperation ?? 100; 60 | this.coverageFilePath = options.coverageFilePath ?? `./${this.apiClientFileName.replace('.ts', '')}-coverage.log`; 61 | this.htmlFilePath = options.htmlFilePath ?? `./${this.apiClientFileName.replace('.ts', '')}-coverage.html`; 62 | this.saveGqlCoverageLog = options.saveGqlCoverageLog ?? false; 63 | this.saveHtmlSummary = options.saveHtmlSummary ?? false; 64 | } 65 | 66 | async onBegin() { 67 | if (await isFileExists(this.coverageDir)) await rm(this.coverageDir, { recursive: true }); 68 | await mkdir(this.coverageDir); 69 | } 70 | 71 | async onEnd() { 72 | if (!await isFileExists(this.coverageDir)) throw new Error(`Directory with logged coverage was not found: ${this.coverageDir}`); 73 | const operationsFiles = await readdir(this.coverageDir); 74 | 75 | const coveredOperationsWithArgs: { name: string, calls: { name: string, inputParams: any[] }[] }[] = await Promise.all( 76 | operationsFiles.map(async (fileName) => { 77 | const operationFile = await readFile(join(this.coverageDir, fileName), { encoding: 'utf8' }); 78 | return { 79 | name: fileName, 80 | calls: JSON.parse(`[${operationFile.slice(0, -1)}]`), // .slice(0, -1) because last char always will be a comma. 81 | }; 82 | }) 83 | ); 84 | 85 | coveredOperationsWithArgs.forEach((operation) => { 86 | const operationSchema = this.operationsSchema.find(i => i.name === operation.name); 87 | if (operationSchema) { 88 | operation.calls.forEach((i) => { 89 | if (i.inputParams.length) { 90 | incrementCountersInOperationSchema(operationSchema, i.inputParams); 91 | } 92 | }); 93 | } 94 | }); 95 | 96 | if (await isFileExists(this.coverageFilePath)) await rm(this.coverageFilePath); 97 | 98 | const coveredInTests = coveredOperationsWithArgs.map(i => i.name); 99 | 100 | for (const operation of this.operationsSchema) { 101 | const covered = coveredInTests.includes(operation.name); 102 | const operationCoverage = covered ? 103 | !operation.inputParams.length ? 100 : calculateOperationCoverage(operation.inputParams) : 0; 104 | const opCoverageString = `${operationCoverage}%`; 105 | this.summary.operationsArgCoverage.push({ 106 | name: operation.name, 107 | argsCoverage: opCoverageString, 108 | covered: covered ? operationCoverage >= this.minCoveragePerOperation : false 109 | }); 110 | 111 | const argsMap = `(${operation.inputParams.map(param => buildParamCoverageString(param)).join(',\n')});\n`; 112 | const operationCoverageString = `Args coverage: ${opCoverageString}\n${operation.name} ${argsMap}`; 113 | if (this.saveGqlCoverageLog) await writeFile(this.coverageFilePath, operationCoverageString + '\n', { flag: 'a' }); 114 | } 115 | 116 | this.summary.operationsCoverageSummary = `Total arguments coverage: ${calculateTotalArgsCoverage(this.summary.operationsArgCoverage.map(i => i.argsCoverage))}`; 117 | if (this.saveGqlCoverageLog) await writeFile(this.coverageFilePath, this.summary.operationsCoverageSummary + '\n', { flag: 'a' }); 118 | 119 | const coveredOperations = this.summary.operationsArgCoverage 120 | .filter(i => coveredInTests.includes(i.name) && i.covered).length; 121 | [ this.summary.coverage, this.summary.coverageTotal, this.summary.covered ] = [ 122 | `GQL Operations coverage in executed tests: ${floorDecimal((coveredOperations / this.operationsSchema.length) * 100, 2)}%`, 123 | `Total operations: ${this.operationsSchema.length}`, 124 | `Covered operations: ${coveredOperations}`, 125 | ]; 126 | 127 | if (this.saveHtmlSummary) { 128 | await writeFile(this.htmlFilePath, generateHtmlReport(this.summary, this.operationsSchema)); 129 | } 130 | 131 | await rm(this.coverageDir, { recursive: true }); 132 | } 133 | 134 | async onExit(): Promise { 135 | console.log(`\n=== Coverage for ${this.apiClientFileName} client `+'==='); 136 | console.log(this.summary.coverage); 137 | console.log(this.summary.coverageTotal); 138 | console.log(this.summary.covered); 139 | console.log(this.summary.operationsCoverageSummary); 140 | if (this.logUncoveredOperations) { 141 | console.log('==='+' Uncovered operations '+'==='); 142 | console.log(`${this.summary.operationsArgCoverage 143 | .filter(i => !i.covered) 144 | .map(({ name, argsCoverage }) => `${name} ${argsCoverage}`) 145 | .join('\n')}`); 146 | } 147 | console.log('============================\n'); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/coverage-reporter/types.ts: -------------------------------------------------------------------------------- 1 | export type EnumValues = { key: string, value: string | number, called: 0 }[]; 2 | export type ParsedParameter = { key: string, type: string, called: number, subParams?: ParsedParameter[], enumValues?: EnumValues }; 3 | export type ParsedParameters = ParsedParameter[]; 4 | export type OperationSchema = { name: string, inputParams: ParsedParameters }; -------------------------------------------------------------------------------- /src/gql-generator.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'gql-generator'; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { getSdkRequester } from './requester'; 2 | export { coverageLogger } from './coverage-reporter/coverageLogger'; -------------------------------------------------------------------------------- /src/requester.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIRequestContext, 3 | APIResponse 4 | } from 'playwright-core'; 5 | import { print, DocumentNode } from 'graphql'; 6 | import 'json-bigint-patch'; 7 | 8 | type PostOptionsType = NonNullable[1]>; 9 | 10 | type PlaywrightRequesterOptions = { 11 | returnRawJson?: boolean; 12 | failOnEmptyData?: boolean; 13 | } & Omit; 14 | 15 | type Requester = (doc: DocumentNode, vars?: V, options?: C) => Promise | AsyncIterable; 16 | 17 | type RequesterOptions = { gqlEndpoint?: string, rawResponse?: boolean }; 18 | 19 | type GqlEndpoint = string; 20 | 21 | type RequestOptions = { 22 | client: APIRequestContext, 23 | gqlEndpoint: string, 24 | variables: any, 25 | doc: DocumentNode, 26 | options?: Omit 27 | }; 28 | 29 | const defaultOptions: Required = { gqlEndpoint: '/api/graphql', rawResponse: false }; 30 | 31 | const operationDefinition = 'OperationDefinition'; 32 | const subscription = 'subscription'; 33 | const validDocDefOps = ['mutation', 'query', subscription]; 34 | 35 | const returnRawResponseStrategy = async ( 36 | response: APIResponse, 37 | ): Promise => { 38 | try { 39 | return (await response.json()); 40 | } catch (e) { 41 | throw new Error(await buildMessage(e, response)); 42 | } 43 | } 44 | 45 | const returnDataResponseStrategy = async ( 46 | response: APIResponse, 47 | options?: PlaywrightRequesterOptions, 48 | ): Promise => { 49 | let json; 50 | try { 51 | json = await response.json(); 52 | } catch (e) { 53 | throw new Error(await buildMessage(e, response)); 54 | } 55 | 56 | if (options?.returnRawJson) { 57 | return json; 58 | } 59 | 60 | if ([undefined, null].includes(json.data)) { 61 | const failOnEmptyData: boolean = options?.failOnEmptyData ?? true; 62 | 63 | if (!failOnEmptyData) { 64 | return json; 65 | } 66 | 67 | const formattedJsonString = JSON.stringify(JSON.parse(await response.text()), null, ' '); 68 | 69 | throw new Error(`No data presented in the GraphQL response: ${formattedJsonString}`); 70 | } 71 | 72 | return json.data; 73 | } 74 | 75 | 76 | 77 | function doPostRequest(requestParams: RequestOptions): Promise { 78 | return requestParams.client.post(requestParams.gqlEndpoint, { 79 | ...requestParams.options, 80 | data: { variables: requestParams.variables, query: print(requestParams.doc) }, 81 | }); 82 | } 83 | 84 | function initRequest(requestHandler?: (request: () => Promise) => Promise): (requestParams: RequestOptions) => Promise { 85 | return requestHandler ? 86 | (requestParams: RequestOptions) => requestHandler(() => doPostRequest(requestParams)) : 87 | (requestParams: RequestOptions) => doPostRequest(requestParams); 88 | } 89 | 90 | export function getSdkRequester( 91 | client: APIRequestContext, 92 | options: RequesterOptions | GqlEndpoint = defaultOptions, 93 | requestHandler?: (request: () => Promise) => Promise 94 | ): Requester { 95 | 96 | const requesterOptions = { 97 | ...defaultOptions, 98 | ...(typeof options === 'string' ? { gqlEndpoint: options } : options) 99 | }; 100 | 101 | const doRequest = initRequest(requestHandler); 102 | 103 | return requesterOptions.rawResponse ? 104 | async ( 105 | doc: DocumentNode, 106 | variables: V, 107 | options?: Omit, 108 | ): Promise => { 109 | validateDocument(doc); 110 | 111 | const request = doRequest({ client, gqlEndpoint: requesterOptions.gqlEndpoint, variables, doc, options }); 112 | 113 | const response = await request; 114 | 115 | return returnRawResponseStrategy(response); 116 | } 117 | : 118 | async ( 119 | doc: DocumentNode, 120 | variables: V, 121 | options?: PlaywrightRequesterOptions, 122 | ): Promise => { 123 | validateDocument(doc); 124 | 125 | const request = doRequest({ client, gqlEndpoint: requesterOptions.gqlEndpoint, variables, doc, options }); 126 | 127 | const response = await request; 128 | 129 | return returnDataResponseStrategy(response, options); 130 | }; 131 | } 132 | 133 | function validateDocument(doc: DocumentNode): void { 134 | // Valid document should contain *single* query or mutation unless it's has a fragment 135 | if ( 136 | doc.definitions.filter( 137 | (d) => d.kind === operationDefinition && validDocDefOps.includes(d.operation), 138 | ).length !== 1 139 | ) { 140 | throw new Error( 141 | 'DocumentNode passed to Playwright Client must contain single query or mutation', 142 | ); 143 | } 144 | 145 | const definition = doc.definitions[0]; 146 | 147 | // Valid document should contain *OperationDefinition* 148 | if (definition.kind !== operationDefinition) { 149 | throw new Error('DocumentNode passed to Playwright must contain single query or mutation'); 150 | } 151 | 152 | if (definition.operation === subscription) { 153 | throw new Error('Subscription requests through SDK interface are not supported'); 154 | } 155 | } 156 | 157 | async function buildMessage(e: any, response: APIResponse): Promise { 158 | return `${(e as Error).message} 159 | \nStatus code: ${response.status()} 160 | \nHeaders: ${JSON.stringify(response.headers())} 161 | \nResponse body is not a json but: ${await response.text()}` 162 | } 163 | -------------------------------------------------------------------------------- /tests/codegen-cli.test.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'node:util'; 2 | import { exec } from 'child_process'; 3 | import { readdir, mkdir, rm, writeFile, readFile } from 'fs/promises'; 4 | import { startFakeGraphQLServer, stopFakeGraphQLServer, lastRequestHeaders } from './resources/gql-fake-server'; 5 | import { existsSync } from 'fs'; 6 | import * as path from 'path'; 7 | import { join } from 'path'; 8 | import { loadCodegenConfig } from '@graphql-codegen/cli'; 9 | 10 | const execAsync = promisify(exec); 11 | 12 | describe('Setup Codegen CLI', () => { 13 | const cliPath = path.resolve(__dirname, '../lib/codegen-cli/setup-codegen-cli.js'); 14 | const stabServer = 'http://localhost:4000'; 15 | const schemaFile = 'test-schema.gql'; 16 | const schemaName = schemaFile.split('.')[0]; 17 | const gqlDirectory = 'graphql'; 18 | const gqlFile = 'gql.ts'; 19 | const testDir = path.join(__dirname, 'codegen-test'); 20 | 21 | jest.setTimeout(15_000); 22 | 23 | beforeAll(async () => { 24 | await startFakeGraphQLServer(); 25 | }); 26 | 27 | afterAll(async () => { 28 | await stopFakeGraphQLServer(); 29 | }); 30 | 31 | beforeEach(async () => { 32 | await mkdir(testDir); 33 | }); 34 | 35 | afterEach(async () => { 36 | if (existsSync(testDir)) { 37 | await rm(testDir, { recursive: true, force: true }); 38 | } 39 | }); 40 | 41 | test('generates schema and operations from url no headers', async () => { 42 | const cliLogs = await execAsync( 43 | `node ${cliPath} --url ${stabServer} --schema ${schemaFile} --gqlDir ${gqlDirectory} --gqlFile ${gqlFile}`, 44 | { cwd: testDir } 45 | ); 46 | 47 | const dir = await readdir(testDir, { withFileTypes: true }); 48 | expect(dir.map(i => i.name ).sort()).toEqual([ schemaFile, gqlDirectory ].sort()); 49 | 50 | expect(lastRequestHeaders).not.toHaveProperty('authorization'); 51 | 52 | expect(cliLogs).toMatchObject({ 53 | stdout: 'Schema generated from "http://localhost:4000" to "test-schema.gql".\n' + 54 | `Operations were generated and saved to "${gqlDirectory}/${schemaName}/autogenerated-operations".\n` + 55 | 'Type Script types for Playwright auto generated type safe GQL client generated.\n', 56 | }); 57 | }); 58 | 59 | test('no logs with flag silent', async () => { 60 | const cliLogs = await execAsync( 61 | `node ${cliPath} --url ${stabServer} --silent`, 62 | { cwd: testDir } 63 | ); 64 | 65 | const dir = await readdir(testDir, { withFileTypes: true }); 66 | expect(dir.map(i => i.name ).sort()).toEqual([ 'schema.gql', 'gql' ].sort()); 67 | 68 | expect(lastRequestHeaders).not.toHaveProperty('authorization'); 69 | 70 | expect(cliLogs).toMatchObject({ 71 | stdout: '', 72 | }); 73 | }); 74 | 75 | test('generates schemas and operations from multiple urls no headers', async () => { 76 | const cliLogs = await execAsync( 77 | `node ${cliPath} -u ${stabServer} -u ${stabServer} -s first-${schemaFile} -s second-${schemaFile} -f first-gql -f second-gql`, 78 | { cwd: testDir } 79 | ); 80 | 81 | const dir = await readdir(testDir, { withFileTypes: true }); 82 | expect(dir.map(i => i.name ).sort()) 83 | .toEqual([ `first-${schemaFile}`, `second-${schemaFile}`, 'gql' ].sort()); 84 | 85 | const gqlDir = await readdir(join(testDir, 'gql'), { withFileTypes: true }); 86 | expect(gqlDir.map(i => i.name ).sort()) 87 | .toEqual([ `first-test-schema`, `second-test-schema`, `first-gql.ts`, `second-gql.ts` ].sort()); 88 | 89 | // order is not strict since schema introspect works in Promise all 90 | expect(cliLogs.stdout.split('\n').filter(i => i).sort()).toMatchObject([ 91 | `Schema generated from "http://localhost:4000" to "first-${schemaFile}".`, 92 | `Schema generated from "http://localhost:4000" to "second-${schemaFile}".`, 93 | `Operations were generated and saved to "gql/first-${schemaFile.split('.')[0]}/autogenerated-operations".`, 94 | `Operations were generated and saved to "gql/second-${schemaFile.split('.')[0]}/autogenerated-operations".`, 95 | 'Type Script types for Playwright auto generated type safe GQL client generated.', 96 | ].sort()); 97 | }); 98 | 99 | test('generates schemas and operations from multiple schemas default gql naming', async () => { 100 | const cliLogs = await execAsync( 101 | `node ${cliPath} -u ${stabServer} -u ${stabServer} -s first-schema.gql -s second-schema.gql`, 102 | { cwd: testDir } 103 | ); 104 | 105 | const dir = await readdir(testDir, { withFileTypes: true }); 106 | expect(dir.map(i => i.name ).sort()) 107 | .toEqual([ `first-schema.gql`, `second-schema.gql`, 'gql' ].sort()); 108 | 109 | const gqlDir = await readdir(join(testDir, 'gql'), { withFileTypes: true }); 110 | expect(gqlDir.map(i => i.name ).sort()) 111 | .toEqual([ `first-schema`, `second-schema`, `first-schema.ts`, `second-schema.ts` ].sort()); 112 | 113 | // order is not strict since schema introspect works in Promise all 114 | expect(cliLogs.stdout.split('\n').filter(i => i).sort()).toMatchObject([ 115 | `Schema generated from "http://localhost:4000" to "first-schema.gql".`, 116 | `Schema generated from "http://localhost:4000" to "second-schema.gql".`, 117 | `Operations were generated and saved to "gql/first-schema/autogenerated-operations".`, 118 | `Operations were generated and saved to "gql/second-schema/autogenerated-operations".`, 119 | 'Type Script types for Playwright auto generated type safe GQL client generated.', 120 | ].sort()); 121 | }); 122 | 123 | test('generates multiple clients when schemas passed as related path', async () => { 124 | const cliLogs = await execAsync( 125 | `node ${cliPath} -u ${stabServer} -u ${stabServer} -s ./gql/first-schema.gql -s ./gql/second-schema.gql`, 126 | { cwd: testDir } 127 | ); 128 | 129 | const dir = await readdir(testDir, { withFileTypes: true }); 130 | expect(dir.map(i => i.name ).sort()) 131 | .toEqual([ 'gql' ].sort()); 132 | 133 | const gqlDir = await readdir(join(testDir, 'gql'), { withFileTypes: true }); 134 | expect(gqlDir.map(i => i.name ).sort()) 135 | .toEqual([ `first-schema`, `second-schema`, `first-schema.ts`, `second-schema.ts`, `first-schema.gql`, `second-schema.gql` ].sort()); 136 | 137 | // order is not strict since schema introspect works in Promise all 138 | expect(cliLogs.stdout.split('\n').filter(i => i).sort()).toMatchObject([ 139 | `Schema generated from "http://localhost:4000" to "./gql/first-schema.gql".`, 140 | `Schema generated from "http://localhost:4000" to "./gql/second-schema.gql".`, 141 | `Operations were generated and saved to "gql/first-schema/autogenerated-operations".`, 142 | `Operations were generated and saved to "gql/second-schema/autogenerated-operations".`, 143 | 'Type Script types for Playwright auto generated type safe GQL client generated.', 144 | ].sort()); 145 | }); 146 | 147 | test('generates schemas and operations from multiple schemas with custom operations', async () => { 148 | const cliLogs = await execAsync( 149 | `node ${cliPath} -u ${stabServer} -u ${stabServer} -s first-schema.gql -s second-schema.gql -o custom -o more-custom --saveCodegen`, 150 | { cwd: testDir } 151 | ); 152 | 153 | const dir = await readdir(testDir, { withFileTypes: true }); 154 | expect(dir.map(i => i.name ).sort()) 155 | .toEqual([ 'codegen.ts', 'first-schema.gql', 'second-schema.gql', 'gql' ].sort()); 156 | 157 | const gqlDir = await readdir(join(testDir, 'gql'), { withFileTypes: true }); 158 | expect(gqlDir.map(i => i.name ).sort()) 159 | .toEqual([ `first-schema`, `second-schema`, `first-schema.ts`, `second-schema.ts` ].sort()); 160 | 161 | // order is not strict since schema introspect works in Promise all 162 | expect(cliLogs.stdout.split('\n').filter(i => i).sort()).toMatchObject([ 163 | `Schema generated from "http://localhost:4000" to "first-schema.gql".`, 164 | `Schema generated from "http://localhost:4000" to "second-schema.gql".`, 165 | `Operations were generated and saved to "gql/first-schema/autogenerated-operations".`, 166 | `Operations were generated and saved to "gql/second-schema/autogenerated-operations".`, 167 | 'Type Script types for Playwright auto generated type safe GQL client generated.', 168 | 'File "codegen.ts" generated.' 169 | ].sort()); 170 | 171 | const codegen = await loadCodegenConfig({ configFilePath: path.join(testDir, 'codegen.ts') }); 172 | 173 | expect(codegen.config.generates).toMatchObject({ 174 | ['gql/first-schema.ts']: { 175 | schema: 'first-schema.gql', 176 | documents: [ 177 | 'custom/**/*.{gql,graphql}', 178 | 'gql/first-schema/autogenerated-operations/**/*.{gql,graphql}' 179 | ], 180 | config: { 181 | rawRequest: false, 182 | scalars: { 183 | BigInt: 'bigint|number', 184 | Date: 'string' 185 | } 186 | } 187 | }, 188 | ['gql/second-schema.ts']: { 189 | schema: 'second-schema.gql', 190 | documents: [ 191 | 'more-custom/**/*.{gql,graphql}', 192 | 'gql/second-schema/autogenerated-operations/**/*.{gql,graphql}' 193 | ], 194 | config: { 195 | rawRequest: false, 196 | scalars: { 197 | BigInt: 'bigint|number', 198 | Date: 'string' 199 | } 200 | } 201 | }, 202 | }); 203 | }); 204 | 205 | test('generates with introspect without custom operations', async () => { 206 | const cliLogs = await execAsync( 207 | `node ${cliPath} -u ${stabServer} -i false`, 208 | { cwd: testDir } 209 | ); 210 | 211 | const dir = await readdir(testDir, { withFileTypes: true }); 212 | expect(dir.map(i => i.name).sort()).toEqual(['schema.gql'].sort()); 213 | 214 | expect(cliLogs).toMatchObject({ 215 | stdout: 'Schema generated from "http://localhost:4000" to "schema.gql".\n' + 216 | 'Client can not be build without any operations, in case of introspect false set path to custom operations: "-o path/to/folder-with-operations"\n', 217 | }); 218 | }); 219 | 220 | test('generates schema and operations from url with headers', async () => { 221 | const cliLogs =await execAsync(`node ${cliPath} -u ${stabServer} -s ${schemaFile} -h "Authorization=Bearer blablaHash256" -h "Cookies={'Authorization': 'Bearer blablaHash'}"`, { 222 | cwd: testDir, 223 | }); 224 | 225 | const dir = await readdir(testDir, { withFileTypes: true }); 226 | expect(dir.map(i => i.name ).sort()).toEqual([schemaFile, 'gql' ].sort()); 227 | expect(lastRequestHeaders).toHaveProperty('authorization', 'Bearer blablaHash256'); 228 | expect(lastRequestHeaders).toHaveProperty('cookies', '{\'Authorization\': \'Bearer blablaHash\'}'); 229 | 230 | expect(cliLogs).toMatchObject({ 231 | stdout: 'Schema generated from "http://localhost:4000" to "test-schema.gql".\n' + 232 | `Operations were generated and saved to "gql/${schemaName}/autogenerated-operations".\n` + 233 | 'Type Script types for Playwright auto generated type safe GQL client generated.\n', 234 | }); 235 | }); 236 | 237 | test('generates types from existing schema and adds coverage logger to client', async () => { 238 | const schemaFile = 'existing-schema.gql'; 239 | const schemaContext = ` 240 | type Query { 241 | hello: String 242 | } 243 | `; 244 | await writeFile(path.join(testDir, schemaFile), schemaContext); 245 | 246 | const cliLogs = await execAsync(`node ${cliPath} -s ${schemaFile} --coverage`, { 247 | cwd: testDir, 248 | }); 249 | 250 | const dir = await readdir(testDir, { withFileTypes: true }); 251 | expect(dir.map(i => i.name ).sort()).toEqual([ schemaFile, 'gql' ].sort()); 252 | 253 | expect(cliLogs).toMatchObject({ 254 | stdout: `Operations were generated and saved to "gql/existing-schema/autogenerated-operations".\n` + 255 | 'Type Script types for Playwright auto generated type safe GQL client generated.\n', 256 | }); 257 | }); 258 | 259 | test('skip execution with message about absent schema', async () => { 260 | const cliLogs = await execAsync(`node ${cliPath} --schema ${schemaFile}`, { 261 | cwd: testDir, 262 | }); 263 | 264 | const dir = await readdir(testDir, { withFileTypes: true }); 265 | expect(dir.map(i => i.name )).toHaveLength(0); 266 | 267 | expect(cliLogs).toMatchObject({ 268 | stdout: 'Schema file: "test-schema.gql" was not found.\n' + 269 | 'Exit with no generated output.\n', 270 | }); 271 | }); 272 | 273 | test('generated type script should contain modification', async () => { 274 | await execAsync( 275 | `node ${cliPath} --url ${stabServer} --schema ${schemaFile} --gqlDir ${gqlDirectory} --gqlFile ${gqlFile}`, 276 | { cwd: testDir } 277 | ); 278 | 279 | const dir = await readdir(testDir, { withFileTypes: true }); 280 | expect(dir.map(i => i.name ).sort()).toEqual([ schemaFile, gqlDirectory ].sort()); 281 | 282 | const generatedFile = await readFile(join(testDir, gqlDirectory, gqlFile), 'utf8'); 283 | expect(generatedFile).toContain(`import { getSdkRequester } from 'playwright-graphql';`); 284 | expect(generatedFile).toContain(`export type APIRequestContext = Parameters[0];`); 285 | expect(generatedFile).toContain(`export type RequesterOptions = Parameters[1] | string;`); 286 | expect(generatedFile).toContain(`export type RequestHandler = Parameters[2];`); 287 | expect(generatedFile).toContain(`export const getClient = (apiContext: APIRequestContext, options?: RequesterOptions, requestHandler?: RequestHandler) => getSdk(getSdkRequester(apiContext, options, requestHandler));`); 288 | expect(generatedFile).toContain(`export type GqlAPI = ReturnType;\n`); 289 | }); 290 | 291 | test('generated type script should contain modification with coverage logger', async () => { 292 | await execAsync( 293 | `node ${cliPath} --url ${stabServer} --schema ${schemaFile} --gqlDir ${gqlDirectory} --gqlFile ${gqlFile} --coverage`, 294 | { cwd: testDir } 295 | ); 296 | 297 | const dir = await readdir(testDir, { withFileTypes: true }); 298 | expect(dir.map(i => i.name ).sort()).toEqual([ schemaFile, gqlDirectory ].sort()); 299 | 300 | const generatedFile = await readFile(join(testDir, gqlDirectory, gqlFile), 'utf8'); 301 | expect(generatedFile).toContain(`import { getSdkRequester, coverageLogger } from 'playwright-graphql';`); 302 | expect(generatedFile).toContain(`export type APIRequestContext = Parameters[0];`); 303 | expect(generatedFile).toContain(`export type RequesterOptions = Parameters[1] | string;`); 304 | expect(generatedFile).toContain(`export type RequestHandler = Parameters[2];`); 305 | expect(generatedFile).toContain(`export const getClient = (apiContext: APIRequestContext, options?: RequesterOptions, requestHandler?: RequestHandler) => coverageLogger(getSdk(getSdkRequester(apiContext, options, requestHandler)), '${join(testDir, `.${gqlFile.replace('.ts', '')}-coverage`)}');`); 306 | expect(generatedFile).toContain(`export type GqlAPI = ReturnType;\n`); 307 | }); 308 | 309 | test('generate type safe client with --saveCodegen', async () => { 310 | const cliLogs = await execAsync( 311 | `node ${cliPath} --url ${stabServer} -d ${gqlDirectory} -f ${gqlFile} --saveCodegen`, 312 | { cwd: testDir } 313 | ); 314 | 315 | const dir = await readdir(testDir, { withFileTypes: true }); 316 | expect(dir.map(i => i.name ).sort()).toEqual([ 'codegen.ts', 'schema.gql', gqlDirectory ].sort()); 317 | 318 | const generatedFile = await readFile(join(testDir, gqlDirectory, gqlFile), 'utf8'); 319 | expect(generatedFile).toContain(`import { getSdkRequester } from 'playwright-graphql';`); 320 | expect(generatedFile).toContain(`export type APIRequestContext = Parameters[0];`); 321 | expect(generatedFile).toContain(`export type RequesterOptions = Parameters[1] | string;`); 322 | expect(generatedFile).toContain(`export type RequestHandler = Parameters[2];`); 323 | expect(generatedFile).toContain(`export const getClient = (apiContext: APIRequestContext, options?: RequesterOptions, requestHandler?: RequestHandler) => getSdk(getSdkRequester(apiContext, options, requestHandler));`); 324 | expect(generatedFile).toContain(`export type GqlAPI = ReturnType;\n`); 325 | 326 | expect(cliLogs).toMatchObject({ 327 | stdout: `Schema generated from "${stabServer}" to "schema.gql".\n` + 328 | `Operations were generated and saved to "${gqlDirectory}/schema/autogenerated-operations".\n` + 329 | `Type Script types for Playwright auto generated type safe GQL client generated.\n` + 330 | `File "codegen.ts" generated.\n`, 331 | }); 332 | 333 | const codegen = await loadCodegenConfig({ configFilePath: path.join(testDir, 'codegen.ts') }); 334 | 335 | expect(codegen.config.generates).toMatchObject({ 336 | [`${gqlDirectory}/${gqlFile}`]: { 337 | schema: 'schema.gql', 338 | documents: [`${gqlDirectory}/schema/autogenerated-operations/**/*.{gql,graphql}`], 339 | config: { rawRequest: false } 340 | }, 341 | }); 342 | }); 343 | 344 | test('generate type safe client with custom codegen (--custom and --codegen)', async () => { 345 | const codegen = 'custom-codegen.ts'; 346 | 347 | await execAsync( 348 | `node ${cliPath} --url ${stabServer} -d ${gqlDirectory} -f ${gqlFile} --saveCodegen -c ${codegen}`, 349 | { cwd: testDir } 350 | ); 351 | 352 | const cliLogs = await execAsync( 353 | `node ${cliPath} --custom -c ${codegen}`, 354 | { cwd: testDir } 355 | ); 356 | 357 | const dir = await readdir(testDir, { withFileTypes: true }); 358 | expect(dir.map(i => i.name ).sort()).toEqual([ codegen, 'schema.gql', gqlDirectory ].sort()); 359 | 360 | const generatedFile = await readFile(join(testDir, gqlDirectory, gqlFile), 'utf8'); 361 | expect(generatedFile).toContain(`import { getSdkRequester } from 'playwright-graphql';`); 362 | expect(generatedFile).toContain(`export type APIRequestContext = Parameters[0];`); 363 | expect(generatedFile).toContain(`export type RequesterOptions = Parameters[1] | string;`); 364 | expect(generatedFile).toContain(`export type RequestHandler = Parameters[2];`); 365 | expect(generatedFile).toContain(`export const getClient = (apiContext: APIRequestContext, options?: RequesterOptions, requestHandler?: RequestHandler) => getSdk(getSdkRequester(apiContext, options, requestHandler));`); 366 | expect(generatedFile).toContain(`export type GqlAPI = ReturnType;\n`); 367 | 368 | expect(cliLogs).toMatchObject({ 369 | stdout: `Type Script types for Playwright auto generated type safe GQL client generated.\n` 370 | }); 371 | }); 372 | 373 | test('throws error when --depthLimit is not number', async () => { 374 | const cliLogs = await execAsync( 375 | `node ${cliPath} --url ${stabServer} --depthLimit .`, 376 | { cwd: testDir } 377 | ); 378 | 379 | expect(cliLogs).toMatchObject({ 380 | stderr: expect.stringContaining(`--depthLimit NaN but should be number`) 381 | }); 382 | }); 383 | }); 384 | -------------------------------------------------------------------------------- /tests/coverage-logger.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { coverageLogger } from '../src'; 3 | 4 | jest.mock('fs/promises'); 5 | 6 | describe('Coverage logger', () => { 7 | const mockObject = { 8 | async testMethod(param1: string, param2: number) { 9 | return `Result: ${param1}, ${param2}`; 10 | }, 11 | }; 12 | 13 | const dirStashPath = 'some-path'; 14 | 15 | beforeEach(() => { 16 | jest.clearAllMocks(); 17 | }); 18 | 19 | test('should call the original method and return its result', async () => { 20 | const proxiedObject = coverageLogger(mockObject, dirStashPath); 21 | 22 | const result = await proxiedObject.testMethod('test', 42); 23 | 24 | expect(result).toBe('Result: test, 42'); 25 | }); 26 | 27 | test('should log coverage to the correct file', async () => { 28 | const proxiedObject = coverageLogger(mockObject, dirStashPath); 29 | 30 | const writeFileMock = jest.spyOn(require('fs/promises'), 'writeFile').mockResolvedValue(undefined); 31 | 32 | await proxiedObject.testMethod('test', 42); 33 | 34 | const expectedLogPath = join(dirStashPath, 'testMethod'); 35 | const expectedLogContent = `${JSON.stringify({ name: 'testMethod', inputParams: ['test', 42] })},`; 36 | 37 | expect(writeFileMock).toHaveBeenCalledWith(expectedLogPath, expectedLogContent, { flag: 'a' }); 38 | }); 39 | 40 | test('should ensure logging happens before returning the result', async () => { 41 | const proxiedObject = coverageLogger(mockObject, dirStashPath); 42 | 43 | const writeFileMock = jest.spyOn(require('fs/promises'), 'writeFile').mockImplementation(async () => { 44 | return new Promise((resolve) => setTimeout(resolve, 100)); 45 | }); 46 | 47 | const resultPromise = proxiedObject.testMethod('test', 42); 48 | 49 | await new Promise((resolve) => setTimeout(resolve, 50)); 50 | expect(writeFileMock).toHaveBeenCalled(); 51 | 52 | const result = await resultPromise; 53 | expect(result).toBe('Result: test, 42'); 54 | }); 55 | 56 | test('should not log coverage for non-function properties', () => { 57 | const objWithProperties = { value: 42 }; 58 | const proxiedObjWithProperties = coverageLogger(objWithProperties, dirStashPath); 59 | 60 | expect(proxiedObjWithProperties.value).toBe(42); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/reporter.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, beforeEach } from '@jest/globals'; 2 | import GraphqlCoverageReport from '../src/coverage-reporter/report'; 3 | import { resolve, join } from 'path'; 4 | 5 | describe('Graphql Coverage Report', () => { 6 | 7 | const expectedCoverageDir = '.raw-graphql-coverage'; 8 | 9 | beforeEach(() => { 10 | jest.restoreAllMocks(); 11 | }) 12 | 13 | test('should initialize with all options', async () => { 14 | const options = { 15 | graphqlFilePath: './tests/resources/graphql.ts', 16 | coverageFilePath: './mock-coverage.log', 17 | logUncoveredOperations: true, 18 | minCoveragePerOperation: 80, 19 | saveGqlCoverageLog: true, 20 | }; 21 | 22 | const reporter = new GraphqlCoverageReport(options); 23 | 24 | expect(reporter).toBeDefined(); 25 | expect(reporter['operationsSchema']).toEqual([ 26 | { inputParams: [{ called: 0, key: "id", type: "string" }], name: "group" }, 27 | { inputParams: [], name: "groups" }, 28 | { inputParams: [{ called: 0, key: "id", type: "string" }], name: "user" }, 29 | { inputParams: [], name: "users" }] 30 | ); 31 | expect(reporter['coverageFilePath']).toEqual(options.coverageFilePath); 32 | expect(reporter['logUncoveredOperations']).toEqual(options.logUncoveredOperations); 33 | expect(reporter['minCoveragePerOperation']).toEqual(options.minCoveragePerOperation); 34 | expect(reporter['saveGqlCoverageLog']).toEqual(options.saveGqlCoverageLog); 35 | }); 36 | 37 | test('should parse enums as const and enums equally', async () => { 38 | const options1 = { 39 | graphqlFilePath: './tests/resources/clientWithEnum.ts', 40 | coverageFilePath: './mock-coverage.log', 41 | logUncoveredOperations: true, 42 | minCoveragePerOperation: 80, 43 | saveGqlCoverageLog: true, 44 | }; 45 | 46 | const options2 = { 47 | graphqlFilePath: './tests/resources/clientWithEnumsAsConst.ts', 48 | coverageFilePath: './mock-coverage.log', 49 | logUncoveredOperations: true, 50 | minCoveragePerOperation: 80, 51 | saveGqlCoverageLog: true, 52 | }; 53 | 54 | const reporter1 = new GraphqlCoverageReport(options1); 55 | const reporter2 = new GraphqlCoverageReport(options2); 56 | 57 | expect(reporter2['operationsSchema']).toEqual(reporter1['operationsSchema']); 58 | }); 59 | 60 | test('should initialize with required options', async () => { 61 | const options = { 62 | graphqlFilePath: './tests/resources/raw-graphql.ts', 63 | }; 64 | 65 | const reporter = new GraphqlCoverageReport(options); 66 | 67 | expect(reporter).toBeDefined(); 68 | expect(reporter['operationsSchema']).toEqual([ 69 | { inputParams: [{ called: 0, key: "id", type: "string" }], name: "group" }, 70 | { inputParams: [], name: "groups" }, 71 | { inputParams: [{ called: 0, key: "id", type: "string" }], name: "user" }, 72 | { inputParams: [], name: "users" }] 73 | ); 74 | expect(reporter['coverageFilePath']).toEqual('./raw-graphql-coverage.log'); 75 | expect(reporter['logUncoveredOperations']).toEqual(false); 76 | expect(reporter['minCoveragePerOperation']).toEqual(100); 77 | expect(reporter['saveGqlCoverageLog']).toEqual(false); 78 | }); 79 | 80 | test('should throw error if GraphQL file does not exist', () => { 81 | const options = { 82 | graphqlFilePath: './none-graphql.ts', 83 | }; 84 | 85 | expect(() => new GraphqlCoverageReport(options)).toThrowError( 86 | `Source file '${resolve(options.graphqlFilePath)}' does not exist.` 87 | ); 88 | }); 89 | 90 | test('onBegin should create and remove coverage directory', async () => { 91 | const options = { 92 | graphqlFilePath: './tests/resources/raw-graphql.ts', 93 | coverageFilePath: './mock-coverage.log', 94 | }; 95 | 96 | const reporter = new GraphqlCoverageReport(options); 97 | 98 | const accessMock = jest.spyOn(require('fs/promises'), 'access').mockResolvedValue(Promise); 99 | const rmMock = jest.spyOn(require('fs/promises'), 'rm').mockResolvedValue(Promise); 100 | const mkdirMock = jest.spyOn(require('fs/promises'), 'mkdir').mockResolvedValue(Promise); 101 | 102 | await reporter.onBegin(); 103 | 104 | expect(accessMock).toHaveBeenCalledWith(join(process.cwd(), expectedCoverageDir)); 105 | expect(rmMock).toHaveBeenCalledWith(join(process.cwd(), expectedCoverageDir), { recursive: true }); 106 | expect(mkdirMock).toHaveBeenCalledWith(join(process.cwd(), expectedCoverageDir)); 107 | }); 108 | 109 | test('onBegin should create coverage directory', async () => { 110 | const options = { 111 | graphqlFilePath: './tests/resources/raw-graphql.ts', 112 | coverageFilePath: './mock-coverage.log', 113 | }; 114 | 115 | const reporter = new GraphqlCoverageReport(options); 116 | 117 | const accessMock = jest.spyOn(require('fs/promises'), 'access') 118 | .mockRejectedValue(new Error('ENOENT: no such file or directory')); 119 | const rmMock = jest.spyOn(require('fs/promises'), 'rm') 120 | .mockResolvedValue(Promise); 121 | const mkdirMock = jest.spyOn(require('fs/promises'), 'mkdir') 122 | .mockResolvedValue(Promise); 123 | 124 | await reporter.onBegin(); 125 | 126 | expect(accessMock).toHaveBeenCalledWith(join(process.cwd(), expectedCoverageDir)); 127 | expect(rmMock).not.toHaveBeenCalledWith(resolve(expectedCoverageDir), { recursive: true }); 128 | expect(mkdirMock).toHaveBeenCalledWith(join(process.cwd(), expectedCoverageDir)); 129 | }); 130 | 131 | test('onEnd should throw error if no coverage dir found', async () => { 132 | const options = { 133 | graphqlFilePath: './tests/resources/raw-graphql.ts', 134 | coverageFilePath: './mock-coverage.log', 135 | }; 136 | 137 | const reporter = new GraphqlCoverageReport(options); 138 | 139 | jest.spyOn(require('fs/promises'), 'access') 140 | .mockRejectedValue(new Error('ENOENT: no such file or directory')); 141 | 142 | await expect(reporter.onEnd()).rejects.toThrowError(`Directory with logged coverage was not found: ${join(process.cwd(), expectedCoverageDir)}`); 143 | }); 144 | 145 | test('onEnd should calculate coverage and write logs', async () => { 146 | const options = { 147 | graphqlFilePath: './tests/resources/raw-graphql.ts', 148 | coverageFilePath: './test-coverage.log', 149 | saveGqlCoverageLog: true, 150 | }; 151 | 152 | const rmMock = jest.spyOn(require('fs/promises'), 'rm') 153 | .mockResolvedValue(undefined); 154 | const readFileMock = jest.spyOn(require('fs/promises'), 'readFile'); 155 | const writeFileMock = jest.spyOn(require('fs/promises'), 'writeFile') 156 | .mockResolvedValue(Promise); 157 | 158 | const stabCoverageDir = './tests/resources/coverageDir'; 159 | 160 | const reporter = new GraphqlCoverageReport(options); 161 | 162 | // @ts-ignore 163 | reporter['coverageDir'] = stabCoverageDir; 164 | 165 | await reporter.onEnd(); 166 | 167 | expect(readFileMock).toBeCalledTimes(2); 168 | expect(writeFileMock).toBeCalledTimes(5); 169 | expect(rmMock).toHaveBeenCalledWith(stabCoverageDir, { recursive: true }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /tests/requester.test.ts: -------------------------------------------------------------------------------- 1 | import { APIRequestContext, APIResponse } from '@playwright/test'; 2 | import { DocumentNode, Kind, OperationTypeNode } from 'graphql'; 3 | import { getSdkRequester } from '../src'; 4 | 5 | describe('getSdkRequester', () => { 6 | test('should send a GraphQL query and return data', async () => { 7 | const mockResponse = { 8 | json: jest.fn().mockResolvedValue({ data: { user: { id: 1, name: 'Test User' } } }), 9 | text: jest.fn(), 10 | status: jest.fn(), 11 | headers: jest.fn(), 12 | } as unknown as jest.Mocked; 13 | 14 | const mockClient = { 15 | post: jest.fn().mockResolvedValue(mockResponse), 16 | } as unknown as jest.Mocked; 17 | 18 | const gqlEndpoint = '/api/graphql'; 19 | const requester = getSdkRequester(mockClient, { gqlEndpoint }); 20 | const mockDoc: DocumentNode = { 21 | kind: Kind.DOCUMENT, 22 | definitions: [ 23 | { 24 | kind: Kind.OPERATION_DEFINITION, 25 | operation: OperationTypeNode.QUERY, 26 | selectionSet: { 27 | kind: Kind.SELECTION_SET, 28 | selections: [] 29 | } 30 | }, 31 | ], 32 | }; 33 | const variables = { id: 1 }; 34 | 35 | const result = await requester(mockDoc, variables); 36 | 37 | expect(mockClient.post).toHaveBeenCalledWith(gqlEndpoint, { 38 | data: { variables, query: expect.any(String) }, 39 | }); 40 | expect(result).toEqual({ user: { id: 1, name: 'Test User' } }); 41 | }); 42 | 43 | test('should throw an error if the document is invalid', async () => { 44 | const mockClient = { 45 | post: jest.fn(), 46 | } as unknown as jest.Mocked; 47 | 48 | const requester = getSdkRequester(mockClient); 49 | const invalidDoc: DocumentNode = { 50 | kind: Kind.DOCUMENT, 51 | definitions: [], 52 | }; 53 | 54 | await expect(requester(invalidDoc)).rejects.toThrow( 55 | 'DocumentNode passed to Playwright Client must contain single query or mutation' 56 | ); 57 | }); 58 | 59 | test('should throw an error if no data is returned in the response', async () => { 60 | const mockResponse = { 61 | json: jest.fn().mockResolvedValue({}), 62 | text: jest.fn().mockResolvedValue('{}'), 63 | status: jest.fn(), 64 | headers: jest.fn(), 65 | } as unknown as jest.Mocked; 66 | 67 | const mockClient = { 68 | post: jest.fn().mockResolvedValue(mockResponse), 69 | } as unknown as jest.Mocked; 70 | 71 | const requester = getSdkRequester(mockClient); 72 | const mockDoc: DocumentNode = { 73 | kind: Kind.DOCUMENT, 74 | definitions: [ 75 | { 76 | kind: Kind.OPERATION_DEFINITION, 77 | operation: OperationTypeNode.QUERY, 78 | selectionSet: { 79 | kind: Kind.SELECTION_SET, 80 | selections: [] 81 | } 82 | }, 83 | ], 84 | }; 85 | 86 | await expect(requester(mockDoc)).rejects.toThrow( 87 | /No data presented in the GraphQL response/ 88 | ); 89 | }); 90 | 91 | test('should handle raw responses if rawResponse option is true', async () => { 92 | const mockResponse = { 93 | json: jest.fn().mockResolvedValue({ rawDataKey: 'rawDataValue' }), 94 | text: jest.fn(), 95 | status: jest.fn(), 96 | headers: jest.fn(), 97 | } as unknown as jest.Mocked; 98 | 99 | const mockClient = { 100 | post: jest.fn().mockResolvedValue(mockResponse), 101 | } as unknown as jest.Mocked; 102 | 103 | const gqlEndpoint = '/api/graphql'; 104 | const requester = getSdkRequester(mockClient, { gqlEndpoint, rawResponse: true }); 105 | const mockDoc: DocumentNode = { 106 | kind: Kind.DOCUMENT, 107 | definitions: [ 108 | { 109 | kind: Kind.OPERATION_DEFINITION, 110 | operation: OperationTypeNode.QUERY, 111 | selectionSet: { 112 | kind: Kind.SELECTION_SET, 113 | selections: [] 114 | } 115 | }, 116 | ], 117 | }; 118 | const variables = { id: 1 }; 119 | 120 | const result = await requester(mockDoc, variables); 121 | 122 | expect(mockClient.post).toHaveBeenCalledWith(gqlEndpoint, { 123 | data: { variables, query: expect.any(String) }, 124 | }); 125 | expect(result).toEqual({ rawDataKey: 'rawDataValue' }); 126 | }); 127 | 128 | test('should throw an error for unsupported subscription operations', async () => { 129 | const mockClient = { 130 | post: jest.fn(), 131 | } as unknown as jest.Mocked; 132 | 133 | const requester = getSdkRequester(mockClient); 134 | const subscriptionDoc: DocumentNode = { 135 | kind: Kind.DOCUMENT, 136 | definitions: [ 137 | { 138 | kind: Kind.OPERATION_DEFINITION, 139 | operation: OperationTypeNode.SUBSCRIPTION, 140 | selectionSet: { 141 | kind: Kind.SELECTION_SET, 142 | selections: [] 143 | } 144 | }, 145 | ], 146 | }; 147 | 148 | await expect(requester(subscriptionDoc)).rejects.toThrow( 149 | 'Subscription requests through SDK interface are not supported' 150 | ); 151 | }); 152 | 153 | test('should handle errors in request handler', async () => { 154 | const mockResponse = { 155 | json: jest.fn().mockResolvedValue({ data: null, errors: [{ message: 'API Error' }] }), 156 | status: jest.fn().mockReturnValue(400), 157 | } as unknown as jest.Mocked; 158 | 159 | const mockClient = { 160 | post: jest.fn().mockResolvedValue(mockResponse), 161 | } as unknown as jest.Mocked; 162 | 163 | const requestHandlerCallback = async (request: () => Promise) => { 164 | const res = await request(); 165 | if (res.status() >= 400) { 166 | throw new Error(`API call failed with status ${res.status()}`); 167 | } 168 | return res; 169 | }; 170 | 171 | const requester = getSdkRequester(mockClient, { gqlEndpoint: '/graphql' }, requestHandlerCallback); 172 | 173 | const mockDoc: DocumentNode = { 174 | kind: Kind.DOCUMENT, 175 | definitions: [{ 176 | kind: Kind.OPERATION_DEFINITION, 177 | operation: OperationTypeNode.QUERY, 178 | selectionSet: { 179 | kind: Kind.SELECTION_SET, 180 | selections: [] 181 | } 182 | }] 183 | }; 184 | 185 | await expect(requester(mockDoc)).rejects.toThrow('API call failed with status 400'); 186 | }); 187 | 188 | test('should allow request modification in handler', async () => { 189 | const mockResponse = { 190 | json: jest.fn().mockResolvedValue({ data: { user: { id: 1 } } }), 191 | status: jest.fn().mockReturnValue(200), 192 | } as unknown as jest.Mocked; 193 | 194 | const mockClient = { 195 | post: jest.fn().mockResolvedValue(mockResponse), 196 | } as unknown as jest.Mocked; 197 | 198 | let requestCount = 0; 199 | const requestHandlerCallback = async (request: () => Promise) => { 200 | requestCount++; 201 | if (requestCount === 1) { 202 | // Simulate retry logic 203 | await request(); // First attempt 204 | return request(); // Retry 205 | } 206 | return request(); 207 | }; 208 | 209 | const requester = getSdkRequester(mockClient, { gqlEndpoint: '/graphql' }, requestHandlerCallback); 210 | 211 | const mockDoc: DocumentNode = { 212 | kind: Kind.DOCUMENT, 213 | definitions: [{ 214 | kind: Kind.OPERATION_DEFINITION, 215 | operation: OperationTypeNode.QUERY, 216 | selectionSet: { 217 | kind: Kind.SELECTION_SET, 218 | selections: [] 219 | } 220 | }] 221 | }; 222 | 223 | await requester(mockDoc); 224 | expect(mockClient.post).toHaveBeenCalledTimes(2); // Verify retry happened 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /tests/resources/clientWithEnum.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from 'graphql'; 2 | import gql from 'graphql-tag'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | export type MakeEmpty = { [_ in K]?: never }; 9 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 10 | 11 | // Enum з трьома значеннями для ролі користувача 12 | export enum Role { 13 | ADMIN = 'ADMIN', 14 | USER = 'USER', 15 | GUEST = 'GUEST' 16 | } 17 | 18 | /** All built-in and custom scalars, mapped to their actual values */ 19 | export type Scalars = { 20 | ID: { input: string; output: string; } 21 | String: { input: string; output: string; } 22 | Boolean: { input: boolean; output: boolean; } 23 | Int: { input: number; output: number; } 24 | Float: { input: number; output: number; } 25 | }; 26 | 27 | export type Group = { 28 | __typename?: 'Group'; 29 | _id?: Maybe; 30 | name?: Maybe; 31 | }; 32 | 33 | export type Query = { 34 | __typename?: 'Query'; 35 | group?: Maybe; 36 | groups?: Maybe>>; 37 | user?: Maybe; 38 | users?: Maybe>>; 39 | }; 40 | 41 | 42 | export type QueryGroupArgs = { 43 | id: Scalars['String']['input']; 44 | }; 45 | 46 | 47 | export type QueryUserArgs = { 48 | id: Scalars['String']['input']; 49 | role?: InputMaybe; 50 | }; 51 | 52 | 53 | export type QueryUsersArgs = { 54 | role?: InputMaybe; 55 | }; 56 | 57 | export type User = { 58 | __typename?: 'User'; 59 | _id?: Maybe; 60 | group?: Maybe; 61 | username?: Maybe; 62 | role?: Maybe; 63 | }; 64 | 65 | export type GroupQueryVariables = Exact<{ 66 | id: Scalars['String']['input']; 67 | }>; 68 | 69 | 70 | export type GroupQuery = { __typename?: 'Query', group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null }; 71 | 72 | export type GroupsQueryVariables = Exact<{ [key: string]: never; }>; 73 | 74 | 75 | export type GroupsQuery = { __typename?: 'Query', groups?: Array<{ __typename?: 'Group', _id?: string | null, name?: string | null } | null> | null }; 76 | 77 | export type UserQueryVariables = Exact<{ 78 | id: Scalars['String']['input']; 79 | role?: InputMaybe; 80 | }>; 81 | 82 | 83 | export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', _id?: string | null, username?: string | null, role?: Role | null, group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null } | null }; 84 | 85 | export type UsersQueryVariables = Exact<{ 86 | role?: InputMaybe; 87 | }>; 88 | 89 | 90 | export type UsersQuery = { __typename?: 'Query', users?: Array<{ __typename?: 'User', _id?: string | null, username?: string | null, role?: Role | null, group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null } | null> | null }; 91 | 92 | 93 | export const GroupDocument = gql` 94 | query group($id: String!) { 95 | group(id: $id) { 96 | _id 97 | name 98 | } 99 | } 100 | `; 101 | export const GroupsDocument = gql` 102 | query groups { 103 | groups { 104 | _id 105 | name 106 | } 107 | } 108 | `; 109 | export const UserDocument = gql` 110 | query user($id: String!, $role: Role) { 111 | user(id: $id, role: $role) { 112 | _id 113 | username 114 | role 115 | group { 116 | _id 117 | name 118 | } 119 | } 120 | } 121 | `; 122 | export const UsersDocument = gql` 123 | query users($role: Role) { 124 | users(role: $role) { 125 | _id 126 | username 127 | role 128 | group { 129 | _id 130 | name 131 | } 132 | } 133 | } 134 | `; 135 | export type Requester = (doc: DocumentNode, vars?: V, options?: C) => Promise | AsyncIterable 136 | export function getSdk(requester: Requester) { 137 | return { 138 | group(variables: GroupQueryVariables, options?: C): Promise { 139 | return requester(GroupDocument, variables, options) as Promise; 140 | }, 141 | groups(variables?: GroupsQueryVariables, options?: C): Promise { 142 | return requester(GroupsDocument, variables, options) as Promise; 143 | }, 144 | user(variables: UserQueryVariables, options?: C): Promise { 145 | return requester(UserDocument, variables, options) as Promise; 146 | }, 147 | users(variables?: UsersQueryVariables, options?: C): Promise { 148 | return requester(UsersDocument, variables, options) as Promise; 149 | } 150 | }; 151 | } 152 | export type Sdk = ReturnType 153 | -------------------------------------------------------------------------------- /tests/resources/clientWithEnumsAsConst.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from 'graphql'; 2 | import gql from 'graphql-tag'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | export type MakeEmpty = { [_ in K]?: never }; 9 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 10 | 11 | // Об'єкт із значеннями ролей користувача як const 12 | export const Role = { 13 | ADMIN: 'ADMIN', 14 | USER: 'USER', 15 | GUEST: 'GUEST' 16 | } as const; 17 | 18 | // Тип, який походить від ключів об'єкта Role 19 | export type Role = typeof Role[keyof typeof Role]; 20 | 21 | /** All built-in and custom scalars, mapped to their actual values */ 22 | export type Scalars = { 23 | ID: { input: string; output: string; } 24 | String: { input: string; output: string; } 25 | Boolean: { input: boolean; output: boolean; } 26 | Int: { input: number; output: number; } 27 | Float: { input: number; output: number; } 28 | }; 29 | 30 | export type Group = { 31 | __typename?: 'Group'; 32 | _id?: Maybe; 33 | name?: Maybe; 34 | }; 35 | 36 | export type Query = { 37 | __typename?: 'Query'; 38 | group?: Maybe; 39 | groups?: Maybe>>; 40 | user?: Maybe; 41 | users?: Maybe>>; 42 | }; 43 | 44 | 45 | export type QueryGroupArgs = { 46 | id: Scalars['String']['input']; 47 | }; 48 | 49 | 50 | export type QueryUserArgs = { 51 | id: Scalars['String']['input']; 52 | role?: InputMaybe; 53 | }; 54 | 55 | 56 | export type QueryUsersArgs = { 57 | role?: InputMaybe; 58 | }; 59 | 60 | export type User = { 61 | __typename?: 'User'; 62 | _id?: Maybe; 63 | group?: Maybe; 64 | username?: Maybe; 65 | role?: Maybe; 66 | }; 67 | 68 | export type GroupQueryVariables = Exact<{ 69 | id: Scalars['String']['input']; 70 | }>; 71 | 72 | 73 | export type GroupQuery = { __typename?: 'Query', group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null }; 74 | 75 | export type GroupsQueryVariables = Exact<{ [key: string]: never; }>; 76 | 77 | 78 | export type GroupsQuery = { __typename?: 'Query', groups?: Array<{ __typename?: 'Group', _id?: string | null, name?: string | null } | null> | null }; 79 | 80 | export type UserQueryVariables = Exact<{ 81 | id: Scalars['String']['input']; 82 | role?: InputMaybe; 83 | }>; 84 | 85 | 86 | export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', _id?: string | null, username?: string | null, role?: Role | null, group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null } | null }; 87 | 88 | export type UsersQueryVariables = Exact<{ 89 | role?: InputMaybe; 90 | }>; 91 | 92 | 93 | export type UsersQuery = { __typename?: 'Query', users?: Array<{ __typename?: 'User', _id?: string | null, username?: string | null, role?: Role | null, group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null } | null> | null }; 94 | 95 | 96 | export const GroupDocument = gql` 97 | query group($id: String!) { 98 | group(id: $id) { 99 | _id 100 | name 101 | } 102 | } 103 | `; 104 | export const GroupsDocument = gql` 105 | query groups { 106 | groups { 107 | _id 108 | name 109 | } 110 | } 111 | `; 112 | export const UserDocument = gql` 113 | query user($id: String!, $role: String) { 114 | user(id: $id, role: $role) { 115 | _id 116 | username 117 | role 118 | group { 119 | _id 120 | name 121 | } 122 | } 123 | } 124 | `; 125 | export const UsersDocument = gql` 126 | query users($role: String) { 127 | users(role: $role) { 128 | _id 129 | username 130 | role 131 | group { 132 | _id 133 | name 134 | } 135 | } 136 | } 137 | `; 138 | export type Requester = (doc: DocumentNode, vars?: V, options?: C) => Promise | AsyncIterable 139 | export function getSdk(requester: Requester) { 140 | return { 141 | group(variables: GroupQueryVariables, options?: C): Promise { 142 | return requester(GroupDocument, variables, options) as Promise; 143 | }, 144 | groups(variables?: GroupsQueryVariables, options?: C): Promise { 145 | return requester(GroupsDocument, variables, options) as Promise; 146 | }, 147 | user(variables: UserQueryVariables, options?: C): Promise { 148 | return requester(UserDocument, variables, options) as Promise; 149 | }, 150 | users(variables?: UsersQueryVariables, options?: C): Promise { 151 | return requester(UsersDocument, variables, options) as Promise; 152 | } 153 | }; 154 | } 155 | export type Sdk = ReturnType 156 | -------------------------------------------------------------------------------- /tests/resources/coverageDir/group: -------------------------------------------------------------------------------- 1 | { "name": "group", "inputParams": [ { "id": 1 } ] }, -------------------------------------------------------------------------------- /tests/resources/coverageDir/groups: -------------------------------------------------------------------------------- 1 | { "name": "groups", "inputParams": [{}] }, -------------------------------------------------------------------------------- /tests/resources/gql-fake-server.ts: -------------------------------------------------------------------------------- 1 | import { createServer, Server, IncomingHttpHeaders } from 'http'; 2 | import { graphql, buildSchema } from 'graphql'; 3 | 4 | const schema = buildSchema(` 5 | type Query { 6 | hello: String 7 | } 8 | `); 9 | 10 | const rootValue = { 11 | hello: () => 'Hello from the fake GraphQL server!', 12 | }; 13 | 14 | let server: Server | null = null; 15 | export let lastRequestHeaders: IncomingHttpHeaders | undefined; 16 | 17 | export async function startFakeGraphQLServer(port: number = 4000): Promise { 18 | 19 | return new Promise((resolve, reject) => { 20 | server = createServer((req, res) => { 21 | lastRequestHeaders = req.headers; 22 | 23 | if (req.method === 'POST') { 24 | let body = ''; 25 | req.on('data', (chunk) => { 26 | body += chunk; 27 | }); 28 | 29 | req.on('end', async () => { 30 | try { 31 | const { query } = JSON.parse(body); 32 | 33 | const result = await graphql({ schema, source: query, rootValue }); 34 | 35 | res.writeHead(200, { 'Content-Type': 'application/json' }); 36 | res.end(JSON.stringify(result)); 37 | } catch (error) { 38 | 39 | res.writeHead(500); 40 | res.end(JSON.stringify({ error: (error as Error).message })); 41 | } 42 | }); 43 | } else { 44 | res.writeHead(404); 45 | res.end(); 46 | } 47 | }); 48 | 49 | server.listen(port, () => { 50 | resolve(); 51 | }); 52 | 53 | server.on('error', (err) => { 54 | console.error('Error starting server:', err); 55 | reject(err); 56 | }); 57 | }); 58 | } 59 | 60 | 61 | export function stopFakeGraphQLServer(): Promise { 62 | return new Promise((resolve, reject) => { 63 | if (!server) { 64 | return resolve(); 65 | } 66 | server.close((err) => { 67 | if (err) { 68 | return reject(err); 69 | } 70 | resolve(); 71 | }); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /tests/resources/graphql.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode } from 'graphql'; 2 | import gql from 'graphql-tag'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | export type MakeEmpty = { [_ in K]?: never }; 9 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 10 | /** All built-in and custom scalars, mapped to their actual values */ 11 | export type Scalars = { 12 | ID: { input: string; output: string; } 13 | String: { input: string; output: string; } 14 | Boolean: { input: boolean; output: boolean; } 15 | Int: { input: number; output: number; } 16 | Float: { input: number; output: number; } 17 | }; 18 | 19 | export type Group = { 20 | __typename?: 'Group'; 21 | _id?: Maybe; 22 | name?: Maybe; 23 | }; 24 | 25 | export type Query = { 26 | __typename?: 'Query'; 27 | group?: Maybe; 28 | groups?: Maybe>>; 29 | user?: Maybe; 30 | users?: Maybe>>; 31 | }; 32 | 33 | 34 | export type QueryGroupArgs = { 35 | id: Scalars['String']['input']; 36 | }; 37 | 38 | 39 | export type QueryUserArgs = { 40 | id: Scalars['String']['input']; 41 | }; 42 | 43 | export type User = { 44 | __typename?: 'User'; 45 | _id?: Maybe; 46 | group?: Maybe; 47 | username?: Maybe; 48 | }; 49 | 50 | export type GroupQueryVariables = Exact<{ 51 | id: Scalars['String']['input']; 52 | }>; 53 | 54 | 55 | export type GroupQuery = { __typename?: 'Query', group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null }; 56 | 57 | export type GroupsQueryVariables = Exact<{ [key: string]: never; }>; 58 | 59 | 60 | export type GroupsQuery = { __typename?: 'Query', groups?: Array<{ __typename?: 'Group', _id?: string | null, name?: string | null } | null> | null }; 61 | 62 | export type UserQueryVariables = Exact<{ 63 | id: Scalars['String']['input']; 64 | }>; 65 | 66 | 67 | export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', _id?: string | null, username?: string | null, group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null } | null }; 68 | 69 | export type UsersQueryVariables = Exact<{ [key: string]: never; }>; 70 | 71 | 72 | export type UsersQuery = { __typename?: 'Query', users?: Array<{ __typename?: 'User', _id?: string | null, username?: string | null, group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null } | null> | null }; 73 | 74 | 75 | export const GroupDocument = gql` 76 | query group($id: String!) { 77 | group(id: $id) { 78 | _id 79 | name 80 | } 81 | } 82 | `; 83 | export const GroupsDocument = gql` 84 | query groups { 85 | groups { 86 | _id 87 | name 88 | } 89 | } 90 | `; 91 | export const UserDocument = gql` 92 | query user($id: String!) { 93 | user(id: $id) { 94 | _id 95 | username 96 | group { 97 | _id 98 | name 99 | } 100 | } 101 | } 102 | `; 103 | export const UsersDocument = gql` 104 | query users { 105 | users { 106 | _id 107 | username 108 | group { 109 | _id 110 | name 111 | } 112 | } 113 | } 114 | `; 115 | export type Requester = (doc: DocumentNode, vars?: V, options?: C) => Promise | AsyncIterable 116 | export function getSdk(requester: Requester) { 117 | return { 118 | group(variables: GroupQueryVariables, options?: C): Promise { 119 | return requester(GroupDocument, variables, options) as Promise; 120 | }, 121 | groups(variables?: GroupsQueryVariables, options?: C): Promise { 122 | return requester(GroupsDocument, variables, options) as Promise; 123 | }, 124 | user(variables: UserQueryVariables, options?: C): Promise { 125 | return requester(UserDocument, variables, options) as Promise; 126 | }, 127 | users(variables?: UsersQueryVariables, options?: C): Promise { 128 | return requester(UsersDocument, variables, options) as Promise; 129 | } 130 | }; 131 | } 132 | export type Sdk = ReturnType; -------------------------------------------------------------------------------- /tests/resources/raw-graphql.ts: -------------------------------------------------------------------------------- 1 | import { DocumentNode, ExecutionResult } from 'graphql'; 2 | import gql from 'graphql-tag'; 3 | export type Maybe = T | null; 4 | export type InputMaybe = Maybe; 5 | export type Exact = { [K in keyof T]: T[K] }; 6 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; 7 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; 8 | export type MakeEmpty = { [_ in K]?: never }; 9 | export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; 10 | /** All built-in and custom scalars, mapped to their actual values */ 11 | export type Scalars = { 12 | ID: { input: string; output: string; } 13 | String: { input: string; output: string; } 14 | Boolean: { input: boolean; output: boolean; } 15 | Int: { input: number; output: number; } 16 | Float: { input: number; output: number; } 17 | }; 18 | 19 | export type Group = { 20 | __typename?: 'Group'; 21 | _id?: Maybe; 22 | name?: Maybe; 23 | }; 24 | 25 | export type Query = { 26 | __typename?: 'Query'; 27 | group?: Maybe; 28 | groups?: Maybe>>; 29 | user?: Maybe; 30 | users?: Maybe>>; 31 | }; 32 | 33 | 34 | export type QueryGroupArgs = { 35 | id: Scalars['String']['input']; 36 | }; 37 | 38 | 39 | export type QueryUserArgs = { 40 | id: Scalars['String']['input']; 41 | }; 42 | 43 | export type User = { 44 | __typename?: 'User'; 45 | _id?: Maybe; 46 | group?: Maybe; 47 | username?: Maybe; 48 | }; 49 | 50 | export type GroupQueryVariables = Exact<{ 51 | id: Scalars['String']['input']; 52 | }>; 53 | 54 | 55 | export type GroupQuery = { __typename?: 'Query', group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null }; 56 | 57 | export type GroupsQueryVariables = Exact<{ [key: string]: never; }>; 58 | 59 | 60 | export type GroupsQuery = { __typename?: 'Query', groups?: Array<{ __typename?: 'Group', _id?: string | null, name?: string | null } | null> | null }; 61 | 62 | export type UserQueryVariables = Exact<{ 63 | id: Scalars['String']['input']; 64 | }>; 65 | 66 | 67 | export type UserQuery = { __typename?: 'Query', user?: { __typename?: 'User', _id?: string | null, username?: string | null, group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null } | null }; 68 | 69 | export type UsersQueryVariables = Exact<{ [key: string]: never; }>; 70 | 71 | 72 | export type UsersQuery = { __typename?: 'Query', users?: Array<{ __typename?: 'User', _id?: string | null, username?: string | null, group?: { __typename?: 'Group', _id?: string | null, name?: string | null } | null } | null> | null }; 73 | 74 | 75 | export const GroupDocument = gql` 76 | query group($id: String!) { 77 | group(id: $id) { 78 | _id 79 | name 80 | } 81 | } 82 | `; 83 | export const GroupsDocument = gql` 84 | query groups { 85 | groups { 86 | _id 87 | name 88 | } 89 | } 90 | `; 91 | export const UserDocument = gql` 92 | query user($id: String!) { 93 | user(id: $id) { 94 | _id 95 | username 96 | group { 97 | _id 98 | name 99 | } 100 | } 101 | } 102 | `; 103 | export const UsersDocument = gql` 104 | query users { 105 | users { 106 | _id 107 | username 108 | group { 109 | _id 110 | name 111 | } 112 | } 113 | } 114 | `; 115 | export type Requester = (doc: DocumentNode, vars?: V, options?: C) => Promise> | AsyncIterable> 116 | export function getSdk(requester: Requester) { 117 | return { 118 | group(variables: GroupQueryVariables, options?: C): Promise> { 119 | return requester(GroupDocument, variables, options) as Promise>; 120 | }, 121 | groups(variables?: GroupsQueryVariables, options?: C): Promise> { 122 | return requester(GroupsDocument, variables, options) as Promise>; 123 | }, 124 | user(variables: UserQueryVariables, options?: C): Promise> { 125 | return requester(UserDocument, variables, options) as Promise>; 126 | }, 127 | users(variables?: UsersQueryVariables, options?: C): Promise> { 128 | return requester(UsersDocument, variables, options) as Promise>; 129 | } 130 | }; 131 | } 132 | export type Sdk = ReturnType; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "Node16", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./lib", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node16", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | }, 72 | "include": [ 73 | "src/*", 74 | "src/coverage-reporter/*", 75 | "src/codegen-cli/*" 76 | ] 77 | } 78 | --------------------------------------------------------------------------------