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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb59580533e9cd9424757c013ba82dc7f6735ad1064e434a30601f5e388d9423
4
- data.tar.gz: 781c89151d3c0bf01cdc463cce0e3cab5ebeee1b280c5834d9dfc626a5687a2b
3
+ metadata.gz: 9e06b785e8c91c1cc5fa04b08dfaf81a9a33d3e750bf9b52bd6b417db55e01ab
4
+ data.tar.gz: e16dde236261e02ff67d80e4deb1fc8ec1847dd5e432b193918c186f10487c59
5
5
  SHA512:
6
- metadata.gz: 554de4d1324acc2c018c5ef9900455475972c81f6b376d1874c2c7f6515efa5eca56534d55355d4a013729fed74d577dfb1fab48c9032b425e373cabe6374b5e
7
- data.tar.gz: ded963231f3c11f214c2c0bcac8ab2cc21ca9d1b36ce9ed473e991f4d874f5e4ab92d2a8bf5771101780fe67335dc3cb36b1c4ae49964ed0ac856d460f8b8f97
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
@@ -3,7 +3,7 @@
3
3
  module RSpec
4
4
  module SSE
5
5
  module Matchers
6
- VERSION = "0.1.2"
6
+ VERSION = "0.2.0"
7
7
  end
8
8
  end
9
9
  end
@@ -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 == @expected
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
- (extract_actual - @expected).empty? && (@expected - extract_actual).empty? && extract_actual.size == @expected.size
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? { |expected_item| extract_actual.include?(expected_item) }
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.1.2
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