stimulus_spec 0.1.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: b3c093ad148d096d99e96b88f18c7f90814878425dc7c531c4aba4af4149cd81
4
- data.tar.gz: 3e938ad72982b139830f45815c1cc6d4c457f9e8f60af4d1bf362a7cbb641fb6
3
+ metadata.gz: ed3850b553fca15bb450d8ad6e0f50d37593c90bd440d26c6cbf0a8eb99882ea
4
+ data.tar.gz: a0b96c0888a87a93ef3db2e783d747642f09f863003c3f9a8fc43cc04ca4f392
5
5
  SHA512:
6
- metadata.gz: 50ff8b23e522757c5bc8387a27b00299db7c7f23b1134cbe6fbe204bebf005b825ba1e621546c681c47eaa6f7ec28e7418b17672e87fea7c01dcf5bea6425910
7
- data.tar.gz: cd08d477efb9bab97f956c943f30e88ebbfc5362d73fe9ede1af2d448e0125c00fe67a6b601361abf37396a996ccdb9fe2c550d0f346dc884f8caa647e9caa00
6
+ metadata.gz: 0ee41b07ee4796f9670ec0a805ff0b6f52179fddc5cec9169f8272d65bc1bf6c7b114fc173093a091e2f468b42c1a6e06d1d0d3bd6b7c837ceb2b47686c96388
7
+ data.tar.gz: fe153dd036e2c7ab6d27b9e22a3bdc88f30f9d16b73cae11d7fbd146bb7b5567faa4f0331e83bb3f1fb2fde8bba94d9674b45ce99011b3cf6e84d7ee02ad711c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ 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
+
21
+ ## [0.3.0] - 2026-06-22
22
+
23
+ ### Added
24
+
25
+ - `have_stimulus_value(controller, name)` matcher — asserts `data-{controller}-{name}-value` exists
26
+ - `have_stimulus_value(controller, name, expected)` matcher — asserts attribute equals expected value
27
+ - `have_stimulus_class(controller, name)` matcher — asserts `data-{controller}-{name}-class` exists
28
+ - `have_stimulus_class(controller, name, expected)` matcher — asserts attribute equals expected class
29
+
10
30
  ## [0.1.0] - 2026-06-22
11
31
 
12
32
  ### Added
@@ -19,5 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
39
  - `have_stimulus_action(descriptor)` matcher — full descriptor (`~=`) and shorthand without event (`*=`)
20
40
  - `have_stimulus_target(controller, target)` matcher — asserts `[data-{controller}-target~="target"]`
21
41
 
22
- [Unreleased]: https://github.com/eclectic-coding/stimulus_spec/compare/v0.1.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
44
+ [0.3.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.3.0
23
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`
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
 
@@ -22,6 +23,9 @@ Companion gem to [turbo_rspec](https://github.com/eclectic-coding/turbo_rspec)
22
23
  - [have\_stimulus\_controller](#have_stimulus_controller)
23
24
  - [have\_stimulus\_action](#have_stimulus_action)
24
25
  - [have\_stimulus\_target](#have_stimulus_target)
26
+ - [have\_stimulus\_value](#have_stimulus_value)
27
+ - [have\_stimulus\_class](#have_stimulus_class)
28
+ - [have\_stimulus\_outlet](#have_stimulus_outlet)
25
29
  - [Example](#example)
26
30
  - [Relationship to turbo\_rspec](#relationship-to-turbo_rspec)
27
31
  - [Contributing](#contributing)
@@ -43,7 +47,10 @@ end
43
47
 
44
48
  ### Rails + stimulus-rails (automatic)
45
49
 
46
- 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
47
54
 
48
55
  ### Manual include
49
56
 
@@ -52,7 +59,8 @@ For non-Rails projects or custom contexts, include the matchers explicitly:
52
59
  ```ruby
53
60
  # spec/spec_helper.rb
54
61
  RSpec.configure do |config|
55
- config.include StimulusSpec::Matchers
62
+ config.include StimulusSpec::Matchers # request/controller specs
63
+ config.include StimulusSpec::Capybara::Matchers # system/feature specs
56
64
  end
57
65
  ```
58
66
 
@@ -112,6 +120,51 @@ expect(response).to have_stimulus_target("hello", "output")
112
120
  expect(response).not_to have_stimulus_target("hello", "missing")
113
121
  ```
114
122
 
123
+ ### `have_stimulus_value`
124
+
125
+ Assert that rendered HTML contains a `data-{controller}-{name}-value` attribute, optionally with a specific value.
126
+
127
+ ```ruby
128
+ # Assert the value attribute exists
129
+ expect(response).to have_stimulus_value("search", "url")
130
+
131
+ # Assert a specific value
132
+ expect(response).to have_stimulus_value("search", "url", "/results")
133
+
134
+ # Negation
135
+ expect(response).not_to have_stimulus_value("search", "url")
136
+ ```
137
+
138
+ ### `have_stimulus_class`
139
+
140
+ Assert that rendered HTML contains a `data-{controller}-{name}-class` attribute, optionally with a specific class.
141
+
142
+ ```ruby
143
+ # Assert the class attribute exists
144
+ expect(response).to have_stimulus_class("search", "loading")
145
+
146
+ # Assert a specific class value
147
+ expect(response).to have_stimulus_class("search", "loading", "opacity-50")
148
+
149
+ # Negation
150
+ expect(response).not_to have_stimulus_class("search", "loading")
151
+ ```
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
+
115
168
  [Back to top](#stimulusspec)
116
169
 
117
170
  ## Example
@@ -130,6 +183,20 @@ RSpec.describe "Search", type: :request do
130
183
  end
131
184
  ```
132
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
+
133
200
  [Back to top](#stimulusspec)
134
201
 
135
202
  ## Relationship to turbo_rspec
data/ROADMAP.md CHANGED
@@ -11,34 +11,6 @@ RSpec matchers for [Stimulus](https://github.com/hotwired/stimulus-rails): contr
11
11
 
12
12
  ---
13
13
 
14
- ## 0.2.0 — Values and Classes
15
-
16
- - `have_stimulus_value("search", "url")` — assert `data-search-url-value` attribute exists
17
- - `have_stimulus_value("search", "url", "/results")` — assert attribute equals expected value
18
- - `have_stimulus_class("search", "loading")` — assert `data-search-loading-class` exists
19
- - `have_stimulus_class("search", "loading", "opacity-50")` — assert attribute equals expected class
20
-
21
- ---
22
-
23
- ## 0.3.0 — Outlets and Capybara Foundation
24
-
25
- - `have_stimulus_outlet("search", "results")` — assert `data-search-results-outlet` exists
26
- - `have_stimulus_outlet("search", "results", "#results-list")` — assert selector value
27
- - Capybara matchers: `have_stimulus_controller`, `have_stimulus_action`, `have_stimulus_target` using `has_css?` / `has_no_css?` with `wait: 0`
28
- - Auto-include `StimulusSpec::Capybara::Matchers` into `type: :system` and `type: :feature` (gated on `capybara`)
29
-
30
- ---
31
-
32
- ## 0.4.0 — Capybara Values and Rich Failures
33
-
34
- - Capybara `have_stimulus_value` matcher
35
- - Enhanced failure messages across all matchers:
36
- - List all `data-controller` values found in the document on controller mismatch
37
- - Show actual vs expected value on value/class/outlet mismatch
38
- - Include relevant HTML snippet for context
39
-
40
- ---
41
-
42
14
  ## 0.5.0 — Documentation and Polish
43
15
 
44
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
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Matchers
5
+ class HaveStimulusClass
6
+ def initialize(controller, name, expected = nil)
7
+ @controller = controller.to_s
8
+ @name = name.to_s
9
+ @expected = expected
10
+ @attr = "data-#{@controller}-#{@name}-class"
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 @expected
20
+ @actual = @element[@attr]
21
+ @actual == @expected.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 @expected && @actual
33
+ "expected #{@attr} to be \"#{@expected}\" 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 @expected
45
+ "have Stimulus class \"#{@name}\" with \"#{@expected}\" for controller \"#{@controller}\""
46
+ else
47
+ "have Stimulus class \"#{@name}\" 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_class(controller, name, expected = nil)
66
+ HaveStimulusClass.new(controller, name, expected)
67
+ end
68
+ end
69
+ end
@@ -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
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusSpec
4
+ module Matchers
5
+ class HaveStimulusValue
6
+ def initialize(controller, name, expected = nil)
7
+ @controller = controller.to_s
8
+ @name = name.to_s
9
+ @expected = expected
10
+ @attr = "data-#{@controller}-#{@name}-value"
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 @expected
20
+ @actual = @element[@attr]
21
+ @actual == @expected.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 @expected && @actual
33
+ "expected #{@attr} to be \"#{@expected}\" 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 @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
+
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_value(controller, name, expected = nil)
66
+ HaveStimulusValue.new(controller, name, expected)
67
+ end
68
+ end
69
+ end
@@ -5,6 +5,9 @@ require "nokogiri"
5
5
  require_relative "matchers/have_stimulus_controller"
6
6
  require_relative "matchers/have_stimulus_action"
7
7
  require_relative "matchers/have_stimulus_target"
8
+ require_relative "matchers/have_stimulus_value"
9
+ require_relative "matchers/have_stimulus_class"
10
+ require_relative "matchers/have_stimulus_outlet"
8
11
 
9
12
  module StimulusSpec
10
13
  module Matchers
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusSpec
4
- VERSION = "0.1.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
 
@@ -16,6 +16,9 @@ module StimulusSpec
16
16
  def have_stimulus_controller: (String name) -> HaveStimulusController
17
17
  def have_stimulus_action: (String descriptor) -> HaveStimulusAction
18
18
  def have_stimulus_target: (String controller, String target) -> HaveStimulusTarget
19
+ def have_stimulus_value: (String controller, String name, ?String? expected) -> HaveStimulusValue
20
+ def have_stimulus_class: (String controller, String name, ?String? expected) -> HaveStimulusClass
21
+ def have_stimulus_outlet: (String controller, String outlet, ?String? selector) -> HaveStimulusOutlet
19
22
 
20
23
  class HaveStimulusController
21
24
  def initialize: (String name) -> void
@@ -43,5 +46,77 @@ module StimulusSpec
43
46
  def failure_message_when_negated: () -> String
44
47
  def description: () -> String
45
48
  end
49
+
50
+ class HaveStimulusValue
51
+ def initialize: (String controller, String name, ?String? expected) -> void
52
+ def matches?: (untyped subject) -> bool
53
+ def does_not_match?: (untyped subject) -> bool
54
+ def failure_message: () -> String
55
+ def failure_message_when_negated: () -> String
56
+ def description: () -> String
57
+ end
58
+
59
+ class HaveStimulusClass
60
+ def initialize: (String controller, String name, ?String? expected) -> void
61
+ def matches?: (untyped subject) -> bool
62
+ def does_not_match?: (untyped subject) -> bool
63
+ def failure_message: () -> String
64
+ def failure_message_when_negated: () -> String
65
+ def description: () -> String
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
46
121
  end
47
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.1.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
@@ -44,11 +44,19 @@ 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
55
+ - lib/stimulus_spec/matchers/have_stimulus_class.rb
50
56
  - lib/stimulus_spec/matchers/have_stimulus_controller.rb
57
+ - lib/stimulus_spec/matchers/have_stimulus_outlet.rb
51
58
  - lib/stimulus_spec/matchers/have_stimulus_target.rb
59
+ - lib/stimulus_spec/matchers/have_stimulus_value.rb
52
60
  - lib/stimulus_spec/version.rb
53
61
  - sig/stimulus_spec.rbs
54
62
  homepage: https://github.com/eclectic-coding/stimulus_spec