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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +40 -0
- data/ROADMAP.md +0 -7
- 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/match_turbo_stream_snapshot.rb +86 -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 +22 -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: 0a09c91b6fcf16983b8ebbdf20dac6bfb350a9fea07e1779541a05200ba9fc16
|
|
4
|
+
data.tar.gz: 43d963aacdbc42a3d90ff131953418c07063e78537bec4cabde4f9189416f06f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
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
|
|
@@ -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.
|
|
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
|