stimulus_spec 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9f2064cf807ad376e2e9098aa7a400986c40f2fff504d7e4f2db7450ef02154
4
- data.tar.gz: 0c221ae06953f2ce8e74663b2500ee452e30fee9acfbf08edf02a35c99ea4cb5
3
+ metadata.gz: 31b3ef6e3b0ef09e3f6353d975ee2a65dd7c42dbc4d3be8aa78c7508b88d9ba7
4
+ data.tar.gz: 5f79191a5baccfabe6a8502fa8708248dc71efe1ba63d87f816a2c882ad5e941
5
5
  SHA512:
6
- metadata.gz: c06ffeb2e5a16290a59ee122a7dd40a7933a840dd1899e006755184c172a92ff472260976b0fc08f29522bd78c961d5315ab98471867785342099aef49503646
7
- data.tar.gz: a6d8f79f021f954e00c7c2aec8e5c14c972ced90b1e143324fc47b75bc8b5d6f641e9d33efdf95e348600573df5271a43586f4bdaed3145ac567ff0b24b24672
6
+ metadata.gz: 6b789aa21b411f503223b5c6116507f48168cfd675ea198c4c3a8cf68490754ebcaf0bcdea718c4cf492400cf3df6eba5d0079c41b2e2ce48020165b6b4bb348
7
+ data.tar.gz: 97623a443042991da1c942449406f0e92c0b8918d8a9bdbd8b001962c0c238db103d0d2cc9d27a3aff05bd75256d65b593686876fd731f883f8a5c0547af9d0d
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --markup markdown
2
+ --output-dir doc
3
+ --protected
4
+ --no-private
5
+ lib/**/*.rb
6
+ -
7
+ CHANGELOG.md
8
+ LICENSE.txt
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-06-23
11
+
12
+ ### Added
13
+
14
+ - Full YARD documentation (`@param`, `@return`, `@example`) on all public methods and classes
15
+ - `docs/cookbook.md` with common patterns: request specs, system specs, multi-controller, scoped matching, typed values, outlets
16
+ - `.yardopts` configuration file
17
+
18
+ ## [0.6.0] - 2026-06-23
19
+
20
+ ### Added
21
+
22
+ - Multi-controller assertion: `have_stimulus_controller("hello", "clipboard")` asserts multiple controllers on a single element
23
+ - Scoped matching via `.within(selector)` on all matchers (request-spec and Capybara)
24
+
10
25
  ## [0.5.0] - 2026-06-23
11
26
 
12
27
  ### Added
@@ -46,7 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
46
61
  - `have_stimulus_action(descriptor)` matcher — full descriptor (`~=`) and shorthand without event (`*=`)
47
62
  - `have_stimulus_target(controller, target)` matcher — asserts `[data-{controller}-target~="target"]`
48
63
 
49
- [Unreleased]: https://github.com/eclectic-coding/stimulus_spec/compare/v0.5.0...HEAD
64
+ [Unreleased]: https://github.com/eclectic-coding/stimulus_spec/compare/v0.7.0...HEAD
65
+ [0.7.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.7.0
66
+ [0.6.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.6.0
50
67
  [0.5.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.5.0
51
68
  [0.4.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.4.0
52
69
  [0.3.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.3.0
data/README.md CHANGED
@@ -15,6 +15,13 @@ Drop-in RSpec matchers for [hotwired/stimulus-rails](https://github.com/hotwired
15
15
 
16
16
  Companion gem to [turbo_rspec](https://github.com/eclectic-coding/turbo_rspec) — together they cover the full Hotwire testing stack.
17
17
 
18
+ ## Guiding principles
19
+
20
+ - **Zero magic by default.** Auto-include only when it's unambiguous (Rails request specs with `stimulus-rails` present). Everything else is opt-in.
21
+ - **Fail loudly with useful output.** A cryptic failure message is a bug. Show what was expected, what was found, and the relevant HTML.
22
+ - **Stay close to Stimulus conventions.** Matcher names and arguments mirror Stimulus's `data-*` attribute conventions so the docs cross-reference naturally.
23
+ - **Pure test helper.** No Rails engine, no generators, no runtime code — just matchers you include in your specs.
24
+
18
25
  ## Table of Contents
19
26
 
20
27
  - [Installation](#installation)
@@ -26,6 +33,7 @@ Companion gem to [turbo_rspec](https://github.com/eclectic-coding/turbo_rspec)
26
33
  - [have\_stimulus\_value](#have_stimulus_value)
27
34
  - [have\_stimulus\_class](#have_stimulus_class)
28
35
  - [have\_stimulus\_outlet](#have_stimulus_outlet)
36
+ - [Scoped matching with `.within`](#scoped-matching-with-within)
29
37
  - [Example](#example)
30
38
  - [Relationship to turbo\_rspec](#relationship-to-turbo_rspec)
31
39
  - [Contributing](#contributing)
@@ -84,8 +92,8 @@ Assert that rendered HTML contains a `data-controller` attribute with the given
84
92
  ```ruby
85
93
  expect(response).to have_stimulus_controller("hello")
86
94
 
87
- # Works with multiple controllers on a single element
88
- expect(response).to have_stimulus_controller("clipboard")
95
+ # Assert multiple controllers on a single element
96
+ expect(response).to have_stimulus_controller("hello", "clipboard")
89
97
 
90
98
  # Negation
91
99
  expect(response).not_to have_stimulus_controller("missing")
@@ -165,6 +173,20 @@ expect(response).to have_stimulus_outlet("search", "results", "#results-list")
165
173
  expect(response).not_to have_stimulus_outlet("search", "results")
166
174
  ```
167
175
 
176
+ ### Scoped matching with `.within`
177
+
178
+ All matchers support `.within(selector)` to restrict matching to a specific part of the page:
179
+
180
+ ```ruby
181
+ # Request/controller specs — search within a CSS selector
182
+ expect(response).to have_stimulus_controller("search").within(".search-form")
183
+ expect(response).to have_stimulus_action("click->search#query").within(".search-form")
184
+ expect(response).to have_stimulus_target("search", "input").within(".search-form")
185
+
186
+ # System/feature specs — same API
187
+ expect(page).to have_stimulus_controller("search").within(".search-form")
188
+ ```
189
+
168
190
  [Back to top](#stimulusspec)
169
191
 
170
192
  ## Example
data/ROADMAP.md CHANGED
@@ -1,31 +1,5 @@
1
1
  # stimulus_spec Roadmap
2
2
 
3
- RSpec matchers for [Stimulus](https://github.com/hotwired/stimulus-rails): controllers, actions, targets, values, classes, and outlets. The goal is to replace hand-rolled `data-controller` assertions with expressive, purpose-built matchers.
4
-
5
- ## Guiding principles
6
-
7
- - **Zero magic by default.** Auto-include only when it's unambiguous (Rails request specs with `stimulus-rails` present). Everything else is opt-in.
8
- - **Fail loudly with useful output.** A cryptic failure message is a bug. Show what was expected, what was found, and the relevant HTML.
9
- - **Stay close to Stimulus conventions.** Matcher names and arguments mirror Stimulus's `data-*` attribute conventions so the docs cross-reference naturally.
10
- - **Pure test helper.** No Rails engine, no generators, no runtime code — just matchers you include in your specs.
11
-
12
- ---
13
-
14
- ## 0.6.0 — Multi-Controller and Scoped Matching
15
-
16
- - `have_stimulus_controller("hello", "clipboard")` — assert multiple controllers on a single element
17
- - Scoped matching — assert Stimulus attributes within a specific CSS selector (e.g., within a form or component root)
18
-
19
- ---
20
-
21
- ## 0.7.0 — Documentation and Polish
22
-
23
- - Full YARD documentation on all public methods and classes
24
- - `docs/cookbook.md` — common patterns: request specs, system specs, multi-controller elements, typed values, outlets
25
- - Graceful no-op when `stimulus-rails` is not bundled (no `LoadError`)
26
-
27
- ---
28
-
29
3
  ## 1.0.0 — Stable API
30
4
 
31
5
  - API freeze — public method signatures are part of the semver contract
data/docs/cookbook.md ADDED
@@ -0,0 +1,152 @@
1
+ # StimulusSpec Cookbook
2
+
3
+ Common patterns for testing Stimulus wiring with `stimulus_spec`.
4
+
5
+ ## Request specs
6
+
7
+ ```ruby
8
+ RSpec.describe "Search", type: :request do
9
+ describe "GET /search" do
10
+ before { get search_path }
11
+
12
+ it "wires up the search controller" do
13
+ expect(response).to have_stimulus_controller("search")
14
+ expect(response).to have_stimulus_action("input->search#query")
15
+ expect(response).to have_stimulus_target("search", "input")
16
+ end
17
+
18
+ it "sets the URL value" do
19
+ expect(response).to have_stimulus_value("search", "url", "/results")
20
+ end
21
+ end
22
+ end
23
+ ```
24
+
25
+ ## System specs
26
+
27
+ ```ruby
28
+ RSpec.describe "Search", type: :system do
29
+ it "has the search controller wired up" do
30
+ visit search_path
31
+
32
+ expect(page).to have_stimulus_controller("search")
33
+ expect(page).to have_stimulus_action("input->search#query")
34
+ expect(page).to have_stimulus_target("search", "input")
35
+ end
36
+ end
37
+ ```
38
+
39
+ ## Multi-controller elements
40
+
41
+ When a single element declares multiple Stimulus controllers, assert them all at once:
42
+
43
+ ```ruby
44
+ # Assert both controllers are on the same element
45
+ expect(response).to have_stimulus_controller("dropdown", "keyboard-nav")
46
+ ```
47
+
48
+ Or assert them individually:
49
+
50
+ ```ruby
51
+ expect(response).to have_stimulus_controller("dropdown")
52
+ expect(response).to have_stimulus_controller("keyboard-nav")
53
+ ```
54
+
55
+ ## Scoped matching
56
+
57
+ Use `.within(selector)` to restrict matching to a specific part of the page. Useful when the same controller appears in multiple places:
58
+
59
+ ```ruby
60
+ # Only match within the sidebar
61
+ expect(response).to have_stimulus_controller("nav").within(".sidebar")
62
+
63
+ # Assert a target exists within a specific form
64
+ expect(response).to have_stimulus_target("search", "input").within("#search-form")
65
+
66
+ # Works with all matchers
67
+ expect(response).to have_stimulus_action("click->tabs#select").within(".tab-bar")
68
+ expect(response).to have_stimulus_value("editor", "language", "ruby").within(".code-panel")
69
+ expect(response).to have_stimulus_class("menu", "active", "bg-blue-500").within("nav")
70
+ expect(response).to have_stimulus_outlet("search", "results", "#results-list").within(".search-form")
71
+ ```
72
+
73
+ Scoped matching works the same way with Capybara matchers in system/feature specs:
74
+
75
+ ```ruby
76
+ expect(page).to have_stimulus_controller("nav").within(".sidebar")
77
+ ```
78
+
79
+ ## Typed values
80
+
81
+ Stimulus values are always stored as strings in the DOM. Pass the expected string representation:
82
+
83
+ ```ruby
84
+ # Boolean value
85
+ expect(response).to have_stimulus_value("toggle", "open", "true")
86
+
87
+ # Numeric value
88
+ expect(response).to have_stimulus_value("pagination", "page", "1")
89
+
90
+ # JSON value
91
+ expect(response).to have_stimulus_value("chart", "data", '{"labels":["Jan","Feb"]}')
92
+ ```
93
+
94
+ To assert a value attribute exists without checking its content, use the two-argument form:
95
+
96
+ ```ruby
97
+ expect(response).to have_stimulus_value("search", "url")
98
+ ```
99
+
100
+ ## CSS classes
101
+
102
+ Assert Stimulus CSS class attributes (`data-{controller}-{name}-class`):
103
+
104
+ ```ruby
105
+ # Assert the class attribute exists
106
+ expect(response).to have_stimulus_class("search", "loading")
107
+
108
+ # Assert a specific class value
109
+ expect(response).to have_stimulus_class("search", "loading", "opacity-50")
110
+ ```
111
+
112
+ ## Outlets
113
+
114
+ Assert Stimulus outlet attributes (`data-{controller}-{outlet}-outlet`):
115
+
116
+ ```ruby
117
+ # Assert the outlet attribute exists
118
+ expect(response).to have_stimulus_outlet("search", "results")
119
+
120
+ # Assert a specific CSS selector
121
+ expect(response).to have_stimulus_outlet("search", "results", "#results-list")
122
+ ```
123
+
124
+ ## Negation
125
+
126
+ All matchers support `not_to`:
127
+
128
+ ```ruby
129
+ expect(response).not_to have_stimulus_controller("admin")
130
+ expect(response).not_to have_stimulus_action("click->admin#delete")
131
+ expect(response).not_to have_stimulus_target("admin", "panel")
132
+ expect(response).not_to have_stimulus_value("admin", "role")
133
+ expect(response).not_to have_stimulus_class("admin", "active")
134
+ expect(response).not_to have_stimulus_outlet("admin", "panel")
135
+ ```
136
+
137
+ ## Disabling auto-include
138
+
139
+ By default, matchers are auto-included when `stimulus-rails` is in your bundle. To disable:
140
+
141
+ ```ruby
142
+ # spec/support/stimulus_spec.rb
143
+ StimulusSpec.configure do |config|
144
+ config.auto_include = false
145
+ end
146
+
147
+ # Then include manually where needed
148
+ RSpec.configure do |config|
149
+ config.include StimulusSpec::Matchers, type: :request
150
+ config.include StimulusSpec::Capybara::Matchers, type: :system
151
+ end
152
+ ```
@@ -3,18 +3,32 @@
3
3
  module StimulusSpec
4
4
  module Capybara
5
5
  module Matchers
6
+ # Capybara matcher for +data-action+ attributes.
7
+ #
8
+ # @example
9
+ # expect(page).to have_stimulus_action("click->hello#greet")
6
10
  class HaveStimulusAction
11
+ # @param descriptor [String] action descriptor
7
12
  def initialize(descriptor)
8
13
  @descriptor = descriptor.to_s
9
14
  end
10
15
 
16
+ # @param selector [String] CSS selector for the scope element
17
+ # @return [self]
18
+ def within(selector)
19
+ @scope = selector
20
+ self
21
+ end
22
+
23
+ # @param page [Capybara::Session, Capybara::Node::Simple]
24
+ # @return [Boolean]
11
25
  def matches?(page)
12
26
  @page = page
13
- page.has_css?(css_selector, wait: 0)
27
+ page.has_css?(scoped_selector, wait: 0)
14
28
  end
15
29
 
16
30
  def does_not_match?(page)
17
- page.has_no_css?(css_selector, wait: 0)
31
+ page.has_no_css?(scoped_selector, wait: 0)
18
32
  end
19
33
 
20
34
  def failure_message
@@ -31,15 +45,18 @@ module StimulusSpec
31
45
 
32
46
  private
33
47
 
34
- def css_selector
35
- if @descriptor.include?("->")
36
- "[data-action~='#{@descriptor}']"
37
- else
38
- "[data-action*='#{@descriptor}']"
39
- end
48
+ def scoped_selector
49
+ base = if @descriptor.include?("->")
50
+ "[data-action~='#{@descriptor}']"
51
+ else
52
+ "[data-action*='#{@descriptor}']"
53
+ end
54
+ @scope ? "#{@scope} #{base}" : base
40
55
  end
41
56
  end
42
57
 
58
+ # @param descriptor [String] action descriptor
59
+ # @return [HaveStimulusAction]
43
60
  def have_stimulus_action(descriptor)
44
61
  HaveStimulusAction.new(descriptor)
45
62
  end
@@ -3,7 +3,14 @@
3
3
  module StimulusSpec
4
4
  module Capybara
5
5
  module Matchers
6
+ # Capybara matcher for +data-{controller}-{name}-class+ attributes.
7
+ #
8
+ # @example
9
+ # expect(page).to have_stimulus_class("search", "loading", "opacity-50")
6
10
  class HaveStimulusClass
11
+ # @param controller [String] controller name
12
+ # @param name [String] class name
13
+ # @param expected [String, nil] expected attribute value
7
14
  def initialize(controller, name, expected = nil)
8
15
  @controller = controller.to_s
9
16
  @name = name.to_s
@@ -11,9 +18,18 @@ module StimulusSpec
11
18
  @attr = "data-#{@controller}-#{@name}-class"
12
19
  end
13
20
 
21
+ # @param selector [String] CSS selector for the scope element
22
+ # @return [self]
23
+ def within(selector)
24
+ @scope = selector
25
+ self
26
+ end
27
+
28
+ # @param page [Capybara::Session, Capybara::Node::Simple]
29
+ # @return [Boolean]
14
30
  def matches?(page)
15
31
  @page = page
16
- element = page.first("[#{@attr}]", minimum: 0, wait: 0)
32
+ element = page.first(scoped_selector, minimum: 0, wait: 0)
17
33
  return false unless element
18
34
 
19
35
  if @expected
@@ -47,8 +63,19 @@ module StimulusSpec
47
63
  "have Stimulus class \"#{@name}\" for controller \"#{@controller}\""
48
64
  end
49
65
  end
66
+
67
+ private
68
+
69
+ def scoped_selector
70
+ base = "[#{@attr}]"
71
+ @scope ? "#{@scope} #{base}" : base
72
+ end
50
73
  end
51
74
 
75
+ # @param controller [String] controller name
76
+ # @param name [String] class name
77
+ # @param expected [String, nil] expected class value
78
+ # @return [HaveStimulusClass]
52
79
  def have_stimulus_class(controller, name, expected = nil)
53
80
  HaveStimulusClass.new(controller, name, expected)
54
81
  end
@@ -3,35 +3,64 @@
3
3
  module StimulusSpec
4
4
  module Capybara
5
5
  module Matchers
6
+ # Capybara matcher for +data-controller+ attributes.
7
+ #
8
+ # @example
9
+ # expect(page).to have_stimulus_controller("hello")
10
+ # @example Multiple controllers
11
+ # expect(page).to have_stimulus_controller("hello", "clipboard")
6
12
  class HaveStimulusController
7
- def initialize(name)
8
- @name = name.to_s
13
+ # @param names [Array<String>] one or more controller names
14
+ def initialize(*names)
15
+ @names = names.map(&:to_s)
9
16
  end
10
17
 
18
+ # @param selector [String] CSS selector for the scope element
19
+ # @return [self]
20
+ def within(selector)
21
+ @scope = selector
22
+ self
23
+ end
24
+
25
+ # @param page [Capybara::Session, Capybara::Node::Simple] Capybara page object
26
+ # @return [Boolean]
11
27
  def matches?(page)
12
28
  @page = page
13
- page.has_css?("[data-controller~='#{@name}']", wait: 0)
29
+ page.has_css?(scoped_selector, wait: 0)
14
30
  end
15
31
 
16
32
  def does_not_match?(page)
17
- page.has_no_css?("[data-controller~='#{@name}']", wait: 0)
33
+ page.has_no_css?(scoped_selector, wait: 0)
18
34
  end
19
35
 
20
36
  def failure_message
21
- "expected to find an element with data-controller=\"#{@name}\" on the page"
37
+ "expected to find an element with data-controller=#{label} on the page"
22
38
  end
23
39
 
24
40
  def failure_message_when_negated
25
- "expected not to find an element with data-controller=\"#{@name}\" on the page"
41
+ "expected not to find an element with data-controller=#{label} on the page"
26
42
  end
27
43
 
28
44
  def description
29
- "have Stimulus controller \"#{@name}\""
45
+ "have Stimulus controller #{label}"
46
+ end
47
+
48
+ private
49
+
50
+ def label
51
+ @names.map { |n| "\"#{n}\"" }.join(", ")
52
+ end
53
+
54
+ def scoped_selector
55
+ selector = @names.map { |n| "[data-controller~='#{n}']" }.join
56
+ @scope ? "#{@scope} #{selector}" : selector
30
57
  end
31
58
  end
32
59
 
33
- def have_stimulus_controller(name)
34
- HaveStimulusController.new(name)
60
+ # @param names [Array<String>] one or more controller names
61
+ # @return [HaveStimulusController]
62
+ def have_stimulus_controller(*names)
63
+ HaveStimulusController.new(*names)
35
64
  end
36
65
  end
37
66
  end
@@ -3,7 +3,14 @@
3
3
  module StimulusSpec
4
4
  module Capybara
5
5
  module Matchers
6
+ # Capybara matcher for +data-{controller}-{outlet}-outlet+ attributes.
7
+ #
8
+ # @example
9
+ # expect(page).to have_stimulus_outlet("search", "results", "#results-list")
6
10
  class HaveStimulusOutlet
11
+ # @param controller [String] controller name
12
+ # @param outlet [String] outlet name
13
+ # @param selector [String, nil] expected CSS selector value
7
14
  def initialize(controller, outlet, selector = nil)
8
15
  @controller = controller.to_s
9
16
  @outlet = outlet.to_s
@@ -11,9 +18,18 @@ module StimulusSpec
11
18
  @attr = "data-#{@controller}-#{@outlet}-outlet"
12
19
  end
13
20
 
21
+ # @param selector [String] CSS selector for the scope element
22
+ # @return [self]
23
+ def within(selector)
24
+ @scope = selector
25
+ self
26
+ end
27
+
28
+ # @param page [Capybara::Session, Capybara::Node::Simple]
29
+ # @return [Boolean]
14
30
  def matches?(page)
15
31
  @page = page
16
- element = page.first("[#{@attr}]", minimum: 0, wait: 0)
32
+ element = page.first(scoped_selector, minimum: 0, wait: 0)
17
33
  return false unless element
18
34
 
19
35
  if @selector
@@ -47,8 +63,19 @@ module StimulusSpec
47
63
  "have Stimulus outlet \"#{@outlet}\" for controller \"#{@controller}\""
48
64
  end
49
65
  end
66
+
67
+ private
68
+
69
+ def scoped_selector
70
+ base = "[#{@attr}]"
71
+ @scope ? "#{@scope} #{base}" : base
72
+ end
50
73
  end
51
74
 
75
+ # @param controller [String] controller name
76
+ # @param outlet [String] outlet name
77
+ # @param selector [String, nil] expected CSS selector value
78
+ # @return [HaveStimulusOutlet]
52
79
  def have_stimulus_outlet(controller, outlet, selector = nil)
53
80
  HaveStimulusOutlet.new(controller, outlet, selector)
54
81
  end
@@ -3,19 +3,34 @@
3
3
  module StimulusSpec
4
4
  module Capybara
5
5
  module Matchers
6
+ # Capybara matcher for +data-{controller}-target+ attributes.
7
+ #
8
+ # @example
9
+ # expect(page).to have_stimulus_target("hello", "name")
6
10
  class HaveStimulusTarget
11
+ # @param controller [String] controller name
12
+ # @param target [String] target name
7
13
  def initialize(controller, target)
8
14
  @controller = controller.to_s
9
15
  @target = target.to_s
10
16
  end
11
17
 
18
+ # @param selector [String] CSS selector for the scope element
19
+ # @return [self]
20
+ def within(selector)
21
+ @scope = selector
22
+ self
23
+ end
24
+
25
+ # @param page [Capybara::Session, Capybara::Node::Simple]
26
+ # @return [Boolean]
12
27
  def matches?(page)
13
28
  @page = page
14
- page.has_css?("[data-#{@controller}-target~='#{@target}']", wait: 0)
29
+ page.has_css?(scoped_selector, wait: 0)
15
30
  end
16
31
 
17
32
  def does_not_match?(page)
18
- page.has_no_css?("[data-#{@controller}-target~='#{@target}']", wait: 0)
33
+ page.has_no_css?(scoped_selector, wait: 0)
19
34
  end
20
35
 
21
36
  def failure_message
@@ -29,8 +44,18 @@ module StimulusSpec
29
44
  def description
30
45
  "have Stimulus target \"#{@target}\" for controller \"#{@controller}\""
31
46
  end
47
+
48
+ private
49
+
50
+ def scoped_selector
51
+ base = "[data-#{@controller}-target~='#{@target}']"
52
+ @scope ? "#{@scope} #{base}" : base
53
+ end
32
54
  end
33
55
 
56
+ # @param controller [String] controller name
57
+ # @param target [String] target name
58
+ # @return [HaveStimulusTarget]
34
59
  def have_stimulus_target(controller, target)
35
60
  HaveStimulusTarget.new(controller, target)
36
61
  end
@@ -3,7 +3,14 @@
3
3
  module StimulusSpec
4
4
  module Capybara
5
5
  module Matchers
6
+ # Capybara matcher for +data-{controller}-{name}-value+ attributes.
7
+ #
8
+ # @example
9
+ # expect(page).to have_stimulus_value("search", "url", "/results")
6
10
  class HaveStimulusValue
11
+ # @param controller [String] controller name
12
+ # @param name [String] value name
13
+ # @param expected [String, nil] expected attribute value
7
14
  def initialize(controller, name, expected = nil)
8
15
  @controller = controller.to_s
9
16
  @name = name.to_s
@@ -11,9 +18,18 @@ module StimulusSpec
11
18
  @attr = "data-#{@controller}-#{@name}-value"
12
19
  end
13
20
 
21
+ # @param selector [String] CSS selector for the scope element
22
+ # @return [self]
23
+ def within(selector)
24
+ @scope = selector
25
+ self
26
+ end
27
+
28
+ # @param page [Capybara::Session, Capybara::Node::Simple]
29
+ # @return [Boolean]
14
30
  def matches?(page)
15
31
  @page = page
16
- element = page.first("[#{@attr}]", minimum: 0, wait: 0)
32
+ element = page.first(scoped_selector, minimum: 0, wait: 0)
17
33
  return false unless element
18
34
 
19
35
  if @expected
@@ -47,8 +63,19 @@ module StimulusSpec
47
63
  "have Stimulus value \"#{@name}\" for controller \"#{@controller}\""
48
64
  end
49
65
  end
66
+
67
+ private
68
+
69
+ def scoped_selector
70
+ base = "[#{@attr}]"
71
+ @scope ? "#{@scope} #{base}" : base
72
+ end
50
73
  end
51
74
 
75
+ # @param controller [String] controller name
76
+ # @param name [String] value name
77
+ # @param expected [String, nil] expected value
78
+ # @return [HaveStimulusValue]
52
79
  def have_stimulus_value(controller, name, expected = nil)
53
80
  HaveStimulusValue.new(controller, name, expected)
54
81
  end
@@ -9,6 +9,7 @@ require_relative "matchers/have_stimulus_outlet"
9
9
 
10
10
  module StimulusSpec
11
11
  module Capybara
12
+ # Matchers for system and feature specs (Capybara page objects).
12
13
  module Matchers
13
14
  end
14
15
  end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusSpec
4
+ # Controls auto-include behavior for RSpec integration.
4
5
  class Configuration
6
+ # @return [Boolean] whether matchers are auto-included into RSpec groups (default: +true+)
5
7
  attr_accessor :auto_include
6
8
 
7
9
  def initialize
@@ -2,18 +2,37 @@
2
2
 
3
3
  module StimulusSpec
4
4
  module Matchers
5
+ # Asserts that rendered HTML contains a +data-action+ attribute with the given descriptor.
6
+ #
7
+ # @example Full descriptor
8
+ # expect(response).to have_stimulus_action("click->hello#greet")
9
+ # @example Shorthand (any event)
10
+ # expect(response).to have_stimulus_action("hello#greet")
5
11
  class HaveStimulusAction
12
+ # @param descriptor [String] full (+event->controller#method+) or shorthand (+controller#method+) descriptor
6
13
  def initialize(descriptor)
7
14
  @descriptor = descriptor.to_s
8
15
  end
9
16
 
17
+ # @param selector [String] CSS selector for the scope element
18
+ # @return [self]
19
+ def within(selector)
20
+ @scope = selector
21
+ self
22
+ end
23
+
24
+ # @param subject [#body, String] response object or HTML string
25
+ # @return [Boolean]
10
26
  def matches?(subject)
11
27
  @body = extract_body(subject)
12
28
  @doc = Nokogiri::HTML5.fragment(@body)
29
+ root = search_root
30
+ return false unless root
31
+
13
32
  if @descriptor.include?("->")
14
- !@doc.at_css("[data-action~='#{@descriptor}']").nil?
33
+ !root.at_css("[data-action~='#{@descriptor}']").nil?
15
34
  else
16
- !@doc.at_css("[data-action*='#{@descriptor}']").nil?
35
+ !root.at_css("[data-action*='#{@descriptor}']").nil?
17
36
  end
18
37
  end
19
38
 
@@ -43,14 +62,23 @@ module StimulusSpec
43
62
  subject.respond_to?(:body) ? subject.body : subject.to_s
44
63
  end
45
64
 
65
+ def search_root
66
+ return @doc unless @scope
67
+
68
+ @doc.at_css(@scope)
69
+ end
70
+
46
71
  def snippet
47
- elements = @doc.css("[data-action]")
72
+ root = search_root || @doc
73
+ elements = root.css("[data-action]")
48
74
  return @body if elements.empty?
49
75
 
50
76
  elements.map(&:to_html).join("\n")
51
77
  end
52
78
  end
53
79
 
80
+ # @param descriptor [String] action descriptor
81
+ # @return [HaveStimulusAction]
54
82
  def have_stimulus_action(descriptor)
55
83
  HaveStimulusAction.new(descriptor)
56
84
  end
@@ -2,7 +2,16 @@
2
2
 
3
3
  module StimulusSpec
4
4
  module Matchers
5
+ # Asserts that rendered HTML contains a +data-{controller}-{name}-class+ attribute.
6
+ #
7
+ # @example Existence check
8
+ # expect(response).to have_stimulus_class("search", "loading")
9
+ # @example Equality check
10
+ # expect(response).to have_stimulus_class("search", "loading", "opacity-50")
5
11
  class HaveStimulusClass
12
+ # @param controller [String] controller name
13
+ # @param name [String] class name
14
+ # @param expected [String, nil] expected attribute value (omit for existence check)
6
15
  def initialize(controller, name, expected = nil)
7
16
  @controller = controller.to_s
8
17
  @name = name.to_s
@@ -10,18 +19,25 @@ module StimulusSpec
10
19
  @attr = "data-#{@controller}-#{@name}-class"
11
20
  end
12
21
 
22
+ # @param selector [String] CSS selector for the scope element
23
+ # @return [self]
24
+ def within(selector)
25
+ @scope = selector
26
+ self
27
+ end
28
+
29
+ # @param subject [#body, String] response object or HTML string
30
+ # @return [Boolean]
13
31
  def matches?(subject)
14
32
  @body = extract_body(subject)
15
33
  @doc = Nokogiri::HTML5.fragment(@body)
16
- @element = @doc.at_css("[#{@attr}]")
34
+ @element = search_root&.at_css("[#{@attr}]")
17
35
  return false unless @element
18
36
 
19
- if @expected
20
- @actual = @element[@attr]
21
- @actual == @expected.to_s
22
- else
23
- true
24
- end
37
+ return true unless @expected
38
+
39
+ @actual = @element[@attr]
40
+ @actual == @expected.to_s
25
41
  end
26
42
 
27
43
  def does_not_match?(subject)
@@ -54,14 +70,25 @@ module StimulusSpec
54
70
  subject.respond_to?(:body) ? subject.body : subject.to_s
55
71
  end
56
72
 
73
+ def search_root
74
+ return @doc unless @scope
75
+
76
+ @doc.at_css(@scope)
77
+ end
78
+
57
79
  def snippet
58
- elements = @doc.css("[data-controller]")
80
+ root = search_root || @doc
81
+ elements = root.css("[data-controller]")
59
82
  return @body if elements.empty?
60
83
 
61
84
  elements.map(&:to_html).join("\n")
62
85
  end
63
86
  end
64
87
 
88
+ # @param controller [String] controller name
89
+ # @param name [String] class name
90
+ # @param expected [String, nil] expected class value
91
+ # @return [HaveStimulusClass]
65
92
  def have_stimulus_class(controller, name, expected = nil)
66
93
  HaveStimulusClass.new(controller, name, expected)
67
94
  end
@@ -1,17 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusSpec
4
+ # Matchers for request and controller specs (Nokogiri-based HTML parsing).
4
5
  module Matchers
6
+ # Asserts that rendered HTML contains a +data-controller+ attribute with the given name(s).
7
+ #
8
+ # @example Single controller
9
+ # expect(response).to have_stimulus_controller("hello")
10
+ # @example Multiple controllers on one element
11
+ # expect(response).to have_stimulus_controller("hello", "clipboard")
12
+ # @example Scoped
13
+ # expect(response).to have_stimulus_controller("search").within(".search-form")
5
14
  class HaveStimulusController
6
- def initialize(name)
7
- @name = name.to_s
15
+ # @param names [Array<String>] one or more controller names to match
16
+ def initialize(*names)
17
+ @names = names.map(&:to_s)
8
18
  end
9
19
 
20
+ # Restricts matching to descendants of the given CSS selector.
21
+ #
22
+ # @param selector [String] CSS selector for the scope element
23
+ # @return [self]
24
+ def within(selector)
25
+ @scope = selector
26
+ self
27
+ end
28
+
29
+ # @param subject [#body, String] response object or HTML string
30
+ # @return [Boolean]
10
31
  def matches?(subject)
11
32
  @body = extract_body(subject)
12
33
  @doc = Nokogiri::HTML5.fragment(@body)
13
- @found_controllers = @doc.css("[data-controller]").flat_map { |el| el["data-controller"].split }
14
- !@doc.at_css("[data-controller~='#{@name}']").nil?
34
+ root = search_root
35
+ return false unless root
36
+
37
+ @found_controllers = root.css("[data-controller]").flat_map { |el| el["data-controller"].split }
38
+ selector = @names.map { |n| "[data-controller~='#{n}']" }.join
39
+ !root.at_css(selector).nil?
15
40
  end
16
41
 
17
42
  def does_not_match?(subject)
@@ -19,8 +44,9 @@ module StimulusSpec
19
44
  end
20
45
 
21
46
  def failure_message
22
- msg = "expected to find an element with data-controller=\"#{@name}\""
23
- if @found_controllers.any?
47
+ label = @names.map { |n| "\"#{n}\"" }.join(", ")
48
+ msg = "expected to find an element with data-controller=#{label}"
49
+ if @found_controllers&.any?
24
50
  msg += "\n found controllers: #{@found_controllers.uniq.map { |c| "\"#{c}\"" }.join(", ")}"
25
51
  end
26
52
  msg += "\n in:\n#{snippet}"
@@ -28,11 +54,13 @@ module StimulusSpec
28
54
  end
29
55
 
30
56
  def failure_message_when_negated
31
- "expected not to find an element with data-controller=\"#{@name}\" but found one"
57
+ label = @names.map { |n| "\"#{n}\"" }.join(", ")
58
+ "expected not to find an element with data-controller=#{label} but found one"
32
59
  end
33
60
 
34
61
  def description
35
- "have Stimulus controller \"#{@name}\""
62
+ label = @names.map { |n| "\"#{n}\"" }.join(", ")
63
+ "have Stimulus controller #{label}"
36
64
  end
37
65
 
38
66
  private
@@ -41,16 +69,25 @@ module StimulusSpec
41
69
  subject.respond_to?(:body) ? subject.body : subject.to_s
42
70
  end
43
71
 
72
+ def search_root
73
+ return @doc unless @scope
74
+
75
+ @doc.at_css(@scope)
76
+ end
77
+
44
78
  def snippet
45
- elements = @doc.css("[data-controller]")
79
+ root = search_root || @doc
80
+ elements = root.css("[data-controller]")
46
81
  return @body if elements.empty?
47
82
 
48
83
  elements.map(&:to_html).join("\n")
49
84
  end
50
85
  end
51
86
 
52
- def have_stimulus_controller(name)
53
- HaveStimulusController.new(name)
87
+ # @param names [Array<String>] one or more controller names
88
+ # @return [HaveStimulusController]
89
+ def have_stimulus_controller(*names)
90
+ HaveStimulusController.new(*names)
54
91
  end
55
92
  end
56
93
  end
@@ -2,7 +2,16 @@
2
2
 
3
3
  module StimulusSpec
4
4
  module Matchers
5
+ # Asserts that rendered HTML contains a +data-{controller}-{outlet}-outlet+ attribute.
6
+ #
7
+ # @example Existence check
8
+ # expect(response).to have_stimulus_outlet("search", "results")
9
+ # @example Selector check
10
+ # expect(response).to have_stimulus_outlet("search", "results", "#results-list")
5
11
  class HaveStimulusOutlet
12
+ # @param controller [String] controller name
13
+ # @param outlet [String] outlet name
14
+ # @param selector [String, nil] expected CSS selector value (omit for existence check)
6
15
  def initialize(controller, outlet, selector = nil)
7
16
  @controller = controller.to_s
8
17
  @outlet = outlet.to_s
@@ -10,18 +19,25 @@ module StimulusSpec
10
19
  @attr = "data-#{@controller}-#{@outlet}-outlet"
11
20
  end
12
21
 
22
+ # @param selector [String] CSS selector for the scope element
23
+ # @return [self]
24
+ def within(selector)
25
+ @scope = selector
26
+ self
27
+ end
28
+
29
+ # @param subject [#body, String] response object or HTML string
30
+ # @return [Boolean]
13
31
  def matches?(subject)
14
32
  @body = extract_body(subject)
15
33
  @doc = Nokogiri::HTML5.fragment(@body)
16
- @element = @doc.at_css("[#{@attr}]")
34
+ @element = search_root&.at_css("[#{@attr}]")
17
35
  return false unless @element
18
36
 
19
- if @selector
20
- @actual = @element[@attr]
21
- @actual == @selector.to_s
22
- else
23
- true
24
- end
37
+ return true unless @selector
38
+
39
+ @actual = @element[@attr]
40
+ @actual == @selector.to_s
25
41
  end
26
42
 
27
43
  def does_not_match?(subject)
@@ -54,14 +70,25 @@ module StimulusSpec
54
70
  subject.respond_to?(:body) ? subject.body : subject.to_s
55
71
  end
56
72
 
73
+ def search_root
74
+ return @doc unless @scope
75
+
76
+ @doc.at_css(@scope)
77
+ end
78
+
57
79
  def snippet
58
- elements = @doc.css("[data-controller]")
80
+ root = search_root || @doc
81
+ elements = root.css("[data-controller]")
59
82
  return @body if elements.empty?
60
83
 
61
84
  elements.map(&:to_html).join("\n")
62
85
  end
63
86
  end
64
87
 
88
+ # @param controller [String] controller name
89
+ # @param outlet [String] outlet name
90
+ # @param selector [String, nil] expected CSS selector value
91
+ # @return [HaveStimulusOutlet]
65
92
  def have_stimulus_outlet(controller, outlet, selector = nil)
66
93
  HaveStimulusOutlet.new(controller, outlet, selector)
67
94
  end
@@ -2,17 +2,35 @@
2
2
 
3
3
  module StimulusSpec
4
4
  module Matchers
5
+ # Asserts that rendered HTML contains a +data-{controller}-target+ attribute with the given target name.
6
+ #
7
+ # @example
8
+ # expect(response).to have_stimulus_target("hello", "name")
5
9
  class HaveStimulusTarget
10
+ # @param controller [String] controller name
11
+ # @param target [String] target name
6
12
  def initialize(controller, target)
7
13
  @controller = controller.to_s
8
14
  @target = target.to_s
9
15
  @attr = "data-#{@controller}-target"
10
16
  end
11
17
 
18
+ # @param selector [String] CSS selector for the scope element
19
+ # @return [self]
20
+ def within(selector)
21
+ @scope = selector
22
+ self
23
+ end
24
+
25
+ # @param subject [#body, String] response object or HTML string
26
+ # @return [Boolean]
12
27
  def matches?(subject)
13
28
  @body = extract_body(subject)
14
29
  @doc = Nokogiri::HTML5.fragment(@body)
15
- !@doc.at_css("[#{@attr}~='#{@target}']").nil?
30
+ root = search_root
31
+ return false unless root
32
+
33
+ !root.at_css("[#{@attr}~='#{@target}']").nil?
16
34
  end
17
35
 
18
36
  def does_not_match?(subject)
@@ -41,14 +59,24 @@ module StimulusSpec
41
59
  subject.respond_to?(:body) ? subject.body : subject.to_s
42
60
  end
43
61
 
62
+ def search_root
63
+ return @doc unless @scope
64
+
65
+ @doc.at_css(@scope)
66
+ end
67
+
44
68
  def snippet
45
- elements = @doc.css("[#{@attr}]")
69
+ root = search_root || @doc
70
+ elements = root.css("[#{@attr}]")
46
71
  return @body if elements.empty?
47
72
 
48
73
  elements.map(&:to_html).join("\n")
49
74
  end
50
75
  end
51
76
 
77
+ # @param controller [String] controller name
78
+ # @param target [String] target name
79
+ # @return [HaveStimulusTarget]
52
80
  def have_stimulus_target(controller, target)
53
81
  HaveStimulusTarget.new(controller, target)
54
82
  end
@@ -2,7 +2,16 @@
2
2
 
3
3
  module StimulusSpec
4
4
  module Matchers
5
+ # Asserts that rendered HTML contains a +data-{controller}-{name}-value+ attribute.
6
+ #
7
+ # @example Existence check
8
+ # expect(response).to have_stimulus_value("search", "url")
9
+ # @example Equality check
10
+ # expect(response).to have_stimulus_value("search", "url", "/results")
5
11
  class HaveStimulusValue
12
+ # @param controller [String] controller name
13
+ # @param name [String] value name
14
+ # @param expected [String, nil] expected attribute value (omit for existence check)
6
15
  def initialize(controller, name, expected = nil)
7
16
  @controller = controller.to_s
8
17
  @name = name.to_s
@@ -10,18 +19,25 @@ module StimulusSpec
10
19
  @attr = "data-#{@controller}-#{@name}-value"
11
20
  end
12
21
 
22
+ # @param selector [String] CSS selector for the scope element
23
+ # @return [self]
24
+ def within(selector)
25
+ @scope = selector
26
+ self
27
+ end
28
+
29
+ # @param subject [#body, String] response object or HTML string
30
+ # @return [Boolean]
13
31
  def matches?(subject)
14
32
  @body = extract_body(subject)
15
33
  @doc = Nokogiri::HTML5.fragment(@body)
16
- @element = @doc.at_css("[#{@attr}]")
34
+ @element = search_root&.at_css("[#{@attr}]")
17
35
  return false unless @element
18
36
 
19
- if @expected
20
- @actual = @element[@attr]
21
- @actual == @expected.to_s
22
- else
23
- true
24
- end
37
+ return true unless @expected
38
+
39
+ @actual = @element[@attr]
40
+ @actual == @expected.to_s
25
41
  end
26
42
 
27
43
  def does_not_match?(subject)
@@ -54,14 +70,25 @@ module StimulusSpec
54
70
  subject.respond_to?(:body) ? subject.body : subject.to_s
55
71
  end
56
72
 
73
+ def search_root
74
+ return @doc unless @scope
75
+
76
+ @doc.at_css(@scope)
77
+ end
78
+
57
79
  def snippet
58
- elements = @doc.css("[data-controller]")
80
+ root = search_root || @doc
81
+ elements = root.css("[data-controller]")
59
82
  return @body if elements.empty?
60
83
 
61
84
  elements.map(&:to_html).join("\n")
62
85
  end
63
86
  end
64
87
 
88
+ # @param controller [String] controller name
89
+ # @param name [String] value name
90
+ # @param expected [String, nil] expected value
91
+ # @return [HaveStimulusValue]
65
92
  def have_stimulus_value(controller, name, expected = nil)
66
93
  HaveStimulusValue.new(controller, name, expected)
67
94
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusSpec
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/stimulus_spec.rb CHANGED
@@ -5,21 +5,46 @@ require_relative "stimulus_spec/configuration"
5
5
  require_relative "stimulus_spec/matchers"
6
6
  require_relative "stimulus_spec/capybara/matchers"
7
7
 
8
+ # RSpec matchers for testing Stimulus controller wiring in Rails applications.
9
+ #
10
+ # Provides matchers for asserting +data-controller+, +data-action+,
11
+ # +data-*-target+, +data-*-*-value+, +data-*-*-class+, and
12
+ # +data-*-*-outlet+ attributes in rendered HTML and Capybara pages.
13
+ #
14
+ # @example Auto-included in Rails with stimulus-rails
15
+ # RSpec.describe "Search", type: :request do
16
+ # it "wires up the controller" do
17
+ # get search_path
18
+ # expect(response).to have_stimulus_controller("search")
19
+ # end
20
+ # end
8
21
  module StimulusSpec
9
22
  class Error < StandardError; end
10
23
 
24
+ # @return [Configuration] the current configuration instance
11
25
  def self.configuration
12
26
  @configuration ||= Configuration.new
13
27
  end
14
28
 
29
+ # Yields the configuration for modification.
30
+ #
31
+ # @yieldparam config [Configuration]
32
+ # @return [void]
15
33
  def self.configure
16
34
  yield configuration
17
35
  end
18
36
 
37
+ # Replaces the current configuration with a fresh instance.
38
+ #
39
+ # @return [Configuration] the new configuration
19
40
  def self.reset_configuration!
20
41
  @configuration = Configuration.new
21
42
  end
22
43
 
44
+ # Includes matchers into RSpec example groups when stimulus-rails is present.
45
+ #
46
+ # @param config [RSpec::Core::Configuration]
47
+ # @return [void]
23
48
  def self.install_rspec_integration(config)
24
49
  return unless Gem.loaded_specs.key?("stimulus-rails")
25
50
  return unless configuration.auto_include
@@ -13,7 +13,7 @@ module StimulusSpec
13
13
  end
14
14
 
15
15
  module Matchers
16
- def have_stimulus_controller: (String name) -> HaveStimulusController
16
+ def have_stimulus_controller: (*String names) -> HaveStimulusController
17
17
  def have_stimulus_action: (String descriptor) -> HaveStimulusAction
18
18
  def have_stimulus_target: (String controller, String target) -> HaveStimulusTarget
19
19
  def have_stimulus_value: (String controller, String name, ?String? expected) -> HaveStimulusValue
@@ -21,7 +21,8 @@ module StimulusSpec
21
21
  def have_stimulus_outlet: (String controller, String outlet, ?String? selector) -> HaveStimulusOutlet
22
22
 
23
23
  class HaveStimulusController
24
- def initialize: (String name) -> void
24
+ def initialize: (*String names) -> void
25
+ def within: (String selector) -> self
25
26
  def matches?: (untyped subject) -> bool
26
27
  def does_not_match?: (untyped subject) -> bool
27
28
  def failure_message: () -> String
@@ -31,6 +32,7 @@ module StimulusSpec
31
32
 
32
33
  class HaveStimulusAction
33
34
  def initialize: (String descriptor) -> void
35
+ def within: (String selector) -> self
34
36
  def matches?: (untyped subject) -> bool
35
37
  def does_not_match?: (untyped subject) -> bool
36
38
  def failure_message: () -> String
@@ -40,6 +42,7 @@ module StimulusSpec
40
42
 
41
43
  class HaveStimulusTarget
42
44
  def initialize: (String controller, String target) -> void
45
+ def within: (String selector) -> self
43
46
  def matches?: (untyped subject) -> bool
44
47
  def does_not_match?: (untyped subject) -> bool
45
48
  def failure_message: () -> String
@@ -49,6 +52,7 @@ module StimulusSpec
49
52
 
50
53
  class HaveStimulusValue
51
54
  def initialize: (String controller, String name, ?String? expected) -> void
55
+ def within: (String selector) -> self
52
56
  def matches?: (untyped subject) -> bool
53
57
  def does_not_match?: (untyped subject) -> bool
54
58
  def failure_message: () -> String
@@ -58,6 +62,7 @@ module StimulusSpec
58
62
 
59
63
  class HaveStimulusClass
60
64
  def initialize: (String controller, String name, ?String? expected) -> void
65
+ def within: (String selector) -> self
61
66
  def matches?: (untyped subject) -> bool
62
67
  def does_not_match?: (untyped subject) -> bool
63
68
  def failure_message: () -> String
@@ -67,6 +72,7 @@ module StimulusSpec
67
72
 
68
73
  class HaveStimulusOutlet
69
74
  def initialize: (String controller, String outlet, ?String? selector) -> void
75
+ def within: (String selector) -> self
70
76
  def matches?: (untyped subject) -> bool
71
77
  def does_not_match?: (untyped subject) -> bool
72
78
  def failure_message: () -> String
@@ -77,7 +83,7 @@ module StimulusSpec
77
83
 
78
84
  module Capybara
79
85
  module Matchers
80
- def have_stimulus_controller: (String name) -> HaveStimulusController
86
+ def have_stimulus_controller: (*String names) -> HaveStimulusController
81
87
  def have_stimulus_action: (String descriptor) -> HaveStimulusAction
82
88
  def have_stimulus_target: (String controller, String target) -> HaveStimulusTarget
83
89
  def have_stimulus_value: (String controller, String name, ?String? expected) -> HaveStimulusValue
@@ -85,7 +91,8 @@ module StimulusSpec
85
91
  def have_stimulus_outlet: (String controller, String outlet, ?String? selector) -> HaveStimulusOutlet
86
92
 
87
93
  class HaveStimulusController
88
- def initialize: (String name) -> void
94
+ def initialize: (*String names) -> void
95
+ def within: (String selector) -> self
89
96
  def matches?: (untyped page) -> bool
90
97
  def does_not_match?: (untyped page) -> bool
91
98
  def failure_message: () -> String
@@ -95,6 +102,7 @@ module StimulusSpec
95
102
 
96
103
  class HaveStimulusAction
97
104
  def initialize: (String descriptor) -> void
105
+ def within: (String selector) -> self
98
106
  def matches?: (untyped page) -> bool
99
107
  def does_not_match?: (untyped page) -> bool
100
108
  def failure_message: () -> String
@@ -104,6 +112,7 @@ module StimulusSpec
104
112
 
105
113
  class HaveStimulusTarget
106
114
  def initialize: (String controller, String target) -> void
115
+ def within: (String selector) -> self
107
116
  def matches?: (untyped page) -> bool
108
117
  def does_not_match?: (untyped page) -> bool
109
118
  def failure_message: () -> String
@@ -113,6 +122,7 @@ module StimulusSpec
113
122
 
114
123
  class HaveStimulusValue
115
124
  def initialize: (String controller, String name, ?String? expected) -> void
125
+ def within: (String selector) -> self
116
126
  def matches?: (untyped page) -> bool
117
127
  def does_not_match?: (untyped page) -> bool
118
128
  def failure_message: () -> String
@@ -122,6 +132,7 @@ module StimulusSpec
122
132
 
123
133
  class HaveStimulusClass
124
134
  def initialize: (String controller, String name, ?String? expected) -> void
135
+ def within: (String selector) -> self
125
136
  def matches?: (untyped page) -> bool
126
137
  def does_not_match?: (untyped page) -> bool
127
138
  def failure_message: () -> String
@@ -131,6 +142,7 @@ module StimulusSpec
131
142
 
132
143
  class HaveStimulusOutlet
133
144
  def initialize: (String controller, String outlet, ?String? selector) -> void
145
+ def within: (String selector) -> self
134
146
  def matches?: (untyped page) -> bool
135
147
  def does_not_match?: (untyped page) -> bool
136
148
  def failure_message: () -> String
@@ -139,4 +151,4 @@ module StimulusSpec
139
151
  end
140
152
  end
141
153
  end
142
- end
154
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stimulus_spec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -36,6 +36,7 @@ files:
36
36
  - ".github/FUNDING.yml"
37
37
  - ".github/workflows/main.yml"
38
38
  - ".github/workflows/publish.yml"
39
+ - ".yardopts"
39
40
  - CHANGELOG.md
40
41
  - CONTRIBUTING.md
41
42
  - LICENSE.txt
@@ -43,6 +44,7 @@ files:
43
44
  - ROADMAP.md
44
45
  - Rakefile
45
46
  - codecov.yml
47
+ - docs/cookbook.md
46
48
  - lib/stimulus_spec.rb
47
49
  - lib/stimulus_spec/capybara/matchers.rb
48
50
  - lib/stimulus_spec/capybara/matchers/have_stimulus_action.rb