turbo_rspec 1.2.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 +16 -0
- data/README.md +64 -1
- data/ROADMAP.md +0 -11
- data/lib/rubocop/cop/turbo_rspec/use_have_turbo_stream.rb +53 -0
- data/lib/turbo_rspec/capybara/matchers/have_stimulus_action.rb +55 -0
- data/lib/turbo_rspec/capybara/matchers/have_stimulus_controller.rb +46 -0
- data/lib/turbo_rspec/capybara/matchers/have_stimulus_target.rb +47 -0
- data/lib/turbo_rspec/capybara/matchers.rb +15 -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 +52 -0
- metadata +7 -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,21 @@
|
|
|
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
|
+
|
|
11
|
+
## [1.3.0] - 2026-06-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `have_stimulus_controller(name)` — Capybara matcher asserting `data-controller` contains the given controller name
|
|
16
|
+
- `have_stimulus_action(descriptor)` — Capybara matcher asserting `data-action` contains the descriptor; accepts a full descriptor (`click->hello#greet`) or shorthand without event (`hello#greet`)
|
|
17
|
+
- `have_stimulus_target(controller, target)` — Capybara matcher asserting `data-{controller}-target` contains the target name
|
|
18
|
+
|
|
3
19
|
## [1.2.0] - 2026-06-01
|
|
4
20
|
|
|
5
21
|
### Added
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Drop-in test matchers for [hotwired/turbo-rails](https://github.com/hotwired/tur
|
|
|
10
10
|
|
|
11
11
|
- **Request/controller specs** — `have_turbo_stream`, `have_turbo_frame`, `have_turbo_streams`
|
|
12
12
|
- **Broadcast specs** — `have_broadcasted_turbo_stream_to` with count qualifiers
|
|
13
|
-
- **System/feature specs** — Capybara matchers: `have_turbo_frame`, `have_turbo_stream_tag`, `within_turbo_frame`
|
|
13
|
+
- **System/feature specs** — Capybara matchers: `have_turbo_frame`, `have_turbo_stream_tag`, `within_turbo_frame`, `have_stimulus_controller`, `have_stimulus_action`, `have_stimulus_target`
|
|
14
14
|
- **Minitest** — `assert_turbo_stream`, `refute_turbo_stream`, `assert_turbo_frame`, `refute_turbo_frame`
|
|
15
15
|
- **Factory helpers** — `turbo_stream_html`, `turbo_frame_html`
|
|
16
16
|
- **Shared examples** — `it_behaves_like "a turbo stream response"`
|
|
@@ -236,6 +236,29 @@ within_turbo_frame("messages") do
|
|
|
236
236
|
end
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
### Stimulus matchers
|
|
240
|
+
|
|
241
|
+
Assert Stimulus controller, action, and target attributes on the page (Capybara).
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
# Controller — data-controller contains "hello"
|
|
245
|
+
expect(page).to have_stimulus_controller("hello")
|
|
246
|
+
|
|
247
|
+
# Action — full descriptor
|
|
248
|
+
expect(page).to have_stimulus_action("click->hello#greet")
|
|
249
|
+
|
|
250
|
+
# Action — shorthand without event (matches any event prefix)
|
|
251
|
+
expect(page).to have_stimulus_action("hello#greet")
|
|
252
|
+
|
|
253
|
+
# Target — data-hello-target contains "name"
|
|
254
|
+
expect(page).to have_stimulus_target("hello", "name")
|
|
255
|
+
|
|
256
|
+
# Negation
|
|
257
|
+
expect(page).not_to have_stimulus_controller("missing")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
All three matchers use space-separated token matching (`~=`), so they work correctly when multiple controllers, actions, or targets are declared on a single element.
|
|
261
|
+
|
|
239
262
|
### `have_turbo_stream_tag`
|
|
240
263
|
|
|
241
264
|
Assert that a `<turbo-stream-source>` subscription element is on the page.
|
|
@@ -251,6 +274,46 @@ expect(page).to have_turbo_stream_tag("signed_stream_name")
|
|
|
251
274
|
expect(page).not_to have_turbo_stream_tag
|
|
252
275
|
```
|
|
253
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
|
+
|
|
254
317
|
## Test helpers
|
|
255
318
|
|
|
256
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,17 +4,6 @@ RSpec matchers for [Turbo](https://github.com/hotwired/turbo-rails): Turbo Strea
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## 1.3 — Stimulus companion
|
|
8
|
-
|
|
9
|
-
- **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.
|
|
10
|
-
|
|
11
|
-
## 1.4 — Tooling
|
|
12
|
-
|
|
13
|
-
- **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.
|
|
14
|
-
- **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.
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
7
|
## Guiding principles
|
|
19
8
|
|
|
20
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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurboRspec
|
|
4
|
+
module Capybara
|
|
5
|
+
module Matchers
|
|
6
|
+
# Capybara matcher asserting that an element with the given Stimulus
|
|
7
|
+
# action descriptor is present on the page (+data-action+ contains the
|
|
8
|
+
# descriptor as a space-separated token).
|
|
9
|
+
#
|
|
10
|
+
# Accepts either a full descriptor (+click->hello#greet+) or a shorthand
|
|
11
|
+
# without an event (+hello#greet+), which matches any event prefix.
|
|
12
|
+
#
|
|
13
|
+
# @example Full descriptor
|
|
14
|
+
# expect(page).to have_stimulus_action("click->hello#greet")
|
|
15
|
+
#
|
|
16
|
+
# @example Shorthand (any event)
|
|
17
|
+
# expect(page).to have_stimulus_action("hello#greet")
|
|
18
|
+
class HaveStimulusAction
|
|
19
|
+
def initialize(action_descriptor)
|
|
20
|
+
@action_descriptor = action_descriptor.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def matches?(page_or_node)
|
|
24
|
+
page_or_node.has_css?(selector, wait: 0)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def does_not_match?(page_or_node)
|
|
28
|
+
page_or_node.has_no_css?(selector, wait: 0)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def failure_message
|
|
32
|
+
"expected page to have Stimulus action #{@action_descriptor.inspect}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def failure_message_when_negated
|
|
36
|
+
"expected page not to have Stimulus action #{@action_descriptor.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def description
|
|
40
|
+
"have Stimulus action #{@action_descriptor.inspect}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def selector
|
|
46
|
+
if @action_descriptor.include?("->")
|
|
47
|
+
"[data-action~='#{@action_descriptor}']"
|
|
48
|
+
else
|
|
49
|
+
"[data-action*='#{@action_descriptor}']"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurboRspec
|
|
4
|
+
module Capybara
|
|
5
|
+
module Matchers
|
|
6
|
+
# Capybara matcher asserting that an element with the given Stimulus
|
|
7
|
+
# controller is present on the page (+data-controller+ contains the name
|
|
8
|
+
# as a space-separated token).
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# expect(page).to have_stimulus_controller("hello")
|
|
12
|
+
# expect(page).not_to have_stimulus_controller("missing")
|
|
13
|
+
class HaveStimulusController
|
|
14
|
+
def initialize(controller_name)
|
|
15
|
+
@controller_name = controller_name.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def matches?(page_or_node)
|
|
19
|
+
page_or_node.has_css?(selector, wait: 0)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def does_not_match?(page_or_node)
|
|
23
|
+
page_or_node.has_no_css?(selector, wait: 0)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def failure_message
|
|
27
|
+
"expected page to have Stimulus controller #{@controller_name.inspect}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def failure_message_when_negated
|
|
31
|
+
"expected page not to have Stimulus controller #{@controller_name.inspect}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def description
|
|
35
|
+
"have Stimulus controller #{@controller_name.inspect}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def selector
|
|
41
|
+
"[data-controller~='#{@controller_name}']"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TurboRspec
|
|
4
|
+
module Capybara
|
|
5
|
+
module Matchers
|
|
6
|
+
# Capybara matcher asserting that an element with the given Stimulus
|
|
7
|
+
# target is present on the page. Checks +data-{controller}-target+
|
|
8
|
+
# contains the target name as a space-separated token.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# expect(page).to have_stimulus_target("hello", "name")
|
|
12
|
+
# expect(page).not_to have_stimulus_target("hello", "missing")
|
|
13
|
+
class HaveStimulusTarget
|
|
14
|
+
def initialize(controller_name, target_name)
|
|
15
|
+
@controller_name = controller_name.to_s
|
|
16
|
+
@target_name = target_name.to_s
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def matches?(page_or_node)
|
|
20
|
+
page_or_node.has_css?(selector, wait: 0)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def does_not_match?(page_or_node)
|
|
24
|
+
page_or_node.has_no_css?(selector, wait: 0)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def failure_message
|
|
28
|
+
"expected page to have Stimulus target #{@target_name.inspect} for controller #{@controller_name.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def failure_message_when_negated
|
|
32
|
+
"expected page not to have Stimulus target #{@target_name.inspect} for controller #{@controller_name.inspect}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def description
|
|
36
|
+
"have Stimulus target #{@target_name.inspect} for #{@controller_name.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def selector
|
|
42
|
+
"[data-#{@controller_name}-target~='#{@target_name}']"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "matchers/have_turbo_frame"
|
|
4
4
|
require_relative "matchers/have_turbo_stream_tag"
|
|
5
|
+
require_relative "matchers/have_stimulus_controller"
|
|
6
|
+
require_relative "matchers/have_stimulus_action"
|
|
7
|
+
require_relative "matchers/have_stimulus_target"
|
|
5
8
|
|
|
6
9
|
module TurboRspec
|
|
7
10
|
module Capybara
|
|
@@ -14,6 +17,18 @@ module TurboRspec
|
|
|
14
17
|
HaveTurboStreamTag.new(signed_stream_name: signed_stream_name)
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
def have_stimulus_controller(controller_name)
|
|
21
|
+
HaveStimulusController.new(controller_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def have_stimulus_action(action_descriptor)
|
|
25
|
+
HaveStimulusAction.new(action_descriptor)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def have_stimulus_target(controller_name, target_name)
|
|
29
|
+
HaveStimulusTarget.new(controller_name, target_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
17
32
|
# :nocov:
|
|
18
33
|
def within_turbo_frame(id, &block)
|
|
19
34
|
page.within("turbo-frame##{id}", &block)
|
|
@@ -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
|
|
@@ -127,6 +138,9 @@ module TurboRspec
|
|
|
127
138
|
module Matchers
|
|
128
139
|
def have_turbo_frame: (String id) -> HaveTurboFrame
|
|
129
140
|
def have_turbo_stream_tag: (?String? signed_stream_name) -> HaveTurboStreamTag
|
|
141
|
+
def have_stimulus_controller: (String controller_name) -> HaveStimulusController
|
|
142
|
+
def have_stimulus_action: (String action_descriptor) -> HaveStimulusAction
|
|
143
|
+
def have_stimulus_target: (String controller_name, String target_name) -> HaveStimulusTarget
|
|
130
144
|
def within_turbo_frame: (String id) { () -> void } -> void
|
|
131
145
|
|
|
132
146
|
class HaveTurboFrame
|
|
@@ -146,6 +160,33 @@ module TurboRspec
|
|
|
146
160
|
def description: () -> String
|
|
147
161
|
end
|
|
148
162
|
|
|
163
|
+
class HaveStimulusController
|
|
164
|
+
def initialize: (String controller_name) -> void
|
|
165
|
+
def matches?: (untyped page_or_node) -> bool
|
|
166
|
+
def does_not_match?: (untyped page_or_node) -> bool
|
|
167
|
+
def failure_message: () -> String
|
|
168
|
+
def failure_message_when_negated: () -> String
|
|
169
|
+
def description: () -> String
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
class HaveStimulusAction
|
|
173
|
+
def initialize: (String action_descriptor) -> void
|
|
174
|
+
def matches?: (untyped page_or_node) -> bool
|
|
175
|
+
def does_not_match?: (untyped page_or_node) -> bool
|
|
176
|
+
def failure_message: () -> String
|
|
177
|
+
def failure_message_when_negated: () -> String
|
|
178
|
+
def description: () -> String
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class HaveStimulusTarget
|
|
182
|
+
def initialize: (String controller_name, String target_name) -> void
|
|
183
|
+
def matches?: (untyped page_or_node) -> bool
|
|
184
|
+
def does_not_match?: (untyped page_or_node) -> bool
|
|
185
|
+
def failure_message: () -> String
|
|
186
|
+
def failure_message_when_negated: () -> String
|
|
187
|
+
def description: () -> String
|
|
188
|
+
end
|
|
189
|
+
|
|
149
190
|
class HaveTurboStreamTag
|
|
150
191
|
def initialize: (?signed_stream_name: String?) -> void
|
|
151
192
|
def matches?: (untyped page_or_node) -> bool
|
|
@@ -156,4 +197,15 @@ module TurboRspec
|
|
|
156
197
|
end
|
|
157
198
|
end
|
|
158
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
|
|
159
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,9 +51,13 @@ 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
|
|
58
|
+
- lib/turbo_rspec/capybara/matchers/have_stimulus_action.rb
|
|
59
|
+
- lib/turbo_rspec/capybara/matchers/have_stimulus_controller.rb
|
|
60
|
+
- lib/turbo_rspec/capybara/matchers/have_stimulus_target.rb
|
|
57
61
|
- lib/turbo_rspec/capybara/matchers/have_turbo_frame.rb
|
|
58
62
|
- lib/turbo_rspec/capybara/matchers/have_turbo_stream_tag.rb
|
|
59
63
|
- lib/turbo_rspec/configuration.rb
|
|
@@ -63,6 +67,8 @@ files:
|
|
|
63
67
|
- lib/turbo_rspec/matchers/have_turbo_frame.rb
|
|
64
68
|
- lib/turbo_rspec/matchers/have_turbo_stream.rb
|
|
65
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
|
|
66
72
|
- lib/turbo_rspec/shared_examples.rb
|
|
67
73
|
- lib/turbo_rspec/version.rb
|
|
68
74
|
- sig/turbo_rspec.rbs
|