percy-appium-app 0.0.1
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/.github/ISSUE_TEMPLATE/bug_report.md +55 -0
- data/.github/dependabot.yml +14 -0
- data/.github/release-drafter.yml +28 -0
- data/.github/workflows/Semgrep.yml +48 -0
- data/.github/workflows/changelog.yml +11 -0
- data/.github/workflows/release.yml +22 -0
- data/.github/workflows/stale.yml +30 -0
- data/.github/workflows/test.yml +40 -0
- data/Gemfile +13 -0
- data/LICENSE +21 -0
- data/Makefile +3 -0
- data/README.md +1 -0
- data/Rakefile +1 -0
- data/percy/common/common.rb +19 -0
- data/percy/configs/devices.json +232 -0
- data/percy/environment.rb +25 -0
- data/percy/exceptions/exceptions.rb +13 -0
- data/percy/lib/app_percy.rb +55 -0
- data/percy/lib/cache.rb +53 -0
- data/percy/lib/cli_wrapper.rb +132 -0
- data/percy/lib/ignore_region.rb +8 -0
- data/percy/lib/percy_automate.rb +59 -0
- data/percy/lib/percy_options.rb +37 -0
- data/percy/lib/region.rb +22 -0
- data/percy/lib/tile.rb +28 -0
- data/percy/metadata/android_metadata.rb +79 -0
- data/percy/metadata/driver_metadata.rb +40 -0
- data/percy/metadata/ios_metadata.rb +83 -0
- data/percy/metadata/metadata.rb +108 -0
- data/percy/metadata/metadata_resolver.rb +21 -0
- data/percy/providers/app_automate.rb +159 -0
- data/percy/providers/generic_provider.rb +205 -0
- data/percy/providers/provider_resolver.rb +17 -0
- data/percy/screenshot.rb +23 -0
- data/percy/version.rb +5 -0
- data/percy-appium-app.gemspec +37 -0
- data/specs/android_metadata.rb +79 -0
- data/specs/app_automate.rb +124 -0
- data/specs/app_percy.rb +175 -0
- data/specs/cache.rb +56 -0
- data/specs/cli_wrapper.rb +135 -0
- data/specs/driver_metadata.rb +71 -0
- data/specs/generic_providers.rb +370 -0
- data/specs/ignore_regions.rb +51 -0
- data/specs/ios_metadata.rb +88 -0
- data/specs/metadata.rb +105 -0
- data/specs/metadata_resolver.rb +41 -0
- data/specs/mocks/mock_methods.rb +147 -0
- data/specs/percy_options.rb +114 -0
- data/specs/screenshot.rb +342 -0
- data/specs/tile.rb +33 -0
- metadata +194 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../common/common'
|
|
5
|
+
require_relative '../lib/tile'
|
|
6
|
+
require_relative 'generic_provider'
|
|
7
|
+
require_relative '../environment'
|
|
8
|
+
|
|
9
|
+
class AppAutomate < GenericProvider
|
|
10
|
+
def self.supports(remote_url)
|
|
11
|
+
r_index = remote_url.rindex(ENV['AA_DOMAIN'].nil? ? 'browserstack' : ENV['AA_DOMAIN'])
|
|
12
|
+
if r_index
|
|
13
|
+
r_index > -1
|
|
14
|
+
else
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def screenshot(name, **kwargs)
|
|
20
|
+
session_details = execute_percy_screenshot_begin(name)
|
|
21
|
+
|
|
22
|
+
if session_details
|
|
23
|
+
metadata.device_name = session_details['deviceName']
|
|
24
|
+
metadata.os_version = session_details['osVersion']
|
|
25
|
+
set_debug_url(session_details)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
response = super(name, **kwargs)
|
|
30
|
+
percy_screenshot_url = response.fetch('link', '')
|
|
31
|
+
execute_percy_screenshot_end(name, percy_screenshot_url, 'success')
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
execute_percy_screenshot_end(name, percy_screenshot_url, 'failure', e.message)
|
|
34
|
+
raise e
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set_debug_url(session_details)
|
|
39
|
+
build_hash = session_details['buildHash'].to_s
|
|
40
|
+
session_hash = session_details['sessionHash'].to_s
|
|
41
|
+
@debug_url = "https://app-automate.browserstack.com/dashboard/v2/builds/#{build_hash}/sessions/#{session_hash}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def _get_tiles(**kwargs)
|
|
45
|
+
fullpage_ss = kwargs[:fullpage] || false
|
|
46
|
+
if ENV['PERCY_DISABLE_REMOTE_UPLOADS'] == 'true'
|
|
47
|
+
puts("Full page screenshots are only supported when 'PERCY_DISABLE_REMOTE_UPLOADS' is not set") if fullpage_ss
|
|
48
|
+
return super(**kwargs) unless fullpage_ss
|
|
49
|
+
end
|
|
50
|
+
screenshot_type = fullpage_ss ? 'fullpage' : 'singlepage'
|
|
51
|
+
screen_lengths = kwargs[:screen_lengths] || 4
|
|
52
|
+
scrollable_xpath = kwargs[:scollable_xpath]
|
|
53
|
+
scrollable_id = kwargs[:scrollable_id]
|
|
54
|
+
top_scrollview_offset = kwargs[:top_scrollview_offset]
|
|
55
|
+
bottom_scrollview_offset = kwargs[:top_scrollview_offset]
|
|
56
|
+
|
|
57
|
+
data = execute_percy_screenshot(
|
|
58
|
+
metadata.device_screen_size.fetch('height', 1),
|
|
59
|
+
screenshot_type,
|
|
60
|
+
screen_lengths,
|
|
61
|
+
scrollable_xpath,
|
|
62
|
+
scrollable_id,
|
|
63
|
+
metadata.scale_factor,
|
|
64
|
+
top_scrollview_offset,
|
|
65
|
+
bottom_scrollview_offset
|
|
66
|
+
)
|
|
67
|
+
tiles = []
|
|
68
|
+
status_bar_height = metadata.status_bar_height
|
|
69
|
+
nav_bar_height = metadata.navigation_bar_height
|
|
70
|
+
|
|
71
|
+
JSON.parse(data['result']).each do |tile_data|
|
|
72
|
+
tiles << Tile.new(
|
|
73
|
+
status_bar_height,
|
|
74
|
+
nav_bar_height,
|
|
75
|
+
tile_data['header_height'],
|
|
76
|
+
tile_data['footer_height'],
|
|
77
|
+
sha: tile_data['sha'].split('-')[0]
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
tiles
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def execute_percy_screenshot_begin(name)
|
|
85
|
+
request_body = {
|
|
86
|
+
action: 'percyScreenshot',
|
|
87
|
+
arguments: {
|
|
88
|
+
state: 'begin',
|
|
89
|
+
percyBuildId: Environment.percy_build_id,
|
|
90
|
+
percyBuildUrl: Environment.percy_build_url,
|
|
91
|
+
name: name
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
command = "browserstack_executor: #{request_body.to_json}"
|
|
95
|
+
begin
|
|
96
|
+
response = metadata.execute_script(command)
|
|
97
|
+
JSON.parse(response)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
log('Could not set session as Percy session')
|
|
100
|
+
log('Error occurred during begin call', on_debug: true)
|
|
101
|
+
log(e, on_debug: true)
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def execute_percy_screenshot_end(name, percy_screenshot_url, status, status_message = nil)
|
|
107
|
+
request_body = {
|
|
108
|
+
action: 'percyScreenshot',
|
|
109
|
+
arguments: {
|
|
110
|
+
state: 'end',
|
|
111
|
+
percyScreenshotUrl: percy_screenshot_url,
|
|
112
|
+
name: name,
|
|
113
|
+
status: status
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
request_body[:arguments][:statusMessage] = status_message if status_message
|
|
117
|
+
command = "browserstack_executor: #{request_body.to_json}"
|
|
118
|
+
begin
|
|
119
|
+
metadata.execute_script(command)
|
|
120
|
+
rescue StandardError => e
|
|
121
|
+
log('Error occurred during end call', on_debug: true)
|
|
122
|
+
log(e, on_debug: true)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def execute_percy_screenshot(device_height, screenshotType, screen_lengths, scrollable_xpath = nil,
|
|
127
|
+
scrollable_id = nil, scale_factor = 1, top_scrollview_offset = 0,
|
|
128
|
+
bottom_scrollview_offset = 0)
|
|
129
|
+
project_id = ENV['PERCY_ENABLE_DEV'] == 'true' ? 'percy-dev' : 'percy-prod'
|
|
130
|
+
request_body = {
|
|
131
|
+
action: 'percyScreenshot',
|
|
132
|
+
arguments: {
|
|
133
|
+
state: 'screenshot',
|
|
134
|
+
percyBuildId: Environment.percy_build_id,
|
|
135
|
+
screenshotType: screenshotType,
|
|
136
|
+
projectId: project_id,
|
|
137
|
+
scaleFactor: scale_factor,
|
|
138
|
+
options: {
|
|
139
|
+
numOfTiles: screen_lengths,
|
|
140
|
+
deviceHeight: device_height,
|
|
141
|
+
scrollableXpath: scrollable_xpath,
|
|
142
|
+
scrollableId: scrollable_id,
|
|
143
|
+
topScrollviewOffset: top_scrollview_offset,
|
|
144
|
+
bottomScrollviewOffset: bottom_scrollview_offset,
|
|
145
|
+
'FORCE_FULL_PAGE' => ENV['FORCE_FULL_PAGE'] == 'true'
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
command = "browserstack_executor: #{request_body.to_json}"
|
|
150
|
+
begin
|
|
151
|
+
response = metadata.execute_script(command)
|
|
152
|
+
JSON.parse(response)
|
|
153
|
+
rescue StandardError => e
|
|
154
|
+
log('Error occurred during screenshot call', on_debug: true)
|
|
155
|
+
log(e, on_debug: true)
|
|
156
|
+
raise e
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require 'base64'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
require_relative '../lib/cli_wrapper'
|
|
9
|
+
require_relative '../lib/tile'
|
|
10
|
+
require_relative '../common/common'
|
|
11
|
+
|
|
12
|
+
class GenericProvider
|
|
13
|
+
attr_accessor :driver, :metadata, :debug_url
|
|
14
|
+
|
|
15
|
+
def initialize(driver, metadata)
|
|
16
|
+
@driver = driver
|
|
17
|
+
@metadata = metadata
|
|
18
|
+
@debug_url = ''
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.supports(_remote_url)
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def screenshot(name, **kwargs)
|
|
26
|
+
tiles = _get_tiles(**kwargs)
|
|
27
|
+
tag = _get_tag(**kwargs)
|
|
28
|
+
ignore_regions = {
|
|
29
|
+
'ignoreElementsData' => _find_regions(
|
|
30
|
+
xpaths: kwargs.fetch(:ignore_regions_xpaths, []),
|
|
31
|
+
accessibility_ids: kwargs.fetch(:ignore_region_accessibility_ids, []),
|
|
32
|
+
appium_elements: kwargs.fetch(:ignore_region_appium_elements, []),
|
|
33
|
+
custom_locations: kwargs.fetch(:custom_ignore_regions, [])
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
consider_regions = {
|
|
37
|
+
'considerElementsData' => _find_regions(
|
|
38
|
+
xpaths: kwargs.fetch(:consider_regions_xpaths, []),
|
|
39
|
+
accessibility_ids: kwargs.fetch(:consider_region_accessibility_ids, []),
|
|
40
|
+
appium_elements: kwargs.fetch(:consider_region_appium_elements, []),
|
|
41
|
+
custom_locations: kwargs.fetch(:custom_consider_regions, [])
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_post_screenshots(name, tag, tiles, get_debug_url, ignore_regions, consider_regions)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def _get_tag(**kwargs)
|
|
49
|
+
name = kwargs[:device_name] || metadata.device_name
|
|
50
|
+
os_name = metadata.os_name
|
|
51
|
+
os_version = metadata.os_version
|
|
52
|
+
width = metadata.device_screen_size['width'] || 1
|
|
53
|
+
height = metadata.device_screen_size['height'] || 1
|
|
54
|
+
orientation = metadata.get_orientation(**kwargs).downcase
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
'name' => name,
|
|
58
|
+
'os_name' => os_name,
|
|
59
|
+
'os_version' => os_version,
|
|
60
|
+
'width' => width,
|
|
61
|
+
'height' => height,
|
|
62
|
+
'orientation' => orientation
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def _get_tiles(**kwargs)
|
|
67
|
+
fullpage_ss = kwargs[:fullpage] || false
|
|
68
|
+
if fullpage_ss
|
|
69
|
+
log('Full page screenshot is only supported on App Automate. Falling back to single page screenshot.')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
png_bytes = driver.screenshot_as(:png)
|
|
73
|
+
directory = _get_dir
|
|
74
|
+
path = _write_screenshot(png_bytes, directory)
|
|
75
|
+
|
|
76
|
+
fullscreen = kwargs[:full_screen] || false
|
|
77
|
+
status_bar_height = kwargs[:status_bar_height] || metadata.status_bar_height
|
|
78
|
+
nav_bar_height = kwargs[:nav_bar_height] || metadata.navigation_bar_height
|
|
79
|
+
header_height = 0
|
|
80
|
+
footer_height = 0
|
|
81
|
+
[
|
|
82
|
+
Tile.new(status_bar_height, nav_bar_height, header_height, footer_height, filepath: path, fullscreen: fullscreen)
|
|
83
|
+
]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def _find_regions(xpaths:, accessibility_ids:, appium_elements:, custom_locations:)
|
|
87
|
+
elements_array = []
|
|
88
|
+
get_regions_by_xpath(elements_array, xpaths)
|
|
89
|
+
get_regions_by_ids(elements_array, accessibility_ids)
|
|
90
|
+
get_regions_by_elements(elements_array, appium_elements)
|
|
91
|
+
get_regions_by_location(elements_array, custom_locations)
|
|
92
|
+
elements_array
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def _post_screenshots(name, tag, tiles, debug_url, ignored_regions, considered_regions)
|
|
96
|
+
CLIWrapper.new.post_screenshots(name, tag, tiles, debug_url, ignored_regions, considered_regions)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def _write_screenshot(png_bytes, directory)
|
|
100
|
+
filepath = _get_path(directory)
|
|
101
|
+
File.open(filepath, 'wb') { |f| f.write(png_bytes) }
|
|
102
|
+
filepath
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def get_region_object(selector, element)
|
|
106
|
+
scale_factor = metadata.scale_factor
|
|
107
|
+
location = hashed(element.location)
|
|
108
|
+
size = hashed(element.size)
|
|
109
|
+
coordinates = {
|
|
110
|
+
'top' => location['y'] * scale_factor,
|
|
111
|
+
'bottom' => (location['y'] + size['height']) * scale_factor,
|
|
112
|
+
'left' => location['x'] * scale_factor,
|
|
113
|
+
'right' => (location['x'] + size['width']) * scale_factor
|
|
114
|
+
}
|
|
115
|
+
{ 'selector' => selector, 'coOrdinates' => coordinates }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def get_regions_by_xpath(elements_array, xpaths)
|
|
119
|
+
xpaths.each do |xpath|
|
|
120
|
+
element = driver.find_element(Appium::Core::Base::SearchContext::FINDERS[:xpath], xpath)
|
|
121
|
+
selector = "xpath: #{xpath}"
|
|
122
|
+
if element
|
|
123
|
+
region = get_region_object(selector, element)
|
|
124
|
+
elements_array << region
|
|
125
|
+
end
|
|
126
|
+
rescue Appium::Core::Error::NoSuchElementError => e
|
|
127
|
+
log("Appium Element with xpath: #{xpath} not found. Ignoring this xpath.")
|
|
128
|
+
log(e, on_debug: true)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def get_regions_by_ids(elements_array, ids)
|
|
133
|
+
ids.each do |id|
|
|
134
|
+
element = driver.find_element(Appium::Core::Base::SearchContext::FINDERS[:accessibility_id], id)
|
|
135
|
+
selector = "id: #{id}"
|
|
136
|
+
region = get_region_object(selector, element)
|
|
137
|
+
elements_array << region
|
|
138
|
+
rescue Appium::Core::Error::NoSuchElementError => e
|
|
139
|
+
log("Appium Element with id: #{id} not found. Ignoring this id.")
|
|
140
|
+
log(e, on_debug: true)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def get_regions_by_elements(elements_array, elements)
|
|
145
|
+
elements.each_with_index do |element, index|
|
|
146
|
+
class_name = element.attribute('class')
|
|
147
|
+
selector = "element: #{index} #{class_name}"
|
|
148
|
+
region = get_region_object(selector, element)
|
|
149
|
+
elements_array << region
|
|
150
|
+
rescue Appium::Core::Error::NoSuchElementError => e
|
|
151
|
+
log("Correct Element not passed at index #{index}")
|
|
152
|
+
log(e, on_debug: true)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def get_regions_by_location(elements_array, custom_locations)
|
|
157
|
+
custom_locations.each_with_index do |custom_location, index|
|
|
158
|
+
screen_width = metadata.device_screen_size['width']
|
|
159
|
+
screen_height = metadata.device_screen_size['height']
|
|
160
|
+
if custom_location.valid?(screen_height, screen_width)
|
|
161
|
+
region = {
|
|
162
|
+
selector: "custom ignore region: #{index}",
|
|
163
|
+
coOrdinates: {
|
|
164
|
+
top: custom_location.top,
|
|
165
|
+
bottom: custom_location.bottom,
|
|
166
|
+
left: custom_location.left,
|
|
167
|
+
right: custom_location.right
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
elements_array << region
|
|
171
|
+
else
|
|
172
|
+
log("Values passed in custom ignored region at index: #{index} are not valid")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def log(message, on_debug: false)
|
|
178
|
+
puts message if on_debug
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def get_debug_url
|
|
182
|
+
debug_url
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def get_device_name
|
|
186
|
+
''
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def _get_dir
|
|
190
|
+
dir_path = ENV['PERCY_TMP_DIR'] || nil
|
|
191
|
+
if dir_path
|
|
192
|
+
Pathname.new(dir_path).mkpath
|
|
193
|
+
return dir_path
|
|
194
|
+
end
|
|
195
|
+
Dir.mktmpdir
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def _get_path(directory)
|
|
199
|
+
suffix = '.png'
|
|
200
|
+
prefix = 'percy-appium-'
|
|
201
|
+
file = Tempfile.new([prefix, suffix], directory)
|
|
202
|
+
file.close
|
|
203
|
+
file.path
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../exceptions/exceptions'
|
|
4
|
+
require_relative '../metadata/metadata_resolver'
|
|
5
|
+
require_relative 'app_automate'
|
|
6
|
+
require_relative 'generic_provider'
|
|
7
|
+
|
|
8
|
+
class ProviderResolver
|
|
9
|
+
def self.resolve(driver)
|
|
10
|
+
metadata = MetadataResolver.resolve(driver)
|
|
11
|
+
providers = [AppAutomate, GenericProvider]
|
|
12
|
+
providers.each do |provider|
|
|
13
|
+
return provider.new(driver, metadata) if provider.supports(metadata.remote_url)
|
|
14
|
+
end
|
|
15
|
+
raise UnknownProvider
|
|
16
|
+
end
|
|
17
|
+
end
|
data/percy/screenshot.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'common/common'
|
|
4
|
+
require_relative 'lib/app_percy'
|
|
5
|
+
require_relative 'lib/percy_automate'
|
|
6
|
+
require_relative 'lib/cli_wrapper'
|
|
7
|
+
require_relative 'environment'
|
|
8
|
+
|
|
9
|
+
def percy_screenshot(driver, name, **kwargs)
|
|
10
|
+
return nil unless CLIWrapper.percy_enabled?
|
|
11
|
+
|
|
12
|
+
app_percy = nil
|
|
13
|
+
provider_class = Environment.session_type == 'automate' ? PercyOnAutomate : AppPercy
|
|
14
|
+
app_percy = provider_class.new(driver)
|
|
15
|
+
app_percy.screenshot(name, **kwargs)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
CLIWrapper.post_failed_event(e.to_s)
|
|
18
|
+
log("Could not take screenshot \"#{name}\"")
|
|
19
|
+
raise e if app_percy && !app_percy.percy_options.ignore_errors
|
|
20
|
+
|
|
21
|
+
log(e, on_debug: true)
|
|
22
|
+
nil
|
|
23
|
+
end
|
data/percy/version.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
percy = File.expand_path('../percy', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(percy) unless $LOAD_PATH.include?(percy)
|
|
4
|
+
require 'version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'percy-appium-app'
|
|
8
|
+
spec.version = Percy::VERSION
|
|
9
|
+
spec.authors = ['BroswerStack']
|
|
10
|
+
spec.email = ['support@browserstack.com']
|
|
11
|
+
spec.summary = %q{Percy visual testing for Ruby Appium Mobile Apps}
|
|
12
|
+
spec.description = %q{}
|
|
13
|
+
spec.homepage = ''
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 2.6.0'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
spec.metadata = {
|
|
19
|
+
'bug_tracker_uri' => 'https://github.com/percy/percy-appium-ruby/issues',
|
|
20
|
+
'source_code_uri' => 'https://github.com/percy/percy-appium-ruby',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
24
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
25
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
26
|
+
spec.require_paths = ['percy']
|
|
27
|
+
|
|
28
|
+
spec.add_runtime_dependency 'appium_lib', '~> 12.0'
|
|
29
|
+
|
|
30
|
+
spec.add_development_dependency 'bundler', '~> 2.4'
|
|
31
|
+
spec.add_development_dependency 'minitest', '~> 5.20'
|
|
32
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
33
|
+
spec.add_development_dependency 'percy-style', '~> 0.7.0'
|
|
34
|
+
spec.add_development_dependency 'webmock', '~> 3.18'
|
|
35
|
+
spec.add_development_dependency 'webrick', '~> 1.3'
|
|
36
|
+
|
|
37
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/MethodLength
|
|
4
|
+
|
|
5
|
+
require 'minitest/autorun'
|
|
6
|
+
require 'minitest/mock'
|
|
7
|
+
require 'appium_lib'
|
|
8
|
+
require_relative 'mocks/mock_methods'
|
|
9
|
+
require_relative '../percy/metadata/android_metadata'
|
|
10
|
+
|
|
11
|
+
# Test suite for the AndroidMetadata class
|
|
12
|
+
class TestAndroidMetadata < Minitest::Test
|
|
13
|
+
def setup
|
|
14
|
+
@mock_webdriver = Minitest::Mock.new
|
|
15
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
16
|
+
@android_metadata = AndroidMetadata.new(@mock_webdriver)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_android_execute_script
|
|
20
|
+
command = 'some dummy command'
|
|
21
|
+
output = 'some output'
|
|
22
|
+
@mock_webdriver.expect(:execute_script, output, [command])
|
|
23
|
+
|
|
24
|
+
assert_equal(output, @android_metadata.execute_script(command))
|
|
25
|
+
@mock_webdriver.verify
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_viewport
|
|
29
|
+
viewport = { 'left' => 0, 'top' => 84, 'width' => 1440, 'height' => 2708 }
|
|
30
|
+
android_capabilities = get_android_capabilities
|
|
31
|
+
@mock_webdriver.expect(:capabilities, android_capabilities.merge('viewportRect' => viewport))
|
|
32
|
+
|
|
33
|
+
assert(viewport, @android_metadata.viewport)
|
|
34
|
+
|
|
35
|
+
@mock_webdriver.verify
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_get_system_bars
|
|
39
|
+
system_bars = {
|
|
40
|
+
'statusBar' => { 'height' => 83 },
|
|
41
|
+
'navigationBar' => { 'height' => 44 }
|
|
42
|
+
}
|
|
43
|
+
android_capabilities = get_android_capabilities
|
|
44
|
+
session_id = 'session_id_123'
|
|
45
|
+
@mock_webdriver.expect(:session_id, session_id)
|
|
46
|
+
@mock_webdriver.expect(:session_id, session_id)
|
|
47
|
+
@mock_webdriver.expect(:capabilities, android_capabilities.merge('viewportRect' => nil))
|
|
48
|
+
@mock_webdriver.expect(:get_system_bars, system_bars)
|
|
49
|
+
|
|
50
|
+
assert(system_bars, @android_metadata.get_system_bars)
|
|
51
|
+
@mock_webdriver.verify
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_status_bar
|
|
55
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
56
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
57
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
58
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
59
|
+
mock_get_system_bars = { 'statusBar' => { 'height' => 1 } }
|
|
60
|
+
@android_metadata.stub(:get_system_bars, mock_get_system_bars) do
|
|
61
|
+
assert_equal(0, @android_metadata.status_bar_height)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_navigation_bar
|
|
66
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
67
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
68
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
69
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
70
|
+
mock_get_system_bars = { 'navigationBar' => { 'height' => 1 } }
|
|
71
|
+
@android_metadata.stub(:get_system_bars, mock_get_system_bars) do
|
|
72
|
+
assert_equal(0, @android_metadata.navigation_bar_height)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_scale_factor
|
|
77
|
+
assert_equal(1, @android_metadata.scale_factor)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Metrics/MethodLength
|
|
4
|
+
# rubocop:disable Metrics/AbcSize
|
|
5
|
+
|
|
6
|
+
require 'minitest/autorun'
|
|
7
|
+
require 'minitest/mock'
|
|
8
|
+
require_relative '../percy/providers/app_automate'
|
|
9
|
+
require_relative '../percy/metadata/android_metadata'
|
|
10
|
+
require_relative 'mocks/mock_methods'
|
|
11
|
+
|
|
12
|
+
# Test suite for the AppAutomate class
|
|
13
|
+
class TestAppAutomate < Minitest::Test
|
|
14
|
+
COMPARISON_RESPONSE = { 'success' => true, 'link' => 'https://snapshots-url' }.freeze
|
|
15
|
+
|
|
16
|
+
def setup
|
|
17
|
+
@mock_webdriver = Minitest::Mock.new
|
|
18
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
19
|
+
@metadata = AndroidMetadata.new(@mock_webdriver)
|
|
20
|
+
@app_automate = AppAutomate.new(@mock_webdriver, @metadata)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_app_automate_get_debug_url
|
|
24
|
+
@app_automate.set_debug_url('deviceName' => 'Google Pixel 4', 'osVersion' => '12.0', 'buildHash' => 'abc',
|
|
25
|
+
'sessionHash' => 'def')
|
|
26
|
+
debug_url = @app_automate.get_debug_url
|
|
27
|
+
assert_equal 'https://app-automate.browserstack.com/dashboard/v2/builds/abc/sessions/def', debug_url
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_app_automate_supports_with_correct_url
|
|
31
|
+
app_automate_session = AppAutomate.supports('https://hub-cloud.browserstack.com/wd/hub')
|
|
32
|
+
assert_equal true, app_automate_session
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_app_automate_supports_with_incorrect_url
|
|
36
|
+
app_automate_session = AppAutomate.supports('https://hub-cloud.generic.com/wd/hub')
|
|
37
|
+
assert_equal false, app_automate_session
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_app_automate_supports_with_AA_DOMAIN
|
|
41
|
+
ENV['AA_DOMAIN'] = 'bsstag'
|
|
42
|
+
app_automate_session = AppAutomate.supports('bsstag.com')
|
|
43
|
+
assert_equal true, app_automate_session
|
|
44
|
+
ENV['AA_DOMAIN'] = nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_app_automate_execute_percy_screenshot_begin
|
|
48
|
+
@mock_webdriver.expect(:execute_script, '{}', [String])
|
|
49
|
+
assert_empty @app_automate.execute_percy_screenshot_begin('Screebshot 1')
|
|
50
|
+
@mock_webdriver.verify
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_app_automate_execute_percy_screenshot_end
|
|
54
|
+
@mock_webdriver.expect(:execute_script, '{}', [String])
|
|
55
|
+
assert_equal '{}',
|
|
56
|
+
@app_automate.execute_percy_screenshot_end('Screenshot 1', COMPARISON_RESPONSE['link'], 'success')
|
|
57
|
+
@mock_webdriver.verify
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_app_automate_execute_percy_screenshot
|
|
61
|
+
@mock_webdriver.expect(:execute_script, '{"result": "result"}', [String])
|
|
62
|
+
@app_automate.execute_percy_screenshot(1080, 'singlepage', 5)
|
|
63
|
+
@mock_webdriver.verify
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_execute_percy_screenshot_end_throws_error
|
|
67
|
+
@mock_webdriver.expect(:execute_script, proc { raise 'SomeException' }, [String])
|
|
68
|
+
@app_automate.execute_percy_screenshot_end('Screenshot 1', 'snapshot-url', 'success')
|
|
69
|
+
@mock_webdriver.verify
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_execute_percy_screenshot_end
|
|
73
|
+
@app_automate.stub(:execute_percy_screenshot_begin, 'deviceName' => 'abc', 'osVersion' => '123') do
|
|
74
|
+
@app_automate.stub(:execute_percy_screenshot_end, nil) do
|
|
75
|
+
@app_automate.stub(:screenshot, 'link' => 'https://link') do
|
|
76
|
+
@app_automate.screenshot('name')
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_get_tiles
|
|
83
|
+
# Mocking Metadata's session_id method
|
|
84
|
+
metadata_mock = Minitest::Mock.new
|
|
85
|
+
metadata_mock.expect(:session_id, 'session_id_123')
|
|
86
|
+
|
|
87
|
+
# Mocking AndroidMetadata's methods
|
|
88
|
+
android_metadata_mock = Minitest::Mock.new
|
|
89
|
+
android_metadata_mock.expect(:device_screen_size, { 'width' => 1080, 'height' => 1920 })
|
|
90
|
+
android_metadata_mock.expect(:navigation_bar_height, 150)
|
|
91
|
+
android_metadata_mock.expect(:status_bar_height, 100)
|
|
92
|
+
|
|
93
|
+
Metadata.class_eval do
|
|
94
|
+
define_method(:session_id) do
|
|
95
|
+
metadata_mock.session_id
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
AndroidMetadata.class_eval do
|
|
100
|
+
define_method(:device_screen_size) do
|
|
101
|
+
android_metadata_mock.device_screen_size
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
define_method(:navigation_bar_height) do
|
|
105
|
+
android_metadata_mock.navigation_bar_height
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
define_method(:status_bar_height) do
|
|
109
|
+
android_metadata_mock.status_bar_height
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
@app_automate.stub(:execute_percy_screenshot, {
|
|
114
|
+
'result' => '[{"sha":"sha-25568755","status_bar":null,"nav_bar":null,"header_height":120,"footer_height":80,"index":0}]'
|
|
115
|
+
}) do
|
|
116
|
+
result = @app_automate._get_tiles(fullpage: true)[0]
|
|
117
|
+
assert_equal('sha', result.sha)
|
|
118
|
+
assert_equal(100, result.status_bar_height)
|
|
119
|
+
assert_equal(150, result.nav_bar_height)
|
|
120
|
+
assert_equal(120, result.header_height)
|
|
121
|
+
assert_equal(80, result.footer_height)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|