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 +4 -4
- data/CHANGELOG.md +23 -1
- data/README.md +70 -3
- data/ROADMAP.md +0 -28
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_action.rb +48 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_controller.rb +38 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_target.rb +39 -0
- data/lib/stimulus_spec/capybara/matchers/have_stimulus_value.rb +57 -0
- data/lib/stimulus_spec/capybara/matchers.rb +13 -0
- data/lib/stimulus_spec/matchers/have_stimulus_action.rb +13 -5
- data/lib/stimulus_spec/matchers/have_stimulus_class.rb +69 -0
- data/lib/stimulus_spec/matchers/have_stimulus_controller.rb +14 -4
- data/lib/stimulus_spec/matchers/have_stimulus_outlet.rb +69 -0
- data/lib/stimulus_spec/matchers/have_stimulus_target.rb +14 -5
- data/lib/stimulus_spec/matchers/have_stimulus_value.rb +69 -0
- data/lib/stimulus_spec/matchers.rb +3 -0
- data/lib/stimulus_spec/version.rb +1 -1
- data/lib/stimulus_spec.rb +8 -1
- data/sig/stimulus_spec.rbs +75 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed3850b553fca15bb450d8ad6e0f50d37593c90bd440d26c6cbf0a8eb99882ea
|
|
4
|
+
data.tar.gz: a0b96c0888a87a93ef3db2e783d747642f09f863003c3f9a8fc43cc04ca4f392
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
|
-
|
|
14
|
+
!@doc.at_css("[data-action~='#{@descriptor}']").nil?
|
|
14
15
|
else
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
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}\"
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
39
|
-
|
|
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
|
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
|
|
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
|
|
data/sig/stimulus_spec.rbs
CHANGED
|
@@ -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.
|
|
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
|