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.
@@ -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
data/lib/appom/version.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Appom
2
- VERSION = '1.4.0'.freeze
4
+ # Current version of the Appom gem
5
+ VERSION = '2.0.0'
3
6
  end