zenspec 0.2.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +405 -120
- data/examples/progress_loader_demo.rb +0 -0
- data/lib/zenspec/formatters/progress_bar_formatter.rb +15 -5
- data/lib/zenspec/matchers/graphql_matchers.rb +12 -2
- data/lib/zenspec/matchers/graphql_type_matchers.rb +127 -181
- data/lib/zenspec/matchers/interactor_matchers.rb +1 -1
- data/lib/zenspec/version.rb +1 -1
- metadata +1 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd3a368775608b8bae101c6e691275f3193ec2292c768c96219339dbcb6aed42
|
|
4
|
+
data.tar.gz: f7632304b8434c3f65e1b1b1a46a7900ce26eea59e732b1355fcd3007432269d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed10bcc9ebb95e02a3e43d404a4633b96b3363932c0c5430dd2d25c2b218162d12e33ccf5fcc078a809b24c53de2a5b774eb2fbd64fc68b5471c5aacdc348808
|
|
7
|
+
data.tar.gz: 67d338300f733a60ff08ee6c0fc26c5c0ffdfb53e200ab73ad369372f72f1bf4cfd20239deb8baa24bbdd509e5dc6dc5f0df3ee340365ce7e77809d1939198e1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2025-11-16
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Critical**: Fixed falsy value handling in `have_graphql_data` matcher - now correctly handles `false`, `0`, and empty string values
|
|
7
|
+
- **Critical**: Fixed falsy data value bug in interactor `succeed` matcher - now correctly handles `false` and `0` as expected data
|
|
8
|
+
- Improved snake_case to camelCase conversion to handle edge cases (leading/trailing underscores, consecutive underscores)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Extracted duplicated type conversion methods to shared `FieldNameHelper` module (reduces code duplication by ~120 lines)
|
|
12
|
+
- Pre-compiled ANSI regex in `ProgressBarFormatter` for better performance
|
|
13
|
+
- Made global `ProgressBarFormatter` constant conditional to prevent namespace conflicts
|
|
14
|
+
- Added documentation comments to magic numbers explaining their values
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Support for error count validation in `have_graphql_errors` matcher: `expect(result).to have_graphql_errors(2)`
|
|
18
|
+
- CI testing for Ruby 3.2, 3.3, and 3.4 (expanded from single version)
|
|
19
|
+
|
|
3
20
|
## [0.2.0] - 2025-11-16
|
|
4
21
|
|
|
5
22
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,54 +2,269 @@
|
|
|
2
2
|
|
|
3
3
|
A comprehensive RSpec matcher library for testing GraphQL APIs, Interactor service objects, and Rails applications.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Features:**
|
|
6
|
+
- GraphQL schema type matchers with snake_case support
|
|
7
|
+
- GraphQL response matchers for queries and mutations
|
|
8
|
+
- Interactor/service object matchers
|
|
9
|
+
- Shoulda matchers integration
|
|
10
|
+
- Beautiful progress bar formatter
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 30-Second Quickstart
|
|
8
15
|
|
|
9
16
|
```ruby
|
|
17
|
+
# 1. Add to Gemfile
|
|
10
18
|
gem "zenspec"
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
And then execute:
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
# 2. Bundle install
|
|
16
21
|
bundle install
|
|
22
|
+
|
|
23
|
+
# 3. Start testing!
|
|
24
|
+
RSpec.describe UserType do
|
|
25
|
+
# Test GraphQL types (supports snake_case!)
|
|
26
|
+
it { is_expected.to have_field(:id).of_type("ID!") }
|
|
27
|
+
it { is_expected.to have_field(:current_user).of_type("User") }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
RSpec.describe "GraphQL Queries" do
|
|
31
|
+
subject(:result) { graphql_execute(query) }
|
|
32
|
+
|
|
33
|
+
let(:query) do
|
|
34
|
+
<<~GQL
|
|
35
|
+
query { user(id: "123") { id name } }
|
|
36
|
+
GQL
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Test responses
|
|
40
|
+
it { is_expected.to succeed_graphql }
|
|
41
|
+
it { is_expected.to have_graphql_data("user", "name").with_value("John") }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
RSpec.describe CreateUser do
|
|
45
|
+
subject(:result) { described_class.call(name: "John") }
|
|
46
|
+
|
|
47
|
+
# Test interactors
|
|
48
|
+
it { is_expected.to succeed }
|
|
49
|
+
it { is_expected.to set_context(:user) }
|
|
50
|
+
end
|
|
17
51
|
```
|
|
18
52
|
|
|
19
|
-
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Complete Matchers Reference
|
|
56
|
+
|
|
57
|
+
### GraphQL Type Matchers
|
|
58
|
+
|
|
59
|
+
Test your GraphQL schema types, fields, and structure.
|
|
20
60
|
|
|
21
|
-
|
|
61
|
+
| Matcher | Usage | Description |
|
|
62
|
+
|---------|-------|-------------|
|
|
63
|
+
| `have_field(name)` | `have_field(:user)` | Check if type has a field (snake_case supported) |
|
|
64
|
+
| `.of_type(type)` | `.of_type("User!")` | Verify field return type |
|
|
65
|
+
| `.with_argument(name, type)` | `.with_argument(:id, "ID!")` | Check field has argument (snake_case supported) |
|
|
66
|
+
| `have_argument(name)` | `have_argument(:limit)` | Check argument exists on field |
|
|
67
|
+
| `.of_type(type)` | `.of_type("Int")` | Verify argument type |
|
|
68
|
+
| `have_enum_values(*values)` | `have_enum_values("ACTIVE", "PENDING")` | Verify enum contains values |
|
|
69
|
+
| `have_query(name)` | `have_query(:current_user)` | Check schema has query (snake_case supported) |
|
|
70
|
+
| `have_mutation(name)` | `have_mutation(:create_user)` | Check schema has mutation (snake_case supported) |
|
|
71
|
+
|
|
72
|
+
**Examples:**
|
|
22
73
|
|
|
23
74
|
```ruby
|
|
24
|
-
#
|
|
25
|
-
|
|
75
|
+
# Field testing (snake_case supported!)
|
|
76
|
+
expect(UserType).to have_field(:id).of_type("ID!")
|
|
77
|
+
expect(UserType).to have_field(:current_user).of_type("User") # Works with snake_case!
|
|
78
|
+
expect(UserType).to have_field(:posts).of_type("[Post!]!")
|
|
79
|
+
|
|
80
|
+
# Fields with arguments (snake_case supported!)
|
|
81
|
+
expect(UserType).to have_field(:posts)
|
|
82
|
+
.with_argument(:limit, "Int")
|
|
83
|
+
.with_argument(:include_archived, "Boolean") # snake_case works!
|
|
26
84
|
|
|
27
|
-
#
|
|
85
|
+
# Enum testing
|
|
86
|
+
expect(StatusEnum).to have_enum_values("ACTIVE", "INACTIVE", "PENDING")
|
|
87
|
+
|
|
88
|
+
# Schema queries (snake_case supported!)
|
|
89
|
+
expect(AppSchema).to have_query(:user).with_argument(:id, "ID!")
|
|
90
|
+
expect(AppSchema).to have_query(:current_user).of_type("User") # snake_case!
|
|
91
|
+
|
|
92
|
+
# Schema mutations (snake_case supported!)
|
|
93
|
+
expect(AppSchema).to have_mutation(:create_user).of_type("UserPayload!")
|
|
94
|
+
expect(AppSchema).to have_mutation(:update_user_status)
|
|
95
|
+
.with_argument(:user_id, "ID!") # snake_case arguments!
|
|
96
|
+
.with_argument(:new_status, "Status!")
|
|
28
97
|
```
|
|
29
98
|
|
|
30
|
-
###
|
|
99
|
+
### GraphQL Response Matchers
|
|
100
|
+
|
|
101
|
+
Test GraphQL query and mutation responses.
|
|
102
|
+
|
|
103
|
+
| Matcher | Usage | Description |
|
|
104
|
+
|---------|-------|-------------|
|
|
105
|
+
| `succeed_graphql` | `expect(result).to succeed_graphql` | Verify query/mutation succeeded (no errors) |
|
|
106
|
+
| `have_graphql_data(path...)` | `have_graphql_data("user", "name")` | Check response data at path |
|
|
107
|
+
| `.with_value(expected)` | `.with_value("John")` | Exact value match |
|
|
108
|
+
| `.matching(hash)` | `.matching(id: "123", name: "John")` | Partial hash match |
|
|
109
|
+
| `.that_includes(hash)` | `.that_includes(name: "John")` | Includes these key-value pairs |
|
|
110
|
+
| `.that_is_present` | `.that_is_present` | Value exists and not null |
|
|
111
|
+
| `.that_is_null` | `.that_is_null` | Value is explicitly null |
|
|
112
|
+
| `.with_count(n)` | `.with_count(5)` | Array has exactly n items |
|
|
113
|
+
| `have_graphql_error` | `have_graphql_error` | Has at least one error |
|
|
114
|
+
| `.with_message(msg)` | `.with_message("Not found")` | Error message matches |
|
|
115
|
+
| `.with_extensions(hash)` | `.with_extensions(code: "NOT_FOUND")` | Error extensions match |
|
|
116
|
+
| `.at_path(array)` | `.at_path(["user", "email"])` | Error occurred at path |
|
|
117
|
+
| `have_graphql_errors(n)` | `have_graphql_errors(2)` | Has exactly n errors |
|
|
118
|
+
| `have_graphql_field(name)` | `have_graphql_field("user")` | Response has field |
|
|
119
|
+
| `have_graphql_fields(hash)` | `have_graphql_fields("user" => true)` | Response has multiple fields |
|
|
120
|
+
|
|
121
|
+
**Examples:**
|
|
31
122
|
|
|
123
|
+
```ruby
|
|
124
|
+
# Basic success check
|
|
125
|
+
expect(result).to succeed_graphql
|
|
126
|
+
|
|
127
|
+
# Data checks
|
|
128
|
+
expect(result).to have_graphql_data("user")
|
|
129
|
+
expect(result).to have_graphql_data("user", "name").with_value("John")
|
|
130
|
+
expect(result).to have_graphql_data("user", "email").that_is_present
|
|
131
|
+
|
|
132
|
+
# Hash matching
|
|
133
|
+
expect(result).to have_graphql_data("user").matching(
|
|
134
|
+
id: "123",
|
|
135
|
+
name: "John Doe",
|
|
136
|
+
email: "john@example.com"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Array checks
|
|
140
|
+
expect(result).to have_graphql_data("users").with_count(5)
|
|
141
|
+
expect(result).to have_graphql_data("users").that_is_present
|
|
142
|
+
|
|
143
|
+
# Error handling
|
|
144
|
+
expect(result).to have_graphql_error
|
|
145
|
+
expect(result).to have_graphql_error.with_message("Not found")
|
|
146
|
+
expect(result).to have_graphql_error.with_extensions(code: "NOT_FOUND")
|
|
147
|
+
expect(result).to have_graphql_error.at_path(["user", "email"])
|
|
148
|
+
expect(result).to have_graphql_errors(2)
|
|
32
149
|
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
150
|
+
|
|
151
|
+
### Interactor Matchers
|
|
152
|
+
|
|
153
|
+
Test your Interactor service objects.
|
|
154
|
+
|
|
155
|
+
| Matcher | Usage | Description |
|
|
156
|
+
|---------|-------|-------------|
|
|
157
|
+
| `succeed` | `expect(result).to succeed` | Interactor succeeded |
|
|
158
|
+
| `.with_context(key, value)` | `.with_context(:user, user)` | Check context value |
|
|
159
|
+
| `.with_data(value)` | `.with_data(user)` | Check context.data value |
|
|
160
|
+
| `fail_interactor` | `expect(result).to fail_interactor` | Interactor failed |
|
|
161
|
+
| `.with_error(code)` | `.with_error("not_found")` | Failed with specific error code |
|
|
162
|
+
| `.with_errors(*codes)` | `.with_errors("invalid", "missing")` | Failed with multiple error codes |
|
|
163
|
+
| `set_context(key)` | `set_context(:user)` | Context key was set |
|
|
164
|
+
| `set_context(key, value)` | `set_context(:user, user)` | Context key set to value |
|
|
165
|
+
| `have_error_code(code)` | `have_error_code("not_found")` | Has specific error code |
|
|
166
|
+
| `have_error_codes(*codes)` | `have_error_codes("a", "b")` | Has multiple error codes |
|
|
167
|
+
|
|
168
|
+
**Examples:**
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
# Success checks
|
|
172
|
+
expect(result).to succeed
|
|
173
|
+
expect(result).to succeed.with_context(:user, kind_of(User))
|
|
174
|
+
expect(result).to succeed.with_data(user)
|
|
175
|
+
expect(result).to set_context(:user)
|
|
176
|
+
|
|
177
|
+
# Failure checks
|
|
178
|
+
expect(result).to fail_interactor
|
|
179
|
+
expect(result).to fail_interactor.with_error("validation_failed")
|
|
180
|
+
expect(result).to fail_interactor.with_errors("invalid_email", "missing_name")
|
|
181
|
+
expect(result).to have_error_code("not_found")
|
|
182
|
+
expect(result).to have_error_codes("invalid", "missing")
|
|
37
183
|
```
|
|
38
184
|
|
|
39
|
-
|
|
185
|
+
### GraphQL Helpers
|
|
186
|
+
|
|
187
|
+
Helper methods for executing GraphQL queries and mutations in tests.
|
|
188
|
+
|
|
189
|
+
| Helper | Usage | Description |
|
|
190
|
+
|--------|-------|-------------|
|
|
191
|
+
| `graphql_execute(query, **options)` | `graphql_execute(query, variables: {id: "123"})` | Execute GraphQL query |
|
|
192
|
+
| `graphql_execute_as(user, query, **options)` | `graphql_execute_as(user, query)` | Execute query with user in context |
|
|
193
|
+
| `graphql_mutate(mutation, **options)` | `graphql_mutate(mutation, input: {name: "John"})` | Execute GraphQL mutation |
|
|
194
|
+
| `graphql_mutate_as(user, mutation, **options)` | `graphql_mutate_as(user, mutation, input: {})` | Execute mutation with user in context |
|
|
195
|
+
|
|
196
|
+
**Examples:**
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
# Execute query
|
|
200
|
+
result = graphql_execute(query, variables: { id: "123" }, context: { current_user: user })
|
|
201
|
+
|
|
202
|
+
# Execute as user (adds to context automatically)
|
|
203
|
+
result = graphql_execute_as(user, query, variables: { id: "123" })
|
|
204
|
+
|
|
205
|
+
# Execute mutation
|
|
206
|
+
result = graphql_mutate(mutation, input: { name: "John" }, context: { current_user: user })
|
|
207
|
+
|
|
208
|
+
# Execute mutation as user
|
|
209
|
+
result = graphql_mutate_as(user, mutation, input: { name: "John" })
|
|
210
|
+
```
|
|
40
211
|
|
|
41
212
|
---
|
|
42
213
|
|
|
43
|
-
##
|
|
214
|
+
## Detailed Usage Examples
|
|
215
|
+
|
|
216
|
+
### Testing GraphQL Types
|
|
44
217
|
|
|
45
|
-
|
|
218
|
+
```ruby
|
|
219
|
+
RSpec.describe UserType do
|
|
220
|
+
subject { described_class }
|
|
46
221
|
|
|
47
|
-
|
|
222
|
+
# Basic fields (snake_case supported!)
|
|
223
|
+
it { is_expected.to have_field(:id).of_type("ID!") }
|
|
224
|
+
it { is_expected.to have_field(:name).of_type("String!") }
|
|
225
|
+
it { is_expected.to have_field(:email).of_type("String") }
|
|
226
|
+
it { is_expected.to have_field(:current_user).of_type("User") } # snake_case!
|
|
227
|
+
|
|
228
|
+
# Array types
|
|
229
|
+
it { is_expected.to have_field(:posts).of_type("[Post!]!") }
|
|
230
|
+
|
|
231
|
+
# Fields with arguments (snake_case supported!)
|
|
232
|
+
it do
|
|
233
|
+
is_expected.to have_field(:posts)
|
|
234
|
+
.with_argument(:limit, "Int")
|
|
235
|
+
.with_argument(:offset, "Int")
|
|
236
|
+
.with_argument(:include_archived, "Boolean") # snake_case works!
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
RSpec.describe StatusEnum do
|
|
241
|
+
subject { described_class }
|
|
242
|
+
|
|
243
|
+
it { is_expected.to have_enum_values("ACTIVE", "INACTIVE", "PENDING") }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
RSpec.describe AppSchema do
|
|
247
|
+
subject { described_class }
|
|
248
|
+
|
|
249
|
+
# Queries (snake_case supported!)
|
|
250
|
+
it { is_expected.to have_query(:user).with_argument(:id, "ID!") }
|
|
251
|
+
it { is_expected.to have_query(:current_user).of_type("User") }
|
|
252
|
+
|
|
253
|
+
# Mutations (snake_case supported!)
|
|
254
|
+
it { is_expected.to have_mutation(:create_user).of_type("UserPayload!") }
|
|
255
|
+
it { is_expected.to have_mutation(:update_user_status)
|
|
256
|
+
.with_argument(:user_id, "ID!")
|
|
257
|
+
.with_argument(:new_status, "Status!") }
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Testing GraphQL Queries
|
|
48
262
|
|
|
49
263
|
```ruby
|
|
50
264
|
RSpec.describe "User Queries" do
|
|
51
265
|
subject(:result) { graphql_execute_as(user, query, variables: { id: user.id }) }
|
|
52
266
|
|
|
267
|
+
let(:user) { create(:user, name: "John Doe", email: "john@example.com") }
|
|
53
268
|
let(:query) do
|
|
54
269
|
<<~GQL
|
|
55
270
|
query GetUser($id: ID!) {
|
|
@@ -65,9 +280,11 @@ RSpec.describe "User Queries" do
|
|
|
65
280
|
# Basic checks
|
|
66
281
|
it { is_expected.to succeed_graphql }
|
|
67
282
|
it { is_expected.to have_graphql_data("user") }
|
|
68
|
-
it { is_expected.to have_graphql_data("user", "
|
|
283
|
+
it { is_expected.to have_graphql_data("user", "id").with_value(user.id) }
|
|
284
|
+
it { is_expected.to have_graphql_data("user", "name").with_value("John Doe") }
|
|
285
|
+
it { is_expected.to have_graphql_data("user", "email").that_is_present }
|
|
69
286
|
|
|
70
|
-
#
|
|
287
|
+
# Full object matching
|
|
71
288
|
it "returns correct user data" do
|
|
72
289
|
expect(result).to have_graphql_data("user").matching(
|
|
73
290
|
id: user.id,
|
|
@@ -76,111 +293,113 @@ RSpec.describe "User Queries" do
|
|
|
76
293
|
)
|
|
77
294
|
end
|
|
78
295
|
|
|
79
|
-
#
|
|
80
|
-
it
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Error handling
|
|
84
|
-
expect(result).to have_graphql_error.with_message("Not found")
|
|
85
|
-
expect(result).to have_graphql_error.with_extensions(code: "NOT_FOUND")
|
|
86
|
-
expect(result).to have_graphql_error.at_path(["user", "email"])
|
|
296
|
+
# Partial matching
|
|
297
|
+
it "includes user name" do
|
|
298
|
+
expect(result).to have_graphql_data("user").that_includes(name: "John Doe")
|
|
299
|
+
end
|
|
87
300
|
end
|
|
88
|
-
```
|
|
89
301
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- `have_graphql_data(path...)` - Check response data
|
|
93
|
-
- `have_graphql_error` / `have_graphql_errors` - Verify errors
|
|
94
|
-
- `have_graphql_field(name)` / `have_graphql_fields(hash)` - Check field presence
|
|
302
|
+
RSpec.describe "User List Query" do
|
|
303
|
+
subject(:result) { graphql_execute(query) }
|
|
95
304
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
305
|
+
let!(:users) { create_list(:user, 5) }
|
|
306
|
+
let(:query) do
|
|
307
|
+
<<~GQL
|
|
308
|
+
query { users { id name } }
|
|
309
|
+
GQL
|
|
310
|
+
end
|
|
102
311
|
|
|
103
|
-
|
|
312
|
+
it { is_expected.to succeed_graphql }
|
|
313
|
+
it { is_expected.to have_graphql_data("users").with_count(5) }
|
|
314
|
+
it { is_expected.to have_graphql_data("users").that_is_present }
|
|
315
|
+
end
|
|
316
|
+
```
|
|
104
317
|
|
|
105
|
-
|
|
318
|
+
### Testing GraphQL Mutations
|
|
106
319
|
|
|
107
320
|
```ruby
|
|
108
|
-
RSpec.describe
|
|
109
|
-
subject
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
it { is_expected.to have_field(:id).of_type("ID!") }
|
|
113
|
-
it { is_expected.to have_field(:name).of_type("String!") }
|
|
114
|
-
it { is_expected.to have_field(:email).of_type("String") }
|
|
115
|
-
it { is_expected.to have_field(:posts).of_type("[Post!]!") }
|
|
321
|
+
RSpec.describe "Create User Mutation" do
|
|
322
|
+
subject(:result) do
|
|
323
|
+
graphql_mutate_as(admin, mutation, input: { name: "Jane", email: "jane@example.com" })
|
|
324
|
+
end
|
|
116
325
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
326
|
+
let(:admin) { create(:user, :admin) }
|
|
327
|
+
let(:mutation) do
|
|
328
|
+
<<~GQL
|
|
329
|
+
mutation CreateUser($input: CreateUserInput!) {
|
|
330
|
+
createUser(input: $input) {
|
|
331
|
+
user { id name email }
|
|
332
|
+
errors
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
GQL
|
|
122
336
|
end
|
|
123
|
-
end
|
|
124
337
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
338
|
+
context "with valid input" do
|
|
339
|
+
it { is_expected.to succeed_graphql }
|
|
340
|
+
it { is_expected.to have_graphql_data("createUser", "user", "name").with_value("Jane") }
|
|
341
|
+
it { is_expected.to have_graphql_data("createUser", "user", "email").with_value("jane@example.com") }
|
|
342
|
+
it { is_expected.to have_graphql_data("createUser", "errors").that_is_null }
|
|
128
343
|
|
|
129
|
-
|
|
130
|
-
|
|
344
|
+
it "creates a new user" do
|
|
345
|
+
expect { result }.to change(User, :count).by(1)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
131
348
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
349
|
+
context "with invalid input" do
|
|
350
|
+
subject(:result) do
|
|
351
|
+
graphql_mutate_as(admin, mutation, input: { name: "", email: "invalid" })
|
|
352
|
+
end
|
|
135
353
|
|
|
136
|
-
|
|
137
|
-
|
|
354
|
+
it { is_expected.to have_graphql_data("createUser", "user").that_is_null }
|
|
355
|
+
it { is_expected.to have_graphql_data("createUser", "errors").that_is_present }
|
|
356
|
+
end
|
|
138
357
|
end
|
|
139
358
|
```
|
|
140
359
|
|
|
141
|
-
|
|
142
|
-
- `have_field(name)` - Check field exists
|
|
143
|
-
- `of_type(type)` - Verify field type
|
|
144
|
-
- `with_argument(name, type)` - Check field arguments
|
|
145
|
-
- `have_argument(name)` - Check argument on field
|
|
146
|
-
- `have_enum_values(*values)` - Verify enum values
|
|
147
|
-
- `have_query(name)` / `have_mutation(name)` - Schema-level checks
|
|
148
|
-
|
|
149
|
-
### GraphQL Helpers
|
|
150
|
-
|
|
151
|
-
Execute queries and mutations easily in your tests.
|
|
360
|
+
### Testing GraphQL Errors
|
|
152
361
|
|
|
153
362
|
```ruby
|
|
154
|
-
|
|
155
|
-
result
|
|
363
|
+
RSpec.describe "User Query with errors" do
|
|
364
|
+
subject(:result) { graphql_execute(query, variables: { id: "999" }) }
|
|
156
365
|
|
|
157
|
-
|
|
158
|
-
|
|
366
|
+
let(:query) do
|
|
367
|
+
<<~GQL
|
|
368
|
+
query GetUser($id: ID!) {
|
|
369
|
+
user(id: $id) { id name }
|
|
370
|
+
}
|
|
371
|
+
GQL
|
|
372
|
+
end
|
|
159
373
|
|
|
160
|
-
|
|
161
|
-
|
|
374
|
+
it { is_expected.not_to succeed_graphql }
|
|
375
|
+
it { is_expected.to have_graphql_error }
|
|
376
|
+
it { is_expected.to have_graphql_error.with_message("User not found") }
|
|
377
|
+
it { is_expected.to have_graphql_error.with_extensions(code: "NOT_FOUND") }
|
|
378
|
+
it { is_expected.to have_graphql_error.at_path(["user"]) }
|
|
162
379
|
|
|
163
|
-
|
|
164
|
-
|
|
380
|
+
context "with multiple errors" do
|
|
381
|
+
it { is_expected.to have_graphql_errors(2) }
|
|
382
|
+
end
|
|
383
|
+
end
|
|
165
384
|
```
|
|
166
385
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
## Interactor Testing
|
|
170
|
-
|
|
171
|
-
Test your Interactor service objects with clean, expressive matchers.
|
|
386
|
+
### Testing Interactors
|
|
172
387
|
|
|
173
388
|
```ruby
|
|
174
389
|
RSpec.describe CreateUser do
|
|
175
390
|
subject(:result) { described_class.call(params) }
|
|
176
391
|
|
|
177
392
|
context "with valid params" do
|
|
178
|
-
let(:params) { { name: "John", email: "john@example.com" } }
|
|
393
|
+
let(:params) { { name: "John Doe", email: "john@example.com" } }
|
|
179
394
|
|
|
180
|
-
# Basic checks
|
|
181
395
|
it { is_expected.to succeed }
|
|
182
396
|
it { is_expected.to set_context(:user) }
|
|
183
397
|
it { is_expected.to succeed.with_context(:user, kind_of(User)) }
|
|
398
|
+
|
|
399
|
+
it "creates a user" do
|
|
400
|
+
expect(result.user).to be_persisted
|
|
401
|
+
expect(result.user.name).to eq("John Doe")
|
|
402
|
+
end
|
|
184
403
|
end
|
|
185
404
|
|
|
186
405
|
context "with invalid params" do
|
|
@@ -189,25 +408,39 @@ RSpec.describe CreateUser do
|
|
|
189
408
|
it { is_expected.to fail_interactor }
|
|
190
409
|
it { is_expected.to fail_interactor.with_error("validation_failed") }
|
|
191
410
|
it { is_expected.to have_error_code("validation_failed") }
|
|
411
|
+
|
|
412
|
+
it "does not create a user" do
|
|
413
|
+
expect { result }.not_to change(User, :count)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
context "with multiple errors" do
|
|
418
|
+
let(:params) { { name: "", email: "" } }
|
|
419
|
+
|
|
420
|
+
it { is_expected.to fail_interactor.with_errors("missing_name", "missing_email") }
|
|
421
|
+
it { is_expected.to have_error_codes("missing_name", "missing_email") }
|
|
192
422
|
end
|
|
193
423
|
end
|
|
194
|
-
```
|
|
195
424
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
- `.with_context(key, value)` - Check context value
|
|
199
|
-
- `.with_data(value)` - Check context.data value
|
|
200
|
-
- `fail_interactor` - Verify interactor failed
|
|
201
|
-
- `.with_error(code)` - Check single error code
|
|
202
|
-
- `.with_errors(*codes)` - Check multiple error codes
|
|
203
|
-
- `set_context(key, value)` - Check context was set
|
|
204
|
-
- `have_error_code(code)` / `have_error_codes(*codes)` - Check error codes
|
|
425
|
+
RSpec.describe UpdateUser do
|
|
426
|
+
subject(:result) { described_class.call(user: user, name: new_name) }
|
|
205
427
|
|
|
206
|
-
|
|
428
|
+
let(:user) { create(:user, name: "Old Name") }
|
|
429
|
+
let(:new_name) { "New Name" }
|
|
430
|
+
|
|
431
|
+
it { is_expected.to succeed }
|
|
432
|
+
it { is_expected.to succeed.with_context(:user, user) }
|
|
433
|
+
it { is_expected.to succeed.with_data(user) }
|
|
434
|
+
|
|
435
|
+
it "updates the user's name" do
|
|
436
|
+
expect { result }.to change { user.reload.name }.from("Old Name").to("New Name")
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
```
|
|
207
440
|
|
|
208
|
-
|
|
441
|
+
### Testing with Shoulda Matchers
|
|
209
442
|
|
|
210
|
-
|
|
443
|
+
Shoulda matchers are automatically included for Rails projects.
|
|
211
444
|
|
|
212
445
|
```ruby
|
|
213
446
|
RSpec.describe User do
|
|
@@ -216,26 +449,42 @@ RSpec.describe User do
|
|
|
216
449
|
# Validations
|
|
217
450
|
it { is_expected.to validate_presence_of(:email) }
|
|
218
451
|
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
|
|
452
|
+
it { is_expected.to validate_length_of(:name).is_at_least(2).is_at_most(100) }
|
|
219
453
|
|
|
220
454
|
# Associations
|
|
221
455
|
it { is_expected.to have_many(:posts).dependent(:destroy) }
|
|
222
456
|
it { is_expected.to belong_to(:organization) }
|
|
457
|
+
it { is_expected.to have_one(:profile) }
|
|
223
458
|
end
|
|
224
459
|
```
|
|
225
460
|
|
|
226
461
|
---
|
|
227
462
|
|
|
228
|
-
##
|
|
463
|
+
## Installation
|
|
229
464
|
|
|
230
|
-
|
|
465
|
+
Add this line to your application's Gemfile:
|
|
231
466
|
|
|
232
|
-
|
|
467
|
+
```ruby
|
|
468
|
+
gem "zenspec"
|
|
469
|
+
```
|
|
233
470
|
|
|
234
|
-
|
|
471
|
+
And then execute:
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
bundle install
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
That's it! All matchers and helpers are automatically available in your RSpec tests.
|
|
478
|
+
|
|
479
|
+
### Optional: Progress Bar Formatter
|
|
480
|
+
|
|
481
|
+
For a cleaner test output with a progress bar, add to your `.rspec` file:
|
|
235
482
|
|
|
236
483
|
```
|
|
237
484
|
--require zenspec/formatters/progress_bar_formatter
|
|
238
485
|
--format ProgressBarFormatter
|
|
486
|
+
--color
|
|
487
|
+
--require spec_helper
|
|
239
488
|
```
|
|
240
489
|
|
|
241
490
|
Or use via command line:
|
|
@@ -244,25 +493,28 @@ Or use via command line:
|
|
|
244
493
|
rspec --require zenspec/formatters/progress_bar_formatter --format ProgressBarFormatter
|
|
245
494
|
```
|
|
246
495
|
|
|
247
|
-
|
|
496
|
+
**Example output:**
|
|
248
497
|
|
|
249
498
|
```
|
|
250
|
-
✔ user_spec.rb [10%
|
|
251
|
-
✔ post_spec.rb [20%
|
|
252
|
-
⠿ auth_spec.rb --> authenticates with OAuth [30%
|
|
499
|
+
✔ user_spec.rb [10% 15/152]
|
|
500
|
+
✔ post_spec.rb [20% 30/152]
|
|
501
|
+
⠿ auth_spec.rb --> authenticates with OAuth [30% 45/152]
|
|
253
502
|
```
|
|
254
503
|
|
|
255
504
|
**Icons:**
|
|
256
505
|
- ✔ Green - Passed
|
|
257
506
|
- ✗ Red - Failed
|
|
258
|
-
- ⠿ Yellow - Running (shows test description)
|
|
507
|
+
- ⠿ Yellow - Running (shows current test description)
|
|
259
508
|
- ⊘ Cyan - Pending
|
|
260
509
|
|
|
261
510
|
---
|
|
262
511
|
|
|
263
512
|
## Configuration
|
|
264
513
|
|
|
265
|
-
Zenspec works out of the box with sensible defaults. For Rails applications, it automatically
|
|
514
|
+
Zenspec works out of the box with sensible defaults. For Rails applications, it automatically:
|
|
515
|
+
- Includes all matchers in RSpec
|
|
516
|
+
- Configures Shoulda Matchers
|
|
517
|
+
- Sets up GraphQL helpers with your `AppSchema`
|
|
266
518
|
|
|
267
519
|
### Non-Rails Projects
|
|
268
520
|
|
|
@@ -270,21 +522,54 @@ Zenspec works out of the box with sensible defaults. For Rails applications, it
|
|
|
270
522
|
# spec/spec_helper.rb
|
|
271
523
|
require "zenspec"
|
|
272
524
|
|
|
273
|
-
#
|
|
525
|
+
# All matchers and helpers are now available!
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Custom GraphQL Schema
|
|
529
|
+
|
|
530
|
+
If your schema is not named `AppSchema`, you can configure it:
|
|
531
|
+
|
|
532
|
+
```ruby
|
|
533
|
+
# spec/spec_helper.rb
|
|
534
|
+
RSpec.configure do |config|
|
|
535
|
+
config.include Zenspec::Helpers::GraphQLHelpers
|
|
536
|
+
|
|
537
|
+
# Override the schema
|
|
538
|
+
config.before do
|
|
539
|
+
stub_const("AppSchema", YourCustomSchema)
|
|
540
|
+
end
|
|
541
|
+
end
|
|
274
542
|
```
|
|
275
543
|
|
|
276
544
|
---
|
|
277
545
|
|
|
278
546
|
## Development
|
|
279
547
|
|
|
280
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then
|
|
548
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run the tests:
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
bundle exec rspec
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
To install this gem onto your local machine:
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
bundle exec rake install
|
|
558
|
+
```
|
|
281
559
|
|
|
282
|
-
|
|
560
|
+
---
|
|
283
561
|
|
|
284
562
|
## Contributing
|
|
285
563
|
|
|
286
564
|
Bug reports and pull requests are welcome on GitHub at https://github.com/zyxzen/zenspec.
|
|
287
565
|
|
|
566
|
+
Please ensure:
|
|
567
|
+
1. All tests pass (`bundle exec rspec`)
|
|
568
|
+
2. Code follows the existing style
|
|
569
|
+
3. New features include tests and documentation
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
288
573
|
## License
|
|
289
574
|
|
|
290
575
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
File without changes
|
|
@@ -56,15 +56,18 @@ module Zenspec
|
|
|
56
56
|
# Spinner frames for animation
|
|
57
57
|
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].freeze
|
|
58
58
|
|
|
59
|
-
# Spinner update interval in seconds
|
|
59
|
+
# Spinner update interval in seconds (80ms provides smooth 12.5 FPS animation)
|
|
60
60
|
SPINNER_INTERVAL = 0.08
|
|
61
61
|
|
|
62
|
-
# Default terminal width if unable to detect
|
|
62
|
+
# Default terminal width if unable to detect (standard wide terminal)
|
|
63
63
|
DEFAULT_TERMINAL_WIDTH = 120
|
|
64
64
|
|
|
65
|
-
# Minimum padding between left and right parts
|
|
65
|
+
# Minimum padding between left and right parts for readability
|
|
66
66
|
MIN_PADDING = 2
|
|
67
67
|
|
|
68
|
+
# Pre-compiled regex for stripping ANSI color codes (performance optimization)
|
|
69
|
+
ANSI_REGEX = /\e\[[0-9;]*m/.freeze
|
|
70
|
+
|
|
68
71
|
def initialize(output)
|
|
69
72
|
super
|
|
70
73
|
@current_count = 0
|
|
@@ -338,8 +341,9 @@ module Zenspec
|
|
|
338
341
|
end
|
|
339
342
|
|
|
340
343
|
# Strip ANSI color codes from text
|
|
344
|
+
# Uses pre-compiled regex for performance in hot path
|
|
341
345
|
def strip_ansi(text)
|
|
342
|
-
text.gsub(
|
|
346
|
+
text.gsub(ANSI_REGEX, "")
|
|
343
347
|
end
|
|
344
348
|
|
|
345
349
|
# Extract short filename from example
|
|
@@ -430,4 +434,10 @@ module Zenspec
|
|
|
430
434
|
end
|
|
431
435
|
|
|
432
436
|
# Register the formatter as ProgressBarFormatter for easier use
|
|
433
|
-
|
|
437
|
+
# Users can use the short form by requiring this file directly:
|
|
438
|
+
# --require zenspec/formatters/progress_bar_formatter
|
|
439
|
+
# --format ProgressBarFormatter
|
|
440
|
+
# Or use the full constant name: Zenspec::Formatters::ProgressBarFormatter
|
|
441
|
+
unless Object.const_defined?(:ProgressBarFormatter)
|
|
442
|
+
ProgressBarFormatter = Zenspec::Formatters::ProgressBarFormatter
|
|
443
|
+
end
|
|
@@ -141,7 +141,7 @@ module Zenspec
|
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
# Check exact value
|
|
144
|
-
|
|
144
|
+
unless @expected_value.nil?
|
|
145
145
|
data == @expected_value
|
|
146
146
|
else
|
|
147
147
|
!data.nil?
|
|
@@ -210,13 +210,15 @@ module Zenspec
|
|
|
210
210
|
#
|
|
211
211
|
# @example
|
|
212
212
|
# expect(result).to have_graphql_errors
|
|
213
|
+
# expect(result).to have_graphql_errors(2) # Exactly 2 errors
|
|
213
214
|
# expect(result).to have_graphql_error.with_message("Not found")
|
|
214
215
|
# expect(result).to have_graphql_error.with_extensions(code: "NOT_FOUND")
|
|
215
216
|
# expect(result).to have_graphql_error.at_path(["user", "email"])
|
|
216
217
|
# expect { execute_query }.to have_graphql_errors
|
|
217
218
|
#
|
|
218
|
-
RSpec::Matchers.define :have_graphql_errors do
|
|
219
|
+
RSpec::Matchers.define :have_graphql_errors do |expected_count = nil|
|
|
219
220
|
match do |result_or_block|
|
|
221
|
+
@expected_count = expected_count
|
|
220
222
|
if result_or_block.is_a?(Proc)
|
|
221
223
|
begin
|
|
222
224
|
result_or_block.call
|
|
@@ -232,6 +234,12 @@ module Zenspec
|
|
|
232
234
|
errors = result_or_block["errors"]
|
|
233
235
|
return false if errors.nil? || (errors.is_a?(Array) && errors.empty?)
|
|
234
236
|
|
|
237
|
+
# Check for exact error count if specified
|
|
238
|
+
if @expected_count
|
|
239
|
+
@actual_count = errors.is_a?(Array) ? errors.length : 0
|
|
240
|
+
return false unless @actual_count == @expected_count
|
|
241
|
+
end
|
|
242
|
+
|
|
235
243
|
# Check for specific message
|
|
236
244
|
if @expected_message
|
|
237
245
|
return errors.any? { |error| error["message"]&.include?(@expected_message) }
|
|
@@ -269,6 +277,8 @@ module Zenspec
|
|
|
269
277
|
failure_message do
|
|
270
278
|
if @raised_error
|
|
271
279
|
"expected no errors, but got: #{@error.message}"
|
|
280
|
+
elsif @expected_count
|
|
281
|
+
"expected exactly #{@expected_count} error(s), but got #{@actual_count}"
|
|
272
282
|
elsif @expected_message
|
|
273
283
|
errors = @result["errors"] || []
|
|
274
284
|
messages = errors.map { |e| e["message"] }
|
|
@@ -5,6 +5,118 @@ require "rspec/expectations"
|
|
|
5
5
|
module Zenspec
|
|
6
6
|
module Matchers
|
|
7
7
|
module GraphQLTypeMatchers
|
|
8
|
+
# Helper module for field name conversion
|
|
9
|
+
module FieldNameHelper
|
|
10
|
+
# Convert snake_case to camelCase for GraphQL field lookup
|
|
11
|
+
# GraphQL-Ruby automatically converts snake_case field definitions to camelCase
|
|
12
|
+
#
|
|
13
|
+
# Handles edge cases:
|
|
14
|
+
# - Empty strings: "" -> ""
|
|
15
|
+
# - No underscores: "foo" -> "foo"
|
|
16
|
+
# - Leading underscores: "_foo_bar" -> "fooBar"
|
|
17
|
+
# - Trailing underscores: "foo_bar_" -> "fooBar"
|
|
18
|
+
# - Consecutive underscores: "foo__bar" -> "fooBar"
|
|
19
|
+
def camelize(name)
|
|
20
|
+
str = name.to_s
|
|
21
|
+
return str if str.empty? || !str.include?("_")
|
|
22
|
+
|
|
23
|
+
# Split on underscores and reject empty parts (handles consecutive underscores)
|
|
24
|
+
parts = str.split("_").reject(&:empty?)
|
|
25
|
+
return str if parts.empty?
|
|
26
|
+
|
|
27
|
+
parts.map.with_index { |word, i| i.zero? ? word : word.capitalize }.join
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Try to get a field by name, attempting both the original name and camelCase version
|
|
31
|
+
def get_field_with_conversion(type, field_name)
|
|
32
|
+
field_name_str = field_name.to_s
|
|
33
|
+
|
|
34
|
+
# Try the exact name first (for backward compatibility)
|
|
35
|
+
field = get_field_exact(type, field_name_str)
|
|
36
|
+
return field if field
|
|
37
|
+
|
|
38
|
+
# Try the camelCase version if the original name contains underscores
|
|
39
|
+
if field_name_str.include?("_")
|
|
40
|
+
camel_name = camelize(field_name_str)
|
|
41
|
+
field = get_field_exact(type, camel_name)
|
|
42
|
+
return field if field
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def get_field_exact(type, field_name)
|
|
49
|
+
if type.respond_to?(:fields)
|
|
50
|
+
type.fields[field_name]
|
|
51
|
+
elsif type.respond_to?(:get_field)
|
|
52
|
+
type.get_field(field_name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Try to get an argument by name, attempting both the original name and camelCase version
|
|
57
|
+
def get_argument_with_conversion(field, arg_name)
|
|
58
|
+
return nil unless field.respond_to?(:arguments)
|
|
59
|
+
|
|
60
|
+
arg_name_str = arg_name.to_s
|
|
61
|
+
|
|
62
|
+
# Try the exact name first (for backward compatibility)
|
|
63
|
+
arg = field.arguments[arg_name_str]
|
|
64
|
+
return arg if arg
|
|
65
|
+
|
|
66
|
+
# Try the camelCase version if the original name contains underscores
|
|
67
|
+
if arg_name_str.include?("_")
|
|
68
|
+
camel_name = camelize(arg_name_str)
|
|
69
|
+
arg = field.arguments[camel_name]
|
|
70
|
+
return arg if arg
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Shared type conversion methods to avoid duplication across matchers
|
|
77
|
+
|
|
78
|
+
# Convert a GraphQL type object to its string representation
|
|
79
|
+
# Handles NonNull (!), List ([]), and nested types
|
|
80
|
+
def type_to_string(type)
|
|
81
|
+
case type
|
|
82
|
+
when GraphQL::Schema::NonNull
|
|
83
|
+
"#{type_to_string(type.of_type)}!"
|
|
84
|
+
when GraphQL::Schema::List
|
|
85
|
+
"[#{type_to_string(type.of_type)}]"
|
|
86
|
+
when GraphQL::Schema::Member
|
|
87
|
+
type.graphql_name
|
|
88
|
+
else
|
|
89
|
+
# Handle wrapped types from graphql-ruby
|
|
90
|
+
if type.respond_to?(:unwrap)
|
|
91
|
+
unwrapped = type.unwrap
|
|
92
|
+
base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
|
|
93
|
+
|
|
94
|
+
# Build the type string with wrappers
|
|
95
|
+
result = base_name
|
|
96
|
+
result = "[#{result}]" if list?(type)
|
|
97
|
+
result = "#{result}!" if non_null?(type)
|
|
98
|
+
result
|
|
99
|
+
elsif type.respond_to?(:graphql_name)
|
|
100
|
+
type.graphql_name
|
|
101
|
+
else
|
|
102
|
+
type.to_s
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if a type is a List type
|
|
108
|
+
def list?(type)
|
|
109
|
+
type.is_a?(GraphQL::Schema::List) ||
|
|
110
|
+
(type.respond_to?(:list?) && type.list?)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if a type is a NonNull type
|
|
114
|
+
def non_null?(type)
|
|
115
|
+
type.is_a?(GraphQL::Schema::NonNull) ||
|
|
116
|
+
(type.respond_to?(:non_null?) && type.non_null?)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
8
120
|
# Matcher for checking if a GraphQL type has a specific field with type and arguments
|
|
9
121
|
#
|
|
10
122
|
# @example
|
|
@@ -14,12 +126,14 @@ module Zenspec
|
|
|
14
126
|
# expect(UserType).to have_field(:posts).with_argument(:limit, "Int")
|
|
15
127
|
#
|
|
16
128
|
RSpec::Matchers.define :have_field do |field_name|
|
|
129
|
+
include FieldNameHelper
|
|
130
|
+
|
|
17
131
|
match do |type|
|
|
18
132
|
@field_name = field_name.to_s
|
|
19
133
|
@type = type
|
|
20
134
|
|
|
21
135
|
# Get the field from the type
|
|
22
|
-
@field =
|
|
136
|
+
@field = get_field_with_conversion(type, @field_name)
|
|
23
137
|
return false unless @field
|
|
24
138
|
|
|
25
139
|
# Check field type if specified
|
|
@@ -62,14 +176,6 @@ module Zenspec
|
|
|
62
176
|
|
|
63
177
|
private
|
|
64
178
|
|
|
65
|
-
def get_field(type, field_name)
|
|
66
|
-
if type.respond_to?(:fields)
|
|
67
|
-
type.fields[field_name]
|
|
68
|
-
elsif type.respond_to?(:get_field)
|
|
69
|
-
type.get_field(field_name)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
179
|
def field_type_string(field)
|
|
74
180
|
return nil unless field
|
|
75
181
|
|
|
@@ -77,47 +183,10 @@ module Zenspec
|
|
|
77
183
|
type_to_string(type)
|
|
78
184
|
end
|
|
79
185
|
|
|
80
|
-
def type_to_string(type)
|
|
81
|
-
case type
|
|
82
|
-
when GraphQL::Schema::NonNull
|
|
83
|
-
"#{type_to_string(type.of_type)}!"
|
|
84
|
-
when GraphQL::Schema::List
|
|
85
|
-
"[#{type_to_string(type.of_type)}]"
|
|
86
|
-
when GraphQL::Schema::Member
|
|
87
|
-
type.graphql_name
|
|
88
|
-
else
|
|
89
|
-
# Handle wrapped types from graphql-ruby
|
|
90
|
-
if type.respond_to?(:unwrap)
|
|
91
|
-
unwrapped = type.unwrap
|
|
92
|
-
base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
|
|
93
|
-
|
|
94
|
-
# Build the type string with wrappers
|
|
95
|
-
result = base_name
|
|
96
|
-
result = "[#{result}]" if list?(type)
|
|
97
|
-
result = "#{result}!" if non_null?(type)
|
|
98
|
-
result
|
|
99
|
-
elsif type.respond_to?(:graphql_name)
|
|
100
|
-
type.graphql_name
|
|
101
|
-
else
|
|
102
|
-
type.to_s
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def list?(type)
|
|
108
|
-
type.is_a?(GraphQL::Schema::List) ||
|
|
109
|
-
(type.respond_to?(:list?) && type.list?)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def non_null?(type)
|
|
113
|
-
type.is_a?(GraphQL::Schema::NonNull) ||
|
|
114
|
-
(type.respond_to?(:non_null?) && type.non_null?)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
186
|
def check_argument(arg_name, expected_type)
|
|
118
187
|
return false unless @field.respond_to?(:arguments)
|
|
119
188
|
|
|
120
|
-
arg = @field
|
|
189
|
+
arg = get_argument_with_conversion(@field, arg_name)
|
|
121
190
|
return false unless arg
|
|
122
191
|
|
|
123
192
|
if expected_type
|
|
@@ -206,12 +275,14 @@ module Zenspec
|
|
|
206
275
|
# expect(QueryType.fields["users"]).to have_argument(:filter).of_type("UserFilterInput")
|
|
207
276
|
#
|
|
208
277
|
RSpec::Matchers.define :have_argument do |arg_name|
|
|
278
|
+
include FieldNameHelper
|
|
279
|
+
|
|
209
280
|
match do |field|
|
|
210
281
|
@field = field
|
|
211
282
|
@arg_name = arg_name.to_s
|
|
212
283
|
|
|
213
284
|
# Get the argument
|
|
214
|
-
@argument =
|
|
285
|
+
@argument = get_argument_with_conversion(field, @arg_name)
|
|
215
286
|
return false unless @argument
|
|
216
287
|
|
|
217
288
|
# Check argument type if specified
|
|
@@ -239,55 +310,12 @@ module Zenspec
|
|
|
239
310
|
|
|
240
311
|
private
|
|
241
312
|
|
|
242
|
-
def get_argument(field, arg_name)
|
|
243
|
-
return nil unless field.respond_to?(:arguments)
|
|
244
|
-
|
|
245
|
-
field.arguments[arg_name]
|
|
246
|
-
end
|
|
247
|
-
|
|
248
313
|
def argument_type_string(argument)
|
|
249
314
|
return nil unless argument
|
|
250
315
|
|
|
251
316
|
type = argument.type
|
|
252
317
|
type_to_string(type)
|
|
253
318
|
end
|
|
254
|
-
|
|
255
|
-
def type_to_string(type)
|
|
256
|
-
case type
|
|
257
|
-
when GraphQL::Schema::NonNull
|
|
258
|
-
"#{type_to_string(type.of_type)}!"
|
|
259
|
-
when GraphQL::Schema::List
|
|
260
|
-
"[#{type_to_string(type.of_type)}]"
|
|
261
|
-
when GraphQL::Schema::Member
|
|
262
|
-
type.graphql_name
|
|
263
|
-
else
|
|
264
|
-
# Handle wrapped types from graphql-ruby
|
|
265
|
-
if type.respond_to?(:unwrap)
|
|
266
|
-
unwrapped = type.unwrap
|
|
267
|
-
base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
|
|
268
|
-
|
|
269
|
-
# Build the type string with wrappers
|
|
270
|
-
result = base_name
|
|
271
|
-
result = "[#{result}]" if list?(type)
|
|
272
|
-
result = "#{result}!" if non_null?(type)
|
|
273
|
-
result
|
|
274
|
-
elsif type.respond_to?(:graphql_name)
|
|
275
|
-
type.graphql_name
|
|
276
|
-
else
|
|
277
|
-
type.to_s
|
|
278
|
-
end
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def list?(type)
|
|
283
|
-
type.is_a?(GraphQL::Schema::List) ||
|
|
284
|
-
(type.respond_to?(:list?) && type.list?)
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def non_null?(type)
|
|
288
|
-
type.is_a?(GraphQL::Schema::NonNull) ||
|
|
289
|
-
(type.respond_to?(:non_null?) && type.non_null?)
|
|
290
|
-
end
|
|
291
319
|
end
|
|
292
320
|
|
|
293
321
|
# Matcher for checking if a schema has a specific query field
|
|
@@ -297,6 +325,8 @@ module Zenspec
|
|
|
297
325
|
# expect(schema).to have_query(:users).of_type("[User!]!")
|
|
298
326
|
#
|
|
299
327
|
RSpec::Matchers.define :have_query do |query_name|
|
|
328
|
+
include FieldNameHelper
|
|
329
|
+
|
|
300
330
|
match do |schema|
|
|
301
331
|
@schema = schema
|
|
302
332
|
@query_name = query_name.to_s
|
|
@@ -306,7 +336,7 @@ module Zenspec
|
|
|
306
336
|
return false unless query_type
|
|
307
337
|
|
|
308
338
|
# Get the field
|
|
309
|
-
@field =
|
|
339
|
+
@field = get_field_with_conversion(query_type, @query_name)
|
|
310
340
|
return false unless @field
|
|
311
341
|
|
|
312
342
|
# Check field type if specified
|
|
@@ -353,14 +383,6 @@ module Zenspec
|
|
|
353
383
|
schema.respond_to?(:query) ? schema.query : nil
|
|
354
384
|
end
|
|
355
385
|
|
|
356
|
-
def get_field(type, field_name)
|
|
357
|
-
if type.respond_to?(:fields)
|
|
358
|
-
type.fields[field_name]
|
|
359
|
-
elsif type.respond_to?(:get_field)
|
|
360
|
-
type.get_field(field_name)
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
|
|
364
386
|
def field_type_string(field)
|
|
365
387
|
return nil unless field
|
|
366
388
|
|
|
@@ -368,45 +390,10 @@ module Zenspec
|
|
|
368
390
|
type_to_string(type)
|
|
369
391
|
end
|
|
370
392
|
|
|
371
|
-
def type_to_string(type)
|
|
372
|
-
case type
|
|
373
|
-
when GraphQL::Schema::NonNull
|
|
374
|
-
"#{type_to_string(type.of_type)}!"
|
|
375
|
-
when GraphQL::Schema::List
|
|
376
|
-
"[#{type_to_string(type.of_type)}]"
|
|
377
|
-
when GraphQL::Schema::Member
|
|
378
|
-
type.graphql_name
|
|
379
|
-
else
|
|
380
|
-
if type.respond_to?(:unwrap)
|
|
381
|
-
unwrapped = type.unwrap
|
|
382
|
-
base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
|
|
383
|
-
|
|
384
|
-
result = base_name
|
|
385
|
-
result = "[#{result}]" if list?(type)
|
|
386
|
-
result = "#{result}!" if non_null?(type)
|
|
387
|
-
result
|
|
388
|
-
elsif type.respond_to?(:graphql_name)
|
|
389
|
-
type.graphql_name
|
|
390
|
-
else
|
|
391
|
-
type.to_s
|
|
392
|
-
end
|
|
393
|
-
end
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
def list?(type)
|
|
397
|
-
type.is_a?(GraphQL::Schema::List) ||
|
|
398
|
-
(type.respond_to?(:list?) && type.list?)
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def non_null?(type)
|
|
402
|
-
type.is_a?(GraphQL::Schema::NonNull) ||
|
|
403
|
-
(type.respond_to?(:non_null?) && type.non_null?)
|
|
404
|
-
end
|
|
405
|
-
|
|
406
393
|
def check_argument(arg_name, expected_type)
|
|
407
394
|
return false unless @field.respond_to?(:arguments)
|
|
408
395
|
|
|
409
|
-
arg = @field
|
|
396
|
+
arg = get_argument_with_conversion(@field, arg_name)
|
|
410
397
|
return false unless arg
|
|
411
398
|
|
|
412
399
|
if expected_type
|
|
@@ -425,6 +412,8 @@ module Zenspec
|
|
|
425
412
|
# expect(schema).to have_mutation(:deleteUser).of_type("DeleteUserPayload!")
|
|
426
413
|
#
|
|
427
414
|
RSpec::Matchers.define :have_mutation do |mutation_name|
|
|
415
|
+
include FieldNameHelper
|
|
416
|
+
|
|
428
417
|
match do |schema|
|
|
429
418
|
@schema = schema
|
|
430
419
|
@mutation_name = mutation_name.to_s
|
|
@@ -434,7 +423,7 @@ module Zenspec
|
|
|
434
423
|
return false unless mutation_type
|
|
435
424
|
|
|
436
425
|
# Get the field
|
|
437
|
-
@field =
|
|
426
|
+
@field = get_field_with_conversion(mutation_type, @mutation_name)
|
|
438
427
|
return false unless @field
|
|
439
428
|
|
|
440
429
|
# Check field type if specified
|
|
@@ -481,14 +470,6 @@ module Zenspec
|
|
|
481
470
|
schema.respond_to?(:mutation) ? schema.mutation : nil
|
|
482
471
|
end
|
|
483
472
|
|
|
484
|
-
def get_field(type, field_name)
|
|
485
|
-
if type.respond_to?(:fields)
|
|
486
|
-
type.fields[field_name]
|
|
487
|
-
elsif type.respond_to?(:get_field)
|
|
488
|
-
type.get_field(field_name)
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
473
|
def field_type_string(field)
|
|
493
474
|
return nil unless field
|
|
494
475
|
|
|
@@ -496,45 +477,10 @@ module Zenspec
|
|
|
496
477
|
type_to_string(type)
|
|
497
478
|
end
|
|
498
479
|
|
|
499
|
-
def type_to_string(type)
|
|
500
|
-
case type
|
|
501
|
-
when GraphQL::Schema::NonNull
|
|
502
|
-
"#{type_to_string(type.of_type)}!"
|
|
503
|
-
when GraphQL::Schema::List
|
|
504
|
-
"[#{type_to_string(type.of_type)}]"
|
|
505
|
-
when GraphQL::Schema::Member
|
|
506
|
-
type.graphql_name
|
|
507
|
-
else
|
|
508
|
-
if type.respond_to?(:unwrap)
|
|
509
|
-
unwrapped = type.unwrap
|
|
510
|
-
base_name = unwrapped.respond_to?(:graphql_name) ? unwrapped.graphql_name : unwrapped.to_s
|
|
511
|
-
|
|
512
|
-
result = base_name
|
|
513
|
-
result = "[#{result}]" if list?(type)
|
|
514
|
-
result = "#{result}!" if non_null?(type)
|
|
515
|
-
result
|
|
516
|
-
elsif type.respond_to?(:graphql_name)
|
|
517
|
-
type.graphql_name
|
|
518
|
-
else
|
|
519
|
-
type.to_s
|
|
520
|
-
end
|
|
521
|
-
end
|
|
522
|
-
end
|
|
523
|
-
|
|
524
|
-
def list?(type)
|
|
525
|
-
type.is_a?(GraphQL::Schema::List) ||
|
|
526
|
-
(type.respond_to?(:list?) && type.list?)
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
def non_null?(type)
|
|
530
|
-
type.is_a?(GraphQL::Schema::NonNull) ||
|
|
531
|
-
(type.respond_to?(:non_null?) && type.non_null?)
|
|
532
|
-
end
|
|
533
|
-
|
|
534
480
|
def check_argument(arg_name, expected_type)
|
|
535
481
|
return false unless @field.respond_to?(:arguments)
|
|
536
482
|
|
|
537
|
-
arg = @field
|
|
483
|
+
arg = get_argument_with_conversion(@field, arg_name)
|
|
538
484
|
return false unless arg
|
|
539
485
|
|
|
540
486
|
if expected_type
|
|
@@ -18,7 +18,7 @@ module Zenspec
|
|
|
18
18
|
@result = get_result(actual)
|
|
19
19
|
return false if @result.failure?
|
|
20
20
|
|
|
21
|
-
return false if
|
|
21
|
+
return false if !@expected_data.nil? && @result.data != @expected_data
|
|
22
22
|
|
|
23
23
|
if @context_checks
|
|
24
24
|
@context_checks.all? do |key, value|
|
data/lib/zenspec/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zenspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wilson Anciro
|
|
@@ -97,7 +97,6 @@ homepage: https://github.com/zyxzen/zenspec
|
|
|
97
97
|
licenses:
|
|
98
98
|
- MIT
|
|
99
99
|
metadata:
|
|
100
|
-
homepage_uri: https://github.com/zyxzen/zenspec
|
|
101
100
|
source_code_uri: https://github.com/zyxzen/zenspec
|
|
102
101
|
changelog_uri: https://github.com/zyxzen/zenspec/blob/main/CHANGELOG.md
|
|
103
102
|
rubygems_mfa_required: 'true'
|