Commit 82cc94da authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch '13495-c-design-at-version-types' into 'master'

13495-c: Add design-at-version GraphQL entities

See merge request gitlab-org/gitlab!21276
parents c40136fb e4f91bdc
......@@ -86,4 +86,4 @@ jsdoc/
.projections.json
/qa/.rakeTasks
webpack-dev-server.json
.nvimrc
/.nvimrc
......@@ -57,13 +57,55 @@ class GitlabSchema < GraphQL::Schema
object.to_global_id
end
# Find an object by looking it up from its global ID, passed as a string.
#
# This is the composition of 'parse_gid' and 'find_by_gid', see these
# methods for further documentation.
def object_from_id(global_id, ctx = {})
gid = parse_gid(global_id, ctx)
find_by_gid(gid)
end
# Find an object by looking it up from its 'GlobalID'.
#
# * For `ApplicationRecord`s, this is equivalent to
# `global_id.model_class.find(gid.model_id)`, but more efficient.
# * For classes that implement `.lazy_find(global_id)`, this class method
# will be called.
# * All other classes will use `GlobalID#find`
def find_by_gid(gid)
if gid.model_class < ApplicationRecord
Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
elsif gid.model_class.respond_to?(:lazy_find)
gid.model_class.lazy_find(gid.model_id)
else
gid.find
end
end
# Parse a string to a GlobalID, raising ArgumentError if there are problems
# with it.
#
# Problems that may occur:
# * it may not be syntactically valid
# * it may not match the expected type (see below)
#
# Options:
# * :expected_type [Class] - the type of object this GlobalID should refer to.
#
# e.g.
#
# ```
# gid = GitlabSchema.parse_gid(my_string, expected_type: ::Project)
# project_id = gid.model_id
# gid.model_class == ::Project
# ```
def parse_gid(global_id, ctx = {})
expected_type = ctx[:expected_type]
gid = GlobalID.parse(global_id)
unless gid
raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab id."
end
raise Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab id." unless gid
if expected_type && !gid.model_class.ancestors.include?(expected_type)
vars = { global_id: global_id, expected_type: expected_type }
......@@ -71,13 +113,7 @@ class GitlabSchema < GraphQL::Schema
raise Gitlab::Graphql::Errors::ArgumentError, msg
end
if gid.model_class < ApplicationRecord
Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
elsif gid.model_class.respond_to?(:lazy_find)
gid.model_class.lazy_find(gid.model_id)
else
gid.find
end
gid
end
private
......
......@@ -529,9 +529,9 @@ type CreateSnippetPayload {
snippet: Snippet
}
type Design implements Noteable {
type Design implements DesignFields & Noteable {
"""
Diff refs of the design
The diff refs for this design
"""
diffRefs: DiffRefs!
......@@ -561,33 +561,32 @@ type Design implements Noteable {
): DiscussionConnection!
"""
Type of change made to the design at the version specified by the `atVersion`
argument if supplied. Defaults to the latest version
How this design was changed in the current version
"""
event: DesignVersionEvent!
"""
Filename of the design file
The filename of the design
"""
filename: String!
"""
Full path of the design file
The full path to the design file
"""
fullPath: String!
"""
ID of the design
The ID of this design
"""
id: ID!
"""
Image of the design
The URL of the image
"""
image: String!
"""
Issue associated with the design
The issue the design belongs to
"""
issue: Issue!
......@@ -617,17 +616,17 @@ type Design implements Noteable {
): NoteConnection!
"""
Total count of user-created notes for the design
The total count of user-created notes for this design
"""
notesCount: Int!
"""
Project associated with the design
The project the design belongs to
"""
project: Project!
"""
All versions related to the design, ordered newest first
All versions related to this design ordered newest first
"""
versions(
"""
......@@ -765,6 +764,53 @@ type DesignEdge {
node: Design
}
interface DesignFields {
"""
The diff refs for this design
"""
diffRefs: DiffRefs!
"""
How this design was changed in the current version
"""
event: DesignVersionEvent!
"""
The filename of the design
"""
filename: String!
"""
The full path to the design file
"""
fullPath: String!
"""
The ID of this design
"""
id: ID!
"""
The URL of the image
"""
image: String!
"""
The issue the design belongs to
"""
issue: Issue!
"""
The total count of user-created notes for this design
"""
notesCount: Int!
"""
The project the design belongs to
"""
project: Project!
}
"""
Autogenerated input type of DesignManagementDelete
"""
......
......@@ -10350,7 +10350,7 @@
"fields": [
{
"name": "diffRefs",
"description": "Diff refs of the design",
"description": "The diff refs for this design",
"args": [
],
......@@ -10425,7 +10425,7 @@
},
{
"name": "event",
"description": "Type of change made to the design at the version specified by the `atVersion` argument if supplied. Defaults to the latest version",
"description": "How this design was changed in the current version",
"args": [
],
......@@ -10443,7 +10443,7 @@
},
{
"name": "filename",
"description": "Filename of the design file",
"description": "The filename of the design",
"args": [
],
......@@ -10461,7 +10461,7 @@
},
{
"name": "fullPath",
"description": "Full path of the design file",
"description": "The full path to the design file",
"args": [
],
......@@ -10479,7 +10479,7 @@
},
{
"name": "id",
"description": "ID of the design",
"description": "The ID of this design",
"args": [
],
......@@ -10497,7 +10497,7 @@
},
{
"name": "image",
"description": "Image of the design",
"description": "The URL of the image",
"args": [
],
......@@ -10515,7 +10515,7 @@
},
{
"name": "issue",
"description": "Issue associated with the design",
"description": "The issue the design belongs to",
"args": [
],
......@@ -10590,7 +10590,7 @@
},
{
"name": "notesCount",
"description": "Total count of user-created notes for the design",
"description": "The total count of user-created notes for this design",
"args": [
],
......@@ -10608,7 +10608,7 @@
},
{
"name": "project",
"description": "Project associated with the design",
"description": "The project the design belongs to",
"args": [
],
......@@ -10626,7 +10626,7 @@
},
{
"name": "versions",
"description": "All versions related to the design, ordered newest first",
"description": "All versions related to this design ordered newest first",
"args": [
{
"name": "after",
......@@ -10688,11 +10688,195 @@
"kind": "INTERFACE",
"name": "Noteable",
"ofType": null
},
{
"kind": "INTERFACE",
"name": "DesignFields",
"ofType": null
}
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "DesignFields",
"description": null,
"fields": [
{
"name": "diffRefs",
"description": "The diff refs for this design",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DiffRefs",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "event",
"description": "How this design was changed in the current version",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "DesignVersionEvent",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "filename",
"description": "The filename of the design",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fullPath",
"description": "The full path to the design file",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "The ID of this design",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "image",
"description": "The URL of the image",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issue",
"description": "The issue the design belongs to",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "notesCount",
"description": "The total count of user-created notes for this design",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "project",
"description": "The project the design belongs to",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Project",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "Design",
"ofType": null
}
]
},
{
"kind": "ENUM",
"name": "DesignVersionEvent",
......
......@@ -104,15 +104,15 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | ID of the design |
| `project` | Project! | Project associated with the design |
| `issue` | Issue! | Issue associated with the design |
| `notesCount` | Int! | Total count of user-created notes for the design |
| `filename` | String! | Filename of the design file |
| `fullPath` | String! | Full path of the design file |
| `event` | DesignVersionEvent! | Type of change made to the design at the version specified by the `atVersion` argument if supplied. Defaults to the latest version |
| `image` | String! | Image of the design |
| `diffRefs` | DiffRefs! | Diff refs of the design |
| `id` | ID! | The ID of this design |
| `project` | Project! | The project the design belongs to |
| `issue` | Issue! | The issue the design belongs to |
| `filename` | String! | The filename of the design |
| `fullPath` | String! | The full path to the design file |
| `image` | String! | The URL of the image |
| `diffRefs` | DiffRefs! | The diff refs for this design |
| `event` | DesignVersionEvent! | How this design was changed in the current version |
| `notesCount` | Int! | The total count of user-created notes for this design |
### DesignCollection
......
# frozen_string_literal: true
module Types
module DesignManagement
class DesignAtVersionType < BaseObject
graphql_name 'DesignAtVersion'
description 'A design pinned to a specific version'
authorize :read_design
delegate :design, :version, to: :object
delegate :issue, :filename, :full_path, :diff_refs, to: :design
implements ::Types::DesignManagement::DesignFields
field :version,
Types::DesignManagement::VersionType,
null: false,
description: 'The version this design-at-versions is pinned to'
field :design,
Types::DesignManagement::DesignType,
null: false,
description: 'The underlying design.'
def cached_stateful_version(_parent)
version
end
def notes_count
design.user_notes_count
end
end
end
end
# frozen_string_literal: true
module Types
module DesignManagement
module DesignFields
include BaseInterface
field_class Types::BaseField
field :id, GraphQL::ID_TYPE, description: 'The ID of this design', null: false
field :project, Types::ProjectType, null: false, description: 'The project the design belongs to'
field :issue, Types::IssueType, null: false, description: 'The issue the design belongs to'
field :filename, GraphQL::STRING_TYPE, null: false, description: 'The filename of the design'
field :full_path, GraphQL::STRING_TYPE, null: false, description: 'The full path to the design file'
field :image, GraphQL::STRING_TYPE, null: false, extras: [:parent], description: 'The URL of the image'
field :diff_refs, Types::DiffRefsType,
null: false,
calls_gitaly: true,
extras: [:parent],
description: 'The diff refs for this design'
field :event, Types::DesignManagement::DesignVersionEventEnum,
null: false,
extras: [:parent],
description: 'How this design was changed in the current version'
field :notes_count,
GraphQL::INT_TYPE,
null: false,
method: :user_notes_count,
description: 'The total count of user-created notes for this design'
def diff_refs(parent:)
version = cached_stateful_version(parent)
version.diff_refs
end
def image(parent:)
sha = cached_stateful_version(parent).sha
::Gitlab::Routing.url_helpers.project_design_url(design.project, design, sha)
end
def event(parent:)
version = cached_stateful_version(parent)
action = cached_actions_for_version(version)[design.id]
action&.event || ::Types::DesignManagement::DesignVersionEventEnum::NONE
end
def cached_actions_for_version(version)
Gitlab::SafeRequestStore.fetch(['DesignFields', 'actions_for_version', version.id]) do
version.actions.to_h { |dv| [dv.design_id, dv] }
end
end
def project
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Project, design.project_id).find
end
def issue
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Issue, design.issue_id).find
end
end
end
end
......@@ -7,53 +7,17 @@ module Types
authorize :read_design
implements(Types::Notes::NoteableType)
alias_method :design, :object
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the design'
field :project, Types::ProjectType, null: false,
description: 'Project associated with the design'
field :issue, Types::IssueType, null: false,
description: 'Issue associated with the design'
field :notes_count, GraphQL::INT_TYPE, null: false,
method: :user_notes_count,
description: 'Total count of user-created notes for the design'
field :filename, GraphQL::STRING_TYPE, null: false,
description: 'Filename of the design file'
field :full_path, GraphQL::STRING_TYPE, null: false,
description: 'Full path of the design file'
field :event, Types::DesignManagement::DesignVersionEventEnum, null: false,
description: 'Type of change made to the design at the version specified by the `atVersion` argument '\
'if supplied. Defaults to the latest version',
extras: [:parent]
field :image, GraphQL::STRING_TYPE, null: false,
description: 'Image of the design',
extras: [:parent]
field :diff_refs, Types::DiffRefsType, null: false,
description: 'Diff refs of the design',
calls_gitaly: true
implements(Types::Notes::NoteableType)
implements(Types::DesignManagement::DesignFields)
field :versions,
Types::DesignManagement::VersionType.connection_type,
resolver: Resolvers::DesignManagement::VersionResolver,
description: 'All versions related to the design, ordered newest first',
description: "All versions related to this design ordered newest first",
extras: [:parent]
def image(parent:)
sha = cached_stateful_version(parent).sha
Gitlab::Routing.url_helpers.project_design_url(design.project, design, sha)
end
def event(parent:)
version = cached_stateful_version(parent)
action = cached_actions_for_version(version)[design.id]
action&.event || Types::DesignManagement::DesignVersionEventEnum::NONE
end
# Returns a `DesignManagement::Version` for this query based on the
# `atVersion` argument passed to a parent node if present, or otherwise
# the most recent `Version` for the issue.
......@@ -71,12 +35,6 @@ module Types
end
end
def cached_actions_for_version(version)
Gitlab::SafeRequestStore.fetch([request_cache_base_key, 'actions_for_version', version.id]) do
version.actions.to_h { |dv| [dv.design_id, dv] }
end
end
def request_cache_base_key
self.class.name
end
......
......@@ -34,6 +34,14 @@ module DesignManagement
"#{design.id}.#{version.id}"
end
def ==(other)
return false unless other && self.class == other.class
other.id == id
end
alias_method :eql?, :==
def self.lazy_find(id)
BatchLoader.for(id).batch do |ids, callback|
find(ids).each do |record|
......
# frozen_string_literal: true
require 'spec_helper'
# describe GitlabSchema.types['DesignAtVersion'] do
# This not available on the schema until we mount it somewhere
describe ::Types::DesignManagement::DesignAtVersionType.to_graphql do
it_behaves_like 'a GraphQL type with design fields' do
let(:extra_design_fields) { %i[version design] }
let_it_be(:design) { create(:design, :with_versions) }
let(:object_id) do
version = design.versions.first
GitlabSchema.id_from_object(create(:design_at_version, design: design, version: version))
end
let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design_at_version)) }
let(:object_type) { ::Types::DesignManagement::DesignAtVersionType }
end
end
......@@ -3,26 +3,11 @@
require 'spec_helper'
describe GitlabSchema.types['Design'] do
it { expect(described_class).to require_graphql_authorizations(:read_design) }
it { expect(described_class.interfaces).to include(Types::Notes::NoteableType.to_graphql) }
it 'exposes the expected fields' do
expected_fields = %i[
diff_refs
discussions
event
filename
full_path
id
image
issue
notes
notes_count
project
versions
]
is_expected.to have_graphql_fields(*expected_fields)
it_behaves_like 'a GraphQL type with design fields' do
let(:extra_design_fields) { %i[notes discussions versions] }
let_it_be(:design) { create(:design, :with_versions) }
let(:object_id) { GitlabSchema.id_from_object(design) }
let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design, :with_versions)) }
let(:object_type) { ::Types::DesignManagement::DesignType }
end
end
......@@ -18,6 +18,59 @@ describe DesignManagement::DesignAtVersion do
end
end
describe '#==' do
it 'identifies objects created with the same parameters as equal' do
design = build_stubbed(:design, issue: issue)
version = build_stubbed(:design_version, designs: [design], issue: issue)
this = build_stubbed(:design_at_version, design: design, version: version)
other = build_stubbed(:design_at_version, design: design, version: version)
expect(this).to eq(other)
expect(other).to eq(this)
end
it 'identifies unequal objects as unequal, by virtue of their version' do
design = build_stubbed(:design, issue: issue)
version_a = build_stubbed(:design_version, designs: [design])
version_b = build_stubbed(:design_version, designs: [design])
this = build_stubbed(:design_at_version, design: design, version: version_a)
other = build_stubbed(:design_at_version, design: design, version: version_b)
expect(this).not_to eq(nil)
expect(this).not_to eq(design)
expect(this).not_to eq(other)
expect(other).not_to eq(this)
end
it 'identifies unequal objects as unequal, by virtue of their design' do
design_a = build_stubbed(:design, issue: issue)
design_b = build_stubbed(:design, issue: issue)
version = build_stubbed(:design_version, designs: [design_a, design_b])
this = build_stubbed(:design_at_version, design: design_a, version: version)
other = build_stubbed(:design_at_version, design: design_b, version: version)
expect(this).not_to eq(other)
expect(other).not_to eq(this)
end
it 'rejects objects with the same id and the wrong class' do
dav = build_stubbed(:design_at_version)
expect(dav).not_to eq(OpenStruct.new(id: dav.id))
end
it 'expects objects to be of the same type, not subtypes' do
subtype = Class.new(described_class)
dav = build_stubbed(:design_at_version)
other = subtype.new(design: dav.design, version: dav.version)
expect(dav).not_to eq(other)
end
end
describe 'status methods' do
let!(:design_a) { create(:design, issue: issue) }
let!(:design_b) { create(:design, issue: issue) }
......
# frozen_string_literal: true
# To use these shared examples, you may define a value in scope named
# `extra_design_fields`, to pass any extra fields in addition to the
# standard design fields.
shared_examples 'a GraphQL type with design fields' do
let(:extra_design_fields) { [] }
it { expect(described_class).to require_graphql_authorizations(:read_design) }
it 'exposes the expected design fields' do
expected_fields = %i[
id
project
issue
filename
full_path
image
diff_refs
event
notes_count
] + extra_design_fields
is_expected.to have_graphql_fields(*expected_fields).only
end
describe '#image' do
let(:schema) { GitlabSchema }
let(:query) { GraphQL::Query.new(schema) }
let(:context) { double('Context', schema: schema, query: query, parent: nil) }
let(:field) { described_class.fields['image'] }
let(:args) { GraphQL::Query::Arguments::NO_ARGS }
let(:instance) do
object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id))
object_type.authorized_new(object, query.context)
end
let(:instance_b) do
object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b))
object_type.authorized_new(object_b, query.context)
end
it 'resolves to the design image URL' do
image = field.resolve(instance, args, context)
sha = design.versions.first.sha
url = ::Gitlab::Routing.url_helpers.project_design_url(design.project, design, sha)
expect(image).to eq(url)
end
it 'has better than O(N) peformance', :request_store do
# Assuming designs have been loaded (as they must be), the following
# queries are required:
# For each distinct version:
# - design_management_versions
# (Request store is needed so that each version is fetched only once.)
# For each distinct issue
# - issues
# For each distinct project
# - projects
# - routes
# - namespaces
# Here that total is:
# - 2 x issues
# - 2 x versions
# - 2 x (projects + routes + namespaces)
# = 10
expect(instance).not_to eq(instance_b) # preload designs themselves.
expect do
image_a = field.resolve(instance, args, context)
image_b = field.resolve(instance, args, context)
image_c = field.resolve(instance_b, args, context)
image_d = field.resolve(instance_b, args, context)
expect(image_a).to eq(image_b)
expect(image_c).not_to eq(image_b)
expect(image_c).to eq(image_d)
end.not_to exceed_query_limit(10)
end
end
end
......@@ -124,14 +124,26 @@ describe GitlabSchema do
describe '.object_from_id' do
context 'for subclasses of `ApplicationRecord`' do
it 'returns the correct record' do
user = create(:user)
let_it_be(:user) { create(:user) }
it 'returns the correct record' do
result = described_class.object_from_id(user.to_global_id.to_s)
expect(result.sync).to eq(user)
end
it 'returns the correct record, of the expected type' do
result = described_class.object_from_id(user.to_global_id.to_s, expected_type: ::User)
expect(result.sync).to eq(user)
end
it 'fails if the type does not match' do
expect do
described_class.object_from_id(user.to_global_id.to_s, expected_type: ::Project)
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
it 'batchloads the queries' do
user1 = create(:user)
user2 = create(:user)
......
......@@ -7,7 +7,16 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata, :current_user, :snippets) }
it do
is_expected.to have_graphql_fields(:project,
:namespace,
:group,
:echo,
:metadata,
:current_user,
:snippets
).at_least
end
describe 'namespace field' do
subject { described_class.fields['namespace'] }
......
......@@ -105,12 +105,15 @@ module GraphqlHelpers
end
def query_graphql_field(name, attributes = {}, fields = nil)
fields ||= all_graphql_fields_for(name.classify)
attributes = attributes_to_graphql(attributes)
attributes = "(#{attributes})" if attributes.present?
field_params = if attributes.present?
"(#{attributes_to_graphql(attributes)})"
else
''
end
<<~QUERY
#{name}#{attributes}
#{wrap_fields(fields)}
#{GraphqlHelpers.fieldnamerize(name.to_s)}#{field_params}
#{wrap_fields(fields || all_graphql_fields_for(name.to_s.classify))}
QUERY
end
......@@ -301,6 +304,17 @@ module GraphqlHelpers
def global_id_of(model)
model.to_global_id.to_s
end
def missing_required_argument(path, argument)
a_hash_including(
'path' => ['query'].concat(path),
'extensions' => a_hash_including('code' => 'missingRequiredArguments', 'arguments' => argument.to_s)
)
end
def custom_graphql_error(path, msg)
a_hash_including('path' => path, 'message' => msg)
end
end
# This warms our schema, doing this as part of loading the helpers to avoid
......
......@@ -11,8 +11,22 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
Array.wrap(expected).map { |name| GraphqlHelpers.fieldnamerize(name) }
end
@allow_extra = false
chain :only do
@allow_extra = false
end
chain :at_least do
@allow_extra = true
end
match do |kls|
expect(kls.fields.keys).to contain_exactly(*expected_field_names)
if @allow_extra
expect(kls.fields.keys).to include(*expected_field_names)
else
expect(kls.fields.keys).to contain_exactly(*expected_field_names)
end
end
failure_message do |kls|
......@@ -22,7 +36,7 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
message = []
message << "is missing fields: <#{missing.inspect}>" if missing.any?
message << "contained unexpected fields: <#{extra.inspect}>" if extra.any?
message << "contained unexpected fields: <#{extra.inspect}>" if extra.any? && !@allow_extra
message.join("\n")
end
......
# frozen_string_literal: true
require 'spec_helper'
# Shared example for legal queries that are expected to return nil.
# Requires the following let bindings to be defined:
# - post_query: action to send the query
# - path: array of keys from query root to the result
shared_examples 'a failure to find anything' do
it 'finds nothing' do
post_query
data = graphql_data.dig(*path)
expect(data).to be_nil
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment