percy-appium-app 1.0.0 → 1.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 +4 -4
- data/.github/workflows/Semgrep.yml +1 -2
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/test.yml +10 -2
- data/.gitignore +3 -0
- data/Gemfile +7 -2
- data/percy/metadata/android_metadata.rb +21 -7
- data/percy/metadata/ios_metadata.rb +1 -5
- data/percy/metadata/metadata.rb +41 -6
- data/percy/metadata/metadata_resolver.rb +5 -3
- data/percy/providers/generic_provider.rb +4 -1
- data/percy/version.rb +1 -1
- data/specs/android_metadata.rb +52 -0
- data/specs/app_automate.rb +157 -2
- data/specs/app_percy.rb +27 -0
- data/specs/cli_wrapper.rb +66 -0
- data/specs/driver_metadata.rb +22 -0
- data/specs/generic_providers.rb +48 -3
- data/specs/ios_metadata.rb +30 -0
- data/specs/metadata.rb +32 -0
- data/specs/metadata_resolver.rb +45 -0
- data/specs/percy_automate.rb +65 -0
- data/specs/screenshot.rb +84 -1
- data/specs/support/coverage_check.rb +12 -0
- data/specs/support/run_spec.rb +9 -0
- data/specs/support/spec_helper.rb +15 -0
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85a82be9fb7970cea7e6bfbf92341377a6cf4647f07ce09b3a3c4bb52ff25c11
|
|
4
|
+
data.tar.gz: 03be307ae51ff85c7853cf0284ded40713a91f8bbf4ce36f8f6b7ae69165d26e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 986183165da611f44bfd356562c084bdb942d7f87a2e38418746562c1d056894e8a373c4fe3e61a5aa2c3a44a48c9c42d7701e845b30eec7727fdafbe2a78faa
|
|
7
|
+
data.tar.gz: a21a495cb3cda29740602eb262e23ef67ed7c3fce651d191d1ec91c73bc56b0540f6c8cf55772816da6acdcadf1d07c6040e35a49f4fdfae95e1ca0016ccf2fe
|
|
@@ -27,8 +27,7 @@ jobs:
|
|
|
27
27
|
|
|
28
28
|
container:
|
|
29
29
|
# A Docker image with Semgrep installed. Do not change this.
|
|
30
|
-
image: returntocorp/semgrep
|
|
31
|
-
|
|
30
|
+
image: returntocorp/semgrep:1.166.0
|
|
32
31
|
# Skip any PR created by dependabot to avoid permission issues:
|
|
33
32
|
if: (github.actor != 'dependabot[bot]')
|
|
34
33
|
|
data/.github/workflows/test.yml
CHANGED
|
@@ -2,6 +2,7 @@ name: Test
|
|
|
2
2
|
on:
|
|
3
3
|
push:
|
|
4
4
|
branches: [main]
|
|
5
|
+
pull_request:
|
|
5
6
|
workflow_dispatch:
|
|
6
7
|
inputs:
|
|
7
8
|
branch:
|
|
@@ -12,6 +13,10 @@ jobs:
|
|
|
12
13
|
test:
|
|
13
14
|
name: Test
|
|
14
15
|
strategy:
|
|
16
|
+
# Run every Ruby version to completion so one leg failing doesn't cancel
|
|
17
|
+
# the others. Ruby 3.0's appium_lib API drift is fixed by pinning
|
|
18
|
+
# appium_lib ~> 12.0 (see Gemfile), so all three legs are green.
|
|
19
|
+
fail-fast: false
|
|
15
20
|
matrix:
|
|
16
21
|
os: [ubuntu-latest]
|
|
17
22
|
ruby: ['2.6', '2.7', '3.0']
|
|
@@ -31,10 +36,13 @@ jobs:
|
|
|
31
36
|
with:
|
|
32
37
|
ruby-version: ${{matrix.ruby}}
|
|
33
38
|
bundler-cache: true
|
|
34
|
-
- uses: actions/cache@
|
|
39
|
+
- uses: actions/cache@v3
|
|
35
40
|
with:
|
|
36
41
|
path: "./vendor/bundle"
|
|
37
42
|
key: v1/${{ runner.os }}/ruby-${{ matrix.ruby }}/${{ hashFiles('**/Gemfile.lock') }}
|
|
38
43
|
restore-keys: v1/${{ runner.os }}/ruby-${{ matrix.ruby }}/
|
|
39
44
|
- run: bundle install
|
|
40
|
-
|
|
45
|
+
# Run each spec via the SimpleCov runner so per-process coverage is
|
|
46
|
+
# recorded and merged, then collate the merged result and enforce the gate.
|
|
47
|
+
- run: for file in specs/*.rb; do echo "$file"; bundle exec ruby specs/support/run_spec.rb "$file"; done
|
|
48
|
+
- run: bundle exec ruby specs/support/coverage_check.rb
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Pin appium_lib to the 12.x line so all Ruby legs (2.6/2.7/3.0) resolve
|
|
6
|
+
# appium_lib_core ~> 5.0, which still exposes Appium::Core::Base::SearchContext.
|
|
7
|
+
# Unpinned, Ruby 3.0 resolved appium_lib 15 / appium_lib_core 9.x, which removed
|
|
8
|
+
# that constant and broke the specs. appium_console 3.0.0 requires appium_lib = 12.0.0.
|
|
9
|
+
gem 'appium_console', '~> 3.0'
|
|
10
|
+
gem 'appium_lib', '~> 12.0'
|
|
7
11
|
gem 'dotenv'
|
|
8
12
|
gem 'minitest'
|
|
9
13
|
gem 'rubocop', require: false
|
|
@@ -11,3 +15,4 @@ gem 'tempfile'
|
|
|
11
15
|
gem 'webmock', '~> 3.18', '>= 3.18.1'
|
|
12
16
|
gem 'webrick', '~> 1.3', '>= 1.3.1'
|
|
13
17
|
gem 'rake', '~> 13.0'
|
|
18
|
+
gem 'simplecov', require: false
|
|
@@ -9,14 +9,26 @@ module Percy
|
|
|
9
9
|
def initialize(driver)
|
|
10
10
|
super(driver)
|
|
11
11
|
@_bars = nil
|
|
12
|
+
# Intentionally left as the original lookup: this path already degrades to
|
|
13
|
+
# driver.get_system_bars consistently across all appium_lib_core versions
|
|
14
|
+
# (the rect read yields a non-Hash, so the rect arithmetic in
|
|
15
|
+
# get_system_bars rescues to nil and falls back), so it is out of scope for
|
|
16
|
+
# the snake_case capability fix.
|
|
12
17
|
@_viewport_rect = capabilities.to_json['viewportRect']
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
def device_screen_size
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
width
|
|
19
|
-
|
|
21
|
+
# Use string keys to match the IosMetadata implementation and every
|
|
22
|
+
# consumer (generic_provider, app_automate, _get_tag), all of which read
|
|
23
|
+
# device_screen_size['width'] / ['height'].
|
|
24
|
+
device_screen_size_cap = get_capability_value('deviceScreenSize')
|
|
25
|
+
if device_screen_size_cap.nil?
|
|
26
|
+
size = driver.window_size
|
|
27
|
+
{ 'width' => size.width.to_i, 'height' => size.height.to_i }
|
|
28
|
+
else
|
|
29
|
+
width, height = device_screen_size_cap.split('x')
|
|
30
|
+
{ 'width' => width.to_i, 'height' => height.to_i }
|
|
31
|
+
end
|
|
20
32
|
end
|
|
21
33
|
|
|
22
34
|
def get_system_bars
|
|
@@ -68,11 +80,13 @@ module Percy
|
|
|
68
80
|
|
|
69
81
|
def _device_name
|
|
70
82
|
if @device_name.nil?
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
# Normalize the nested desired-caps hash too, so its keys are matched
|
|
84
|
+
# regardless of casing (camelCase or appium_lib_core 13+ snake_case).
|
|
85
|
+
desired_caps = Percy::Metadata.normalize_hash(get_capability_value('desired'))
|
|
86
|
+
device_name = desired_caps['devicename']
|
|
73
87
|
device = desired_caps['device']
|
|
74
88
|
device_name ||= device
|
|
75
|
-
device_model =
|
|
89
|
+
device_model = get_capability_value('deviceModel')
|
|
76
90
|
@device_name = device_name || device_model
|
|
77
91
|
end
|
|
78
92
|
@device_name
|
|
@@ -67,11 +67,7 @@ module Percy
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def device_name
|
|
70
|
-
if @device_name.nil?
|
|
71
|
-
caps = capabilities
|
|
72
|
-
caps = caps.as_json unless caps.is_a?(Hash)
|
|
73
|
-
@device_name = caps['deviceName']
|
|
74
|
-
end
|
|
70
|
+
@device_name = get_capability_value('deviceName') if @device_name.nil?
|
|
75
71
|
@device_name
|
|
76
72
|
end
|
|
77
73
|
|
data/percy/metadata/metadata.rb
CHANGED
|
@@ -25,19 +25,54 @@ module Percy
|
|
|
25
25
|
caps
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Normalizes a capability key so lookups are resilient to the differences
|
|
29
|
+
# across appium_lib_core versions and protocols: camelCase ("platformName"),
|
|
30
|
+
# snake_case ("platform_name", as returned by appium_lib_core 13+),
|
|
31
|
+
# SCREAMING ("PLATFORM_NAME") and the W3C vendor prefix ("appium:platformName").
|
|
32
|
+
# Note: all colons are stripped, not just the vendor-prefix one. This is safe
|
|
33
|
+
# for every known Appium capability (e.g. "bstack:options" -> "bstackoptions").
|
|
34
|
+
def self.normalize_capability_key(key)
|
|
35
|
+
key.to_s.downcase.gsub(/[_:]/, '').sub(/\Aappium/, '')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Builds a {normalized_key => value} view of a capabilities hash. First key
|
|
39
|
+
# wins, so a camelCase key (e.g. "platformName") takes precedence over a
|
|
40
|
+
# snake_case duplicate ("platform_name") when both are present, matching the
|
|
41
|
+
# previous MetadataResolver semantics.
|
|
42
|
+
def self.normalize_hash(hash)
|
|
43
|
+
normalized = {}
|
|
44
|
+
(hash || {}).each do |k, v|
|
|
45
|
+
nk = normalize_capability_key(k)
|
|
46
|
+
normalized[nk] = v unless normalized.key?(nk)
|
|
47
|
+
end
|
|
48
|
+
normalized
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Builds a {normalized_key => value} view of the driver's capabilities,
|
|
52
|
+
# coercing the appium_lib_core Capabilities object into a plain Hash first.
|
|
53
|
+
def self.normalized_capabilities(driver)
|
|
54
|
+
caps = driver.capabilities
|
|
55
|
+
caps = caps.as_json if caps.respond_to?(:as_json) && !caps.is_a?(Hash)
|
|
56
|
+
caps = caps.to_h if caps.respond_to?(:to_h) && !caps.is_a?(Hash)
|
|
57
|
+
normalize_hash(caps)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Reads capabilities fresh on every call (no memoization) so callers always
|
|
61
|
+
# observe the driver's current capabilities, matching the prior behaviour.
|
|
62
|
+
def get_capability_value(name)
|
|
63
|
+
self.class.normalized_capabilities(driver)[self.class.normalize_capability_key(name)]
|
|
64
|
+
end
|
|
65
|
+
|
|
28
66
|
def session_id
|
|
29
67
|
driver.session_id
|
|
30
68
|
end
|
|
31
69
|
|
|
32
70
|
def os_name
|
|
33
|
-
|
|
71
|
+
get_capability_value('platformName')
|
|
34
72
|
end
|
|
35
73
|
|
|
36
74
|
def os_version
|
|
37
|
-
|
|
38
|
-
caps = caps.as_json unless caps.is_a?(Hash)
|
|
39
|
-
|
|
40
|
-
os_version = caps['os_version'] || caps['platformVersion'] || ''
|
|
75
|
+
os_version = get_capability_value('os_version') || get_capability_value('platformVersion') || ''
|
|
41
76
|
os_version = @os_version || os_version
|
|
42
77
|
begin
|
|
43
78
|
os_version.to_f.to_i.to_s
|
|
@@ -51,7 +86,7 @@ module Percy
|
|
|
51
86
|
end
|
|
52
87
|
|
|
53
88
|
def get_orientation(**kwargs)
|
|
54
|
-
orientation = kwargs[:orientation] ||
|
|
89
|
+
orientation = kwargs[:orientation] || get_capability_value('orientation') || 'PORTRAIT'
|
|
55
90
|
orientation = orientation.downcase
|
|
56
91
|
orientation = orientation == 'auto' ? _orientation : orientation
|
|
57
92
|
orientation.upcase
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../exceptions/exceptions'
|
|
4
|
+
require_relative 'metadata'
|
|
4
5
|
require_relative 'android_metadata'
|
|
5
6
|
require_relative 'ios_metadata'
|
|
6
7
|
|
|
7
8
|
module Percy
|
|
8
9
|
class MetadataResolver
|
|
9
10
|
def self.resolve(driver)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
# Resolve via normalized capability keys so platformName is found
|
|
12
|
+
# regardless of casing/prefix (camelCase, snake_case as in appium_lib_core
|
|
13
|
+
# 13+, or the appium: vendor prefix).
|
|
14
|
+
platform_name = Percy::Metadata.normalized_capabilities(driver)['platformname']&.to_s&.downcase
|
|
13
15
|
case platform_name
|
|
14
16
|
when 'android'
|
|
15
17
|
Percy::AndroidMetadata.new(driver)
|
|
@@ -137,7 +137,10 @@ module Percy
|
|
|
137
137
|
|
|
138
138
|
def get_regions_by_xpath(elements_array, xpaths)
|
|
139
139
|
xpaths.each do |xpath|
|
|
140
|
-
|
|
140
|
+
# Pass the :xpath finder symbol directly. appium_lib_core 12.x removed
|
|
141
|
+
# Appium::Core::Base::SearchContext::FINDERS; the symbol form works
|
|
142
|
+
# across all supported appium_lib versions.
|
|
143
|
+
element = driver.find_element(:xpath, xpath)
|
|
141
144
|
selector = "xpath: #{xpath}"
|
|
142
145
|
if element
|
|
143
146
|
region = get_region_object(selector, element)
|
data/percy/version.rb
CHANGED
data/specs/android_metadata.rb
CHANGED
|
@@ -29,7 +29,35 @@ class TestAndroidMetadata < Minitest::Test
|
|
|
29
29
|
@mock_webdriver.expect(:capabilities, android_capabilities.merge('viewportRect' => viewport))
|
|
30
30
|
|
|
31
31
|
assert(viewport, @android_metadata.viewport)
|
|
32
|
+
@mock_webdriver.verify
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_device_screen_size_when_device_screen_size_is_nil
|
|
36
|
+
# Mock capabilities to return a hash without 'deviceScreenSize'
|
|
37
|
+
android_capabilities = get_android_capabilities
|
|
38
|
+
android_capabilities.delete('deviceScreenSize')
|
|
39
|
+
@mock_webdriver.expect(:capabilities, android_capabilities)
|
|
40
|
+
|
|
41
|
+
# Mock driver.window_size to return a double with width and height
|
|
42
|
+
mock_window_size = Minitest::Mock.new
|
|
43
|
+
mock_window_size.expect(:width, 1080)
|
|
44
|
+
mock_window_size.expect(:height, 1920)
|
|
45
|
+
@mock_webdriver.expect(:window_size, mock_window_size)
|
|
32
46
|
|
|
47
|
+
# Call the method and assert the result
|
|
48
|
+
result = @android_metadata.device_screen_size
|
|
49
|
+
assert_equal({ 'width' => 1080, 'height' => 1920 }, result)
|
|
50
|
+
|
|
51
|
+
# Verify mocks
|
|
52
|
+
mock_window_size.verify
|
|
53
|
+
@mock_webdriver.verify
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_device_screen_size_when_device_screen_size_is_present
|
|
57
|
+
# 'deviceScreenSize' => '1080x2280' from get_android_capabilities; parsed into string-keyed hash
|
|
58
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
59
|
+
result = @android_metadata.device_screen_size
|
|
60
|
+
assert_equal({ 'width' => 1080, 'height' => 2280 }, result)
|
|
33
61
|
@mock_webdriver.verify
|
|
34
62
|
end
|
|
35
63
|
|
|
@@ -74,4 +102,28 @@ class TestAndroidMetadata < Minitest::Test
|
|
|
74
102
|
def test_scale_factor
|
|
75
103
|
assert_equal(1, @android_metadata.scale_factor)
|
|
76
104
|
end
|
|
105
|
+
|
|
106
|
+
# Regression: appium_lib_core 13.x returns capabilities with snake_case keys
|
|
107
|
+
# (e.g. "device_screen_size" instead of "deviceScreenSize").
|
|
108
|
+
def test_device_screen_size_with_snake_case_caps
|
|
109
|
+
android_capabilities = get_android_capabilities
|
|
110
|
+
android_capabilities.delete('deviceScreenSize')
|
|
111
|
+
android_capabilities['device_screen_size'] = '1080x2280'
|
|
112
|
+
@mock_webdriver.expect(:capabilities, android_capabilities)
|
|
113
|
+
|
|
114
|
+
result = @android_metadata.device_screen_size
|
|
115
|
+
assert_equal({ 'width' => 1080, 'height' => 2280 }, result)
|
|
116
|
+
@mock_webdriver.verify
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Regression: the nested desired-caps hash is also normalized, so a
|
|
120
|
+
# snake_case "device_name" inside "desired" still resolves the device.
|
|
121
|
+
def test_device_name_with_snake_case_desired_caps
|
|
122
|
+
android_capabilities = get_android_capabilities
|
|
123
|
+
android_capabilities['desired'] = { 'device_name' => 'google pixel 4' }
|
|
124
|
+
@mock_webdriver.expect(:capabilities, android_capabilities)
|
|
125
|
+
@mock_webdriver.expect(:capabilities, android_capabilities)
|
|
126
|
+
|
|
127
|
+
assert_equal('google pixel 4', @android_metadata._device_name)
|
|
128
|
+
end
|
|
77
129
|
end
|
data/specs/app_automate.rb
CHANGED
|
@@ -67,15 +67,170 @@ class TestAppAutomate < Minitest::Test
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def test_execute_percy_screenshot_end
|
|
70
|
-
|
|
70
|
+
# Wrap the stub return hashes in explicit braces. Under Ruby 3 keyword
|
|
71
|
+
# argument separation, a bare trailing hash is otherwise parsed as keyword
|
|
72
|
+
# args to Minitest's stub, raising ArgumentError.
|
|
73
|
+
@app_automate.stub(:execute_percy_screenshot_begin, { 'deviceName' => 'abc', 'osVersion' => '123' }) do
|
|
71
74
|
@app_automate.stub(:execute_percy_screenshot_end, nil) do
|
|
72
|
-
@app_automate.stub(:screenshot, 'link' => 'https://link') do
|
|
75
|
+
@app_automate.stub(:screenshot, { 'link' => 'https://link' }) do
|
|
73
76
|
@app_automate.screenshot('name')
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
end
|
|
77
80
|
end
|
|
78
81
|
|
|
82
|
+
# Covers app_automate.rb lines 34-36: the rescue branch in #screenshot where the
|
|
83
|
+
# super call fails, #execute_percy_screenshot_end is invoked with 'failure', and the
|
|
84
|
+
# error is re-raised.
|
|
85
|
+
def test_screenshot_failure_calls_end_and_reraises
|
|
86
|
+
captured = {}
|
|
87
|
+
@app_automate.stub(:execute_percy_screenshot_begin, nil) do
|
|
88
|
+
# super (GenericProvider#screenshot) calls _get_tiles first; make it blow up.
|
|
89
|
+
@app_automate.stub(:_get_tiles, ->(**_kwargs) { raise StandardError, 'boom' }) do
|
|
90
|
+
@app_automate.stub(:execute_percy_screenshot_end,
|
|
91
|
+
lambda do |name, url, status, sync = nil, message = nil|
|
|
92
|
+
captured[:name] = name
|
|
93
|
+
captured[:url] = url
|
|
94
|
+
captured[:status] = status
|
|
95
|
+
captured[:sync] = sync
|
|
96
|
+
captured[:message] = message
|
|
97
|
+
end) do
|
|
98
|
+
error = assert_raises(StandardError) { @app_automate.screenshot('failing-shot') }
|
|
99
|
+
assert_equal 'boom', error.message
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
assert_equal 'failing-shot', captured[:name]
|
|
104
|
+
assert_equal 'failure', captured[:status]
|
|
105
|
+
assert_equal 'boom', captured[:message]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Covers app_automate.rb lines 24-26: when begin returns session details, the device
|
|
109
|
+
# metadata and debug url are set before the super screenshot (driven through the stubbed
|
|
110
|
+
# collaborators _get_tiles/_find_regions/_post_screenshots) succeeds.
|
|
111
|
+
def test_screenshot_success_sets_metadata_and_debug_url
|
|
112
|
+
session_details = {
|
|
113
|
+
'deviceName' => 'Google Pixel 7',
|
|
114
|
+
'osVersion' => '13.0',
|
|
115
|
+
'buildHash' => 'bhash',
|
|
116
|
+
'sessionHash' => 'shash'
|
|
117
|
+
}
|
|
118
|
+
post_response = { 'link' => 'https://link', 'data' => { 'ok' => true } }
|
|
119
|
+
@app_automate.stub(:execute_percy_screenshot_begin, session_details) do
|
|
120
|
+
@app_automate.stub(:_get_tiles, []) do
|
|
121
|
+
@app_automate.stub(:_get_tag, {}) do
|
|
122
|
+
@app_automate.stub(:_find_regions, []) do
|
|
123
|
+
@app_automate.stub(:_post_screenshots, post_response) do
|
|
124
|
+
@app_automate.stub(:execute_percy_screenshot_end, nil) do
|
|
125
|
+
result = @app_automate.screenshot('shot')
|
|
126
|
+
assert_equal({ 'ok' => true }, result)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
assert_equal 'Google Pixel 7', @metadata.device_name
|
|
134
|
+
@mock_webdriver.expect(:capabilities, get_android_capabilities)
|
|
135
|
+
assert_equal '13', @metadata.os_version
|
|
136
|
+
assert_equal 'https://app-automate.browserstack.com/dashboard/v2/builds/bhash/sessions/shash',
|
|
137
|
+
@app_automate.get_debug_url
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Covers app_automate.rb line 50: when PERCY_DISABLE_REMOTE_UPLOADS=true and the
|
|
141
|
+
# screenshot is NOT fullpage, _get_tiles falls back to the generic provider's _get_tiles
|
|
142
|
+
# (run for real against the mock webdriver), returning a single file-backed tile.
|
|
143
|
+
def test_get_tiles_disable_remote_uploads_non_fullpage_falls_back_to_super
|
|
144
|
+
ENV['PERCY_DISABLE_REMOTE_UPLOADS'] = 'true'
|
|
145
|
+
@mock_webdriver.expect(:screenshot_as, 'png-bytes', [:png])
|
|
146
|
+
|
|
147
|
+
tiles = nil
|
|
148
|
+
@metadata.stub(:status_bar_height, 100) do
|
|
149
|
+
@metadata.stub(:navigation_bar_height, 150) do
|
|
150
|
+
tiles = @app_automate._get_tiles(fullpage: false)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
assert_equal 1, tiles.length
|
|
155
|
+
dict_tile = tiles[0].to_h
|
|
156
|
+
assert_includes dict_tile, 'filepath'
|
|
157
|
+
assert(File.exist?(dict_tile['filepath']))
|
|
158
|
+
assert_equal 100, dict_tile['status_bar_height']
|
|
159
|
+
assert_equal 150, dict_tile['nav_bar_height']
|
|
160
|
+
File.delete(tiles[0].filepath)
|
|
161
|
+
ensure
|
|
162
|
+
ENV.delete('PERCY_DISABLE_REMOTE_UPLOADS')
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Covers app_automate.rb lines 49 (fullpage warning puts) and the path that does NOT
|
|
166
|
+
# fall back to super (line 50 skipped because fullpage_ss is true): full remote
|
|
167
|
+
# screenshot orchestration continues with PERCY_DISABLE_REMOTE_UPLOADS=true.
|
|
168
|
+
def test_get_tiles_disable_remote_uploads_fullpage_warns_and_continues
|
|
169
|
+
ENV['PERCY_DISABLE_REMOTE_UPLOADS'] = 'true'
|
|
170
|
+
metadata_mock = Minitest::Mock.new
|
|
171
|
+
metadata_mock.expect(:device_screen_size, { 'width' => 1080, 'height' => 1920 })
|
|
172
|
+
metadata_mock.expect(:scale_factor, 1)
|
|
173
|
+
metadata_mock.expect(:status_bar_height, 100)
|
|
174
|
+
metadata_mock.expect(:navigation_bar_height, 150)
|
|
175
|
+
|
|
176
|
+
@metadata.stub(:device_screen_size, { 'width' => 1080, 'height' => 1920 }) do
|
|
177
|
+
@metadata.stub(:scale_factor, 1) do
|
|
178
|
+
@metadata.stub(:status_bar_height, 100) do
|
|
179
|
+
@metadata.stub(:navigation_bar_height, 150) do
|
|
180
|
+
@app_automate.stub(:execute_percy_screenshot, {
|
|
181
|
+
'result' => '[{"sha":"abc-1234","header_height":10,"footer_height":20}]'
|
|
182
|
+
}) do
|
|
183
|
+
tiles = @app_automate._get_tiles(fullpage: true)
|
|
184
|
+
assert_equal 1, tiles.length
|
|
185
|
+
assert_equal 'abc', tiles[0].sha
|
|
186
|
+
assert_equal 100, tiles[0].status_bar_height
|
|
187
|
+
assert_equal 150, tiles[0].nav_bar_height
|
|
188
|
+
assert_equal 10, tiles[0].header_height
|
|
189
|
+
assert_equal 20, tiles[0].footer_height
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
ensure
|
|
196
|
+
ENV.delete('PERCY_DISABLE_REMOTE_UPLOADS')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Covers app_automate.rb lines 100-104: the rescue in #execute_percy_screenshot_begin
|
|
200
|
+
# when execute_script raises; it logs and returns nil.
|
|
201
|
+
def test_execute_percy_screenshot_begin_handles_error_returns_nil
|
|
202
|
+
@mock_webdriver.expect(:execute_script, nil) do
|
|
203
|
+
raise StandardError, 'begin failed'
|
|
204
|
+
end
|
|
205
|
+
result = @app_automate.execute_percy_screenshot_begin('Screenshot 1')
|
|
206
|
+
assert_nil result
|
|
207
|
+
@mock_webdriver.verify
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Covers app_automate.rb lines 123-125: the rescue in #execute_percy_screenshot_end
|
|
211
|
+
# when execute_script raises; it logs and swallows the error (returns nil).
|
|
212
|
+
def test_execute_percy_screenshot_end_handles_error
|
|
213
|
+
@mock_webdriver.expect(:execute_script, nil) do
|
|
214
|
+
raise StandardError, 'end failed'
|
|
215
|
+
end
|
|
216
|
+
result = @app_automate.execute_percy_screenshot_end('Screenshot 1', 'snapshot-url', 'success')
|
|
217
|
+
assert_nil result
|
|
218
|
+
@mock_webdriver.verify
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Covers app_automate.rb lines 156-159: the rescue in #execute_percy_screenshot when
|
|
222
|
+
# execute_script raises; it logs and re-raises the error.
|
|
223
|
+
def test_execute_percy_screenshot_handles_error_reraises
|
|
224
|
+
@mock_webdriver.expect(:execute_script, nil) do
|
|
225
|
+
raise StandardError, 'screenshot failed'
|
|
226
|
+
end
|
|
227
|
+
error = assert_raises(StandardError) do
|
|
228
|
+
@app_automate.execute_percy_screenshot(1080, 'singlepage', 5)
|
|
229
|
+
end
|
|
230
|
+
assert_equal 'screenshot failed', error.message
|
|
231
|
+
@mock_webdriver.verify
|
|
232
|
+
end
|
|
233
|
+
|
|
79
234
|
def test_get_tiles
|
|
80
235
|
# Mocking Percy::Metadata's session_id method
|
|
81
236
|
metadata_mock = Minitest::Mock.new
|
data/specs/app_percy.rb
CHANGED
|
@@ -136,6 +136,33 @@ class TestAppPercy < Minitest::Test
|
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
def test_throws_error_when_string_arg_type_mismatch
|
|
140
|
+
# Three constructions, each followed by a screenshot call with a
|
|
141
|
+
# non-String value for test_case / labels / th_test_case_execution_id.
|
|
142
|
+
3.times do
|
|
143
|
+
@mock_android_webdriver.expect(:is_a?, true, [Appium::Core::Base::Driver])
|
|
144
|
+
end
|
|
145
|
+
20.times do
|
|
146
|
+
@mock_android_webdriver.expect(:capabilities, get_android_capabilities)
|
|
147
|
+
end
|
|
148
|
+
3.times do
|
|
149
|
+
@mock_android_webdriver.expect(:instance_variable_get, @bridge, [:@bridge])
|
|
150
|
+
@http.expect(:instance_variable_get, @server_url, [:@server_url])
|
|
151
|
+
@bridge.expect(:instance_variable_get, @http, [:@http])
|
|
152
|
+
@server_url.expect(:to_s, 'https://hub-cloud.browserstack.com/wd/hub')
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
assert_raises(TypeError) do
|
|
156
|
+
Percy::AppPercy.new(@mock_android_webdriver).screenshot('screenshot 1', test_case: 123)
|
|
157
|
+
end
|
|
158
|
+
assert_raises(TypeError) do
|
|
159
|
+
Percy::AppPercy.new(@mock_android_webdriver).screenshot('screenshot 1', labels: 123)
|
|
160
|
+
end
|
|
161
|
+
assert_raises(TypeError) do
|
|
162
|
+
Percy::AppPercy.new(@mock_android_webdriver).screenshot('screenshot 1', th_test_case_execution_id: 123)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
139
166
|
private
|
|
140
167
|
|
|
141
168
|
def disable_percy_options(mock_webdriver, num = 1)
|
data/specs/cli_wrapper.rb
CHANGED
|
@@ -16,6 +16,20 @@ def mock_poa_screenshot(fail: false)
|
|
|
16
16
|
.to_return(body: "{\"success\": #{fail ? 'false, "error": "test"' : 'true'}}", status: (fail ? 500 : 200))
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def mock_healthcheck_with_version(version)
|
|
20
|
+
stub_request(:get, 'http://localhost:5338/percy/healthcheck')
|
|
21
|
+
.to_return(
|
|
22
|
+
body: JSON.dump(success: true, build: { 'id' => '123', 'url' => 'dummy_url' }, type: 'automate'),
|
|
23
|
+
status: 200,
|
|
24
|
+
headers: { 'X-Percy-Core-Version' => version }
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def mock_failed_event(fail: false)
|
|
29
|
+
stub_request(:post, 'http://localhost:5338/percy/events')
|
|
30
|
+
.to_return(body: "{\"success\": #{fail ? 'false, "error": "test"' : 'true'}}", status: (fail ? 500 : 200))
|
|
31
|
+
end
|
|
32
|
+
|
|
19
33
|
# Test suite for the Percy::CLIWrapper class
|
|
20
34
|
class TestCLIWrapper < Minitest::Test
|
|
21
35
|
def setup
|
|
@@ -143,4 +157,56 @@ class TestCLIWrapper < Minitest::Test
|
|
|
143
157
|
assert_nil response['labels']
|
|
144
158
|
assert_nil response['th_test_case_execution_id']
|
|
145
159
|
end
|
|
160
|
+
|
|
161
|
+
# Targets cli_wrapper.rb lines 42-43: CLI minor version below 27 disables the SDK.
|
|
162
|
+
def test_percy_enabled_with_unsupported_minor_version
|
|
163
|
+
Percy::CLIWrapper.instance_variable_set(:@percy_enabled, nil)
|
|
164
|
+
mock_healthcheck_with_version('1.26.0')
|
|
165
|
+
|
|
166
|
+
assert_output(/Please upgrade to the latest CLI version/) do
|
|
167
|
+
assert_equal(false, Percy::CLIWrapper.percy_enabled?)
|
|
168
|
+
end
|
|
169
|
+
ensure
|
|
170
|
+
Percy::CLIWrapper.instance_variable_set(:@percy_enabled, nil)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Targets cli_wrapper.rb lines 74, 79, 80, 89: post_failed_event success path
|
|
174
|
+
# builds the body, posts to /percy/events and parses the response.
|
|
175
|
+
def test_post_failed_event_success
|
|
176
|
+
mock_failed_event
|
|
177
|
+
|
|
178
|
+
result = Percy::CLIWrapper.post_failed_event('some error')
|
|
179
|
+
|
|
180
|
+
assert_equal({ 'success' => true }, result)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Targets cli_wrapper.rb lines 83-86 (error branch) and 91-92 (rescue -> nil):
|
|
184
|
+
# a non-200 response raises CLIException internally which is rescued and nil returned.
|
|
185
|
+
def test_post_failed_event_handles_error_response
|
|
186
|
+
mock_failed_event(fail: true)
|
|
187
|
+
|
|
188
|
+
assert_nil Percy::CLIWrapper.post_failed_event('some error')
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Targets cli_wrapper.rb lines 91-92: a network/parse failure is rescued and nil returned.
|
|
192
|
+
def test_post_failed_event_rescues_exceptions
|
|
193
|
+
stub_request(:post, 'http://localhost:5338/percy/events').to_raise(StandardError.new('boom'))
|
|
194
|
+
|
|
195
|
+
assert_nil Percy::CLIWrapper.post_failed_event('some error')
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Targets cli_wrapper.rb lines 121-122: response is a 2xx success but not exactly 200,
|
|
199
|
+
# so the error message is extracted from the body and raised.
|
|
200
|
+
def test_post_poa_screenshots_raises_on_non_200_success
|
|
201
|
+
stub_request(:post, 'http://localhost:5338/percy/automateScreenshot')
|
|
202
|
+
.to_return(body: '{"success": false, "error": "poa error"}', status: 201)
|
|
203
|
+
|
|
204
|
+
error = assert_raises(CLIException) do
|
|
205
|
+
@cli_wrapper.post_poa_screenshots(
|
|
206
|
+
'snapshot', 'session-id', 'http://example.com',
|
|
207
|
+
{ 'browser' => 'chrome' }, { 'platform' => 'Windows' }, {}
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
assert_equal('poa error', error.message)
|
|
211
|
+
end
|
|
146
212
|
end
|
data/specs/driver_metadata.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'minitest/mock'
|
|
|
5
5
|
require 'appium_lib'
|
|
6
6
|
|
|
7
7
|
require_relative '../percy/metadata/driver_metadata'
|
|
8
|
+
require_relative '../percy/lib/cache'
|
|
8
9
|
|
|
9
10
|
# Test suite for the Percy::DriverMetadata class
|
|
10
11
|
class TestDriverMetadata < Minitest::Test
|
|
@@ -66,4 +67,25 @@ class TestDriverMetadata < Minitest::Test
|
|
|
66
67
|
|
|
67
68
|
assert(session_caps, @metadata.session_capabilities)
|
|
68
69
|
end
|
|
70
|
+
|
|
71
|
+
def test_session_capabilities_caches_desired_capabilities_on_cache_miss
|
|
72
|
+
# Force a clean cache and use a unique session id so the cache lookup misses
|
|
73
|
+
# and the desired_capabilities branch (set_cache) is exercised.
|
|
74
|
+
Percy::Cache.force_cleanup_cache
|
|
75
|
+
session_id = 'session_id_session_caps_miss'
|
|
76
|
+
desired_caps = {
|
|
77
|
+
'platform' => 'chrome_android',
|
|
78
|
+
'browserVersion' => '115.0.1',
|
|
79
|
+
'session_name' => 'abc'
|
|
80
|
+
}
|
|
81
|
+
# session_id is read by session_capabilities, get_cache and set_cache.
|
|
82
|
+
3.times { @mock_webdriver.expect(:session_id, session_id) }
|
|
83
|
+
@mock_webdriver.expect(:desired_capabilities, desired_caps)
|
|
84
|
+
|
|
85
|
+
fetched = @metadata.session_capabilities
|
|
86
|
+
assert_equal(desired_caps, fetched)
|
|
87
|
+
# Now the value is cached: read straight back without another driver call.
|
|
88
|
+
@mock_webdriver.expect(:session_id, session_id)
|
|
89
|
+
assert_equal(desired_caps, @metadata.session_capabilities)
|
|
90
|
+
end
|
|
69
91
|
end
|
data/specs/generic_providers.rb
CHANGED
|
@@ -13,6 +13,13 @@ class TestGenericProvider < Minitest::Test
|
|
|
13
13
|
include Appium
|
|
14
14
|
COMPARISON_RESPONSE = { 'comparison' => { 'id' => 123, 'url' => 'https://percy-build-url' } }.freeze
|
|
15
15
|
|
|
16
|
+
# Some tests below permanently redefine Percy::Metadata#session_id and
|
|
17
|
+
# Percy::AndroidMetadata#get_system_bars via class_eval to route them at fixed-count
|
|
18
|
+
# mocks. Snapshot the pristine implementations once so teardown can restore them and the
|
|
19
|
+
# suite stays order-independent (the original suite is flaky on certain seeds otherwise).
|
|
20
|
+
PRISTINE_SESSION_ID = Percy::Metadata.instance_method(:session_id)
|
|
21
|
+
PRISTINE_GET_SYSTEM_BARS = Percy::AndroidMetadata.instance_method(:get_system_bars)
|
|
22
|
+
|
|
16
23
|
def setup
|
|
17
24
|
@existing_dir = 'existing-dir'
|
|
18
25
|
teardown
|
|
@@ -37,6 +44,20 @@ class TestGenericProvider < Minitest::Test
|
|
|
37
44
|
if Dir.exist?(@existing_dir)
|
|
38
45
|
FileUtils.remove_dir(@existing_dir, force: true)
|
|
39
46
|
end
|
|
47
|
+
restore_pristine_metadata_methods
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Undo the class_eval monkey-patching done by tests like test_post_screenshots and
|
|
51
|
+
# test_non_app_automate so subsequent tests see the real implementations.
|
|
52
|
+
def restore_pristine_metadata_methods
|
|
53
|
+
pristine_session_id = PRISTINE_SESSION_ID
|
|
54
|
+
pristine_get_system_bars = PRISTINE_GET_SYSTEM_BARS
|
|
55
|
+
Percy::Metadata.class_eval do
|
|
56
|
+
define_method(:session_id) { |*args, &blk| pristine_session_id.bind(self).call(*args, &blk) }
|
|
57
|
+
end
|
|
58
|
+
Percy::AndroidMetadata.class_eval do
|
|
59
|
+
define_method(:get_system_bars) { |*args, &blk| pristine_get_system_bars.bind(self).call(*args, &blk) }
|
|
60
|
+
end
|
|
40
61
|
end
|
|
41
62
|
|
|
42
63
|
def test_get_dir_without_env_variable
|
|
@@ -133,6 +154,30 @@ class TestGenericProvider < Minitest::Test
|
|
|
133
154
|
File.delete(tile.filepath)
|
|
134
155
|
end
|
|
135
156
|
|
|
157
|
+
# Covers generic_provider.rb line 75: requesting a fullpage screenshot on the generic
|
|
158
|
+
# provider logs the "only supported on App Automate" fallback message and still produces
|
|
159
|
+
# a single page tile.
|
|
160
|
+
def test_get_tiles_fullpage_logs_fallback
|
|
161
|
+
status_bar_height = 11
|
|
162
|
+
nav_bar_height = 22
|
|
163
|
+
logged = []
|
|
164
|
+
@generic_provider.stub(:log, ->(message, **_kwargs) { logged << message }) do
|
|
165
|
+
tile = @generic_provider._get_tiles(
|
|
166
|
+
fullpage: true,
|
|
167
|
+
status_bar_height: status_bar_height,
|
|
168
|
+
nav_bar_height: nav_bar_height
|
|
169
|
+
)[0]
|
|
170
|
+
dict_tile = tile.to_h
|
|
171
|
+
assert_includes dict_tile, 'filepath'
|
|
172
|
+
assert(File.exist?(dict_tile['filepath']))
|
|
173
|
+
assert_equal status_bar_height, dict_tile['status_bar_height']
|
|
174
|
+
assert_equal nav_bar_height, dict_tile['nav_bar_height']
|
|
175
|
+
File.delete(tile.filepath)
|
|
176
|
+
end
|
|
177
|
+
assert_includes logged,
|
|
178
|
+
'Full page screenshot is only supported on App Automate. Falling back to single page screenshot.'
|
|
179
|
+
end
|
|
180
|
+
|
|
136
181
|
def test_get_tiles_kwargs
|
|
137
182
|
status_bar_height = 135
|
|
138
183
|
nav_bar_height = 246
|
|
@@ -256,7 +301,7 @@ class TestGenericProvider < Minitest::Test
|
|
|
256
301
|
end
|
|
257
302
|
|
|
258
303
|
@mock_webdriver.expect(:find_element, mock_element,
|
|
259
|
-
[
|
|
304
|
+
[:xpath, '//path/to/element'])
|
|
260
305
|
|
|
261
306
|
elements_array = []
|
|
262
307
|
xpaths = ['//path/to/element']
|
|
@@ -271,7 +316,7 @@ class TestGenericProvider < Minitest::Test
|
|
|
271
316
|
end
|
|
272
317
|
|
|
273
318
|
def test_get_regions_by_xpath_with_non_existing_element
|
|
274
|
-
@mock_webdriver.expect(:find_element, [
|
|
319
|
+
@mock_webdriver.expect(:find_element, [:xpath, '//path/to/element']) do
|
|
275
320
|
raise Appium::Core::Error::NoSuchElementError, 'Test error'
|
|
276
321
|
end
|
|
277
322
|
elements_array = []
|
|
@@ -309,7 +354,7 @@ class TestGenericProvider < Minitest::Test
|
|
|
309
354
|
mock_element.expect(:size, Selenium::WebDriver::Dimension.new(100, 200))
|
|
310
355
|
end
|
|
311
356
|
|
|
312
|
-
@mock_webdriver.expect(:find_element, [
|
|
357
|
+
@mock_webdriver.expect(:find_element, [:accessibility_id, 'id1']) do
|
|
313
358
|
raise Appium::Core::Error::NoSuchElementError, 'Test error'
|
|
314
359
|
end
|
|
315
360
|
|
data/specs/ios_metadata.rb
CHANGED
|
@@ -69,6 +69,25 @@ class TestIOSMetadata < Minitest::Test
|
|
|
69
69
|
assert_equal({ 'height' => 40 }, status_bar)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
def test_status_bar_from_viewport_top
|
|
73
|
+
# Seed a viewport with a non-zero `top` so status_bar uses the viewport
|
|
74
|
+
# height (view_port['top']) instead of the static config scale_factor path.
|
|
75
|
+
Percy::Cache.force_cleanup_cache
|
|
76
|
+
session_id = 'session_id_viewport_top'
|
|
77
|
+
Percy::Cache.set_cache(session_id, Percy::Cache::VIEWPORT, { 'top' => 30, 'height' => 200, 'width' => 100 })
|
|
78
|
+
|
|
79
|
+
# status_bar -> viewport reads the seeded cache (1 session_id call); since
|
|
80
|
+
# top != 0 it returns view_port['top'] directly.
|
|
81
|
+
@mock_webdriver.expect(:session_id, session_id)
|
|
82
|
+
|
|
83
|
+
status_bar = @ios_metadata.status_bar
|
|
84
|
+
assert_equal({ 'height' => 30 }, status_bar)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_navigation_bar
|
|
88
|
+
assert_equal({ 'height' => 0 }, @ios_metadata.navigation_bar)
|
|
89
|
+
end
|
|
90
|
+
|
|
72
91
|
def test_scale_factor_present_in_devices_json
|
|
73
92
|
@mock_webdriver.expect(:capabilities, { 'deviceName' => 'iPhone 6' })
|
|
74
93
|
assert_equal(2, @ios_metadata.scale_factor)
|
|
@@ -86,4 +105,15 @@ class TestIOSMetadata < Minitest::Test
|
|
|
86
105
|
|
|
87
106
|
assert_equal(2, @ios_metadata.scale_factor)
|
|
88
107
|
end
|
|
108
|
+
|
|
109
|
+
def test_device_name_with_camel_case_caps
|
|
110
|
+
@mock_webdriver.expect(:capabilities, { 'deviceName' => 'iPhone 14' })
|
|
111
|
+
assert_equal('iPhone 14', @ios_metadata.device_name)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Regression: appium_lib_core 13.x returns capabilities with snake_case keys.
|
|
115
|
+
def test_device_name_with_snake_case_caps
|
|
116
|
+
@mock_webdriver.expect(:capabilities, { 'device_name' => 'iPhone 14' })
|
|
117
|
+
assert_equal('iPhone 14', @ios_metadata.device_name)
|
|
118
|
+
end
|
|
89
119
|
end
|
data/specs/metadata.rb
CHANGED
|
@@ -108,6 +108,16 @@ class TestMetadata < Minitest::Test
|
|
|
108
108
|
assert_equal('10', os_ver)
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
def test_metadata_os_version_returns_empty_string_on_error
|
|
112
|
+
# os_version that does not respond to #to_f (a Hash) makes the
|
|
113
|
+
# `os_version.to_f.to_i.to_s` conversion raise, exercising the rescue path.
|
|
114
|
+
capabilities = { 'os_version' => {} }
|
|
115
|
+
@mock_webdriver.expect(:capabilities, capabilities)
|
|
116
|
+
@mock_webdriver.expect(:capabilities, capabilities)
|
|
117
|
+
@mock_webdriver.expect(:capabilities, capabilities)
|
|
118
|
+
assert_equal('', @metadata.os_version)
|
|
119
|
+
end
|
|
120
|
+
|
|
111
121
|
def test_metadata_value_from_devices_info_for_android
|
|
112
122
|
android_device = 'google pixel 7'
|
|
113
123
|
android_device_info = { '13' => { 'status_bar' => '118', 'nav_bar' => '63' } }
|
|
@@ -127,4 +137,26 @@ class TestMetadata < Minitest::Test
|
|
|
127
137
|
@metadata.value_from_devices_info('scale_factor', ios_device)
|
|
128
138
|
)
|
|
129
139
|
end
|
|
140
|
+
|
|
141
|
+
def test_normalize_capability_key
|
|
142
|
+
# camelCase, snake_case, SCREAMING and the appium: vendor prefix all
|
|
143
|
+
# collapse to the same normalized key.
|
|
144
|
+
%w[platformName platform_name PLATFORM_NAME appium:platformName].each do |key|
|
|
145
|
+
assert_equal('platformname', Percy::Metadata.normalize_capability_key(key))
|
|
146
|
+
end
|
|
147
|
+
assert_equal('platformname', Percy::Metadata.normalize_capability_key(:platformName))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Regression: appium_lib_core 13.x returns capabilities with snake_case keys.
|
|
151
|
+
def test_metadata_os_name_with_snake_case_caps
|
|
152
|
+
@mock_webdriver.expect(:capabilities, { 'platform_name' => 'Android' })
|
|
153
|
+
assert_equal('Android', @metadata.os_name)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_metadata_os_version_with_snake_case_platform_version
|
|
157
|
+
capabilities = { 'platform_version' => '14' }
|
|
158
|
+
@mock_webdriver.expect(:capabilities, capabilities)
|
|
159
|
+
@mock_webdriver.expect(:capabilities, capabilities)
|
|
160
|
+
assert_equal('14', @metadata.os_version)
|
|
161
|
+
end
|
|
130
162
|
end
|
data/specs/metadata_resolver.rb
CHANGED
|
@@ -20,6 +20,24 @@ class MetadataResolverTestCase < Minitest::Test
|
|
|
20
20
|
assert_instance_of(Percy::AndroidMetadata, resolved_metadata)
|
|
21
21
|
@mock_webdriver.verify
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
def test_android_resolved_with_PLATFORM_NAME
|
|
25
|
+
@mock_webdriver.expect(:capabilities, { 'PLATFORM_NAME' => 'Android' })
|
|
26
|
+
@mock_webdriver.expect(:capabilities, { 'PLATFORM_NAME' => 'Android' })
|
|
27
|
+
resolved_metadata = Percy::MetadataResolver.resolve(@mock_webdriver)
|
|
28
|
+
|
|
29
|
+
assert_instance_of(Percy::AndroidMetadata, resolved_metadata)
|
|
30
|
+
@mock_webdriver.verify
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_android_resolved_with_platform_name
|
|
34
|
+
@mock_webdriver.expect(:capabilities, { 'platform_name' => 'Android' })
|
|
35
|
+
@mock_webdriver.expect(:capabilities, { 'platform_name' => 'Android' })
|
|
36
|
+
resolved_metadata = Percy::MetadataResolver.resolve(@mock_webdriver)
|
|
37
|
+
|
|
38
|
+
assert_instance_of(Percy::AndroidMetadata, resolved_metadata)
|
|
39
|
+
@mock_webdriver.verify
|
|
40
|
+
end
|
|
23
41
|
|
|
24
42
|
def test_ios_resolved
|
|
25
43
|
@mock_webdriver.expect(:capabilities, { 'platformName' => 'iOS' })
|
|
@@ -29,6 +47,23 @@ class MetadataResolverTestCase < Minitest::Test
|
|
|
29
47
|
@mock_webdriver.verify
|
|
30
48
|
end
|
|
31
49
|
|
|
50
|
+
def test_ios_resolved_with_platform_name
|
|
51
|
+
@mock_webdriver.expect(:capabilities, { 'platform_name' => 'iOS' })
|
|
52
|
+
resolved_metadata = Percy::MetadataResolver.resolve(@mock_webdriver)
|
|
53
|
+
|
|
54
|
+
assert_instance_of(Percy::IOSMetadata, resolved_metadata)
|
|
55
|
+
@mock_webdriver.verify
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_platformName_precedence_over_platform_name
|
|
59
|
+
@mock_webdriver.expect(:capabilities, { 'platformName' => 'Android', 'platform_name' => 'iOS' })
|
|
60
|
+
@mock_webdriver.expect(:capabilities, { 'platformName' => 'Android', 'platform_name' => 'iOS' })
|
|
61
|
+
resolved_metadata = Percy::MetadataResolver.resolve(@mock_webdriver)
|
|
62
|
+
|
|
63
|
+
assert_instance_of(Percy::AndroidMetadata, resolved_metadata)
|
|
64
|
+
@mock_webdriver.verify
|
|
65
|
+
end
|
|
66
|
+
|
|
32
67
|
def test_unknown_platform_exception
|
|
33
68
|
@mock_webdriver.expect(:capabilities, { 'platformName' => 'Something Random' })
|
|
34
69
|
|
|
@@ -38,4 +73,14 @@ class MetadataResolverTestCase < Minitest::Test
|
|
|
38
73
|
|
|
39
74
|
@mock_webdriver.verify
|
|
40
75
|
end
|
|
76
|
+
|
|
77
|
+
def test_unknown_platform_exception_with_platform_name
|
|
78
|
+
@mock_webdriver.expect(:capabilities, { 'platform_name' => 'Something Random' })
|
|
79
|
+
|
|
80
|
+
assert_raises(PlatformNotSupported) do
|
|
81
|
+
Percy::MetadataResolver.resolve(@mock_webdriver)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@mock_webdriver.verify
|
|
85
|
+
end
|
|
41
86
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'webmock/minitest'
|
|
6
|
+
require 'appium_lib'
|
|
7
|
+
|
|
8
|
+
require_relative '../percy/lib/percy_automate'
|
|
9
|
+
require_relative '../percy/lib/percy_options'
|
|
10
|
+
require_relative '../percy/lib/cli_wrapper'
|
|
11
|
+
require_relative 'mocks/mock_methods'
|
|
12
|
+
|
|
13
|
+
def mock_poa_screenshot(fail: false)
|
|
14
|
+
stub_request(:post, 'http://localhost:5338/percy/automateScreenshot')
|
|
15
|
+
.to_return(body: "{\"success\": #{fail ? 'false, "error": "test"' : 'true'}}", status: (fail ? 500 : 200))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Test suite for the Percy::PercyOnAutomate class
|
|
19
|
+
class TestPercyOnAutomate < Minitest::Test
|
|
20
|
+
def setup
|
|
21
|
+
@mock_driver = Minitest::Mock.new
|
|
22
|
+
@bridge = Minitest::Mock.new
|
|
23
|
+
@http = Minitest::Mock.new
|
|
24
|
+
@server_url = Minitest::Mock.new
|
|
25
|
+
WebMock.enable!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def teardown
|
|
29
|
+
WebMock.disable!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Targets percy_automate.rb line 17: an unsupported driver raises DriverNotSupported.
|
|
33
|
+
def test_initialize_raises_for_unsupported_driver
|
|
34
|
+
assert_raises(DriverNotSupported) do
|
|
35
|
+
Percy::PercyOnAutomate.new(Object.new)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Targets percy_automate.rb lines 56-58: when the screenshot post fails, the error
|
|
40
|
+
# is rescued and logged rather than propagated.
|
|
41
|
+
def test_screenshot_rescues_and_logs_on_failure
|
|
42
|
+
mock_poa_screenshot(fail: true)
|
|
43
|
+
|
|
44
|
+
@mock_driver.expect(:is_a?, true, [Appium::Core::Base::Driver])
|
|
45
|
+
@mock_driver.expect(:capabilities, { 'percy:options' => { 'enabled' => true } })
|
|
46
|
+
|
|
47
|
+
5.times do
|
|
48
|
+
@mock_driver.expect(:session_id, 'Dummy_session_id')
|
|
49
|
+
end
|
|
50
|
+
@mock_driver.expect(:instance_variable_get, @bridge, [:@bridge])
|
|
51
|
+
@http.expect(:instance_variable_get, @server_url, [:@server_url])
|
|
52
|
+
@bridge.expect(:instance_variable_get, @http, [:@http])
|
|
53
|
+
@server_url.expect(:to_s, 'https://hub-cloud.browserstack.com/wd/hub')
|
|
54
|
+
@mock_driver.expect(:capabilities, { 'key' => 'value' })
|
|
55
|
+
@mock_driver.expect(:desired_capabilities, { 'key' => 'value' })
|
|
56
|
+
|
|
57
|
+
percy = Percy::PercyOnAutomate.new(@mock_driver)
|
|
58
|
+
|
|
59
|
+
result = nil
|
|
60
|
+
assert_output(/Could not take Screenshot 'Snapshot 1'/) do
|
|
61
|
+
result = percy.screenshot('Snapshot 1', options: {})
|
|
62
|
+
end
|
|
63
|
+
assert_nil result
|
|
64
|
+
end
|
|
65
|
+
end
|
data/specs/screenshot.rb
CHANGED
|
@@ -383,7 +383,7 @@ class TestPercyScreenshot < Minitest::Test
|
|
|
383
383
|
})
|
|
384
384
|
end
|
|
385
385
|
driver.expect(:find_element, mock_element,
|
|
386
|
-
[
|
|
386
|
+
[:xpath, '//path/to/element'])
|
|
387
387
|
|
|
388
388
|
percy_screenshot(driver, 'screenshot 1', ignore_regions_xpaths: xpaths)
|
|
389
389
|
|
|
@@ -416,4 +416,87 @@ class TestPercyScreenshot < Minitest::Test
|
|
|
416
416
|
mock_healthcheck(type: 'automate')
|
|
417
417
|
assert_raises(Exception) { percy_screenshot(@mock_webdriver) }
|
|
418
418
|
end
|
|
419
|
+
|
|
420
|
+
def test_swallows_screenshot_error_when_ignore_errors_true
|
|
421
|
+
mock_healthcheck
|
|
422
|
+
# /percy/events is hit by post_failed_event in the rescue path.
|
|
423
|
+
stub_request(:post, 'http://localhost:5338/percy/events')
|
|
424
|
+
.to_return(status: 200, body: '{"success": true}')
|
|
425
|
+
|
|
426
|
+
# Force the AppPercy (non-automate) provider so the rescue path in
|
|
427
|
+
# percy-appium-app.rb is exercised. session_type is memoized across tests.
|
|
428
|
+
Percy::Environment.session_type = nil
|
|
429
|
+
|
|
430
|
+
# Android caps have percy:options.ignoreErrors => true, so the error is
|
|
431
|
+
# swallowed and percy_screenshot returns nil. A non-String name makes
|
|
432
|
+
# AppPercy#screenshot raise a TypeError after construction succeeds.
|
|
433
|
+
driver = build_app_driver(get_android_capabilities)
|
|
434
|
+
|
|
435
|
+
result = nil
|
|
436
|
+
assert_output(/Could not take screenshot/) do
|
|
437
|
+
result = percy_screenshot(driver, 12_345)
|
|
438
|
+
end
|
|
439
|
+
assert_nil result
|
|
440
|
+
assert(@requests.any? { |r| r.uri.path == '/percy/events' })
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def test_reraises_screenshot_error_when_ignore_errors_false
|
|
444
|
+
mock_healthcheck
|
|
445
|
+
stub_request(:post, 'http://localhost:5338/percy/events')
|
|
446
|
+
.to_return(status: 200, body: '{"success": true}')
|
|
447
|
+
|
|
448
|
+
Percy::Environment.session_type = nil
|
|
449
|
+
|
|
450
|
+
# Build caps whose percy:options.ignoreErrors is false so the rescue path
|
|
451
|
+
# re-raises after posting the failed event.
|
|
452
|
+
caps = get_android_capabilities.dup
|
|
453
|
+
caps['percy:options'] = { 'enabled' => true, 'ignoreErrors' => false }
|
|
454
|
+
caps['percyOptions'] = { 'enabled' => true, 'ignoreErrors' => false }
|
|
455
|
+
driver = build_app_driver(caps)
|
|
456
|
+
|
|
457
|
+
assert_output(/Could not take screenshot/) do
|
|
458
|
+
assert_raises(TypeError) { percy_screenshot(driver, 12_345) }
|
|
459
|
+
end
|
|
460
|
+
assert(@requests.any? { |r| r.uri.path == '/percy/events' })
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
private
|
|
464
|
+
|
|
465
|
+
# Builds a Minitest::Mock driver that survives AppPercy construction
|
|
466
|
+
# (is_a? Appium driver, capabilities, remote_url) for the rescue-path tests.
|
|
467
|
+
def build_app_driver(caps)
|
|
468
|
+
driver = Minitest::Mock.new
|
|
469
|
+
bridge = Minitest::Mock.new
|
|
470
|
+
http = Minitest::Mock.new
|
|
471
|
+
server_url = Minitest::Mock.new
|
|
472
|
+
|
|
473
|
+
driver.expect(:is_a?, true, [Appium::Core::Base::Driver])
|
|
474
|
+
20.times { driver.expect(:capabilities, caps) }
|
|
475
|
+
3.times do
|
|
476
|
+
driver.expect(:instance_variable_get, bridge, [:@bridge])
|
|
477
|
+
http.expect(:instance_variable_get, server_url, [:@server_url])
|
|
478
|
+
bridge.expect(:instance_variable_get, http, [:@http])
|
|
479
|
+
server_url.expect(:to_s, 'https://hub-cloud.browserstack.com/wd/hub')
|
|
480
|
+
end
|
|
481
|
+
driver
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Test suite for the `hashed` helper in percy/common/common.rb
|
|
486
|
+
class TestHashedHelper < Minitest::Test
|
|
487
|
+
# Stand-in object that responds to #as_json (core objects do not in this env).
|
|
488
|
+
class JsonableDouble
|
|
489
|
+
def as_json
|
|
490
|
+
{ 'converted' => true }
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def test_hashed_returns_hash_unchanged
|
|
495
|
+
input = { 'a' => 1 }
|
|
496
|
+
assert_same(input, hashed(input))
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def test_hashed_converts_non_hash_via_as_json
|
|
500
|
+
assert_equal({ 'converted' => true }, hashed(JsonableDouble.new))
|
|
501
|
+
end
|
|
419
502
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Collates the per-process SimpleCov results written during the spec loop and
|
|
4
|
+
# enforces the coverage gate. Run after all specs:
|
|
5
|
+
# bundle exec ruby specs/support/coverage_check.rb
|
|
6
|
+
require 'simplecov'
|
|
7
|
+
|
|
8
|
+
SimpleCov.collate Dir['coverage/.resultset.json'] do
|
|
9
|
+
add_filter '/specs/'
|
|
10
|
+
track_files 'percy/**/*.rb'
|
|
11
|
+
minimum_coverage line: 100
|
|
12
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Per-spec runner used by CI: `bundle exec ruby specs/support/run_spec.rb specs/<file>.rb`.
|
|
4
|
+
# Requires spec_helper first (starting SimpleCov within the bundler context, which
|
|
5
|
+
# a command-line `-r` flag cannot do reliably) and then loads the target spec.
|
|
6
|
+
require_relative 'spec_helper'
|
|
7
|
+
|
|
8
|
+
spec_file = ARGV[0] or abort('usage: run_spec.rb <spec_file>')
|
|
9
|
+
load File.expand_path(spec_file)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Loaded (via `ruby -r ./specs/support/spec_helper`) before each spec so
|
|
4
|
+
# SimpleCov can instrument the percy source. The suite runs one spec per
|
|
5
|
+
# process, so each process records its own result under a unique command_name;
|
|
6
|
+
# SimpleCov merges them into coverage/.resultset.json, and
|
|
7
|
+
# specs/support/coverage_check.rb collates the merged result and enforces the gate.
|
|
8
|
+
require 'simplecov'
|
|
9
|
+
|
|
10
|
+
SimpleCov.start do
|
|
11
|
+
add_filter '/specs/'
|
|
12
|
+
track_files 'percy/**/*.rb'
|
|
13
|
+
command_name "specs:#{File.basename(ARGV.first || $PROGRAM_NAME)}"
|
|
14
|
+
merge_timeout 3600
|
|
15
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: percy-appium-app
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- BroswerStack
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: appium_lib
|
|
@@ -178,8 +177,12 @@ files:
|
|
|
178
177
|
- specs/metadata.rb
|
|
179
178
|
- specs/metadata_resolver.rb
|
|
180
179
|
- specs/mocks/mock_methods.rb
|
|
180
|
+
- specs/percy_automate.rb
|
|
181
181
|
- specs/percy_options.rb
|
|
182
182
|
- specs/screenshot.rb
|
|
183
|
+
- specs/support/coverage_check.rb
|
|
184
|
+
- specs/support/run_spec.rb
|
|
185
|
+
- specs/support/spec_helper.rb
|
|
183
186
|
- specs/tile.rb
|
|
184
187
|
homepage: ''
|
|
185
188
|
licenses:
|
|
@@ -187,7 +190,6 @@ licenses:
|
|
|
187
190
|
metadata:
|
|
188
191
|
bug_tracker_uri: https://github.com/percy/percy-appium-ruby/issues
|
|
189
192
|
source_code_uri: https://github.com/percy/percy-appium-ruby
|
|
190
|
-
post_install_message:
|
|
191
193
|
rdoc_options: []
|
|
192
194
|
require_paths:
|
|
193
195
|
- percy
|
|
@@ -202,8 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
202
204
|
- !ruby/object:Gem::Version
|
|
203
205
|
version: '0'
|
|
204
206
|
requirements: []
|
|
205
|
-
rubygems_version: 3.
|
|
206
|
-
signing_key:
|
|
207
|
+
rubygems_version: 3.6.9
|
|
207
208
|
specification_version: 4
|
|
208
209
|
summary: Percy visual testing for Ruby Appium Mobile Apps
|
|
209
210
|
test_files: []
|