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.
@@ -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
- module Appom
2
- class Page
3
- include Appium
4
- include ElementContainer
5
- include ElementFinder
6
-
7
- def page
8
- @page || Appom.driver
9
- end
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