stimulus_spec 0.6.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: 016bc15660c32251a12c1c9129fa88c86a3e2ac1059949535a67cdd5d64448ce
4
- data.tar.gz: 76277d4b548369259e6ae9f28b8ad2b4d22637659f7c13a938e5cf2fd95127e5
3
+ metadata.gz: 31b3ef6e3b0ef09e3f6353d975ee2a65dd7c42dbc4d3be8aa78c7508b88d9ba7
4
+ data.tar.gz: 5f79191a5baccfabe6a8502fa8708248dc71efe1ba63d87f816a2c882ad5e941
5
5
  SHA512:
6
- metadata.gz: 8502635e782f3c870ee6aa638e09cba1bbc054c731d1c4599e5b23a31551bd0fb5dce9fc71eaa3d2ca8d3713b2ab8ba26591749a59b9bcd51bb09d871d11a2cc
7
- data.tar.gz: 300a7bc9351bb8d0dbcc3f70122094d03705fd876322f36187c587daa0bf44446477a57df6e2130252d3501577b1e410bd1510836e70c97729b430b7dd5a6f33
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,14 @@ 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
+
10
18
  ## [0.6.0] - 2026-06-23
11
19
 
12
20
  ### Added
@@ -53,7 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
53
61
  - `have_stimulus_action(descriptor)` matcher — full descriptor (`~=`) and shorthand without event (`*=`)
54
62
  - `have_stimulus_target(controller, target)` matcher — asserts `[data-{controller}-target~="target"]`
55
63
 
56
- [Unreleased]: https://github.com/eclectic-coding/stimulus_spec/compare/v0.6.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
57
66
  [0.6.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.6.0
58
67
  [0.5.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.5.0
59
68
  [0.4.0]: https://github.com/eclectic-coding/stimulus_spec/releases/tag/v0.4.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)
data/ROADMAP.md CHANGED
@@ -1,24 +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.7.0 — Documentation and Polish
15
-
16
- - Full YARD documentation on all public methods and classes
17
- - `docs/cookbook.md` — common patterns: request specs, system specs, multi-controller elements, typed values, outlets
18
- - Graceful no-op when `stimulus-rails` is not bundled (no `LoadError`)
19
-
20
- ---
21
-
22
3
  ## 1.0.0 — Stable API
23
4
 
24
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,16 +3,25 @@
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]
11
18
  def within(selector)
12
19
  @scope = selector
13
20
  self
14
21
  end
15
22
 
23
+ # @param page [Capybara::Session, Capybara::Node::Simple]
24
+ # @return [Boolean]
16
25
  def matches?(page)
17
26
  @page = page
18
27
  page.has_css?(scoped_selector, wait: 0)
@@ -46,6 +55,8 @@ module StimulusSpec
46
55
  end
47
56
  end
48
57
 
58
+ # @param descriptor [String] action descriptor
59
+ # @return [HaveStimulusAction]
49
60
  def have_stimulus_action(descriptor)
50
61
  HaveStimulusAction.new(descriptor)
51
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,11 +18,15 @@ 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]
14
23
  def within(selector)
15
24
  @scope = selector
16
25
  self
17
26
  end
18
27
 
28
+ # @param page [Capybara::Session, Capybara::Node::Simple]
29
+ # @return [Boolean]
19
30
  def matches?(page)
20
31
  @page = page
21
32
  element = page.first(scoped_selector, minimum: 0, wait: 0)
@@ -61,6 +72,10 @@ module StimulusSpec
61
72
  end
62
73
  end
63
74
 
75
+ # @param controller [String] controller name
76
+ # @param name [String] class name
77
+ # @param expected [String, nil] expected class value
78
+ # @return [HaveStimulusClass]
64
79
  def have_stimulus_class(controller, name, expected = nil)
65
80
  HaveStimulusClass.new(controller, name, expected)
66
81
  end
@@ -3,16 +3,27 @@
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
13
+ # @param names [Array<String>] one or more controller names
7
14
  def initialize(*names)
8
15
  @names = names.map(&:to_s)
9
16
  end
10
17
 
18
+ # @param selector [String] CSS selector for the scope element
19
+ # @return [self]
11
20
  def within(selector)
12
21
  @scope = selector
13
22
  self
14
23
  end
15
24
 
25
+ # @param page [Capybara::Session, Capybara::Node::Simple] Capybara page object
26
+ # @return [Boolean]
16
27
  def matches?(page)
17
28
  @page = page
18
29
  page.has_css?(scoped_selector, wait: 0)
@@ -46,6 +57,8 @@ module StimulusSpec
46
57
  end
47
58
  end
48
59
 
60
+ # @param names [Array<String>] one or more controller names
61
+ # @return [HaveStimulusController]
49
62
  def have_stimulus_controller(*names)
50
63
  HaveStimulusController.new(*names)
51
64
  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,11 +18,15 @@ 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]
14
23
  def within(selector)
15
24
  @scope = selector
16
25
  self
17
26
  end
18
27
 
28
+ # @param page [Capybara::Session, Capybara::Node::Simple]
29
+ # @return [Boolean]
19
30
  def matches?(page)
20
31
  @page = page
21
32
  element = page.first(scoped_selector, minimum: 0, wait: 0)
@@ -61,6 +72,10 @@ module StimulusSpec
61
72
  end
62
73
  end
63
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]
64
79
  def have_stimulus_outlet(controller, outlet, selector = nil)
65
80
  HaveStimulusOutlet.new(controller, outlet, selector)
66
81
  end
@@ -3,17 +3,27 @@
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]
12
20
  def within(selector)
13
21
  @scope = selector
14
22
  self
15
23
  end
16
24
 
25
+ # @param page [Capybara::Session, Capybara::Node::Simple]
26
+ # @return [Boolean]
17
27
  def matches?(page)
18
28
  @page = page
19
29
  page.has_css?(scoped_selector, wait: 0)
@@ -43,6 +53,9 @@ module StimulusSpec
43
53
  end
44
54
  end
45
55
 
56
+ # @param controller [String] controller name
57
+ # @param target [String] target name
58
+ # @return [HaveStimulusTarget]
46
59
  def have_stimulus_target(controller, target)
47
60
  HaveStimulusTarget.new(controller, target)
48
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,11 +18,15 @@ 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]
14
23
  def within(selector)
15
24
  @scope = selector
16
25
  self
17
26
  end
18
27
 
28
+ # @param page [Capybara::Session, Capybara::Node::Simple]
29
+ # @return [Boolean]
19
30
  def matches?(page)
20
31
  @page = page
21
32
  element = page.first(scoped_selector, minimum: 0, wait: 0)
@@ -61,6 +72,10 @@ module StimulusSpec
61
72
  end
62
73
  end
63
74
 
75
+ # @param controller [String] controller name
76
+ # @param name [String] value name
77
+ # @param expected [String, nil] expected value
78
+ # @return [HaveStimulusValue]
64
79
  def have_stimulus_value(controller, name, expected = nil)
65
80
  HaveStimulusValue.new(controller, name, expected)
66
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,16 +2,27 @@
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]
10
19
  def within(selector)
11
20
  @scope = selector
12
21
  self
13
22
  end
14
23
 
24
+ # @param subject [#body, String] response object or HTML string
25
+ # @return [Boolean]
15
26
  def matches?(subject)
16
27
  @body = extract_body(subject)
17
28
  @doc = Nokogiri::HTML5.fragment(@body)
@@ -66,6 +77,8 @@ module StimulusSpec
66
77
  end
67
78
  end
68
79
 
80
+ # @param descriptor [String] action descriptor
81
+ # @return [HaveStimulusAction]
69
82
  def have_stimulus_action(descriptor)
70
83
  HaveStimulusAction.new(descriptor)
71
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,11 +19,15 @@ 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]
13
24
  def within(selector)
14
25
  @scope = selector
15
26
  self
16
27
  end
17
28
 
29
+ # @param subject [#body, String] response object or HTML string
30
+ # @return [Boolean]
18
31
  def matches?(subject)
19
32
  @body = extract_body(subject)
20
33
  @doc = Nokogiri::HTML5.fragment(@body)
@@ -72,6 +85,10 @@ module StimulusSpec
72
85
  end
73
86
  end
74
87
 
88
+ # @param controller [String] controller name
89
+ # @param name [String] class name
90
+ # @param expected [String, nil] expected class value
91
+ # @return [HaveStimulusClass]
75
92
  def have_stimulus_class(controller, name, expected = nil)
76
93
  HaveStimulusClass.new(controller, name, expected)
77
94
  end
@@ -1,17 +1,33 @@
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
15
+ # @param names [Array<String>] one or more controller names to match
6
16
  def initialize(*names)
7
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]
10
24
  def within(selector)
11
25
  @scope = selector
12
26
  self
13
27
  end
14
28
 
29
+ # @param subject [#body, String] response object or HTML string
30
+ # @return [Boolean]
15
31
  def matches?(subject)
16
32
  @body = extract_body(subject)
17
33
  @doc = Nokogiri::HTML5.fragment(@body)
@@ -68,6 +84,8 @@ module StimulusSpec
68
84
  end
69
85
  end
70
86
 
87
+ # @param names [Array<String>] one or more controller names
88
+ # @return [HaveStimulusController]
71
89
  def have_stimulus_controller(*names)
72
90
  HaveStimulusController.new(*names)
73
91
  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,11 +19,15 @@ 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]
13
24
  def within(selector)
14
25
  @scope = selector
15
26
  self
16
27
  end
17
28
 
29
+ # @param subject [#body, String] response object or HTML string
30
+ # @return [Boolean]
18
31
  def matches?(subject)
19
32
  @body = extract_body(subject)
20
33
  @doc = Nokogiri::HTML5.fragment(@body)
@@ -72,6 +85,10 @@ module StimulusSpec
72
85
  end
73
86
  end
74
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]
75
92
  def have_stimulus_outlet(controller, outlet, selector = nil)
76
93
  HaveStimulusOutlet.new(controller, outlet, selector)
77
94
  end
@@ -2,18 +2,28 @@
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]
12
20
  def within(selector)
13
21
  @scope = selector
14
22
  self
15
23
  end
16
24
 
25
+ # @param subject [#body, String] response object or HTML string
26
+ # @return [Boolean]
17
27
  def matches?(subject)
18
28
  @body = extract_body(subject)
19
29
  @doc = Nokogiri::HTML5.fragment(@body)
@@ -64,6 +74,9 @@ module StimulusSpec
64
74
  end
65
75
  end
66
76
 
77
+ # @param controller [String] controller name
78
+ # @param target [String] target name
79
+ # @return [HaveStimulusTarget]
67
80
  def have_stimulus_target(controller, target)
68
81
  HaveStimulusTarget.new(controller, target)
69
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,11 +19,15 @@ 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]
13
24
  def within(selector)
14
25
  @scope = selector
15
26
  self
16
27
  end
17
28
 
29
+ # @param subject [#body, String] response object or HTML string
30
+ # @return [Boolean]
18
31
  def matches?(subject)
19
32
  @body = extract_body(subject)
20
33
  @doc = Nokogiri::HTML5.fragment(@body)
@@ -72,6 +85,10 @@ module StimulusSpec
72
85
  end
73
86
  end
74
87
 
88
+ # @param controller [String] controller name
89
+ # @param name [String] value name
90
+ # @param expected [String, nil] expected value
91
+ # @return [HaveStimulusValue]
75
92
  def have_stimulus_value(controller, name, expected = nil)
76
93
  HaveStimulusValue.new(controller, name, expected)
77
94
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusSpec
4
- VERSION = "0.6.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
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.6.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