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,455 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Smart waiting functionality for Appom automation framework
|
|
4
|
+
# Provides intelligent wait conditions and strategies
|
|
5
|
+
module Appom::SmartWait
|
|
6
|
+
DEFAULT_INTERVAL = 0.25
|
|
7
|
+
|
|
8
|
+
# Enhanced wait conditions beyond basic timeout/interval
|
|
9
|
+
class WaitConditions
|
|
10
|
+
class << self
|
|
11
|
+
# Wait for element to be visible/displayed
|
|
12
|
+
def element_visible(element)
|
|
13
|
+
lambda do
|
|
14
|
+
element.displayed?
|
|
15
|
+
rescue StandardError
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Wait for element to be enabled
|
|
21
|
+
def element_enabled(element)
|
|
22
|
+
lambda do
|
|
23
|
+
element.enabled?
|
|
24
|
+
rescue StandardError
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Wait for element to be clickable (visible and enabled)
|
|
30
|
+
def element_clickable(element)
|
|
31
|
+
lambda do
|
|
32
|
+
element.displayed? && element.enabled?
|
|
33
|
+
rescue StandardError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Wait for text to be present
|
|
39
|
+
def text_present(element, expected_text)
|
|
40
|
+
lambda do
|
|
41
|
+
if expected_text.is_a?(Regexp)
|
|
42
|
+
!(element.text =~ expected_text).nil?
|
|
43
|
+
else
|
|
44
|
+
element.text.include?(expected_text.to_s)
|
|
45
|
+
end
|
|
46
|
+
rescue StandardError
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Wait for text to change from initial value
|
|
52
|
+
def text_changed(element, initial_text)
|
|
53
|
+
lambda do
|
|
54
|
+
element.text != initial_text
|
|
55
|
+
rescue StandardError
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Wait for attribute to contain value
|
|
61
|
+
def attribute_contains(element, attribute_name, expected_value)
|
|
62
|
+
lambda do
|
|
63
|
+
(element.attribute(attribute_name) || '').include?(expected_value.to_s)
|
|
64
|
+
rescue StandardError
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Create custom condition from block
|
|
70
|
+
def custom_condition(&block)
|
|
71
|
+
block
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Combine conditions with OR logic
|
|
75
|
+
def any_condition(conditions)
|
|
76
|
+
-> { conditions.any?(&:call) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Combine conditions with AND logic
|
|
80
|
+
def all_conditions(conditions)
|
|
81
|
+
-> { conditions.all?(&:call) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Wait for element to be invisible
|
|
85
|
+
def element_invisible(element)
|
|
86
|
+
lambda do
|
|
87
|
+
!element.displayed?
|
|
88
|
+
rescue StandardError
|
|
89
|
+
# Element not found means it's invisible
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Wait for element attribute to have specific value
|
|
95
|
+
def attribute_equals(element, attribute_name, expected_value)
|
|
96
|
+
lambda do
|
|
97
|
+
actual_value = element.attribute(attribute_name)
|
|
98
|
+
actual_value == expected_value
|
|
99
|
+
rescue StandardError
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Condition for clickable element (used by factory methods)
|
|
105
|
+
def clickable(element)
|
|
106
|
+
lambda do
|
|
107
|
+
return element.displayed? && element.enabled? if element
|
|
108
|
+
|
|
109
|
+
# For factory method usage without element instance
|
|
110
|
+
false
|
|
111
|
+
rescue StandardError
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Condition for text matching (used by factory methods)
|
|
117
|
+
def text_matches(expected_text, exact: false)
|
|
118
|
+
lambda do |element|
|
|
119
|
+
actual_text = element.text
|
|
120
|
+
if exact
|
|
121
|
+
actual_text == expected_text.to_s
|
|
122
|
+
else
|
|
123
|
+
actual_text.include?(expected_text.to_s)
|
|
124
|
+
end
|
|
125
|
+
rescue StandardError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Condition for invisible element (used by factory methods)
|
|
131
|
+
def invisible
|
|
132
|
+
lambda do |element|
|
|
133
|
+
!element.displayed?
|
|
134
|
+
rescue StandardError
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Condition for element count (used by factory methods)
|
|
140
|
+
def count_equals(expected_count)
|
|
141
|
+
lambda do |elements|
|
|
142
|
+
elements.length == expected_count
|
|
143
|
+
rescue StandardError
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Enhanced Wait class with smart conditions
|
|
151
|
+
class ConditionalWait < Appom::Wait
|
|
152
|
+
attr_reader :condition, :condition_description
|
|
153
|
+
|
|
154
|
+
def initialize(timeout: Appom.max_wait_time, interval: DEFAULT_INTERVAL, condition: nil,
|
|
155
|
+
description: nil)
|
|
156
|
+
super(timeout: timeout, interval: interval)
|
|
157
|
+
@condition = condition
|
|
158
|
+
@condition_description = description || 'custom condition'
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Wait for element with specific condition
|
|
162
|
+
def for_element(*find_args, &condition_block)
|
|
163
|
+
condition = condition_block || @condition
|
|
164
|
+
raise Appom::ArgumentError, 'No condition provided' unless condition
|
|
165
|
+
|
|
166
|
+
log_wait_start(@condition_description, @timeout)
|
|
167
|
+
start_time = Time.now
|
|
168
|
+
|
|
169
|
+
until_with_condition do
|
|
170
|
+
element = _find_element(*find_args)
|
|
171
|
+
if condition.call(element)
|
|
172
|
+
duration = Time.now - start_time
|
|
173
|
+
log_wait_end(@condition_description, duration.round(3), success: true)
|
|
174
|
+
return element
|
|
175
|
+
end
|
|
176
|
+
false
|
|
177
|
+
rescue StandardError
|
|
178
|
+
# Continue waiting for condition even if element not found initially
|
|
179
|
+
false
|
|
180
|
+
end
|
|
181
|
+
rescue Appom::WaitError
|
|
182
|
+
duration = Time.now - start_time
|
|
183
|
+
log_wait_end(@condition_description, duration.round(3), success: false)
|
|
184
|
+
raise Appom::ElementNotFoundError.new(
|
|
185
|
+
"#{find_args.join(', ')} with condition: #{@condition_description}", @timeout,
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Wait for elements collection with condition
|
|
190
|
+
def for_elements(*find_args, &condition_block)
|
|
191
|
+
condition = condition_block || @condition
|
|
192
|
+
raise Appom::ArgumentError, 'No condition provided' unless condition
|
|
193
|
+
|
|
194
|
+
log_wait_start("#{@condition_description} (collection)", @timeout)
|
|
195
|
+
start_time = Time.now
|
|
196
|
+
|
|
197
|
+
until_with_condition do
|
|
198
|
+
elements = _find_elements(*find_args)
|
|
199
|
+
if condition.call(elements)
|
|
200
|
+
duration = Time.now - start_time
|
|
201
|
+
log_wait_end("#{@condition_description} (collection)", duration.round(3), success: true)
|
|
202
|
+
return elements
|
|
203
|
+
end
|
|
204
|
+
false
|
|
205
|
+
rescue StandardError
|
|
206
|
+
false
|
|
207
|
+
end
|
|
208
|
+
rescue Appom::WaitError
|
|
209
|
+
duration = Time.now - start_time
|
|
210
|
+
log_wait_end("#{@condition_description} (collection)", duration.round(3), success: false)
|
|
211
|
+
raise Appom::ElementNotFoundError.new(
|
|
212
|
+
"#{find_args.join(', ')} collection with condition: #{@condition_description}", @timeout,
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Wait for any of multiple conditions to be met
|
|
217
|
+
def for_any_condition(*conditions_with_elements)
|
|
218
|
+
raise ArgumentError, 'No conditions provided' unless conditions_with_elements.any?
|
|
219
|
+
|
|
220
|
+
log_wait_start("any of #{conditions_with_elements.size} conditions", @timeout)
|
|
221
|
+
start_time = Time.now
|
|
222
|
+
|
|
223
|
+
until_with_condition do
|
|
224
|
+
conditions_with_elements.each_with_index do |(find_args, condition), index|
|
|
225
|
+
element = _find_element(*find_args)
|
|
226
|
+
if condition.call(element)
|
|
227
|
+
duration = Time.now - start_time
|
|
228
|
+
log_wait_end("condition #{index + 1}", duration.round(3), success: true)
|
|
229
|
+
return { index: index, element: element, find_args: find_args }
|
|
230
|
+
end
|
|
231
|
+
rescue StandardError
|
|
232
|
+
# Continue to next condition
|
|
233
|
+
end
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
rescue Appom::WaitError
|
|
237
|
+
duration = Time.now - start_time
|
|
238
|
+
log_wait_end('any condition', duration.round(3), success: false)
|
|
239
|
+
descriptions = conditions_with_elements.map.with_index do |(find_args, _), i|
|
|
240
|
+
"#{i + 1}: #{find_args.join(', ')}"
|
|
241
|
+
end
|
|
242
|
+
raise Appom::ElementNotFoundError.new("any of: #{descriptions.join('; ')}", @timeout)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Wait until condition becomes true
|
|
246
|
+
def wait_until(condition, timeout: @timeout, interval: @interval, backoff_factor: nil,
|
|
247
|
+
max_interval: nil)
|
|
248
|
+
start_time = Time.now
|
|
249
|
+
last_error = nil
|
|
250
|
+
current_interval = interval
|
|
251
|
+
|
|
252
|
+
loop do
|
|
253
|
+
result = evaluate_condition_safely(condition, last_error)
|
|
254
|
+
return true if result[:success]
|
|
255
|
+
|
|
256
|
+
last_error = result[:error]
|
|
257
|
+
|
|
258
|
+
check_timeout_reached(start_time, timeout, last_error)
|
|
259
|
+
|
|
260
|
+
sleep current_interval
|
|
261
|
+
current_interval = apply_backoff(current_interval, backoff_factor, max_interval)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Wait while condition remains true (until it becomes false)
|
|
266
|
+
def wait_while(condition, timeout: @timeout, interval: @interval) # rubocop:disable Naming/PredicateMethod
|
|
267
|
+
start_time = Time.now
|
|
268
|
+
while condition.call
|
|
269
|
+
raise Appom::TimeoutError, "Condition remained true for #{timeout}s" if Time.now - start_time > timeout
|
|
270
|
+
|
|
271
|
+
sleep interval
|
|
272
|
+
end
|
|
273
|
+
true
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Wait for condition to remain stable for specified duration
|
|
277
|
+
def wait_for_stable_condition(condition, stable_duration: 1.0, timeout: @timeout,
|
|
278
|
+
interval: @interval)
|
|
279
|
+
start_time = Time.now
|
|
280
|
+
stable_start = nil
|
|
281
|
+
|
|
282
|
+
loop do
|
|
283
|
+
begin
|
|
284
|
+
if condition.call
|
|
285
|
+
stable_start ||= Time.now
|
|
286
|
+
return true if Time.now - stable_start >= stable_duration
|
|
287
|
+
else
|
|
288
|
+
stable_start = nil
|
|
289
|
+
end
|
|
290
|
+
rescue StandardError
|
|
291
|
+
stable_start = nil
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if Time.now - start_time > timeout
|
|
295
|
+
raise Appom::TimeoutError,
|
|
296
|
+
"Condition did not remain stable for #{stable_duration}s within #{timeout}s"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
sleep interval
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
private
|
|
304
|
+
|
|
305
|
+
def evaluate_condition_safely(condition, last_error)
|
|
306
|
+
success = condition.call
|
|
307
|
+
{ success: success, error: last_error }
|
|
308
|
+
rescue StandardError => e
|
|
309
|
+
{ success: false, error: e }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def check_timeout_reached(start_time, timeout, last_error)
|
|
313
|
+
return unless Time.now - start_time > timeout
|
|
314
|
+
raise last_error if last_error
|
|
315
|
+
|
|
316
|
+
raise Appom::TimeoutError, "Condition not met within #{timeout}s"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def apply_backoff(current_interval, backoff_factor, max_interval)
|
|
320
|
+
if backoff_factor && max_interval
|
|
321
|
+
[current_interval * backoff_factor, max_interval].min
|
|
322
|
+
else
|
|
323
|
+
current_interval
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Wait until condition becomes true with exponential backoff
|
|
328
|
+
def wait_until_with_backoff(condition, timeout: @timeout, interval: @interval,
|
|
329
|
+
backoff_factor: 2, max_interval: 5)
|
|
330
|
+
wait_until(condition, timeout: timeout, interval: interval, backoff_factor: backoff_factor,
|
|
331
|
+
max_interval: max_interval,)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def until_with_condition
|
|
335
|
+
timeout = @timeout || 5
|
|
336
|
+
start_time = Time.now
|
|
337
|
+
loop do
|
|
338
|
+
return true if yield
|
|
339
|
+
raise Appom::WaitError, 'Timeout waiting for condition' if (Time.now - start_time) > timeout
|
|
340
|
+
|
|
341
|
+
sleep 0.1
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def _find_element(*find_args)
|
|
346
|
+
# Use the same finding logic as ElementFinder
|
|
347
|
+
if respond_to?(:page)
|
|
348
|
+
page.find_element(*find_args)
|
|
349
|
+
else
|
|
350
|
+
Appom.driver.find_element(*find_args)
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def _find_elements(*find_args)
|
|
355
|
+
if respond_to?(:page)
|
|
356
|
+
page.find_elements(*find_args)
|
|
357
|
+
else
|
|
358
|
+
Appom.driver.find_elements(*find_args)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Factory methods for creating conditional waits
|
|
364
|
+
class << self
|
|
365
|
+
# Create a wait with clickable condition
|
|
366
|
+
def until_clickable(*find_args, timeout: Appom.max_wait_time)
|
|
367
|
+
wait = ConditionalWait.new(
|
|
368
|
+
timeout: timeout,
|
|
369
|
+
condition: WaitConditions.clickable(nil),
|
|
370
|
+
description: 'clickable',
|
|
371
|
+
)
|
|
372
|
+
wait.for_element(*find_args)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Create a wait for specific text
|
|
376
|
+
def until_text_matches(*find_args, text:, exact: false, timeout: Appom.max_wait_time)
|
|
377
|
+
wait = ConditionalWait.new(
|
|
378
|
+
timeout: timeout,
|
|
379
|
+
condition: WaitConditions.text_matches(text, exact: exact),
|
|
380
|
+
description: "text #{exact ? 'equals' : 'matches'} '#{text}'",
|
|
381
|
+
)
|
|
382
|
+
wait.for_element(*find_args)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Create a wait for invisible element
|
|
386
|
+
def until_invisible(*find_args, timeout: Appom.max_wait_time)
|
|
387
|
+
wait = ConditionalWait.new(
|
|
388
|
+
timeout: timeout,
|
|
389
|
+
condition: WaitConditions.invisible,
|
|
390
|
+
description: 'invisible',
|
|
391
|
+
)
|
|
392
|
+
wait.for_element(*find_args)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Create a wait for element count
|
|
396
|
+
def until_count_equals(*find_args, count:, timeout: Appom.max_wait_time)
|
|
397
|
+
wait = ConditionalWait.new(
|
|
398
|
+
timeout: timeout,
|
|
399
|
+
condition: WaitConditions.count_equals(count),
|
|
400
|
+
description: "count equals #{count}",
|
|
401
|
+
)
|
|
402
|
+
wait.for_elements(*find_args)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Create a wait for custom condition
|
|
406
|
+
def until_condition(*find_args, timeout: Appom.max_wait_time,
|
|
407
|
+
description: 'custom condition', &condition_block)
|
|
408
|
+
wait = ConditionalWait.new(
|
|
409
|
+
timeout: timeout,
|
|
410
|
+
condition: condition_block,
|
|
411
|
+
description: description,
|
|
412
|
+
)
|
|
413
|
+
wait.for_element(*find_args, &condition_block)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Module-level convenience methods
|
|
418
|
+
module_function
|
|
419
|
+
|
|
420
|
+
def wait_until(condition, timeout: Appom.max_wait_time, backoff_factor: nil, max_interval: nil)
|
|
421
|
+
wait = ConditionalWait.new(timeout: timeout)
|
|
422
|
+
if backoff_factor && max_interval
|
|
423
|
+
wait.wait_until_with_backoff(condition, timeout: timeout, backoff_factor: backoff_factor,
|
|
424
|
+
max_interval: max_interval,)
|
|
425
|
+
else
|
|
426
|
+
wait.wait_until(condition, timeout: timeout)
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def wait_for_element_visible(element, timeout: Appom.max_wait_time)
|
|
431
|
+
condition = WaitConditions.element_visible(element)
|
|
432
|
+
wait_until(condition, timeout: timeout)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def wait_for_element_clickable(element, timeout: Appom.max_wait_time)
|
|
436
|
+
condition = WaitConditions.element_clickable(element)
|
|
437
|
+
wait_until(condition, timeout: timeout)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def wait_for_text_present(element, text, timeout: Appom.max_wait_time)
|
|
441
|
+
condition = WaitConditions.text_present(element, text)
|
|
442
|
+
wait_until(condition, timeout: timeout)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def wait_for_text_to_change(element, initial_text, timeout: Appom.max_wait_time)
|
|
446
|
+
condition = WaitConditions.text_changed(element, initial_text)
|
|
447
|
+
wait_until(condition, timeout: timeout)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def wait_for_stable_element(element, timeout: Appom.max_wait_time, stable_duration: 1.0)
|
|
451
|
+
condition = -> { element.displayed? && element.enabled? }
|
|
452
|
+
wait = ConditionalWait.new(timeout: timeout)
|
|
453
|
+
wait.wait_for_stable_condition(condition, stable_duration: stable_duration, timeout: timeout)
|
|
454
|
+
end
|
|
455
|
+
end
|