stimulus_spec 0.4.0 → 0.6.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: ed3850b553fca15bb450d8ad6e0f50d37593c90bd440d26c6cbf0a8eb99882ea
4
- data.tar.gz: a0b96c0888a87a93ef3db2e783d747642f09f863003c3f9a8fc43cc04ca4f392
3
+ metadata.gz: 016bc15660c32251a12c1c9129fa88c86a3e2ac1059949535a67cdd5d64448ce
4
+ data.tar.gz: 76277d4b548369259e6ae9f28b8ad2b4d22637659f7c13a938e5cf2fd95127e5
5
5
  SHA512:
6
- metadata.gz: 0ee41b07ee4796f9670ec0a805ff0b6f52179fddc5cec9169f8272d65bc1bf6c7b114fc173093a091e2f468b42c1a6e06d1d0d3bd6b7c837ceb2b47686c96388
7
- data.tar.gz: fe153dd036e2c7ab6d27b9e22a3bdc88f30f9d16b73cae11d7fbd146bb7b5567faa4f0331e83bb3f1fb2fde8bba94d9674b45ce99011b3cf6e84d7ee02ad711c
6
+ metadata.gz: 8502635e782f3c870ee6aa638e09cba1bbc054c731d1c4599e5b23a31551bd0fb5dce9fc71eaa3d2ca8d3713b2ab8ba26591749a59b9bcd51bb09d871d11a2cc
7
+ data.tar.gz: 300a7bc9351bb8d0dbcc3f70122094d03705fd876322f36187c587daa0bf44446477a57df6e2130252d3501577b1e410bd1510836e70c97729b430b7dd5a6f33
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-06-23
11
+
12
+ ### Added
13
+
14
+ - Multi-controller assertion: `have_stimulus_controller("hello", "clipboard")` asserts multiple controllers on a single element
15
+ - Scoped matching via `.within(selector)` on all matchers (request-spec and Capybara)
16
+
17
+ ## [0.5.0] - 2026-06-23
18
+
19
+ ### Added
20
+
21
+ - Capybara `have_stimulus_class` matcher (existence and equality modes) for system/feature specs
22
+ - Capybara `have_stimulus_outlet` matcher (existence and selector modes) for system/feature specs
23
+
10
24
  ## [0.4.0] - 2026-06-22
11
25
 
12
26
  ### Added
@@ -39,7 +53,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
39
53
  - `have_stimulus_action(descriptor)` matcher — full descriptor (`~=`) and shorthand without event (`*=`)
40
54
  - `have_stimulus_target(controller, target)` matcher — asserts `[data-{controller}-target~="target"]`
41
55
 
42
- [Unreleased]: https://github.com/eclectic-coding/stimulus_spec/compare/v0.4.0...HEAD
56
+ [Unreleased]: https://github.com/eclectic-coding/stimulus_spec/compare/v0.6.0...HEAD
57
+ [0.6.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.6.0
58
+ [0.5.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.5.0
43
59
  [0.4.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.4.0
44
60
  [0.3.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.3.0
45
61
  [0.1.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.1.0
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  Drop-in RSpec matchers for [hotwired/stimulus-rails](https://github.com/hotwired/stimulus-rails) — stop hand-rolling `data-controller` assertions and test your Stimulus wiring with expressive, purpose-built matchers.
10
10
 
11
11
  - **Request/controller specs** — `have_stimulus_controller`, `have_stimulus_action`, `have_stimulus_target`, `have_stimulus_value`, `have_stimulus_class`, `have_stimulus_outlet`
12
- - **System/feature specs** — Capybara matchers: `have_stimulus_controller`, `have_stimulus_action`, `have_stimulus_target`, `have_stimulus_value`
12
+ - **System/feature specs** — Capybara matchers: `have_stimulus_controller`, `have_stimulus_action`, `have_stimulus_target`, `have_stimulus_value`, `have_stimulus_class`, `have_stimulus_outlet`
13
13
  - **Auto-included** — zero setup required when `stimulus-rails` is in your bundle
14
14
  - **Configurable** — disable auto-include when you need manual control
15
15
 
@@ -26,6 +26,7 @@ Companion gem to [turbo_rspec](https://github.com/eclectic-coding/turbo_rspec)
26
26
  - [have\_stimulus\_value](#have_stimulus_value)
27
27
  - [have\_stimulus\_class](#have_stimulus_class)
28
28
  - [have\_stimulus\_outlet](#have_stimulus_outlet)
29
+ - [Scoped matching with `.within`](#scoped-matching-with-within)
29
30
  - [Example](#example)
30
31
  - [Relationship to turbo\_rspec](#relationship-to-turbo_rspec)
31
32
  - [Contributing](#contributing)
@@ -84,8 +85,8 @@ Assert that rendered HTML contains a `data-controller` attribute with the given
84
85
  ```ruby
85
86
  expect(response).to have_stimulus_controller("hello")
86
87
 
87
- # Works with multiple controllers on a single element
88
- expect(response).to have_stimulus_controller("clipboard")
88
+ # Assert multiple controllers on a single element
89
+ expect(response).to have_stimulus_controller("hello", "clipboard")
89
90
 
90
91
  # Negation
91
92
  expect(response).not_to have_stimulus_controller("missing")
@@ -165,6 +166,20 @@ expect(response).to have_stimulus_outlet("search", "results", "#results-list")
165
166
  expect(response).not_to have_stimulus_outlet("search", "results")
166
167
  ```
167
168
 
169
+ ### Scoped matching with `.within`
170
+
171
+ All matchers support `.within(selector)` to restrict matching to a specific part of the page:
172
+
173
+ ```ruby
174
+ # Request/controller specs — search within a CSS selector
175
+ expect(response).to have_stimulus_controller("search").within(".search-form")
176
+ expect(response).to have_stimulus_action("click->search#query").within(".search-form")
177
+ expect(response).to have_stimulus_target("search", "input").within(".search-form")
178
+
179
+ # System/feature specs — same API
180
+ expect(page).to have_stimulus_controller("search").within(".search-form")
181
+ ```
182
+
168
183
  [Back to top](#stimulusspec)
169
184
 
170
185
  ## Example
data/ROADMAP.md CHANGED
@@ -11,7 +11,7 @@ RSpec matchers for [Stimulus](https://github.com/hotwired/stimulus-rails): contr
11
11
 
12
12
  ---
13
13
 
14
- ## 0.5.0 — Documentation and Polish
14
+ ## 0.7.0 — Documentation and Polish
15
15
 
16
16
  - Full YARD documentation on all public methods and classes
17
17
  - `docs/cookbook.md` — common patterns: request specs, system specs, multi-controller elements, typed values, outlets
@@ -8,13 +8,18 @@ module StimulusSpec
8
8
  @descriptor = descriptor.to_s
9
9
  end
10
10
 
11
+ def within(selector)
12
+ @scope = selector
13
+ self
14
+ end
15
+
11
16
  def matches?(page)
12
17
  @page = page
13
- page.has_css?(css_selector, wait: 0)
18
+ page.has_css?(scoped_selector, wait: 0)
14
19
  end
15
20
 
16
21
  def does_not_match?(page)
17
- page.has_no_css?(css_selector, wait: 0)
22
+ page.has_no_css?(scoped_selector, wait: 0)
18
23
  end
19
24
 
20
25
  def failure_message
@@ -31,12 +36,13 @@ module StimulusSpec
31
36
 
32
37
  private
33
38
 
34
- def css_selector
35
- if @descriptor.include?("->")
36
- "[data-action~='#{@descriptor}']"
37
- else
38
- "[data-action*='#{@descriptor}']"
39
- end
39
+ def scoped_selector
40
+ base = if @descriptor.include?("->")
41
+ "[data-action~='#{@descriptor}']"
42
+ else
43
+ "[data-action*='#{@descriptor}']"
44
+ end
45
+ @scope ? "#{@scope} #{base}" : base
40
46
  end
41
47
  end
42
48
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Capybara
5
+ module Matchers
6
+ class HaveStimulusClass
7
+ def initialize(controller, name, expected = nil)
8
+ @controller = controller.to_s
9
+ @name = name.to_s
10
+ @expected = expected
11
+ @attr = "data-#{@controller}-#{@name}-class"
12
+ end
13
+
14
+ def within(selector)
15
+ @scope = selector
16
+ self
17
+ end
18
+
19
+ def matches?(page)
20
+ @page = page
21
+ element = page.first(scoped_selector, minimum: 0, wait: 0)
22
+ return false unless element
23
+
24
+ if @expected
25
+ @actual = element[@attr]
26
+ @actual == @expected.to_s
27
+ else
28
+ true
29
+ end
30
+ end
31
+
32
+ def does_not_match?(page)
33
+ !matches?(page)
34
+ end
35
+
36
+ def failure_message
37
+ if @expected && @actual
38
+ "expected #{@attr} to be \"#{@expected}\" but was \"#{@actual}\""
39
+ else
40
+ "expected to find an element with #{@attr} on the page"
41
+ end
42
+ end
43
+
44
+ def failure_message_when_negated
45
+ "expected not to find an element with #{@attr} on the page"
46
+ end
47
+
48
+ def description
49
+ if @expected
50
+ "have Stimulus class \"#{@name}\" with \"#{@expected}\" for controller \"#{@controller}\""
51
+ else
52
+ "have Stimulus class \"#{@name}\" for controller \"#{@controller}\""
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def scoped_selector
59
+ base = "[#{@attr}]"
60
+ @scope ? "#{@scope} #{base}" : base
61
+ end
62
+ end
63
+
64
+ def have_stimulus_class(controller, name, expected = nil)
65
+ HaveStimulusClass.new(controller, name, expected)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -4,34 +4,50 @@ module StimulusSpec
4
4
  module Capybara
5
5
  module Matchers
6
6
  class HaveStimulusController
7
- def initialize(name)
8
- @name = name.to_s
7
+ def initialize(*names)
8
+ @names = names.map(&:to_s)
9
+ end
10
+
11
+ def within(selector)
12
+ @scope = selector
13
+ self
9
14
  end
10
15
 
11
16
  def matches?(page)
12
17
  @page = page
13
- page.has_css?("[data-controller~='#{@name}']", wait: 0)
18
+ page.has_css?(scoped_selector, wait: 0)
14
19
  end
15
20
 
16
21
  def does_not_match?(page)
17
- page.has_no_css?("[data-controller~='#{@name}']", wait: 0)
22
+ page.has_no_css?(scoped_selector, wait: 0)
18
23
  end
19
24
 
20
25
  def failure_message
21
- "expected to find an element with data-controller=\"#{@name}\" on the page"
26
+ "expected to find an element with data-controller=#{label} on the page"
22
27
  end
23
28
 
24
29
  def failure_message_when_negated
25
- "expected not to find an element with data-controller=\"#{@name}\" on the page"
30
+ "expected not to find an element with data-controller=#{label} on the page"
26
31
  end
27
32
 
28
33
  def description
29
- "have Stimulus controller \"#{@name}\""
34
+ "have Stimulus controller #{label}"
35
+ end
36
+
37
+ private
38
+
39
+ def label
40
+ @names.map { |n| "\"#{n}\"" }.join(", ")
41
+ end
42
+
43
+ def scoped_selector
44
+ selector = @names.map { |n| "[data-controller~='#{n}']" }.join
45
+ @scope ? "#{@scope} #{selector}" : selector
30
46
  end
31
47
  end
32
48
 
33
- def have_stimulus_controller(name)
34
- HaveStimulusController.new(name)
49
+ def have_stimulus_controller(*names)
50
+ HaveStimulusController.new(*names)
35
51
  end
36
52
  end
37
53
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Capybara
5
+ module Matchers
6
+ class HaveStimulusOutlet
7
+ def initialize(controller, outlet, selector = nil)
8
+ @controller = controller.to_s
9
+ @outlet = outlet.to_s
10
+ @selector = selector
11
+ @attr = "data-#{@controller}-#{@outlet}-outlet"
12
+ end
13
+
14
+ def within(selector)
15
+ @scope = selector
16
+ self
17
+ end
18
+
19
+ def matches?(page)
20
+ @page = page
21
+ element = page.first(scoped_selector, minimum: 0, wait: 0)
22
+ return false unless element
23
+
24
+ if @selector
25
+ @actual = element[@attr]
26
+ @actual == @selector.to_s
27
+ else
28
+ true
29
+ end
30
+ end
31
+
32
+ def does_not_match?(page)
33
+ !matches?(page)
34
+ end
35
+
36
+ def failure_message
37
+ if @selector && @actual
38
+ "expected #{@attr} to be \"#{@selector}\" but was \"#{@actual}\""
39
+ else
40
+ "expected to find an element with #{@attr} on the page"
41
+ end
42
+ end
43
+
44
+ def failure_message_when_negated
45
+ "expected not to find an element with #{@attr} on the page"
46
+ end
47
+
48
+ def description
49
+ if @selector
50
+ "have Stimulus outlet \"#{@outlet}\" with \"#{@selector}\" for controller \"#{@controller}\""
51
+ else
52
+ "have Stimulus outlet \"#{@outlet}\" for controller \"#{@controller}\""
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def scoped_selector
59
+ base = "[#{@attr}]"
60
+ @scope ? "#{@scope} #{base}" : base
61
+ end
62
+ end
63
+
64
+ def have_stimulus_outlet(controller, outlet, selector = nil)
65
+ HaveStimulusOutlet.new(controller, outlet, selector)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -9,13 +9,18 @@ module StimulusSpec
9
9
  @target = target.to_s
10
10
  end
11
11
 
12
+ def within(selector)
13
+ @scope = selector
14
+ self
15
+ end
16
+
12
17
  def matches?(page)
13
18
  @page = page
14
- page.has_css?("[data-#{@controller}-target~='#{@target}']", wait: 0)
19
+ page.has_css?(scoped_selector, wait: 0)
15
20
  end
16
21
 
17
22
  def does_not_match?(page)
18
- page.has_no_css?("[data-#{@controller}-target~='#{@target}']", wait: 0)
23
+ page.has_no_css?(scoped_selector, wait: 0)
19
24
  end
20
25
 
21
26
  def failure_message
@@ -29,6 +34,13 @@ module StimulusSpec
29
34
  def description
30
35
  "have Stimulus target \"#{@target}\" for controller \"#{@controller}\""
31
36
  end
37
+
38
+ private
39
+
40
+ def scoped_selector
41
+ base = "[data-#{@controller}-target~='#{@target}']"
42
+ @scope ? "#{@scope} #{base}" : base
43
+ end
32
44
  end
33
45
 
34
46
  def have_stimulus_target(controller, target)
@@ -11,9 +11,14 @@ module StimulusSpec
11
11
  @attr = "data-#{@controller}-#{@name}-value"
12
12
  end
13
13
 
14
+ def within(selector)
15
+ @scope = selector
16
+ self
17
+ end
18
+
14
19
  def matches?(page)
15
20
  @page = page
16
- element = page.first("[#{@attr}]", minimum: 0, wait: 0)
21
+ element = page.first(scoped_selector, minimum: 0, wait: 0)
17
22
  return false unless element
18
23
 
19
24
  if @expected
@@ -47,6 +52,13 @@ module StimulusSpec
47
52
  "have Stimulus value \"#{@name}\" for controller \"#{@controller}\""
48
53
  end
49
54
  end
55
+
56
+ private
57
+
58
+ def scoped_selector
59
+ base = "[#{@attr}]"
60
+ @scope ? "#{@scope} #{base}" : base
61
+ end
50
62
  end
51
63
 
52
64
  def have_stimulus_value(controller, name, expected = nil)
@@ -4,6 +4,8 @@ require_relative "matchers/have_stimulus_controller"
4
4
  require_relative "matchers/have_stimulus_action"
5
5
  require_relative "matchers/have_stimulus_target"
6
6
  require_relative "matchers/have_stimulus_value"
7
+ require_relative "matchers/have_stimulus_class"
8
+ require_relative "matchers/have_stimulus_outlet"
7
9
 
8
10
  module StimulusSpec
9
11
  module Capybara
@@ -7,13 +7,21 @@ module StimulusSpec
7
7
  @descriptor = descriptor.to_s
8
8
  end
9
9
 
10
+ def within(selector)
11
+ @scope = selector
12
+ self
13
+ end
14
+
10
15
  def matches?(subject)
11
16
  @body = extract_body(subject)
12
17
  @doc = Nokogiri::HTML5.fragment(@body)
18
+ root = search_root
19
+ return false unless root
20
+
13
21
  if @descriptor.include?("->")
14
- !@doc.at_css("[data-action~='#{@descriptor}']").nil?
22
+ !root.at_css("[data-action~='#{@descriptor}']").nil?
15
23
  else
16
- !@doc.at_css("[data-action*='#{@descriptor}']").nil?
24
+ !root.at_css("[data-action*='#{@descriptor}']").nil?
17
25
  end
18
26
  end
19
27
 
@@ -43,8 +51,15 @@ module StimulusSpec
43
51
  subject.respond_to?(:body) ? subject.body : subject.to_s
44
52
  end
45
53
 
54
+ def search_root
55
+ return @doc unless @scope
56
+
57
+ @doc.at_css(@scope)
58
+ end
59
+
46
60
  def snippet
47
- elements = @doc.css("[data-action]")
61
+ root = search_root || @doc
62
+ elements = root.css("[data-action]")
48
63
  return @body if elements.empty?
49
64
 
50
65
  elements.map(&:to_html).join("\n")
@@ -10,18 +10,21 @@ module StimulusSpec
10
10
  @attr = "data-#{@controller}-#{@name}-class"
11
11
  end
12
12
 
13
+ def within(selector)
14
+ @scope = selector
15
+ self
16
+ end
17
+
13
18
  def matches?(subject)
14
19
  @body = extract_body(subject)
15
20
  @doc = Nokogiri::HTML5.fragment(@body)
16
- @element = @doc.at_css("[#{@attr}]")
21
+ @element = search_root&.at_css("[#{@attr}]")
17
22
  return false unless @element
18
23
 
19
- if @expected
20
- @actual = @element[@attr]
21
- @actual == @expected.to_s
22
- else
23
- true
24
- end
24
+ return true unless @expected
25
+
26
+ @actual = @element[@attr]
27
+ @actual == @expected.to_s
25
28
  end
26
29
 
27
30
  def does_not_match?(subject)
@@ -54,8 +57,15 @@ module StimulusSpec
54
57
  subject.respond_to?(:body) ? subject.body : subject.to_s
55
58
  end
56
59
 
60
+ def search_root
61
+ return @doc unless @scope
62
+
63
+ @doc.at_css(@scope)
64
+ end
65
+
57
66
  def snippet
58
- elements = @doc.css("[data-controller]")
67
+ root = search_root || @doc
68
+ elements = root.css("[data-controller]")
59
69
  return @body if elements.empty?
60
70
 
61
71
  elements.map(&:to_html).join("\n")
@@ -3,15 +3,24 @@
3
3
  module StimulusSpec
4
4
  module Matchers
5
5
  class HaveStimulusController
6
- def initialize(name)
7
- @name = name.to_s
6
+ def initialize(*names)
7
+ @names = names.map(&:to_s)
8
+ end
9
+
10
+ def within(selector)
11
+ @scope = selector
12
+ self
8
13
  end
9
14
 
10
15
  def matches?(subject)
11
16
  @body = extract_body(subject)
12
17
  @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?
18
+ root = search_root
19
+ return false unless root
20
+
21
+ @found_controllers = root.css("[data-controller]").flat_map { |el| el["data-controller"].split }
22
+ selector = @names.map { |n| "[data-controller~='#{n}']" }.join
23
+ !root.at_css(selector).nil?
15
24
  end
16
25
 
17
26
  def does_not_match?(subject)
@@ -19,8 +28,9 @@ module StimulusSpec
19
28
  end
20
29
 
21
30
  def failure_message
22
- msg = "expected to find an element with data-controller=\"#{@name}\""
23
- if @found_controllers.any?
31
+ label = @names.map { |n| "\"#{n}\"" }.join(", ")
32
+ msg = "expected to find an element with data-controller=#{label}"
33
+ if @found_controllers&.any?
24
34
  msg += "\n found controllers: #{@found_controllers.uniq.map { |c| "\"#{c}\"" }.join(", ")}"
25
35
  end
26
36
  msg += "\n in:\n#{snippet}"
@@ -28,11 +38,13 @@ module StimulusSpec
28
38
  end
29
39
 
30
40
  def failure_message_when_negated
31
- "expected not to find an element with data-controller=\"#{@name}\" but found one"
41
+ label = @names.map { |n| "\"#{n}\"" }.join(", ")
42
+ "expected not to find an element with data-controller=#{label} but found one"
32
43
  end
33
44
 
34
45
  def description
35
- "have Stimulus controller \"#{@name}\""
46
+ label = @names.map { |n| "\"#{n}\"" }.join(", ")
47
+ "have Stimulus controller #{label}"
36
48
  end
37
49
 
38
50
  private
@@ -41,16 +53,23 @@ module StimulusSpec
41
53
  subject.respond_to?(:body) ? subject.body : subject.to_s
42
54
  end
43
55
 
56
+ def search_root
57
+ return @doc unless @scope
58
+
59
+ @doc.at_css(@scope)
60
+ end
61
+
44
62
  def snippet
45
- elements = @doc.css("[data-controller]")
63
+ root = search_root || @doc
64
+ elements = root.css("[data-controller]")
46
65
  return @body if elements.empty?
47
66
 
48
67
  elements.map(&:to_html).join("\n")
49
68
  end
50
69
  end
51
70
 
52
- def have_stimulus_controller(name)
53
- HaveStimulusController.new(name)
71
+ def have_stimulus_controller(*names)
72
+ HaveStimulusController.new(*names)
54
73
  end
55
74
  end
56
75
  end
@@ -10,18 +10,21 @@ module StimulusSpec
10
10
  @attr = "data-#{@controller}-#{@outlet}-outlet"
11
11
  end
12
12
 
13
+ def within(selector)
14
+ @scope = selector
15
+ self
16
+ end
17
+
13
18
  def matches?(subject)
14
19
  @body = extract_body(subject)
15
20
  @doc = Nokogiri::HTML5.fragment(@body)
16
- @element = @doc.at_css("[#{@attr}]")
21
+ @element = search_root&.at_css("[#{@attr}]")
17
22
  return false unless @element
18
23
 
19
- if @selector
20
- @actual = @element[@attr]
21
- @actual == @selector.to_s
22
- else
23
- true
24
- end
24
+ return true unless @selector
25
+
26
+ @actual = @element[@attr]
27
+ @actual == @selector.to_s
25
28
  end
26
29
 
27
30
  def does_not_match?(subject)
@@ -54,8 +57,15 @@ module StimulusSpec
54
57
  subject.respond_to?(:body) ? subject.body : subject.to_s
55
58
  end
56
59
 
60
+ def search_root
61
+ return @doc unless @scope
62
+
63
+ @doc.at_css(@scope)
64
+ end
65
+
57
66
  def snippet
58
- elements = @doc.css("[data-controller]")
67
+ root = search_root || @doc
68
+ elements = root.css("[data-controller]")
59
69
  return @body if elements.empty?
60
70
 
61
71
  elements.map(&:to_html).join("\n")
@@ -9,10 +9,18 @@ module StimulusSpec
9
9
  @attr = "data-#{@controller}-target"
10
10
  end
11
11
 
12
+ def within(selector)
13
+ @scope = selector
14
+ self
15
+ end
16
+
12
17
  def matches?(subject)
13
18
  @body = extract_body(subject)
14
19
  @doc = Nokogiri::HTML5.fragment(@body)
15
- !@doc.at_css("[#{@attr}~='#{@target}']").nil?
20
+ root = search_root
21
+ return false unless root
22
+
23
+ !root.at_css("[#{@attr}~='#{@target}']").nil?
16
24
  end
17
25
 
18
26
  def does_not_match?(subject)
@@ -41,8 +49,15 @@ module StimulusSpec
41
49
  subject.respond_to?(:body) ? subject.body : subject.to_s
42
50
  end
43
51
 
52
+ def search_root
53
+ return @doc unless @scope
54
+
55
+ @doc.at_css(@scope)
56
+ end
57
+
44
58
  def snippet
45
- elements = @doc.css("[#{@attr}]")
59
+ root = search_root || @doc
60
+ elements = root.css("[#{@attr}]")
46
61
  return @body if elements.empty?
47
62
 
48
63
  elements.map(&:to_html).join("\n")
@@ -10,18 +10,21 @@ module StimulusSpec
10
10
  @attr = "data-#{@controller}-#{@name}-value"
11
11
  end
12
12
 
13
+ def within(selector)
14
+ @scope = selector
15
+ self
16
+ end
17
+
13
18
  def matches?(subject)
14
19
  @body = extract_body(subject)
15
20
  @doc = Nokogiri::HTML5.fragment(@body)
16
- @element = @doc.at_css("[#{@attr}]")
21
+ @element = search_root&.at_css("[#{@attr}]")
17
22
  return false unless @element
18
23
 
19
- if @expected
20
- @actual = @element[@attr]
21
- @actual == @expected.to_s
22
- else
23
- true
24
- end
24
+ return true unless @expected
25
+
26
+ @actual = @element[@attr]
27
+ @actual == @expected.to_s
25
28
  end
26
29
 
27
30
  def does_not_match?(subject)
@@ -54,8 +57,15 @@ module StimulusSpec
54
57
  subject.respond_to?(:body) ? subject.body : subject.to_s
55
58
  end
56
59
 
60
+ def search_root
61
+ return @doc unless @scope
62
+
63
+ @doc.at_css(@scope)
64
+ end
65
+
57
66
  def snippet
58
- elements = @doc.css("[data-controller]")
67
+ root = search_root || @doc
68
+ elements = root.css("[data-controller]")
59
69
  return @body if elements.empty?
60
70
 
61
71
  elements.map(&:to_html).join("\n")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusSpec
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -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,13 +83,16 @@ 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
90
+ def have_stimulus_class: (String controller, String name, ?String? expected) -> HaveStimulusClass
91
+ def have_stimulus_outlet: (String controller, String outlet, ?String? selector) -> HaveStimulusOutlet
84
92
 
85
93
  class HaveStimulusController
86
- def initialize: (String name) -> void
94
+ def initialize: (*String names) -> void
95
+ def within: (String selector) -> self
87
96
  def matches?: (untyped page) -> bool
88
97
  def does_not_match?: (untyped page) -> bool
89
98
  def failure_message: () -> String
@@ -93,6 +102,7 @@ module StimulusSpec
93
102
 
94
103
  class HaveStimulusAction
95
104
  def initialize: (String descriptor) -> void
105
+ def within: (String selector) -> self
96
106
  def matches?: (untyped page) -> bool
97
107
  def does_not_match?: (untyped page) -> bool
98
108
  def failure_message: () -> String
@@ -102,6 +112,7 @@ module StimulusSpec
102
112
 
103
113
  class HaveStimulusTarget
104
114
  def initialize: (String controller, String target) -> void
115
+ def within: (String selector) -> self
105
116
  def matches?: (untyped page) -> bool
106
117
  def does_not_match?: (untyped page) -> bool
107
118
  def failure_message: () -> String
@@ -111,6 +122,27 @@ module StimulusSpec
111
122
 
112
123
  class HaveStimulusValue
113
124
  def initialize: (String controller, String name, ?String? expected) -> void
125
+ def within: (String selector) -> self
126
+ def matches?: (untyped page) -> bool
127
+ def does_not_match?: (untyped page) -> bool
128
+ def failure_message: () -> String
129
+ def failure_message_when_negated: () -> String
130
+ def description: () -> String
131
+ end
132
+
133
+ class HaveStimulusClass
134
+ def initialize: (String controller, String name, ?String? expected) -> void
135
+ def within: (String selector) -> self
136
+ def matches?: (untyped page) -> bool
137
+ def does_not_match?: (untyped page) -> bool
138
+ def failure_message: () -> String
139
+ def failure_message_when_negated: () -> String
140
+ def description: () -> String
141
+ end
142
+
143
+ class HaveStimulusOutlet
144
+ def initialize: (String controller, String outlet, ?String? selector) -> void
145
+ def within: (String selector) -> self
114
146
  def matches?: (untyped page) -> bool
115
147
  def does_not_match?: (untyped page) -> bool
116
148
  def failure_message: () -> String
@@ -119,4 +151,4 @@ module StimulusSpec
119
151
  end
120
152
  end
121
153
  end
122
- 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.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -46,7 +46,9 @@ files:
46
46
  - lib/stimulus_spec.rb
47
47
  - lib/stimulus_spec/capybara/matchers.rb
48
48
  - lib/stimulus_spec/capybara/matchers/have_stimulus_action.rb
49
+ - lib/stimulus_spec/capybara/matchers/have_stimulus_class.rb
49
50
  - lib/stimulus_spec/capybara/matchers/have_stimulus_controller.rb
51
+ - lib/stimulus_spec/capybara/matchers/have_stimulus_outlet.rb
50
52
  - lib/stimulus_spec/capybara/matchers/have_stimulus_target.rb
51
53
  - lib/stimulus_spec/capybara/matchers/have_stimulus_value.rb
52
54
  - lib/stimulus_spec/configuration.rb