turbo_rspec 1.2.0 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7341e9dfd6be8235a2269e94ef9923e470aa5da675ccd52f8d507f42c3950398
4
- data.tar.gz: 65f4f2bb0aa32c4032d4ba2daad8c9442e20d056b3a455b0937eaeaf380f924a
3
+ metadata.gz: cdd85fcc9f87814d171569e835ad85de4767daf48642d5797f15ac02decf4641
4
+ data.tar.gz: a92c047884633fd146aa6806dc350edef72bd18747c39e3da618feb12223a29e
5
5
  SHA512:
6
- metadata.gz: b433e046578d5f3914a95cd443ed32020bff526bddf3bf79eea34ef7bc52a7ce06823ac25714f309842c357d97e0e70edd208b75a85c8e0a118e7d8b86b36c9f
7
- data.tar.gz: 44d25746b61d50d932f74f92b2f6a07c50c70a6d320c452f302693b507617da38facf8fca1d0ac61f66cf01a49187e8288a20c6182b27cbe4ab91db42fe28ec5
6
+ metadata.gz: 7ecdc1dc0c21c083a32270e7ab085e31e5fd4e6afde0193c9f58a967e7d79aaf543387e874d8cc2fd4f1afdcb0b56a2b2b53b6c6a230f3d80a41736b68c5c01e
7
+ data.tar.gz: b12927d25812f2991d889a7cedc37fe02cf4feb439adc76555dcfc8d6fd31151f9dd5cd80257ec82057d350c52e607cc2206e924bf3b159257453de9fad4ced5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0] - 2026-06-02
4
+
5
+ ### Added
6
+
7
+ - `have_stimulus_controller(name)` — Capybara matcher asserting `data-controller` contains the given controller name
8
+ - `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`)
9
+ - `have_stimulus_target(controller, target)` — Capybara matcher asserting `data-{controller}-target` contains the target name
10
+
3
11
  ## [1.2.0] - 2026-06-01
4
12
 
5
13
  ### 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.
data/ROADMAP.md CHANGED
@@ -4,10 +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
7
  ## 1.4 — Tooling
12
8
 
13
9
  - **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.
@@ -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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurboRspec
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
data/sig/turbo_rspec.rbs CHANGED
@@ -127,6 +127,9 @@ module TurboRspec
127
127
  module Matchers
128
128
  def have_turbo_frame: (String id) -> HaveTurboFrame
129
129
  def have_turbo_stream_tag: (?String? signed_stream_name) -> HaveTurboStreamTag
130
+ def have_stimulus_controller: (String controller_name) -> HaveStimulusController
131
+ def have_stimulus_action: (String action_descriptor) -> HaveStimulusAction
132
+ def have_stimulus_target: (String controller_name, String target_name) -> HaveStimulusTarget
130
133
  def within_turbo_frame: (String id) { () -> void } -> void
131
134
 
132
135
  class HaveTurboFrame
@@ -146,6 +149,33 @@ module TurboRspec
146
149
  def description: () -> String
147
150
  end
148
151
 
152
+ class HaveStimulusController
153
+ def initialize: (String controller_name) -> void
154
+ def matches?: (untyped page_or_node) -> bool
155
+ def does_not_match?: (untyped page_or_node) -> bool
156
+ def failure_message: () -> String
157
+ def failure_message_when_negated: () -> String
158
+ def description: () -> String
159
+ end
160
+
161
+ class HaveStimulusAction
162
+ def initialize: (String action_descriptor) -> void
163
+ def matches?: (untyped page_or_node) -> bool
164
+ def does_not_match?: (untyped page_or_node) -> bool
165
+ def failure_message: () -> String
166
+ def failure_message_when_negated: () -> String
167
+ def description: () -> String
168
+ end
169
+
170
+ class HaveStimulusTarget
171
+ def initialize: (String controller_name, String target_name) -> void
172
+ def matches?: (untyped page_or_node) -> bool
173
+ def does_not_match?: (untyped page_or_node) -> bool
174
+ def failure_message: () -> String
175
+ def failure_message_when_negated: () -> String
176
+ def description: () -> String
177
+ end
178
+
149
179
  class HaveTurboStreamTag
150
180
  def initialize: (?signed_stream_name: String?) -> void
151
181
  def matches?: (untyped page_or_node) -> bool
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.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -54,6 +54,9 @@ files:
54
54
  - lib/turbo_rspec.rb
55
55
  - lib/turbo_rspec/assertions.rb
56
56
  - lib/turbo_rspec/capybara/matchers.rb
57
+ - lib/turbo_rspec/capybara/matchers/have_stimulus_action.rb
58
+ - lib/turbo_rspec/capybara/matchers/have_stimulus_controller.rb
59
+ - lib/turbo_rspec/capybara/matchers/have_stimulus_target.rb
57
60
  - lib/turbo_rspec/capybara/matchers/have_turbo_frame.rb
58
61
  - lib/turbo_rspec/capybara/matchers/have_turbo_stream_tag.rb
59
62
  - lib/turbo_rspec/configuration.rb