capybara-simulated 0.0.2
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 +7 -0
- data/LICENSE +20 -0
- data/README.md +225 -0
- data/lib/capybara/simulated/browser.rb +1012 -0
- data/lib/capybara/simulated/driver.rb +191 -0
- data/lib/capybara/simulated/errors.rb +9 -0
- data/lib/capybara/simulated/node.rb +235 -0
- data/lib/capybara/simulated/version.rb +5 -0
- data/lib/capybara/simulated.rb +10 -0
- data/lib/capybara-simulated.rb +1 -0
- data/vendor/esbuild-wasm/LICENSE.md +21 -0
- data/vendor/esbuild-wasm/bin/esbuild +91 -0
- data/vendor/esbuild-wasm/esbuild.wasm +0 -0
- data/vendor/esbuild-wasm/lib/main.js +2337 -0
- data/vendor/esbuild-wasm/wasm_exec.js +575 -0
- data/vendor/esbuild-wasm/wasm_exec_node.js +40 -0
- data/vendor/js/bundle-modules.mjs +168 -0
- data/vendor/js/csim.bundle.js +101015 -0
- data/vendor/js/entry.mjs +8 -0
- data/vendor/js/prelude.js +186 -0
- data/vendor/js/runtime.js +2054 -0
- metadata +106 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2a1b71c5bedf4ee1236631f583accd90b10e71317c00e5b20252c5b8f48477ea
|
|
4
|
+
data.tar.gz: b1b3a3bb91e784b579cb4d465f253ed78c3bb606ef9629afe2197693114fa10b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ba4e4836908d7f607c475321edc01592b89e53df666e531330e4283725633201622461df829b4e40c37f1ad9ba5d67d74e71fbaef14efda60c11ec7abaa6441c
|
|
7
|
+
data.tar.gz: d6ecc0867f4ae527d483c337a7129e65e6f31fd24a5c507247df66ddb89ac1eeb98060101868f3e43f787caea6e1ad35390afd94504aff8c7f05b095fcaa1a09
|
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Keita Urashima
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE OUTSIDE OF THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# capybara-simulated
|
|
2
|
+
|
|
3
|
+
A lightweight Capybara driver that runs JavaScript in a long-lived
|
|
4
|
+
[mini_racer](https://github.com/rubyjs/mini_racer) V8 context against a
|
|
5
|
+
[happy-dom](https://github.com/capricorn86/happy-dom) DOM. XPath queries
|
|
6
|
+
are powered by [fontoxpath](https://github.com/FontoXML/fontoxpath).
|
|
7
|
+
|
|
8
|
+
The goal is the middle ground between `rack-test` (zero JS) and full
|
|
9
|
+
headless browsers like cuprite/selenium: in-process tests, no Chrome,
|
|
10
|
+
inline `<script>` and event handlers run, the Capybara DSL works, and
|
|
11
|
+
forms submit through `Rack::MockRequest`.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
PoC. Against Capybara 3.40's shared `Capybara::SpecHelper.spec` suite the
|
|
16
|
+
driver passes **1337 / 1357 examples (98.5%)** with the unsupported-
|
|
17
|
+
capability tags `about_scheme`, `css`, `download`, `frames`, `hover`,
|
|
18
|
+
`screenshot`, `scroll`, `server`, `spatial`, `windows` filtered out.
|
|
19
|
+
Tags that started skipped but now pass: `active_element`, `modals`
|
|
20
|
+
(alert/confirm/prompt incl. nested + page-change + async),
|
|
21
|
+
`html_validation`, `send_keys`, `shadow_dom`. The remaining 20
|
|
22
|
+
failures all need capabilities the driver intentionally does not
|
|
23
|
+
implement:
|
|
24
|
+
|
|
25
|
+
- 19 `#drag_to` tests — Dragula / SortableJS / jsTree resolve drop
|
|
26
|
+
targets through `elementFromPoint(clientX, clientY)`, which needs a
|
|
27
|
+
real layout engine with stacking-context awareness.
|
|
28
|
+
- 1 `#click should not retry clicking when wait is disabled` — depends
|
|
29
|
+
on the same `elementFromPoint`-based obscured-element detection.
|
|
30
|
+
|
|
31
|
+
`evaluate_async_script` is supported by polling the Ruby↔V8 bridge while
|
|
32
|
+
draining the virtual clock until the user callback fires (or
|
|
33
|
+
`Capybara.default_max_wait_time` elapses). Click offsets and `Element#drop`
|
|
34
|
+
work without a real layout engine: click x/y are resolved into clientX/Y
|
|
35
|
+
by walking computed `top`/`left`/`width`/`height` (px only, ancestor-sum)
|
|
36
|
+
on the way out of the runtime.
|
|
37
|
+
|
|
38
|
+
### Turbo + Stimulus (Rails)
|
|
39
|
+
|
|
40
|
+
The driver targets Rails apps using importmap-rails + Turbo + Stimulus
|
|
41
|
+
out of the box:
|
|
42
|
+
|
|
43
|
+
- `<script type="importmap">` is parsed; `<script type="module">` and
|
|
44
|
+
every reachable `import` are pre-fetched through the Rack app and
|
|
45
|
+
bundled on the fly via `node vendor/js/bundle-modules.mjs` (a small
|
|
46
|
+
esbuild driver). The resulting IIFE is evaluated in the same isolate
|
|
47
|
+
as inline scripts and shares the same happy-dom Window.
|
|
48
|
+
- `fetch` is replaced with a Rack-routed implementation: cookies follow
|
|
49
|
+
the jar, `X-CSRF-Token` propagates, redirects are followed up to 20
|
|
50
|
+
hops, and `Response`/`Headers`/`Request`/`AbortController` come from
|
|
51
|
+
happy-dom. WebSocket / EventSource / Action Cable broadcasts are out
|
|
52
|
+
of scope — use Selenium for those flows.
|
|
53
|
+
- Custom-element upgrade is patched at `customElements.define` to
|
|
54
|
+
rebuild the document's id-element index — happy-dom 20's auto-upgrade
|
|
55
|
+
leaves the original (un-upgraded) element in the index, so
|
|
56
|
+
`getElementById('records')` after a Turbo Stream `<turbo-frame>` swap
|
|
57
|
+
would otherwise return a detached ghost.
|
|
58
|
+
- Click on `<button type="submit">` lets happy-dom auto-dispatch the
|
|
59
|
+
`submit` event with the proper `submitter` field; we capture whether
|
|
60
|
+
the event was preventDefaulted and skip our own redundant submit
|
|
61
|
+
dispatch. Without this, Turbo's `FormSubmitObserver` saw a second
|
|
62
|
+
submitter-less event and intercepted forms whose submitter had
|
|
63
|
+
`data-turbo="false"`.
|
|
64
|
+
- Page-level globals (`addEventListener`, `MutationObserver`,
|
|
65
|
+
`requestAnimationFrame`, `CustomEvent`, `getComputedStyle`,
|
|
66
|
+
`IntersectionObserver` stub, etc.) are mirrored from the active
|
|
67
|
+
Window onto `globalThis` so module bundles running at the top level
|
|
68
|
+
find them without going through `window.*`.
|
|
69
|
+
- happy-dom's `MutationObserverListener` keeps its dispatch callback in
|
|
70
|
+
a `WeakRef`, with no other strong reference. V8 collects the arrow
|
|
71
|
+
before the next mutation fires, so `target[mutationListeners]` still
|
|
72
|
+
carries the listener but `callback.deref()` returns undefined and
|
|
73
|
+
every record (and every subtree-propagation hop on `appendChild`) is
|
|
74
|
+
silently dropped — Stimulus loses sight of buttons added inside a
|
|
75
|
+
swapped `<turbo-frame>`. We patch `MutationObserver.prototype.observe`
|
|
76
|
+
per Window to swap each WeakRef out for a strong-reference shim with
|
|
77
|
+
the same `.deref()` shape, so listeners survive the next GC.
|
|
78
|
+
|
|
79
|
+
WebSocket, frames and multi-window remain explicitly out of scope — they
|
|
80
|
+
need a real browser (Selenium / Cuprite) or a separate transport that
|
|
81
|
+
this driver does not provide.
|
|
82
|
+
|
|
83
|
+
The V8 isolate is created once per `Capybara::Simulated::Browser` instance
|
|
84
|
+
and reused across all visits and resets — only the happy-dom `Window` is
|
|
85
|
+
torn down between specs. This keeps `reset!` cheap.
|
|
86
|
+
|
|
87
|
+
## Build
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
npm install
|
|
91
|
+
npm run build # produces vendor/js/csim.bundle.js (~3.3MB)
|
|
92
|
+
bundle install
|
|
93
|
+
bundle exec rspec
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Install
|
|
97
|
+
|
|
98
|
+
Add to your Gemfile (development / test group):
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
gem 'capybara-simulated', '~> 0.0', group: :test
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then `bundle install`. The gem ships its own pre-built happy-dom bundle
|
|
105
|
+
under `vendor/js/`, so no `npm install` is required at consume time.
|
|
106
|
+
|
|
107
|
+
## Use
|
|
108
|
+
|
|
109
|
+
`require 'capybara/simulated'` registers the `:simulated` driver. The
|
|
110
|
+
snippets below are minimal — drop them into your existing test bootstrap.
|
|
111
|
+
|
|
112
|
+
### RSpec
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# spec/spec_helper.rb (or spec/rails_helper.rb)
|
|
116
|
+
require 'capybara/rspec'
|
|
117
|
+
require 'capybara/simulated'
|
|
118
|
+
|
|
119
|
+
Capybara.javascript_driver = :simulated
|
|
120
|
+
# Optional: make :simulated the default for non-JS specs too.
|
|
121
|
+
# Capybara.default_driver = :simulated
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Tests tagged `js: true` (or `type: :system, js: true` in Rails) will run
|
|
125
|
+
in the simulated driver:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
RSpec.describe 'sign-in', type: :system, js: true do
|
|
129
|
+
it 'logs the user in' do
|
|
130
|
+
visit '/login'
|
|
131
|
+
fill_in 'Email', with: 'alice@example.com'
|
|
132
|
+
fill_in 'Password', with: 'hunter2'
|
|
133
|
+
click_button 'Log in'
|
|
134
|
+
expect(page).to have_text('Welcome, Alice')
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For a Rails system test, set the driver in `before_setup` /
|
|
140
|
+
`driven_by`:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# spec/system/sign_in_spec.rb
|
|
144
|
+
RSpec.describe 'sign-in', type: :system do
|
|
145
|
+
before { driven_by :simulated }
|
|
146
|
+
# ...
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Minitest
|
|
151
|
+
|
|
152
|
+
`Capybara.javascript_driver` is RSpec-only — `ActionDispatch::SystemTestCase`
|
|
153
|
+
ignores it and `Capybara::Minitest::Test` has no `js: true` metadata
|
|
154
|
+
mechanism. Set the driver explicitly:
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
# test/application_system_test_case.rb
|
|
158
|
+
require 'capybara/minitest'
|
|
159
|
+
require 'capybara/simulated'
|
|
160
|
+
|
|
161
|
+
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
|
162
|
+
driven_by :simulated
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
(For pure `Capybara::Minitest::Test` outside Rails, set
|
|
167
|
+
`Capybara.default_driver = :simulated` in your test_helper.)
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# test/system/sign_in_test.rb
|
|
171
|
+
require 'application_system_test_case'
|
|
172
|
+
|
|
173
|
+
class SignInTest < ApplicationSystemTestCase
|
|
174
|
+
test 'logs the user in' do
|
|
175
|
+
visit '/login'
|
|
176
|
+
fill_in 'Email', with: 'alice@example.com'
|
|
177
|
+
fill_in 'Password', with: 'hunter2'
|
|
178
|
+
click_button 'Log in'
|
|
179
|
+
assert_text 'Welcome, Alice'
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Plain Capybara DSL (no framework)
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
require 'capybara/dsl'
|
|
188
|
+
require 'capybara/simulated'
|
|
189
|
+
|
|
190
|
+
Capybara.app = MyRackApp
|
|
191
|
+
Capybara.default_driver = :simulated
|
|
192
|
+
|
|
193
|
+
include Capybara::DSL
|
|
194
|
+
|
|
195
|
+
visit '/'
|
|
196
|
+
click_link 'About'
|
|
197
|
+
puts page.text
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## How it fits together
|
|
201
|
+
|
|
202
|
+
- `vendor/js/prelude.js` — minimal Web Platform polyfills (TextEncoder,
|
|
203
|
+
atob/btoa, crypto.getRandomValues, performance, timers, process).
|
|
204
|
+
- `vendor/js/csim.bundle.js` — bundled happy-dom + whatwg-url +
|
|
205
|
+
fontoxpath. Built via `build.mjs` with esbuild, with shims for the
|
|
206
|
+
Node built-ins happy-dom imports (`url`, `buffer`, `vm`, `path`, etc.).
|
|
207
|
+
- `vendor/js/runtime.js` — driver glue exposed on `globalThis.__csim`.
|
|
208
|
+
Manages the active happy-dom `Window`, an integer→DOM-node handle
|
|
209
|
+
table, modal capture, form serialization, click/submit dispatch, and
|
|
210
|
+
XPath via fontoxpath with a custom DOM facade.
|
|
211
|
+
- `lib/capybara/simulated/browser.rb` — owns the `MiniRacer::Context`,
|
|
212
|
+
drives HTTP via `Rack::MockRequest`, fetches `<script src>` inline,
|
|
213
|
+
and routes form submissions back through Rack.
|
|
214
|
+
- `lib/capybara/simulated/{driver,node}.rb` — Capybara `Driver::Base` and
|
|
215
|
+
`Driver::Node` implementations.
|
|
216
|
+
|
|
217
|
+
## Known limits
|
|
218
|
+
|
|
219
|
+
- happy-dom is not a layout engine — `visible?` is heuristic
|
|
220
|
+
(`display:none`, `visibility:hidden`, `hidden` attribute, head/script).
|
|
221
|
+
- No fetch/XHR. `<script src>` is inlined via `Rack::MockRequest`. Real
|
|
222
|
+
navigation only happens on link click and form submit.
|
|
223
|
+
- `evaluate_async_script`, frames, multi-window, file uploads, screenshots,
|
|
224
|
+
CSS computed-style filters, scroll/drag pixel coordinates are out of
|
|
225
|
+
scope.
|