opal-vite 0.2.3 → 0.2.5
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/lib/opal/vite/compiler.rb +113 -18
- data/lib/opal/vite/testing/stable_helpers.rb +399 -0
- data/lib/opal/vite/version.rb +1 -1
- data/lib/opal-vite.rb +9 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 589d2115317b97d0c2c1452001b128f4ac3269d8e05c390e60e87d0446c8ebf0
|
|
4
|
+
data.tar.gz: 2811c208a9e64dfd800d2f1d4540a052f348b82b03f255ff6cae820895af8098
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a85ead98a10e206d858a508ef808f9410af6bf6597c6f167b93560da2cb706cbffb252b90ac5144eb9003ef45291bcdbdd84f8dab7c56425d08933be952bbbe8
|
|
7
|
+
data.tar.gz: 117923f00c860592866c31fba50371b84a8c47007c822ce240fa377c510119b1f640443cfa338b78298e5fe59ec1c7d44137340c71503634a71f9b26d9813783
|
data/lib/opal/vite/compiler.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'opal'
|
|
2
2
|
require 'json'
|
|
3
|
+
require 'pathname'
|
|
3
4
|
|
|
4
5
|
module Opal
|
|
5
6
|
module Vite
|
|
@@ -38,12 +39,10 @@ module Opal
|
|
|
38
39
|
dependencies: extract_dependencies(builder)
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
#
|
|
42
|
+
# Extract source map if enabled
|
|
42
43
|
if @config.source_map_enabled
|
|
43
44
|
begin
|
|
44
|
-
|
|
45
|
-
# Try to get it from the processed assets
|
|
46
|
-
source_map = extract_source_map(builder)
|
|
45
|
+
source_map = extract_source_map(builder, file_path)
|
|
47
46
|
result[:map] = source_map if source_map
|
|
48
47
|
rescue => e
|
|
49
48
|
# Source map extraction failed, log but don't fail compilation
|
|
@@ -141,26 +140,122 @@ module Opal
|
|
|
141
140
|
builder.processed.map { |asset| asset.filename }.compact
|
|
142
141
|
end
|
|
143
142
|
|
|
144
|
-
def extract_source_map(builder)
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
return nil unless builder.
|
|
143
|
+
def extract_source_map(builder, file_path)
|
|
144
|
+
# Use builder's combined source_map which includes all compiled files
|
|
145
|
+
# This allows debugging of all required files (controllers, services, etc.)
|
|
146
|
+
return nil unless builder.respond_to?(:source_map) && builder.source_map
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return nil unless
|
|
148
|
+
source_map = builder.source_map
|
|
149
|
+
map_hash = source_map.to_h
|
|
150
|
+
return nil unless map_hash
|
|
152
151
|
|
|
153
|
-
#
|
|
154
|
-
if
|
|
155
|
-
|
|
152
|
+
# If it's an index format with sections, merge all sections
|
|
153
|
+
if map_hash['sections']
|
|
154
|
+
map_hash = merge_all_sections(map_hash, file_path)
|
|
156
155
|
end
|
|
157
156
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
return nil unless map_hash
|
|
158
|
+
|
|
159
|
+
# Normalize source paths for browser debugging
|
|
160
|
+
if map_hash['sources']
|
|
161
|
+
map_hash['sources'] = map_hash['sources'].map do |source|
|
|
162
|
+
normalize_source_path(source, file_path)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
map_hash.to_json
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def merge_all_sections(index_map, file_path)
|
|
170
|
+
sections = index_map['sections']
|
|
171
|
+
return nil if sections.nil? || sections.empty?
|
|
172
|
+
|
|
173
|
+
# For single section, just return that section's map
|
|
174
|
+
if sections.length == 1
|
|
175
|
+
return sections.first['map']
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Merge all sections into a single standard source map
|
|
179
|
+
# This allows debugging of all files (application.rb + all required files)
|
|
180
|
+
merged = {
|
|
181
|
+
'version' => 3,
|
|
182
|
+
'file' => File.basename(file_path),
|
|
183
|
+
'sources' => [],
|
|
184
|
+
'sourcesContent' => [],
|
|
185
|
+
'names' => [],
|
|
186
|
+
'mappings' => ''
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
current_line = 0
|
|
190
|
+
|
|
191
|
+
sections.each_with_index do |section, idx|
|
|
192
|
+
section_map = section['map']
|
|
193
|
+
next unless section_map
|
|
194
|
+
|
|
195
|
+
offset = section['offset'] || { 'line' => 0, 'column' => 0 }
|
|
196
|
+
section_start_line = offset['line'] || 0
|
|
197
|
+
|
|
198
|
+
# Add empty lines to reach the section's starting line
|
|
199
|
+
lines_to_add = section_start_line - current_line
|
|
200
|
+
if lines_to_add > 0
|
|
201
|
+
merged['mappings'] += ';' * lines_to_add
|
|
202
|
+
current_line = section_start_line
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Track source index offset for this section
|
|
206
|
+
source_offset = merged['sources'].length
|
|
207
|
+
name_offset = merged['names'].length
|
|
208
|
+
|
|
209
|
+
# Add sources and sourcesContent from this section
|
|
210
|
+
if section_map['sources']
|
|
211
|
+
section_map['sources'].each_with_index do |source, i|
|
|
212
|
+
merged['sources'] << source
|
|
213
|
+
if section_map['sourcesContent'] && section_map['sourcesContent'][i]
|
|
214
|
+
merged['sourcesContent'] << section_map['sourcesContent'][i]
|
|
215
|
+
else
|
|
216
|
+
merged['sourcesContent'] << nil
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Add names from this section
|
|
222
|
+
if section_map['names']
|
|
223
|
+
merged['names'].concat(section_map['names'])
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Add mappings from this section
|
|
227
|
+
if section_map['mappings'] && !section_map['mappings'].empty?
|
|
228
|
+
# If we need to adjust source/name indices, we'd need to decode/re-encode VLQ
|
|
229
|
+
# For now, append as-is (works when each section has its own source indices starting at 0)
|
|
230
|
+
# This is a simplification - proper implementation would adjust indices
|
|
231
|
+
if idx > 0 && !merged['mappings'].empty? && !merged['mappings'].end_with?(';')
|
|
232
|
+
merged['mappings'] += ';'
|
|
233
|
+
end
|
|
234
|
+
merged['mappings'] += section_map['mappings']
|
|
235
|
+
|
|
236
|
+
# Count lines in this section's mappings
|
|
237
|
+
lines_in_section = section_map['mappings'].count(';') + 1
|
|
238
|
+
current_line += lines_in_section
|
|
239
|
+
end
|
|
161
240
|
end
|
|
162
241
|
|
|
163
|
-
|
|
242
|
+
merged
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def normalize_source_path(source, file_path)
|
|
246
|
+
# Convert absolute paths to relative for better browser debugging
|
|
247
|
+
return source if source.nil? || source.empty?
|
|
248
|
+
|
|
249
|
+
# If source is already relative or a URL, keep it
|
|
250
|
+
return source unless source.start_with?('/')
|
|
251
|
+
|
|
252
|
+
# Try to make path relative to the original file
|
|
253
|
+
begin
|
|
254
|
+
Pathname.new(source).relative_path_from(Pathname.new(File.dirname(file_path))).to_s
|
|
255
|
+
rescue ArgumentError
|
|
256
|
+
# On different drives/mounts, keep absolute path
|
|
257
|
+
source
|
|
258
|
+
end
|
|
164
259
|
end
|
|
165
260
|
end
|
|
166
261
|
end
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Opal
|
|
4
|
+
module Vite
|
|
5
|
+
module Testing
|
|
6
|
+
# StableHelpers - JavaScript-based element interaction methods for Capybara
|
|
7
|
+
# All polling and retry logic runs in JavaScript for reliability
|
|
8
|
+
# Reduces Ruby ↔ JavaScript communication overhead
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# # In spec_helper.rb
|
|
12
|
+
# require 'opal/vite/testing/stable_helpers'
|
|
13
|
+
#
|
|
14
|
+
# RSpec.configure do |config|
|
|
15
|
+
# config.include Opal::Vite::Testing::StableHelpers, type: :feature
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
module StableHelpers
|
|
19
|
+
# Default configuration
|
|
20
|
+
DEFAULT_TIMEOUT = 10
|
|
21
|
+
DEFAULT_INTERVAL = 50 # ms (JavaScript side)
|
|
22
|
+
|
|
23
|
+
# Wait for element to be present and stable in DOM using JavaScript polling
|
|
24
|
+
# @param selector [String] CSS selector
|
|
25
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
26
|
+
# @return [Capybara::Node::Element] The found element
|
|
27
|
+
def stable_find(selector, timeout: DEFAULT_TIMEOUT, visible: true)
|
|
28
|
+
js_wait_for_element(selector, timeout: timeout, visible: visible)
|
|
29
|
+
find(selector, visible: visible, wait: 1)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Find all elements after ensuring DOM stability
|
|
33
|
+
# @param selector [String] CSS selector
|
|
34
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
35
|
+
# @return [Array<Capybara::Node::Element>] Found elements
|
|
36
|
+
def stable_all(selector, timeout: DEFAULT_TIMEOUT)
|
|
37
|
+
wait_for_dom_stable(timeout: timeout)
|
|
38
|
+
all(selector, wait: 1)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Click element with retry logic
|
|
42
|
+
# Uses Capybara native click with JS fallback for reliability
|
|
43
|
+
# @param selector [String] CSS selector
|
|
44
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
45
|
+
def stable_click(selector, timeout: DEFAULT_TIMEOUT)
|
|
46
|
+
escaped_selector = escape_js(selector)
|
|
47
|
+
start_time = Time.now
|
|
48
|
+
|
|
49
|
+
loop do
|
|
50
|
+
begin
|
|
51
|
+
# Use Capybara's find with wait, then click
|
|
52
|
+
element = find(selector, wait: 2, visible: true)
|
|
53
|
+
element.click
|
|
54
|
+
# Small delay to allow event processing
|
|
55
|
+
sleep(0.05)
|
|
56
|
+
return true
|
|
57
|
+
rescue Capybara::ElementNotFound, Ferrum::NodeNotFoundError, Ferrum::TimeoutError,
|
|
58
|
+
Capybara::Cuprite::MouseEventFailed => e
|
|
59
|
+
# If native click fails, try JavaScript click
|
|
60
|
+
js_clicked = page.evaluate_script(<<~JS)
|
|
61
|
+
(function() {
|
|
62
|
+
var el = document.querySelector('#{escaped_selector}');
|
|
63
|
+
if (el && el.offsetParent !== null) {
|
|
64
|
+
el.click();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
})()
|
|
69
|
+
JS
|
|
70
|
+
return true if js_clicked
|
|
71
|
+
|
|
72
|
+
elapsed = Time.now - start_time
|
|
73
|
+
raise e if elapsed > timeout
|
|
74
|
+
sleep(DEFAULT_INTERVAL / 1000.0)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Set value on input with stability check
|
|
80
|
+
# @param selector [String] CSS selector
|
|
81
|
+
# @param value [String] Value to set
|
|
82
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
83
|
+
def stable_set(selector, value, timeout: DEFAULT_TIMEOUT)
|
|
84
|
+
escaped_value = escape_js(value)
|
|
85
|
+
js_retry_action(selector, 'set', value: escaped_value, timeout: timeout)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Send keys to element with stability check
|
|
89
|
+
# @param selector [String] CSS selector
|
|
90
|
+
# @param keys [Array] Keys to send
|
|
91
|
+
def stable_send_keys(selector, *keys, timeout: DEFAULT_TIMEOUT)
|
|
92
|
+
element = stable_find(selector, timeout: timeout)
|
|
93
|
+
element.native.send_keys(*keys)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Combined set and send_keys for form inputs (common pattern)
|
|
97
|
+
# Uses JavaScript for value setting, Capybara native for key events
|
|
98
|
+
# @param selector [String] CSS selector
|
|
99
|
+
# @param value [String] Value to type
|
|
100
|
+
# @param submit_key [Symbol] Key to send after value (e.g., :enter)
|
|
101
|
+
def stable_input(selector, value, submit_key: nil, timeout: DEFAULT_TIMEOUT)
|
|
102
|
+
escaped_selector = escape_js(selector)
|
|
103
|
+
escaped_value = escape_js(value)
|
|
104
|
+
start_time = Time.now
|
|
105
|
+
|
|
106
|
+
loop do
|
|
107
|
+
# Set value via JavaScript for reliability
|
|
108
|
+
result = page.evaluate_script(<<~JS)
|
|
109
|
+
(function() {
|
|
110
|
+
var el = document.querySelector('#{escaped_selector}');
|
|
111
|
+
if (!el || el.offsetParent === null) {
|
|
112
|
+
return { success: false, reason: 'not_found' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
el.focus();
|
|
117
|
+
el.value = '#{escaped_value}';
|
|
118
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
119
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
120
|
+
|
|
121
|
+
if (el.value !== '#{escaped_value}') {
|
|
122
|
+
return { success: false, reason: 'value_not_set' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { success: true };
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return { success: false, reason: e.message };
|
|
128
|
+
}
|
|
129
|
+
})()
|
|
130
|
+
JS
|
|
131
|
+
|
|
132
|
+
if result && result['success']
|
|
133
|
+
# Use Capybara's native send_keys for Enter key (more reliable with Stimulus)
|
|
134
|
+
if submit_key
|
|
135
|
+
# Small delay to ensure Stimulus has processed the value change
|
|
136
|
+
sleep(0.05)
|
|
137
|
+
element = find(selector)
|
|
138
|
+
element.native.send_keys(submit_key)
|
|
139
|
+
end
|
|
140
|
+
return true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
elapsed = Time.now - start_time
|
|
144
|
+
if elapsed > timeout
|
|
145
|
+
reason = result ? result['reason'] : 'unknown'
|
|
146
|
+
raise Capybara::ElementNotFound, "stable_input failed on '#{selector}': #{reason}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
sleep(DEFAULT_INTERVAL / 1000.0)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Wait for element count to match expected (all polling in JavaScript)
|
|
154
|
+
# @param selector [String] CSS selector
|
|
155
|
+
# @param count [Integer] Expected count
|
|
156
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
157
|
+
def wait_for_count(selector, count, timeout: DEFAULT_TIMEOUT)
|
|
158
|
+
escaped_selector = escape_js(selector)
|
|
159
|
+
js_poll_until(
|
|
160
|
+
"document.querySelectorAll('#{escaped_selector}').length === #{count}",
|
|
161
|
+
timeout: timeout,
|
|
162
|
+
error_message: "Expected #{count} elements for '#{selector}'"
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Wait for element to contain text
|
|
167
|
+
# @param selector [String] CSS selector
|
|
168
|
+
# @param text [String] Text to find
|
|
169
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
170
|
+
def wait_for_text(selector, text, timeout: DEFAULT_TIMEOUT)
|
|
171
|
+
escaped_selector = escape_js(selector)
|
|
172
|
+
escaped_text = escape_js(text)
|
|
173
|
+
js_poll_until(
|
|
174
|
+
"(function() { var el = document.querySelector('#{escaped_selector}'); return el && el.textContent.includes('#{escaped_text}'); })()",
|
|
175
|
+
timeout: timeout,
|
|
176
|
+
error_message: "Text '#{text}' not found in '#{selector}'"
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Wait for element to have specific class
|
|
181
|
+
# @param selector [String] CSS selector
|
|
182
|
+
# @param class_name [String] CSS class name
|
|
183
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
184
|
+
def wait_for_class(selector, class_name, timeout: DEFAULT_TIMEOUT)
|
|
185
|
+
escaped_selector = escape_js(selector)
|
|
186
|
+
escaped_class = escape_js(class_name)
|
|
187
|
+
js_poll_until(
|
|
188
|
+
"(function() { var el = document.querySelector('#{escaped_selector}'); return el && el.classList.contains('#{escaped_class}'); })()",
|
|
189
|
+
timeout: timeout,
|
|
190
|
+
error_message: "Class '#{class_name}' not found on '#{selector}'"
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Wait for element to NOT have specific class
|
|
195
|
+
# @param selector [String] CSS selector
|
|
196
|
+
# @param class_name [String] CSS class name
|
|
197
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
198
|
+
def wait_for_no_class(selector, class_name, timeout: DEFAULT_TIMEOUT)
|
|
199
|
+
escaped_selector = escape_js(selector)
|
|
200
|
+
escaped_class = escape_js(class_name)
|
|
201
|
+
js_poll_until(
|
|
202
|
+
"(function() { var el = document.querySelector('#{escaped_selector}'); return el && !el.classList.contains('#{escaped_class}'); })()",
|
|
203
|
+
timeout: timeout,
|
|
204
|
+
error_message: "Class '#{class_name}' still present on '#{selector}'"
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Wait for checkbox to be checked
|
|
209
|
+
# @param selector [String] CSS selector
|
|
210
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
211
|
+
def wait_for_checked(selector, timeout: DEFAULT_TIMEOUT)
|
|
212
|
+
escaped_selector = escape_js(selector)
|
|
213
|
+
js_poll_until(
|
|
214
|
+
"(function() { var el = document.querySelector('#{escaped_selector}'); return el && el.checked === true; })()",
|
|
215
|
+
timeout: timeout,
|
|
216
|
+
error_message: "Checkbox '#{selector}' not checked"
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Public API for custom JavaScript conditions
|
|
221
|
+
# @param js_condition [String] JavaScript expression that returns true when ready
|
|
222
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
223
|
+
def js_wait_for(js_condition, timeout: DEFAULT_TIMEOUT)
|
|
224
|
+
js_poll_until(js_condition, timeout: timeout, error_message: "Condition not met: #{js_condition}")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Wait for DOM to be stable (no pending mutations)
|
|
228
|
+
# Uses MutationObserver setup in JS with Ruby-based polling
|
|
229
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
230
|
+
# @param stability_time [Integer] Time in ms with no changes to consider stable
|
|
231
|
+
def wait_for_dom_stable(timeout: DEFAULT_TIMEOUT, stability_time: 100)
|
|
232
|
+
# Setup MutationObserver in JavaScript
|
|
233
|
+
page.execute_script(<<~JS)
|
|
234
|
+
window.__domStabilityState = { stable: false, lastChange: Date.now() };
|
|
235
|
+
if (window.__domObserver) window.__domObserver.disconnect();
|
|
236
|
+
|
|
237
|
+
window.__domObserver = new MutationObserver(function() {
|
|
238
|
+
window.__domStabilityState.stable = false;
|
|
239
|
+
window.__domStabilityState.lastChange = Date.now();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
window.__domObserver.observe(document.body, {
|
|
243
|
+
childList: true,
|
|
244
|
+
subtree: true,
|
|
245
|
+
attributes: true
|
|
246
|
+
});
|
|
247
|
+
JS
|
|
248
|
+
|
|
249
|
+
start_time = Time.now
|
|
250
|
+
|
|
251
|
+
# Poll for stability using Ruby loop
|
|
252
|
+
loop do
|
|
253
|
+
result = page.evaluate_script(<<~JS)
|
|
254
|
+
(function() {
|
|
255
|
+
var state = window.__domStabilityState;
|
|
256
|
+
if (!state) return { stable: true };
|
|
257
|
+
var elapsed = Date.now() - state.lastChange;
|
|
258
|
+
return { stable: elapsed >= #{stability_time}, elapsed: elapsed };
|
|
259
|
+
})()
|
|
260
|
+
JS
|
|
261
|
+
|
|
262
|
+
if result && result['stable']
|
|
263
|
+
# Cleanup observer
|
|
264
|
+
page.execute_script(<<~JS)
|
|
265
|
+
if (window.__domObserver) {
|
|
266
|
+
window.__domObserver.disconnect();
|
|
267
|
+
delete window.__domObserver;
|
|
268
|
+
}
|
|
269
|
+
delete window.__domStabilityState;
|
|
270
|
+
JS
|
|
271
|
+
return true
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
elapsed = Time.now - start_time
|
|
275
|
+
if elapsed > timeout
|
|
276
|
+
# Cleanup observer on timeout
|
|
277
|
+
page.execute_script('if (window.__domObserver) window.__domObserver.disconnect();')
|
|
278
|
+
raise Capybara::ElementNotFound, 'DOM did not stabilize within timeout'
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
sleep(DEFAULT_INTERVAL / 1000.0)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
# Wait for element to be present and visible using JavaScript polling
|
|
288
|
+
def js_wait_for_element(selector, timeout:, visible:)
|
|
289
|
+
escaped_selector = escape_js(selector)
|
|
290
|
+
visibility_check = visible ? ' && el.offsetParent !== null' : ''
|
|
291
|
+
|
|
292
|
+
js_poll_until(
|
|
293
|
+
"(function() { var el = document.querySelector('#{escaped_selector}'); return el !== null#{visibility_check}; })()",
|
|
294
|
+
timeout: timeout,
|
|
295
|
+
error_message: "Element not found: #{selector}"
|
|
296
|
+
)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Wait for input to have specific value
|
|
300
|
+
def wait_for_value(selector, value, timeout:)
|
|
301
|
+
escaped_selector = escape_js(selector)
|
|
302
|
+
escaped_value = escape_js(value)
|
|
303
|
+
js_poll_until(
|
|
304
|
+
"(function() { var el = document.querySelector('#{escaped_selector}'); return el && el.value === '#{escaped_value}'; })()",
|
|
305
|
+
timeout: timeout,
|
|
306
|
+
error_message: "Value '#{value}' not set on '#{selector}'"
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Core JavaScript polling - all retry logic runs in browser
|
|
311
|
+
# @param js_condition [String] JavaScript expression that returns true when ready
|
|
312
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
313
|
+
# @param error_message [String] Error message on timeout
|
|
314
|
+
def js_poll_until(js_condition, timeout:, error_message: nil)
|
|
315
|
+
error_msg = error_message || "Condition not met: #{js_condition}"
|
|
316
|
+
|
|
317
|
+
# Use synchronous polling with Ruby timeout to avoid async script timeout issues
|
|
318
|
+
start_time = Time.now
|
|
319
|
+
|
|
320
|
+
loop do
|
|
321
|
+
result = page.evaluate_script(<<~JS)
|
|
322
|
+
(function() {
|
|
323
|
+
try {
|
|
324
|
+
return #{js_condition} ? true : false;
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
})()
|
|
329
|
+
JS
|
|
330
|
+
|
|
331
|
+
return true if result
|
|
332
|
+
|
|
333
|
+
elapsed = Time.now - start_time
|
|
334
|
+
raise Capybara::ElementNotFound, error_msg if elapsed > timeout
|
|
335
|
+
|
|
336
|
+
sleep(DEFAULT_INTERVAL / 1000.0)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# JavaScript-based action with Ruby retry loop (click, set, etc.)
|
|
341
|
+
# @param selector [String] CSS selector
|
|
342
|
+
# @param action [String] Action to perform ('click', 'set')
|
|
343
|
+
# @param value [String] Value for 'set' action
|
|
344
|
+
# @param timeout [Integer] Maximum wait time in seconds
|
|
345
|
+
def js_retry_action(selector, action, value: nil, timeout: DEFAULT_TIMEOUT)
|
|
346
|
+
escaped_selector = escape_js(selector)
|
|
347
|
+
escaped_value = value ? escape_js(value) : ''
|
|
348
|
+
start_time = Time.now
|
|
349
|
+
|
|
350
|
+
loop do
|
|
351
|
+
result = page.evaluate_script(<<~JS)
|
|
352
|
+
(function() {
|
|
353
|
+
var el = document.querySelector('#{escaped_selector}');
|
|
354
|
+
if (!el || el.offsetParent === null) {
|
|
355
|
+
return { success: false, reason: 'not_found' };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
if ('#{action}' === 'click') {
|
|
360
|
+
el.click();
|
|
361
|
+
return { success: true };
|
|
362
|
+
} else if ('#{action}' === 'set') {
|
|
363
|
+
el.focus();
|
|
364
|
+
el.value = '#{escaped_value}';
|
|
365
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
366
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
367
|
+
return { success: true };
|
|
368
|
+
} else {
|
|
369
|
+
return { success: false, reason: 'unknown_action' };
|
|
370
|
+
}
|
|
371
|
+
} catch (e) {
|
|
372
|
+
return { success: false, reason: e.message };
|
|
373
|
+
}
|
|
374
|
+
})()
|
|
375
|
+
JS
|
|
376
|
+
|
|
377
|
+
return true if result && result['success']
|
|
378
|
+
|
|
379
|
+
elapsed = Time.now - start_time
|
|
380
|
+
if elapsed > timeout
|
|
381
|
+
reason = result ? result['reason'] : 'unknown'
|
|
382
|
+
raise Capybara::ElementNotFound, "Action '#{action}' failed on '#{selector}': #{reason}"
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
sleep(DEFAULT_INTERVAL / 1000.0)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Escape string for JavaScript
|
|
390
|
+
def escape_js(str)
|
|
391
|
+
str.to_s.gsub('\\', '\\\\\\\\').gsub("'", "\\\\'").gsub("\n", '\\n')
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Alias for backward compatibility
|
|
399
|
+
StableHelpers = Opal::Vite::Testing::StableHelpers
|
data/lib/opal/vite/version.rb
CHANGED
data/lib/opal-vite.rb
CHANGED
|
@@ -30,12 +30,20 @@ module Opal
|
|
|
30
30
|
# CLI entry point for compilation
|
|
31
31
|
# @param file_path [String] The path to the Ruby file to compile
|
|
32
32
|
# @param include_concerns [Boolean] Whether to include built-in concerns
|
|
33
|
-
|
|
33
|
+
# @param source_map [Boolean] Whether to generate source maps
|
|
34
|
+
def compile_for_vite(file_path, include_concerns: true, source_map: true)
|
|
35
|
+
# Temporarily override source map setting if specified
|
|
36
|
+
original_source_map = config.source_map_enabled
|
|
37
|
+
config.source_map_enabled = source_map
|
|
38
|
+
|
|
34
39
|
compiler = Compiler.new(include_concerns: include_concerns)
|
|
35
40
|
result = compiler.compile_file(file_path)
|
|
36
41
|
|
|
37
42
|
# Output JSON to stdout for the Vite plugin to consume
|
|
38
43
|
puts JSON.generate(result)
|
|
44
|
+
|
|
45
|
+
# Restore original setting
|
|
46
|
+
config.source_map_enabled = original_source_map
|
|
39
47
|
rescue Compiler::CompilationError => e
|
|
40
48
|
STDERR.puts e.message
|
|
41
49
|
exit 1
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: opal-vite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- stofu1234
|
|
@@ -109,6 +109,7 @@ files:
|
|
|
109
109
|
- lib/opal/vite/compiler.rb
|
|
110
110
|
- lib/opal/vite/config.rb
|
|
111
111
|
- lib/opal/vite/source_map.rb
|
|
112
|
+
- lib/opal/vite/testing/stable_helpers.rb
|
|
112
113
|
- lib/opal/vite/version.rb
|
|
113
114
|
- opal/opal_vite.rb
|
|
114
115
|
- opal/opal_vite/concerns.rb
|