turbo_rspec 1.0.0 → 1.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: '036718d6f9e9e1960f6e12b668a288c6562ec914aa87eaaaddacfbccb47fc2fd'
4
- data.tar.gz: 6f0345d41a0803871c89fbbbaa9e7e1e30b7ca155e9d60b8c41052a1fe0c59e6
3
+ metadata.gz: 7341e9dfd6be8235a2269e94ef9923e470aa5da675ccd52f8d507f42c3950398
4
+ data.tar.gz: 65f4f2bb0aa32c4032d4ba2daad8c9442e20d056b3a455b0937eaeaf380f924a
5
5
  SHA512:
6
- metadata.gz: e2269c5a1df90467e5487cb8b029e75591a91c07df1a5af5182af6075d0803dbbc8f8d97418bfbfb89232f63d0fc09646e894533d78cea61fd6937550707dfca
7
- data.tar.gz: 0b4c4d7ed1c62e796b03eaed49edb1449e0ea265952f8b6a97f055a49f3c1708fee739a0fc47e99282e7e222d090690859bf540b3bfe3893fd8a523c00657640
6
+ metadata.gz: b433e046578d5f3914a95cd443ed32020bff526bddf3bf79eea34ef7bc52a7ce06823ac25714f309842c357d97e0e70edd208b75a85c8e0a118e7d8b86b36c9f
7
+ data.tar.gz: 44d25746b61d50d932f74f92b2f6a07c50c70a6d320c452f302693b507617da38facf8fca1d0ac61f66cf01a49187e8288a20c6182b27cbe4ab91db42fe28ec5
@@ -0,0 +1,15 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: [eclectic-coding] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4
+ patreon: # Replace with a single Patreon username
5
+ open_collective: # Replace with a single Open Collective username
6
+ ko_fi: # Replace with a single Ko-fi username
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12
+ polar: # Replace with a single Polar username
13
+ buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14
+ thanks_dev: # Replace with a single thanks.dev username
15
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.2.0] - 2026-06-01
4
+
5
+ ### Added
6
+
7
+ - `have_turbo_stream.with_attributes(hash)` — assert arbitrary HTML attributes on a stream element; accepts string or symbol keys
8
+ - `have_turbo_frame.with_attributes(hash)` — same for frame elements in request specs
9
+ - `have_turbo_stream.children_only` — assert the `children-only` attribute on Turbo 8 morph streams
10
+ - `TurboRspec.register_action(:name)` — declare custom Turbo stream actions; `with_action` now raises `ArgumentError` for unrecognised actions with a hint to register them
11
+ - `TurboRspec::BUILTIN_ACTIONS` — public constant listing all built-in action names
12
+ - `TurboRspec.known_actions` — returns built-in + registered custom actions
13
+
14
+ ## [1.1.0] - 2026-06-01
15
+
16
+ ### Added
17
+
18
+ - Count qualifiers on `have_turbo_stream`: `.once`, `.twice`, `.exactly(n).times`, `.at_least(n).times`, `.at_most(n).times` — mirrors the API already available on `have_broadcasted_turbo_stream_to`
19
+ - `have_turbo_frame` (Capybara) `.with_src(url)` chain — asserts the `src` attribute of a lazy-loaded frame
20
+ - `have_turbo_frame` (Capybara) `.lazy` chain — asserts `loading="lazy"` is set on the frame
21
+
3
22
  ## [1.0.0] - 2026-05-28
4
23
 
5
24
  ### Added
data/README.md CHANGED
@@ -99,13 +99,33 @@ expect(response).to have_turbo_stream
99
99
  .targeting("messages")
100
100
  .with_content("Hello")
101
101
 
102
+ # With arbitrary attributes
103
+ expect(response).to have_turbo_stream.with_attributes("data-controller" => "messages")
104
+
105
+ # Turbo 8 morph with children-only
106
+ expect(response).to have_turbo_stream.with_action(:morph).children_only
107
+
108
+ # Count qualifiers — assert how many matching streams appear
109
+ expect(response).to have_turbo_stream.with_action(:append).once
110
+ expect(response).to have_turbo_stream.with_action(:append).twice
111
+ expect(response).to have_turbo_stream.with_action(:append).exactly(3).times
112
+ expect(response).to have_turbo_stream.with_action(:append).at_least(2).times
113
+ expect(response).to have_turbo_stream.with_action(:append).at_most(1).times
114
+
102
115
  # Negation
103
116
  expect(response).not_to have_turbo_stream.with_action(:replace)
104
117
  ```
105
118
 
106
119
  #### Actions
107
120
 
108
- Turbo supports the following stream actions: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `refresh`.
121
+ Turbo's built-in stream actions: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `refresh`, `morph`.
122
+
123
+ `with_action` raises `ArgumentError` for unrecognised names. Register custom actions in your test setup:
124
+
125
+ ```ruby
126
+ # spec/support/turbo_rspec.rb
127
+ TurboRspec.register_action(:sparkle, :highlight)
128
+ ```
109
129
 
110
130
  ### `have_turbo_streams`
111
131
 
@@ -189,6 +209,18 @@ expect(page).to have_turbo_frame("messages").with_content("Hello")
189
209
  # Loaded (frame finished loading)
190
210
  expect(page).to have_turbo_frame("messages").loaded
191
211
 
212
+ # Lazy frame — assert loading="lazy" attribute
213
+ expect(page).to have_turbo_frame("messages").lazy
214
+
215
+ # With src — assert the src attribute on a lazy-loaded frame
216
+ expect(page).to have_turbo_frame("messages").with_src("/messages/new")
217
+
218
+ # With arbitrary attributes (request specs only — use Capybara selectors in system specs)
219
+ expect(response).to have_turbo_frame.with_attributes("data-controller" => "chat")
220
+
221
+ # Chained
222
+ expect(page).to have_turbo_frame("messages").lazy.with_src("/messages/new")
223
+
192
224
  # Negation
193
225
  expect(page).not_to have_turbo_frame("notifications")
194
226
  ```
data/ROADMAP.md CHANGED
@@ -4,10 +4,14 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
4
4
 
5
5
  ---
6
6
 
7
- ## Post-1.0 ideas (not scheduled)
7
+ ## 1.3 Stimulus companion
8
8
 
9
- - VS Code / RubyMine snippet pack for common patterns
10
- - Playwright/Puppeteer bridge for headless assertions outside Capybara
9
+ - **Stimulus matchers** `have_stimulus_controller`, `have_stimulus_action`, `have_stimulus_target` Capybara matchers. turbo-rails ships with Stimulus; teams using both already test Stimulus behavior manually. This is the natural next surface area for a complete Hotwire testing toolkit.
10
+
11
+ ## 1.4 — Tooling
12
+
13
+ - **RuboCop cop** — flag request specs that assert `response.body` with a raw string match or `include "<turbo-stream"` instead of using the gem's matchers. Useful for migration and incremental adoption.
14
+ - **Snapshot/fixture matcher** — `match_turbo_stream_snapshot("name")` records the stream on the first run and diffs on subsequent runs. Good for complex multi-stream responses where re-specifying every constraint in detail is noisy.
11
15
 
12
16
  ---
13
17
 
@@ -8,6 +8,8 @@ module TurboRspec
8
8
  @id = id.to_s
9
9
  @content = nil
10
10
  @loaded = false
11
+ @src = nil
12
+ @lazy = false
11
13
  end
12
14
 
13
15
  def with_content(text)
@@ -20,10 +22,27 @@ module TurboRspec
20
22
  self
21
23
  end
22
24
 
25
+ # Constrains the match to frames with the given +src+ attribute.
26
+ # @param url [String]
27
+ # @return [self]
28
+ def with_src(url)
29
+ @src = url.to_s
30
+ self
31
+ end
32
+
33
+ # Constrains the match to frames with +loading="lazy"+.
34
+ # @return [self]
35
+ def lazy
36
+ @lazy = true
37
+ self
38
+ end
39
+
23
40
  def matches?(page_or_node)
24
41
  @node = find_frame(page_or_node)
25
42
  return false unless @node
26
43
  return false if @loaded && !@node[:complete]
44
+ return false if @lazy && @node[:loading] != "lazy"
45
+ return false if @src && @node[:src] != @src
27
46
  return false if @content && !@node.has_content?(@content, wait: 0)
28
47
  true
29
48
  end
@@ -37,6 +56,10 @@ module TurboRspec
37
56
  "expected page to have turbo-frame##{@id}#{constraint_description} but it was not found"
38
57
  elsif @loaded && !@node[:complete]
39
58
  "expected turbo-frame##{@id} to be loaded (missing [complete] attribute)"
59
+ elsif @lazy && @node[:loading] != "lazy"
60
+ "expected turbo-frame##{@id} to be lazy (missing loading=\"lazy\" attribute)"
61
+ elsif @src && @node[:src] != @src
62
+ "expected turbo-frame##{@id} to have src #{@src.inspect}, got #{@node[:src].inspect}"
40
63
  else
41
64
  "expected turbo-frame##{@id} to have content #{@content.inspect}"
42
65
  end
@@ -61,6 +84,8 @@ module TurboRspec
61
84
  def constraint_description
62
85
  parts = []
63
86
  parts << " loaded" if @loaded
87
+ parts << " lazy" if @lazy
88
+ parts << " with src #{@src.inspect}" if @src
64
89
  parts << " with content #{@content.inspect}" if @content
65
90
  parts.join
66
91
  end
@@ -13,8 +13,14 @@ module TurboRspec
13
13
  # @return [Boolean]
14
14
  attr_accessor :auto_include
15
15
 
16
+ # @!attribute [rw] custom_actions
17
+ # Additional stream action names registered via +TurboRspec.register_action+.
18
+ # @return [Array<String>]
19
+ attr_accessor :custom_actions
20
+
16
21
  def initialize
17
22
  @auto_include = true
23
+ @custom_actions = []
18
24
  end
19
25
  end
20
26
  end
@@ -21,6 +21,7 @@ module TurboRspec
21
21
  @id = nil
22
22
  @content = nil
23
23
  @partial = nil
24
+ @attributes = {}
24
25
  end
25
26
 
26
27
  # Constrains the match to frames with the given id attribute.
@@ -47,6 +48,14 @@ module TurboRspec
47
48
  self
48
49
  end
49
50
 
51
+ # Constrains the match to frames that have all of the given HTML attributes.
52
+ # @param attrs [Hash]
53
+ # @return [self]
54
+ def with_attributes(attrs)
55
+ @attributes = attrs.transform_keys(&:to_s).transform_values(&:to_s)
56
+ self
57
+ end
58
+
50
59
  # @param response_or_body [#body, String]
51
60
  # @return [Boolean]
52
61
  def matches?(response_or_body)
@@ -92,7 +101,8 @@ module TurboRspec
92
101
  def frame_matches?(frame)
93
102
  matches_id?(frame) &&
94
103
  matches_content?(frame) &&
95
- matches_partial?(frame)
104
+ matches_partial?(frame) &&
105
+ matches_attributes?(frame)
96
106
  end
97
107
 
98
108
  def matches_id?(frame)
@@ -109,11 +119,17 @@ module TurboRspec
109
119
  frame.to_html.include?(@partial)
110
120
  end
111
121
 
122
+ def matches_attributes?(frame)
123
+ return true if @attributes.empty?
124
+ @attributes.all? { |k, v| frame[k] == v }
125
+ end
126
+
112
127
  def constraint_description
113
128
  parts = []
114
129
  parts << " with id #{@id.inspect}" if @id
115
130
  parts << " with content #{@content.inspect}" if @content
116
131
  parts << " rendering #{@partial.inspect}" if @partial
132
+ parts << " with attributes #{@attributes.inspect}" if @attributes.any?
117
133
  parts.join
118
134
  end
119
135
 
@@ -144,11 +160,13 @@ module TurboRspec
144
160
  count += 1 if !@id.nil? && matches_id?(frame)
145
161
  count += 1 if !@content.nil? && matches_content?(frame)
146
162
  count += 1 if !@partial.nil? && matches_partial?(frame)
163
+ count += 1 if @attributes.any? && matches_attributes?(frame)
147
164
  count
148
165
  end
149
166
 
150
167
  def constraint_count
151
- [@id, @content, @partial].count { |c| !c.nil? }
168
+ [@id, @content, @partial].count { |c| !c.nil? } +
169
+ (@attributes.any? ? 1 : 0)
152
170
  end
153
171
 
154
172
  def constraint_diff(frame)
@@ -156,6 +174,10 @@ module TurboRspec
156
174
  lines << " #{matches_id?(frame) ? "✓" : "✗"} id: expected #{@id.inspect}, got #{frame["id"].inspect}" if @id
157
175
  lines << " #{matches_content?(frame) ? "✓" : "✗"} content: expected to include #{@content.inspect}, got #{frame.text.strip.slice(0, 50).inspect}" if @content
158
176
  lines << " #{matches_partial?(frame) ? "✓" : "✗"} rendering: expected to include #{@partial.inspect}" if @partial
177
+ @attributes.each do |k, v|
178
+ actual = frame[k]
179
+ lines << " #{(actual == v) ? "✓" : "✗"} attr[#{k}]: expected #{v.inspect}, got #{actual.inspect}"
180
+ end
159
181
  lines
160
182
  end
161
183
  end
@@ -28,13 +28,24 @@ module TurboRspec
28
28
  @target_all = nil
29
29
  @content = nil
30
30
  @partial = nil
31
+ @attributes = {}
32
+ @children_only = false
33
+ @expected_count = nil
34
+ @count_type = :at_least
35
+ @matching_count = 0
31
36
  end
32
37
 
33
38
  # Constrains the match to streams with the given action.
34
39
  # @param action [Symbol, String] e.g. +:append+, +:replace+, +:remove+, +:refresh+, +:morph+
40
+ # @raise [ArgumentError] if the action is not a known built-in or registered custom action
35
41
  # @return [self]
36
42
  def with_action(action)
37
- @action = action.to_s
43
+ action_str = action.to_s
44
+ unless TurboRspec.known_actions.include?(action_str)
45
+ raise ArgumentError, "Unknown Turbo stream action #{action_str.inspect}. " \
46
+ "Register custom actions with TurboRspec.register_action(:#{action_str})."
47
+ end
48
+ @action = action_str
38
49
  self
39
50
  end
40
51
 
@@ -70,12 +81,78 @@ module TurboRspec
70
81
  self
71
82
  end
72
83
 
84
+ # Constrains the match to streams that have all of the given HTML attributes.
85
+ # Keys and values are compared as strings.
86
+ # @param attrs [Hash]
87
+ # @return [self]
88
+ # @example
89
+ # expect(response).to have_turbo_stream.with_attributes("data-controller" => "messages")
90
+ def with_attributes(attrs)
91
+ @attributes = attrs.transform_keys(&:to_s).transform_values(&:to_s)
92
+ self
93
+ end
94
+
95
+ # Constrains the match to morph streams with the +children-only+ attribute set.
96
+ # @return [self]
97
+ # @example
98
+ # expect(response).to have_turbo_stream.with_action(:morph).children_only
99
+ def children_only
100
+ @children_only = true
101
+ self
102
+ end
103
+
104
+ # Asserts exactly one matching stream.
105
+ # @return [self]
106
+ def once
107
+ exactly(1)
108
+ end
109
+
110
+ # Asserts exactly two matching streams.
111
+ # @return [self]
112
+ def twice
113
+ exactly(2)
114
+ end
115
+
116
+ # Asserts exactly +n+ matching streams.
117
+ # @param n [Integer]
118
+ # @return [self]
119
+ def exactly(n)
120
+ @expected_count = n
121
+ @count_type = :exactly
122
+ self
123
+ end
124
+
125
+ # Asserts at least +n+ matching streams.
126
+ # @param n [Integer]
127
+ # @return [self]
128
+ def at_least(n)
129
+ @expected_count = n
130
+ @count_type = :at_least
131
+ self
132
+ end
133
+
134
+ # Asserts at most +n+ matching streams.
135
+ # @param n [Integer]
136
+ # @return [self]
137
+ def at_most(n)
138
+ @expected_count = n
139
+ @count_type = :at_most
140
+ self
141
+ end
142
+
143
+ # Fluent terminator so +.exactly(2).times+ reads naturally.
144
+ # @return [self]
145
+ def times
146
+ self
147
+ end
148
+
73
149
  # @param response_or_body [#body, String]
74
150
  # @return [Boolean]
75
151
  def matches?(response_or_body)
76
152
  @body = extract_body(response_or_body)
77
153
  @streams = parse_streams(@body)
78
- @streams.any? { |stream| stream_matches?(stream) }
154
+ @matching_count = @streams.count { |stream| stream_matches?(stream) }
155
+ count_matches?(@matching_count)
79
156
  end
80
157
 
81
158
  # @param response_or_body [#body, String]
@@ -86,17 +163,17 @@ module TurboRspec
86
163
 
87
164
  # @return [String]
88
165
  def failure_message
89
- "expected response to contain a turbo stream#{constraint_description}\n#{found_streams_message}"
166
+ "expected response to contain a turbo stream#{constraint_description}#{count_description}\n#{found_streams_message}"
90
167
  end
91
168
 
92
169
  # @return [String]
93
170
  def failure_message_when_negated
94
- "expected response not to contain a turbo stream#{constraint_description}"
171
+ "expected response not to contain a turbo stream#{constraint_description}#{count_description}"
95
172
  end
96
173
 
97
174
  # @return [String]
98
175
  def description
99
- "have turbo stream#{constraint_description}"
176
+ "have turbo stream#{constraint_description}#{count_description}"
100
177
  end
101
178
 
102
179
  private
@@ -118,7 +195,9 @@ module TurboRspec
118
195
  matches_target?(stream) &&
119
196
  matches_target_all?(stream) &&
120
197
  matches_content?(stream) &&
121
- matches_partial?(stream)
198
+ matches_partial?(stream) &&
199
+ matches_attributes?(stream) &&
200
+ matches_children_only?(stream)
122
201
  end
123
202
 
124
203
  def matches_action?(stream)
@@ -143,6 +222,30 @@ module TurboRspec
143
222
  stream.to_html.include?(@partial)
144
223
  end
145
224
 
225
+ def matches_attributes?(stream)
226
+ return true if @attributes.empty?
227
+ @attributes.all? { |k, v| stream[k] == v }
228
+ end
229
+
230
+ def matches_children_only?(stream)
231
+ return true unless @children_only
232
+ !stream["children-only"].nil?
233
+ end
234
+
235
+ def count_matches?(n)
236
+ if @expected_count.nil?
237
+ n >= 1
238
+ else
239
+ # :nocov:
240
+ case @count_type
241
+ # :nocov:
242
+ when :exactly then n == @expected_count
243
+ when :at_least then n >= @expected_count
244
+ when :at_most then n <= @expected_count
245
+ end
246
+ end
247
+ end
248
+
146
249
  def constraint_description
147
250
  parts = []
148
251
  parts << " with action #{@action.inspect}" if @action
@@ -150,9 +253,22 @@ module TurboRspec
150
253
  parts << " targeting all #{@target_all.inspect}" if @target_all
151
254
  parts << " with content #{@content.inspect}" if @content
152
255
  parts << " rendering #{@partial.inspect}" if @partial
256
+ parts << " with attributes #{@attributes.inspect}" if @attributes.any?
257
+ parts << " children only" if @children_only
153
258
  parts.join
154
259
  end
155
260
 
261
+ def count_description
262
+ return "" if @expected_count.nil?
263
+ # :nocov:
264
+ case @count_type
265
+ # :nocov:
266
+ when :exactly then " exactly #{@expected_count} time(s)"
267
+ when :at_least then " at least #{@expected_count} time(s)"
268
+ when :at_most then " at most #{@expected_count} time(s)"
269
+ end
270
+ end
271
+
156
272
  def found_streams_message
157
273
  return "but no turbo streams were found in the response" if @streams.empty?
158
274
 
@@ -163,10 +279,15 @@ module TurboRspec
163
279
  lines << " #{i + 1}. action=#{s["action"].inspect} target=#{s["target"].inspect} content=#{content_preview}"
164
280
  end
165
281
 
166
- closest = closest_match
167
- lines << ""
168
- lines << "closest match (#{count_matching_constraints(closest)}/#{constraint_count} constraint(s) matched):"
169
- lines.concat(constraint_diff(closest))
282
+ if @expected_count
283
+ lines << ""
284
+ lines << "#{@matching_count} of #{@streams.size} stream(s) matched the constraints"
285
+ else
286
+ closest = closest_match
287
+ lines << ""
288
+ lines << "closest match (#{count_matching_constraints(closest)}/#{constraint_count} constraint(s) matched):"
289
+ lines.concat(constraint_diff(closest))
290
+ end
170
291
 
171
292
  lines.join("\n")
172
293
  end
@@ -182,11 +303,15 @@ module TurboRspec
182
303
  count += 1 if !@target_all.nil? && matches_target_all?(stream)
183
304
  count += 1 if !@content.nil? && matches_content?(stream)
184
305
  count += 1 if !@partial.nil? && matches_partial?(stream)
306
+ count += 1 if @attributes.any? && matches_attributes?(stream)
307
+ count += 1 if @children_only && matches_children_only?(stream)
185
308
  count
186
309
  end
187
310
 
188
311
  def constraint_count
189
- [@action, @target, @target_all, @content, @partial].count { |c| !c.nil? }
312
+ [@action, @target, @target_all, @content, @partial].count { |c| !c.nil? } +
313
+ (@attributes.any? ? 1 : 0) +
314
+ (@children_only ? 1 : 0)
190
315
  end
191
316
 
192
317
  def constraint_diff(stream)
@@ -196,6 +321,11 @@ module TurboRspec
196
321
  lines << " #{matches_target_all?(stream) ? "✓" : "✗"} targets: expected #{@target_all.inspect}, got #{stream["targets"].inspect}" if @target_all
197
322
  lines << " #{matches_content?(stream) ? "✓" : "✗"} content: expected to include #{@content.inspect}, got #{stream.text.strip.slice(0, 50).inspect}" if @content
198
323
  lines << " #{matches_partial?(stream) ? "✓" : "✗"} rendering: expected to include #{@partial.inspect}" if @partial
324
+ @attributes.each do |k, v|
325
+ actual = stream[k]
326
+ lines << " #{(actual == v) ? "✓" : "✗"} attr[#{k}]: expected #{v.inspect}, got #{actual.inspect}"
327
+ end
328
+ lines << " #{matches_children_only?(stream) ? "✓" : "✗"} children-only: expected attribute to be present" if @children_only
199
329
  lines
200
330
  end
201
331
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboRspec
4
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/turbo_rspec.rb CHANGED
@@ -19,6 +19,9 @@ module TurboRspec
19
19
  # Base error class for TurboRspec.
20
20
  class Error < StandardError; end
21
21
 
22
+ # Built-in Turbo stream action names.
23
+ BUILTIN_ACTIONS = %w[append prepend replace update remove before after refresh morph].freeze
24
+
22
25
  class << self
23
26
  # Yields the configuration object for customization.
24
27
  #
@@ -46,6 +49,24 @@ module TurboRspec
46
49
  @configuration = Configuration.new
47
50
  end
48
51
 
52
+ # Registers one or more custom Turbo stream action names so that
53
+ # +have_turbo_stream.with_action(:name)+ does not raise +ArgumentError+.
54
+ #
55
+ # @param actions [Array<Symbol, String>]
56
+ # @return [void]
57
+ # @example
58
+ # TurboRspec.register_action(:sparkle, :highlight)
59
+ def register_action(*actions)
60
+ configuration.custom_actions |= actions.map(&:to_s)
61
+ end
62
+
63
+ # Returns all known stream action names (built-in + registered custom).
64
+ #
65
+ # @return [Array<String>]
66
+ def known_actions
67
+ BUILTIN_ACTIONS + configuration.custom_actions
68
+ end
69
+
49
70
  # Installs RSpec integration — includes matchers and helpers into the
50
71
  # appropriate example groups. Called automatically when RSpec is present.
51
72
  #
data/sig/turbo_rspec.rbs CHANGED
@@ -1,4 +1,159 @@
1
1
  module TurboRspec
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end
3
+
4
+ # All built-in Turbo stream action names.
5
+ BUILTIN_ACTIONS: Array[String]
6
+
7
+ def self.configure: () { (Configuration) -> void } -> void
8
+ def self.configuration: () -> Configuration
9
+ def self.reset_configuration!: () -> void
10
+ def self.register_action: (*(Symbol | String) actions) -> void
11
+ def self.known_actions: () -> Array[String]
12
+ def self.install_rspec_integration: (untyped config) -> void
13
+
14
+ class Error < StandardError
15
+ end
16
+
17
+ class Configuration
18
+ attr_accessor auto_include: bool
19
+ attr_accessor custom_actions: Array[String]
20
+
21
+ def initialize: () -> void
22
+ end
23
+
24
+ module Matchers
25
+ def have_turbo_stream: () -> HaveTurboStream
26
+ def assert_no_turbo_stream: () -> HaveTurboStream
27
+ def have_turbo_streams: (*HaveTurboStream matchers) -> HaveTurboStreams
28
+ def have_turbo_frame: () -> HaveTurboFrame
29
+ def have_broadcasted_turbo_stream_to: (String | untyped stream_or_object) -> HaveBroadcastedTurboStreamTo
30
+ def broadcast_turbo_stream_to: (String | untyped stream_or_object) -> HaveBroadcastedTurboStreamTo
31
+
32
+ class HaveTurboStream
33
+ def initialize: () -> void
34
+
35
+ # Constraint chain
36
+ def with_action: (Symbol | String action) -> self
37
+ def targeting: (String dom_id) -> self
38
+ def targeting_all: (String selector) -> self
39
+ def with_content: (String text) -> self
40
+ def rendering: (String partial) -> self
41
+ def with_attributes: (Hash[String | Symbol, untyped] attrs) -> self
42
+ def children_only: () -> self
43
+
44
+ # Count qualifiers
45
+ def once: () -> self
46
+ def twice: () -> self
47
+ def exactly: (Integer n) -> self
48
+ def at_least: (Integer n) -> self
49
+ def at_most: (Integer n) -> self
50
+ def times: () -> self
51
+
52
+ # RSpec matcher interface
53
+ def matches?: (untyped response_or_body) -> bool
54
+ def does_not_match?: (untyped response_or_body) -> bool
55
+ def failure_message: () -> String
56
+ def failure_message_when_negated: () -> String
57
+ def description: () -> String
58
+ end
59
+
60
+ class HaveTurboFrame
61
+ def initialize: () -> void
62
+
63
+ # Constraint chain
64
+ def with_id: (String id) -> self
65
+ def with_content: (String text) -> self
66
+ def rendering: (String partial) -> self
67
+ def with_attributes: (Hash[String | Symbol, untyped] attrs) -> self
68
+
69
+ # RSpec matcher interface
70
+ def matches?: (untyped response_or_body) -> bool
71
+ def does_not_match?: (untyped response_or_body) -> bool
72
+ def failure_message: () -> String
73
+ def failure_message_when_negated: () -> String
74
+ def description: () -> String
75
+ end
76
+
77
+ class HaveBroadcastedTurboStreamTo
78
+ def initialize: (String | untyped stream_or_object) -> void
79
+
80
+ # Constraint chain (mirrors HaveTurboStream)
81
+ def with_action: (Symbol | String action) -> self
82
+ def targeting: (String dom_id) -> self
83
+ def targeting_all: (String selector) -> self
84
+ def with_content: (String text) -> self
85
+ def rendering: (String partial) -> self
86
+
87
+ # Count qualifiers
88
+ def once: () -> self
89
+ def twice: () -> self
90
+ def exactly: (Integer n) -> self
91
+ def at_least: (Integer n) -> self
92
+ def at_most: (Integer n) -> self
93
+ def times: () -> self
94
+
95
+ # RSpec block-expectation matcher interface
96
+ def supports_block_expectations?: () -> bool
97
+ def matches?: (untyped block) -> bool
98
+ def does_not_match?: (untyped block) -> bool
99
+ def failure_message: () -> String
100
+ def failure_message_when_negated: () -> String
101
+ def description: () -> String
102
+ end
103
+
104
+ class HaveTurboStreams
105
+ def initialize: (Array[HaveTurboStream] expected_streams) -> void
106
+ def matches?: (untyped response_or_body) -> bool
107
+ def does_not_match?: (untyped response_or_body) -> bool
108
+ def failure_message: () -> String
109
+ def failure_message_when_negated: () -> String
110
+ def description: () -> String
111
+ end
112
+ end
113
+
114
+ module Helpers
115
+ def turbo_stream_html: (action: Symbol | String, ?target: String?, ?targets: String?, ?content: String?) -> String
116
+ def turbo_frame_html: (id: String, ?content: String?) -> String
117
+ end
118
+
119
+ module Assertions
120
+ def assert_turbo_stream: (untyped response_or_body, ?action: (Symbol | String)?, ?target: String?, ?targets: String?, ?content: String?, ?partial: String?, ?message: String?) -> void
121
+ def refute_turbo_stream: (untyped response_or_body, ?action: (Symbol | String)?, ?target: String?, ?targets: String?, ?content: String?, ?partial: String?, ?message: String?) -> void
122
+ def assert_turbo_frame: (untyped response_or_body, ?id: String?, ?content: String?, ?partial: String?, ?message: String?) -> void
123
+ def refute_turbo_frame: (untyped response_or_body, ?id: String?, ?content: String?, ?partial: String?, ?message: String?) -> void
124
+ end
125
+
126
+ module Capybara
127
+ module Matchers
128
+ def have_turbo_frame: (String id) -> HaveTurboFrame
129
+ def have_turbo_stream_tag: (?String? signed_stream_name) -> HaveTurboStreamTag
130
+ def within_turbo_frame: (String id) { () -> void } -> void
131
+
132
+ class HaveTurboFrame
133
+ def initialize: (String id) -> void
134
+
135
+ # Constraint chain
136
+ def with_content: (String text) -> self
137
+ def loaded: () -> self
138
+ def with_src: (String url) -> self
139
+ def lazy: () -> self
140
+
141
+ # Capybara matcher interface
142
+ def matches?: (untyped page_or_node) -> bool
143
+ def does_not_match?: (untyped page_or_node) -> bool
144
+ def failure_message: () -> String
145
+ def failure_message_when_negated: () -> String
146
+ def description: () -> String
147
+ end
148
+
149
+ class HaveTurboStreamTag
150
+ def initialize: (?signed_stream_name: String?) -> void
151
+ def matches?: (untyped page_or_node) -> bool
152
+ def does_not_match?: (untyped page_or_node) -> bool
153
+ def failure_message: () -> String
154
+ def failure_message_when_negated: () -> String
155
+ def description: () -> String
156
+ end
157
+ end
158
+ end
159
+ end
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: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -34,6 +34,7 @@ executables: []
34
34
  extensions: []
35
35
  extra_rdoc_files: []
36
36
  files:
37
+ - ".github/FUNDING.yml"
37
38
  - ".github/workflows/ci.yml"
38
39
  - ".github/workflows/publish.yml"
39
40
  - ".rubocop.yml"