turbo_rspec 1.3.0 → 1.4.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: cdd85fcc9f87814d171569e835ad85de4767daf48642d5797f15ac02decf4641
4
- data.tar.gz: a92c047884633fd146aa6806dc350edef72bd18747c39e3da618feb12223a29e
3
+ metadata.gz: 0a09c91b6fcf16983b8ebbdf20dac6bfb350a9fea07e1779541a05200ba9fc16
4
+ data.tar.gz: 43d963aacdbc42a3d90ff131953418c07063e78537bec4cabde4f9189416f06f
5
5
  SHA512:
6
- metadata.gz: 7ecdc1dc0c21c083a32270e7ab085e31e5fd4e6afde0193c9f58a967e7d79aaf543387e874d8cc2fd4f1afdcb0b56a2b2b53b6c6a230f3d80a41736b68c5c01e
7
- data.tar.gz: b12927d25812f2991d889a7cedc37fe02cf4feb439adc76555dcfc8d6fd31151f9dd5cd80257ec82057d350c52e607cc2206e924bf3b159257453de9fad4ced5
6
+ metadata.gz: 5d01faa2f37db0c9ea82322fd3445a70a8c0201f9176a0f578f6d87a84954ce2f0dbecc5ccaeb6a347aad02d1d47ab16c2fb9568eb0b0b19a2230a6f06917267
7
+ data.tar.gz: a3958b07e62ff57af1d72540364df29e374b912752a16c0faf7a3533ed8a08f4fb51dc54ad691c3a67fae39f3aac378f40f16af3d3b567650fc469d076db1707
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.4.0] - 2026-06-02
4
+
5
+ ### Added
6
+
7
+ - `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
8
+ - `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`
9
+ - `TurboRspec.configuration.snapshot_dir` — configure snapshot storage path (default: `spec/snapshots/turbo`)
10
+
3
11
  ## [1.3.0] - 2026-06-02
4
12
 
5
13
  ### Added
data/README.md CHANGED
@@ -274,6 +274,46 @@ expect(page).to have_turbo_stream_tag("signed_stream_name")
274
274
  expect(page).not_to have_turbo_stream_tag
275
275
  ```
276
276
 
277
+ ### `match_turbo_stream_snapshot`
278
+
279
+ 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.
280
+
281
+ ```ruby
282
+ # First run — writes spec/snapshots/turbo/messages/new.turbo
283
+ expect(response).to match_turbo_stream_snapshot("messages/new")
284
+
285
+ # Subsequent runs — diffs against stored snapshot
286
+ expect(response).to match_turbo_stream_snapshot("messages/new")
287
+ ```
288
+
289
+ Set `UPDATE_TURBO_SNAPSHOTS=1` to overwrite an existing snapshot. Configure the storage directory:
290
+
291
+ ```ruby
292
+ # spec/support/turbo_rspec.rb
293
+ TurboRspec.configure do |config|
294
+ config.snapshot_dir = "spec/fixtures/turbo_snapshots"
295
+ end
296
+ ```
297
+
298
+ ### RuboCop cop
299
+
300
+ Load the `TurboRspec/UseHaveTurboStream` cop to catch raw `response.body` assertions:
301
+
302
+ ```yaml
303
+ # .rubocop.yml
304
+ require:
305
+ - turbo_rspec/rubocop
306
+ ```
307
+
308
+ ```ruby
309
+ # Flagged
310
+ expect(response.body).to include("<turbo-stream")
311
+ expect(response.body).to match(/turbo-stream/)
312
+
313
+ # Preferred
314
+ expect(response).to have_turbo_stream.with_action(:append)
315
+ ```
316
+
277
317
  ## Test helpers
278
318
 
279
319
  `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
@@ -4,13 +4,6 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
4
4
 
5
5
  ---
6
6
 
7
- ## 1.4 — Tooling
8
-
9
- - **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.
10
- - **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
-
12
- ---
13
-
14
7
  ## Guiding principles
15
8
 
16
9
  - **Zero magic by default.** Auto-include only when it's unambiguous (Rails request specs). Everything else is opt-in.
@@ -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
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module TurboRspec
6
+ module Matchers
7
+ # RSpec matcher that records a turbo stream response on the first run and
8
+ # diffs against the stored snapshot on subsequent runs.
9
+ #
10
+ # Snapshots are written to the directory configured by
11
+ # +TurboRspec.configuration.snapshot_dir+ (default: +spec/snapshots/turbo+).
12
+ # Each snapshot is stored as +{name}.turbo+.
13
+ #
14
+ # Set +UPDATE_TURBO_SNAPSHOTS=1+ to overwrite existing snapshots.
15
+ #
16
+ # @example
17
+ # expect(response).to match_turbo_stream_snapshot("messages/new")
18
+ class MatchTurboStreamSnapshot
19
+ def initialize(name)
20
+ @name = name
21
+ end
22
+
23
+ # @param response_or_body [#body, String]
24
+ # @return [Boolean]
25
+ def matches?(response_or_body)
26
+ @actual = extract_body(response_or_body)
27
+ @path = snapshot_path
28
+
29
+ if update_snapshots? || !File.exist?(@path)
30
+ write_snapshot(@actual)
31
+ true
32
+ else
33
+ @stored = File.read(@path)
34
+ @actual.strip == @stored.strip
35
+ end
36
+ end
37
+
38
+ # @param response_or_body [#body, String]
39
+ # @return [Boolean]
40
+ def does_not_match?(response_or_body)
41
+ !matches?(response_or_body)
42
+ end
43
+
44
+ # @return [String]
45
+ def failure_message
46
+ "expected response to match turbo stream snapshot #{@name.inspect}\n\n" \
47
+ "stored:\n#{@stored}\n\n" \
48
+ "actual:\n#{@actual}\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
+ end
85
+ end
86
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require_relative "../rubocop/cop/turbo_rspec/use_have_turbo_stream"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboRspec
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
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
@@ -101,6 +103,15 @@ module TurboRspec
101
103
  def description: () -> String
102
104
  end
103
105
 
106
+ class MatchTurboStreamSnapshot
107
+ def initialize: (String name) -> void
108
+ def matches?: (untyped response_or_body) -> bool
109
+ def does_not_match?: (untyped response_or_body) -> bool
110
+ def failure_message: () -> String
111
+ def failure_message_when_negated: () -> String
112
+ def description: () -> String
113
+ end
114
+
104
115
  class HaveTurboStreams
105
116
  def initialize: (Array[HaveTurboStream] expected_streams) -> void
106
117
  def matches?: (untyped response_or_body) -> bool
@@ -186,4 +197,15 @@ module TurboRspec
186
197
  end
187
198
  end
188
199
  end
200
+ end
201
+
202
+ module RuboCop
203
+ module Cop
204
+ module TurboRspec
205
+ class UseHaveTurboStream < Base
206
+ MSG: String
207
+ RESTRICT_ON_SEND: Array[Symbol]
208
+ end
209
+ end
210
+ end
189
211
  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.3.0
4
+ version: 1.4.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