diff options
Diffstat (limited to 'spec/tooling/graphql/docs')
-rw-r--r-- | spec/tooling/graphql/docs/renderer_spec.rb | 643 |
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 |