percy-appium-app 0.0.9.beta1 → 0.0.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47bb435221c28272d50b8e17f6540177bf9c656f128bcfb51f3e5468bf89772d
4
- data.tar.gz: 5ea3c75e8ea54331a06729fd253219fa40acbabf3936929b0465b77ca10b6c84
3
+ metadata.gz: 82432d403ea8909fd0dd473251c8b406378497ff06e6e1806144e83350598013
4
+ data.tar.gz: 1f351d8ede37fef600554f98f1ac8bdc7b60124ea62374be65175a26b0cd74c0
5
5
  SHA512:
6
- metadata.gz: c73191b25d04b044662995228926349eb37838cb03edd9a3f25c72afc6898872dde22b2b01a87f54f4d7b9277873e2784e8874766c40ae947b4e5690f5fc2118
7
- data.tar.gz: b3b16e11cb7c83f5845d92e1773d71d140b4a0b4ccc5c905cbdab11a52beeb96b1d40762e02a0ba8ace7ce25cb45c112711226d0db08b9eef8824bfbaa21cc6e
6
+ metadata.gz: 0bc03604787ae7757d95e29d33dfcdc176c9b0733c0b639ed04d75a7aac4061ff915046b34c0d65f21cf3360514f3c9f970dfef46e6bb38f363a5c2b3cd611d7
7
+ data.tar.gz: c339d7e5282e110b6351284f72124cab0876ddb4b40c95606096219923ca64cf9b4cd4c8084022274625a6bbdd6c087a90bec4a6a105a3ed9c61a49b756bb1f3
@@ -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
 
@@ -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']
@@ -37,4 +42,7 @@ jobs:
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
- - run: for file in specs/*.rb; do echo $file; bundle exec ruby $file; done
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
@@ -1 +1,4 @@
1
1
  Gemfile.lock
2
+ /vendor/
3
+ /.bundle/
4
+ coverage/
data/Gemfile CHANGED
@@ -2,8 +2,12 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- gem 'appium_console'
6
- gem 'appium_lib', '>= 11.2.0'
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
@@ -15,12 +15,15 @@ module Percy
15
15
  def device_screen_size
16
16
  caps = capabilities
17
17
  caps = caps.as_json unless caps.is_a?(Hash)
18
+ # Use string keys to match the IosMetadata implementation and every
19
+ # consumer (generic_provider, app_automate, _get_tag), all of which read
20
+ # device_screen_size['width'] / ['height'].
18
21
  if caps['deviceScreenSize'].nil?
19
22
  size = driver.window_size
20
- { width: size.width.to_i, height: size.height.to_i }
23
+ { 'width' => size.width.to_i, 'height' => size.height.to_i }
21
24
  else
22
25
  width, height = caps['deviceScreenSize'].split('x')
23
- { width: width.to_i, height: height.to_i }
26
+ { 'width' => width.to_i, 'height' => height.to_i }
24
27
  end
25
28
  end
26
29
 
@@ -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
- element = driver.find_element(Appium::Core::Base::SearchContext::FINDERS[:xpath], xpath)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Percy
4
- VERSION = '0.0.9.beta1'.freeze
4
+ VERSION = '0.0.9'.freeze
5
5
  end
@@ -46,13 +46,21 @@ class TestAndroidMetadata < Minitest::Test
46
46
 
47
47
  # Call the method and assert the result
48
48
  result = @android_metadata.device_screen_size
49
- assert_equal({ width: 1080, height: 1920 }, result)
49
+ assert_equal({ 'width' => 1080, 'height' => 1920 }, result)
50
50
 
51
51
  # Verify mocks
52
52
  mock_window_size.verify
53
53
  @mock_webdriver.verify
54
54
  end
55
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)
61
+ @mock_webdriver.verify
62
+ end
63
+
56
64
  def test_get_system_bars
57
65
  system_bars = {
58
66
  'statusBar' => { 'height' => 83 },
@@ -67,15 +67,170 @@ class TestAppAutomate < Minitest::Test
67
67
  end
68
68
 
69
69
  def test_execute_percy_screenshot_end
70
- @app_automate.stub(:execute_percy_screenshot_begin, 'deviceName' => 'abc', 'osVersion' => '123') do
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
@@ -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
@@ -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
- [Appium::Core::Base::SearchContext::FINDERS[:xpath], '//path/to/element'])
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, [Appium::Core::Base::SearchContext::FINDERS[:xpath], '//path/to/element']) do
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, [Appium::Core::Base::SearchContext::FINDERS[:accessibility_id], 'id1']) do
357
+ @mock_webdriver.expect(:find_element, [:accessibility_id, 'id1']) do
313
358
  raise Appium::Core::Error::NoSuchElementError, 'Test error'
314
359
  end
315
360
 
@@ -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)
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' } }
@@ -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
- [Appium::Core::Base::SearchContext::FINDERS[:xpath], '//path/to/element'])
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: percy-appium-app
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9.beta1
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - BroswerStack
@@ -177,8 +177,12 @@ files:
177
177
  - specs/metadata.rb
178
178
  - specs/metadata_resolver.rb
179
179
  - specs/mocks/mock_methods.rb
180
+ - specs/percy_automate.rb
180
181
  - specs/percy_options.rb
181
182
  - specs/screenshot.rb
183
+ - specs/support/coverage_check.rb
184
+ - specs/support/run_spec.rb
185
+ - specs/support/spec_helper.rb
182
186
  - specs/tile.rb
183
187
  homepage: ''
184
188
  licenses:
@@ -200,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
200
204
  - !ruby/object:Gem::Version
201
205
  version: '0'
202
206
  requirements: []
203
- rubygems_version: 3.6.7
207
+ rubygems_version: 3.6.9
204
208
  specification_version: 4
205
209
  summary: Percy visual testing for Ruby Appium Mobile Apps
206
210
  test_files: []