turbo_rspec 0.1.0 → 0.2.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 +10 -0
- data/ROADMAP.md +0 -13
- data/lib/turbo_rspec/matchers/have_broadcasted_turbo_stream_to.rb +203 -0
- data/lib/turbo_rspec/matchers.rb +7 -0
- data/lib/turbo_rspec/version.rb +1 -1
- data/lib/turbo_rspec.rb +8 -4
- 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: 8cfaf54d65cd35ab2ea68138d58d70252fecf6778511beebf78ad40cfd7e998e
|
|
4
|
+
data.tar.gz: 7bc4f9a0949b25625b59408c551d3e554ba48112b7f1c0e0f511ef5b79354878
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a13ab561704b49f3659e83160724cd2afdd1def359a70f0f1057a396fc9d1f6eaba99bf4b20e54b81af62c4a1aa1ffb9b530b2565977d2cf5ea711163cd5f6d
|
|
7
|
+
data.tar.gz: 8889b026ce58c96a12d0bf5b974dd2bdba7ce6d77520de5af6a31d45aa52a79899e9107c7b8148a1b1ce9f742ab5c1cf0da9f1999ebcd2e06120b9ee4c34b21c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-05-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `have_broadcasted_turbo_stream_to(stream)` block matcher for asserting ActionCable broadcasts contain a `<turbo-stream>` element
|
|
8
|
+
- Same fluent chain as `have_turbo_stream`: `.with_action`, `.targeting`, `.targeting_all`, `.with_content`, `.rendering`
|
|
9
|
+
- Count qualifiers: `.once`, `.twice`, `.exactly(n).times`, `.at_least(n).times`, `.at_most(n).times`
|
|
10
|
+
- `broadcast_turbo_stream_to` alias for naming symmetry with ActionCable's API
|
|
11
|
+
- Negation via `not_to have_broadcasted_turbo_stream_to` works out of the box
|
|
12
|
+
|
|
3
13
|
## [0.1.0] - 2026-05-28
|
|
4
14
|
|
|
5
15
|
### Added
|
data/ROADMAP.md
CHANGED
|
@@ -5,19 +5,6 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
## v0.2.0 — Broadcast matchers
|
|
9
|
-
|
|
10
|
-
**Goal:** cover the broadcast side — jobs/services that push streams over ActionCable.
|
|
11
|
-
|
|
12
|
-
- `have_broadcasted_turbo_stream_to(channel_or_object)` — wraps ActionCable's test adapter
|
|
13
|
-
- Same fluent chain as `have_turbo_stream`: `.with_action`, `.targeting`, `.rendering`, `.with_content`
|
|
14
|
-
- Count qualifiers: `.exactly(n).times`, `.at_least(n).times`, `.at_most(n).times`, `.once`, `.twice`
|
|
15
|
-
- Works inside `expect { }.to have_broadcasted_turbo_stream_to(...)` blocks
|
|
16
|
-
- Helper `broadcast_turbo_stream_to` alias for symmetry with ActionCable's naming
|
|
17
|
-
- Docs: "testing broadcasts in job specs and service specs"
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
8
|
## v0.3.0 — Capybara / system spec integration
|
|
22
9
|
|
|
23
10
|
**Goal:** assertions that work against a live browser in feature/system specs.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module TurboRspec
|
|
6
|
+
module Matchers
|
|
7
|
+
class HaveBroadcastedTurboStreamTo
|
|
8
|
+
def initialize(stream_or_object)
|
|
9
|
+
@stream_or_object = stream_or_object
|
|
10
|
+
@action = nil
|
|
11
|
+
@target = nil
|
|
12
|
+
@target_all = nil
|
|
13
|
+
@content = nil
|
|
14
|
+
@partial = nil
|
|
15
|
+
@expected_count = nil
|
|
16
|
+
@count_type = :at_least
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Stream constraints (mirrors HaveTurboStream)
|
|
20
|
+
|
|
21
|
+
def with_action(action)
|
|
22
|
+
@action = action.to_s
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def targeting(dom_id)
|
|
27
|
+
@target = dom_id.to_s
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def targeting_all(selector)
|
|
32
|
+
@target_all = selector.to_s
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def with_content(text)
|
|
37
|
+
@content = text.to_s
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rendering(partial)
|
|
42
|
+
@partial = partial.to_s
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Count qualifiers
|
|
47
|
+
|
|
48
|
+
def once
|
|
49
|
+
exactly(1)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def twice
|
|
53
|
+
exactly(2)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def exactly(n)
|
|
57
|
+
@expected_count = n
|
|
58
|
+
@count_type = :exactly
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def at_least(n)
|
|
63
|
+
@expected_count = n
|
|
64
|
+
@count_type = :at_least
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def at_most(n)
|
|
69
|
+
@expected_count = n
|
|
70
|
+
@count_type = :at_most
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def times
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def supports_block_expectations?
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def matches?(block)
|
|
83
|
+
before = snapshot
|
|
84
|
+
block.call
|
|
85
|
+
@matching = (snapshot - before).select { |msg| message_matches?(msg) }
|
|
86
|
+
count_matches?(@matching.size)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def does_not_match?(block)
|
|
90
|
+
!matches?(block)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def failure_message
|
|
94
|
+
"expected block to broadcast a turbo stream to #{stream_name.inspect}#{constraint_description}#{count_description}\n#{found_message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def failure_message_when_negated
|
|
98
|
+
"expected block not to broadcast a turbo stream to #{stream_name.inspect}#{constraint_description}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def description
|
|
102
|
+
"broadcast a turbo stream to #{stream_name.inspect}#{constraint_description}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def stream_name
|
|
108
|
+
@stream_name ||= if @stream_or_object.respond_to?(:to_str)
|
|
109
|
+
@stream_or_object
|
|
110
|
+
elsif defined?(Turbo::StreamsChannel)
|
|
111
|
+
Turbo::StreamsChannel.broadcasting_for(@stream_or_object)
|
|
112
|
+
else
|
|
113
|
+
@stream_or_object.to_s
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def snapshot
|
|
118
|
+
ActionCable.server.pubsub.broadcasts(stream_name).dup
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def message_matches?(message)
|
|
122
|
+
html = JSON.parse(message)
|
|
123
|
+
streams = Nokogiri::HTML5.fragment(html).css("turbo-stream")
|
|
124
|
+
streams.any? { |stream| stream_matches?(stream) }
|
|
125
|
+
rescue JSON::ParserError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def stream_matches?(stream)
|
|
130
|
+
matches_action?(stream) &&
|
|
131
|
+
matches_target?(stream) &&
|
|
132
|
+
matches_target_all?(stream) &&
|
|
133
|
+
matches_content?(stream) &&
|
|
134
|
+
matches_partial?(stream)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def matches_action?(stream)
|
|
138
|
+
@action.nil? || stream["action"] == @action
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def matches_target?(stream)
|
|
142
|
+
@target.nil? || stream["target"] == @target
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def matches_target_all?(stream)
|
|
146
|
+
@target_all.nil? || stream["targets"] == @target_all
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def matches_content?(stream)
|
|
150
|
+
return true if @content.nil?
|
|
151
|
+
stream.text.include?(@content)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def matches_partial?(stream)
|
|
155
|
+
return true if @partial.nil?
|
|
156
|
+
stream.to_html.include?(@partial)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def count_matches?(n)
|
|
160
|
+
if @expected_count.nil?
|
|
161
|
+
n >= 1
|
|
162
|
+
else
|
|
163
|
+
# :nocov:
|
|
164
|
+
case @count_type
|
|
165
|
+
# :nocov:
|
|
166
|
+
when :exactly then n == @expected_count
|
|
167
|
+
when :at_least then n >= @expected_count
|
|
168
|
+
when :at_most then n <= @expected_count
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def constraint_description
|
|
174
|
+
parts = []
|
|
175
|
+
parts << " with action #{@action.inspect}" if @action
|
|
176
|
+
parts << " targeting #{@target.inspect}" if @target
|
|
177
|
+
parts << " targeting all #{@target_all.inspect}" if @target_all
|
|
178
|
+
parts << " with content #{@content.inspect}" if @content
|
|
179
|
+
parts << " rendering #{@partial.inspect}" if @partial
|
|
180
|
+
parts.join
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def count_description
|
|
184
|
+
return "" if @expected_count.nil? && @count_type == :at_least
|
|
185
|
+
# :nocov:
|
|
186
|
+
case @count_type
|
|
187
|
+
# :nocov:
|
|
188
|
+
when :exactly then " exactly #{@expected_count} time(s)"
|
|
189
|
+
when :at_least then " at least #{@expected_count} time(s)"
|
|
190
|
+
when :at_most then " at most #{@expected_count} time(s)"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def found_message
|
|
195
|
+
if @matching.empty?
|
|
196
|
+
"but no matching broadcasts were found"
|
|
197
|
+
else
|
|
198
|
+
"found #{@matching.size} matching broadcast(s)"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
data/lib/turbo_rspec/matchers.rb
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "matchers/have_broadcasted_turbo_stream_to"
|
|
3
4
|
require_relative "matchers/have_turbo_frame"
|
|
4
5
|
require_relative "matchers/have_turbo_stream"
|
|
5
6
|
|
|
6
7
|
module TurboRspec
|
|
7
8
|
module Matchers
|
|
9
|
+
def have_broadcasted_turbo_stream_to(stream_or_object)
|
|
10
|
+
HaveBroadcastedTurboStreamTo.new(stream_or_object)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
alias_method :broadcast_turbo_stream_to, :have_broadcasted_turbo_stream_to
|
|
14
|
+
|
|
8
15
|
def have_turbo_frame
|
|
9
16
|
HaveTurboFrame.new
|
|
10
17
|
end
|
data/lib/turbo_rspec/version.rb
CHANGED
data/lib/turbo_rspec.rb
CHANGED
|
@@ -19,12 +19,16 @@ module TurboRspec
|
|
|
19
19
|
def reset_configuration!
|
|
20
20
|
@configuration = Configuration.new
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
def install_rspec_integration(config)
|
|
24
|
+
return unless configuration.auto_include && Gem.loaded_specs.key?("turbo-rails")
|
|
25
|
+
config.include Matchers, type: :request
|
|
26
|
+
end
|
|
22
27
|
end
|
|
23
28
|
end
|
|
24
29
|
|
|
30
|
+
# :nocov:
|
|
25
31
|
if defined?(RSpec)
|
|
26
|
-
RSpec.configure
|
|
27
|
-
config.include TurboRspec::Matchers, type: :request if TurboRspec.configuration.auto_include &&
|
|
28
|
-
Gem.loaded_specs.key?("turbo-rails")
|
|
29
|
-
end
|
|
32
|
+
RSpec.configure { |config| TurboRspec.install_rspec_integration(config) }
|
|
30
33
|
end
|
|
34
|
+
# :nocov:
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- lib/turbo_rspec.rb
|
|
46
46
|
- lib/turbo_rspec/configuration.rb
|
|
47
47
|
- lib/turbo_rspec/matchers.rb
|
|
48
|
+
- lib/turbo_rspec/matchers/have_broadcasted_turbo_stream_to.rb
|
|
48
49
|
- lib/turbo_rspec/matchers/have_turbo_frame.rb
|
|
49
50
|
- lib/turbo_rspec/matchers/have_turbo_stream.rb
|
|
50
51
|
- lib/turbo_rspec/version.rb
|