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,372 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
# Element caching system for Appom automation framework
|
|
6
|
+
# Provides intelligent element caching with TTL and LRU eviction policies
|
|
7
|
+
module Appom::ElementCache
|
|
8
|
+
# Element caching to improve performance for frequently accessed elements
|
|
9
|
+
class Cache
|
|
10
|
+
include Appom::Logging
|
|
11
|
+
|
|
12
|
+
attr_reader :max_size, :ttl
|
|
13
|
+
|
|
14
|
+
def initialize(max_size: 100, ttl: 300)
|
|
15
|
+
@cache = {}
|
|
16
|
+
@access_times = {}
|
|
17
|
+
@max_size = max_size
|
|
18
|
+
@ttl = ttl # Time to live in seconds
|
|
19
|
+
@stats = { hits: 0, misses: 0, evictions: 0, stores: 0, clears: 0, expirations: 0 }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Store element in cache with strategy and value
|
|
23
|
+
def store(strategy, value, element)
|
|
24
|
+
cache_key = generate_key(strategy, value)
|
|
25
|
+
store_in_cache(cache_key, element)
|
|
26
|
+
@stats[:stores] += 1
|
|
27
|
+
cache_key
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get element from cache by key
|
|
31
|
+
def get(cache_key)
|
|
32
|
+
unless @cache.key?(cache_key)
|
|
33
|
+
@stats[:misses] += 1
|
|
34
|
+
return nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
element, timestamp = @cache[cache_key]
|
|
38
|
+
|
|
39
|
+
# Check TTL
|
|
40
|
+
if Time.now - timestamp > @ttl
|
|
41
|
+
@cache.delete(cache_key)
|
|
42
|
+
@access_times.delete(cache_key)
|
|
43
|
+
@stats[:expirations] ||= 0
|
|
44
|
+
@stats[:expirations] += 1
|
|
45
|
+
@stats[:misses] += 1
|
|
46
|
+
return nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if element is still valid
|
|
50
|
+
unless valid_element?(element)
|
|
51
|
+
@cache.delete(cache_key)
|
|
52
|
+
@access_times.delete(cache_key)
|
|
53
|
+
@stats[:misses] += 1
|
|
54
|
+
return nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Update access time and refresh TTL
|
|
58
|
+
current_time = Time.now
|
|
59
|
+
@cache[cache_key] = [element, current_time]
|
|
60
|
+
@access_times[cache_key] = current_time
|
|
61
|
+
@stats[:hits] += 1
|
|
62
|
+
element
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if key exists in cache
|
|
66
|
+
def hit?(cache_key)
|
|
67
|
+
return false unless @cache.key?(cache_key)
|
|
68
|
+
|
|
69
|
+
element, timestamp = @cache[cache_key]
|
|
70
|
+
|
|
71
|
+
# Check TTL without updating stats
|
|
72
|
+
return false if Time.now - timestamp > @ttl
|
|
73
|
+
|
|
74
|
+
valid_element?(element)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get cache size
|
|
78
|
+
def size
|
|
79
|
+
@cache.size
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get element from cache or find and cache it
|
|
83
|
+
def get_or_find(*find_args)
|
|
84
|
+
cache_key = generate_key(find_args)
|
|
85
|
+
|
|
86
|
+
if (cached_element = get(cache_key))
|
|
87
|
+
log_debug("Cache HIT for #{find_args.join(', ')}")
|
|
88
|
+
return cached_element
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@stats[:misses] += 1
|
|
92
|
+
log_debug("Cache MISS for #{find_args.join(', ')}")
|
|
93
|
+
|
|
94
|
+
# Find element and cache it
|
|
95
|
+
element = yield if block_given?
|
|
96
|
+
store_in_cache(cache_key, element) if element
|
|
97
|
+
element
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Invalidate specific element
|
|
101
|
+
def invalidate(*find_args) # rubocop:disable Naming/PredicateMethod
|
|
102
|
+
cache_key = generate_key(find_args)
|
|
103
|
+
if @cache.delete(cache_key)
|
|
104
|
+
@access_times.delete(cache_key)
|
|
105
|
+
log_debug("Invalidated cache for #{find_args.join(', ')}")
|
|
106
|
+
true
|
|
107
|
+
else
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Clear all cached elements
|
|
113
|
+
def clear
|
|
114
|
+
@cache.clear
|
|
115
|
+
@access_times.clear
|
|
116
|
+
@stats[:clears] += 1
|
|
117
|
+
log_info('Element cache cleared')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Clear all cached elements and reset statistics
|
|
121
|
+
def reset
|
|
122
|
+
@cache.clear
|
|
123
|
+
@access_times.clear
|
|
124
|
+
@stats = { hits: 0, misses: 0, evictions: 0, stores: 0, clears: 0, expirations: 0 }
|
|
125
|
+
log_info('Element cache reset')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Get cache statistics
|
|
129
|
+
def statistics
|
|
130
|
+
{
|
|
131
|
+
size: @cache.size,
|
|
132
|
+
max_size: @max_size,
|
|
133
|
+
hit_rate: calculate_hit_rate,
|
|
134
|
+
**@stats,
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Alias for backward compatibility
|
|
139
|
+
def stats
|
|
140
|
+
statistics
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Check if element is still valid (exists and is stale)
|
|
144
|
+
def valid_element?(element)
|
|
145
|
+
return false unless element
|
|
146
|
+
return true unless element.respond_to?(:displayed?)
|
|
147
|
+
|
|
148
|
+
begin
|
|
149
|
+
# Try to access a property to check if element is stale
|
|
150
|
+
element.displayed?
|
|
151
|
+
true
|
|
152
|
+
rescue StandardError
|
|
153
|
+
# Element is stale or invalid
|
|
154
|
+
false
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def generate_key(*args)
|
|
159
|
+
# Handle both old and new calling patterns
|
|
160
|
+
find_args = if args.length == 1 && args[0].is_a?(Array)
|
|
161
|
+
# Old pattern: generate_key([strategy, value])
|
|
162
|
+
args[0]
|
|
163
|
+
else
|
|
164
|
+
# New pattern: generate_key(strategy, value)
|
|
165
|
+
args
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Create consistent cache key from find arguments
|
|
169
|
+
Digest::MD5.hexdigest(find_args.to_s)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def get_from_cache(cache_key)
|
|
175
|
+
get(cache_key)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def store_in_cache(cache_key, element)
|
|
179
|
+
# Clean up expired entries first
|
|
180
|
+
cleanup_expired
|
|
181
|
+
|
|
182
|
+
# Evict oldest if at capacity
|
|
183
|
+
evict_lru if @cache.size >= @max_size
|
|
184
|
+
|
|
185
|
+
timestamp = Time.now
|
|
186
|
+
@cache[cache_key] = [element, timestamp]
|
|
187
|
+
@access_times[cache_key] = timestamp
|
|
188
|
+
|
|
189
|
+
log_debug("Cached element with key #{cache_key[0..8]}...")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def evict_lru
|
|
193
|
+
# Remove least recently used item
|
|
194
|
+
oldest_key = @access_times.min_by { |_k, v| v }&.first
|
|
195
|
+
return unless oldest_key
|
|
196
|
+
|
|
197
|
+
@cache.delete(oldest_key)
|
|
198
|
+
@access_times.delete(oldest_key)
|
|
199
|
+
@stats[:evictions] += 1
|
|
200
|
+
log_debug('Evicted LRU element from cache')
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def cleanup_expired
|
|
204
|
+
current_time = Time.now
|
|
205
|
+
expired_keys = []
|
|
206
|
+
|
|
207
|
+
@cache.each do |key, (_element, timestamp)|
|
|
208
|
+
expired_keys << key if current_time - timestamp > @ttl
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
expired_keys.each do |key|
|
|
212
|
+
@cache.delete(key)
|
|
213
|
+
@access_times.delete(key)
|
|
214
|
+
@stats[:expirations] ||= 0
|
|
215
|
+
@stats[:expirations] += 1
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
return unless expired_keys.any?
|
|
219
|
+
|
|
220
|
+
log_debug("Cleaned up #{expired_keys.size} expired elements from cache")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def calculate_hit_rate
|
|
224
|
+
total = @stats[:hits] + @stats[:misses]
|
|
225
|
+
return 0.0 if total.zero?
|
|
226
|
+
|
|
227
|
+
(@stats[:hits].to_f / total * 100).round(2)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Cache-aware element finder mixin
|
|
232
|
+
module CacheAwareFinder
|
|
233
|
+
def self.included(klass)
|
|
234
|
+
# Don't alias if methods don't exist yet
|
|
235
|
+
klass.send(:alias_method, :original_find_element, :find_element) if klass.method_defined?(:find_element)
|
|
236
|
+
return unless klass.method_defined?(:find_elements)
|
|
237
|
+
|
|
238
|
+
klass.send(:alias_method, :original_find_elements, :find_elements)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def find_element(strategy, locator, use_cache: true)
|
|
242
|
+
# If not using cache or caching is disabled, use standard find method
|
|
243
|
+
return _find_without_cache(strategy, locator) unless use_cache && begin
|
|
244
|
+
Appom.cache_config[:enabled]
|
|
245
|
+
rescue StandardError
|
|
246
|
+
true
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Use global cache
|
|
250
|
+
cache_key = Appom::ElementCache.cache.generate_key(strategy, locator)
|
|
251
|
+
cached = Appom::ElementCache.cache.get(cache_key)
|
|
252
|
+
|
|
253
|
+
return cached if cached
|
|
254
|
+
|
|
255
|
+
element = _find_without_cache(strategy, locator)
|
|
256
|
+
Appom::ElementCache.cache.store(strategy, locator, element) if element
|
|
257
|
+
element
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
private
|
|
261
|
+
|
|
262
|
+
def _find_without_cache(strategy, locator)
|
|
263
|
+
# If original method doesn't exist, delegate to the page/driver
|
|
264
|
+
if respond_to?(:original_find_element)
|
|
265
|
+
original_find_element(strategy, locator)
|
|
266
|
+
elsif respond_to?(:_find)
|
|
267
|
+
_find(strategy, locator)
|
|
268
|
+
else
|
|
269
|
+
page.find_element(strategy, locator)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
public
|
|
274
|
+
|
|
275
|
+
def find_elements(strategy, locator, use_cache: true)
|
|
276
|
+
# If original method doesn't exist, delegate to the page/driver
|
|
277
|
+
return page.find_elements(strategy, locator) unless respond_to?(:original_find_elements)
|
|
278
|
+
|
|
279
|
+
return original_find_elements(strategy, locator) unless use_cache && begin
|
|
280
|
+
Appom.cache_config[:enabled]
|
|
281
|
+
rescue StandardError
|
|
282
|
+
true
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Use global cache
|
|
286
|
+
cache_key = Appom::ElementCache.cache.generate_key(strategy, locator)
|
|
287
|
+
cached = Appom::ElementCache.cache.get(cache_key)
|
|
288
|
+
|
|
289
|
+
return cached if cached
|
|
290
|
+
|
|
291
|
+
elements = original_find_elements(strategy, locator)
|
|
292
|
+
Appom::ElementCache.cache.store(strategy, locator, elements) if elements
|
|
293
|
+
elements
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def element_cache
|
|
297
|
+
@element_cache ||= Cache.new(
|
|
298
|
+
max_size: Appom.cache_config[:max_size],
|
|
299
|
+
ttl: Appom.cache_config[:ttl],
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Clear cache for this finder
|
|
304
|
+
def clear_element_cache
|
|
305
|
+
element_cache.clear
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Get cache statistics
|
|
309
|
+
def cache_stats
|
|
310
|
+
element_cache.statistics
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Global cache instance and module methods
|
|
315
|
+
@global_cache = nil
|
|
316
|
+
|
|
317
|
+
module_function
|
|
318
|
+
|
|
319
|
+
def cache
|
|
320
|
+
@global_cache || (@cache ||= Cache.new)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def clear_cache
|
|
324
|
+
cache.clear
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def reset_cache
|
|
328
|
+
cache.reset
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def cache_element(strategy, value, element)
|
|
332
|
+
cache.store(strategy, value, element)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def get_cached_element(key)
|
|
336
|
+
cache.get(key)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def cache_hit?(key)
|
|
340
|
+
cache.hit?(key)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def cache_statistics
|
|
344
|
+
cache.statistics
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def configure_cache(**)
|
|
348
|
+
@global_cache = Cache.new(**)
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Configuration for element caching
|
|
353
|
+
module Appom
|
|
354
|
+
class << self
|
|
355
|
+
attr_accessor :cache_config
|
|
356
|
+
|
|
357
|
+
def configure_cache(max_size: 50, ttl: 30, enabled: true)
|
|
358
|
+
@cache_config = {
|
|
359
|
+
max_size: max_size,
|
|
360
|
+
ttl: ttl,
|
|
361
|
+
enabled: enabled,
|
|
362
|
+
}
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Default cache configuration
|
|
367
|
+
@cache_config = {
|
|
368
|
+
max_size: 50,
|
|
369
|
+
ttl: 30,
|
|
370
|
+
enabled: true,
|
|
371
|
+
}
|
|
372
|
+
end
|