├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── babel.config.js
├── jest.config.js
├── package.json
├── src
├── index.ts
└── visitor.ts
├── tests
├── documents
│ ├── GetRepository.graphql
│ └── RepositoryFragment.graphql
├── globalSetup.ts
├── integration.test.ts
├── setup.ts
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/generated
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | 'jest': true,
5 | },
6 | parser: '@typescript-eslint/parser',
7 | plugins: ['@typescript-eslint'],
8 | extends: [
9 | 'eslint:recommended',
10 | 'plugin:@typescript-eslint/eslint-recommended',
11 | 'plugin:@typescript-eslint/recommended',
12 | ],
13 | rules: {
14 | '@typescript-eslint/no-explicit-any': 'off',
15 | '@typescript-eslint/explicit-module-boundary-types': 'off',
16 | },
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 |
16 | - name: Setup Node
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: '15'
20 |
21 | - uses: actions/cache@v2
22 | with:
23 | path: '**/node_modules'
24 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
25 |
26 | - name: Install Yarn
27 | run: npm install -g yarn
28 |
29 | - name: Install node dependencies
30 | run: yarn install
31 |
32 | - name: Lint
33 | run: yarn lint
34 |
35 | - name: Type Check
36 | run: yarn typecheck
37 |
38 | - name: Test
39 | run: yarn test --collectCoverage
40 |
41 | - name: Upload Code Coverage
42 | run: bash <(curl -s https://codecov.io/bash)
43 | env:
44 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | generated/
4 | tests/schema/
5 | yarn-error.log
6 | coverage/
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | tests
2 | babel.config.js
3 | jest.config.js
4 | tsconfig.json
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Suyeol Jeon (https://github.com/devxoul)
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 | # graphql-codegen-typescript-fixtures
2 |
3 | [](https://badge.fury.io/js/graphql-codegen-typescript-fixtures)
4 | [](https://github.com/devxoul/graphql-codegen-typescript-fixtures/actions/workflows/ci.yml)
5 |
6 | A plugin for [graphql-code-generator](https://www.graphql-code-generator.com/) that generates TypeScript fixtures for testing.
7 |
8 | ## At a Glance
9 |
10 | ```tsx
11 | import fixture from './generated/graphql-fixtures.ts'
12 |
13 | const user = fixture('User')
14 | user.name // ""
15 | user.followers.totalCount // 0
16 |
17 | // with Immer.js
18 | const repo = fixture('Repository', repo => {
19 | repo.name = 'my-cool-stuff'
20 | repo.stargazerCount = 1234
21 | })
22 | repo.name // "my-cool-stuff"
23 | repo.stargazerCount // 1234
24 | ```
25 |
26 | ## Features
27 |
28 | * 🍭 Strongly typed.
29 |
30 |
31 |
32 | * 🧬 Built-in support for [Immer](https://github.com/immerjs/immer) integration.
33 |
34 |
35 |
36 | ## Installation
37 |
38 | * Using Yarn:
39 | ```console
40 | $ yarn add graphql-codegen-typescript-fixtures --dev
41 | ```
42 | * Using npm:
43 | ```console
44 | $ npm install graphql-codegen-typescript-fixtures --dev
45 | ```
46 |
47 | Add lines below in your graphql-codegen configuration file. Check out [Configuration](Configuration) section for more details.
48 |
49 | ```diff
50 | generates:
51 | src/generated/graphql.ts:
52 | plugins:
53 | - "typescript"
54 | - "typescript-operations"
55 | + src/generated/graphql-fixtures.ts:
56 | + plugins:
57 | + - graphql-codegen-typescript-fixtures
58 |
59 | config:
60 | scalars:
61 | Date: string
62 | DateTime: string
63 | + fixtures:
64 | + typeDefinitionModule: "path/to/graphql/types.ts"
65 | ```
66 |
67 | ## Configuration
68 |
69 | ### typeDefinitionModule
70 |
71 | *(Required)* A path for the GraphQL type definition module. This value is used to import the GraphQL type definitions.
72 |
73 | For example:
74 |
75 | ```yaml
76 | config:
77 | fixtures:
78 | typeDefinitionModule: "@src/generated/graphql"
79 | ```
80 |
81 | And the generated code will be:
82 |
83 | ```ts
84 | // src/generated/graphql-fixtures.ts
85 | import * as types from '@src/generated/graphql'
86 | ```
87 |
88 | ### immer
89 |
90 | *(Optional)* Whether to generate [Immer](https://github.com/immerjs/immer) integration.
91 |
92 | For example:
93 |
94 | ```yaml
95 | config:
96 | fixtures:
97 | immer: true
98 | ```
99 |
100 | Then the second parameter of `fixture()` will become available.
101 |
102 | ```ts
103 | fixture('User', user => {
104 | user.name = 'Suyeol Jeon'
105 | })
106 | ```
107 |
108 | ### scalarDefaults
109 |
110 | *(Optional)* The default values of scalar types. Note that the values are directly written to the TypeScript code so you need to wrap strings with quotes properly.
111 |
112 | For example:
113 |
114 | ```yaml
115 | config:
116 | fixtures:
117 | scalarDefaults:
118 | Date: "'2021-01-01'"
119 | DateTime: "'2021-01-01T00:00:00+00:00'"
120 | Timestamp: 1609426800
121 | ```
122 |
123 | ## License
124 |
125 | This project is under MIT license. See the [LICENSE](LICENSE) file for more info.
126 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {
4 | 'targets': {
5 | 'node': 'current'
6 | }
7 | }],
8 | '@babel/preset-typescript',
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | globalSetup: '/tests/globalSetup.ts',
3 | watchPathIgnorePatterns: [
4 | '/build/*',
5 | '/tests/generated/*',
6 | ],
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphql-codegen-typescript-fixtures",
3 | "version": "1.1.1",
4 | "description": "A plugin for graphql-code-generator that generates TypeScript fixtures for testing.",
5 | "main": "./build/index.js",
6 | "repository": "https://github.com/devxoul/graphql-codegen-typescript-fixtures.git",
7 | "author": "Suyeol Jeon ",
8 | "license": "MIT",
9 | "private": false,
10 | "scripts": {
11 | "clean": "rm -rf ./build ./tests/generated ./tests/schema",
12 | "build": "tsc",
13 | "typecheck": "yarn tsc -p tsconfig.json --noemit --skipLibCheck",
14 | "lint": "eslint . --ext .ts",
15 | "test": "jest"
16 | },
17 | "devDependencies": {
18 | "@babel/preset-env": "^7.15.6",
19 | "@babel/preset-typescript": "^7.15.0",
20 | "@graphql-codegen/cli": "^2.6.2",
21 | "@graphql-codegen/typescript": "^2.4.5",
22 | "@types/dedent": "^0.7.0",
23 | "@types/jest": "^27.0.1",
24 | "@typescript-eslint/eslint-plugin": "^4.31.2",
25 | "@typescript-eslint/parser": "^4.31.2",
26 | "babel-jest": "^27.2.0",
27 | "eslint": "^7.32.0",
28 | "graphql": "^16.3.0",
29 | "immer": "^9.0.6",
30 | "jest": "^27.2.0",
31 | "js-yaml": "^4.1.0",
32 | "typescript": "^4.4.3"
33 | },
34 | "dependencies": {
35 | "@graphql-codegen/schema-ast": "^2.2.0",
36 | "dedent": "^0.7.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import dedent from 'dedent'
2 | import { GraphQLSchema, parse, printSchema, visit } from 'graphql'
3 |
4 | import { PluginFunction, Types } from '@graphql-codegen/plugin-helpers'
5 |
6 | import { FixturesVisitor } from './visitor'
7 |
8 | export const plugin: PluginFunction = (
9 | schema,
10 | documents,
11 | config,
12 | ) => {
13 | const visitor = new FixturesVisitor(schema, config)
14 | return {
15 | prepend: getPrepend(visitor),
16 | content: getContent(schema, visitor),
17 | append: getAppend(visitor)
18 | }
19 | }
20 |
21 | const getPrepend = (visitor: FixturesVisitor) => {
22 | const prepend: string[] = []
23 |
24 | if (visitor.config.immer) {
25 | prepend.push(`import produce, { Draft } from 'immer'`)
26 | }
27 |
28 | if (visitor.config.typeDefinitionModule) {
29 | prepend.push(`import * as types from '${visitor.config.typeDefinitionModule}'\n`)
30 | }
31 |
32 | prepend.push('const fixtureMap = {')
33 | return prepend
34 | }
35 |
36 | const getContent = (schema: GraphQLSchema, visitor: FixturesVisitor) => {
37 | const printedSchema = printSchema(schema)
38 | const astNode = parse(printedSchema)
39 |
40 | const content = visit(astNode, visitor)
41 | .definitions.filter(Boolean)
42 | .join('\n')
43 | return ' ' + content.split('\n').join('\n ')
44 | }
45 |
46 | const getAppend = (visitor: FixturesVisitor) => {
47 | const closingBracket = '}'
48 | const fixtureFunctionDefinition = getFixtureFunctionDefinition(visitor.config.immer)
49 | const exportDefaultStatement = 'export default fixture'
50 |
51 | const append = filterTruthy([
52 | closingBracket,
53 | fixtureFunctionDefinition,
54 | exportDefaultStatement,
55 | ])
56 | return append.map(line => line + '\n')
57 | }
58 |
59 | const getFixtureFunctionDefinition = (immer: boolean) => {
60 | const generics = filterTruthy([
61 | 'Name extends keyof typeof fixtureMap',
62 | 'Fixture = ReturnType',
63 | ])
64 | const parameters = filterTruthy([
65 | 'name: Name',
66 | immer && 'recipe?: (draft: Draft) => Draft | void,',
67 | ])
68 | const returnType = 'Fixture'
69 | const returnStatement = immer
70 | ? 'return recipe ? produce(fixture, recipe) : fixture'
71 | : 'return fixture'
72 |
73 | return dedent`
74 | const fixture = <
75 | ${generics.join(',\n ')}
76 | >(
77 | ${parameters.join(',\n ')}
78 | ): ${returnType} => {
79 | const fixture: Fixture = fixtureMap[name]()
80 | ${returnStatement}
81 | }
82 | `
83 | }
84 |
85 | const filterTruthy = (array: (string | false | undefined | null)[]): string[] => {
86 | return array.filter((item): item is string => !!item)
87 | }
88 |
--------------------------------------------------------------------------------
/src/visitor.ts:
--------------------------------------------------------------------------------
1 | import dedent from 'dedent'
2 | import {
3 | EnumTypeDefinitionNode, GraphQLSchema, InputObjectTypeDefinitionNode, InterfaceTypeDefinitionNode,
4 | NamedTypeNode, NameNode, ObjectTypeDefinitionNode, ScalarTypeDefinitionNode, TypeNode,
5 | UnionTypeDefinitionNode
6 | } from 'graphql'
7 |
8 | import {
9 | BaseVisitor, ParsedTypesConfig, RawTypesConfig
10 | } from '@graphql-codegen/visitor-plugin-common'
11 |
12 | export type FixturesVisitorRawConfig = RawTypesConfig & {
13 | fixtures?: {
14 | typeDefinitionModule?: string
15 | scalarDefaults?: Record
16 | immer?: boolean
17 | }
18 | }
19 |
20 | type FixturesVisitorParsedConfig = ParsedTypesConfig & {
21 | typeDefinitionModule?: string
22 | scalarDefaults: Record
23 | immer: boolean
24 | }
25 |
26 | export class FixturesVisitor extends BaseVisitor<
27 | FixturesVisitorRawConfig,
28 | FixturesVisitorParsedConfig
29 | > {
30 | private scalarMap: { [name: string]: string }
31 |
32 | constructor(schema: GraphQLSchema, config: FixturesVisitorRawConfig) {
33 | super(config, {
34 | typeDefinitionModule: config.fixtures?.typeDefinitionModule,
35 | scalarDefaults: config.fixtures?.scalarDefaults ?? {},
36 | immer: config.fixtures?.immer ?? false,
37 | })
38 | this.scalarMap = typeof config.scalars !== 'string' ? config.scalars ?? {} : {}
39 | }
40 |
41 | ScalarTypeDefinition(node: ScalarTypeDefinitionNode): string {
42 | const defaultValue = (() => {
43 | if (this.config.scalarDefaults[node.name.value]) {
44 | return this.config.scalarDefaults[node.name.value]
45 | }
46 | const scalarAlias = this.scalarMap[node.name.value]
47 | switch (scalarAlias) {
48 | case 'string':
49 | return '\'\''
50 | case 'number':
51 | return '0'
52 | default:
53 | return '({})'
54 | }
55 | })()
56 | return dedent`
57 | ${node.name.value}(): ${this.getTypeDefinition('Scalars')}['${node.name.value}'] {
58 | return ${defaultValue}
59 | },
60 | `
61 | }
62 |
63 | ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string {
64 | if (['Query', 'Mutation'].includes(node.name.value)) {
65 | return ''
66 | }
67 | return this.generateObjectFixture(node, {
68 | typename: true,
69 | })
70 | }
71 |
72 | InterfaceTypeDefinition(node: InterfaceTypeDefinitionNode): string {
73 | return this.generateObjectFixture(node, {
74 | typename: false,
75 | })
76 | }
77 |
78 | UnionTypeDefinition(node: UnionTypeDefinitionNode): string | null {
79 | const firstType = node.types?.[0]
80 | if (!firstType) {
81 | return null
82 | }
83 | return dedent`
84 | ${node.name.value}(): ${this.getTypeDefinition(node.name.value)} {
85 | return ${this.getNamedTypeDefaultValue(firstType)}
86 | },
87 | `
88 | }
89 |
90 | EnumTypeDefinition(node: EnumTypeDefinitionNode): string {
91 | return dedent`
92 | ${node.name.value}(): ${this.getTypeDefinition(node.name.value)} {
93 | return '${node.values?.[0].name.value}' as ${this.getTypeDefinition(node.name.value)}
94 | },
95 | `
96 | }
97 |
98 | InputObjectTypeDefinition(node: InputObjectTypeDefinitionNode): string {
99 | return this.generateObjectFixture(node, {
100 | typename: false,
101 | })
102 | }
103 |
104 | DirectiveDefinition(): string | null {
105 | return null
106 | }
107 |
108 | private generateObjectFixture(node: ObjectDefinitionNodeCompatible, options: {
109 | typename: boolean,
110 | }) {
111 | const fields = node.fields ?? []
112 | return dedent`
113 | ${node.name.value}(): ${this.getTypeDefinition(node.name.value)} {
114 | const fixture: Partial<${this.getTypeDefinition(node.name.value)}> = {
115 | ${options.typename ? `__typename: '${node.name.value}',` : ''}
116 | }
117 | ${fields.map(field => (`Object.defineProperties(fixture, {
118 | ${field.name.value}: {
119 | get: () => {
120 | const self = this as any
121 | const value = self.__resolved_${field.name.value} ?? ${this.getObjectFieldDefaultValue(field)}
122 | self.__resolved_${field.name.value} = value
123 | return value
124 | }
125 | },
126 | __resolved_${field.name.value}: {
127 | value: undefined,
128 | writable: true,
129 | }
130 | })`)).join('\n ')}
131 | return fixture as ${this.getTypeDefinition(node.name.value)}
132 | },
133 | `
134 | }
135 |
136 | private getTypeDefinition(name: string) {
137 | return this.config.typeDefinitionModule
138 | ? `types.${this.convertName(name)}`
139 | : name
140 | }
141 |
142 | private getObjectFieldDefaultValue(field: FieldDefinitioinNodeCompatible) {
143 | switch (field.type.kind) {
144 | case 'NamedType':
145 | return 'undefined'
146 |
147 | case 'ListType':
148 | return 'undefined'
149 |
150 | case 'NonNullType':
151 | if (field.type.type.kind === 'ListType') {
152 | return '[]'
153 | } else if (field.type.type.kind === 'NamedType') {
154 | return this.getNamedTypeDefaultValue(field.type.type)
155 | } else {
156 | return null
157 | }
158 |
159 | default:
160 | return null
161 | }
162 | }
163 |
164 | private getNamedTypeDefaultValue(node: NamedTypeNode) {
165 | const typeName = node.name.value
166 |
167 | if (this.config.scalarDefaults[typeName]) {
168 | return this.config.scalarDefaults[typeName]
169 | }
170 |
171 | switch (typeName) {
172 | case 'ID':
173 | return '\'\''
174 |
175 | case 'String':
176 | return '\'\''
177 |
178 | case 'Int':
179 | return '0'
180 |
181 | case 'Float':
182 | return '0'
183 |
184 | case 'Boolean':
185 | return 'false'
186 |
187 | default:
188 | return `this.${typeName}()`
189 | }
190 | }
191 | }
192 |
193 | type ObjectDefinitionNodeCompatible = {
194 | name: NameNode,
195 | fields?: ReadonlyArray,
196 | }
197 |
198 | type FieldDefinitioinNodeCompatible = {
199 | name: NameNode
200 | type: TypeNode
201 | }
202 |
--------------------------------------------------------------------------------
/tests/documents/GetRepository.graphql:
--------------------------------------------------------------------------------
1 | query GetRepository($owner: String!, $name: String!) {
2 | repository(owner: $owner, name: $name) {
3 | ...RepositoryFragment
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tests/documents/RepositoryFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment RepositoryFragment on Repository {
2 | name
3 | owner {
4 | login
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tests/globalSetup.ts:
--------------------------------------------------------------------------------
1 | import setup from './setup'
2 | import { downloadFile, execAsync } from './utils'
3 |
4 | export default async () => {
5 | await clean()
6 | await buildPlugin()
7 | await makeDirectories()
8 | await downloadSchema()
9 | await generateFixturesForTyping()
10 | await generateFixturesForTypingWithImmer()
11 | }
12 |
13 | const clean = async () => {
14 | await execAsync('yarn clean')
15 | }
16 |
17 | const buildPlugin = async () => {
18 | await execAsync('yarn build')
19 | }
20 |
21 | const makeDirectories = async () => {
22 | await execAsync(`mkdir -p ./tests/schema`)
23 | await execAsync(`mkdir -p ./tests/generated`)
24 | }
25 |
26 |
27 | const downloadSchema = async () => {
28 | const url = 'https://docs.github.com/public/schema.docs.graphql'
29 | const destination = './tests/schema/github.schema.graphql'
30 | await downloadFile(url, destination)
31 | }
32 |
33 | const generateFixturesForTyping = async () => {
34 | await setup({
35 | suffix: 'for-typing',
36 | ignoreErrors: true,
37 | })
38 | }
39 |
40 | const generateFixturesForTypingWithImmer = async () => {
41 | await setup({
42 | suffix: 'for-typing-with-immer',
43 | ignoreErrors: true,
44 | fixtureConfig: {
45 | immer: true,
46 | },
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/tests/integration.test.ts:
--------------------------------------------------------------------------------
1 | import setup, { Fixture } from './setup'
2 | import { execAsync } from './utils'
3 |
4 | jest.setTimeout(10000)
5 |
6 | let fixture: Fixture
7 |
8 | beforeAll(async () => {
9 | fixture = await setup({
10 | fixtureConfig: {
11 | scalarDefaults: {
12 | URI: `'https://example.com'`,
13 | },
14 | }
15 | })
16 | })
17 |
18 | it('passes type checks', async () => {
19 | await execAsync(`yarn tsc tests/generated/graphql-fixtures-for-typing.ts --noemit --skipLibCheck`)
20 | await execAsync(`yarn tsc tests/generated/graphql-fixtures-for-typing-with-immer.ts --noemit --skipLibCheck`)
21 | })
22 |
23 | describe('Scalar', () => {
24 | describe('when the default value is not configured', () => {
25 | it('defaults to the empty object', async () => {
26 | expect(fixture('Base64String')).toEqual({})
27 | expect(fixture('FileAddition').contents).toEqual({})
28 | })
29 | })
30 |
31 | describe('when the default value is configured', () => {
32 | it('defaults to the pre-configured value', () => {
33 | expect(fixture('URI')).toEqual('https://example.com')
34 | expect(fixture('Issue').url).toEqual('https://example.com')
35 | })
36 | })
37 | })
38 |
39 | describe('Object', () => {
40 | it('properties are empty by default', () => {
41 | const repository = fixture('Repository')
42 | expect(repository.name).toEqual('')
43 | expect(repository.owner.login).toEqual('')
44 | expect(repository.stargazerCount).toEqual(0)
45 | expect(repository.isEmpty).toEqual(false)
46 | expect(repository.homepageUrl).toBeUndefined()
47 | expect(repository.createdAt).toEqual('')
48 | expect(repository.url).toEqual('https://example.com')
49 | })
50 | })
51 |
52 | describe('Interface', () => {
53 | it('properties are empty by default', () => {
54 | const repositoryOwner = fixture('RepositoryOwner')
55 | expect(repositoryOwner.login).toEqual('')
56 | expect(repositoryOwner.avatarUrl).toEqual('https://example.com')
57 | })
58 | })
59 |
60 | describe('Union', () => {
61 | it('defaults to the first type', () => {
62 | const issueOrPullRequest = fixture('IssueOrPullRequest')
63 | expect(issueOrPullRequest.__typename).toEqual('Issue')
64 | })
65 | })
66 |
67 | describe('Enum', () => {
68 | it('defaults to the first value', () => {
69 | const issueState = fixture('IssueState')
70 | expect(issueState).toEqual('CLOSED')
71 | })
72 | })
73 |
74 | describe('InputObject', () => {
75 | it('properties are empty by default', () => {
76 | const createIssueInput = fixture('CreateIssueInput')
77 | expect(createIssueInput.title).toEqual('')
78 | expect(createIssueInput.body).toBeUndefined()
79 | })
80 | })
81 |
82 | describe('a fixture with immer', () => {
83 | it('properties can be configured', async () => {
84 | const fixture = await setup({
85 | fixtureConfig: {
86 | immer: true,
87 | },
88 | })
89 | const repository = fixture('Repository', repo => {
90 | repo.name = 'graphql-codegen-typescript-fixtures'
91 | repo.owner.login = 'devxoul'
92 | repo.stargazerCount = 1234
93 | repo.homepageUrl = 'https://example.com'
94 | })
95 | expect(repository.name).toEqual('graphql-codegen-typescript-fixtures')
96 | expect(repository.owner.login).toEqual('devxoul')
97 | expect(repository.stargazerCount).toEqual(1234)
98 | expect(repository.isEmpty).toEqual(false)
99 | expect(repository.homepageUrl).toEqual('https://example.com')
100 | })
101 | })
102 |
103 | it('typecheck', async () => {
104 | let error: Error | undefined
105 | try {
106 | await execAsync('yarn tsc tests/generated/graphql-fixtures-for-typing.ts --noemit --skipLibCheck')
107 | error = undefined
108 | } catch (err) {
109 | error = err
110 | }
111 | expect(error).toBeUndefined()
112 | })
113 |
--------------------------------------------------------------------------------
/tests/setup.ts:
--------------------------------------------------------------------------------
1 | import yaml from 'js-yaml'
2 |
3 | import { execAsync, getRandomString, writeFileAsync } from './utils'
4 |
5 | type Options = {
6 | suffix?: string
7 | fixtureConfig?: FixtureConfig
8 | ignoreErrors?: boolean
9 | }
10 |
11 | type FixtureConfig = {
12 | typeDefinitionModule?: string
13 | scalarDefaults?: Record
14 | immer?: boolean
15 | }
16 |
17 | type OptionsWithoutImmer = Options & (
18 | { fixtureConfig: undefined } | {
19 | fixtureConfig: {
20 | immer: false
21 | }
22 | }
23 | )
24 | type OptionsWithImmer = Options & {
25 | fixtureConfig: {
26 | immer: true
27 | }
28 | }
29 |
30 | export type Fixture = typeof import('./generated/graphql-fixtures-for-typing').default
31 | export type FixtureWithImmer = typeof import('./generated/graphql-fixtures-for-typing-with-immer').default
32 |
33 | async function setup(options?: OptionsWithoutImmer): Promise
34 | async function setup(options?: OptionsWithImmer): Promise
35 | async function setup(options?: Options): Promise
36 |
37 | async function setup(options?: Options): Promise {
38 | const suffix = options?.suffix || getRandomString()
39 | const configFilePath = `./tests/generated/codegen-${suffix}.yml`
40 | const fixtureModuleName = `graphql-fixtures-${suffix}`
41 |
42 | await writeConfig({
43 | configFilePath,
44 | fixtureModuleName,
45 | additionalFixtureConfig: options?.fixtureConfig
46 | })
47 | await runCodegen(configFilePath)
48 |
49 | const fixture = await safeImport({
50 | fixtureModuleName,
51 | ignoreErrors: options.ignoreErrors,
52 | })
53 | return fixture
54 | }
55 |
56 | export default setup
57 |
58 |
59 | type WriteConfigOptions = {
60 | configFilePath: string
61 | fixtureModuleName: string
62 | additionalFixtureConfig?: FixtureConfig
63 | }
64 |
65 | const writeConfig = async (options: WriteConfigOptions) => {
66 | const jsonConfig = {
67 | overwrite: true,
68 | schema: './tests/schema/github.schema.graphql',
69 | documents: './tests/documents/*.graphql',
70 | generates: {
71 | './tests/generated/graphql.ts': {
72 | plugins: [
73 | 'typescript',
74 | ],
75 | },
76 | [`./tests/generated/${options.fixtureModuleName}.ts`]: {
77 | plugins: [
78 | './build/index.js',
79 | ],
80 | }
81 | },
82 | config: {
83 | scalars: {
84 | Date: 'string',
85 | DateTime: 'string',
86 | URI: 'string',
87 | },
88 | fixtures: {
89 | typeDefinitionModule: './graphql',
90 | ...(options.additionalFixtureConfig || {}),
91 | }
92 | },
93 | }
94 | const yamlConfig = yaml.dump(jsonConfig, {
95 | quotingType: '"',
96 | forceQuotes: true,
97 | })
98 | await writeFileAsync(options.configFilePath, yamlConfig)
99 | }
100 |
101 |
102 | const runCodegen = async (configFilePath: string) => {
103 | await execAsync(`yarn graphql-codegen --config ${configFilePath}`)
104 | }
105 |
106 |
107 | type SafeImportOptions = {
108 | fixtureModuleName: string
109 | ignoreErrors?: boolean
110 | }
111 |
112 | const safeImport = async (options: SafeImportOptions) => {
113 | try {
114 | const { default: fixture } = await import(`./generated/${options.fixtureModuleName}`)
115 | return fixture
116 | } catch (error) {
117 | if (!options.ignoreErrors) {
118 | throw error
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/tests/utils.ts:
--------------------------------------------------------------------------------
1 | import { exec } from 'child_process'
2 | import fs from 'fs'
3 | import https from 'https'
4 |
5 | export const execAsync = async (command: string) => {
6 | return new Promise((resolve, reject) => {
7 | exec(command, (error) => {
8 | if (error) {
9 | reject(error)
10 | } else {
11 | resolve()
12 | }
13 | })
14 | })
15 | }
16 |
17 | export const downloadFile = async (url: string, destination: string) => {
18 | const file = fs.createWriteStream(destination)
19 | return new Promise((resolve) => {
20 | https.get(url, (response) => {
21 | response.pipe(file)
22 | resolve()
23 | })
24 | })
25 | }
26 |
27 | export const writeFileAsync = async (path: string, data: string) => {
28 | return new Promise((resolve, reject) => {
29 | fs.writeFile(path, data, (error) => {
30 | if (error) {
31 | reject(error)
32 | } else {
33 | resolve()
34 | }
35 | })
36 | })
37 | }
38 |
39 | export const getRandomString = () => {
40 | return Math.random().toString(36).substr(2, 5)
41 | }
42 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "commonjs",
5 | "lib": ["esnext"],
6 | "strict": true,
7 | "skipLibCheck": true,
8 | "esModuleInterop": true,
9 | "outDir": "build",
10 | },
11 | "exclude": [
12 | "node_modules",
13 | "tests",
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------