turbo_rspec 0.6.0 → 0.7.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/.yardopts +6 -0
- data/CHANGELOG.md +8 -0
- data/README.md +2 -0
- data/ROADMAP.md +1 -12
- data/docs/cookbook.md +185 -0
- data/docs/migration_guide.md +98 -0
- data/lib/turbo_rspec/configuration.rb +9 -0
- data/lib/turbo_rspec/helpers.rb +21 -0
- data/lib/turbo_rspec/matchers/have_turbo_frame.rb +27 -0
- data/lib/turbo_rspec/matchers/have_turbo_stream.rb +39 -0
- data/lib/turbo_rspec/matchers.rb +20 -0
- data/lib/turbo_rspec/version.rb +1 -1
- data/lib/turbo_rspec.rb +27 -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: 3049c79cdd7a1c8b6dc2447fb2034458f129b900343dcc759e5f8b1ae200189b
|
|
4
|
+
data.tar.gz: ca51420063475b7870a6d77e95d0e4b33f5d2b0e725c4f9b2419acdd4dbb4cd9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e096c87e0b36b9c291308e95a30f075c4c3761d25c9b2d6779647a5e6ace92ed7ff60b5e04cd31b837611dfe92098067a9cb7a4c302e8c12d26eba7bd0d6840
|
|
7
|
+
data.tar.gz: 82fcd0aaf837b61d78c079619d52ab99d79d99bc6d2eafec7cd1ce56f0a6558e3b69748928fadbc81fc0651b8910c4b0d44d45795f623c183b012ff423bf44e2
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-05-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Full YARD documentation on all public methods, classes, and modules
|
|
8
|
+
- `docs/migration_guide.md` — how to replace hand-rolled Turbo helpers with `turbo_rspec` matchers
|
|
9
|
+
- `docs/cookbook.md` — common patterns: request specs, lazy frames, broadcast job specs, multi-stream responses, Minitest, controller specs
|
|
10
|
+
|
|
3
11
|
## [0.6.0] - 2026-05-28
|
|
4
12
|
|
|
5
13
|
### Added
|
data/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails) — assert Turbo Stream responses, Turbo Frame content, and ActionCable broadcasts without hand-rolling helpers in every project.
|
|
10
10
|
|
|
11
|
+
**Docs:** [API Reference](https://rubydoc.info/gems/turbo_rspec) · [Migration Guide](docs/migration_guide.md) · [Cookbook](docs/cookbook.md)
|
|
12
|
+
|
|
11
13
|
## Installation
|
|
12
14
|
|
|
13
15
|
Add to your application's `Gemfile`:
|
data/ROADMAP.md
CHANGED
|
@@ -4,17 +4,6 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## v0.7.0 — Documentation
|
|
8
|
-
|
|
9
|
-
**Goal:** full docs before freezing the API.
|
|
10
|
-
|
|
11
|
-
- Full YARD documentation on all public methods and classes
|
|
12
|
-
- Migration guide: "replacing hand-rolled Turbo helpers in your test suite"
|
|
13
|
-
- Cookbook: common patterns (lazy-loaded frames, job broadcast testing, multi-stream responses, controller specs)
|
|
14
|
-
- Hosted on RubyDoc.info
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
7
|
## v1.0.0 — Stable API
|
|
19
8
|
|
|
20
9
|
**Goal:** API freeze. Commit to semver stability. Make the gem the obvious default choice.
|
|
@@ -24,13 +13,13 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
|
|
|
24
13
|
- 100% branch coverage enforced in CI (`simplecov`)
|
|
25
14
|
- Performance: benchmark matcher overhead to keep it negligible in large suites
|
|
26
15
|
- `bin/release` script (mirrors solid_queue_web pattern): bump version, update CHANGELOG, tag, push; CI publishes via Trusted Publishing
|
|
16
|
+
- `turbo_rspec` generator (`rails generate turbo_rspec:install`) to scaffold `spec/support/turbo.rb`
|
|
27
17
|
|
|
28
18
|
---
|
|
29
19
|
|
|
30
20
|
## Post-1.0 ideas (not scheduled)
|
|
31
21
|
|
|
32
22
|
- VS Code / RubyMine snippet pack for common patterns
|
|
33
|
-
- `turbo_rspec` generator (`rails generate turbo_rspec:install`) to scaffold `spec/support/turbo.rb`
|
|
34
23
|
- Playwright/Puppeteer bridge for headless assertions outside Capybara
|
|
35
24
|
- Shared examples: `it_behaves_like "a turbo stream response"` for controller testing
|
|
36
25
|
|
data/docs/cookbook.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Cookbook: Common Turbo Testing Patterns
|
|
2
|
+
|
|
3
|
+
## Request specs
|
|
4
|
+
|
|
5
|
+
### Asserting a single stream
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
RSpec.describe "Messages", type: :request do
|
|
9
|
+
it "appends the new message" do
|
|
10
|
+
post messages_path, params: { message: { body: "Hello" } },
|
|
11
|
+
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
12
|
+
|
|
13
|
+
expect(response).to have_turbo_stream
|
|
14
|
+
.with_action(:append)
|
|
15
|
+
.targeting("messages")
|
|
16
|
+
.with_content("Hello")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Asserting multiple streams in one expectation
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
it "updates the list and clears the form" do
|
|
25
|
+
post messages_path, params: { message: { body: "Hello" } }, as: :turbo_stream
|
|
26
|
+
|
|
27
|
+
expect(response).to have_turbo_streams(
|
|
28
|
+
have_turbo_stream.with_action(:append).targeting("messages"),
|
|
29
|
+
have_turbo_stream.with_action(:replace).targeting("message_form")
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Using shared examples
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
RSpec.describe "Messages", type: :request do
|
|
38
|
+
describe "POST /messages" do
|
|
39
|
+
before { post messages_path, params: { body: "Hello" }, as: :turbo_stream }
|
|
40
|
+
|
|
41
|
+
it_behaves_like "a turbo stream response", action: :append, target: "messages"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Asserting a remove stream
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
it "removes the deleted message" do
|
|
50
|
+
delete message_path(message), as: :turbo_stream
|
|
51
|
+
|
|
52
|
+
expect(response).to have_turbo_stream
|
|
53
|
+
.with_action(:remove)
|
|
54
|
+
.targeting("message_#{message.id}")
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Asserting a Turbo Frame response
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
it "renders the edit form in the frame" do
|
|
62
|
+
get edit_message_path(message)
|
|
63
|
+
|
|
64
|
+
expect(response).to have_turbo_frame.with_id("message_#{message.id}")
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Lazy-loaded Turbo Frames
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
it "lazy-loads the message list frame" do
|
|
72
|
+
get messages_path
|
|
73
|
+
|
|
74
|
+
# Assert the frame tag is rendered in the page
|
|
75
|
+
expect(response.body).to include('turbo-frame id="messages"')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "responds to the frame src request" do
|
|
79
|
+
get messages_path, headers: { "Turbo-Frame" => "messages" }
|
|
80
|
+
|
|
81
|
+
expect(response).to have_turbo_frame.with_id("messages")
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Broadcast matchers in job specs
|
|
86
|
+
|
|
87
|
+
### Basic broadcast assertion
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
RSpec.describe NotifyUsersJob, type: :job do
|
|
91
|
+
it "broadcasts a stream to the user channel" do
|
|
92
|
+
expect { described_class.perform_now(user) }
|
|
93
|
+
.to have_broadcasted_turbo_stream_to("user_#{user.id}")
|
|
94
|
+
.with_action(:append)
|
|
95
|
+
.targeting("notifications")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Count qualifiers
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
it "broadcasts exactly once per recipient" do
|
|
104
|
+
expect { described_class.perform_now(users) }
|
|
105
|
+
.to have_broadcasted_turbo_stream_to("notifications")
|
|
106
|
+
.exactly(users.count).times
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Broadcast to a model (requires turbo-rails)
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
it "broadcasts to the conversation channel" do
|
|
114
|
+
expect { described_class.perform_now }
|
|
115
|
+
.to have_broadcasted_turbo_stream_to(conversation)
|
|
116
|
+
.with_action(:append)
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Multi-stream responses
|
|
121
|
+
|
|
122
|
+
A single Turbo Stream response can contain multiple `<turbo-stream>` tags. All matchers handle this correctly — `have_turbo_stream` checks if *any* stream matches, while `have_turbo_streams` requires *all* listed streams to be present.
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
it "broadcasts multiple updates" do
|
|
126
|
+
post bulk_update_path, as: :turbo_stream
|
|
127
|
+
|
|
128
|
+
# passes if any one stream is :append
|
|
129
|
+
expect(response).to have_turbo_stream.with_action(:append)
|
|
130
|
+
|
|
131
|
+
# passes only if both streams are present
|
|
132
|
+
expect(response).to have_turbo_streams(
|
|
133
|
+
have_turbo_stream.with_action(:append).targeting("list"),
|
|
134
|
+
have_turbo_stream.with_action(:replace).targeting("count")
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Using factory helpers
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
RSpec.describe "Messages", type: :request do
|
|
143
|
+
# Build test HTML without hand-rolling strings
|
|
144
|
+
let(:stream_body) { turbo_stream_html(action: :append, target: "messages", content: "Hello") }
|
|
145
|
+
|
|
146
|
+
it "matches the expected stream" do
|
|
147
|
+
expect(stream_body).to have_turbo_stream.with_action(:append).with_content("Hello")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Minitest integration
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class MessagesControllerTest < ActionDispatch::IntegrationTest
|
|
156
|
+
include TurboRspec::Assertions
|
|
157
|
+
|
|
158
|
+
test "appends the new message" do
|
|
159
|
+
post messages_url, params: { message: { body: "Hello" } }, as: :turbo_stream
|
|
160
|
+
|
|
161
|
+
assert_turbo_stream(response, action: :append, target: "messages", content: "Hello")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
test "does not render a replace stream" do
|
|
165
|
+
post messages_url, params: { message: { body: "Hello" } }, as: :turbo_stream
|
|
166
|
+
|
|
167
|
+
refute_turbo_stream(response, action: :replace)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Controller specs
|
|
173
|
+
|
|
174
|
+
Matchers and helpers are also available in `type: :controller` specs:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
RSpec.describe MessagesController, type: :controller do
|
|
178
|
+
it "responds with a turbo stream" do
|
|
179
|
+
post :create, params: { message: { body: "Hello" } },
|
|
180
|
+
format: :turbo_stream
|
|
181
|
+
|
|
182
|
+
expect(response).to have_turbo_stream.with_action(:append).targeting("messages")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Migration Guide: Replacing Hand-Rolled Turbo Helpers
|
|
2
|
+
|
|
3
|
+
If your test suite has accumulated custom helpers for asserting Turbo Stream responses, this guide shows how to replace them with `turbo_rspec` matchers.
|
|
4
|
+
|
|
5
|
+
## Common hand-rolled patterns
|
|
6
|
+
|
|
7
|
+
### Pattern 1: Parsing the response body manually
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Before
|
|
11
|
+
def assert_turbo_stream_append(target:)
|
|
12
|
+
doc = Nokogiri::HTML(response.body)
|
|
13
|
+
stream = doc.at_css("turbo-stream[action='append'][target='#{target}']")
|
|
14
|
+
assert stream, "Expected append stream targeting #{target}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "appends the message" do
|
|
18
|
+
post messages_path, params: { body: "Hello" }, as: :turbo_stream
|
|
19
|
+
assert_turbo_stream_append(target: "messages")
|
|
20
|
+
end
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# After — RSpec
|
|
25
|
+
expect(response).to have_turbo_stream.with_action(:append).targeting("messages")
|
|
26
|
+
|
|
27
|
+
# After — Minitest
|
|
28
|
+
assert_turbo_stream(response, action: :append, target: "messages")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Pattern 2: String matching on the response body
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# Before
|
|
35
|
+
def expect_turbo_stream(action:, target:)
|
|
36
|
+
expect(response.body).to include("action=\"#{action}\"")
|
|
37
|
+
expect(response.body).to include("target=\"#{target}\"")
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# After
|
|
43
|
+
expect(response).to have_turbo_stream.with_action(:append).targeting("messages")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Pattern 3: Checking multiple streams
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Before
|
|
50
|
+
def assert_turbo_streams(*expected)
|
|
51
|
+
expected.each do |action:, target:|
|
|
52
|
+
assert response.body.include?("action=\"#{action}\"")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# After
|
|
59
|
+
expect(response).to have_turbo_streams(
|
|
60
|
+
have_turbo_stream.with_action(:append).targeting("messages"),
|
|
61
|
+
have_turbo_stream.with_action(:replace).targeting("header")
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Pattern 4: Custom broadcast helpers in job specs
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
# Before
|
|
69
|
+
def expect_broadcast_to(stream, action:, target:)
|
|
70
|
+
messages = ActionCable.server.pubsub.broadcasts(stream)
|
|
71
|
+
assert messages.any? { |m| m.include?(action.to_s) && m.include?(target) }
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# After
|
|
77
|
+
expect { MyJob.perform_now }.to have_broadcasted_turbo_stream_to("stream")
|
|
78
|
+
.with_action(:append)
|
|
79
|
+
.targeting("messages")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Setup
|
|
83
|
+
|
|
84
|
+
Remove any custom helpers from `spec/support/` or `test/test_helper.rb` and add to your `Gemfile`:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
group :test do
|
|
88
|
+
gem "turbo_rspec"
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
With `turbo-rails` in your bundle, matchers are automatically included in `type: :request` and `type: :controller` specs. For Minitest, include manually:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
class ActionDispatch::IntegrationTest
|
|
96
|
+
include TurboRspec::Assertions
|
|
97
|
+
end
|
|
98
|
+
```
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module TurboRspec
|
|
4
|
+
# Holds global configuration for TurboRspec.
|
|
5
|
+
#
|
|
6
|
+
# @see TurboRspec.configure
|
|
4
7
|
class Configuration
|
|
8
|
+
# @!attribute [rw] auto_include
|
|
9
|
+
# When +true+ (default), matchers are automatically included into
|
|
10
|
+
# +type: :request+ and +type: :controller+ example groups when
|
|
11
|
+
# +turbo-rails+ is present, and Capybara matchers into +type: :system+
|
|
12
|
+
# and +type: :feature+ when +capybara+ is also present.
|
|
13
|
+
# @return [Boolean]
|
|
5
14
|
attr_accessor :auto_include
|
|
6
15
|
|
|
7
16
|
def initialize
|
data/lib/turbo_rspec/helpers.rb
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module TurboRspec
|
|
4
|
+
# Factory helpers for building Turbo HTML strings inline in tests.
|
|
5
|
+
# Auto-included into +type: :request+ and +type: :controller+ example groups.
|
|
4
6
|
module Helpers
|
|
7
|
+
# Builds a +<turbo-stream>+ HTML string for use in test assertions.
|
|
8
|
+
#
|
|
9
|
+
# @param action [Symbol, String] the stream action (e.g. +:append+, +:replace+)
|
|
10
|
+
# @param target [String, nil] the +target+ DOM id attribute
|
|
11
|
+
# @param targets [String, nil] the +targets+ CSS selector attribute
|
|
12
|
+
# @param content [String, nil] optional content to place inside the template
|
|
13
|
+
# @return [String]
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# turbo_stream_html(action: :append, target: "messages", content: "Hello")
|
|
17
|
+
# turbo_stream_html(action: :remove, targets: ".item")
|
|
5
18
|
def turbo_stream_html(action:, target: nil, targets: nil, content: nil)
|
|
6
19
|
attrs = "action=\"#{action}\""
|
|
7
20
|
attrs += " target=\"#{target}\"" if target
|
|
@@ -10,6 +23,14 @@ module TurboRspec
|
|
|
10
23
|
"<turbo-stream #{attrs}>#{inner}</turbo-stream>"
|
|
11
24
|
end
|
|
12
25
|
|
|
26
|
+
# Builds a +<turbo-frame>+ HTML string for use in test assertions.
|
|
27
|
+
#
|
|
28
|
+
# @param id [String] the frame's +id+ attribute
|
|
29
|
+
# @param content [String, nil] optional content inside the frame
|
|
30
|
+
# @return [String]
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# turbo_frame_html(id: "messages", content: "Hello")
|
|
13
34
|
def turbo_frame_html(id:, content: nil)
|
|
14
35
|
"<turbo-frame id=\"#{id}\">#{content}</turbo-frame>"
|
|
15
36
|
end
|
|
@@ -4,6 +4,18 @@ require "nokogiri"
|
|
|
4
4
|
|
|
5
5
|
module TurboRspec
|
|
6
6
|
module Matchers
|
|
7
|
+
# RSpec matcher for asserting that a response body contains a
|
|
8
|
+
# +<turbo-frame>+ element. Use in request or controller specs.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# expect(response).to have_turbo_frame
|
|
12
|
+
#
|
|
13
|
+
# @example With constraints
|
|
14
|
+
# expect(response).to have_turbo_frame
|
|
15
|
+
# .with_id("messages")
|
|
16
|
+
# .with_content("Hello")
|
|
17
|
+
#
|
|
18
|
+
# @see TurboRspec::Matchers#have_turbo_frame
|
|
7
19
|
class HaveTurboFrame
|
|
8
20
|
def initialize
|
|
9
21
|
@id = nil
|
|
@@ -11,35 +23,50 @@ module TurboRspec
|
|
|
11
23
|
@partial = nil
|
|
12
24
|
end
|
|
13
25
|
|
|
26
|
+
# Constrains the match to frames with the given id attribute.
|
|
27
|
+
# @param id [String]
|
|
28
|
+
# @return [self]
|
|
14
29
|
def with_id(id)
|
|
15
30
|
@id = id.to_s
|
|
16
31
|
self
|
|
17
32
|
end
|
|
18
33
|
|
|
34
|
+
# Constrains the match to frames whose content includes the given text.
|
|
35
|
+
# @param text [String]
|
|
36
|
+
# @return [self]
|
|
19
37
|
def with_content(text)
|
|
20
38
|
@content = text.to_s
|
|
21
39
|
self
|
|
22
40
|
end
|
|
23
41
|
|
|
42
|
+
# Constrains the match to frames whose HTML includes the given partial path.
|
|
43
|
+
# @param partial [String]
|
|
44
|
+
# @return [self]
|
|
24
45
|
def rendering(partial)
|
|
25
46
|
@partial = partial.to_s
|
|
26
47
|
self
|
|
27
48
|
end
|
|
28
49
|
|
|
50
|
+
# @param response_or_body [#body, String]
|
|
51
|
+
# @return [Boolean]
|
|
29
52
|
def matches?(response_or_body)
|
|
30
53
|
@body = extract_body(response_or_body)
|
|
31
54
|
@frames = parse_frames(@body)
|
|
32
55
|
@frames.any? { |frame| frame_matches?(frame) }
|
|
33
56
|
end
|
|
34
57
|
|
|
58
|
+
# @param response_or_body [#body, String]
|
|
59
|
+
# @return [Boolean]
|
|
35
60
|
def does_not_match?(response_or_body)
|
|
36
61
|
!matches?(response_or_body)
|
|
37
62
|
end
|
|
38
63
|
|
|
64
|
+
# @return [String]
|
|
39
65
|
def failure_message
|
|
40
66
|
"expected response to contain a turbo frame#{constraint_description}\n#{found_frames_message}"
|
|
41
67
|
end
|
|
42
68
|
|
|
69
|
+
# @return [String]
|
|
43
70
|
def failure_message_when_negated
|
|
44
71
|
"expected response not to contain a turbo frame#{constraint_description}"
|
|
45
72
|
end
|
|
@@ -4,6 +4,23 @@ require "nokogiri"
|
|
|
4
4
|
|
|
5
5
|
module TurboRspec
|
|
6
6
|
module Matchers
|
|
7
|
+
# RSpec matcher for asserting that a response body contains a
|
|
8
|
+
# +<turbo-stream>+ element. Constraints are applied via a fluent chain;
|
|
9
|
+
# all specified constraints must match the *same* stream element.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# expect(response).to have_turbo_stream
|
|
13
|
+
#
|
|
14
|
+
# @example Chained constraints
|
|
15
|
+
# expect(response).to have_turbo_stream
|
|
16
|
+
# .with_action(:append)
|
|
17
|
+
# .targeting("messages")
|
|
18
|
+
# .with_content("Hello")
|
|
19
|
+
#
|
|
20
|
+
# @example Negation
|
|
21
|
+
# expect(response).not_to have_turbo_stream.with_action(:replace)
|
|
22
|
+
#
|
|
23
|
+
# @see TurboRspec::Matchers#have_turbo_stream
|
|
7
24
|
class HaveTurboStream
|
|
8
25
|
def initialize
|
|
9
26
|
@action = nil
|
|
@@ -13,49 +30,71 @@ module TurboRspec
|
|
|
13
30
|
@partial = nil
|
|
14
31
|
end
|
|
15
32
|
|
|
33
|
+
# Constrains the match to streams with the given action.
|
|
34
|
+
# @param action [Symbol, String] e.g. +:append+, +:replace+, +:remove+, +:refresh+, +:morph+
|
|
35
|
+
# @return [self]
|
|
16
36
|
def with_action(action)
|
|
17
37
|
@action = action.to_s
|
|
18
38
|
self
|
|
19
39
|
end
|
|
20
40
|
|
|
41
|
+
# Constrains the match to streams targeting a specific DOM id.
|
|
42
|
+
# @param dom_id [String]
|
|
43
|
+
# @return [self]
|
|
21
44
|
def targeting(dom_id)
|
|
22
45
|
@target = dom_id.to_s
|
|
23
46
|
self
|
|
24
47
|
end
|
|
25
48
|
|
|
49
|
+
# Constrains the match to streams targeting a CSS selector (the +targets+ attribute).
|
|
50
|
+
# @param selector [String] e.g. +".message-item"+
|
|
51
|
+
# @return [self]
|
|
26
52
|
def targeting_all(selector)
|
|
27
53
|
@target_all = selector.to_s
|
|
28
54
|
self
|
|
29
55
|
end
|
|
30
56
|
|
|
57
|
+
# Constrains the match to streams whose template content includes the given text.
|
|
58
|
+
# @param text [String]
|
|
59
|
+
# @return [self]
|
|
31
60
|
def with_content(text)
|
|
32
61
|
@content = text.to_s
|
|
33
62
|
self
|
|
34
63
|
end
|
|
35
64
|
|
|
65
|
+
# Constrains the match to streams whose rendered HTML includes the given partial path.
|
|
66
|
+
# @param partial [String] e.g. +"messages/_message"+
|
|
67
|
+
# @return [self]
|
|
36
68
|
def rendering(partial)
|
|
37
69
|
@partial = partial.to_s
|
|
38
70
|
self
|
|
39
71
|
end
|
|
40
72
|
|
|
73
|
+
# @param response_or_body [#body, String]
|
|
74
|
+
# @return [Boolean]
|
|
41
75
|
def matches?(response_or_body)
|
|
42
76
|
@body = extract_body(response_or_body)
|
|
43
77
|
@streams = parse_streams(@body)
|
|
44
78
|
@streams.any? { |stream| stream_matches?(stream) }
|
|
45
79
|
end
|
|
46
80
|
|
|
81
|
+
# @param response_or_body [#body, String]
|
|
82
|
+
# @return [Boolean]
|
|
47
83
|
def does_not_match?(response_or_body)
|
|
48
84
|
!matches?(response_or_body)
|
|
49
85
|
end
|
|
50
86
|
|
|
87
|
+
# @return [String]
|
|
51
88
|
def failure_message
|
|
52
89
|
"expected response to contain a turbo stream#{constraint_description}\n#{found_streams_message}"
|
|
53
90
|
end
|
|
54
91
|
|
|
92
|
+
# @return [String]
|
|
55
93
|
def failure_message_when_negated
|
|
56
94
|
"expected response not to contain a turbo stream#{constraint_description}"
|
|
57
95
|
end
|
|
58
96
|
|
|
97
|
+
# @return [String]
|
|
59
98
|
def description
|
|
60
99
|
"have turbo stream#{constraint_description}"
|
|
61
100
|
end
|
data/lib/turbo_rspec/matchers.rb
CHANGED
|
@@ -6,23 +6,43 @@ require_relative "matchers/have_turbo_stream"
|
|
|
6
6
|
require_relative "matchers/have_turbo_streams"
|
|
7
7
|
|
|
8
8
|
module TurboRspec
|
|
9
|
+
# RSpec matchers for Turbo Stream and Turbo Frame assertions.
|
|
10
|
+
# Auto-included in +type: :request+ and +type: :controller+ example groups.
|
|
11
|
+
# Include explicitly for other contexts:
|
|
12
|
+
#
|
|
13
|
+
# RSpec.configure do |config|
|
|
14
|
+
# config.include TurboRspec::Matchers
|
|
15
|
+
# end
|
|
9
16
|
module Matchers
|
|
17
|
+
# Assert that a block broadcasts a +<turbo-stream>+ to the given stream.
|
|
18
|
+
# @param stream_or_object [String, Object] stream name or streamable object
|
|
19
|
+
# @return [HaveBroadcastedTurboStreamTo]
|
|
10
20
|
def have_broadcasted_turbo_stream_to(stream_or_object)
|
|
11
21
|
HaveBroadcastedTurboStreamTo.new(stream_or_object)
|
|
12
22
|
end
|
|
13
23
|
|
|
24
|
+
# @see #have_broadcasted_turbo_stream_to
|
|
14
25
|
alias_method :broadcast_turbo_stream_to, :have_broadcasted_turbo_stream_to
|
|
15
26
|
|
|
27
|
+
# Assert that a response body contains a +<turbo-frame>+ element.
|
|
28
|
+
# @return [HaveTurboFrame]
|
|
16
29
|
def have_turbo_frame
|
|
17
30
|
HaveTurboFrame.new
|
|
18
31
|
end
|
|
19
32
|
|
|
33
|
+
# Assert that a response body contains a +<turbo-stream>+ element.
|
|
34
|
+
# @return [HaveTurboStream]
|
|
20
35
|
def have_turbo_stream
|
|
21
36
|
HaveTurboStream.new
|
|
22
37
|
end
|
|
23
38
|
|
|
39
|
+
# Alias of {#have_turbo_stream} for teams using minitest-style naming.
|
|
40
|
+
# @see #have_turbo_stream
|
|
24
41
|
alias_method :assert_no_turbo_stream, :have_turbo_stream
|
|
25
42
|
|
|
43
|
+
# Assert that a response body contains *all* of the given turbo streams.
|
|
44
|
+
# @param matchers [Array<HaveTurboStream>] one or more {#have_turbo_stream} matchers
|
|
45
|
+
# @return [HaveTurboStreams]
|
|
26
46
|
def have_turbo_streams(*matchers)
|
|
27
47
|
HaveTurboStreams.new(matchers)
|
|
28
48
|
end
|
data/lib/turbo_rspec/version.rb
CHANGED
data/lib/turbo_rspec.rb
CHANGED
|
@@ -8,22 +8,49 @@ require_relative "turbo_rspec/assertions"
|
|
|
8
8
|
require_relative "turbo_rspec/shared_examples"
|
|
9
9
|
require_relative "turbo_rspec/capybara/matchers"
|
|
10
10
|
|
|
11
|
+
# TurboRspec provides RSpec matchers and Minitest assertions for
|
|
12
|
+
# {https://github.com/hotwired/turbo-rails turbo-rails}.
|
|
13
|
+
#
|
|
14
|
+
# @example Configure auto-include
|
|
15
|
+
# TurboRspec.configure do |config|
|
|
16
|
+
# config.auto_include = false
|
|
17
|
+
# end
|
|
11
18
|
module TurboRspec
|
|
19
|
+
# Base error class for TurboRspec.
|
|
12
20
|
class Error < StandardError; end
|
|
13
21
|
|
|
14
22
|
class << self
|
|
23
|
+
# Yields the configuration object for customization.
|
|
24
|
+
#
|
|
25
|
+
# @yieldparam config [Configuration]
|
|
26
|
+
# @return [void]
|
|
27
|
+
# @example
|
|
28
|
+
# TurboRspec.configure do |config|
|
|
29
|
+
# config.auto_include = false
|
|
30
|
+
# end
|
|
15
31
|
def configure
|
|
16
32
|
yield configuration
|
|
17
33
|
end
|
|
18
34
|
|
|
35
|
+
# Returns the global configuration instance.
|
|
36
|
+
#
|
|
37
|
+
# @return [Configuration]
|
|
19
38
|
def configuration
|
|
20
39
|
@configuration ||= Configuration.new
|
|
21
40
|
end
|
|
22
41
|
|
|
42
|
+
# Resets configuration to defaults. Primarily for use in test suites.
|
|
43
|
+
#
|
|
44
|
+
# @return [void]
|
|
23
45
|
def reset_configuration!
|
|
24
46
|
@configuration = Configuration.new
|
|
25
47
|
end
|
|
26
48
|
|
|
49
|
+
# Installs RSpec integration — includes matchers and helpers into the
|
|
50
|
+
# appropriate example groups. Called automatically when RSpec is present.
|
|
51
|
+
#
|
|
52
|
+
# @param config [RSpec::Core::Configuration]
|
|
53
|
+
# @return [void]
|
|
27
54
|
def install_rspec_integration(config)
|
|
28
55
|
return unless configuration.auto_include && Gem.loaded_specs.key?("turbo-rails")
|
|
29
56
|
config.include Matchers, type: :request
|
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.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -35,6 +35,7 @@ files:
|
|
|
35
35
|
- ".github/workflows/ci.yml"
|
|
36
36
|
- ".github/workflows/publish.yml"
|
|
37
37
|
- ".rubocop.yml"
|
|
38
|
+
- ".yardopts"
|
|
38
39
|
- CHANGELOG.md
|
|
39
40
|
- CLAUDE.md
|
|
40
41
|
- LICENSE.txt
|
|
@@ -42,6 +43,8 @@ files:
|
|
|
42
43
|
- ROADMAP.md
|
|
43
44
|
- Rakefile
|
|
44
45
|
- codecov.yml
|
|
46
|
+
- docs/cookbook.md
|
|
47
|
+
- docs/migration_guide.md
|
|
45
48
|
- lib/turbo_rspec.rb
|
|
46
49
|
- lib/turbo_rspec/assertions.rb
|
|
47
50
|
- lib/turbo_rspec/capybara/matchers.rb
|