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 +4 -4
- data/.yardopts +8 -0
- data/CHANGELOG.md +10 -1
- data/README.md +7 -0
- data/ROADMAP.md +0 -19
- data/docs/cookbook.md +152 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_action.rb +11 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_class.rb +15 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_controller.rb +13 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_outlet.rb +15 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_target.rb +13 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_value.rb +15 -0
- data/lib/stimulus_spec/capybara/matchers.rb +1 -0
- data/lib/stimulus_spec/configuration.rb +2 -0
- data/lib/stimulus_spec/matchers/have_stimulus_action.rb +13 -0
- data/lib/stimulus_spec/matchers/have_stimulus_class.rb +17 -0
- data/lib/stimulus_spec/matchers/have_stimulus_controller.rb +18 -0
- data/lib/stimulus_spec/matchers/have_stimulus_outlet.rb +17 -0
- data/lib/stimulus_spec/matchers/have_stimulus_target.rb +13 -0
- data/lib/stimulus_spec/matchers/have_stimulus_value.rb +17 -0
- data/lib/stimulus_spec/version.rb +1 -1
- data/lib/stimulus_spec.rb +25 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31b3ef6e3b0ef09e3f6353d975ee2a65dd7c42dbc4d3be8aa78c7508b88d9ba7
|
|
4
|
+
data.tar.gz: 5f79191a5baccfabe6a8502fa8708248dc71efe1ba63d87f816a2c882ad5e941
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b789aa21b411f503223b5c6116507f48168cfd675ea198c4c3a8cf68490754ebcaf0bcdea718c4cf492400cf3df6eba5d0079c41b2e2ce48020165b6b4bb348
|
|
7
|
+
data.tar.gz: 97623a443042991da1c942449406f0e92c0b8918d8a9bdbd8b001962c0c238db103d0d2cc9d27a3aff05bd75256d65b593686876fd731f883f8a5c0547af9d0d
|
data/.yardopts
ADDED
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.
|
|
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
|
|
@@ -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
|
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.
|
|
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
|