capybara-dommy 0.10.0 → 0.12.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 +14 -0
- data/README.md +12 -3
- data/lib/capybara/dommy/driver.rb +46 -3
- data/lib/capybara/dommy/modal.rb +114 -0
- data/lib/capybara/dommy/version.rb +1 -1
- data/lib/capybara/dommy.rb +1 -0
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09b82196b4134a59d541a83732013edf23a017a47776dd35b326ebc573747789'
|
|
4
|
+
data.tar.gz: 282eb35b9f51404a80602537ad3fe6f29a7af768da2b30210c483fc8a841f86b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f9e7b3ca7e5bb3bde8c5c465bbdf783ba7497c4a40f032927fb6d34a1416c53cb73e321ee5902e1a9c215028e0b1e7665dbe7dc3ad92842a64fa001a3844d0a
|
|
7
|
+
data.tar.gz: 9aed884b51865fd48c233f9d7c4c8b63b50798f1cf18850701048d42671c8b0e5c071657850db0e49f9487b2b519b50ab8b74ae3f58cc58913646f53b6171cee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 — 2026-09-22
|
|
4
|
+
|
|
5
|
+
Versioned in lockstep with [`dommy`](https://github.com/takahashim/dommy) 0.12.0.
|
|
6
|
+
No functional changes to capybara-dommy itself.
|
|
7
|
+
|
|
8
|
+
## 0.11.0 — 2026-09-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Native dialog helpers in JS mode.** `accept_confirm`, `dismiss_confirm`, `accept_alert`, `accept_prompt` (with `with:`) and `dismiss_prompt` work against the page's real `confirm` / `alert` / `prompt`, including a `text:` string or Regexp to say which dialog is expected. Nested helper blocks answer the dialogs in the order the page opens them, and a dialog nobody is waiting for falls back to the headless default, so a stray `confirm` cannot silently accept. A block that opens no matching dialog raises `Capybara::ModalNotFound`, naming what turned up instead.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- Turbo-driven navigation in JS apps is followed, so `have_current_path` and the matchers after a Turbo visit see the page the app actually moved to.
|
|
15
|
+
- `current_url` advances the virtual clock like the other queries do, so a `have_current_path` poll converges when the navigation settles in a scheduled task rather than a microtask.
|
|
16
|
+
|
|
3
17
|
## 0.10.0 — 2026-07-13
|
|
4
18
|
|
|
5
19
|
### Added
|
data/README.md
CHANGED
|
@@ -19,15 +19,24 @@ speed and simplicity of a Rack-style driver.
|
|
|
19
19
|
- Preserves session state such as cookies, follows redirects by default, and
|
|
20
20
|
supports browser-like back, forward, and refresh navigation.
|
|
21
21
|
- Implements HTML-level visibility through `dommy-rack`.
|
|
22
|
+
- In JavaScript-enabled mode, supports Capybara's `accept_alert`,
|
|
23
|
+
`accept_confirm`, `dismiss_confirm`, `accept_prompt`, and `dismiss_prompt`
|
|
24
|
+
helpers with deterministic responses.
|
|
22
25
|
- Provides a Rails convenience require for `driven_by :dommy`.
|
|
23
26
|
|
|
24
27
|
## Limitations
|
|
25
28
|
|
|
26
29
|
`capybara-dommy` is intentionally not a browser automation driver.
|
|
27
30
|
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
- The default driver does not execute JavaScript. Use the JavaScript-enabled
|
|
32
|
+
variant for embedded QuickJS execution.
|
|
33
|
+
- Screenshots and browser windows are not supported. Native alerts, confirms,
|
|
34
|
+
and prompts are supported by the JavaScript-enabled variant through
|
|
35
|
+
Capybara's modal helpers.
|
|
36
|
+
- Constructable stylesheets can be built (`new CSSStyleSheet()`), but
|
|
37
|
+
`adoptedStyleSheets` is not implemented — assigning one applies no style.
|
|
38
|
+
Component libraries that feature-detect (`'adoptedStyleSheets' in
|
|
39
|
+
Document.prototype`) fall back to injecting a `<style>`, which is handled.
|
|
31
40
|
- CSS layout is not calculated. Visibility is based on HTML-level rules such as
|
|
32
41
|
`hidden`, `type="hidden"`, and inline `display: none` handling provided by
|
|
33
42
|
`dommy-rack`.
|
|
@@ -4,9 +4,10 @@ module Capybara
|
|
|
4
4
|
module Dommy
|
|
5
5
|
# A Capybara driver backed by Dommy::Rack::Session. Implements the
|
|
6
6
|
# navigation / query / reset! parts of the Capybara::Driver::Base contract;
|
|
7
|
-
# element interaction lives in Capybara::Dommy::Node.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# element interaction lives in Capybara::Dommy::Node. The JS-enabled mode
|
|
8
|
+
# additionally supplies deterministic native dialog responses for Capybara's
|
|
9
|
+
# alert/confirm/prompt helpers. Screenshot and window methods remain with
|
|
10
|
+
# Driver::Base (which raises Capybara::NotSupportedByDriverError).
|
|
10
11
|
class Driver < Capybara::Driver::Base
|
|
11
12
|
VISIBILITY_MODES = %i[all html none].freeze
|
|
12
13
|
|
|
@@ -94,6 +95,12 @@ module Capybara
|
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
def current_url
|
|
98
|
+
# Capybara polls current_url for have_current_path. In JS mode that poll
|
|
99
|
+
# must advance the virtual clock too: Turbo/fetch continuations often
|
|
100
|
+
# settle in a scheduled task rather than the interaction's microtask
|
|
101
|
+
# drain. The Rack session's History hook then reflects pushState in its
|
|
102
|
+
# current URL.
|
|
103
|
+
pump!
|
|
97
104
|
rack_session.current_url.to_s
|
|
98
105
|
end
|
|
99
106
|
|
|
@@ -240,6 +247,22 @@ module Capybara
|
|
|
240
247
|
unsupported_js!("evaluate_async_script")
|
|
241
248
|
end
|
|
242
249
|
|
|
250
|
+
# --- Native dialogs ---
|
|
251
|
+
#
|
|
252
|
+
# A native dialog is synchronous in Dommy, so installing the expected
|
|
253
|
+
# answer before the triggering block runs is sufficient. What is expected,
|
|
254
|
+
# what it answers and what it saw instead live in ModalExpectation; the
|
|
255
|
+
# stack they sit on, which is the session's dialog handler while a helper
|
|
256
|
+
# block is running, is ModalStack.
|
|
257
|
+
|
|
258
|
+
def accept_modal(type, **options, &block)
|
|
259
|
+
respond_to_modal(type, accept: true, **options, &block)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def dismiss_modal(type, **options, &block)
|
|
263
|
+
respond_to_modal(type, accept: false, **options, &block)
|
|
264
|
+
end
|
|
265
|
+
|
|
243
266
|
# Visibility decision used by Node#visible?. :all / :none treat every
|
|
244
267
|
# element as visible; :html defers to dommy-rack's HTML-level check.
|
|
245
268
|
def visible?(element)
|
|
@@ -323,6 +346,26 @@ module Capybara
|
|
|
323
346
|
raise Capybara::NotSupportedByDriverError,
|
|
324
347
|
"capybara-dommy does not support JavaScript (#{name})"
|
|
325
348
|
end
|
|
349
|
+
|
|
350
|
+
def respond_to_modal(type, accept:, text: nil, with: nil, **_options)
|
|
351
|
+
expectation = ModalExpectation.new(type: type, accept: accept, text: text, with: with)
|
|
352
|
+
modals.push(expectation)
|
|
353
|
+
rack_session.dialog_handler = modals
|
|
354
|
+
yield if block_given?
|
|
355
|
+
|
|
356
|
+
raise Capybara::ModalNotFound, expectation.not_found_message unless expectation.answered?
|
|
357
|
+
|
|
358
|
+
expectation.message
|
|
359
|
+
ensure
|
|
360
|
+
if expectation
|
|
361
|
+
modals.delete(expectation)
|
|
362
|
+
rack_session.dialog_handler = nil if modals.empty? && @rack_session
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def modals
|
|
367
|
+
@modals ||= ModalStack.new
|
|
368
|
+
end
|
|
326
369
|
end
|
|
327
370
|
end
|
|
328
371
|
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Capybara
|
|
4
|
+
module Dommy
|
|
5
|
+
# What one `accept_modal` / `dismiss_modal` block is waiting for, and what
|
|
6
|
+
# it saw. A native dialog is synchronous in Dommy, so the expectation is
|
|
7
|
+
# installed before the triggering block runs and answers the dialog the
|
|
8
|
+
# block opens.
|
|
9
|
+
class ModalExpectation
|
|
10
|
+
attr_reader :type, :text
|
|
11
|
+
# The message of the dialog this expectation answered — nil while it is
|
|
12
|
+
# still waiting, which is what makes it "not found" at the end of the
|
|
13
|
+
# block.
|
|
14
|
+
attr_reader :message
|
|
15
|
+
|
|
16
|
+
def initialize(type:, accept:, text: nil, with: nil)
|
|
17
|
+
@type = type.to_sym
|
|
18
|
+
@accept = accept
|
|
19
|
+
@text = text
|
|
20
|
+
@with = with
|
|
21
|
+
@message = nil
|
|
22
|
+
@actual = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def matches?(type, message)
|
|
26
|
+
@type == type && text_matches?(message)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def answered? = !@message.nil?
|
|
30
|
+
|
|
31
|
+
# Record that this dialog is the one, and answer it: a confirm with the
|
|
32
|
+
# accept/dismiss decision, a prompt with the text to type (`with:`, else
|
|
33
|
+
# what the page offered) or nil when dismissed. An alert has no answer.
|
|
34
|
+
def answer(message, default_value)
|
|
35
|
+
@message = message
|
|
36
|
+
case @type
|
|
37
|
+
when :confirm then @accept
|
|
38
|
+
when :prompt then @accept ? (@with || default_value) : nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# A dialog this expectation did not want. Remembered for the error at the
|
|
43
|
+
# end of the block, which says what turned up instead.
|
|
44
|
+
def saw(type, message)
|
|
45
|
+
@actual = {type: type, message: message}
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def not_found_message
|
|
50
|
+
if @actual
|
|
51
|
+
"Unable to find #{@type} dialog with #{@text.inspect} - found " \
|
|
52
|
+
"#{@actual[:type]} dialog with #{@actual[:message].inspect} instead."
|
|
53
|
+
else
|
|
54
|
+
"Unable to find #{@type} dialog#{@text ? " with #{@text.inspect}" : ""}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def text_matches?(message)
|
|
61
|
+
return true if @text.nil?
|
|
62
|
+
|
|
63
|
+
pattern = @text.is_a?(Regexp) ? @text : Regexp.new(Regexp.escape(@text.to_s))
|
|
64
|
+
pattern.match?(message)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The expectations currently in scope, innermost first, as the dialog
|
|
69
|
+
# handler a Dommy Window asks (it calls `#call`).
|
|
70
|
+
#
|
|
71
|
+
# A stack, because Capybara expresses nested confirms as nested helper
|
|
72
|
+
# blocks while the page opens them sequentially in one JS call stack:
|
|
73
|
+
# consuming the inner expectation exposes the outer answer to the next
|
|
74
|
+
# confirm immediately.
|
|
75
|
+
class ModalStack
|
|
76
|
+
def initialize
|
|
77
|
+
@expectations = []
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def empty? = @expectations.empty?
|
|
81
|
+
|
|
82
|
+
def push(expectation)
|
|
83
|
+
@expectations << expectation
|
|
84
|
+
expectation
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# By identity: two helper blocks with the same arguments build EQUAL
|
|
88
|
+
# expectations, and removing by value would drop the caller's alongside
|
|
89
|
+
# this one.
|
|
90
|
+
def delete(expectation)
|
|
91
|
+
@expectations.delete_if { |e| e.equal?(expectation) }
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The dialog handler protocol: answer the dialog, or return
|
|
96
|
+
# DIALOG_UNANSWERED to leave it to the Window's headless default. An
|
|
97
|
+
# expectation that does not want this dialog remembers it (so the block
|
|
98
|
+
# can say what turned up instead) and declines rather than answering.
|
|
99
|
+
def call(type, message, default_value)
|
|
100
|
+
expectation = @expectations.last
|
|
101
|
+
return ::Dommy::Window::DIALOG_UNANSWERED if expectation.nil?
|
|
102
|
+
|
|
103
|
+
unless expectation.matches?(type, message)
|
|
104
|
+
expectation.saw(type, message)
|
|
105
|
+
return ::Dommy::Window::DIALOG_UNANSWERED
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
answer = expectation.answer(message, default_value)
|
|
109
|
+
delete(expectation)
|
|
110
|
+
answer
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/capybara/dommy.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: capybara-dommy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- takahashim
|
|
@@ -29,28 +29,28 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.
|
|
32
|
+
version: 0.12.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.12.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: dommy-rack
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.
|
|
46
|
+
version: 0.12.0
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - "~>"
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 0.
|
|
53
|
+
version: 0.12.0
|
|
54
54
|
description: |
|
|
55
55
|
capybara-dommy is a Capybara driver backed by Dommy and dommy-rack. It drives
|
|
56
56
|
Rack/Rails apps through the Capybara DSL without a real browser or JavaScript,
|
|
@@ -69,6 +69,7 @@ files:
|
|
|
69
69
|
- lib/capybara/dommy/configuration.rb
|
|
70
70
|
- lib/capybara/dommy/driver.rb
|
|
71
71
|
- lib/capybara/dommy/errors.rb
|
|
72
|
+
- lib/capybara/dommy/modal.rb
|
|
72
73
|
- lib/capybara/dommy/node.rb
|
|
73
74
|
- lib/capybara/dommy/rails.rb
|
|
74
75
|
- lib/capybara/dommy/text_extractor.rb
|
|
@@ -94,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
94
95
|
- !ruby/object:Gem::Version
|
|
95
96
|
version: '0'
|
|
96
97
|
requirements: []
|
|
97
|
-
rubygems_version:
|
|
98
|
+
rubygems_version: 3.6.9
|
|
98
99
|
specification_version: 4
|
|
99
100
|
summary: A Dommy-backed Capybara driver
|
|
100
101
|
test_files: []
|