rspec-notifications 0.1.0 → 0.1.1

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: 2a6a99147391a7c47e824a7d9b7aa3517feb8725926193f28bfda6a329fea44f
4
- data.tar.gz: b45d825be7f67e08b68973745c48a2993f468f733563c6e368c7df95d6d557db
3
+ metadata.gz: 90353f6fe62024ddf5a604bd029bded6850e03fc42f118e1f7177cf669643373
4
+ data.tar.gz: ef2d34c00ed2a986321c6be69d085798fbe4f4dbb794e25d1bd30a2c5d53a93b
5
5
  SHA512:
6
- metadata.gz: eb2e953a251fc0e7da9b5b8218efcb033cb78573f9b24dcd888def44a6131624c200f4555364d4e450d5524b4d9e311ee104a01be16b9e005ef85a8ce5c2cefd
7
- data.tar.gz: 0f593443c3cb6123d2de099fd965d4c51207b351cb5ffb3d583a1a41842e6df4aa3334c9490e104797e118fd0d2894e7c77098a6fae402a91b690d4a4e559303
6
+ metadata.gz: 5b970b58a3cb7236216a149b62e7433fa06852403bbef724e31dd725a993bf5b997f20988ec2e47c46e00071fe8618e232bbbe60d9b19c010c1b52b67241d422
7
+ data.tar.gz: 9a8ddf9e99ff34417dea49bd90d986c71521b78af316300ebbcd3822ac3553c3ef2e64a850d653744c5fdd873443b053bcc9a6b3afb6cf6fb94c46d2535292f4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 0.1.1 (2026-11-02)
2
+ - **breaking:** `not_to` with a count (`.once`, `.exactly(n)`, `.at_least(n)`, ...) now raises `ArgumentError`, as in rspec-mocks. It used to mean "not exactly once", so it passed when the notification fired repeatedly. Rewrite these as `to` with the count you expect.
3
+ - `.with` accepts a matcher (e.g. `a_hash_including`) or a non-hash value, matched against the whole payload, instead of raising `NoMethodError`
4
+ - notification names may be Symbols; any other type (including `nil`, which used to match every event) raises `ArgumentError`
5
+ - counts must be non-negative Integers; `exactly("2")` and the like raise `ArgumentError` instead of silently never matching
6
+ - using the matcher without a block (`expect(value).to emit_notification(...)`) is reported as unsupported
7
+ - fix failure message grammar for a single matching notification
8
+ - `.with(nil)` and `.with(false)` are checked against the payload instead of being ignored
9
+
1
10
  ## 0.1.0
2
11
  - initial release
3
12
  - `emit_notification` matcher for `ActiveSupport::Notifications`
data/Gemfile.lock CHANGED
@@ -1,14 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-notifications (0.1.0)
4
+ rspec-notifications (0.1.1)
5
5
  activesupport (>= 7)
6
6
  rspec-expectations (>= 3)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- activesupport (8.1.3)
11
+ activesupport (8.1.4)
12
12
  base64
13
13
  bigdecimal
14
14
  concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -22,18 +22,17 @@ GEM
22
22
  tzinfo (~> 2.0, >= 2.0.5)
23
23
  uri (>= 0.13.1)
24
24
  base64 (0.3.0)
25
- bigdecimal (4.1.2)
26
- concurrent-ruby (1.3.6)
25
+ bigdecimal (4.1.3)
26
+ concurrent-ruby (1.3.8)
27
27
  connection_pool (3.0.2)
28
28
  date (3.5.1)
29
29
  debug (1.11.1)
30
30
  irb (~> 1.10)
31
31
  reline (>= 0.3.8)
32
32
  diff-lcs (1.6.2)
33
- docile (1.4.1)
34
33
  drb (2.2.3)
35
34
  erb (6.0.4)
36
- i18n (1.14.8)
35
+ i18n (1.15.2)
37
36
  concurrent-ruby (~> 1.0)
38
37
  io-console (0.8.2)
39
38
  irb (1.17.0)
@@ -41,7 +40,7 @@ GEM
41
40
  prism (>= 1.3.0)
42
41
  rdoc (>= 4.0.0)
43
42
  reline (>= 0.4.2)
44
- json (2.19.8)
43
+ json (3.0.2)
45
44
  logger (1.7.0)
46
45
  minitest (6.0.6)
47
46
  drb (~> 2.0)
@@ -73,12 +72,7 @@ GEM
73
72
  rspec-support (~> 3.13.0)
74
73
  rspec-support (3.13.7)
75
74
  securerandom (0.4.1)
76
- simplecov (0.22.0)
77
- docile (~> 1.1)
78
- simplecov-html (~> 0.11)
79
- simplecov_json_formatter (~> 0.1)
80
- simplecov-html (0.13.2)
81
- simplecov_json_formatter (0.1.4)
75
+ simplecov (1.2.0)
82
76
  stringio (3.2.0)
83
77
  tsort (0.2.0)
84
78
  tzinfo (2.0.6)
@@ -95,21 +89,20 @@ DEPENDENCIES
95
89
  simplecov
96
90
 
97
91
  CHECKSUMS
98
- activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
92
+ activesupport (8.1.4) sha256=891dd3fe43c4fc8391934b237b1189bb1f661c932a84a840043db9d46a4e80df
99
93
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
100
- bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
101
- concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
94
+ bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66
95
+ concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
102
96
  connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
103
97
  date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
104
98
  debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
105
99
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
106
- docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
107
100
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
108
101
  erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
109
- i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
102
+ i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
110
103
  io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
111
104
  irb (1.17.0) sha256=168c4ddb93d8a361a045c41d92b2952c7a118fa73f23fe14e55609eb7a863aae
112
- json (2.19.8) sha256=6354310fd76ef69b87d5bd1f38b40d730613baf90b6803d2d0a48f618d32dfaa
105
+ json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df
113
106
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
114
107
  minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
115
108
  pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
@@ -122,12 +115,10 @@ CHECKSUMS
122
115
  rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
123
116
  rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
124
117
  rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
125
- rspec-notifications (0.1.0)
118
+ rspec-notifications (0.1.1)
126
119
  rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
127
120
  securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
128
- simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
129
- simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
130
- simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
121
+ simplecov (1.2.0) sha256=ea6acd05eece5a41990e2a5171c57d15700d329326c7666c85ee8c6a0dd0977e
131
122
  stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
132
123
  tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
133
124
  tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
data/README.md CHANGED
@@ -4,14 +4,14 @@ rspec-notifications
4
4
  [![codecov](https://codecov.io/gh/dpep/rspec-notifications/branch/main/graph/badge.svg)](https://codecov.io/gh/dpep/rspec-notifications)
5
5
 
6
6
 
7
- RSpec matchers for testing `ActiveSupport::Notifications`.
7
+ RSpec matchers for [`ActiveSupport::Notifications`](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html)
8
8
 
9
9
  ```ruby
10
10
  require "rspec/notifications"
11
11
 
12
12
  expect {
13
13
  service.call
14
- }.to emit_notification("user.created")
14
+ }.to emit_notification("user.created").with(user_id: user.id).once
15
15
  ```
16
16
 
17
17
  The matcher subscribes to `ActiveSupport::Notifications` before running the
@@ -20,8 +20,8 @@ block and unsubscribes afterward, so there is nothing to set up or tear down.
20
20
 
21
21
  ## Payloads
22
22
 
23
- Match the payload with `.with`. Matching is partial — only the keys you name
24
- are checked, so extra payload keys are ignored.
23
+ Match the payload with `.with`. A hash matches partially — only the keys you
24
+ name are checked, so extra payload keys are ignored.
25
25
 
26
26
  ```ruby
27
27
  expect {
@@ -29,20 +29,28 @@ expect {
29
29
  }.to emit_notification("user.created").with(user_id: user.id)
30
30
  ```
31
31
 
32
- Values support embedded RSpec matchers, nested hashes, and any composable
33
- matcher:
32
+ Values can be embedded RSpec matchers. Nested hashes match exactly; use
33
+ `a_hash_including` for a nested subset:
34
34
 
35
35
  ```ruby
36
36
  expect {
37
37
  service.call
38
38
  }.to emit_notification("user.created").with(user_id: kind_of(Integer))
39
39
 
40
- # nested hashes match exactly; use a_hash_including for a nested subset
41
40
  expect {
42
41
  service.call
43
42
  }.to emit_notification("user.created").with(user: a_hash_including(role: "admin"))
44
43
  ```
45
44
 
45
+ Pass anything other than a hash — a matcher or a plain value — and it's
46
+ matched against the whole payload:
47
+
48
+ ```ruby
49
+ expect {
50
+ service.call
51
+ }.to emit_notification("user.created").with(a_hash_including(user_id: 1))
52
+ ```
53
+
46
54
 
47
55
  ## Counts
48
56
 
@@ -55,24 +63,37 @@ expect { service.call }.to emit_notification("user.created").at_most(2).times
55
63
  ```
56
64
 
57
65
  Without a count, the matcher passes when the notification is emitted at least
58
- once. Counts apply to events matching both the name and the payload.
66
+ once. Counts apply to events matching both the name and the payload, and must
67
+ be non-negative Integers.
59
68
 
60
69
 
61
70
  ## Names
62
71
 
63
- Names can be matched exactly, with a `*` wildcard, or with a `Regexp`:
64
-
65
72
  ```ruby
66
73
  emit_notification("user.created") # exact
74
+ emit_notification(:"user.created") # Symbol, same as the String
67
75
  emit_notification("user.*") # wildcard
68
76
  emit_notification(/user\./) # regexp
69
77
  ```
70
78
 
79
+ `*` matches any characters, dots included, so `"user.*"` also matches
80
+ `"user.created.v2"`. Use a `Regexp` when you need tighter control. Any other
81
+ name type raises `ArgumentError`.
82
+
71
83
 
72
84
  ## Negation
73
85
 
74
86
  ```ruby
75
87
  expect { service.call }.not_to emit_notification("user.created")
88
+ expect { service.call }.not_to emit_notification("user.created").with(role: "admin")
89
+ ```
90
+
91
+ Like rspec-mocks, `not_to` refuses a count: `not_to ... .once` raises
92
+ `ArgumentError`, since it's unclear whether you mean "never" or "any number
93
+ but one". Say the count you expect instead:
94
+
95
+ ```ruby
96
+ expect { service.call }.to emit_notification("user.created").twice
76
97
  ```
77
98
 
78
99
 
@@ -90,7 +111,14 @@ expect { service.call }.not_to emit_notification("user.created")
90
111
  it adds `:exception` / `:exception_object` to the payload, which you can match
91
112
  on with `.with`.
92
113
  - **Concurrency** — notifications delivered from other threads during the block
93
- are captured too; appends are guarded by a mutex.
114
+ are captured too.
115
+ - **Mutated payloads** — the payload is captured as a shallow copy. Replacing a
116
+ top-level key after emitting doesn't affect matching, but mutating a nested
117
+ object does (e.g. `instrument("user.updated", user: user)` followed by
118
+ `user.update(...)` in the same block).
119
+ - **Two-argument `publish`** — `publish(name, payload)` doesn't follow the
120
+ `(name, started, finished, id, payload)` convention, so the payload isn't
121
+ seen. `instrument`, five-argument `publish`, and `publish_event` all work.
94
122
 
95
123
 
96
124
  ----
@@ -6,7 +6,13 @@ module RSpec
6
6
  include RSpec::Matchers::Composable
7
7
 
8
8
  def initialize(pattern)
9
+ pattern = pattern.to_s if pattern.is_a?(Symbol)
10
+ unless pattern.is_a?(String) || pattern.is_a?(Regexp)
11
+ raise ArgumentError, "notification name must be a String, Symbol, or Regexp, got #{pattern.inspect}"
12
+ end
13
+
9
14
  @pattern = pattern
15
+ @payload_expected = false
10
16
  @expected_payload = nil
11
17
  @count_type = nil
12
18
  @count = nil
@@ -15,6 +21,7 @@ module RSpec
15
21
  # --- chained expectations ------------------------------------------
16
22
 
17
23
  def with(payload)
24
+ @payload_expected = true
18
25
  @expected_payload = payload
19
26
  self
20
27
  end
@@ -49,13 +56,19 @@ module RSpec
49
56
  def matches?(block)
50
57
  @events = Subscriber.capture(subscribe_pattern, &block)
51
58
  @matching_events = @events.select do |event|
52
- PayloadMatcher.matches?(@expected_payload, event.payload)
59
+ !@payload_expected || PayloadMatcher.matches?(@expected_payload, event.payload)
53
60
  end
54
61
 
55
62
  count_satisfied?(@matching_events.size)
56
63
  end
57
64
 
58
65
  def does_not_match?(block)
66
+ # Mirrors rspec-mocks: "not to emit once" reads as "never", so refuse it.
67
+ if @count_type
68
+ raise ArgumentError, "a count (#{count_description}) can't be used with not_to; " \
69
+ "use `to` with a different count, or not_to without one"
70
+ end
71
+
59
72
  !matches?(block)
60
73
  end
61
74
 
@@ -63,9 +76,13 @@ module RSpec
63
76
  true
64
77
  end
65
78
 
79
+ def supports_value_expectations?
80
+ false
81
+ end
82
+
66
83
  def description
67
84
  desc = "emit #{@pattern.inspect} notification"
68
- desc += " with payload #{description_of(@expected_payload)}" if @expected_payload
85
+ desc += " with payload #{description_of(@expected_payload)}" if @payload_expected
69
86
  desc += " #{count_description}" if @count_type
70
87
  desc
71
88
  end
@@ -81,6 +98,10 @@ module RSpec
81
98
  private
82
99
 
83
100
  def set_count(type, count)
101
+ unless count.is_a?(Integer) && count >= 0
102
+ raise ArgumentError, "count must be a non-negative Integer, got #{count.inspect}"
103
+ end
104
+
84
105
  @count_type = type
85
106
  @count = count
86
107
  self
@@ -112,8 +133,8 @@ module RSpec
112
133
  def observed_summary
113
134
  matched = @matching_events.size
114
135
 
115
- if @expected_payload && @events.any?
116
- "#{pluralize(matched, "matching notification")} were emitted " \
136
+ if @payload_expected && @events.any?
137
+ "#{pluralize(matched, "matching notification")} #{matched == 1 ? "was" : "were"} emitted " \
117
138
  "(#{pluralize(@events.size, "notification")} matched the name)"
118
139
  elsif matched.zero?
119
140
  "no matching notifications were emitted"
@@ -4,17 +4,18 @@ module RSpec
4
4
  module Notifications
5
5
  # Matches an expected payload against an actual notification payload.
6
6
  #
7
- # Matching is partial at the top level: only the keys named in +expected+
7
+ # A Hash is matched partially at the top level: only the keys named in +expected+
8
8
  # are checked, so unrelated payload keys are ignored. Values are compared
9
9
  # with RSpec's fuzzy matching, which supports embedded matchers (e.g.
10
10
  # +kind_of(Integer)+), regexps, ranges, and nested structures. Nested
11
11
  # hashes are compared exactly -- use +a_hash_including(...)+ for a nested
12
- # partial match.
12
+ # partial match. Anything else (e.g. +a_hash_including+) is matched against
13
+ # the whole payload.
13
14
  module PayloadMatcher
14
15
  module_function
15
16
 
16
17
  def matches?(expected, actual)
17
- return true if expected.nil?
18
+ return RSpec::Support::FuzzyMatcher.values_match?(expected, actual) unless expected.is_a?(Hash)
18
19
  return false unless actual.is_a?(Hash)
19
20
 
20
21
  expected.all? do |key, value|
@@ -1,5 +1,5 @@
1
1
  module RSpec
2
2
  module Notifications
3
- VERSION = "0.1.0"
3
+ VERSION = "0.1.1"
4
4
  end
5
5
  end
@@ -17,7 +17,7 @@ module RSpec
17
17
  # expect { service.call }.to emit_notification("user.*")
18
18
  # expect { service.call }.to emit_notification(/user\./)
19
19
  #
20
- # +pattern+ may be an exact String, a String with "*" wildcards, or a Regexp.
20
+ # +pattern+ may be an exact String or Symbol, a String with "*" wildcards, or a Regexp.
21
21
  def emit_notification(pattern)
22
22
  RSpec::Notifications::Matcher.new(pattern)
23
23
  end
@@ -5,11 +5,17 @@ Gem::Specification.new do |s|
5
5
  s.version = RSpec::Notifications::VERSION
6
6
  s.authors = ["Daniel Pepper"]
7
7
  s.description = "RSpec matchers for ActiveSupport::Notifications"
8
- s.files = `git ls-files * ':!:spec'`.split("\n")
8
+ s.files = `git ls-files * ':!:spec' ':!:gemfiles'`.split("\n")
9
9
  s.homepage = "https://github.com/dpep/rspec-notifications"
10
10
  s.license = "MIT"
11
11
  s.summary = "RSpec::Notifications"
12
12
 
13
+ s.metadata = {
14
+ "changelog_uri" => "#{s.homepage}/blob/main/CHANGELOG.md",
15
+ "rubygems_mfa_required" => "true",
16
+ "source_code_uri" => s.homepage,
17
+ }
18
+
13
19
  s.required_ruby_version = ">= 3.2"
14
20
 
15
21
  s.add_dependency "activesupport", ">= 7"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-notifications
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
@@ -99,7 +99,10 @@ files:
99
99
  homepage: https://github.com/dpep/rspec-notifications
100
100
  licenses:
101
101
  - MIT
102
- metadata: {}
102
+ metadata:
103
+ changelog_uri: https://github.com/dpep/rspec-notifications/blob/main/CHANGELOG.md
104
+ rubygems_mfa_required: 'true'
105
+ source_code_uri: https://github.com/dpep/rspec-notifications
103
106
  rdoc_options: []
104
107
  require_paths:
105
108
  - lib
@@ -114,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
117
  - !ruby/object:Gem::Version
115
118
  version: '0'
116
119
  requirements: []
117
- rubygems_version: 3.6.9
120
+ rubygems_version: 4.0.16
118
121
  specification_version: 4
119
122
  summary: RSpec::Notifications
120
123
  test_files: []