turbo_rspec 0.1.0 → 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: ba06cebbff8c0ae4b76e280171ccd5ae57d06fe8e027010071f9fd9c923696d2
4
- data.tar.gz: 1838772f6e8673a0949e3e19ccdadd1f7706b6cdd02b77a1dc272bb7c3524f58
3
+ metadata.gz: 8cfaf54d65cd35ab2ea68138d58d70252fecf6778511beebf78ad40cfd7e998e
4
+ data.tar.gz: 7bc4f9a0949b25625b59408c551d3e554ba48112b7f1c0e0f511ef5b79354878
5
5
  SHA512:
6
- metadata.gz: f7e2693b3fdd61b62e66e248b9a4dbf16ff52096e4084dc24c3dd56c6dfabfed222f458ef1df188f81378d00815e0c1733399fccddb2520a345c9d0e367d6ed5
7
- data.tar.gz: 2dbd1d94ff557eb93b7f98882a80a03dbc7c43eaece27fdeef5755c7671e179999aafd762a9a8c32ef1ab4c8708291f0bcef8825b860dfcb2d43c75a8b98e10a
6
+ metadata.gz: 1a13ab561704b49f3659e83160724cd2afdd1def359a70f0f1057a396fc9d1f6eaba99bf4b20e54b81af62c4a1aa1ffb9b530b2565977d2cf5ea711163cd5f6d
7
+ data.tar.gz: 8889b026ce58c96a12d0bf5b974dd2bdba7ce6d77520de5af6a31d45aa52a79899e9107c7b8148a1b1ce9f742ab5c1cf0da9f1999ebcd2e06120b9ee4c34b21c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2026-05-28
4
+
5
+ ### Added
6
+
7
+ - `have_broadcasted_turbo_stream_to(stream)` block matcher for asserting ActionCable broadcasts contain a `<turbo-stream>` element
8
+ - Same fluent chain as `have_turbo_stream`: `.with_action`, `.targeting`, `.targeting_all`, `.with_content`, `.rendering`
9
+ - Count qualifiers: `.once`, `.twice`, `.exactly(n).times`, `.at_least(n).times`, `.at_most(n).times`
10
+ - `broadcast_turbo_stream_to` alias for naming symmetry with ActionCable's API
11
+ - Negation via `not_to have_broadcasted_turbo_stream_to` works out of the box
12
+
3
13
  ## [0.1.0] - 2026-05-28
4
14
 
5
15
  ### Added
data/ROADMAP.md CHANGED
@@ -5,19 +5,6 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
5
5
  ---
6
6
 
7
7
 
8
- ## v0.2.0 — Broadcast matchers
9
-
10
- **Goal:** cover the broadcast side — jobs/services that push streams over ActionCable.
11
-
12
- - `have_broadcasted_turbo_stream_to(channel_or_object)` — wraps ActionCable's test adapter
13
- - Same fluent chain as `have_turbo_stream`: `.with_action`, `.targeting`, `.rendering`, `.with_content`
14
- - Count qualifiers: `.exactly(n).times`, `.at_least(n).times`, `.at_most(n).times`, `.once`, `.twice`
15
- - Works inside `expect { }.to have_broadcasted_turbo_stream_to(...)` blocks
16
- - Helper `broadcast_turbo_stream_to` alias for symmetry with ActionCable's naming
17
- - Docs: "testing broadcasts in job specs and service specs"
18
-
19
- ---
20
-
21
8
  ## v0.3.0 — Capybara / system spec integration
22
9
 
23
10
  **Goal:** assertions that work against a live browser in feature/system specs.
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module TurboRspec
6
+ module Matchers
7
+ class HaveBroadcastedTurboStreamTo
8
+ def initialize(stream_or_object)
9
+ @stream_or_object = stream_or_object
10
+ @action = nil
11
+ @target = nil
12
+ @target_all = nil
13
+ @content = nil
14
+ @partial = nil
15
+ @expected_count = nil
16
+ @count_type = :at_least
17
+ end
18
+
19
+ # Stream constraints (mirrors HaveTurboStream)
20
+
21
+ def with_action(action)
22
+ @action = action.to_s
23
+ self
24
+ end
25
+
26
+ def targeting(dom_id)
27
+ @target = dom_id.to_s
28
+ self
29
+ end
30
+
31
+ def targeting_all(selector)
32
+ @target_all = selector.to_s
33
+ self
34
+ end
35
+
36
+ def with_content(text)
37
+ @content = text.to_s
38
+ self
39
+ end
40
+
41
+ def rendering(partial)
42
+ @partial = partial.to_s
43
+ self
44
+ end
45
+
46
+ # Count qualifiers
47
+
48
+ def once
49
+ exactly(1)
50
+ end
51
+
52
+ def twice
53
+ exactly(2)
54
+ end
55
+
56
+ def exactly(n)
57
+ @expected_count = n
58
+ @count_type = :exactly
59
+ self
60
+ end
61
+
62
+ def at_least(n)
63
+ @expected_count = n
64
+ @count_type = :at_least
65
+ self
66
+ end
67
+
68
+ def at_most(n)
69
+ @expected_count = n
70
+ @count_type = :at_most
71
+ self
72
+ end
73
+
74
+ def times
75
+ self
76
+ end
77
+
78
+ def supports_block_expectations?
79
+ true
80
+ end
81
+
82
+ def matches?(block)
83
+ before = snapshot
84
+ block.call
85
+ @matching = (snapshot - before).select { |msg| message_matches?(msg) }
86
+ count_matches?(@matching.size)
87
+ end
88
+
89
+ def does_not_match?(block)
90
+ !matches?(block)
91
+ end
92
+
93
+ def failure_message
94
+ "expected block to broadcast a turbo stream to #{stream_name.inspect}#{constraint_description}#{count_description}\n#{found_message}"
95
+ end
96
+
97
+ def failure_message_when_negated
98
+ "expected block not to broadcast a turbo stream to #{stream_name.inspect}#{constraint_description}"
99
+ end
100
+
101
+ def description
102
+ "broadcast a turbo stream to #{stream_name.inspect}#{constraint_description}"
103
+ end
104
+
105
+ private
106
+
107
+ def stream_name
108
+ @stream_name ||= if @stream_or_object.respond_to?(:to_str)
109
+ @stream_or_object
110
+ elsif defined?(Turbo::StreamsChannel)
111
+ Turbo::StreamsChannel.broadcasting_for(@stream_or_object)
112
+ else
113
+ @stream_or_object.to_s
114
+ end
115
+ end
116
+
117
+ def snapshot
118
+ ActionCable.server.pubsub.broadcasts(stream_name).dup
119
+ end
120
+
121
+ def message_matches?(message)
122
+ html = JSON.parse(message)
123
+ streams = Nokogiri::HTML5.fragment(html).css("turbo-stream")
124
+ streams.any? { |stream| stream_matches?(stream) }
125
+ rescue JSON::ParserError
126
+ false
127
+ end
128
+
129
+ def stream_matches?(stream)
130
+ matches_action?(stream) &&
131
+ matches_target?(stream) &&
132
+ matches_target_all?(stream) &&
133
+ matches_content?(stream) &&
134
+ matches_partial?(stream)
135
+ end
136
+
137
+ def matches_action?(stream)
138
+ @action.nil? || stream["action"] == @action
139
+ end
140
+
141
+ def matches_target?(stream)
142
+ @target.nil? || stream["target"] == @target
143
+ end
144
+
145
+ def matches_target_all?(stream)
146
+ @target_all.nil? || stream["targets"] == @target_all
147
+ end
148
+
149
+ def matches_content?(stream)
150
+ return true if @content.nil?
151
+ stream.text.include?(@content)
152
+ end
153
+
154
+ def matches_partial?(stream)
155
+ return true if @partial.nil?
156
+ stream.to_html.include?(@partial)
157
+ end
158
+
159
+ def count_matches?(n)
160
+ if @expected_count.nil?
161
+ n >= 1
162
+ else
163
+ # :nocov:
164
+ case @count_type
165
+ # :nocov:
166
+ when :exactly then n == @expected_count
167
+ when :at_least then n >= @expected_count
168
+ when :at_most then n <= @expected_count
169
+ end
170
+ end
171
+ end
172
+
173
+ def constraint_description
174
+ parts = []
175
+ parts << " with action #{@action.inspect}" if @action
176
+ parts << " targeting #{@target.inspect}" if @target
177
+ parts << " targeting all #{@target_all.inspect}" if @target_all
178
+ parts << " with content #{@content.inspect}" if @content
179
+ parts << " rendering #{@partial.inspect}" if @partial
180
+ parts.join
181
+ end
182
+
183
+ def count_description
184
+ return "" if @expected_count.nil? && @count_type == :at_least
185
+ # :nocov:
186
+ case @count_type
187
+ # :nocov:
188
+ when :exactly then " exactly #{@expected_count} time(s)"
189
+ when :at_least then " at least #{@expected_count} time(s)"
190
+ when :at_most then " at most #{@expected_count} time(s)"
191
+ end
192
+ end
193
+
194
+ def found_message
195
+ if @matching.empty?
196
+ "but no matching broadcasts were found"
197
+ else
198
+ "found #{@matching.size} matching broadcast(s)"
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "matchers/have_broadcasted_turbo_stream_to"
3
4
  require_relative "matchers/have_turbo_frame"
4
5
  require_relative "matchers/have_turbo_stream"
5
6
 
6
7
  module TurboRspec
7
8
  module Matchers
9
+ def have_broadcasted_turbo_stream_to(stream_or_object)
10
+ HaveBroadcastedTurboStreamTo.new(stream_or_object)
11
+ end
12
+
13
+ alias_method :broadcast_turbo_stream_to, :have_broadcasted_turbo_stream_to
14
+
8
15
  def have_turbo_frame
9
16
  HaveTurboFrame.new
10
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboRspec
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/turbo_rspec.rb CHANGED
@@ -19,12 +19,16 @@ module TurboRspec
19
19
  def reset_configuration!
20
20
  @configuration = Configuration.new
21
21
  end
22
+
23
+ def install_rspec_integration(config)
24
+ return unless configuration.auto_include && Gem.loaded_specs.key?("turbo-rails")
25
+ config.include Matchers, type: :request
26
+ end
22
27
  end
23
28
  end
24
29
 
30
+ # :nocov:
25
31
  if defined?(RSpec)
26
- RSpec.configure do |config|
27
- config.include TurboRspec::Matchers, type: :request if TurboRspec.configuration.auto_include &&
28
- Gem.loaded_specs.key?("turbo-rails")
29
- end
32
+ RSpec.configure { |config| TurboRspec.install_rspec_integration(config) }
30
33
  end
34
+ # :nocov:
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_rspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -45,6 +45,7 @@ files:
45
45
  - lib/turbo_rspec.rb
46
46
  - lib/turbo_rspec/configuration.rb
47
47
  - lib/turbo_rspec/matchers.rb
48
+ - lib/turbo_rspec/matchers/have_broadcasted_turbo_stream_to.rb
48
49
  - lib/turbo_rspec/matchers/have_turbo_frame.rb
49
50
  - lib/turbo_rspec/matchers/have_turbo_stream.rb
50
51
  - lib/turbo_rspec/version.rb