turbo_rspec 1.3.0 → 1.5.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/CHANGELOG.md +18 -0
- data/README.md +48 -0
- data/ROADMAP.md +4 -5
- data/lib/rubocop/cop/turbo_rspec/use_have_turbo_stream.rb +53 -0
- data/lib/turbo_rspec/configuration.rb +7 -0
- data/lib/turbo_rspec/matchers/have_broadcasted_turbo_stream_to.rb +103 -6
- data/lib/turbo_rspec/matchers/match_turbo_stream_snapshot.rb +102 -0
- data/lib/turbo_rspec/matchers.rb +10 -0
- data/lib/turbo_rspec/rubocop.rb +4 -0
- data/lib/turbo_rspec/version.rb +1 -1
- data/sig/turbo_rspec.rbs +24 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d66e89583c0c374bd4b69c308390e275e70623726fd8cdc6f7f44e4df7028e06
|
|
4
|
+
data.tar.gz: a0be5a9653a1cfc59604b506ac2d890db6f1fd931800e21f9eccf528895d3bb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49f3d0f02e97d332ed7a8c9bc28bd5c555c7ce8d969647a691e38d064afe94a77e8f0b1bd4c42b3138917c0291031aab073ae132482db60569c252acbe889c97
|
|
7
|
+
data.tar.gz: 41dad12a7885d0091afc1d5fa01b322f884c295b12ae753795c59ba86bbf86123281c768415c9bfa89c79d3f2812210680fcb7fd19e7f66a402db0b31cf7106e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.5.0] - 2026-06-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `match_turbo_stream_snapshot` failure message now shows a line-by-line inline diff (stored → actual) instead of raw before/after strings
|
|
8
|
+
- `have_broadcasted_turbo_stream_to` failure message now lists all streams broadcast to the channel and shows a closest-match diff with per-constraint ✓/✗ indicators — mirrors the behaviour already present in `have_turbo_stream`
|
|
9
|
+
- `have_broadcasted_turbo_stream_to` now validates action names via `TurboRspec.known_actions`, raising `ArgumentError` for unrecognised actions — consistent with `have_turbo_stream`
|
|
10
|
+
- `have_broadcasted_turbo_stream_to.with_attributes(hash)` — assert arbitrary HTML attributes on broadcast stream elements
|
|
11
|
+
- `have_broadcasted_turbo_stream_to.children_only` — assert the `[children-only]` attribute on morph broadcasts
|
|
12
|
+
|
|
13
|
+
## [1.4.0] - 2026-06-02
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `match_turbo_stream_snapshot(name)` — records the turbo stream response on the first run and diffs against it on subsequent runs; set `UPDATE_TURBO_SNAPSHOTS=1` to overwrite
|
|
18
|
+
- `TurboRspec::Cop::UseHaveTurboStream` RuboCop cop — flags `expect(response.body).to include("<turbo-stream")` and similar raw-body patterns; load via `require "turbo_rspec/rubocop"` in `.rubocop.yml`
|
|
19
|
+
- `TurboRspec.configuration.snapshot_dir` — configure snapshot storage path (default: `spec/snapshots/turbo`)
|
|
20
|
+
|
|
3
21
|
## [1.3.0] - 2026-06-02
|
|
4
22
|
|
|
5
23
|
### Added
|
data/README.md
CHANGED
|
@@ -183,6 +183,14 @@ expect { MyJob.perform_now }.to have_broadcasted_turbo_stream_to("notifications"
|
|
|
183
183
|
.targeting("messages")
|
|
184
184
|
.with_content("Hello")
|
|
185
185
|
|
|
186
|
+
# With arbitrary attributes
|
|
187
|
+
expect { MyJob.perform_now }.to have_broadcasted_turbo_stream_to("notifications")
|
|
188
|
+
.with_attributes("data-controller" => "messages")
|
|
189
|
+
|
|
190
|
+
# Turbo 8 morph with children-only
|
|
191
|
+
expect { MyJob.perform_now }.to have_broadcasted_turbo_stream_to("body")
|
|
192
|
+
.with_action(:morph).children_only
|
|
193
|
+
|
|
186
194
|
# Count qualifiers
|
|
187
195
|
expect { MyJob.perform_now }.to have_broadcasted_turbo_stream_to("notifications").once
|
|
188
196
|
expect { MyJob.perform_now }.to have_broadcasted_turbo_stream_to("notifications").exactly(3).times
|
|
@@ -274,6 +282,46 @@ expect(page).to have_turbo_stream_tag("signed_stream_name")
|
|
|
274
282
|
expect(page).not_to have_turbo_stream_tag
|
|
275
283
|
```
|
|
276
284
|
|
|
285
|
+
### `match_turbo_stream_snapshot`
|
|
286
|
+
|
|
287
|
+
Record a turbo stream response on the first run and diff against it on subsequent runs. Good for complex multi-stream responses where specifying every constraint inline is noisy.
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
# First run — writes spec/snapshots/turbo/messages/new.turbo
|
|
291
|
+
expect(response).to match_turbo_stream_snapshot("messages/new")
|
|
292
|
+
|
|
293
|
+
# Subsequent runs — diffs against stored snapshot
|
|
294
|
+
expect(response).to match_turbo_stream_snapshot("messages/new")
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Set `UPDATE_TURBO_SNAPSHOTS=1` to overwrite an existing snapshot. Configure the storage directory:
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
# spec/support/turbo_rspec.rb
|
|
301
|
+
TurboRspec.configure do |config|
|
|
302
|
+
config.snapshot_dir = "spec/fixtures/turbo_snapshots"
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### RuboCop cop
|
|
307
|
+
|
|
308
|
+
Load the `TurboRspec/UseHaveTurboStream` cop to catch raw `response.body` assertions:
|
|
309
|
+
|
|
310
|
+
```yaml
|
|
311
|
+
# .rubocop.yml
|
|
312
|
+
require:
|
|
313
|
+
- turbo_rspec/rubocop
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
# Flagged
|
|
318
|
+
expect(response.body).to include("<turbo-stream")
|
|
319
|
+
expect(response.body).to match(/turbo-stream/)
|
|
320
|
+
|
|
321
|
+
# Preferred
|
|
322
|
+
expect(response).to have_turbo_stream.with_action(:append)
|
|
323
|
+
```
|
|
324
|
+
|
|
277
325
|
## Test helpers
|
|
278
326
|
|
|
279
327
|
`TurboRspec::Helpers` provides factory methods for building Turbo HTML inline in tests. Auto-included in `type: :request` and `type: :controller` example groups.
|
data/ROADMAP.md
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Streams, Turbo Frames, and ActionCable broadcasts. The goal is to replace the hand-rolled helpers that every Rails/Turbo project accumulates.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## 1.4 — Tooling
|
|
5
|
+
## 1.6 — Minitest & Turbo 8 extensions
|
|
8
6
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
7
|
+
- **`assert_broadcasted_turbo_stream_to` / `refute_broadcasted_turbo_stream_to`** — `TurboRspec::Assertions` covers request-spec stream and frame assertions but has no broadcast counterpart. Teams using Minitest with ActionCable have no parity with the RSpec broadcast matcher.
|
|
8
|
+
- **`have_turbo_frame` Capybara `.eager` and `.strict` chains** — `.lazy` is already supported; Turbo 8 also added `loading="eager"` and a `[strict]` boolean attribute for strict frame loading. Both are missing from the Capybara matcher.
|
|
9
|
+
- **Stimulus request-spec matchers** — `have_stimulus_controller`, `have_stimulus_action`, and `have_stimulus_target` currently only work in Capybara system/feature specs. Teams writing request specs can't assert Stimulus attributes on rendered HTML responses without dropping to raw string matching.
|
|
11
10
|
|
|
12
11
|
---
|
|
13
12
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module TurboRspec
|
|
6
|
+
# Flags request specs that assert turbo stream content by matching against
|
|
7
|
+
# +response.body+ as a raw string. Use +have_turbo_stream+ instead.
|
|
8
|
+
#
|
|
9
|
+
# @example Bad — string matching on response.body
|
|
10
|
+
# expect(response.body).to include("<turbo-stream")
|
|
11
|
+
# expect(response.body).to match(/turbo-stream/)
|
|
12
|
+
#
|
|
13
|
+
# @example Good
|
|
14
|
+
# expect(response).to have_turbo_stream
|
|
15
|
+
# expect(response).to have_turbo_stream.with_action(:append).targeting("list")
|
|
16
|
+
class UseHaveTurboStream < Base
|
|
17
|
+
MSG = "Use `expect(response).to have_turbo_stream` instead of " \
|
|
18
|
+
"asserting on `response.body` directly."
|
|
19
|
+
|
|
20
|
+
RESTRICT_ON_SEND = %i[to not_to].freeze
|
|
21
|
+
|
|
22
|
+
# Matches: expect(response.body).to <matcher>(...)
|
|
23
|
+
def_node_matcher :response_body_expectation?, <<~PATTERN
|
|
24
|
+
(send
|
|
25
|
+
(send nil? :expect
|
|
26
|
+
(send (send nil? :response) :body))
|
|
27
|
+
{:to :not_to}
|
|
28
|
+
...)
|
|
29
|
+
PATTERN
|
|
30
|
+
|
|
31
|
+
def on_send(node)
|
|
32
|
+
return unless response_body_expectation?(node)
|
|
33
|
+
return unless turbo_stream_related?(node)
|
|
34
|
+
|
|
35
|
+
add_offense(node)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def turbo_stream_related?(node)
|
|
41
|
+
node.descendants.any? do |n|
|
|
42
|
+
(n.str_type? && turbo_stream_string?(n.value)) ||
|
|
43
|
+
(n.regexp_type? && turbo_stream_string?(n.loc.expression.source))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def turbo_stream_string?(str)
|
|
48
|
+
str.include?("turbo-stream") || str.include?("turbo_stream")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -18,9 +18,16 @@ module TurboRspec
|
|
|
18
18
|
# @return [Array<String>]
|
|
19
19
|
attr_accessor :custom_actions
|
|
20
20
|
|
|
21
|
+
# @!attribute [rw] snapshot_dir
|
|
22
|
+
# Directory where turbo stream snapshots are stored.
|
|
23
|
+
# Defaults to +"spec/snapshots/turbo"+.
|
|
24
|
+
# @return [String]
|
|
25
|
+
attr_accessor :snapshot_dir
|
|
26
|
+
|
|
21
27
|
def initialize
|
|
22
28
|
@auto_include = true
|
|
23
29
|
@custom_actions = []
|
|
30
|
+
@snapshot_dir = "spec/snapshots/turbo"
|
|
24
31
|
end
|
|
25
32
|
end
|
|
26
33
|
end
|
|
@@ -12,6 +12,8 @@ module TurboRspec
|
|
|
12
12
|
@target_all = nil
|
|
13
13
|
@content = nil
|
|
14
14
|
@partial = nil
|
|
15
|
+
@attributes = {}
|
|
16
|
+
@children_only = false
|
|
15
17
|
@expected_count = nil
|
|
16
18
|
@count_type = :at_least
|
|
17
19
|
end
|
|
@@ -19,7 +21,12 @@ module TurboRspec
|
|
|
19
21
|
# Stream constraints (mirrors HaveTurboStream)
|
|
20
22
|
|
|
21
23
|
def with_action(action)
|
|
22
|
-
|
|
24
|
+
action_str = action.to_s
|
|
25
|
+
unless TurboRspec.known_actions.include?(action_str)
|
|
26
|
+
raise ArgumentError, "Unknown Turbo stream action #{action_str.inspect}. " \
|
|
27
|
+
"Register custom actions with TurboRspec.register_action(:#{action_str})."
|
|
28
|
+
end
|
|
29
|
+
@action = action_str
|
|
23
30
|
self
|
|
24
31
|
end
|
|
25
32
|
|
|
@@ -43,6 +50,16 @@ module TurboRspec
|
|
|
43
50
|
self
|
|
44
51
|
end
|
|
45
52
|
|
|
53
|
+
def with_attributes(attrs)
|
|
54
|
+
@attributes = attrs.transform_keys(&:to_s).transform_values(&:to_s)
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def children_only
|
|
59
|
+
@children_only = true
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
46
63
|
# Count qualifiers
|
|
47
64
|
|
|
48
65
|
def once
|
|
@@ -82,7 +99,9 @@ module TurboRspec
|
|
|
82
99
|
def matches?(block)
|
|
83
100
|
before = snapshot
|
|
84
101
|
block.call
|
|
85
|
-
@
|
|
102
|
+
@new_broadcasts = snapshot - before
|
|
103
|
+
@all_streams = parse_all_streams(@new_broadcasts)
|
|
104
|
+
@matching = @new_broadcasts.select { |msg| message_matches?(msg) }
|
|
86
105
|
count_matches?(@matching.size)
|
|
87
106
|
end
|
|
88
107
|
|
|
@@ -131,7 +150,9 @@ module TurboRspec
|
|
|
131
150
|
matches_target?(stream) &&
|
|
132
151
|
matches_target_all?(stream) &&
|
|
133
152
|
matches_content?(stream) &&
|
|
134
|
-
matches_partial?(stream)
|
|
153
|
+
matches_partial?(stream) &&
|
|
154
|
+
matches_attributes?(stream) &&
|
|
155
|
+
matches_children_only?(stream)
|
|
135
156
|
end
|
|
136
157
|
|
|
137
158
|
def matches_action?(stream)
|
|
@@ -156,6 +177,16 @@ module TurboRspec
|
|
|
156
177
|
stream.to_html.include?(@partial)
|
|
157
178
|
end
|
|
158
179
|
|
|
180
|
+
def matches_attributes?(stream)
|
|
181
|
+
return true if @attributes.empty?
|
|
182
|
+
@attributes.all? { |k, v| stream[k] == v }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def matches_children_only?(stream)
|
|
186
|
+
return true unless @children_only
|
|
187
|
+
!stream["children-only"].nil?
|
|
188
|
+
end
|
|
189
|
+
|
|
159
190
|
def count_matches?(n)
|
|
160
191
|
if @expected_count.nil?
|
|
161
192
|
n >= 1
|
|
@@ -177,6 +208,8 @@ module TurboRspec
|
|
|
177
208
|
parts << " targeting all #{@target_all.inspect}" if @target_all
|
|
178
209
|
parts << " with content #{@content.inspect}" if @content
|
|
179
210
|
parts << " rendering #{@partial.inspect}" if @partial
|
|
211
|
+
parts << " with attributes #{@attributes.inspect}" if @attributes.any?
|
|
212
|
+
parts << " children only" if @children_only
|
|
180
213
|
parts.join
|
|
181
214
|
end
|
|
182
215
|
|
|
@@ -192,11 +225,75 @@ module TurboRspec
|
|
|
192
225
|
end
|
|
193
226
|
|
|
194
227
|
def found_message
|
|
195
|
-
if @matching.
|
|
196
|
-
"but no matching broadcasts were found"
|
|
197
|
-
else
|
|
228
|
+
if @matching.size > 0
|
|
198
229
|
"found #{@matching.size} matching broadcast(s)"
|
|
230
|
+
elsif @all_streams.empty?
|
|
231
|
+
"but no broadcasts were made to #{stream_name.inspect}"
|
|
232
|
+
else
|
|
233
|
+
no_match_message
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def no_match_message
|
|
238
|
+
lines = ["found #{@all_streams.size} stream(s) broadcast to #{stream_name.inspect}:"]
|
|
239
|
+
@all_streams.each_with_index do |s, i|
|
|
240
|
+
content_preview = s.text.strip.slice(0, 50)
|
|
241
|
+
content_preview = content_preview.empty? ? "(empty)" : content_preview.inspect
|
|
242
|
+
lines << " #{i + 1}. action=#{s["action"].inspect} target=#{s["target"].inspect} content=#{content_preview}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# :nocov:
|
|
246
|
+
if constraint_count > 0
|
|
247
|
+
# :nocov:
|
|
248
|
+
closest = @all_streams.max_by { |s| count_matching_constraints(s) }
|
|
249
|
+
lines << ""
|
|
250
|
+
lines << "closest match (#{count_matching_constraints(closest)}/#{constraint_count} constraint(s) matched):"
|
|
251
|
+
lines.concat(constraint_diff(closest))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
lines.join("\n")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def parse_all_streams(broadcasts)
|
|
258
|
+
broadcasts.flat_map do |msg|
|
|
259
|
+
html = JSON.parse(msg)
|
|
260
|
+
Nokogiri::HTML5.fragment(html).css("turbo-stream").to_a
|
|
261
|
+
rescue JSON::ParserError
|
|
262
|
+
[]
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def count_matching_constraints(stream)
|
|
267
|
+
count = 0
|
|
268
|
+
count += 1 if !@action.nil? && matches_action?(stream)
|
|
269
|
+
count += 1 if !@target.nil? && matches_target?(stream)
|
|
270
|
+
count += 1 if !@target_all.nil? && matches_target_all?(stream)
|
|
271
|
+
count += 1 if !@content.nil? && matches_content?(stream)
|
|
272
|
+
count += 1 if !@partial.nil? && matches_partial?(stream)
|
|
273
|
+
count += 1 if @attributes.any? && matches_attributes?(stream)
|
|
274
|
+
count += 1 if @children_only && matches_children_only?(stream)
|
|
275
|
+
count
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def constraint_count
|
|
279
|
+
[@action, @target, @target_all, @content, @partial].count { |c| !c.nil? } +
|
|
280
|
+
(@attributes.any? ? 1 : 0) +
|
|
281
|
+
(@children_only ? 1 : 0)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def constraint_diff(stream)
|
|
285
|
+
lines = []
|
|
286
|
+
lines << " #{matches_action?(stream) ? "✓" : "✗"} action: expected #{@action.inspect}, got #{stream["action"].inspect}" if @action
|
|
287
|
+
lines << " #{matches_target?(stream) ? "✓" : "✗"} target: expected #{@target.inspect}, got #{stream["target"].inspect}" if @target
|
|
288
|
+
lines << " #{matches_target_all?(stream) ? "✓" : "✗"} targets: expected #{@target_all.inspect}, got #{stream["targets"].inspect}" if @target_all
|
|
289
|
+
lines << " #{matches_content?(stream) ? "✓" : "✗"} content: expected to include #{@content.inspect}, got #{stream.text.strip.slice(0, 50).inspect}" if @content
|
|
290
|
+
lines << " #{matches_partial?(stream) ? "✓" : "✗"} rendering: expected to include #{@partial.inspect}" if @partial
|
|
291
|
+
@attributes.each do |k, v|
|
|
292
|
+
actual = stream[k]
|
|
293
|
+
lines << " #{(actual == v) ? "✓" : "✗"} attr[#{k}]: expected #{v.inspect}, got #{actual.inspect}"
|
|
199
294
|
end
|
|
295
|
+
lines << " #{matches_children_only?(stream) ? "✓" : "✗"} children-only: expected attribute to be present" if @children_only
|
|
296
|
+
lines
|
|
200
297
|
end
|
|
201
298
|
end
|
|
202
299
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "diff/lcs"
|
|
5
|
+
|
|
6
|
+
module TurboRspec
|
|
7
|
+
module Matchers
|
|
8
|
+
# RSpec matcher that records a turbo stream response on the first run and
|
|
9
|
+
# diffs against the stored snapshot on subsequent runs.
|
|
10
|
+
#
|
|
11
|
+
# Snapshots are written to the directory configured by
|
|
12
|
+
# +TurboRspec.configuration.snapshot_dir+ (default: +spec/snapshots/turbo+).
|
|
13
|
+
# Each snapshot is stored as +{name}.turbo+.
|
|
14
|
+
#
|
|
15
|
+
# Set +UPDATE_TURBO_SNAPSHOTS=1+ to overwrite existing snapshots.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# expect(response).to match_turbo_stream_snapshot("messages/new")
|
|
19
|
+
class MatchTurboStreamSnapshot
|
|
20
|
+
def initialize(name)
|
|
21
|
+
@name = name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param response_or_body [#body, String]
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def matches?(response_or_body)
|
|
27
|
+
@actual = extract_body(response_or_body)
|
|
28
|
+
@path = snapshot_path
|
|
29
|
+
|
|
30
|
+
if update_snapshots? || !File.exist?(@path)
|
|
31
|
+
write_snapshot(@actual)
|
|
32
|
+
true
|
|
33
|
+
else
|
|
34
|
+
@stored = File.read(@path)
|
|
35
|
+
@actual.strip == @stored.strip
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param response_or_body [#body, String]
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def does_not_match?(response_or_body)
|
|
42
|
+
!matches?(response_or_body)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [String]
|
|
46
|
+
def failure_message
|
|
47
|
+
"expected response to match turbo stream snapshot #{@name.inspect}\n\n" \
|
|
48
|
+
"diff (stored → actual):\n#{inline_diff}\n\n" \
|
|
49
|
+
"To update: run with UPDATE_TURBO_SNAPSHOTS=1"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @return [String]
|
|
53
|
+
def failure_message_when_negated
|
|
54
|
+
"expected response not to match turbo stream snapshot #{@name.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [String]
|
|
58
|
+
def description
|
|
59
|
+
"match turbo stream snapshot #{@name.inspect}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def extract_body(response_or_body)
|
|
65
|
+
if response_or_body.respond_to?(:body)
|
|
66
|
+
response_or_body.body
|
|
67
|
+
else
|
|
68
|
+
response_or_body.to_s
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def snapshot_path
|
|
73
|
+
File.join(TurboRspec.configuration.snapshot_dir, "#{@name}.turbo")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def write_snapshot(content)
|
|
77
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
78
|
+
File.write(@path, content)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def update_snapshots?
|
|
82
|
+
ENV["UPDATE_TURBO_SNAPSHOTS"] == "1"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def inline_diff
|
|
86
|
+
stored_lines = @stored.strip.lines
|
|
87
|
+
actual_lines = @actual.strip.lines
|
|
88
|
+
hunks = Diff::LCS.diff(stored_lines, actual_lines)
|
|
89
|
+
return " (no textual difference — whitespace only?)" if hunks.empty?
|
|
90
|
+
|
|
91
|
+
lines = []
|
|
92
|
+
hunks.each do |hunk|
|
|
93
|
+
hunk.each do |change|
|
|
94
|
+
prefix = (change.action == "+") ? "+" : "-"
|
|
95
|
+
lines << "#{prefix} #{change.element.chomp}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
lines.join("\n")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/turbo_rspec/matchers.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "matchers/have_broadcasted_turbo_stream_to"
|
|
|
4
4
|
require_relative "matchers/have_turbo_frame"
|
|
5
5
|
require_relative "matchers/have_turbo_stream"
|
|
6
6
|
require_relative "matchers/have_turbo_streams"
|
|
7
|
+
require_relative "matchers/match_turbo_stream_snapshot"
|
|
7
8
|
|
|
8
9
|
module TurboRspec
|
|
9
10
|
# RSpec matchers for Turbo Stream and Turbo Frame assertions.
|
|
@@ -46,5 +47,14 @@ module TurboRspec
|
|
|
46
47
|
def have_turbo_streams(*matchers)
|
|
47
48
|
HaveTurboStreams.new(matchers)
|
|
48
49
|
end
|
|
50
|
+
|
|
51
|
+
# Assert that a response body matches a stored turbo stream snapshot.
|
|
52
|
+
# Creates the snapshot on the first run; diffs against it on subsequent runs.
|
|
53
|
+
# Set +UPDATE_TURBO_SNAPSHOTS=1+ to overwrite an existing snapshot.
|
|
54
|
+
# @param name [String] snapshot name, used as the file path within +snapshot_dir+
|
|
55
|
+
# @return [MatchTurboStreamSnapshot]
|
|
56
|
+
def match_turbo_stream_snapshot(name)
|
|
57
|
+
MatchTurboStreamSnapshot.new(name)
|
|
58
|
+
end
|
|
49
59
|
end
|
|
50
60
|
end
|
data/lib/turbo_rspec/version.rb
CHANGED
data/sig/turbo_rspec.rbs
CHANGED
|
@@ -17,6 +17,7 @@ module TurboRspec
|
|
|
17
17
|
class Configuration
|
|
18
18
|
attr_accessor auto_include: bool
|
|
19
19
|
attr_accessor custom_actions: Array[String]
|
|
20
|
+
attr_accessor snapshot_dir: String
|
|
20
21
|
|
|
21
22
|
def initialize: () -> void
|
|
22
23
|
end
|
|
@@ -25,6 +26,7 @@ module TurboRspec
|
|
|
25
26
|
def have_turbo_stream: () -> HaveTurboStream
|
|
26
27
|
def assert_no_turbo_stream: () -> HaveTurboStream
|
|
27
28
|
def have_turbo_streams: (*HaveTurboStream matchers) -> HaveTurboStreams
|
|
29
|
+
def match_turbo_stream_snapshot: (String name) -> MatchTurboStreamSnapshot
|
|
28
30
|
def have_turbo_frame: () -> HaveTurboFrame
|
|
29
31
|
def have_broadcasted_turbo_stream_to: (String | untyped stream_or_object) -> HaveBroadcastedTurboStreamTo
|
|
30
32
|
def broadcast_turbo_stream_to: (String | untyped stream_or_object) -> HaveBroadcastedTurboStreamTo
|
|
@@ -83,6 +85,8 @@ module TurboRspec
|
|
|
83
85
|
def targeting_all: (String selector) -> self
|
|
84
86
|
def with_content: (String text) -> self
|
|
85
87
|
def rendering: (String partial) -> self
|
|
88
|
+
def with_attributes: (Hash[String | Symbol, untyped] attrs) -> self
|
|
89
|
+
def children_only: () -> self
|
|
86
90
|
|
|
87
91
|
# Count qualifiers
|
|
88
92
|
def once: () -> self
|
|
@@ -101,6 +105,15 @@ module TurboRspec
|
|
|
101
105
|
def description: () -> String
|
|
102
106
|
end
|
|
103
107
|
|
|
108
|
+
class MatchTurboStreamSnapshot
|
|
109
|
+
def initialize: (String name) -> void
|
|
110
|
+
def matches?: (untyped response_or_body) -> bool
|
|
111
|
+
def does_not_match?: (untyped response_or_body) -> bool
|
|
112
|
+
def failure_message: () -> String
|
|
113
|
+
def failure_message_when_negated: () -> String
|
|
114
|
+
def description: () -> String
|
|
115
|
+
end
|
|
116
|
+
|
|
104
117
|
class HaveTurboStreams
|
|
105
118
|
def initialize: (Array[HaveTurboStream] expected_streams) -> void
|
|
106
119
|
def matches?: (untyped response_or_body) -> bool
|
|
@@ -186,4 +199,15 @@ module TurboRspec
|
|
|
186
199
|
end
|
|
187
200
|
end
|
|
188
201
|
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
module RuboCop
|
|
205
|
+
module Cop
|
|
206
|
+
module TurboRspec
|
|
207
|
+
class UseHaveTurboStream < Base
|
|
208
|
+
MSG: String
|
|
209
|
+
RESTRICT_ON_SEND: Array[Symbol]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
189
213
|
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.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- lib/generators/turbo_rspec/install_generator.rb
|
|
52
52
|
- lib/generators/turbo_rspec/templates/README
|
|
53
53
|
- lib/generators/turbo_rspec/templates/turbo_rspec.rb
|
|
54
|
+
- lib/rubocop/cop/turbo_rspec/use_have_turbo_stream.rb
|
|
54
55
|
- lib/turbo_rspec.rb
|
|
55
56
|
- lib/turbo_rspec/assertions.rb
|
|
56
57
|
- lib/turbo_rspec/capybara/matchers.rb
|
|
@@ -66,6 +67,8 @@ files:
|
|
|
66
67
|
- lib/turbo_rspec/matchers/have_turbo_frame.rb
|
|
67
68
|
- lib/turbo_rspec/matchers/have_turbo_stream.rb
|
|
68
69
|
- lib/turbo_rspec/matchers/have_turbo_streams.rb
|
|
70
|
+
- lib/turbo_rspec/matchers/match_turbo_stream_snapshot.rb
|
|
71
|
+
- lib/turbo_rspec/rubocop.rb
|
|
69
72
|
- lib/turbo_rspec/shared_examples.rb
|
|
70
73
|
- lib/turbo_rspec/version.rb
|
|
71
74
|
- sig/turbo_rspec.rbs
|