turbo_rspec 1.0.0 → 1.1.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/.github/FUNDING.yml +15 -0
- data/CHANGELOG.md +8 -0
- data/ROADMAP.md +18 -3
- data/lib/turbo_rspec/capybara/matchers/have_turbo_frame.rb +25 -0
- data/lib/turbo_rspec/matchers/have_turbo_stream.rb +87 -8
- 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: 8356be265d63e581709f7252b6981d9f721172d68e417e23bc02581214773629
|
|
4
|
+
data.tar.gz: 3394a171db1031c16533bde8eb141eb1cd1f40bfed737b47c4f6e80b05ca23b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2094508851098b590cd9539c01b60768a44f959b4af7bb20083e736a6e9b7631776f8bdde870fd543a7302d0fb1983a839932d8839b74de450274d00470c335
|
|
7
|
+
data.tar.gz: 06c44da054b038c40cb83b681da7d9897f3d76f436afe2adf484c1cd2cab243caf2f824d763ce958172d9d66f02c11e2646130fbe31c47a59df68ac800e0ab33
|
data/.github/FUNDING.yml
ADDED
|
@@ -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,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.1.0] - 2026-06-01
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- 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`
|
|
8
|
+
- `have_turbo_frame` (Capybara) `.with_src(url)` chain — asserts the `src` attribute of a lazy-loaded frame
|
|
9
|
+
- `have_turbo_frame` (Capybara) `.lazy` chain — asserts `loading="lazy"` is set on the frame
|
|
10
|
+
|
|
3
11
|
## [1.0.0] - 2026-05-28
|
|
4
12
|
|
|
5
13
|
### Added
|
data/ROADMAP.md
CHANGED
|
@@ -4,10 +4,25 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 1.1 — Matcher parity
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
9
|
+
- **Count qualifiers on `have_turbo_stream`** — `.once`, `.twice`, `.exactly(n).times` for request specs, matching the parity already present on `have_broadcasted_turbo_stream_to`. A single response with two `append` streams is a real case that currently requires two separate expectations.
|
|
10
|
+
- **Capybara `have_turbo_frame` additions** — `.with_src(url)` (assert the `src` attribute of a lazy frame) and `.lazy` (assert `loading="lazy"` is set). These cover the lazy-loading pattern that's very common with Turbo Frames.
|
|
11
|
+
|
|
12
|
+
## 1.2 — Turbo 8 / morphing
|
|
13
|
+
|
|
14
|
+
- **`with_attributes(hash)` chain** — generic attribute assertion for both `have_turbo_stream` and `have_turbo_frame`. Covers non-standard attributes (e.g., `data-*` passed through custom actions) without needing new named chains per attribute.
|
|
15
|
+
- **Morph-aware assertions** — `morph` is a supported action but Turbo 8's `<turbo-stream action="morph">` has distinct semantics (`[children-only]` attribute). A `.children_only` chain on `have_turbo_stream` would make morph specs readable without raw attribute checks.
|
|
16
|
+
- **Custom Turbo action support** — Turbo 8 allows registering custom stream actions. A `TurboRspec.register_action(:sparkle)` API would let teams assert their own actions without them being treated as unknown values.
|
|
17
|
+
|
|
18
|
+
## 1.3 — Stimulus companion
|
|
19
|
+
|
|
20
|
+
- **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.
|
|
21
|
+
|
|
22
|
+
## 1.4 — Tooling
|
|
23
|
+
|
|
24
|
+
- **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.
|
|
25
|
+
- **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
26
|
|
|
12
27
|
---
|
|
13
28
|
|
|
@@ -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
|
|
@@ -28,6 +28,9 @@ module TurboRspec
|
|
|
28
28
|
@target_all = nil
|
|
29
29
|
@content = nil
|
|
30
30
|
@partial = nil
|
|
31
|
+
@expected_count = nil
|
|
32
|
+
@count_type = :at_least
|
|
33
|
+
@matching_count = 0
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
# Constrains the match to streams with the given action.
|
|
@@ -70,12 +73,58 @@ module TurboRspec
|
|
|
70
73
|
self
|
|
71
74
|
end
|
|
72
75
|
|
|
76
|
+
# Asserts exactly one matching stream.
|
|
77
|
+
# @return [self]
|
|
78
|
+
def once
|
|
79
|
+
exactly(1)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Asserts exactly two matching streams.
|
|
83
|
+
# @return [self]
|
|
84
|
+
def twice
|
|
85
|
+
exactly(2)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Asserts exactly +n+ matching streams.
|
|
89
|
+
# @param n [Integer]
|
|
90
|
+
# @return [self]
|
|
91
|
+
def exactly(n)
|
|
92
|
+
@expected_count = n
|
|
93
|
+
@count_type = :exactly
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Asserts at least +n+ matching streams.
|
|
98
|
+
# @param n [Integer]
|
|
99
|
+
# @return [self]
|
|
100
|
+
def at_least(n)
|
|
101
|
+
@expected_count = n
|
|
102
|
+
@count_type = :at_least
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Asserts at most +n+ matching streams.
|
|
107
|
+
# @param n [Integer]
|
|
108
|
+
# @return [self]
|
|
109
|
+
def at_most(n)
|
|
110
|
+
@expected_count = n
|
|
111
|
+
@count_type = :at_most
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Fluent terminator so +.exactly(2).times+ reads naturally.
|
|
116
|
+
# @return [self]
|
|
117
|
+
def times
|
|
118
|
+
self
|
|
119
|
+
end
|
|
120
|
+
|
|
73
121
|
# @param response_or_body [#body, String]
|
|
74
122
|
# @return [Boolean]
|
|
75
123
|
def matches?(response_or_body)
|
|
76
124
|
@body = extract_body(response_or_body)
|
|
77
125
|
@streams = parse_streams(@body)
|
|
78
|
-
@streams.
|
|
126
|
+
@matching_count = @streams.count { |stream| stream_matches?(stream) }
|
|
127
|
+
count_matches?(@matching_count)
|
|
79
128
|
end
|
|
80
129
|
|
|
81
130
|
# @param response_or_body [#body, String]
|
|
@@ -86,17 +135,17 @@ module TurboRspec
|
|
|
86
135
|
|
|
87
136
|
# @return [String]
|
|
88
137
|
def failure_message
|
|
89
|
-
"expected response to contain a turbo stream#{constraint_description}\n#{found_streams_message}"
|
|
138
|
+
"expected response to contain a turbo stream#{constraint_description}#{count_description}\n#{found_streams_message}"
|
|
90
139
|
end
|
|
91
140
|
|
|
92
141
|
# @return [String]
|
|
93
142
|
def failure_message_when_negated
|
|
94
|
-
"expected response not to contain a turbo stream#{constraint_description}"
|
|
143
|
+
"expected response not to contain a turbo stream#{constraint_description}#{count_description}"
|
|
95
144
|
end
|
|
96
145
|
|
|
97
146
|
# @return [String]
|
|
98
147
|
def description
|
|
99
|
-
"have turbo stream#{constraint_description}"
|
|
148
|
+
"have turbo stream#{constraint_description}#{count_description}"
|
|
100
149
|
end
|
|
101
150
|
|
|
102
151
|
private
|
|
@@ -143,6 +192,20 @@ module TurboRspec
|
|
|
143
192
|
stream.to_html.include?(@partial)
|
|
144
193
|
end
|
|
145
194
|
|
|
195
|
+
def count_matches?(n)
|
|
196
|
+
if @expected_count.nil?
|
|
197
|
+
n >= 1
|
|
198
|
+
else
|
|
199
|
+
# :nocov:
|
|
200
|
+
case @count_type
|
|
201
|
+
# :nocov:
|
|
202
|
+
when :exactly then n == @expected_count
|
|
203
|
+
when :at_least then n >= @expected_count
|
|
204
|
+
when :at_most then n <= @expected_count
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
146
209
|
def constraint_description
|
|
147
210
|
parts = []
|
|
148
211
|
parts << " with action #{@action.inspect}" if @action
|
|
@@ -153,6 +216,17 @@ module TurboRspec
|
|
|
153
216
|
parts.join
|
|
154
217
|
end
|
|
155
218
|
|
|
219
|
+
def count_description
|
|
220
|
+
return "" if @expected_count.nil?
|
|
221
|
+
# :nocov:
|
|
222
|
+
case @count_type
|
|
223
|
+
# :nocov:
|
|
224
|
+
when :exactly then " exactly #{@expected_count} time(s)"
|
|
225
|
+
when :at_least then " at least #{@expected_count} time(s)"
|
|
226
|
+
when :at_most then " at most #{@expected_count} time(s)"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
156
230
|
def found_streams_message
|
|
157
231
|
return "but no turbo streams were found in the response" if @streams.empty?
|
|
158
232
|
|
|
@@ -163,10 +237,15 @@ module TurboRspec
|
|
|
163
237
|
lines << " #{i + 1}. action=#{s["action"].inspect} target=#{s["target"].inspect} content=#{content_preview}"
|
|
164
238
|
end
|
|
165
239
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
240
|
+
if @expected_count
|
|
241
|
+
lines << ""
|
|
242
|
+
lines << "#{@matching_count} of #{@streams.size} stream(s) matched the constraints"
|
|
243
|
+
else
|
|
244
|
+
closest = closest_match
|
|
245
|
+
lines << ""
|
|
246
|
+
lines << "closest match (#{count_matching_constraints(closest)}/#{constraint_count} constraint(s) matched):"
|
|
247
|
+
lines.concat(constraint_diff(closest))
|
|
248
|
+
end
|
|
170
249
|
|
|
171
250
|
lines.join("\n")
|
|
172
251
|
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: 1.
|
|
4
|
+
version: 1.1.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"
|