turbo_rspec 0.3.0 → 0.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 +21 -0
- data/ROADMAP.md +23 -16
- data/lib/turbo_rspec/matchers/have_turbo_frame.rb +38 -5
- data/lib/turbo_rspec/matchers/have_turbo_stream.rb +42 -5
- data/lib/turbo_rspec/matchers/have_turbo_streams.rb +65 -0
- data/lib/turbo_rspec/matchers.rb +7 -0
- data/lib/turbo_rspec/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9752bc68a0676737d5e31eda8279871c6aa23e09c0cafb6eab046ae153f8a103
|
|
4
|
+
data.tar.gz: d6a13d4d95ee05e4fb6221a36aaebc807e367d0a313b9f8e00eaa307138183dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78309d93bfac9a17816989f570dd287f10ea149bb4781be1a6e514449387862d46cf93cf30914e456bc5d0c88c7585aff998fe62831902688edd0a3081007f86
|
|
7
|
+
data.tar.gz: dc224517ae6717151a75f01c45028732949fe06fc65248b20dc17807318a363b1aa96c375c10c2dcbb7ebadab686a3cf4d859105d15b1a00b579c1dba4478f18
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `have_turbo_streams(*matchers)` — assert multiple streams in one expectation; failure lists each unmatched stream
|
|
8
|
+
- `assert_no_turbo_stream` — alias of `have_turbo_stream` for teams that mix RSpec/minitest terminology
|
|
9
|
+
- Rich failure messages for `have_turbo_stream` and `have_turbo_frame`: content preview on each found element, plus a "closest match" section with per-constraint pass (✓) / fail (✗) indicators
|
|
10
|
+
|
|
3
11
|
## [0.3.0] - 2026-05-28
|
|
4
12
|
|
|
5
13
|
### Added
|
data/README.md
CHANGED
|
@@ -89,6 +89,27 @@ expect(response).not_to have_turbo_stream.with_action(:replace)
|
|
|
89
89
|
|
|
90
90
|
Turbo supports the following stream actions: `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `refresh`.
|
|
91
91
|
|
|
92
|
+
### `have_turbo_streams`
|
|
93
|
+
|
|
94
|
+
Assert that a response contains **all** of the specified streams in one expectation.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
expect(response).to have_turbo_streams(
|
|
98
|
+
have_turbo_stream.with_action(:append).targeting("messages"),
|
|
99
|
+
have_turbo_stream.with_action(:replace).targeting("header")
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
When a stream is missing the failure message lists each unmatched matcher so you can see at a glance which ones failed.
|
|
104
|
+
|
|
105
|
+
### `assert_no_turbo_stream`
|
|
106
|
+
|
|
107
|
+
Alias of `have_turbo_stream` for teams that mix RSpec and minitest terminology.
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
expect(response).not_to assert_no_turbo_stream
|
|
111
|
+
```
|
|
112
|
+
|
|
92
113
|
### `have_turbo_frame`
|
|
93
114
|
|
|
94
115
|
Assert that a response body contains a `<turbo-frame>` element.
|
data/ROADMAP.md
CHANGED
|
@@ -5,26 +5,13 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## v0.4.0 — Developer experience pass
|
|
12
|
-
|
|
13
|
-
**Goal:** make failure output good enough that you never have to drop into a debugger just to read a matcher failure.
|
|
14
|
-
|
|
15
|
-
- Rich failure messages: show actual stream actions/targets found vs. expected
|
|
16
|
-
- `assert_no_turbo_stream` alias for teams that mix RSpec/minitest terminology
|
|
17
|
-
- Composable matchers: `include(have_turbo_stream(...), have_turbo_stream(...))` for multi-stream assertions
|
|
18
|
-
- `have_turbo_streams` (plural) — assert multiple streams in one expectation with an array DSL
|
|
19
|
-
- Support for `aggregate_failures` blocks
|
|
20
|
-
|
|
21
8
|
---
|
|
22
9
|
|
|
23
10
|
## v0.5.0 — Compatibility and edge cases
|
|
24
11
|
|
|
25
12
|
**Goal:** harden the gem against real-world app variations.
|
|
26
13
|
|
|
27
|
-
- Rails 7.
|
|
14
|
+
- Rails 7.2/8.0/8.1 and Turbo 1.x/2.x compatibility matrix in CI (7.1 is EOL)
|
|
28
15
|
- Multi-stream response body parsing (a single response can contain multiple `<turbo-stream>` tags)
|
|
29
16
|
- `refresh` action support (Turbo 8 page refresh streams)
|
|
30
17
|
- `morph` action support (Turbo Morphing)
|
|
@@ -33,14 +20,34 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
|
|
|
33
20
|
|
|
34
21
|
---
|
|
35
22
|
|
|
23
|
+
## v0.6.0 — Testing utilities
|
|
24
|
+
|
|
25
|
+
**Goal:** reduce boilerplate in real test suites and close the controller spec gap.
|
|
26
|
+
|
|
27
|
+
- `turbo_stream_html(action:, target:, content: nil)` — factory helper for building `<turbo-stream>` HTML inline in tests
|
|
28
|
+
- Shared examples: `it_behaves_like "a turbo stream response"` and `"a turbo frame response"` for common assertions
|
|
29
|
+
- Controller spec support — `have_turbo_stream` and `have_turbo_frame` working against `response` in `type: :controller`
|
|
30
|
+
- Auto-include `TurboRspec::Matchers` into `type: :controller` when `turbo-rails` is present
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## v0.7.0 — Documentation
|
|
35
|
+
|
|
36
|
+
**Goal:** full docs before freezing the API.
|
|
37
|
+
|
|
38
|
+
- Full YARD documentation on all public methods and classes
|
|
39
|
+
- Migration guide: "replacing hand-rolled Turbo helpers in your test suite"
|
|
40
|
+
- Cookbook: common patterns (lazy-loaded frames, job broadcast testing, multi-stream responses, controller specs)
|
|
41
|
+
- Hosted on RubyDoc.info
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
36
45
|
## v1.0.0 — Stable API
|
|
37
46
|
|
|
38
47
|
**Goal:** API freeze. Commit to semver stability. Make the gem the obvious default choice.
|
|
39
48
|
|
|
40
49
|
- API stability guarantee: no breaking changes without a major version bump
|
|
41
50
|
- `TurboRspec::VERSION` semantic versioning enforced via CI check
|
|
42
|
-
- Migration guide: "replacing hand-rolled Turbo Stream helpers" in the docs
|
|
43
|
-
- Full YARD documentation with `yard` and hosted on RubyDoc.info
|
|
44
51
|
- 100% branch coverage enforced in CI (`simplecov`)
|
|
45
52
|
- Performance: benchmark matcher overhead to keep it negligible in large suites
|
|
46
53
|
- `bin/release` script (mirrors solid_queue_web pattern): bump version, update CHANGELOG, tag, push; CI publishes via Trusted Publishing
|
|
@@ -91,12 +91,45 @@ module TurboRspec
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def found_frames_message
|
|
94
|
-
if @frames.empty?
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
return "but no turbo frames were found in the response" if @frames.empty?
|
|
95
|
+
|
|
96
|
+
lines = ["found #{@frames.size} turbo frame(s):"]
|
|
97
|
+
@frames.each_with_index do |f, i|
|
|
98
|
+
content_preview = f.text.strip.slice(0, 50)
|
|
99
|
+
content_preview = content_preview.empty? ? "(empty)" : content_preview.inspect
|
|
100
|
+
lines << " #{i + 1}. id=#{f["id"].inspect} content=#{content_preview}"
|
|
99
101
|
end
|
|
102
|
+
|
|
103
|
+
closest = closest_match
|
|
104
|
+
lines << ""
|
|
105
|
+
lines << "closest match (#{count_matching_constraints(closest)}/#{constraint_count} constraint(s) matched):"
|
|
106
|
+
lines.concat(constraint_diff(closest))
|
|
107
|
+
|
|
108
|
+
lines.join("\n")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def closest_match
|
|
112
|
+
@frames.max_by { |f| count_matching_constraints(f) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def count_matching_constraints(frame)
|
|
116
|
+
count = 0
|
|
117
|
+
count += 1 if !@id.nil? && matches_id?(frame)
|
|
118
|
+
count += 1 if !@content.nil? && matches_content?(frame)
|
|
119
|
+
count += 1 if !@partial.nil? && matches_partial?(frame)
|
|
120
|
+
count
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def constraint_count
|
|
124
|
+
[@id, @content, @partial].count { |c| !c.nil? }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def constraint_diff(frame)
|
|
128
|
+
lines = []
|
|
129
|
+
lines << " #{matches_id?(frame) ? "✓" : "✗"} id: expected #{@id.inspect}, got #{frame["id"].inspect}" if @id
|
|
130
|
+
lines << " #{matches_content?(frame) ? "✓" : "✗"} content: expected to include #{@content.inspect}, got #{frame.text.strip.slice(0, 50).inspect}" if @content
|
|
131
|
+
lines << " #{matches_partial?(frame) ? "✓" : "✗"} rendering: expected to include #{@partial.inspect}" if @partial
|
|
132
|
+
lines
|
|
100
133
|
end
|
|
101
134
|
end
|
|
102
135
|
end
|
|
@@ -115,12 +115,49 @@ module TurboRspec
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def found_streams_message
|
|
118
|
-
if @streams.empty?
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
return "but no turbo streams were found in the response" if @streams.empty?
|
|
119
|
+
|
|
120
|
+
lines = ["found #{@streams.size} turbo stream(s):"]
|
|
121
|
+
@streams.each_with_index do |s, i|
|
|
122
|
+
content_preview = s.text.strip.slice(0, 50)
|
|
123
|
+
content_preview = content_preview.empty? ? "(empty)" : content_preview.inspect
|
|
124
|
+
lines << " #{i + 1}. action=#{s["action"].inspect} target=#{s["target"].inspect} content=#{content_preview}"
|
|
123
125
|
end
|
|
126
|
+
|
|
127
|
+
closest = closest_match
|
|
128
|
+
lines << ""
|
|
129
|
+
lines << "closest match (#{count_matching_constraints(closest)}/#{constraint_count} constraint(s) matched):"
|
|
130
|
+
lines.concat(constraint_diff(closest))
|
|
131
|
+
|
|
132
|
+
lines.join("\n")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def closest_match
|
|
136
|
+
@streams.max_by { |s| count_matching_constraints(s) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def count_matching_constraints(stream)
|
|
140
|
+
count = 0
|
|
141
|
+
count += 1 if !@action.nil? && matches_action?(stream)
|
|
142
|
+
count += 1 if !@target.nil? && matches_target?(stream)
|
|
143
|
+
count += 1 if !@target_all.nil? && matches_target_all?(stream)
|
|
144
|
+
count += 1 if !@content.nil? && matches_content?(stream)
|
|
145
|
+
count += 1 if !@partial.nil? && matches_partial?(stream)
|
|
146
|
+
count
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def constraint_count
|
|
150
|
+
[@action, @target, @target_all, @content, @partial].count { |c| !c.nil? }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def constraint_diff(stream)
|
|
154
|
+
lines = []
|
|
155
|
+
lines << " #{matches_action?(stream) ? "✓" : "✗"} action: expected #{@action.inspect}, got #{stream["action"].inspect}" if @action
|
|
156
|
+
lines << " #{matches_target?(stream) ? "✓" : "✗"} target: expected #{@target.inspect}, got #{stream["target"].inspect}" if @target
|
|
157
|
+
lines << " #{matches_target_all?(stream) ? "✓" : "✗"} targets: expected #{@target_all.inspect}, got #{stream["targets"].inspect}" if @target_all
|
|
158
|
+
lines << " #{matches_content?(stream) ? "✓" : "✗"} content: expected to include #{@content.inspect}, got #{stream.text.strip.slice(0, 50).inspect}" if @content
|
|
159
|
+
lines << " #{matches_partial?(stream) ? "✓" : "✗"} rendering: expected to include #{@partial.inspect}" if @partial
|
|
160
|
+
lines
|
|
124
161
|
end
|
|
125
162
|
end
|
|
126
163
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module TurboRspec
|
|
6
|
+
module Matchers
|
|
7
|
+
class HaveTurboStreams
|
|
8
|
+
def initialize(expected_streams)
|
|
9
|
+
@expected_streams = expected_streams
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def matches?(response_or_body)
|
|
13
|
+
@body = extract_body(response_or_body)
|
|
14
|
+
@found = parse_streams(@body)
|
|
15
|
+
@unmatched = @expected_streams.reject { |expected| any_stream_matches?(expected) }
|
|
16
|
+
@unmatched.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def does_not_match?(response_or_body)
|
|
20
|
+
!matches?(response_or_body)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def failure_message
|
|
24
|
+
descriptions = @unmatched.map { |m| " #{m.description}" }.join("\n")
|
|
25
|
+
"expected response to contain all turbo streams, but missing:\n#{descriptions}\n\n" \
|
|
26
|
+
"found streams:\n#{found_streams_summary}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def failure_message_when_negated
|
|
30
|
+
"expected response not to contain all of the specified turbo streams"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def description
|
|
34
|
+
"have turbo streams: #{@expected_streams.map(&:description).join(", ")}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def extract_body(response_or_body)
|
|
40
|
+
if response_or_body.respond_to?(:body)
|
|
41
|
+
response_or_body.body
|
|
42
|
+
else
|
|
43
|
+
response_or_body.to_s
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_streams(body)
|
|
48
|
+
Nokogiri::HTML5.fragment(body).css("turbo-stream")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def any_stream_matches?(matcher)
|
|
52
|
+
@found.any? { |stream| stream_matches_matcher?(stream, matcher) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stream_matches_matcher?(stream, matcher)
|
|
56
|
+
matcher.send(:stream_matches?, stream)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def found_streams_summary
|
|
60
|
+
return " (none)" if @found.empty?
|
|
61
|
+
@found.map { |s| " <turbo-stream action=#{s["action"].inspect} target=#{s["target"].inspect}>" }.join("\n")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/turbo_rspec/matchers.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
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
|
+
require_relative "matchers/have_turbo_streams"
|
|
6
7
|
|
|
7
8
|
module TurboRspec
|
|
8
9
|
module Matchers
|
|
@@ -19,5 +20,11 @@ module TurboRspec
|
|
|
19
20
|
def have_turbo_stream
|
|
20
21
|
HaveTurboStream.new
|
|
21
22
|
end
|
|
23
|
+
|
|
24
|
+
alias_method :assert_no_turbo_stream, :have_turbo_stream
|
|
25
|
+
|
|
26
|
+
def have_turbo_streams(*matchers)
|
|
27
|
+
HaveTurboStreams.new(matchers)
|
|
28
|
+
end
|
|
22
29
|
end
|
|
23
30
|
end
|
data/lib/turbo_rspec/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -51,6 +51,7 @@ files:
|
|
|
51
51
|
- lib/turbo_rspec/matchers/have_broadcasted_turbo_stream_to.rb
|
|
52
52
|
- lib/turbo_rspec/matchers/have_turbo_frame.rb
|
|
53
53
|
- lib/turbo_rspec/matchers/have_turbo_stream.rb
|
|
54
|
+
- lib/turbo_rspec/matchers/have_turbo_streams.rb
|
|
54
55
|
- lib/turbo_rspec/version.rb
|
|
55
56
|
- sig/turbo_rspec.rbs
|
|
56
57
|
homepage: https://github.com/eclectic-coding/turbo_rspec
|