rspec-sse-matchers 0.1.2 → 0.2.0
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/README.md +45 -0
- data/examples/custom_matchers_example.rb +134 -0
- data/lib/rspec/sse/matchers/version.rb +1 -1
- data/lib/rspec/sse/matchers.rb +54 -3
- data/sig/generated/rspec/sse/matchers.rbs +16 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e06b785e8c91c1cc5fa04b08dfaf81a9a33d3e750bf9b52bd6b417db55e01ab
|
4
|
+
data.tar.gz: e16dde236261e02ff67d80e4deb1fc8ec1847dd5e432b193918c186f10487c59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f14b23281a644ca36dd8a636ed43a594954e4a7095a1f30debd81c82e4d718a1792ed77711e71ea9147f2e41b409e002a132e70f9b29471d42836bdb350a45ff
|
7
|
+
data.tar.gz: bc1ff76aa4c587d292cc329686d0c61e04d7e854a50c966f6d315af2166e8da572f0d84788f3899dbd3a52bf794a49960705034fc884bde168326cdf3d052999
|
data/README.md
CHANGED
@@ -127,6 +127,51 @@ expect(response).to be_sse_events([
|
|
127
127
|
|
128
128
|
When the `json: true` option is enabled, the matcher attempts to parse each event's `data` as JSON. If parsing fails (the data is not valid JSON), it raises an error.
|
129
129
|
|
130
|
+
### Using Custom RSpec Matchers
|
131
|
+
|
132
|
+
The SSE matchers now support RSpec custom matchers like `hash_including`, `a_kind_of`, `be_an`, etc. This allows for more flexible matching when testing SSE events with complex data structures:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# With hash_including for partial matching
|
136
|
+
expect(response).to be_sse_events([
|
137
|
+
{type: "in_progress", data: {"event" => "in_progress", "data" => {}}, id: "1", retry: 250},
|
138
|
+
hash_including(
|
139
|
+
type: "finished",
|
140
|
+
data: hash_including(
|
141
|
+
"event" => "finished",
|
142
|
+
"data" => hash_including(
|
143
|
+
"object_id" => a_kind_of(String), # or a_string_starting_with("prefix_")
|
144
|
+
"results" => be_an(Array)
|
145
|
+
)
|
146
|
+
),
|
147
|
+
id: "2",
|
148
|
+
retry: 250
|
149
|
+
)
|
150
|
+
], json: true)
|
151
|
+
|
152
|
+
# With type checking matchers
|
153
|
+
expect(response).to be_sse_event_data([
|
154
|
+
hash_including("id" => a_kind_of(Integer), "name" => a_kind_of(String)),
|
155
|
+
hash_including("status" => match(/active|pending/))
|
156
|
+
], json: true)
|
157
|
+
|
158
|
+
# Works with all matcher types
|
159
|
+
expect(response).to contain_exactly_sse_events([
|
160
|
+
hash_including(data: hash_including("status" => "pending")),
|
161
|
+
hash_including(data: hash_including("status" => "active"))
|
162
|
+
], json: true)
|
163
|
+
|
164
|
+
expect(response).to have_sse_events([
|
165
|
+
hash_including(data: hash_including("important_field" => "value"))
|
166
|
+
], json: true)
|
167
|
+
```
|
168
|
+
|
169
|
+
Custom matchers are particularly useful when:
|
170
|
+
- You only care about specific fields in the event data
|
171
|
+
- You want to match patterns or types rather than exact values
|
172
|
+
- You need to test complex nested structures
|
173
|
+
- You want to ignore irrelevant fields
|
174
|
+
|
130
175
|
## Examples
|
131
176
|
|
132
177
|
### Testing Event Types
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rspec"
|
4
|
+
require "rspec/sse/matchers"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
# Mock response for demonstration
|
8
|
+
class MockResponse
|
9
|
+
attr_reader :body, :headers, :status
|
10
|
+
def initialize(body:, status: 200, headers: {"content-type" => "text/event-stream", "cache-control" => "no-store"})
|
11
|
+
@body = body
|
12
|
+
@status = status
|
13
|
+
@headers = headers
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_sse_body(events)
|
18
|
+
body = ""
|
19
|
+
events.each do |event|
|
20
|
+
body += "id: #{event[:id]}\n" if event[:id]
|
21
|
+
body += "event: #{event[:type]}\n" if event[:type]
|
22
|
+
body += "data: #{event[:data]}\n" if event[:data]
|
23
|
+
body += "retry: #{event[:retry]}\n" if event[:retry]
|
24
|
+
body += "\n"
|
25
|
+
end
|
26
|
+
body
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec.describe "SSE with custom matchers" do
|
30
|
+
include RSpec::Matchers
|
31
|
+
|
32
|
+
describe "example from the original request" do
|
33
|
+
let(:event1) { {type: "in_progress", data: '{"event":"in_progress","data":{}}', id: "", retry: 250} }
|
34
|
+
let(:event2) { {type: "finished", data: '{"event":"finished","data":{"object_id":"special_prefix_12345","results":[1,2,3]}}', id: "", retry: 250} }
|
35
|
+
let(:response) { MockResponse.new(body: create_sse_body([event1, event2])) }
|
36
|
+
|
37
|
+
it "matches SSE events with custom matchers" do
|
38
|
+
expect(response).to be_sse_events([
|
39
|
+
{type: "in_progress", data: {"event" => "in_progress", "data" => {}}, id: "", retry: 250},
|
40
|
+
hash_including(
|
41
|
+
type: "finished",
|
42
|
+
data: hash_including(
|
43
|
+
"event" => "finished",
|
44
|
+
"data" => hash_including(
|
45
|
+
"object_id" => a_kind_of(String), # or a_string_starting_with("special_prefix_")
|
46
|
+
"results" => be_an(Array)
|
47
|
+
)
|
48
|
+
),
|
49
|
+
id: "",
|
50
|
+
retry: 250
|
51
|
+
)
|
52
|
+
], json: true)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "other examples" do
|
57
|
+
context "partial matching with hash_including" do
|
58
|
+
let(:events) {
|
59
|
+
[
|
60
|
+
{type: "user_update", data: '{"user_id":123,"name":"John","email":"john@example.com","status":"active"}', id: "1", retry: 1000}
|
61
|
+
]
|
62
|
+
}
|
63
|
+
let(:response) { MockResponse.new(body: create_sse_body(events)) }
|
64
|
+
|
65
|
+
it "matches only specific fields" do
|
66
|
+
expect(response).to be_sse_events([
|
67
|
+
hash_including(
|
68
|
+
type: "user_update",
|
69
|
+
data: hash_including(
|
70
|
+
"user_id" => 123,
|
71
|
+
"status" => "active"
|
72
|
+
# Other fields are ignored
|
73
|
+
)
|
74
|
+
)
|
75
|
+
], json: true)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "type checking with RSpec matchers" do
|
80
|
+
let(:events) {
|
81
|
+
[
|
82
|
+
{type: "data_point", data: '{"value":42.5,"timestamp":"2023-01-01T10:00:00Z","tags":["sensor","temperature"]}', id: "2", retry: 500}
|
83
|
+
]
|
84
|
+
}
|
85
|
+
let(:response) { MockResponse.new(body: create_sse_body(events)) }
|
86
|
+
|
87
|
+
it "validates data types" do
|
88
|
+
expect(response).to be_sse_events([
|
89
|
+
hash_including(
|
90
|
+
type: "data_point",
|
91
|
+
data: hash_including(
|
92
|
+
"value" => a_kind_of(Numeric),
|
93
|
+
"timestamp" => match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/),
|
94
|
+
"tags" => include("sensor")
|
95
|
+
)
|
96
|
+
)
|
97
|
+
], json: true)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "array matching" do
|
102
|
+
let(:events) {
|
103
|
+
[
|
104
|
+
{type: "batch", data: '{"items":[{"id":1,"status":"ok"},{"id":2,"status":"error"},{"id":3,"status":"ok"}]}', id: "3", retry: 1000}
|
105
|
+
]
|
106
|
+
}
|
107
|
+
let(:response) { MockResponse.new(body: create_sse_body(events)) }
|
108
|
+
|
109
|
+
it "matches arrays with custom expectations" do
|
110
|
+
expect(response).to be_sse_events([
|
111
|
+
hash_including(
|
112
|
+
type: "batch",
|
113
|
+
data: hash_including(
|
114
|
+
"items" => include(
|
115
|
+
hash_including("id" => 2, "status" => "error")
|
116
|
+
)
|
117
|
+
)
|
118
|
+
)
|
119
|
+
], json: true)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "matches array properties" do
|
123
|
+
expect(response).to be_sse_events([
|
124
|
+
hash_including(
|
125
|
+
type: "batch",
|
126
|
+
data: hash_including(
|
127
|
+
"items" => have_attributes(size: 3)
|
128
|
+
)
|
129
|
+
)
|
130
|
+
], json: true)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/rspec/sse/matchers.rb
CHANGED
@@ -187,6 +187,40 @@ module RSpec
|
|
187
187
|
@json = json
|
188
188
|
end
|
189
189
|
|
190
|
+
# Check if individual items match
|
191
|
+
# Supports RSpec matchers as expected values
|
192
|
+
#
|
193
|
+
# @rbs actual_item: Object
|
194
|
+
# @rbs expected_item: Object
|
195
|
+
# @rbs return: bool
|
196
|
+
def match_items(actual_item, expected_item)
|
197
|
+
if expected_item.respond_to?(:===)
|
198
|
+
# It's an RSpec argument matcher (hash_including, etc.)
|
199
|
+
expected_item === actual_item
|
200
|
+
elsif expected_item.respond_to?(:matches?)
|
201
|
+
# It's an RSpec regular matcher
|
202
|
+
expected_item.matches?(actual_item)
|
203
|
+
else
|
204
|
+
actual_item == expected_item
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Check if two arrays match element by element
|
209
|
+
# Supports RSpec matchers as expected values
|
210
|
+
#
|
211
|
+
# @rbs actual_array: Array[Object]
|
212
|
+
# @rbs expected_array: Array[Object]
|
213
|
+
# @rbs return: bool
|
214
|
+
def match_arrays(actual_array, expected_array)
|
215
|
+
return false unless actual_array.size == expected_array.size
|
216
|
+
|
217
|
+
actual_array.each_with_index do |actual_item, index|
|
218
|
+
expected_item = expected_array[index]
|
219
|
+
return false unless match_items(actual_item, expected_item)
|
220
|
+
end
|
221
|
+
true
|
222
|
+
end
|
223
|
+
|
190
224
|
# @rbs actual: Object
|
191
225
|
# @rbs return: bool
|
192
226
|
def matches?(actual)
|
@@ -302,7 +336,7 @@ module RSpec
|
|
302
336
|
#
|
303
337
|
# @rbs return: bool
|
304
338
|
def match_condition
|
305
|
-
extract_actual
|
339
|
+
match_arrays(extract_actual, @expected)
|
306
340
|
end
|
307
341
|
|
308
342
|
# @rbs return: String
|
@@ -317,7 +351,22 @@ module RSpec
|
|
317
351
|
#
|
318
352
|
# @rbs return: bool
|
319
353
|
def match_condition
|
320
|
-
|
354
|
+
return false unless extract_actual.size == @expected.size
|
355
|
+
|
356
|
+
matched_indices = {}
|
357
|
+
@expected.each do |expected_item|
|
358
|
+
matched = false
|
359
|
+
extract_actual.each_with_index do |actual_item, index|
|
360
|
+
next if matched_indices[index]
|
361
|
+
if match_items(actual_item, expected_item)
|
362
|
+
matched_indices[index] = true
|
363
|
+
matched = true
|
364
|
+
break
|
365
|
+
end
|
366
|
+
end
|
367
|
+
return false unless matched
|
368
|
+
end
|
369
|
+
true
|
321
370
|
end
|
322
371
|
|
323
372
|
# @rbs return: String
|
@@ -332,7 +381,9 @@ module RSpec
|
|
332
381
|
#
|
333
382
|
# @rbs return: bool
|
334
383
|
def match_condition
|
335
|
-
@expected.all?
|
384
|
+
@expected.all? do |expected_item|
|
385
|
+
extract_actual.any? { |actual_item| match_items(actual_item, expected_item) }
|
386
|
+
end
|
336
387
|
end
|
337
388
|
|
338
389
|
# @rbs return: String
|
@@ -133,6 +133,22 @@ module RSpec
|
|
133
133
|
# @rbs expected: Array[Object]
|
134
134
|
def initialize: (Array[Object] expected, ?json: untyped) -> untyped
|
135
135
|
|
136
|
+
# Check if individual items match
|
137
|
+
# Supports RSpec matchers as expected values
|
138
|
+
#
|
139
|
+
# @rbs actual_item: Object
|
140
|
+
# @rbs expected_item: Object
|
141
|
+
# @rbs return: bool
|
142
|
+
def match_items: (Object actual_item, Object expected_item) -> bool
|
143
|
+
|
144
|
+
# Check if two arrays match element by element
|
145
|
+
# Supports RSpec matchers as expected values
|
146
|
+
#
|
147
|
+
# @rbs actual_array: Array[Object]
|
148
|
+
# @rbs expected_array: Array[Object]
|
149
|
+
# @rbs return: bool
|
150
|
+
def match_arrays: (Array[Object] actual_array, Array[Object] expected_array) -> bool
|
151
|
+
|
136
152
|
# @rbs actual: Object
|
137
153
|
# @rbs return: bool
|
138
154
|
def matches?: (Object actual) -> bool
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-sse-matchers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- moznion
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- LICENSE
|
37
37
|
- README.md
|
38
38
|
- Rakefile
|
39
|
+
- examples/custom_matchers_example.rb
|
39
40
|
- examples/sse_spec.rb
|
40
41
|
- lib/rspec/sse/matchers.rb
|
41
42
|
- lib/rspec/sse/matchers/version.rb
|