testcentricity_apps 4.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +3 -0
  3. data/CHANGELOG.md +193 -0
  4. data/LICENSE.md +27 -0
  5. data/README.md +2297 -0
  6. data/lib/testcentricity_apps/app_core/appium_connect_helper.rb +667 -0
  7. data/lib/testcentricity_apps/app_core/screen_object.rb +494 -0
  8. data/lib/testcentricity_apps/app_core/screen_objects_helper.rb +211 -0
  9. data/lib/testcentricity_apps/app_core/screen_section.rb +669 -0
  10. data/lib/testcentricity_apps/app_elements/alert.rb +152 -0
  11. data/lib/testcentricity_apps/app_elements/app_element.rb +728 -0
  12. data/lib/testcentricity_apps/app_elements/button.rb +10 -0
  13. data/lib/testcentricity_apps/app_elements/checkbox.rb +61 -0
  14. data/lib/testcentricity_apps/app_elements/image.rb +10 -0
  15. data/lib/testcentricity_apps/app_elements/label.rb +10 -0
  16. data/lib/testcentricity_apps/app_elements/list.rb +188 -0
  17. data/lib/testcentricity_apps/app_elements/menu.rb +159 -0
  18. data/lib/testcentricity_apps/app_elements/menubar.rb +78 -0
  19. data/lib/testcentricity_apps/app_elements/radio.rb +61 -0
  20. data/lib/testcentricity_apps/app_elements/selectlist.rb +126 -0
  21. data/lib/testcentricity_apps/app_elements/switch.rb +66 -0
  22. data/lib/testcentricity_apps/app_elements/textfield.rb +51 -0
  23. data/lib/testcentricity_apps/appium_server.rb +76 -0
  24. data/lib/testcentricity_apps/data_objects/data_objects_helper.rb +100 -0
  25. data/lib/testcentricity_apps/data_objects/environment.rb +423 -0
  26. data/lib/testcentricity_apps/exception_queue_helper.rb +160 -0
  27. data/lib/testcentricity_apps/utility_helpers.rb +48 -0
  28. data/lib/testcentricity_apps/version.rb +3 -0
  29. data/lib/testcentricity_apps/world_extensions.rb +61 -0
  30. data/lib/testcentricity_apps.rb +103 -0
  31. metadata +322 -0
@@ -0,0 +1,494 @@
1
+ require 'test/unit'
2
+
3
+ module TestCentricity
4
+ class ScreenObject < BaseScreenSectionObject
5
+ include Test::Unit::Assertions
6
+
7
+ attr_reader :locator
8
+
9
+ def initialize
10
+ raise "Screen object #{self.class.name} does not have a screen_name trait defined" unless defined?(screen_name)
11
+ @locator = screen_locator if defined?(screen_locator)
12
+ end
13
+
14
+ # Declare and instantiate a single generic UI Element for this screen object.
15
+ #
16
+ # @param element_name [Symbol] name of UI object (as a symbol)
17
+ # @param locator [Hash] { locator_strategy: locator_identifier }
18
+ # The locator_strategy (a Symbol) specifies the selector strategy that Appium will use to find the UI element. Valid
19
+ # selectors are accessibility_id:, id:, name:, class:, xpath:, predicate: (iOS only), class_chain: (iOS only), and
20
+ # css: (WebViews in hybrid apps only).
21
+ # * The locator_identifier (a String) is the value or attribute that uniquely and unambiguously identifies the UI element.
22
+ #
23
+ # @example
24
+ # element :video_player, { accessibility_id: 'YouTube Video Player' }
25
+ #
26
+ def self.element(element_name, locator)
27
+ define_screen_element(element_name, TestCentricity::AppElements::AppUIElement, locator)
28
+ end
29
+
30
+ # Declare and instantiate a collection of generic UI Elements for this screen object.
31
+ #
32
+ # @param element_hash [Hash] names of UI objects (as a Symbol) and locator Hash
33
+ # @example
34
+ # elements drop_down_field: { accessibility_id: 'drop_trigger' },
35
+ # settings_item: { accessibility_id: 'settings' },
36
+ # video_player: { accessibility_id: 'YouTube Video Player' }
37
+ #
38
+ def self.elements(element_hash)
39
+ element_hash.each do |element_name, locator|
40
+ element(element_name, locator)
41
+ end
42
+ end
43
+
44
+ # Declare and instantiate a single button UI Element for this screen object.
45
+ #
46
+ # @param element_name [Symbol] name of button object (as a symbol)
47
+ # @param locator [Hash] { locator_strategy: locator_identifier }
48
+ # @example
49
+ # button :video_play, { accessibility_id: 'video icon play' }
50
+ #
51
+ def self.button(element_name, locator)
52
+ define_screen_element(element_name, TestCentricity::AppElements::AppButton, locator)
53
+ end
54
+
55
+ # Declare and instantiate a collection of buttons for this screen object.
56
+ #
57
+ # @param element_hash [Hash] names of buttons (as symbol) and locator Hash
58
+ # @example
59
+ # buttons video_back: { accessibility_id: 'video icon backward' },
60
+ # video_play: { accessibility_id: 'video icon play' },
61
+ # video_pause: { accessibility_id: 'video icon stop' },
62
+ # video_forward: { accessibility_id: 'video icon forward' }
63
+ #
64
+ def self.buttons(element_hash)
65
+ element_hash.each do |element_name, locator|
66
+ button(element_name, locator)
67
+ end
68
+ end
69
+
70
+ # Declare and instantiate a single textfield UI Element for this screen object.
71
+ #
72
+ # @param element_name [Symbol] name of textfield object (as a symbol)
73
+ # @param locator [Hash] { locator_strategy: locator_identifier }
74
+ # @example
75
+ # textfield :payee_name_field, { xpath: '//android.widget.EditText[@content-desc="Full Name* input field"]' }
76
+ # textfield :payee_name_field, { xpath: '//XCUIElementTypeTextField[@name="Full Name* input field"]' }
77
+ #
78
+ def self.textfield(element_name, locator)
79
+ define_screen_element(element_name, TestCentricity::AppElements::AppTextField, locator)
80
+ end
81
+
82
+ # Declare and instantiate a collection of textfields for this screen object.
83
+ #
84
+ # @param element_hash [Hash] names of textfields (as symbol) and locator Hash
85
+ # @example
86
+ # textfields username_field: { accessibility_id: 'Username input field' },
87
+ # password_field: { accessibility_id: 'Password input field' }
88
+ #
89
+ def self.textfields(element_hash)
90
+ element_hash.each do |element_name, locator|
91
+ textfield(element_name, locator)
92
+ end
93
+ end
94
+
95
+ # Declare and instantiate a single switch UI Element for this screen object.
96
+ #
97
+ # @param element_name [Symbol] name of switch object (as a symbol)
98
+ # @param locator [Hash] { locator_strategy: locator_identifier }
99
+ # @example
100
+ # switch :debug_mode_switch, { accessibility_id: 'debug mode' }
101
+ #
102
+ def self.switch(element_name, locator)
103
+ define_screen_element(element_name, TestCentricity::AppElements::AppSwitch, locator)
104
+ end
105
+
106
+ # Declare and instantiate a collection of switches for this screen object.
107
+ #
108
+ # @param element_hash [Hash] names of switches (as symbol) and locator Hash
109
+ # @example
110
+ # switches debug_mode_switch: { accessibility_id: 'debug mode' },
111
+ # metrics_switch: { accessibility_id: 'metrics' }
112
+ #
113
+ def self.switches(element_hash)
114
+ element_hash.each do |element_name, locator|
115
+ switch(element_name, locator)
116
+ end
117
+ end
118
+
119
+ # Declare and instantiate a single checkbox UI Element for this screen object.
120
+ #
121
+ # @param element_name [Symbol] name of checkbox object (as a symbol)
122
+ # @param locator [Hash] { locator_strategy: locator_identifier }
123
+ # @example
124
+ # checkbox :bill_address_check, { xpath: '//XCUIElementTypeOther[contains(@name, "billing checkbox")]'}
125
+ #
126
+ def self.checkbox(element_name, locator)
127
+ define_screen_element(element_name, TestCentricity::AppElements::AppCheckBox, locator)
128
+ end
129
+
130
+ # Declare and instantiate a collection of checkboxes for this screen object.
131
+ #
132
+ # @param element_hash [Hash] names of checkboxes (as symbol) and locator Hash
133
+ # @example
134
+ # checkboxes bill_address_check: { xpath: '//XCUIElementTypeOther[contains(@name, "billing checkbox")]'},
135
+ # is_gift_check: { accessibility_id: 'is a gift' }
136
+ #
137
+ def self.checkboxes(element_hash)
138
+ element_hash.each do |element_name, locator|
139
+ checkbox(element_name, locator)
140
+ end
141
+ end
142
+
143
+ # Declare and instantiate a single radio button UI Element for this screen object.
144
+ #
145
+ # @param element_name [Symbol] name of radio button object (as a symbol)
146
+ # @param locator [Hash] { locator_strategy: locator_identifier }
147
+ # @example
148
+ # radio :unicode_radio, { xpath: '//XCUIElementTypeRadioButton[@label="Unicode"]'}
149
+ #
150
+ def self.radio(element_name, locator)
151
+ define_screen_element(element_name, TestCentricity::AppElements::AppRadio, locator)
152
+ end
153
+
154
+ # Declare and instantiate a collection of radio buttons for this screen object.
155
+ #
156
+ # @param element_hash [Hash] names of radio buttons (as symbol) and locator Hash
157
+ # @example
158
+ # radios unicode_radio: { xpath: '//XCUIElementTypeRadioButton[@label="Unicode"]'},
159
+ # ascii_radio: { xpath: '//XCUIElementTypeRadioButton[@label="ASCII"] }
160
+ #
161
+ def self.radios(element_hash)
162
+ element_hash.each do |element_name, locator|
163
+ radio(element_name, locator)
164
+ end
165
+ end
166
+
167
+ # Declare and instantiate a single label UI Element for this screen object.
168
+ #
169
+ # @param element_name [Symbol] name of label object (as a symbol)
170
+ # @param locator [Hash] { locator_strategy: locator_identifier }
171
+ # @example
172
+ # label :header_label, { accessibility_id: 'container header' }
173
+ #
174
+ def self.label(element_name, locator)
175
+ define_screen_element(element_name, TestCentricity::AppElements::AppLabel, locator)
176
+ end
177
+
178
+ # Declare and instantiate a collection of labels for this screen object.
179
+ #
180
+ # @param element_hash [Hash] names of labels (as symbol) and locator Hash
181
+ # @example
182
+ # labels total_qty_value: { accessibility_id: 'total number' },
183
+ # total_price_value: { accessibility_id: 'total price' }
184
+ #
185
+ def self.labels(element_hash)
186
+ element_hash.each do |element_name, locator|
187
+ label(element_name, locator)
188
+ end
189
+ end
190
+
191
+ # Declare and instantiate a single list UI Element for this screen object.
192
+ #
193
+ # @param element_name [Symbol] name of list object (as a symbol)
194
+ # @param locator [Hash] { locator_strategy: locator_identifier }
195
+ # @example
196
+ # list :carousel_list, { accessibility_id: 'Carousel' }
197
+ #
198
+ def self.list(element_name, locator)
199
+ define_screen_element(element_name, TestCentricity::AppElements::AppList, locator)
200
+ end
201
+
202
+ # Declare and instantiate a collection of lists for this screen object.
203
+ #
204
+ # @param element_hash [Hash] names of lists (as symbol) and locator Hash
205
+ # @example
206
+ # lists product_grid: { xpath: '//android.widget.ScrollView/android.view.ViewGroup' },
207
+ # cart_list: { xpath: '//android.widget.ScrollView[@content-desc="cart screen"]' }
208
+ #
209
+ def self.lists(element_hash)
210
+ element_hash.each do |element_name, locator|
211
+ list(element_name, locator)
212
+ end
213
+ end
214
+
215
+ # Declare and instantiate a single selectlist UI Element for this screen object.
216
+ #
217
+ # @param element_name [Symbol] name of selectlist object (as a symbol)
218
+ # @param locator [Hash] { locator_strategy: locator_identifier }
219
+ # @example
220
+ # selectlist :convert_list, { xpath: '//XCUIElementTypePopUpButton[@label="convert"]' }
221
+ #
222
+ def self.selectlist(element_name, locator)
223
+ define_screen_element(element_name, TestCentricity::AppElements::AppSelectList, locator)
224
+ end
225
+
226
+ # Declare and instantiate a collection of selectlists for this screen object.
227
+ #
228
+ # @param element_hash [Hash] names of selectlists (as symbol) and locator Hash
229
+ # @example
230
+ # selectlists convert_list: { xpath: '//XCUIElementTypePopUpButton[@label="convert"]' },
231
+ # from_list: { xpath: '//XCUIElementTypePopUpButton[@label="convert_from"]' }
232
+ #
233
+ def self.selectlists(element_hash)
234
+ element_hash.each do |element_name, locator|
235
+ selectlist(element_name, locator)
236
+ end
237
+ end
238
+
239
+ # Declare and instantiate a single image UI Element for this screen object.
240
+ #
241
+ # @param element_name [Symbol] name of image object (as a symbol)
242
+ # @param locator [Hash] { locator_strategy: locator_identifier }
243
+ # @example
244
+ # image :product_image, { xpath: '//XCUIElementTypeImage' }
245
+ #
246
+ def self.image(element_name, locator)
247
+ define_screen_element(element_name, TestCentricity::AppElements::AppImage, locator)
248
+ end
249
+
250
+ # Declare and instantiate a collection of images for this screen object.
251
+ #
252
+ # @param element_hash [Hash] names of images (as symbol) and locator Hash
253
+ # @example
254
+ # images empty_cart_image: { accessibility_id: 'empty_cart' },
255
+ # logo_image: { accessibility_id: 'WebdriverIO logo' }
256
+ #
257
+ def self.images(element_hash)
258
+ element_hash.each do |element_name, locator|
259
+ image(element_name, locator)
260
+ end
261
+ end
262
+
263
+ # Declare and instantiate a single alert UI Element for this screen object.
264
+ #
265
+ # @param element_name [Symbol] name of alert object (as a symbol)
266
+ # @param locator [Hash] { locator_strategy: locator_identifier }
267
+ # @example
268
+ # alert :generic_alert_modal, { id: 'android:id/parentPanel' }
269
+ # alert :generic_alert_modal, { class: 'XCUIElementTypeAlert' }
270
+ #
271
+ def self.alert(element_name, locator)
272
+ define_screen_element(element_name, TestCentricity::AppElements::AppAlert, locator)
273
+ end
274
+
275
+ # Declare and instantiate a collection of alerts for this screen object.
276
+ #
277
+ # @param element_hash [Hash] names of alerts (as symbol) and locator Hash
278
+ # @example
279
+ # alerts grant_modal: { id: 'com.android.permissioncontroller:id/grant_dialog' },
280
+ # alert_modal: { id: 'android:id/parentPanel' }
281
+ #
282
+ def self.alerts(element_hash)
283
+ element_hash.each do |element_name, locator|
284
+ alert(element_name, locator)
285
+ end
286
+ end
287
+
288
+ # Declare and instantiate a single MenuBar object for this screen object.
289
+ #
290
+ # @param element_name [Symbol] name of MenuBar object (as a symbol)
291
+ # @param class_name [Class] Class name of MenuBar object
292
+ # @example
293
+ # menubar :menu_bar, CalculatorMenuBar
294
+ #
295
+ def self.menubar(section_name, obj, locator = 0)
296
+ define_screen_element(section_name, obj, locator)
297
+ end
298
+
299
+ # Instantiate a single ScreenSection object within this ScreenObject.
300
+ #
301
+ # @param section_name [Symbol] name of ScreenSection object (as a symbol)
302
+ # @param class_name [Class] Class name of ScreenSection object
303
+ # @example
304
+ # section :nav_menu, NavMenu
305
+ #
306
+ def self.section(section_name, obj, locator = 0)
307
+ define_screen_element(section_name, obj, locator)
308
+ end
309
+
310
+ # Declare and instantiate a collection of ScreenSection objects for this screen object.
311
+ #
312
+ # @param element_hash [Hash] names of ScreenSections (as symbol) and class name
313
+ # @example
314
+ # sections nav_bar: NavBar,
315
+ # nav_menu: NavMenu
316
+ #
317
+ def self.sections(section_hash)
318
+ section_hash.each do |section_name, class_name|
319
+ section(section_name, class_name)
320
+ end
321
+ end
322
+
323
+ # Does Screen object exists?
324
+ #
325
+ # @return [Boolean]
326
+ # @example
327
+ # home_screen.exists?
328
+ #
329
+ def exists?
330
+ @locator.is_a?(Array) ? tries ||= 2 : tries ||= 1
331
+ if @locator.is_a?(Array)
332
+ loc = @locator[tries - 1]
333
+ find_element(loc.keys[0], loc.values[0])
334
+ else
335
+ find_element(@locator.keys[0], @locator.values[0])
336
+ end
337
+ true
338
+ rescue
339
+ retry if (tries -= 1) > 0
340
+ false
341
+ end
342
+
343
+ # Return window title. For MacOS app testing only. Raises exception if called while testing iOS or Android
344
+ #
345
+ # @return [String]
346
+ # @example
347
+ # calculator_screen.title
348
+ #
349
+ def title
350
+ if Environ.is_macos?
351
+ find_element(@locator.keys[0], @locator.values[0]).title
352
+ else
353
+ raise 'title is not a supported attribute'
354
+ end
355
+ end
356
+
357
+ def identifier
358
+ if Environ.is_macos?
359
+ find_element(@locator.keys[0], @locator.values[0]).identifier
360
+ else
361
+ raise 'identifier is not a supported attribute'
362
+ end
363
+ end
364
+
365
+ # Wait until the Screen object exists, or until the specified wait time has expired. If the wait time is nil, then
366
+ # the wait time will be Environ.default_max_wait_time.
367
+ #
368
+ # @param seconds [Integer or Float] wait time in seconds
369
+ # @example
370
+ # cart_screen.wait_until_exists(15)
371
+ #
372
+ def wait_until_exists(seconds = nil, post_exception = true)
373
+ timeout = seconds.nil? ? Environ.default_max_wait_time : seconds
374
+ wait = Selenium::WebDriver::Wait.new(timeout: timeout)
375
+ wait.until { exists? }
376
+ rescue
377
+ if post_exception
378
+ raise "Screen object #{self.class.name} not found after #{timeout} seconds" unless exists?
379
+ else
380
+ exists?
381
+ end
382
+ end
383
+
384
+ # Wait until the Screen object is gone, or until the specified wait time has expired. If the wait time is nil, then
385
+ # the wait time will be Environ.default_max_wait_time.
386
+ #
387
+ # @param seconds [Integer or Float] wait time in seconds
388
+ # @example
389
+ # login_screen.wait_until_gone(15)
390
+ #
391
+ def wait_until_gone(seconds = nil, post_exception = true)
392
+ timeout = seconds.nil? ? Environ.default_max_wait_time : seconds
393
+ wait = Selenium::WebDriver::Wait.new(timeout: timeout)
394
+ wait.until { !exists? }
395
+ rescue
396
+ if post_exception
397
+ raise "Screen object #{self.class.name} remained visible after #{timeout} seconds" if exists?
398
+ else
399
+ exists?
400
+ end
401
+ end
402
+
403
+ def navigate_to; end
404
+
405
+ def verify_screen_ui; end
406
+
407
+ # Verifies that the target screen is displayed, and sets ScreenManager.current_screen to reference the target screen
408
+ # instance.
409
+ #
410
+ def verify_screen_exists
411
+ wait = Selenium::WebDriver::Wait.new(timeout: Environ.default_max_wait_time)
412
+ wait.until { exists? }
413
+ ScreenManager.current_screen = self
414
+ rescue
415
+ raise "Could not find screen_locator for screen object '#{self.class.name}' (#{@locator}) after #{Environ.default_max_wait_time} seconds"
416
+ end
417
+
418
+ # Load the screen using its defined deep_link trait. When testing on physical iOS devices running iOS/iPadOS versions
419
+ # earlier than version 16.4, deep links can only be opened by sending the deeplink URL to the mobile Safari web browser,
420
+ # and then accepting the confirmation modal that pops up. This method handles invoking deeplinks on Android and iOS/iPadOS
421
+ # simulators and physical devices.
422
+ #
423
+ # This method verifies that the target screen is loaded and displayed, and sets ScreenManager.current_screen to reference
424
+ # the target screen instance.
425
+ #
426
+ # @example
427
+ # cart_screen.load_screen
428
+ #
429
+ def load_screen
430
+ # return if target screen is already loaded
431
+ if exists?
432
+ ScreenManager.current_screen = self
433
+ return
434
+ end
435
+
436
+ url = if deep_link.include?("://")
437
+ deep_link
438
+ elsif !Environ.current.deep_link_prefix.blank?
439
+ "#{Environ.current.deep_link_prefix}://#{deep_link}"
440
+ end
441
+
442
+ if Environ.is_android?
443
+ Environ.appium_driver.execute_script('mobile: deepLink', { url: url, package: Environ.current.android_app_id })
444
+ elsif Environ.is_macos?
445
+ Environ.appium_driver.execute_script('macos: deepLink', { url: url, package: Environ.current.android_app_id })
446
+ elsif Environ.is_ios?
447
+ if Environ.is_device? && Environ.device_os_version.to_f < 16.4
448
+ # launch Safari browser on iOS real device if iOS version is below 16.4
449
+ Environ.appium_driver.execute_script('mobile: launchApp', { bundleId: 'com.apple.mobilesafari' })
450
+ unless Environ.appium_driver.is_keyboard_shown
451
+ begin
452
+ # attempt to find and click URL button on iOS 15 Safari browser
453
+ find_element(:accessibility_id, 'TabBarItemTitle').click
454
+ rescue
455
+ # fall back to URL button on iOS 14 Safari browser
456
+ find_element(:xpath, '//XCUIElementTypeButton[@name="URL"]').click
457
+ end
458
+ end
459
+ # enter deep-link URL
460
+ wait_for_object(:xpath, '//XCUIElementTypeTextField[@name="URL"]', 5).send_keys("#{url}\uE007")
461
+ # wait for and accept the popup modal
462
+ wait_for_object(:xpath, '//XCUIElementTypeButton[@name="Open"]', 10).click
463
+ else
464
+ # iOS version is >= 16.4 so directly load screen via deepLink
465
+ Environ.appium_driver.get(url)
466
+ end
467
+ else
468
+ raise "#{Environ.device_os} is not supported"
469
+ end
470
+ verify_screen_exists
471
+ end
472
+
473
+ private
474
+
475
+ def self.define_screen_element(element_name, obj, locator)
476
+ define_method(element_name) do
477
+ ivar_name = "@#{element_name}"
478
+ ivar = instance_variable_get(ivar_name)
479
+ return ivar if ivar
480
+ instance_variable_set(ivar_name, obj.new(element_name, self, locator, :screen))
481
+ end
482
+ end
483
+
484
+ def wait_for_object(find_method, locator, seconds)
485
+ wait = Selenium::WebDriver::Wait.new(timeout: seconds)
486
+ obj = nil
487
+ wait.until do
488
+ obj = find_element(find_method, locator)
489
+ obj.displayed?
490
+ end
491
+ obj
492
+ end
493
+ end
494
+ end