appom 1.3.3 → 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 -121
- 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 -33
- data/lib/appom.rb +191 -31
- metadata +35 -19
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appom
|
|
4
|
+
# Base exception for all Appom-related errors
|
|
5
|
+
class AppomError < StandardError
|
|
6
|
+
attr_reader :context
|
|
7
|
+
|
|
8
|
+
def initialize(message = nil, context = {})
|
|
9
|
+
super(message)
|
|
10
|
+
@context = context
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def detailed_message
|
|
14
|
+
message_parts = [message]
|
|
15
|
+
message_parts << "Context: #{context}" unless context.empty?
|
|
16
|
+
message_parts.join("\n")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Element-related errors
|
|
21
|
+
class ElementError < AppomError; end
|
|
22
|
+
|
|
23
|
+
# Raised when an element was defined without proper arguments
|
|
24
|
+
class InvalidElementError < ElementError
|
|
25
|
+
def initialize(element_name = nil)
|
|
26
|
+
if element_name.nil?
|
|
27
|
+
message = 'You should provide search arguments in element creation'
|
|
28
|
+
else
|
|
29
|
+
message = 'Element'
|
|
30
|
+
message += " '#{element_name}'" if element_name
|
|
31
|
+
message += ' was defined without proper selector arguments'
|
|
32
|
+
end
|
|
33
|
+
super(message)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Raised when an element cannot be found within the timeout
|
|
38
|
+
class ElementNotFoundError < ElementError
|
|
39
|
+
def initialize(selector = nil, timeout = nil)
|
|
40
|
+
message = 'Element not found'
|
|
41
|
+
message += " with selector: #{selector}" if selector
|
|
42
|
+
message += " within #{timeout}s" if timeout
|
|
43
|
+
super(message, { selector: selector, timeout: timeout })
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raised when an element is found but not in the expected state
|
|
48
|
+
class ElementStateError < ElementError
|
|
49
|
+
def initialize(element_name, expected_state, actual_state = nil)
|
|
50
|
+
message = "Element '#{element_name}' expected to be #{expected_state}"
|
|
51
|
+
message += " but was #{actual_state}" if actual_state
|
|
52
|
+
super(message, { element: element_name, expected: expected_state, actual: actual_state })
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Wait-related errors
|
|
57
|
+
class WaitError < AppomError
|
|
58
|
+
attr_reader :condition, :timeout
|
|
59
|
+
|
|
60
|
+
def initialize(condition = 'unknown condition', timeout = nil)
|
|
61
|
+
@condition = condition
|
|
62
|
+
@timeout = timeout
|
|
63
|
+
message = "Wait condition '#{condition}' not met"
|
|
64
|
+
message += " within #{timeout}s" if timeout
|
|
65
|
+
super(message, { condition: condition, timeout: timeout })
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Driver-related errors
|
|
70
|
+
class DriverError < AppomError; end
|
|
71
|
+
|
|
72
|
+
# Raised when driver is not properly initialized
|
|
73
|
+
class DriverNotInitializedError < DriverError
|
|
74
|
+
def initialize
|
|
75
|
+
super('Appium driver not initialized. Please call Appom.register_driver first.')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Raised when driver operations fail
|
|
80
|
+
class DriverOperationError < DriverError
|
|
81
|
+
attr_reader :operation, :cause
|
|
82
|
+
|
|
83
|
+
def initialize(operation, cause = nil)
|
|
84
|
+
@operation = operation
|
|
85
|
+
@cause = cause
|
|
86
|
+
message = "Driver operation '#{operation}' failed"
|
|
87
|
+
message += ": #{cause}" if cause
|
|
88
|
+
super(message, { operation: operation, cause: cause })
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Configuration-related errors
|
|
93
|
+
class ConfigurationError < AppomError
|
|
94
|
+
def initialize(setting, value = nil, reason = nil)
|
|
95
|
+
message = "Invalid configuration for '#{setting}'"
|
|
96
|
+
message += " (value: #{value})" if value
|
|
97
|
+
message += ": #{reason}" if reason
|
|
98
|
+
super(message, { setting: setting, value: value, reason: reason })
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Block/syntax errors
|
|
103
|
+
class UnsupportedBlockError < AppomError
|
|
104
|
+
def initialize(method_name, type)
|
|
105
|
+
super("#{type}##{method_name} does not accept blocks",
|
|
106
|
+
{ method: method_name, type: type })
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Section-related errors
|
|
111
|
+
class SectionError < AppomError; end
|
|
112
|
+
|
|
113
|
+
# Invalid section definition error
|
|
114
|
+
class InvalidSectionError < SectionError
|
|
115
|
+
def initialize(reason)
|
|
116
|
+
super("Invalid section definition: #{reason}")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Timeout and waiting errors
|
|
121
|
+
class TimeoutError < AppomError
|
|
122
|
+
def initialize(message = 'Operation timed out')
|
|
123
|
+
super
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Argument validation errors
|
|
128
|
+
class ArgumentError < AppomError
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'appom/retry'
|
|
4
|
+
|
|
5
|
+
# Helper utilities for Appom automation framework
|
|
6
|
+
# Provides common interaction patterns and utility methods
|
|
7
|
+
module Appom::Helpers
|
|
8
|
+
# Get the performance module, allowing for test mocking
|
|
9
|
+
def self.performance_module
|
|
10
|
+
defined?(Performance) ? Performance : Appom::Performance
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Common element interaction patterns
|
|
14
|
+
module ElementHelpers
|
|
15
|
+
include Appom::Retry::RetryMethods
|
|
16
|
+
|
|
17
|
+
# Tap an element and wait for it to be enabled
|
|
18
|
+
def tap_and_wait(element_name, timeout: nil)
|
|
19
|
+
Appom::Helpers.performance_module.time_operation("tap_and_wait_#{element_name}") do
|
|
20
|
+
timeout ||= Appom.max_wait_time
|
|
21
|
+
element = send(element_name)
|
|
22
|
+
element.tap
|
|
23
|
+
|
|
24
|
+
# Wait for element to be enabled if it has an enable checker
|
|
25
|
+
send("#{element_name}_enable") if respond_to?(:"#{element_name}_enable")
|
|
26
|
+
element
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get element text with retry
|
|
31
|
+
def get_text_with_retry(element_name, retries: 3)
|
|
32
|
+
attempt = 0
|
|
33
|
+
begin
|
|
34
|
+
send(element_name).text
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
attempt += 1
|
|
37
|
+
raise e unless attempt <= retries
|
|
38
|
+
|
|
39
|
+
sleep(0.5)
|
|
40
|
+
retry
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Wait for element to be visible and tap
|
|
45
|
+
def wait_and_tap(element_name, timeout: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
46
|
+
Appom::Helpers.performance_module.time_operation("wait_and_tap_#{element_name}") do
|
|
47
|
+
method_name = "has_#{element_name}"
|
|
48
|
+
send(method_name) if respond_to?(method_name)
|
|
49
|
+
send(element_name).tap
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get element attribute with fallback
|
|
54
|
+
def get_attribute_with_fallback(element_name, attribute, fallback_value = nil)
|
|
55
|
+
send(element_name).attribute(attribute) || fallback_value
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
log_warn("Failed to get attribute #{attribute} for #{element_name}: #{e.message}")
|
|
58
|
+
fallback_value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if element contains text
|
|
62
|
+
def element_contains_text?(element_name, text)
|
|
63
|
+
element_text = get_text_with_retry(element_name)
|
|
64
|
+
element_text&.include?(text) || false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Basic scroll method for scrolling in specified direction
|
|
68
|
+
def scroll(direction = :down)
|
|
69
|
+
# Basic implementation - actual implementation would depend on platform
|
|
70
|
+
case direction
|
|
71
|
+
when :down
|
|
72
|
+
Appom.driver.execute_script('mobile: scrollGesture', {
|
|
73
|
+
left: 100, top: 100, width: 200, height: 200,
|
|
74
|
+
direction: 'down', percent: 3.0,
|
|
75
|
+
})
|
|
76
|
+
when :up
|
|
77
|
+
Appom.driver.execute_script('mobile: scrollGesture', {
|
|
78
|
+
left: 100, top: 100, width: 200, height: 200,
|
|
79
|
+
direction: 'up', percent: 3.0,
|
|
80
|
+
})
|
|
81
|
+
end
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
# Fallback scroll for different platforms
|
|
84
|
+
log_warn("Scroll gesture failed: #{e.message}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Scroll to element if needed and tap
|
|
88
|
+
def scroll_to_and_tap(element_name, direction: :down)
|
|
89
|
+
max_scrolls = 5
|
|
90
|
+
scrolls = 0
|
|
91
|
+
|
|
92
|
+
while scrolls < max_scrolls
|
|
93
|
+
return wait_and_tap(element_name) if respond_to?("has_#{element_name}") && send("has_#{element_name}")
|
|
94
|
+
|
|
95
|
+
scroll(direction)
|
|
96
|
+
scrolls += 1
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
raise Appom::ElementNotFoundError.new(element_name, "after #{max_scrolls} scrolls")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Common wait patterns
|
|
104
|
+
module WaitHelpers
|
|
105
|
+
include Appom::Retry::RetryMethods
|
|
106
|
+
|
|
107
|
+
# Wait for element to be clickable (visible and enabled)
|
|
108
|
+
def wait_for_clickable(element_name, timeout: nil)
|
|
109
|
+
timeout ||= Appom.max_wait_time
|
|
110
|
+
find_args = send("#{element_name}_params")
|
|
111
|
+
Appom::SmartWait.until_clickable(*find_args, timeout: timeout)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Wait for element text to match pattern
|
|
115
|
+
def wait_for_text_match(element_name, text, exact: false, timeout: nil)
|
|
116
|
+
timeout ||= Appom.max_wait_time
|
|
117
|
+
find_args = send("#{element_name}_params")
|
|
118
|
+
Appom::SmartWait.until_text_matches(*find_args, text: text, exact: exact, timeout: timeout)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Wait for element to become invisible
|
|
122
|
+
def wait_for_invisible(element_name, timeout: nil)
|
|
123
|
+
timeout ||= Appom.max_wait_time
|
|
124
|
+
find_args = send("#{element_name}_params")
|
|
125
|
+
Appom::SmartWait.until_invisible(*find_args, timeout: timeout)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Wait for elements collection to have specific count
|
|
129
|
+
def wait_for_count(elements_name, count, timeout: nil)
|
|
130
|
+
timeout ||= Appom.max_wait_time
|
|
131
|
+
find_args = send("#{elements_name}_params")
|
|
132
|
+
Appom::SmartWait.until_count_equals(*find_args, count: count, timeout: timeout)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Advanced: Wait for custom condition on element
|
|
136
|
+
def wait_for_condition(element_name, description: 'custom condition', timeout: nil, &condition_block)
|
|
137
|
+
timeout ||= Appom.max_wait_time
|
|
138
|
+
find_args = send("#{element_name}_params")
|
|
139
|
+
Appom::SmartWait.until_condition(*find_args, timeout: timeout, description: description, &condition_block)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Wait for any of multiple elements to appear
|
|
143
|
+
def wait_for_any(*element_names, timeout: nil)
|
|
144
|
+
timeout ||= Appom.max_wait_time
|
|
145
|
+
wait = Appom::Wait.new(timeout: timeout)
|
|
146
|
+
|
|
147
|
+
wait.until do
|
|
148
|
+
element_names.each do |element_name|
|
|
149
|
+
return element_name if respond_to?("has_#{element_name}") && send("has_#{element_name}")
|
|
150
|
+
end
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
rescue Appom::WaitError
|
|
154
|
+
raise Appom::ElementNotFoundError.new("any of: #{element_names.join(', ')}", timeout)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Wait for element to disappear
|
|
158
|
+
def wait_for_disappear(element_name, timeout: nil)
|
|
159
|
+
if respond_to?("has_no_#{element_name}")
|
|
160
|
+
send("has_no_#{element_name}")
|
|
161
|
+
else
|
|
162
|
+
timeout ||= Appom.max_wait_time
|
|
163
|
+
wait = Appom::Wait.new(timeout: timeout)
|
|
164
|
+
wait.until do
|
|
165
|
+
send(element_name)
|
|
166
|
+
false
|
|
167
|
+
rescue Appom::ElementNotFoundError
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Wait for text to appear in element
|
|
174
|
+
def wait_for_text_in_element(element_name, expected_text, timeout: nil)
|
|
175
|
+
timeout ||= Appom.max_wait_time
|
|
176
|
+
wait = Appom::Wait.new(timeout: timeout)
|
|
177
|
+
|
|
178
|
+
wait.until do
|
|
179
|
+
element_text = get_text_with_retry(element_name, retries: 1)
|
|
180
|
+
element_text&.include?(expected_text)
|
|
181
|
+
rescue StandardError
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Debugging helpers
|
|
188
|
+
module DebugHelpers
|
|
189
|
+
# Take screenshot with automatic naming
|
|
190
|
+
def take_debug_screenshot(prefix = 'debug')
|
|
191
|
+
Screenshot.capture(prefix)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Take screenshot of specific element
|
|
195
|
+
def take_element_screenshot(element_name, prefix = 'element')
|
|
196
|
+
element = send(element_name)
|
|
197
|
+
Screenshot.capture("#{prefix}_#{element_name}", element: element)
|
|
198
|
+
rescue StandardError => e
|
|
199
|
+
log_error("Failed to take element screenshot: #{e.message}")
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Take before/after screenshots around an action
|
|
204
|
+
def screenshot_action(action_name, &)
|
|
205
|
+
Screenshot.capture_before_after(action_name, &)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Take screenshot sequence during complex interaction
|
|
209
|
+
def screenshot_sequence(name, interval: 1.0, max_duration: 10.0, &)
|
|
210
|
+
Screenshot.capture_sequence(name, interval: interval, max_duration: max_duration, &)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Take screenshot on test failure with exception info
|
|
214
|
+
def screenshot_failure(test_name, exception = nil)
|
|
215
|
+
Screenshot.capture_on_failure(test_name, exception)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Dump current page source for debugging
|
|
219
|
+
def dump_page_source(prefix = 'page_source')
|
|
220
|
+
return unless respond_to?(:driver) && driver
|
|
221
|
+
|
|
222
|
+
timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
|
223
|
+
filename = "#{prefix}_#{timestamp}.xml"
|
|
224
|
+
|
|
225
|
+
begin
|
|
226
|
+
File.write(filename, driver.page_source)
|
|
227
|
+
log_info("Page source saved: #{filename}")
|
|
228
|
+
filename
|
|
229
|
+
rescue StandardError => e
|
|
230
|
+
log_error("Failed to save page source: #{e.message}")
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Get information about all elements matching a locator
|
|
236
|
+
def debug_elements_info(*find_args)
|
|
237
|
+
elements = _all(*find_args)
|
|
238
|
+
info = elements.map.with_index do |element, index|
|
|
239
|
+
{
|
|
240
|
+
index: index,
|
|
241
|
+
tag_name: element.tag_name,
|
|
242
|
+
text: element.text.to_s.strip,
|
|
243
|
+
displayed: element.displayed?,
|
|
244
|
+
enabled: element.enabled?,
|
|
245
|
+
location: element.location,
|
|
246
|
+
size: element.size,
|
|
247
|
+
}
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
{ index: index, error: e.message }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
log_info("Found #{elements.count} elements matching #{find_args.join(', ')}")
|
|
253
|
+
info.each { |element_info| log_debug("Element info: #{element_info}") }
|
|
254
|
+
info
|
|
255
|
+
rescue StandardError => e
|
|
256
|
+
log_error("Failed to get elements info: #{e.message}")
|
|
257
|
+
[]
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Phase 2 Performance monitoring helpers
|
|
262
|
+
module PerformanceHelpers
|
|
263
|
+
# Time any element operation
|
|
264
|
+
def time_element_operation(element_name, operation, &)
|
|
265
|
+
Appom::Helpers.performance_module.time_operation("#{element_name}_#{operation}", &)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Get performance stats for specific element operations
|
|
269
|
+
def element_performance_stats(element_name = nil)
|
|
270
|
+
if element_name
|
|
271
|
+
Appom::Helpers.performance_module.stats.select { |name, _| name.include?(element_name.to_s) }
|
|
272
|
+
else
|
|
273
|
+
Appom::Helpers.performance_module.summary
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Phase 2 Visual testing helpers
|
|
279
|
+
module VisualHelpers
|
|
280
|
+
# Take screenshot with element highlighted
|
|
281
|
+
def screenshot_with_highlight(element_name, filename: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
282
|
+
element = send(element_name)
|
|
283
|
+
Visual.test_helpers.highlight_element(element)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Visual regression test for current page
|
|
287
|
+
def visual_regression_test(test_name, options = {})
|
|
288
|
+
Visual.regression_test(test_name, options)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Wait for visual stability before continuing
|
|
292
|
+
def wait_for_visual_stability(element_name = nil, **)
|
|
293
|
+
element = element_name ? send(element_name) : nil
|
|
294
|
+
Visual.test_helpers.wait_for_visual_stability(element: element, **)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Phase 2 Element state tracking helpers
|
|
299
|
+
module ElementStateHelpers
|
|
300
|
+
# Start tracking an element's state changes
|
|
301
|
+
def track_element_state(element_name, context: {})
|
|
302
|
+
element = send(element_name)
|
|
303
|
+
ElementState.track_element(element, name: element_name.to_s, context: context)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Wait for element state to change
|
|
307
|
+
def wait_for_element_state_change(element_name, expected_changes: {}, **)
|
|
308
|
+
element_id = element_name.to_s
|
|
309
|
+
ElementState.wait_for_state_change(element_id, expected_changes: expected_changes, **)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Get current state of tracked element
|
|
313
|
+
def element_current_state(element_name)
|
|
314
|
+
ElementState.element_state(element_name.to_s)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Include all helper modules
|
|
319
|
+
def self.included(klass)
|
|
320
|
+
klass.include ElementHelpers
|
|
321
|
+
klass.include WaitHelpers
|
|
322
|
+
klass.include DebugHelpers
|
|
323
|
+
klass.include PerformanceHelpers
|
|
324
|
+
klass.include VisualHelpers
|
|
325
|
+
klass.include ElementStateHelpers
|
|
326
|
+
klass.include Appom::Logging
|
|
327
|
+
end
|
|
328
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
# Main module for Appom automation framework
|
|
6
|
+
module Appom
|
|
7
|
+
# Logging functionality for Appom automation framework
|
|
8
|
+
# Provides centralized logging with configurable levels and formatters
|
|
9
|
+
module Logging
|
|
10
|
+
class << self
|
|
11
|
+
attr_writer :logger
|
|
12
|
+
|
|
13
|
+
def logger
|
|
14
|
+
@logger ||= create_default_logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def level=(level)
|
|
18
|
+
logger.level = level
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def level
|
|
22
|
+
logger.level
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def create_default_logger
|
|
28
|
+
logger = Logger.new($stdout)
|
|
29
|
+
logger.level = Logger::INFO
|
|
30
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
31
|
+
# Handle datetime parameter which can be Time object or integer timestamp
|
|
32
|
+
time = datetime.is_a?(Time) ? datetime : Time.at(datetime)
|
|
33
|
+
"[#{time.strftime('%Y-%m-%d %H:%M:%S')}] #{severity.ljust(5)} [Appom] #{msg}\n"
|
|
34
|
+
end
|
|
35
|
+
logger
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Instance methods for including in classes
|
|
40
|
+
def logger
|
|
41
|
+
Logging.logger
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def log_debug(message, context = {})
|
|
45
|
+
logger.debug(format_message(message, context))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def log_info(message, context = {})
|
|
49
|
+
logger.info(format_message(message, context))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def log_warn(message, context = {})
|
|
53
|
+
logger.warn(format_message(message, context))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def log_error(message, context = {})
|
|
57
|
+
logger.error(format_message(message, context))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def log_element_action(action, element_info, duration = nil)
|
|
61
|
+
message = "#{action.upcase}: #{element_info}"
|
|
62
|
+
message += " (#{duration}ms)" if duration
|
|
63
|
+
log_info(message)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def log_wait_start(condition, timeout)
|
|
67
|
+
log_debug("WAIT: Starting wait for '#{condition}' (timeout: #{timeout}s)")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def log_wait_end(condition, duration, success: true)
|
|
71
|
+
status = success ? 'SUCCESS' : 'TIMEOUT'
|
|
72
|
+
log_debug("WAIT: #{status} for '#{condition}' (#{duration}s)")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def format_message(message, context)
|
|
78
|
+
return message if context.empty?
|
|
79
|
+
|
|
80
|
+
context_str = context.map { |k, v| "#{k}=#{v}" }.join(' ')
|
|
81
|
+
"#{message} | #{context_str}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Configure logging
|
|
86
|
+
def self.configure_logging(level: :info, output: nil, custom_logger: nil)
|
|
87
|
+
if custom_logger
|
|
88
|
+
Logging.logger = custom_logger
|
|
89
|
+
else
|
|
90
|
+
logger = Logger.new(output || $stdout)
|
|
91
|
+
logger.level = case level.to_s.downcase
|
|
92
|
+
when 'debug' then Logger::DEBUG
|
|
93
|
+
when 'warn' then Logger::WARN
|
|
94
|
+
when 'error' then Logger::ERROR
|
|
95
|
+
when 'fatal' then Logger::FATAL
|
|
96
|
+
else Logger::INFO # This covers both 'info' and any invalid values
|
|
97
|
+
end
|
|
98
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
99
|
+
# Handle the case where datetime might be mocked as an integer in tests
|
|
100
|
+
timestamp = datetime.respond_to?(:strftime) ? datetime.strftime('%Y-%m-%d %H:%M:%S') : datetime.to_s
|
|
101
|
+
"[#{timestamp}] #{severity.ljust(5)} [Appom] #{msg}\n"
|
|
102
|
+
end
|
|
103
|
+
Logging.logger = logger
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/appom/page.rb
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'appom/helpers'
|
|
4
|
+
|
|
5
|
+
# Base page class for Appom automation framework
|
|
6
|
+
# Provides common functionality for page objects
|
|
7
|
+
class Appom::Page
|
|
8
|
+
include Appium
|
|
9
|
+
include Appom::ElementContainer
|
|
10
|
+
include Appom::ElementFinder
|
|
11
|
+
include Appom::Helpers
|
|
12
|
+
|
|
13
|
+
def initialize(driver = nil)
|
|
14
|
+
@page = driver
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def page
|
|
18
|
+
@page || Appom.driver
|
|
10
19
|
end
|
|
11
|
-
end
|
|
20
|
+
end
|