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 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.