appom 1.4.0 → 2.0.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/README.md +170 -42
- data/lib/appom/configuration.rb +490 -0
- data/lib/appom/element_cache.rb +372 -0
- data/lib/appom/element_container.rb +257 -244
- data/lib/appom/element_finder.rb +142 -138
- data/lib/appom/element_state.rb +458 -0
- data/lib/appom/element_validation.rb +138 -0
- data/lib/appom/exceptions.rb +130 -0
- data/lib/appom/helpers.rb +328 -0
- data/lib/appom/logging.rb +106 -0
- data/lib/appom/page.rb +19 -10
- data/lib/appom/performance.rb +394 -0
- data/lib/appom/retry.rb +178 -0
- data/lib/appom/screenshot.rb +371 -0
- data/lib/appom/section.rb +24 -21
- data/lib/appom/smart_wait.rb +455 -0
- data/lib/appom/version.rb +4 -1
- data/lib/appom/visual.rb +600 -0
- data/lib/appom/wait.rb +96 -35
- data/lib/appom.rb +191 -20
- metadata +35 -19
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Element state tracking for Appom automation framework
|
|
4
|
+
# Tracks element state changes and provides monitoring capabilities
|
|
5
|
+
module Appom::ElementState
|
|
6
|
+
# Tracks element states and changes over time
|
|
7
|
+
class Tracker
|
|
8
|
+
include Appom::Logging
|
|
9
|
+
|
|
10
|
+
attr_reader :tracked_elements, :state_history
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@tracked_elements = {}
|
|
14
|
+
@state_history = []
|
|
15
|
+
@observers = []
|
|
16
|
+
@tracking_enabled = true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Start tracking an element
|
|
20
|
+
def track_element(element, name: nil, context: {})
|
|
21
|
+
element_id = generate_element_id(element, name)
|
|
22
|
+
|
|
23
|
+
state = capture_element_state(element)
|
|
24
|
+
|
|
25
|
+
@tracked_elements[element_id] = {
|
|
26
|
+
element: element,
|
|
27
|
+
name: name || element_id,
|
|
28
|
+
context: context,
|
|
29
|
+
first_seen: Time.now,
|
|
30
|
+
last_updated: Time.now,
|
|
31
|
+
current_state: state,
|
|
32
|
+
previous_states: [],
|
|
33
|
+
change_count: 0,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
log_debug("Started tracking element: #{name || element_id}")
|
|
37
|
+
notify_observers(:element_tracked, element_id, state)
|
|
38
|
+
|
|
39
|
+
element_id
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Update element state and detect changes
|
|
43
|
+
def update_element_state(element_id)
|
|
44
|
+
return unless @tracking_enabled
|
|
45
|
+
|
|
46
|
+
tracked = @tracked_elements[element_id]
|
|
47
|
+
return unless tracked
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
new_state = capture_element_state(tracked[:element])
|
|
51
|
+
old_state = tracked[:current_state]
|
|
52
|
+
|
|
53
|
+
if state_changed?(old_state, new_state)
|
|
54
|
+
# Store previous state
|
|
55
|
+
tracked[:previous_states] << {
|
|
56
|
+
state: old_state,
|
|
57
|
+
timestamp: tracked[:last_updated],
|
|
58
|
+
duration: Time.now - tracked[:last_updated],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Keep only last 10 states
|
|
62
|
+
tracked[:previous_states] = tracked[:previous_states].last(10)
|
|
63
|
+
|
|
64
|
+
# Update current state
|
|
65
|
+
tracked[:current_state] = new_state
|
|
66
|
+
tracked[:last_updated] = Time.now
|
|
67
|
+
tracked[:change_count] += 1
|
|
68
|
+
|
|
69
|
+
# Record in history
|
|
70
|
+
change_event = {
|
|
71
|
+
timestamp: Time.now,
|
|
72
|
+
element_id: element_id,
|
|
73
|
+
element_name: tracked[:name],
|
|
74
|
+
old_state: old_state,
|
|
75
|
+
new_state: new_state,
|
|
76
|
+
changes: calculate_changes(old_state, new_state),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@state_history << change_event
|
|
80
|
+
@state_history = @state_history.last(1000) # Keep last 1000 changes
|
|
81
|
+
|
|
82
|
+
log_debug("Element state changed: #{tracked[:name]}", change_event[:changes])
|
|
83
|
+
notify_observers(:state_changed, element_id, change_event)
|
|
84
|
+
|
|
85
|
+
change_event
|
|
86
|
+
end
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
log_warn("Failed to update element state: #{e.message}")
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get current state of tracked element
|
|
94
|
+
def element_state(element_id)
|
|
95
|
+
tracked = @tracked_elements[element_id]
|
|
96
|
+
return nil unless tracked
|
|
97
|
+
|
|
98
|
+
# Update state before returning
|
|
99
|
+
update_element_state(element_id)
|
|
100
|
+
tracked[:current_state]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get element state history
|
|
104
|
+
def element_history(element_id, limit: 50)
|
|
105
|
+
@state_history
|
|
106
|
+
.select { |event| event[:element_id] == element_id }
|
|
107
|
+
.last(limit)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Wait for element state change
|
|
111
|
+
def wait_for_state_change(element_id, expected_changes: {}, timeout: 10, interval: 0.5)
|
|
112
|
+
start_time = Time.now
|
|
113
|
+
|
|
114
|
+
loop do
|
|
115
|
+
current_state = element_state(element_id)
|
|
116
|
+
|
|
117
|
+
return current_state if expected_changes.all? { |key, value| current_state&.dig(key) == value }
|
|
118
|
+
|
|
119
|
+
if Time.now - start_time > timeout
|
|
120
|
+
raise Appom::TimeoutError,
|
|
121
|
+
"Element state did not change as expected within #{timeout}s. " \
|
|
122
|
+
"Expected: #{expected_changes}, Current: #{current_state}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
sleep interval
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Wait for element to be in specific state
|
|
130
|
+
def wait_for_state(element_id, condition, timeout: 10, interval: 0.5)
|
|
131
|
+
start_time = Time.now
|
|
132
|
+
|
|
133
|
+
loop do
|
|
134
|
+
current_state = element_state(element_id)
|
|
135
|
+
|
|
136
|
+
result = case condition
|
|
137
|
+
when Proc
|
|
138
|
+
condition.call(current_state)
|
|
139
|
+
when Hash
|
|
140
|
+
condition.all? { |key, value| current_state&.dig(key) == value }
|
|
141
|
+
else
|
|
142
|
+
false
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
return current_state if result
|
|
146
|
+
|
|
147
|
+
if Time.now - start_time > timeout
|
|
148
|
+
raise Appom::TimeoutError,
|
|
149
|
+
"Element did not reach expected state within #{timeout}s. Current: #{current_state}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
sleep interval
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Stop tracking an element
|
|
157
|
+
def stop_tracking(element_id)
|
|
158
|
+
tracked = @tracked_elements.delete(element_id)
|
|
159
|
+
return unless tracked
|
|
160
|
+
|
|
161
|
+
log_debug("Stopped tracking element: #{tracked[:name]}")
|
|
162
|
+
notify_observers(:element_untracked, element_id)
|
|
163
|
+
|
|
164
|
+
# Return final summary
|
|
165
|
+
{
|
|
166
|
+
name: tracked[:name],
|
|
167
|
+
tracking_duration: Time.now - tracked[:first_seen],
|
|
168
|
+
change_count: tracked[:change_count],
|
|
169
|
+
final_state: tracked[:current_state],
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Get tracking summary
|
|
174
|
+
def tracking_summary
|
|
175
|
+
{
|
|
176
|
+
total_tracked: @tracked_elements.count,
|
|
177
|
+
total_changes: @state_history.count,
|
|
178
|
+
most_active: most_active_elements(5),
|
|
179
|
+
recent_changes: @state_history.last(10),
|
|
180
|
+
tracking_enabled: @tracking_enabled,
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Find elements by state criteria
|
|
185
|
+
def find_elements_by_state(criteria)
|
|
186
|
+
if criteria.is_a?(Hash)
|
|
187
|
+
# Always return array of tracked hashes for consistency with test expectations
|
|
188
|
+
@tracked_elements.values.select { |tracked| criteria.all? { |key, value| tracked[:current_state]&.dig(key) == value } }
|
|
189
|
+
else
|
|
190
|
+
@tracked_elements.values.select do |tracked|
|
|
191
|
+
current_state = tracked[:current_state]
|
|
192
|
+
case criteria
|
|
193
|
+
when Proc
|
|
194
|
+
criteria.call(current_state)
|
|
195
|
+
else
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Add state change observer
|
|
203
|
+
def add_observer(&block)
|
|
204
|
+
@observers << block
|
|
205
|
+
block
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Remove observer
|
|
209
|
+
def remove_observer(observer)
|
|
210
|
+
@observers.delete(observer)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Enable/disable tracking
|
|
214
|
+
def tracking_enabled=(enabled)
|
|
215
|
+
@tracking_enabled = enabled
|
|
216
|
+
log_info("Element state tracking #{enabled ? 'enabled' : 'disabled'}")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Clear all tracking data
|
|
220
|
+
def clear!
|
|
221
|
+
@tracked_elements.clear
|
|
222
|
+
@state_history.clear
|
|
223
|
+
log_info('Element state tracking data cleared')
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Export tracking data
|
|
227
|
+
def export_tracking_data(file_path: nil, format: :json)
|
|
228
|
+
file_path ||= "element_state_tracking_#{Time.now.strftime('%Y%m%d_%H%M%S')}.#{format}"
|
|
229
|
+
|
|
230
|
+
data = {
|
|
231
|
+
exported_at: Time.now,
|
|
232
|
+
summary: tracking_summary,
|
|
233
|
+
tracked_elements: serialize_tracked_elements,
|
|
234
|
+
state_history: @state_history,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case format
|
|
238
|
+
when :json
|
|
239
|
+
File.write(file_path, JSON.pretty_generate(data))
|
|
240
|
+
when :yaml
|
|
241
|
+
File.write(file_path, YAML.dump(data))
|
|
242
|
+
else
|
|
243
|
+
raise ArgumentError, "Unsupported format: #{format}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
log_info("Element state tracking data exported to #{file_path}")
|
|
247
|
+
file_path
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
def generate_element_id(element, name)
|
|
253
|
+
if name
|
|
254
|
+
name.to_s
|
|
255
|
+
else
|
|
256
|
+
# Try to generate meaningful ID from element
|
|
257
|
+
begin
|
|
258
|
+
attrs = []
|
|
259
|
+
attrs << element.attribute(:id) if element.attribute(:id)
|
|
260
|
+
attrs << element.attribute(:class) if element.attribute(:class)
|
|
261
|
+
attrs << element.tag_name if element.respond_to?(:tag_name)
|
|
262
|
+
|
|
263
|
+
id = attrs.any? ? attrs.join('_') : "element_#{element.object_id}"
|
|
264
|
+
id.gsub(/[^\w\-_]/, '_')
|
|
265
|
+
rescue StandardError
|
|
266
|
+
"element_#{element.object_id}"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def capture_element_state(element)
|
|
272
|
+
state = {
|
|
273
|
+
captured_at: Time.now,
|
|
274
|
+
exists: false,
|
|
275
|
+
displayed: false,
|
|
276
|
+
enabled: false,
|
|
277
|
+
selected: false,
|
|
278
|
+
text: nil,
|
|
279
|
+
attributes: {},
|
|
280
|
+
location: nil,
|
|
281
|
+
size: nil,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
state[:exists] = element.respond_to?(:displayed?) || element.respond_to?(:enabled?)
|
|
286
|
+
|
|
287
|
+
if state[:exists]
|
|
288
|
+
state[:displayed] = element.displayed? if element.respond_to?(:displayed?)
|
|
289
|
+
state[:enabled] = element.enabled? if element.respond_to?(:enabled?)
|
|
290
|
+
state[:selected] = element.selected? if element.respond_to?(:selected?)
|
|
291
|
+
state[:text] = element.text if element.respond_to?(:text)
|
|
292
|
+
|
|
293
|
+
# Capture common attributes
|
|
294
|
+
if element.respond_to?(:attribute)
|
|
295
|
+
%w[id class name type value placeholder].each do |attr|
|
|
296
|
+
value = element.attribute(attr.to_sym) || element.attribute(attr)
|
|
297
|
+
state[:attributes][attr.to_sym] = value if value
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Capture location and size
|
|
302
|
+
state[:location] = element.location if element.respond_to?(:location)
|
|
303
|
+
|
|
304
|
+
state[:size] = element.size if element.respond_to?(:size)
|
|
305
|
+
end
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
state[:error] = e.message
|
|
308
|
+
state[:exists] = false
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
state
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def state_changed?(old_state, new_state)
|
|
315
|
+
# Compare relevant state properties, excluding timestamp fields
|
|
316
|
+
comparison_keys = %i[exists displayed enabled selected text attributes location size]
|
|
317
|
+
|
|
318
|
+
comparison_keys.any? { |key| old_state[key] != new_state[key] }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def calculate_changes(old_state, new_state)
|
|
322
|
+
changes = {}
|
|
323
|
+
|
|
324
|
+
old_state.each do |key, old_value|
|
|
325
|
+
new_value = new_state[key]
|
|
326
|
+
|
|
327
|
+
next unless old_value != new_value
|
|
328
|
+
|
|
329
|
+
changes[key] = {
|
|
330
|
+
from: old_value,
|
|
331
|
+
to: new_value,
|
|
332
|
+
}
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
changes
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def most_active_elements(limit)
|
|
339
|
+
@tracked_elements
|
|
340
|
+
.map { |_id, tracked| [tracked[:name], tracked[:change_count]] }
|
|
341
|
+
.sort_by { |_, count| -count }
|
|
342
|
+
.first(limit)
|
|
343
|
+
.to_h
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def notify_observers(event_type, *args)
|
|
347
|
+
@observers.each do |observer|
|
|
348
|
+
observer.call(event_type, *args)
|
|
349
|
+
rescue StandardError => e
|
|
350
|
+
log_error("Observer error: #{e.message}")
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def serialize_tracked_elements
|
|
355
|
+
@tracked_elements.transform_values do |tracked|
|
|
356
|
+
{
|
|
357
|
+
name: tracked[:name],
|
|
358
|
+
context: tracked[:context],
|
|
359
|
+
first_seen: tracked[:first_seen],
|
|
360
|
+
last_updated: tracked[:last_updated],
|
|
361
|
+
current_state: tracked[:current_state],
|
|
362
|
+
change_count: tracked[:change_count],
|
|
363
|
+
previous_states_count: tracked[:previous_states].size,
|
|
364
|
+
}
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Element state monitoring mixin
|
|
370
|
+
module Monitoring
|
|
371
|
+
def self.included(base)
|
|
372
|
+
base.extend(ClassMethods)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Class methods for element state tracking
|
|
376
|
+
module ClassMethods
|
|
377
|
+
def track_state_changes
|
|
378
|
+
@state_tracking_enabled = true
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def state_tracking_enabled?
|
|
382
|
+
@state_tracking_enabled ||= false
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Track this element's state
|
|
387
|
+
def track_state(name: nil, context: {})
|
|
388
|
+
return unless self.class.state_tracking_enabled?
|
|
389
|
+
|
|
390
|
+
::Appom::ElementState.tracker.track_element(self, name: name, context: context)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Update state and return changes
|
|
394
|
+
def update_state
|
|
395
|
+
return unless self.class.state_tracking_enabled?
|
|
396
|
+
|
|
397
|
+
::Appom::ElementState.tracker.update_element_state(element_id) if defined?(@element_id)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Get current state
|
|
401
|
+
def current_state
|
|
402
|
+
return unless self.class.state_tracking_enabled?
|
|
403
|
+
|
|
404
|
+
::Appom::ElementState.tracker.element_state(element_id) if defined?(@element_id)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Wait for state change
|
|
408
|
+
def wait_for_state_change(**args)
|
|
409
|
+
return unless self.class.state_tracking_enabled?
|
|
410
|
+
|
|
411
|
+
::Appom::ElementState.tracker.wait_for_state_change(element_id, args.empty? ? {} : args) if defined?(@element_id)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
private
|
|
415
|
+
|
|
416
|
+
def element_id
|
|
417
|
+
@element_id ||= ::Appom::ElementState.tracker.generate_element_id(self, nil)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def initialize(*)
|
|
421
|
+
super if defined?(super)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Global state tracker
|
|
426
|
+
class << self
|
|
427
|
+
attr_writer :tracker
|
|
428
|
+
|
|
429
|
+
def tracker
|
|
430
|
+
@tracker ||= Tracker.new
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Convenience methods
|
|
434
|
+
def track_element(element, **)
|
|
435
|
+
tracker.track_element(element, **)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def element_state(element_id)
|
|
439
|
+
tracker.element_state(element_id)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def wait_for_state_change(element_id, **)
|
|
443
|
+
tracker.wait_for_state_change(element_id, **)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def tracking_summary
|
|
447
|
+
tracker.tracking_summary
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def clear!
|
|
451
|
+
tracker.clear!
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def export_data(**)
|
|
455
|
+
tracker.export_tracking_data(**)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Element validation module for Appom automation framework
|
|
4
|
+
# Provides validation for element definitions and arguments
|
|
5
|
+
module Appom::ElementValidation
|
|
6
|
+
# Boolean values for validation
|
|
7
|
+
BOOLEAN_VALUES = [true, false].freeze
|
|
8
|
+
|
|
9
|
+
# Valid Appium locator strategies
|
|
10
|
+
VALID_LOCATOR_STRATEGIES = [
|
|
11
|
+
:accessibility_id,
|
|
12
|
+
:android_uiautomator,
|
|
13
|
+
:android_viewtag,
|
|
14
|
+
:android_data_matcher,
|
|
15
|
+
:android_view_matcher,
|
|
16
|
+
:ios_predicate,
|
|
17
|
+
:ios_uiautomation,
|
|
18
|
+
:ios_class_chain,
|
|
19
|
+
:class_name,
|
|
20
|
+
:class, # Alias for class_name
|
|
21
|
+
:css,
|
|
22
|
+
:id,
|
|
23
|
+
:name,
|
|
24
|
+
:link_text,
|
|
25
|
+
:partial_link_text,
|
|
26
|
+
:tag_name,
|
|
27
|
+
:xpath,
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Validate element definition arguments
|
|
32
|
+
def validate_element_args(name, *find_args)
|
|
33
|
+
validate_element_name(name)
|
|
34
|
+
validate_find_arguments(find_args)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Validate section definition arguments
|
|
38
|
+
def validate_section_args(name, *args)
|
|
39
|
+
validate_element_name(name)
|
|
40
|
+
|
|
41
|
+
# Extract section class and find args
|
|
42
|
+
find_args = args.dup
|
|
43
|
+
|
|
44
|
+
if find_args.first.is_a?(Class)
|
|
45
|
+
section_class = find_args.shift
|
|
46
|
+
validate_section_class(section_class)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
validate_find_arguments(find_args) unless find_args.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def validate_element_name(name)
|
|
55
|
+
raise Appom::ConfigurationError.new('element_name', name, 'Element name must be a symbol') unless name.is_a?(Symbol)
|
|
56
|
+
|
|
57
|
+
raise Appom::ConfigurationError.new('element_name', name, 'Element name cannot be empty') if name.to_s.empty?
|
|
58
|
+
|
|
59
|
+
# Check for reserved method names
|
|
60
|
+
reserved_methods = %i[page parent root_element initialize]
|
|
61
|
+
return unless reserved_methods.include?(name)
|
|
62
|
+
|
|
63
|
+
raise Appom::ConfigurationError.new('element_name', name, 'Element name conflicts with reserved method')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_find_arguments(find_args)
|
|
67
|
+
return if find_args.empty? # Allow empty args for some cases
|
|
68
|
+
|
|
69
|
+
flattened_args = find_args.flatten
|
|
70
|
+
return if flattened_args.empty?
|
|
71
|
+
|
|
72
|
+
# First argument should be a locator strategy (symbol)
|
|
73
|
+
locator_strategy = flattened_args.first
|
|
74
|
+
unless locator_strategy.is_a?(Symbol)
|
|
75
|
+
raise Appom::ConfigurationError.new('locator_strategy', locator_strategy,
|
|
76
|
+
'First argument must be a symbol representing locator strategy',)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
unless VALID_LOCATOR_STRATEGIES.include?(locator_strategy)
|
|
80
|
+
valid_strategies = VALID_LOCATOR_STRATEGIES.map(&:to_s).join(', ')
|
|
81
|
+
raise Appom::ConfigurationError.new('locator_strategy', locator_strategy,
|
|
82
|
+
"Invalid locator strategy. Valid strategies: #{valid_strategies}",)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Second argument should be the locator value (string)
|
|
86
|
+
if flattened_args.size > 1
|
|
87
|
+
locator_value = flattened_args[1]
|
|
88
|
+
unless locator_value.is_a?(String) || locator_value.is_a?(Hash)
|
|
89
|
+
raise Appom::ConfigurationError.new('locator_value', locator_value,
|
|
90
|
+
'Locator value must be a string or hash',)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if locator_value.is_a?(String) && locator_value.empty?
|
|
94
|
+
raise Appom::ConfigurationError.new('locator_value', locator_value,
|
|
95
|
+
'Locator value cannot be empty',)
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
raise Appom::ConfigurationError.new('find_arguments', find_args,
|
|
99
|
+
'Missing locator value. Expected format: :strategy, "value"',)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Validate optional hash arguments
|
|
103
|
+
flattened_args[2..]&.each do |arg|
|
|
104
|
+
validate_element_options(arg) if arg.is_a?(Hash)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def validate_element_options(options)
|
|
109
|
+
valid_options = %i[text visible enabled timeout]
|
|
110
|
+
|
|
111
|
+
options.each do |key, value|
|
|
112
|
+
unless valid_options.include?(key)
|
|
113
|
+
valid_keys = valid_options.map(&:to_s).join(', ')
|
|
114
|
+
raise Appom::ConfigurationError.new('element_option', key,
|
|
115
|
+
"Invalid option. Valid options: #{valid_keys}",)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
case key
|
|
119
|
+
when :text
|
|
120
|
+
raise Appom::ConfigurationError.new('text_option', value, 'Text option must be a string') unless value.is_a?(String)
|
|
121
|
+
when :visible, :enabled
|
|
122
|
+
raise Appom::ConfigurationError.new("#{key}_option", value, "#{key.capitalize} option must be true or false") unless BOOLEAN_VALUES.include?(value)
|
|
123
|
+
when :timeout
|
|
124
|
+
raise Appom::ConfigurationError.new('timeout_option', value, 'Timeout must be a positive number') unless value.is_a?(Numeric) && value.positive?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_section_class(klass)
|
|
130
|
+
raise Appom::ConfigurationError.new('section_class', klass, 'Section class must be a Class') unless klass.is_a?(Class)
|
|
131
|
+
|
|
132
|
+
return if klass.ancestors.include?(Appom::Section)
|
|
133
|
+
|
|
134
|
+
raise Appom::ConfigurationError.new('section_class', klass,
|
|
135
|
+
'Section class must inherit from Appom::Section',)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|