summaryrefslogtreecommitdiff
path: root/spec/tooling/graphql/docs/renderer_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/tooling/graphql/docs/renderer_spec.rb')
-rw-r--r--spec/tooling/graphql/docs/renderer_spec.rb643
1 files changed, 643 insertions, 0 deletions
diff --git a/spec/tooling/graphql/docs/renderer_spec.rb b/spec/tooling/graphql/docs/renderer_spec.rb
new file mode 100644
index 00000000000..50ebb754ca4
--- /dev/null
+++ b/spec/tooling/graphql/docs/renderer_spec.rb
@@ -0,0 +1,643 @@
+# frozen_string_literal: true
+
+require_relative '../../../../tooling/graphql/docs/renderer'
+
+RSpec.describe Tooling::Graphql::Docs::Renderer do
+ describe '#contents' do
+ shared_examples 'renders correctly as GraphQL documentation' do
+ it 'contains the expected section' do
+ # duplicative - but much better error messages!
+ section.lines.each { |line| expect(contents).to include(line) }
+ expect(contents).to include(section)
+ end
+ end
+
+ let(:template) { Rails.root.join('tooling/graphql/docs/templates/default.md.haml') }
+ let(:field_description) { 'List of objects.' }
+ let(:type) { ::GraphQL::INT_TYPE }
+
+ let(:query_type) do
+ Class.new(Types::BaseObject) { graphql_name 'Query' }.tap do |t|
+ # this keeps type and field_description in scope.
+ t.field :foo, type, null: true, description: field_description do
+ argument :id, GraphQL::ID_TYPE, required: false, description: 'ID of the object.'
+ end
+ end
+ end
+
+ let(:mutation_root) do
+ Class.new(::Types::BaseObject) do
+ include ::Gitlab::Graphql::MountMutation
+ graphql_name 'Mutation'
+ end
+ end
+
+ let(:mock_schema) do
+ Class.new(GraphQL::Schema) do
+ def resolve_type(obj, ctx)
+ raise 'Not a real schema'
+ end
+ end
+ end
+
+ subject(:contents) do
+ mock_schema.query(query_type)
+ mock_schema.mutation(mutation_root) if mutation_root.fields.any?
+
+ described_class.new(
+ mock_schema,
+ output_dir: nil,
+ template: template
+ ).contents
+ end
+
+ describe 'headings' do
+ it 'contains the expected sections' do
+ expect(contents.lines.map(&:chomp)).to include(
+ '## `Query` type',
+ '## `Mutation` type',
+ '## Connections',
+ '## Object types',
+ '## Enumeration types',
+ '## Scalar types',
+ '## Abstract types',
+ '### Unions',
+ '### Interfaces',
+ '## Input types'
+ )
+ end
+ end
+
+ context 'when a field has a list type' do
+ let(:type) do
+ Class.new(Types::BaseObject) do
+ graphql_name 'ArrayTest'
+
+ field :foo, [GraphQL::STRING_TYPE], null: false, description: 'A description.'
+ end
+ end
+
+ specify do
+ type_name = '[String!]!'
+ inner_type = 'string'
+ expectation = <<~DOC
+ ### `ArrayTest`
+
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="arraytestfoo"></a>`foo` | [`#{type_name}`](##{inner_type}) | A description. |
+ DOC
+
+ is_expected.to include(expectation)
+ end
+
+ describe 'a top level query field' do
+ let(:expectation) do
+ <<~DOC
+ ### `Query.foo`
+
+ List of objects.
+
+ Returns [`ArrayTest`](#arraytest).
+
+ #### Arguments
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="queryfooid"></a>`id` | [`ID`](#id) | ID of the object. |
+ DOC
+ end
+
+ it 'generates the query with arguments' do
+ expect(subject).to include(expectation)
+ end
+
+ context 'when description does not end with `.`' do
+ let(:field_description) { 'List of objects' }
+
+ it 'adds the `.` to the end' do
+ expect(subject).to include(expectation)
+ end
+ end
+ end
+ end
+
+ describe 'when fields are not defined in alphabetical order' do
+ let(:type) do
+ Class.new(Types::BaseObject) do
+ graphql_name 'OrderingTest'
+
+ field :foo, GraphQL::STRING_TYPE, null: false, description: 'A description of foo field.'
+ field :bar, GraphQL::STRING_TYPE, null: false, description: 'A description of bar field.'
+ end
+ end
+
+ it 'lists the fields in alphabetical order' do
+ expectation = <<~DOC
+ ### `OrderingTest`
+
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="orderingtestbar"></a>`bar` | [`String!`](#string) | A description of bar field. |
+ | <a id="orderingtestfoo"></a>`foo` | [`String!`](#string) | A description of foo field. |
+ DOC
+
+ is_expected.to include(expectation)
+ end
+ end
+
+ context 'when a field has a documentation reference' do
+ let(:type) do
+ wibble = Class.new(::Types::BaseObject) do
+ graphql_name 'Wibble'
+ field :x, ::GraphQL::INT_TYPE, null: false
+ end
+
+ Class.new(Types::BaseObject) do
+ graphql_name 'DocRefSpec'
+ description 'Testing doc refs'
+
+ field :foo,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ description: 'The foo.',
+ see: { 'A list of foos' => 'https://example.com/foos' }
+ field :bar,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ description: 'The bar.',
+ see: { 'A list of bars' => 'https://example.com/bars' } do
+ argument :barity, ::GraphQL::INT_TYPE, required: false, description: '?'
+ end
+ field :wibbles,
+ type: wibble.connection_type,
+ null: true,
+ description: 'The wibbles',
+ see: { 'wibblance' => 'https://example.com/wibbles' }
+ end
+ end
+
+ let(:section) do
+ <<~DOC
+ ### `DocRefSpec`
+
+ Testing doc refs.
+
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="docrefspecfoo"></a>`foo` | [`String!`](#string) | The foo. See [A list of foos](https://example.com/foos). |
+ | <a id="docrefspecwibbles"></a>`wibbles` | [`WibbleConnection`](#wibbleconnection) | The wibbles. See [wibblance](https://example.com/wibbles). (see [Connections](#connections)) |
+
+ #### Fields with arguments
+
+ ##### `DocRefSpec.bar`
+
+ The bar. See [A list of bars](https://example.com/bars).
+
+ Returns [`String!`](#string).
+
+ ###### Arguments
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="docrefspecbarbarity"></a>`barity` | [`Int`](#int) | ?. |
+ DOC
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation'
+ end
+
+ context 'when an argument is deprecated' do
+ let(:type) do
+ Class.new(Types::BaseObject) do
+ graphql_name 'DeprecatedTest'
+ description 'A thing we used to use, but no longer support'
+
+ field :foo,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ description: 'A description.' do
+ argument :foo_arg, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'The argument.',
+ deprecated: { reason: 'Bad argument', milestone: '101.2' }
+ end
+ end
+ end
+
+ let(:section) do
+ <<~DOC
+ ##### `DeprecatedTest.foo`
+
+ A description.
+
+ Returns [`String!`](#string).
+
+ ###### Arguments
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="deprecatedtestfoofooarg"></a>`fooArg` **{warning-solid}** | [`String`](#string) | **Deprecated** in 101.2. Bad argument. |
+ DOC
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation'
+ end
+
+ context 'when a field is deprecated' do
+ let(:type) do
+ Class.new(Types::BaseObject) do
+ graphql_name 'DeprecatedTest'
+ description 'A thing we used to use, but no longer support'
+
+ field :foo,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ deprecated: { reason: 'This is deprecated', milestone: '1.10' },
+ description: 'A description.'
+ field :foo_with_args,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ deprecated: { reason: 'Do not use', milestone: '1.10', replacement: 'X.y' },
+ description: 'A description.' do
+ argument :arg, GraphQL::INT_TYPE, required: false, description: 'Argity'
+ end
+ field :bar,
+ type: GraphQL::STRING_TYPE,
+ null: false,
+ description: 'A description.',
+ deprecated: {
+ reason: :renamed,
+ milestone: '1.10',
+ replacement: 'Query.boom'
+ }
+ end
+ end
+
+ let(:section) do
+ <<~DOC
+ ### `DeprecatedTest`
+
+ A thing we used to use, but no longer support.
+
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="deprecatedtestbar"></a>`bar` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This was renamed. Use: [`Query.boom`](#queryboom). |
+ | <a id="deprecatedtestfoo"></a>`foo` **{warning-solid}** | [`String!`](#string) | **Deprecated** in 1.10. This is deprecated. |
+
+ #### Fields with arguments
+
+ ##### `DeprecatedTest.fooWithArgs`
+
+ A description.
+
+ WARNING:
+ **Deprecated** in 1.10.
+ Do not use.
+ Use: [`X.y`](#xy).
+
+ Returns [`String!`](#string).
+
+ ###### Arguments
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="deprecatedtestfoowithargsarg"></a>`arg` | [`Int`](#int) | Argity. |
+ DOC
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation'
+ end
+
+ context 'when a Query.field is deprecated' do
+ before do
+ query_type.field(
+ name: :bar,
+ type: type,
+ null: true,
+ description: 'A bar',
+ deprecated: { reason: :renamed, milestone: '10.11', replacement: 'Query.foo' }
+ )
+ end
+
+ let(:type) { ::GraphQL::INT_TYPE }
+ let(:section) do
+ <<~DOC
+ ### `Query.bar`
+
+ A bar.
+
+ WARNING:
+ **Deprecated** in 10.11.
+ This was renamed.
+ Use: [`Query.foo`](#queryfoo).
+
+ Returns [`Int`](#int).
+ DOC
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation'
+ end
+
+ context 'when a field has an Enumeration type' do
+ let(:type) do
+ enum_type = Class.new(Types::BaseEnum) do
+ graphql_name 'MyEnum'
+ description 'A test of an enum.'
+
+ value 'BAZ',
+ description: 'A description of BAZ.'
+ value 'BAR',
+ description: 'A description of BAR.',
+ deprecated: { reason: 'This is deprecated', milestone: '1.10' }
+ value 'BOOP',
+ description: 'A description of BOOP.',
+ deprecated: { reason: :renamed, replacement: 'MyEnum.BAR', milestone: '1.10' }
+ end
+
+ Class.new(Types::BaseObject) do
+ graphql_name 'EnumTest'
+
+ field :foo, enum_type, null: false, description: 'A description of foo field.'
+ end
+ end
+
+ let(:section) do
+ <<~DOC
+ ### `MyEnum`
+
+ A test of an enum.
+
+ | Value | Description |
+ | ----- | ----------- |
+ | <a id="myenumbar"></a>`BAR` **{warning-solid}** | **Deprecated** in 1.10. This is deprecated. |
+ | <a id="myenumbaz"></a>`BAZ` | A description of BAZ. |
+ | <a id="myenumboop"></a>`BOOP` **{warning-solid}** | **Deprecated** in 1.10. This was renamed. Use: [`MyEnum.BAR`](#myenumbar). |
+ DOC
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation'
+ end
+
+ context 'when a field has a global ID type' do
+ let(:type) do
+ Class.new(Types::BaseObject) do
+ graphql_name 'IDTest'
+ description 'A test for rendering IDs.'
+
+ field :foo, ::Types::GlobalIDType[::User], null: true, description: 'A user foo.'
+ end
+ end
+
+ describe 'section for IDTest' do
+ let(:section) do
+ <<~DOC
+ ### `IDTest`
+
+ A test for rendering IDs.
+
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="idtestfoo"></a>`foo` | [`UserID`](#userid) | A user foo. |
+ DOC
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation'
+ end
+
+ describe 'section for UserID' do
+ let(:section) do
+ <<~DOC
+ ### `UserID`
+
+ A `UserID` is a global ID. It is encoded as a string.
+
+ An example `UserID` is: `"gid://gitlab/User/1"`.
+ DOC
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation'
+ end
+ end
+
+ context 'when there is a mutation' do
+ let(:mutation) do
+ mutation = Class.new(::Mutations::BaseMutation)
+
+ mutation.graphql_name 'MakeItPretty'
+ mutation.description 'Make everything very pretty.'
+
+ mutation.argument :prettiness_factor,
+ type: GraphQL::FLOAT_TYPE,
+ required: true,
+ description: 'How much prettier?'
+
+ mutation.argument :pulchritude,
+ type: GraphQL::FLOAT_TYPE,
+ required: false,
+ description: 'How much prettier?',
+ deprecated: {
+ reason: :renamed,
+ replacement: 'prettinessFactor',
+ milestone: '72.34'
+ }
+
+ mutation.field :everything,
+ type: GraphQL::STRING_TYPE,
+ null: true,
+ description: 'What we made prettier.'
+
+ mutation.field :omnis,
+ type: GraphQL::STRING_TYPE,
+ null: true,
+ description: 'What we made prettier.',
+ deprecated: {
+ reason: :renamed,
+ replacement: 'everything',
+ milestone: '72.34'
+ }
+
+ mutation
+ end
+
+ before do
+ mutation_root.mount_mutation mutation
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation' do
+ let(:section) do
+ <<~DOC
+ ### `Mutation.makeItPretty`
+
+ Make everything very pretty.
+
+ Input type: `MakeItPrettyInput`
+
+ #### Arguments
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="mutationmakeitprettyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+ | <a id="mutationmakeitprettyprettinessfactor"></a>`prettinessFactor` | [`Float!`](#float) | How much prettier?. |
+ | <a id="mutationmakeitprettypulchritude"></a>`pulchritude` **{warning-solid}** | [`Float`](#float) | **Deprecated:** This was renamed. Please use `prettinessFactor`. Deprecated in 72.34. |
+
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="mutationmakeitprettyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+ | <a id="mutationmakeitprettyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+ | <a id="mutationmakeitprettyeverything"></a>`everything` | [`String`](#string) | What we made prettier. |
+ | <a id="mutationmakeitprettyomnis"></a>`omnis` **{warning-solid}** | [`String`](#string) | **Deprecated:** This was renamed. Please use `everything`. Deprecated in 72.34. |
+ DOC
+ end
+ end
+
+ it 'does not render the automatically generated payload type' do
+ expect(contents).not_to include('MakeItPrettyPayload')
+ end
+
+ it 'does not render the automatically generated input type as its own section' do
+ expect(contents).not_to include('# `MakeItPrettyInput`')
+ end
+ end
+
+ context 'when there is an input type' do
+ let(:type) do
+ Class.new(::Types::BaseObject) do
+ graphql_name 'Foo'
+ field :wibble, type: ::GraphQL::INT_TYPE, null: true do
+ argument :date_range,
+ type: ::Types::TimeframeInputType,
+ required: true,
+ description: 'When the foo happened.'
+ end
+ end
+ end
+
+ let(:section) do
+ <<~DOC
+ ### `Timeframe`
+
+ A time-frame defined as a closed inclusive range of two dates.
+
+ #### Arguments
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="timeframeend"></a>`end` | [`Date!`](#date) | The end of the range. |
+ | <a id="timeframestart"></a>`start` | [`Date!`](#date) | The start of the range. |
+ DOC
+ end
+
+ it_behaves_like 'renders correctly as GraphQL documentation'
+ end
+
+ context 'when there is an interface and a union' do
+ let(:type) do
+ user = Class.new(::Types::BaseObject)
+ user.graphql_name 'User'
+ user.field :user_field, ::GraphQL::STRING_TYPE, null: true
+ group = Class.new(::Types::BaseObject)
+ group.graphql_name 'Group'
+ group.field :group_field, ::GraphQL::STRING_TYPE, null: true
+
+ union = Class.new(::Types::BaseUnion)
+ union.graphql_name 'UserOrGroup'
+ union.description 'Either a user or a group.'
+ union.possible_types user, group
+
+ interface = Module.new
+ interface.include(::Types::BaseInterface)
+ interface.graphql_name 'Flying'
+ interface.description 'Something that can fly.'
+ interface.field :flight_speed, GraphQL::INT_TYPE, null: true, description: 'Speed in mph.'
+
+ african_swallow = Class.new(::Types::BaseObject)
+ african_swallow.graphql_name 'AfricanSwallow'
+ african_swallow.description 'A swallow from Africa.'
+ african_swallow.implements interface
+ interface.orphan_types african_swallow
+
+ Class.new(::Types::BaseObject) do
+ graphql_name 'AbstractTypeTest'
+ description 'A test for abstract types.'
+
+ field :foo, union, null: true, description: 'The foo.'
+ field :flying, interface, null: true, description: 'A flying thing.'
+ end
+ end
+
+ it 'lists the fields correctly, and includes descriptions of all the types' do
+ type_section = <<~DOC
+ ### `AbstractTypeTest`
+
+ A test for abstract types.
+
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="abstracttypetestflying"></a>`flying` | [`Flying`](#flying) | A flying thing. |
+ | <a id="abstracttypetestfoo"></a>`foo` | [`UserOrGroup`](#userorgroup) | The foo. |
+ DOC
+
+ union_section = <<~DOC
+ #### `UserOrGroup`
+
+ Either a user or a group.
+
+ One of:
+
+ - [`Group`](#group)
+ - [`User`](#user)
+ DOC
+
+ interface_section = <<~DOC
+ #### `Flying`
+
+ Something that can fly.
+
+ Implementations:
+
+ - [`AfricanSwallow`](#africanswallow)
+
+ ##### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="flyingflightspeed"></a>`flightSpeed` | [`Int`](#int) | Speed in mph. |
+ DOC
+
+ implementation_section = <<~DOC
+ ### `AfricanSwallow`
+
+ A swallow from Africa.
+
+ #### Fields
+
+ | Name | Type | Description |
+ | ---- | ---- | ----------- |
+ | <a id="africanswallowflightspeed"></a>`flightSpeed` | [`Int`](#int) | Speed in mph. |
+ DOC
+
+ is_expected.to include(
+ type_section,
+ union_section,
+ interface_section,
+ implementation_section
+ )
+ end
+ end
+ end
+end