testcentricity_mobile 4.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 +7 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +105 -0
- data/LICENSE.md +27 -0
- data/README.md +1795 -0
- data/lib/testcentricity_mobile/app_core/appium_connect_helper.rb +634 -0
- data/lib/testcentricity_mobile/app_core/screen_object.rb +413 -0
- data/lib/testcentricity_mobile/app_core/screen_objects_helper.rb +183 -0
- data/lib/testcentricity_mobile/app_core/screen_section.rb +607 -0
- data/lib/testcentricity_mobile/app_elements/alert.rb +152 -0
- data/lib/testcentricity_mobile/app_elements/app_element.rb +692 -0
- data/lib/testcentricity_mobile/app_elements/button.rb +10 -0
- data/lib/testcentricity_mobile/app_elements/checkbox.rb +56 -0
- data/lib/testcentricity_mobile/app_elements/image.rb +10 -0
- data/lib/testcentricity_mobile/app_elements/label.rb +10 -0
- data/lib/testcentricity_mobile/app_elements/list.rb +175 -0
- data/lib/testcentricity_mobile/app_elements/switch.rb +66 -0
- data/lib/testcentricity_mobile/app_elements/textfield.rb +51 -0
- data/lib/testcentricity_mobile/appium_server.rb +71 -0
- data/lib/testcentricity_mobile/data_objects/data_objects_helper.rb +100 -0
- data/lib/testcentricity_mobile/data_objects/environment.rb +416 -0
- data/lib/testcentricity_mobile/exception_queue_helper.rb +160 -0
- data/lib/testcentricity_mobile/utility_helpers.rb +48 -0
- data/lib/testcentricity_mobile/version.rb +3 -0
- data/lib/testcentricity_mobile/world_extensions.rb +61 -0
- data/lib/testcentricity_mobile.rb +99 -0
- metadata +317 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
module TestCentricity
|
2
|
+
module AppElements
|
3
|
+
class AppCheckBox < AppUIElement
|
4
|
+
def initialize(name, parent, locator, context)
|
5
|
+
super
|
6
|
+
@type = :checkbox
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is checkbox checked?
|
10
|
+
#
|
11
|
+
# @return [Boolean]
|
12
|
+
# @example
|
13
|
+
# remember_me_checkbox.checked?
|
14
|
+
#
|
15
|
+
def checked?
|
16
|
+
obj = element
|
17
|
+
object_not_found_exception(obj)
|
18
|
+
obj.selected?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set the check state of a checkbox object.
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# remember_me_checkbox.check
|
25
|
+
#
|
26
|
+
def check
|
27
|
+
set_checkbox_state(true)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Uncheck a checkbox object.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# remember_me_checkbox.uncheck
|
34
|
+
#
|
35
|
+
def uncheck
|
36
|
+
set_checkbox_state(false)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set the check state of a checkbox object.
|
40
|
+
#
|
41
|
+
# @param state [Boolean] true = checked / false = unchecked
|
42
|
+
# @example
|
43
|
+
# remember_me_checkbox.set_checkbox_state(true)
|
44
|
+
#
|
45
|
+
def set_checkbox_state(state)
|
46
|
+
obj = element
|
47
|
+
object_not_found_exception(obj)
|
48
|
+
if state
|
49
|
+
obj.click unless obj.selected?
|
50
|
+
else
|
51
|
+
obj.click if obj.selected?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module TestCentricity
|
2
|
+
module AppElements
|
3
|
+
class AppList < AppUIElement
|
4
|
+
attr_accessor :list_item
|
5
|
+
attr_accessor :scrolling
|
6
|
+
attr_accessor :item_objects
|
7
|
+
|
8
|
+
def initialize(name, parent, locator, context)
|
9
|
+
super
|
10
|
+
@type = :list
|
11
|
+
@list_item = nil
|
12
|
+
@scrolling = :vertical
|
13
|
+
@item_objects = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def define_list_elements(element_spec)
|
17
|
+
element_spec.each do |element, value|
|
18
|
+
case element
|
19
|
+
when :list_item
|
20
|
+
@list_item = value
|
21
|
+
when :scrolling
|
22
|
+
@scrolling = value
|
23
|
+
else
|
24
|
+
raise "#{element} is not a recognized list element"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def scroll_mode
|
30
|
+
obj = element
|
31
|
+
object_not_found_exception(obj)
|
32
|
+
@scrolling
|
33
|
+
end
|
34
|
+
|
35
|
+
def item_refs
|
36
|
+
obj = element
|
37
|
+
object_not_found_exception(obj)
|
38
|
+
@item_objects
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return the number of items in a list object.
|
42
|
+
#
|
43
|
+
# @return [Integer]
|
44
|
+
# @example
|
45
|
+
# num_nav_items = nav_list.get_item_count
|
46
|
+
#
|
47
|
+
def get_item_count
|
48
|
+
obj = element
|
49
|
+
object_not_found_exception(obj)
|
50
|
+
if Environ.is_ios? && obj.attribute(:type) == 'XCUIElementTypePickerWheel'
|
51
|
+
raise 'This method is not supported for XCUIElementTypePickerWheel controls'
|
52
|
+
end
|
53
|
+
list_loc = get_list_item_locator
|
54
|
+
items = obj.find_elements(list_loc.keys[0], list_loc.values[0])
|
55
|
+
if items.size > 0 && Environ.is_android?
|
56
|
+
start_count = items.count
|
57
|
+
direction = @scrolling == :horizontal ? :right : :down
|
58
|
+
scroll_count = 0
|
59
|
+
loop do
|
60
|
+
save_count = items.count
|
61
|
+
swipe_gesture(direction)
|
62
|
+
scroll_count += 1
|
63
|
+
obj.find_elements(list_loc.keys[0], list_loc.values[0]).each do |item|
|
64
|
+
items.push(item) unless items.include?(item)
|
65
|
+
end
|
66
|
+
break if items.count == save_count
|
67
|
+
end
|
68
|
+
direction = @scrolling == :horizontal ? :left : :up
|
69
|
+
scroll_count.times do
|
70
|
+
swipe_gesture(direction)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
@item_objects = items if Environ.is_android? && start_count < items.count
|
74
|
+
items.count
|
75
|
+
end
|
76
|
+
|
77
|
+
# Return array of strings of all items in a list object.
|
78
|
+
#
|
79
|
+
# @return [Array]
|
80
|
+
# @example
|
81
|
+
# nav_items = nav_list.get_options
|
82
|
+
#
|
83
|
+
def get_list_items
|
84
|
+
list_items = []
|
85
|
+
obj = element
|
86
|
+
object_not_found_exception(obj)
|
87
|
+
if Environ.is_ios? && obj.attribute(:type) == 'XCUIElementTypePickerWheel'
|
88
|
+
raise 'This method is not supported for XCUIElementTypePickerWheel controls'
|
89
|
+
end
|
90
|
+
list_loc = get_list_item_locator
|
91
|
+
items = obj.find_elements(list_loc.keys[0], list_loc.values[0])
|
92
|
+
items.each do |item|
|
93
|
+
list_items.push(item.text)
|
94
|
+
end
|
95
|
+
list_items
|
96
|
+
end
|
97
|
+
|
98
|
+
# Select the specified item in a list object. Accepts a String or Integer.
|
99
|
+
#
|
100
|
+
# @param item [String, Integer] text or index of item to select
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# province_list.choose_item(2)
|
104
|
+
# province_list.choose_item('Manitoba')
|
105
|
+
#
|
106
|
+
def choose_item(item)
|
107
|
+
obj = element
|
108
|
+
object_not_found_exception(obj)
|
109
|
+
if Environ.is_ios? && obj.attribute(:type) == 'XCUIElementTypePickerWheel'
|
110
|
+
obj.send_keys(item)
|
111
|
+
else
|
112
|
+
list_loc = get_list_item_locator
|
113
|
+
items = obj.find_elements(list_loc.keys[0], list_loc.values[0])
|
114
|
+
if item.is_a?(Integer)
|
115
|
+
items[item - 1].click
|
116
|
+
else
|
117
|
+
items.each do |list_item|
|
118
|
+
if list_item.text == item
|
119
|
+
list_item.click
|
120
|
+
break
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Wait until the list's item_count equals the specified value, or until the specified wait time has expired. If the
|
128
|
+
# wait time is nil, then the wait time will be Environ.default_max_wait_time.
|
129
|
+
#
|
130
|
+
# @param value [Integer or Hash] value expected or comparison hash
|
131
|
+
# @param seconds [Integer or Float] wait time in seconds
|
132
|
+
# @example
|
133
|
+
# search_results_list.wait_until_item_count_is(10, 15)
|
134
|
+
# or
|
135
|
+
# search_results_list.wait_until_item_count_is({ :greater_than_or_equal => 1 }, 5)
|
136
|
+
#
|
137
|
+
def wait_until_item_count_is(value, seconds = nil)
|
138
|
+
timeout = seconds.nil? ? Environ.default_max_wait_time : seconds
|
139
|
+
wait = Selenium::WebDriver::Wait.new(timeout: timeout)
|
140
|
+
wait.until { compare(value, get_item_count) }
|
141
|
+
rescue
|
142
|
+
raise "Value of List #{object_ref_message} failed to equal '#{value}' after #{timeout} seconds" unless get_item_count == value
|
143
|
+
end
|
144
|
+
|
145
|
+
# Wait until the list's item_count changes, or until the specified wait time has expired. If the wait time is nil,
|
146
|
+
# then the wait time will be Environ.default_max_wait_time.
|
147
|
+
#
|
148
|
+
# @param seconds [Integer or Float] wait time in seconds
|
149
|
+
# @example
|
150
|
+
# search_results_list.wait_until_item_count_changes(10)
|
151
|
+
#
|
152
|
+
def wait_until_item_count_changes(seconds = nil)
|
153
|
+
value = get_item_count
|
154
|
+
timeout = seconds.nil? ? Environ.default_max_wait_time : seconds
|
155
|
+
wait = Selenium::WebDriver::Wait.new(timeout: timeout)
|
156
|
+
wait.until { get_item_count != value }
|
157
|
+
rescue
|
158
|
+
raise "Value of List #{object_ref_message} failed to change from '#{value}' after #{timeout} seconds" if get_item_count == value
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def get_list_item_locator
|
164
|
+
if @list_item.nil?
|
165
|
+
if Environ.device_os == :ios
|
166
|
+
define_list_elements({ :list_item => { class: 'XCUIElementTypeCell' } } )
|
167
|
+
else
|
168
|
+
define_list_elements({ :list_item => { class: 'android.widget.FrameLayout' } } )
|
169
|
+
end
|
170
|
+
end
|
171
|
+
@list_item
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module TestCentricity
|
2
|
+
module AppElements
|
3
|
+
class AppSwitch < AppUIElement
|
4
|
+
def initialize(name, parent, locator, context)
|
5
|
+
super
|
6
|
+
@type = :switch
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is switch in ON state?
|
10
|
+
#
|
11
|
+
# @return [Boolean]
|
12
|
+
# @example
|
13
|
+
# use_face_id_switch.on?
|
14
|
+
#
|
15
|
+
def on?
|
16
|
+
obj = element
|
17
|
+
object_not_found_exception(obj)
|
18
|
+
|
19
|
+
if Environ.is_ios?
|
20
|
+
state = obj.attribute(:value)
|
21
|
+
state.to_bool
|
22
|
+
else
|
23
|
+
state = obj.attribute(:checked)
|
24
|
+
state.boolean? ? state : state == 'true'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
alias checked? on?
|
29
|
+
alias selected? on?
|
30
|
+
|
31
|
+
# Set the state of a switch object to ON.
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# use_face_id_switch.on
|
35
|
+
#
|
36
|
+
def on
|
37
|
+
obj = element
|
38
|
+
object_not_found_exception(obj)
|
39
|
+
obj.click unless on?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Set the state of a switch object to OFF.
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# use_face_id_switch.off
|
46
|
+
#
|
47
|
+
def off
|
48
|
+
obj = element
|
49
|
+
object_not_found_exception(obj)
|
50
|
+
obj.click if on?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the ON/OFF state of a switch object.
|
54
|
+
#
|
55
|
+
# @param state [Boolean] true = on / false = off
|
56
|
+
# @example
|
57
|
+
# use_face_id_switch.set_switch_state(true)
|
58
|
+
#
|
59
|
+
def set_switch_state(state)
|
60
|
+
obj = element
|
61
|
+
object_not_found_exception(obj)
|
62
|
+
state ? on : off
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module TestCentricity
|
2
|
+
module AppElements
|
3
|
+
class AppTextField < AppUIElement
|
4
|
+
def initialize(name, parent, locator, context)
|
5
|
+
super
|
6
|
+
@type = :textfield
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is text field set to read-only?
|
10
|
+
#
|
11
|
+
# @return [Boolean]
|
12
|
+
# @example
|
13
|
+
# comments_field.read_only?
|
14
|
+
#
|
15
|
+
def read_only?
|
16
|
+
obj = element
|
17
|
+
object_not_found_exception(obj)
|
18
|
+
!!obj.attribute('readonly')
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return maxlength character count of a text field.
|
22
|
+
#
|
23
|
+
# @return [Integer]
|
24
|
+
# @example
|
25
|
+
# max_num_chars = comments_field.get_max_length
|
26
|
+
#
|
27
|
+
def get_max_length
|
28
|
+
obj = element
|
29
|
+
object_not_found_exception(obj)
|
30
|
+
max_length = obj.attribute('maxlength')
|
31
|
+
max_length.to_i unless max_length.blank?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return placeholder text of a text field.
|
35
|
+
#
|
36
|
+
# @return [String]
|
37
|
+
# @example
|
38
|
+
# placeholder_message = username_field.get_placeholder
|
39
|
+
#
|
40
|
+
def get_placeholder
|
41
|
+
obj = element
|
42
|
+
object_not_found_exception(obj)
|
43
|
+
if AppiumConnect.is_webview?
|
44
|
+
obj.attribute('placeholder')
|
45
|
+
else
|
46
|
+
obj.text
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'childprocess'
|
2
|
+
|
3
|
+
#
|
4
|
+
# This class is designed to start and stop the Appium Server process. In order to use it you must have Appium
|
5
|
+
# and node installed on your computer. You can pass parameters to Appium at startup via the constructor.
|
6
|
+
#
|
7
|
+
module TestCentricity
|
8
|
+
class AppiumServer
|
9
|
+
attr_accessor :process
|
10
|
+
|
11
|
+
def initialize(params = {})
|
12
|
+
@params = params
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Start the Appium Server
|
17
|
+
#
|
18
|
+
def start
|
19
|
+
# terminate any currently running Appium Server
|
20
|
+
if running?
|
21
|
+
system('killall -9 node')
|
22
|
+
puts 'Terminating existing Appium Server'
|
23
|
+
sleep(5)
|
24
|
+
puts 'Appium Server is being restarted'
|
25
|
+
else
|
26
|
+
puts 'Appium Server is starting'
|
27
|
+
end
|
28
|
+
# start new Appium Server
|
29
|
+
@process = ChildProcess.build(*parameters)
|
30
|
+
process.start
|
31
|
+
# wait until confirmation that Appium Server is running
|
32
|
+
wait = Selenium::WebDriver::Wait.new(timeout: 30)
|
33
|
+
wait.until { running? }
|
34
|
+
puts "Appium Server is running - PID = #{process.pid}"
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Check to see if Appium Server is running
|
39
|
+
#
|
40
|
+
def running?
|
41
|
+
response = false
|
42
|
+
begin
|
43
|
+
response = Net::HTTP.get_response(URI('http://0.0.0.0:4723/wd/hub/sessions'))
|
44
|
+
rescue
|
45
|
+
end
|
46
|
+
response && response.code_type == Net::HTTPOK
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Stop the Appium Server
|
51
|
+
#
|
52
|
+
def stop
|
53
|
+
process.stop
|
54
|
+
puts 'Appium Server has been terminated'
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# :nocov:
|
60
|
+
def parameters
|
61
|
+
cmd = ['appium']
|
62
|
+
@params.each do |key, value|
|
63
|
+
cmd << '--' + key.to_s
|
64
|
+
cmd << value.to_s if not value.nil? and value.size > 0
|
65
|
+
end
|
66
|
+
cmd
|
67
|
+
end
|
68
|
+
# :nocov:
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
require 'virtus'
|
4
|
+
require 'time'
|
5
|
+
require 'chronic'
|
6
|
+
require 'faker'
|
7
|
+
|
8
|
+
|
9
|
+
module TestCentricity
|
10
|
+
|
11
|
+
PRIMARY_DATA_PATH ||= 'config/test_data/'
|
12
|
+
PRIMARY_DATA_FILE ||= "#{PRIMARY_DATA_PATH}data."
|
13
|
+
YML_PRIMARY_DATA_FILE ||= "#{PRIMARY_DATA_FILE}yml"
|
14
|
+
JSON_PRIMARY_DATA_FILE ||= "#{PRIMARY_DATA_FILE}json"
|
15
|
+
|
16
|
+
|
17
|
+
class DataObject
|
18
|
+
attr_accessor :current
|
19
|
+
attr_accessor :context
|
20
|
+
attr_accessor :hash_table
|
21
|
+
|
22
|
+
def initialize(data)
|
23
|
+
@hash_table = data
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.current
|
27
|
+
@current
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.current=(current)
|
31
|
+
@current = current
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
class DataPresenter
|
37
|
+
include Virtus.model
|
38
|
+
|
39
|
+
attr_accessor :current
|
40
|
+
attr_accessor :context
|
41
|
+
|
42
|
+
def initialize(data)
|
43
|
+
self.attributes = data unless data.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.current
|
47
|
+
@current
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.current=(current)
|
51
|
+
@current = current
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# :nocov:
|
57
|
+
class DataSource
|
58
|
+
attr_accessor :file_path
|
59
|
+
attr_accessor :node
|
60
|
+
|
61
|
+
def read_yaml_node_data(file_name, node_name)
|
62
|
+
@file_path = "#{PRIMARY_DATA_PATH}#{file_name}"
|
63
|
+
@node = node_name
|
64
|
+
data = YAML.load_file(@file_path)
|
65
|
+
data[node_name]
|
66
|
+
end
|
67
|
+
|
68
|
+
def read_json_node_data(file_name, node_name)
|
69
|
+
@file_path = "#{PRIMARY_DATA_PATH}#{file_name}"
|
70
|
+
@node = node_name
|
71
|
+
raw_data = File.read(@file_path)
|
72
|
+
data = JSON.parse(raw_data)
|
73
|
+
data[node_name]
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def self.calculate_dynamic_value(value)
|
79
|
+
test_value = value.split('!', 2)
|
80
|
+
parameter = test_value[1].split('.', 2)
|
81
|
+
case parameter[0]
|
82
|
+
when 'Date'
|
83
|
+
result = eval("Chronic.parse('#{parameter[1]}')")
|
84
|
+
when 'FormattedDate', 'FormatDate'
|
85
|
+
date_time_params = parameter[1].split(' format! ', 2)
|
86
|
+
date_time = eval("Chronic.parse('#{date_time_params[0].strip}')")
|
87
|
+
result = date_time.to_s.format_date_time("#{date_time_params[1].strip}")
|
88
|
+
else
|
89
|
+
result = if Faker.constants.include?(parameter[0].to_sym)
|
90
|
+
eval("Faker::#{parameter[0]}.#{parameter[1]}")
|
91
|
+
else
|
92
|
+
eval(test_value[1])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
result.to_s
|
96
|
+
end
|
97
|
+
end
|
98
|
+
# :nocov:
|
99
|
+
end
|
100
|
+
|