stimulus_spec 0.3.0 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ffe129c9b92bcc3dfe29e7dd6e9be508fd7c14b99603ca7524f964679fe42b6c
4
- data.tar.gz: a254bb73a1d5e275637e477a333e3bea6ab4248b7555e1d1d3f4bf74de9f213c
3
+ metadata.gz: ed3850b553fca15bb450d8ad6e0f50d37593c90bd440d26c6cbf0a8eb99882ea
4
+ data.tar.gz: a0b96c0888a87a93ef3db2e783d747642f09f863003c3f9a8fc43cc04ca4f392
5
5
  SHA512:
6
- metadata.gz: '038f5d80f4a0b38bc9e0664bb3d476db0af508580bc6476095d2791fe65cb7f515e06d1116fecdc8570f559aaf77331fac6c06e5fa2e0b41b7616b6eb652e069'
7
- data.tar.gz: cc7ed0ded4e4db2426532aeef9a2dcf2ab92aee3b27bf5af792921a70e3ccdfefc84cb679a7004e0231b385a1c6c3254178a5142eccc499dadc31b947711f73a
6
+ metadata.gz: 0ee41b07ee4796f9670ec0a805ff0b6f52179fddc5cec9169f8272d65bc1bf6c7b114fc173093a091e2f468b42c1a6e06d1d0d3bd6b7c837ceb2b47686c96388
7
+ data.tar.gz: fe153dd036e2c7ab6d27b9e22a3bdc88f30f9d16b73cae11d7fbd146bb7b5567faa4f0331e83bb3f1fb2fde8bba94d9674b45ce99011b3cf6e84d7ee02ad711c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-06-22
11
+
12
+ ### Added
13
+
14
+ - `have_stimulus_outlet(controller, outlet)` matcher — asserts `data-{controller}-{outlet}-outlet` exists
15
+ - `have_stimulus_outlet(controller, outlet, selector)` matcher — asserts attribute equals the CSS selector value
16
+ - Capybara matchers: `have_stimulus_controller`, `have_stimulus_action`, `have_stimulus_target` for system/feature specs
17
+ - Auto-include `StimulusSpec::Capybara::Matchers` into `type: :system` and `type: :feature` (gated on `capybara`)
18
+ - Capybara `have_stimulus_value` matcher (existence and equality modes)
19
+ - Enhanced failure messages: controller mismatch lists all found controllers, value/class/outlet mismatch shows actual vs expected with element HTML, all matchers include relevant HTML snippets
20
+
10
21
  ## [0.3.0] - 2026-06-22
11
22
 
12
23
  ### Added
@@ -28,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
28
39
  - `have_stimulus_action(descriptor)` matcher — full descriptor (`~=`) and shorthand without event (`*=`)
29
40
  - `have_stimulus_target(controller, target)` matcher — asserts `[data-{controller}-target~="target"]`
30
41
 
31
- [Unreleased]: https://github.com/eclectic-coding/stimulus_spec/compare/v0.3.0...HEAD
42
+ [Unreleased]: https://github.com/eclectic-coding/stimulus_spec/compare/v0.4.0...HEAD
43
+ [0.4.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.4.0
32
44
  [0.3.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.3.0
33
45
  [0.1.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.1.0
data/README.md CHANGED
@@ -8,7 +8,8 @@
8
8
 
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
- - **Request/controller specs** — `have_stimulus_controller`, `have_stimulus_action`, `have_stimulus_target`, `have_stimulus_value`, `have_stimulus_class`
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
13
  - **Auto-included** — zero setup required when `stimulus-rails` is in your bundle
13
14
  - **Configurable** — disable auto-include when you need manual control
14
15
 
@@ -24,6 +25,7 @@ Companion gem to [turbo_rspec](https://github.com/eclectic-coding/turbo_rspec)
24
25
  - [have\_stimulus\_target](#have_stimulus_target)
25
26
  - [have\_stimulus\_value](#have_stimulus_value)
26
27
  - [have\_stimulus\_class](#have_stimulus_class)
28
+ - [have\_stimulus\_outlet](#have_stimulus_outlet)
27
29
  - [Example](#example)
28
30
  - [Relationship to turbo\_rspec](#relationship-to-turbo_rspec)
29
31
  - [Contributing](#contributing)
@@ -45,7 +47,10 @@ end
45
47
 
46
48
  ### Rails + stimulus-rails (automatic)
47
49
 
48
- No setup needed. When `stimulus-rails` is in your bundle, `StimulusSpec::Matchers` is automatically included in `type: :request`, `:controller`, `:system`, and `:feature` example groups.
50
+ No setup needed. When `stimulus-rails` is in your bundle:
51
+
52
+ - `StimulusSpec::Matchers` is automatically included in `type: :request` and `type: :controller` example groups
53
+ - `StimulusSpec::Capybara::Matchers` is automatically included in `type: :system` and `type: :feature` example groups when `capybara` is also present
49
54
 
50
55
  ### Manual include
51
56
 
@@ -54,7 +59,8 @@ For non-Rails projects or custom contexts, include the matchers explicitly:
54
59
  ```ruby
55
60
  # spec/spec_helper.rb
56
61
  RSpec.configure do |config|
57
- config.include StimulusSpec::Matchers
62
+ config.include StimulusSpec::Matchers # request/controller specs
63
+ config.include StimulusSpec::Capybara::Matchers # system/feature specs
58
64
  end
59
65
  ```
60
66
 
@@ -144,6 +150,21 @@ expect(response).to have_stimulus_class("search", "loading", "opacity-50")
144
150
  expect(response).not_to have_stimulus_class("search", "loading")
145
151
  ```
146
152
 
153
+ ### `have_stimulus_outlet`
154
+
155
+ Assert that rendered HTML contains a `data-{controller}-{outlet}-outlet` attribute with a CSS selector.
156
+
157
+ ```ruby
158
+ # Assert the outlet attribute exists
159
+ expect(response).to have_stimulus_outlet("search", "results")
160
+
161
+ # Assert a specific selector
162
+ expect(response).to have_stimulus_outlet("search", "results", "#results-list")
163
+
164
+ # Negation
165
+ expect(response).not_to have_stimulus_outlet("search", "results")
166
+ ```
167
+
147
168
  [Back to top](#stimulusspec)
148
169
 
149
170
  ## Example
@@ -162,6 +183,20 @@ RSpec.describe "Search", type: :request do
162
183
  end
163
184
  ```
164
185
 
186
+ ### Example: system spec
187
+
188
+ ```ruby
189
+ RSpec.describe "Search", type: :system do
190
+ it "has the search controller wired up" do
191
+ visit search_path
192
+
193
+ expect(page).to have_stimulus_controller("search")
194
+ expect(page).to have_stimulus_action("input->search#query")
195
+ expect(page).to have_stimulus_target("search", "input")
196
+ end
197
+ end
198
+ ```
199
+
165
200
  [Back to top](#stimulusspec)
166
201
 
167
202
  ## Relationship to turbo_rspec
data/ROADMAP.md CHANGED
@@ -11,17 +11,6 @@ RSpec matchers for [Stimulus](https://github.com/hotwired/stimulus-rails): contr
11
11
 
12
12
  ---
13
13
 
14
-
15
- ## 0.4.0 — Capybara Values and Rich Failures
16
-
17
- - Capybara `have_stimulus_value` matcher
18
- - Enhanced failure messages across all matchers:
19
- - List all `data-controller` values found in the document on controller mismatch
20
- - Show actual vs expected value on value/class/outlet mismatch
21
- - Include relevant HTML snippet for context
22
-
23
- ---
24
-
25
14
  ## 0.5.0 — Documentation and Polish
26
15
 
27
16
  - Full YARD documentation on all public methods and classes
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Capybara
5
+ module Matchers
6
+ class HaveStimulusAction
7
+ def initialize(descriptor)
8
+ @descriptor = descriptor.to_s
9
+ end
10
+
11
+ def matches?(page)
12
+ @page = page
13
+ page.has_css?(css_selector, wait: 0)
14
+ end
15
+
16
+ def does_not_match?(page)
17
+ page.has_no_css?(css_selector, wait: 0)
18
+ end
19
+
20
+ def failure_message
21
+ "expected to find an element with data-action=\"#{@descriptor}\" on the page"
22
+ end
23
+
24
+ def failure_message_when_negated
25
+ "expected not to find an element with data-action=\"#{@descriptor}\" on the page"
26
+ end
27
+
28
+ def description
29
+ "have Stimulus action \"#{@descriptor}\""
30
+ end
31
+
32
+ private
33
+
34
+ def css_selector
35
+ if @descriptor.include?("->")
36
+ "[data-action~='#{@descriptor}']"
37
+ else
38
+ "[data-action*='#{@descriptor}']"
39
+ end
40
+ end
41
+ end
42
+
43
+ def have_stimulus_action(descriptor)
44
+ HaveStimulusAction.new(descriptor)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Capybara
5
+ module Matchers
6
+ class HaveStimulusController
7
+ def initialize(name)
8
+ @name = name.to_s
9
+ end
10
+
11
+ def matches?(page)
12
+ @page = page
13
+ page.has_css?("[data-controller~='#{@name}']", wait: 0)
14
+ end
15
+
16
+ def does_not_match?(page)
17
+ page.has_no_css?("[data-controller~='#{@name}']", wait: 0)
18
+ end
19
+
20
+ def failure_message
21
+ "expected to find an element with data-controller=\"#{@name}\" on the page"
22
+ end
23
+
24
+ def failure_message_when_negated
25
+ "expected not to find an element with data-controller=\"#{@name}\" on the page"
26
+ end
27
+
28
+ def description
29
+ "have Stimulus controller \"#{@name}\""
30
+ end
31
+ end
32
+
33
+ def have_stimulus_controller(name)
34
+ HaveStimulusController.new(name)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Capybara
5
+ module Matchers
6
+ class HaveStimulusTarget
7
+ def initialize(controller, target)
8
+ @controller = controller.to_s
9
+ @target = target.to_s
10
+ end
11
+
12
+ def matches?(page)
13
+ @page = page
14
+ page.has_css?("[data-#{@controller}-target~='#{@target}']", wait: 0)
15
+ end
16
+
17
+ def does_not_match?(page)
18
+ page.has_no_css?("[data-#{@controller}-target~='#{@target}']", wait: 0)
19
+ end
20
+
21
+ def failure_message
22
+ "expected to find an element with data-#{@controller}-target=\"#{@target}\" on the page"
23
+ end
24
+
25
+ def failure_message_when_negated
26
+ "expected not to find an element with data-#{@controller}-target=\"#{@target}\" on the page"
27
+ end
28
+
29
+ def description
30
+ "have Stimulus target \"#{@target}\" for controller \"#{@controller}\""
31
+ end
32
+ end
33
+
34
+ def have_stimulus_target(controller, target)
35
+ HaveStimulusTarget.new(controller, target)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Capybara
5
+ module Matchers
6
+ class HaveStimulusValue
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}-value"
12
+ end
13
+
14
+ def matches?(page)
15
+ @page = page
16
+ element = page.first("[#{@attr}]", minimum: 0, wait: 0)
17
+ return false unless element
18
+
19
+ if @expected
20
+ @actual = element[@attr]
21
+ @actual == @expected.to_s
22
+ else
23
+ true
24
+ end
25
+ end
26
+
27
+ def does_not_match?(page)
28
+ !matches?(page)
29
+ end
30
+
31
+ def failure_message
32
+ if @expected && @actual
33
+ "expected #{@attr} to be \"#{@expected}\" but was \"#{@actual}\""
34
+ else
35
+ "expected to find an element with #{@attr} on the page"
36
+ end
37
+ end
38
+
39
+ def failure_message_when_negated
40
+ "expected not to find an element with #{@attr} on the page"
41
+ end
42
+
43
+ def description
44
+ if @expected
45
+ "have Stimulus value \"#{@name}\" with \"#{@expected}\" for controller \"#{@controller}\""
46
+ else
47
+ "have Stimulus value \"#{@name}\" for controller \"#{@controller}\""
48
+ end
49
+ end
50
+ end
51
+
52
+ def have_stimulus_value(controller, name, expected = nil)
53
+ HaveStimulusValue.new(controller, name, expected)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "matchers/have_stimulus_controller"
4
+ require_relative "matchers/have_stimulus_action"
5
+ require_relative "matchers/have_stimulus_target"
6
+ require_relative "matchers/have_stimulus_value"
7
+
8
+ module StimulusSpec
9
+ module Capybara
10
+ module Matchers
11
+ end
12
+ end
13
+ end
@@ -9,10 +9,11 @@ module StimulusSpec
9
9
 
10
10
  def matches?(subject)
11
11
  @body = extract_body(subject)
12
+ @doc = Nokogiri::HTML5.fragment(@body)
12
13
  if @descriptor.include?("->")
13
- !document.at_css("[data-action~='#{@descriptor}']").nil?
14
+ !@doc.at_css("[data-action~='#{@descriptor}']").nil?
14
15
  else
15
- !document.at_css("[data-action*='#{@descriptor}']").nil?
16
+ !@doc.at_css("[data-action*='#{@descriptor}']").nil?
16
17
  end
17
18
  end
18
19
 
@@ -21,7 +22,11 @@ module StimulusSpec
21
22
  end
22
23
 
23
24
  def failure_message
24
- "expected to find an element with data-action=\"#{@descriptor}\" but found none in:\n#{@body}"
25
+ found_actions = @doc.css("[data-action]").flat_map { |el| el["data-action"].split }
26
+ msg = "expected to find an element with data-action=\"#{@descriptor}\""
27
+ msg += "\n found actions: #{found_actions.uniq.map { |a| "\"#{a}\"" }.join(", ")}" if found_actions.any?
28
+ msg += "\n in:\n#{snippet}"
29
+ msg
25
30
  end
26
31
 
27
32
  def failure_message_when_negated
@@ -38,8 +43,11 @@ module StimulusSpec
38
43
  subject.respond_to?(:body) ? subject.body : subject.to_s
39
44
  end
40
45
 
41
- def document
42
- Nokogiri::HTML5.fragment(@body)
46
+ def snippet
47
+ elements = @doc.css("[data-action]")
48
+ return @body if elements.empty?
49
+
50
+ elements.map(&:to_html).join("\n")
43
51
  end
44
52
  end
45
53
 
@@ -12,11 +12,12 @@ module StimulusSpec
12
12
 
13
13
  def matches?(subject)
14
14
  @body = extract_body(subject)
15
- element = document.at_css("[#{@attr}]")
16
- return false unless element
15
+ @doc = Nokogiri::HTML5.fragment(@body)
16
+ @element = @doc.at_css("[#{@attr}]")
17
+ return false unless @element
17
18
 
18
19
  if @expected
19
- @actual = element[@attr]
20
+ @actual = @element[@attr]
20
21
  @actual == @expected.to_s
21
22
  else
22
23
  true
@@ -29,9 +30,9 @@ module StimulusSpec
29
30
 
30
31
  def failure_message
31
32
  if @expected && @actual
32
- "expected #{@attr} to be \"#{@expected}\" but was \"#{@actual}\""
33
+ "expected #{@attr} to be \"#{@expected}\" but was \"#{@actual}\"\n on: #{@element.to_html}"
33
34
  else
34
- "expected to find an element with #{@attr} but found none in:\n#{@body}"
35
+ "expected to find an element with #{@attr} but found none in:\n#{snippet}"
35
36
  end
36
37
  end
37
38
 
@@ -53,8 +54,11 @@ module StimulusSpec
53
54
  subject.respond_to?(:body) ? subject.body : subject.to_s
54
55
  end
55
56
 
56
- def document
57
- Nokogiri::HTML5.fragment(@body)
57
+ def snippet
58
+ elements = @doc.css("[data-controller]")
59
+ return @body if elements.empty?
60
+
61
+ elements.map(&:to_html).join("\n")
58
62
  end
59
63
  end
60
64
 
@@ -9,7 +9,9 @@ module StimulusSpec
9
9
 
10
10
  def matches?(subject)
11
11
  @body = extract_body(subject)
12
- !document.at_css("[data-controller~='#{@name}']").nil?
12
+ @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?
13
15
  end
14
16
 
15
17
  def does_not_match?(subject)
@@ -17,7 +19,12 @@ module StimulusSpec
17
19
  end
18
20
 
19
21
  def failure_message
20
- "expected to find an element with data-controller=\"#{@name}\" but found none in:\n#{@body}"
22
+ msg = "expected to find an element with data-controller=\"#{@name}\""
23
+ if @found_controllers.any?
24
+ msg += "\n found controllers: #{@found_controllers.uniq.map { |c| "\"#{c}\"" }.join(", ")}"
25
+ end
26
+ msg += "\n in:\n#{snippet}"
27
+ msg
21
28
  end
22
29
 
23
30
  def failure_message_when_negated
@@ -34,8 +41,11 @@ module StimulusSpec
34
41
  subject.respond_to?(:body) ? subject.body : subject.to_s
35
42
  end
36
43
 
37
- def document
38
- Nokogiri::HTML5.fragment(@body)
44
+ def snippet
45
+ elements = @doc.css("[data-controller]")
46
+ return @body if elements.empty?
47
+
48
+ elements.map(&:to_html).join("\n")
39
49
  end
40
50
  end
41
51
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Matchers
5
+ class HaveStimulusOutlet
6
+ def initialize(controller, outlet, selector = nil)
7
+ @controller = controller.to_s
8
+ @outlet = outlet.to_s
9
+ @selector = selector
10
+ @attr = "data-#{@controller}-#{@outlet}-outlet"
11
+ end
12
+
13
+ def matches?(subject)
14
+ @body = extract_body(subject)
15
+ @doc = Nokogiri::HTML5.fragment(@body)
16
+ @element = @doc.at_css("[#{@attr}]")
17
+ return false unless @element
18
+
19
+ if @selector
20
+ @actual = @element[@attr]
21
+ @actual == @selector.to_s
22
+ else
23
+ true
24
+ end
25
+ end
26
+
27
+ def does_not_match?(subject)
28
+ !matches?(subject)
29
+ end
30
+
31
+ def failure_message
32
+ if @selector && @actual
33
+ "expected #{@attr} to be \"#{@selector}\" but was \"#{@actual}\"\n on: #{@element.to_html}"
34
+ else
35
+ "expected to find an element with #{@attr} but found none in:\n#{snippet}"
36
+ end
37
+ end
38
+
39
+ def failure_message_when_negated
40
+ "expected not to find an element with #{@attr} but found one"
41
+ end
42
+
43
+ def description
44
+ if @selector
45
+ "have Stimulus outlet \"#{@outlet}\" with \"#{@selector}\" for controller \"#{@controller}\""
46
+ else
47
+ "have Stimulus outlet \"#{@outlet}\" for controller \"#{@controller}\""
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def extract_body(subject)
54
+ subject.respond_to?(:body) ? subject.body : subject.to_s
55
+ end
56
+
57
+ def snippet
58
+ elements = @doc.css("[data-controller]")
59
+ return @body if elements.empty?
60
+
61
+ elements.map(&:to_html).join("\n")
62
+ end
63
+ end
64
+
65
+ def have_stimulus_outlet(controller, outlet, selector = nil)
66
+ HaveStimulusOutlet.new(controller, outlet, selector)
67
+ end
68
+ end
69
+ end
@@ -6,11 +6,13 @@ module StimulusSpec
6
6
  def initialize(controller, target)
7
7
  @controller = controller.to_s
8
8
  @target = target.to_s
9
+ @attr = "data-#{@controller}-target"
9
10
  end
10
11
 
11
12
  def matches?(subject)
12
13
  @body = extract_body(subject)
13
- !document.at_css("[data-#{@controller}-target~='#{@target}']").nil?
14
+ @doc = Nokogiri::HTML5.fragment(@body)
15
+ !@doc.at_css("[#{@attr}~='#{@target}']").nil?
14
16
  end
15
17
 
16
18
  def does_not_match?(subject)
@@ -18,11 +20,15 @@ module StimulusSpec
18
20
  end
19
21
 
20
22
  def failure_message
21
- "expected to find an element with data-#{@controller}-target=\"#{@target}\" but found none in:\n#{@body}"
23
+ found_targets = @doc.css("[#{@attr}]").flat_map { |el| el[@attr].split }
24
+ msg = "expected to find an element with #{@attr}=\"#{@target}\""
25
+ msg += "\n found targets: #{found_targets.uniq.map { |t| "\"#{t}\"" }.join(", ")}" if found_targets.any?
26
+ msg += "\n in:\n#{snippet}"
27
+ msg
22
28
  end
23
29
 
24
30
  def failure_message_when_negated
25
- "expected not to find an element with data-#{@controller}-target=\"#{@target}\" but found one"
31
+ "expected not to find an element with #{@attr}=\"#{@target}\" but found one"
26
32
  end
27
33
 
28
34
  def description
@@ -35,8 +41,11 @@ module StimulusSpec
35
41
  subject.respond_to?(:body) ? subject.body : subject.to_s
36
42
  end
37
43
 
38
- def document
39
- Nokogiri::HTML5.fragment(@body)
44
+ def snippet
45
+ elements = @doc.css("[#{@attr}]")
46
+ return @body if elements.empty?
47
+
48
+ elements.map(&:to_html).join("\n")
40
49
  end
41
50
  end
42
51
 
@@ -12,11 +12,12 @@ module StimulusSpec
12
12
 
13
13
  def matches?(subject)
14
14
  @body = extract_body(subject)
15
- element = document.at_css("[#{@attr}]")
16
- return false unless element
15
+ @doc = Nokogiri::HTML5.fragment(@body)
16
+ @element = @doc.at_css("[#{@attr}]")
17
+ return false unless @element
17
18
 
18
19
  if @expected
19
- @actual = element[@attr]
20
+ @actual = @element[@attr]
20
21
  @actual == @expected.to_s
21
22
  else
22
23
  true
@@ -29,9 +30,9 @@ module StimulusSpec
29
30
 
30
31
  def failure_message
31
32
  if @expected && @actual
32
- "expected #{@attr} to be \"#{@expected}\" but was \"#{@actual}\""
33
+ "expected #{@attr} to be \"#{@expected}\" but was \"#{@actual}\"\n on: #{@element.to_html}"
33
34
  else
34
- "expected to find an element with #{@attr} but found none in:\n#{@body}"
35
+ "expected to find an element with #{@attr} but found none in:\n#{snippet}"
35
36
  end
36
37
  end
37
38
 
@@ -53,8 +54,11 @@ module StimulusSpec
53
54
  subject.respond_to?(:body) ? subject.body : subject.to_s
54
55
  end
55
56
 
56
- def document
57
- Nokogiri::HTML5.fragment(@body)
57
+ def snippet
58
+ elements = @doc.css("[data-controller]")
59
+ return @body if elements.empty?
60
+
61
+ elements.map(&:to_html).join("\n")
58
62
  end
59
63
  end
60
64
 
@@ -7,6 +7,7 @@ require_relative "matchers/have_stimulus_action"
7
7
  require_relative "matchers/have_stimulus_target"
8
8
  require_relative "matchers/have_stimulus_value"
9
9
  require_relative "matchers/have_stimulus_class"
10
+ require_relative "matchers/have_stimulus_outlet"
10
11
 
11
12
  module StimulusSpec
12
13
  module Matchers
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusSpec
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/stimulus_spec.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "stimulus_spec/version"
4
4
  require_relative "stimulus_spec/configuration"
5
5
  require_relative "stimulus_spec/matchers"
6
+ require_relative "stimulus_spec/capybara/matchers"
6
7
 
7
8
  module StimulusSpec
8
9
  class Error < StandardError; end
@@ -23,9 +24,15 @@ module StimulusSpec
23
24
  return unless Gem.loaded_specs.key?("stimulus-rails")
24
25
  return unless configuration.auto_include
25
26
 
26
- %i[request controller system feature].each do |type|
27
+ %i[request controller].each do |type|
27
28
  config.include StimulusSpec::Matchers, type: type
28
29
  end
30
+
31
+ return unless Gem.loaded_specs.key?("capybara")
32
+
33
+ %i[system feature].each do |type|
34
+ config.include StimulusSpec::Capybara::Matchers, type: type
35
+ end
29
36
  end
30
37
  end
31
38
 
@@ -18,6 +18,7 @@ module StimulusSpec
18
18
  def have_stimulus_target: (String controller, String target) -> HaveStimulusTarget
19
19
  def have_stimulus_value: (String controller, String name, ?String? expected) -> HaveStimulusValue
20
20
  def have_stimulus_class: (String controller, String name, ?String? expected) -> HaveStimulusClass
21
+ def have_stimulus_outlet: (String controller, String outlet, ?String? selector) -> HaveStimulusOutlet
21
22
 
22
23
  class HaveStimulusController
23
24
  def initialize: (String name) -> void
@@ -63,5 +64,59 @@ module StimulusSpec
63
64
  def failure_message_when_negated: () -> String
64
65
  def description: () -> String
65
66
  end
67
+
68
+ class HaveStimulusOutlet
69
+ def initialize: (String controller, String outlet, ?String? selector) -> void
70
+ def matches?: (untyped subject) -> bool
71
+ def does_not_match?: (untyped subject) -> bool
72
+ def failure_message: () -> String
73
+ def failure_message_when_negated: () -> String
74
+ def description: () -> String
75
+ end
76
+ end
77
+
78
+ module Capybara
79
+ module Matchers
80
+ def have_stimulus_controller: (String name) -> HaveStimulusController
81
+ def have_stimulus_action: (String descriptor) -> HaveStimulusAction
82
+ def have_stimulus_target: (String controller, String target) -> HaveStimulusTarget
83
+ def have_stimulus_value: (String controller, String name, ?String? expected) -> HaveStimulusValue
84
+
85
+ class HaveStimulusController
86
+ def initialize: (String name) -> void
87
+ def matches?: (untyped page) -> bool
88
+ def does_not_match?: (untyped page) -> bool
89
+ def failure_message: () -> String
90
+ def failure_message_when_negated: () -> String
91
+ def description: () -> String
92
+ end
93
+
94
+ class HaveStimulusAction
95
+ def initialize: (String descriptor) -> void
96
+ def matches?: (untyped page) -> bool
97
+ def does_not_match?: (untyped page) -> bool
98
+ def failure_message: () -> String
99
+ def failure_message_when_negated: () -> String
100
+ def description: () -> String
101
+ end
102
+
103
+ class HaveStimulusTarget
104
+ def initialize: (String controller, String target) -> void
105
+ def matches?: (untyped page) -> bool
106
+ def does_not_match?: (untyped page) -> bool
107
+ def failure_message: () -> String
108
+ def failure_message_when_negated: () -> String
109
+ def description: () -> String
110
+ end
111
+
112
+ class HaveStimulusValue
113
+ def initialize: (String controller, String name, ?String? expected) -> void
114
+ def matches?: (untyped page) -> bool
115
+ def does_not_match?: (untyped page) -> bool
116
+ def failure_message: () -> String
117
+ def failure_message_when_negated: () -> String
118
+ def description: () -> String
119
+ end
120
+ end
66
121
  end
67
122
  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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -44,11 +44,17 @@ files:
44
44
  - Rakefile
45
45
  - codecov.yml
46
46
  - lib/stimulus_spec.rb
47
+ - lib/stimulus_spec/capybara/matchers.rb
48
+ - lib/stimulus_spec/capybara/matchers/have_stimulus_action.rb
49
+ - lib/stimulus_spec/capybara/matchers/have_stimulus_controller.rb
50
+ - lib/stimulus_spec/capybara/matchers/have_stimulus_target.rb
51
+ - lib/stimulus_spec/capybara/matchers/have_stimulus_value.rb
47
52
  - lib/stimulus_spec/configuration.rb
48
53
  - lib/stimulus_spec/matchers.rb
49
54
  - lib/stimulus_spec/matchers/have_stimulus_action.rb
50
55
  - lib/stimulus_spec/matchers/have_stimulus_class.rb
51
56
  - lib/stimulus_spec/matchers/have_stimulus_controller.rb
57
+ - lib/stimulus_spec/matchers/have_stimulus_outlet.rb
52
58
  - lib/stimulus_spec/matchers/have_stimulus_target.rb
53
59
  - lib/stimulus_spec/matchers/have_stimulus_value.rb
54
60
  - lib/stimulus_spec/version.rb