appium_failure_helper 1.5.0 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43096445c0c64d280ed41cc96d50ddd088c660bfac0441b8ff4e9ddc34e9107d
4
- data.tar.gz: a2b4d0c6ebe03d445ae1e39d5dd35da188b01b4835b1a9a16055709b201f0864
3
+ metadata.gz: 3ef5fd316590ddca6a0b9ccc21b06f947c3b439fb998668b2b965ae5f110702a
4
+ data.tar.gz: af17ce648f330a535cd5d0582fd4441f2cbcc881fd67626e8516a202655e747e
5
5
  SHA512:
6
- metadata.gz: f27c3e50dbef777ef84b756738e32ca50779a970e01e8cbd086a13c5a65a15d6c22c3f27e51e357b78555d3ae8cf5619b9e9fa36131c5f6761d54707b3fe8d93
7
- data.tar.gz: a1dc3d6684a2d5afc79c6a7c956075fdeb5a90afd6d5ac4b02ae8a1129e16c18fcdae7cfd2f66794cab9e86ec65880ff7c5cb0edfddbef0b8ab6326ff163e445
6
+ metadata.gz: bec158f942080f960031179836e9830ea7f3fd2cc48117d5b803d5156d0ed89ee27bf1377c10a36f7ec4be92aeea7c9eac60e2232c77e75a5c19fd390b9fb306
7
+ data.tar.gz: 66a21888247d7bb983556428f3aad65dfa8fd9b3f576d670bc736a4db1e520b9874b2b01628b9fc7f3ad94dd06363d2d0faacb78c6ad042d373eb60c75ffcd84
data/.idea/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
@@ -0,0 +1,48 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="ModuleRunConfigurationManager">
4
+ <shared />
5
+ </component>
6
+ <component name="NewModuleRootManager">
7
+ <content url="file://$MODULE_DIR$">
8
+ <sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
9
+ <sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
10
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
11
+ </content>
12
+ <orderEntry type="inheritedJdk" />
13
+ <orderEntry type="sourceFolder" forTests="false" />
14
+ <orderEntry type="library" scope="PROVIDED" name="appium_lib (v10.6.0, ruby-3.1.7-p261) [gem]" level="application" />
15
+ <orderEntry type="library" scope="PROVIDED" name="appium_lib_core (v3.11.1, ruby-3.1.7-p261) [gem]" level="application" />
16
+ <orderEntry type="library" scope="PROVIDED" name="base64 (v0.3.0, ruby-3.1.7-p261) [gem]" level="application" />
17
+ <orderEntry type="library" scope="PROVIDED" name="bundler (v2.6.8, ruby-3.1.7-p261) [gem]" level="application" />
18
+ <orderEntry type="library" scope="PROVIDED" name="cgi (v0.5.0, ruby-3.1.7-p261) [gem]" level="application" />
19
+ <orderEntry type="library" scope="PROVIDED" name="childprocess (v3.0.0, ruby-3.1.7-p261) [gem]" level="application" />
20
+ <orderEntry type="library" scope="PROVIDED" name="date (v3.4.1, ruby-3.1.7-p261) [gem]" level="application" />
21
+ <orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.6.2, ruby-3.1.7-p261) [gem]" level="application" />
22
+ <orderEntry type="library" scope="PROVIDED" name="erb (v4.0.4, ruby-3.1.7-p261) [gem]" level="application" />
23
+ <orderEntry type="library" scope="PROVIDED" name="eventmachine (v1.2.7, ruby-3.1.7-p261) [gem]" level="application" />
24
+ <orderEntry type="library" scope="PROVIDED" name="faye-websocket (v0.11.4, ruby-3.1.7-p261) [gem]" level="application" />
25
+ <orderEntry type="library" scope="PROVIDED" name="ffi (v1.17.2, ruby-3.1.7-p261) [gem]" level="application" />
26
+ <orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.1, ruby-3.1.7-p261) [gem]" level="application" />
27
+ <orderEntry type="library" scope="PROVIDED" name="irb (v1.15.2, ruby-3.1.7-p261) [gem]" level="application" />
28
+ <orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.18.10, ruby-3.1.7-p261) [gem]" level="application" />
29
+ <orderEntry type="library" scope="PROVIDED" name="pp (v0.6.2, ruby-3.1.7-p261) [gem]" level="application" />
30
+ <orderEntry type="library" scope="PROVIDED" name="prettyprint (v0.2.0, ruby-3.1.7-p261) [gem]" level="application" />
31
+ <orderEntry type="library" scope="PROVIDED" name="psych (v5.2.6, ruby-3.1.7-p261) [gem]" level="application" />
32
+ <orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, ruby-3.1.7-p261) [gem]" level="application" />
33
+ <orderEntry type="library" scope="PROVIDED" name="rake (v13.3.0, ruby-3.1.7-p261) [gem]" level="application" />
34
+ <orderEntry type="library" scope="PROVIDED" name="rdoc (v6.14.2, ruby-3.1.7-p261) [gem]" level="application" />
35
+ <orderEntry type="library" scope="PROVIDED" name="reline (v0.6.2, ruby-3.1.7-p261) [gem]" level="application" />
36
+ <orderEntry type="library" scope="PROVIDED" name="rspec (v3.13.1, ruby-3.1.7-p261) [gem]" level="application" />
37
+ <orderEntry type="library" scope="PROVIDED" name="rspec-core (v3.13.5, ruby-3.1.7-p261) [gem]" level="application" />
38
+ <orderEntry type="library" scope="PROVIDED" name="rspec-expectations (v3.13.5, ruby-3.1.7-p261) [gem]" level="application" />
39
+ <orderEntry type="library" scope="PROVIDED" name="rspec-mocks (v3.13.5, ruby-3.1.7-p261) [gem]" level="application" />
40
+ <orderEntry type="library" scope="PROVIDED" name="rspec-support (v3.13.6, ruby-3.1.7-p261) [gem]" level="application" />
41
+ <orderEntry type="library" scope="PROVIDED" name="rubyzip (v3.1.0, ruby-3.1.7-p261) [gem]" level="application" />
42
+ <orderEntry type="library" scope="PROVIDED" name="selenium-webdriver (v3.142.7, ruby-3.1.7-p261) [gem]" level="application" />
43
+ <orderEntry type="library" scope="PROVIDED" name="stringio (v3.1.7, ruby-3.1.7-p261) [gem]" level="application" />
44
+ <orderEntry type="library" scope="PROVIDED" name="tomlrb (v1.3.0, ruby-3.1.7-p261) [gem]" level="application" />
45
+ <orderEntry type="library" scope="PROVIDED" name="websocket-driver (v0.7.7, ruby-3.1.7-p261) [gem]" level="application" />
46
+ <orderEntry type="library" scope="PROVIDED" name="websocket-extensions (v0.1.5, ruby-3.1.7-p261) [gem]" level="application" />
47
+ </component>
48
+ </module>
data/.idea/misc.xml ADDED
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="ruby-3.1.7-p261" project-jdk-type="RUBY_SDK" />
4
+ </project>
data/.idea/modules.xml ADDED
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/Appium_failure_helper.iml" filepath="$PROJECT_DIR$/.idea/Appium_failure_helper.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
data/.idea/vcs.xml ADDED
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -2,12 +2,10 @@ module AppiumFailureHelper
2
2
  module Analyzer
3
3
  def self.triage_error(exception)
4
4
  case exception
5
- when Selenium::WebDriver::Error::NoSuchElementError,
6
- Selenium::WebDriver::Error::TimeoutError,
7
- Selenium::WebDriver::Error::UnknownCommandError
8
- :locator_issue
9
- when Selenium::WebDriver::Error::TimeoutError,
10
- Appium::Core::Wait::TimeoutError
5
+ when Selenium::WebDriver::Error::NoSuchElementError,
6
+ Selenium::WebDriver::Error::TimeoutError,
7
+ Selenium::WebDriver::Error::UnknownCommandError,
8
+ defined?(Appium::Core::Wait::TimeoutError) ? Appium::Core::Wait::TimeoutError : nil
11
9
  :locator_issue
12
10
  when Selenium::WebDriver::Error::ElementNotInteractableError
13
11
  :visibility_issue
@@ -30,69 +28,52 @@ module AppiumFailureHelper
30
28
  def self.extract_failure_details(exception)
31
29
  message = exception.message.to_s
32
30
  info = {}
33
- patterns = [
34
- /using "([^"]+)" with value "([^"]+)"/,
35
- /element with locator ['"]?(#?\w+)['"]?/i,
36
- /(?:could not be found|cannot find element) using (.+?)=['"]?([^'"]+)['"]?/i,
37
- /no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
38
- ]
39
- patterns.each do |pattern|
40
- match = message.match(pattern)
41
- if match
42
- if match.captures.size == 2
43
- info[:selector_type], info[:selector_value] = match.captures.map(&:strip)
44
- else
45
- info[:selector_value] = match.captures.first.strip
46
- info[:selector_type] = 'logical_name'
47
- end
48
- return info
49
- end
31
+ pattern = /using "([^"]+)" with value "([^"]+)"/
32
+ match = message.match(pattern)
33
+ if match
34
+ info[:selector_type], info[:selector_value] = match.captures
50
35
  end
51
36
  info
52
37
  end
53
-
54
- def self.find_de_para_match(failed_info, element_map)
55
- failed_value = (failed_info || {})[:selector_value].to_s
56
- return nil if failed_value.empty?
57
- logical_name_key = failed_value.gsub(/^#/, '')
58
- if element_map.key?(logical_name_key)
59
- return { logical_name: logical_name_key, correct_locator: element_map[logical_name_key] }
60
- end
61
- cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
62
- element_map.each do |name, locator_info|
63
- mapped_locator = (locator_info || {})['valor'].to_s
64
- cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
65
- distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
66
- max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
67
- next if max_len.zero?
68
- similarity_score = 1.0 - (distance.to_f / max_len)
69
- if similarity_score > 0.85
70
- return { logical_name: name, correct_locator: locator_info }
38
+
39
+ def self.perform_advanced_analysis(failed_info, all_page_elements, platform)
40
+ return nil if failed_info.empty? || all_page_elements.empty?
41
+ expected_attrs = parse_locator(failed_info[:selector_type], failed_info[:selector_value], platform)
42
+ return nil if expected_attrs.empty?
43
+
44
+ id_key_to_check = (platform.to_s == 'ios') ? 'name' : 'resource-id'
45
+ candidates = all_page_elements.map do |element_on_screen|
46
+ score = 0
47
+ analysis = {}
48
+
49
+ if expected_attrs[id_key_to_check]
50
+ actual_id = element_on_screen[:attributes][id_key_to_check]
51
+ distance = DidYouMean::Levenshtein.distance(expected_attrs[id_key_to_check].to_s, actual_id.to_s)
52
+ max_len = [expected_attrs[id_key_to_check].to_s.length, actual_id.to_s.length].max
53
+ similarity = max_len.zero? ? 0 : 1.0 - (distance.to_f / max_len)
54
+ score += 100 * similarity
55
+ analysis[id_key_to_check.to_sym] = { similarity: similarity, expected: expected_attrs[id_key_to_check], actual: actual_id }
71
56
  end
72
- end
73
- nil
57
+
58
+ { score: score, name: element_on_screen[:name], attributes: element_on_screen[:attributes], analysis: analysis } if score > 75
59
+ end.compact
60
+
61
+ candidates.sort_by { |c| -c[:score] }.first
74
62
  end
75
63
 
76
- def self.find_similar_elements(failed_info, all_page_suggestions)
77
- failed_locator_value = (failed_info || {})[:selector_value]
78
- failed_locator_type = (failed_info || {})[:selector_type]
79
- return [] unless failed_locator_value && failed_locator_type
80
- normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
81
- cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
82
- similarities = []
83
- all_page_suggestions.each do |suggestion|
84
- candidate_locator = (suggestion[:locators] || []).find { |loc| loc[:strategy] == normalized_failed_type }
85
- next unless candidate_locator
86
- cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
87
- distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
88
- max_len = [cleaned_failed_locator.length, cleaned_candidate_locator.length].max
89
- next if max_len.zero?
90
- similarity_score = 1.0 - (distance.to_f / max_len)
91
- if similarity_score > 0.85
92
- similarities << { name: suggestion[:name], locators: suggestion[:locators], score: similarity_score, attributes: suggestion[:attributes] }
93
- end
64
+ private
65
+
66
+ def self.parse_locator(type, value, platform)
67
+ attrs = {}
68
+ if platform.to_s == 'ios'
69
+ attrs['name'] = value if type.to_s.include?('id')
70
+ else # Android
71
+ attrs['resource-id'] = value if type.to_s.include?('id')
72
+ end
73
+ if type.to_s == 'xpath'
74
+ value.scan(/@([\w\-]+)='([^']+)'/).each { |match| attrs[match[0]] = match[1] }
94
75
  end
95
- similarities.sort_by { |s| -s[:score] }.first(5)
76
+ attrs
96
77
  end
97
78
  end
98
79
  end
@@ -12,66 +12,69 @@ module AppiumFailureHelper
12
12
  end
13
13
 
14
14
  def call
15
- report_data = {}
16
15
  begin
17
16
  unless @driver && @driver.session_id
18
- Utils.logger.error("Helper não executado: driver nulo ou sessão encerrada.")
19
- return {}
17
+ return
20
18
  end
21
19
 
22
20
  FileUtils.mkdir_p(@output_folder)
23
21
 
24
22
  triage_result = Analyzer.triage_error(@exception)
25
- screenshot_b64 = begin
26
- @driver.screenshot_as(:base64)
27
- rescue
28
- nil
29
- end
23
+ platform_value = @driver.capabilities[:platform_name] || @driver.capabilities['platformName']
24
+ platform = platform_value&.downcase || 'unknown'
25
+
30
26
  report_data = {
31
- exception: @exception,
32
- triage_result: triage_result,
33
- timestamp: @timestamp,
34
- platform: (@driver.capabilities['platformName'] rescue @driver.capabilities[:platform_name]) || 'unknown',
35
- screenshot_base64: screenshot_b64
27
+ exception: @exception, triage_result: triage_result,
28
+ timestamp: @timestamp, platform: platform,
29
+ screenshot_base_64: @driver.screenshot_as(:base64)
36
30
  }
37
31
 
38
32
  if triage_result == :locator_issue
39
- page_source = @driver.page_source rescue nil
40
- doc = Nokogiri::XML(page_source) rescue nil
41
-
42
- failed_info = fetch_failed_element
43
-
44
- report_data[:page_source] = page_source
45
- report_data[:failed_element] = failed_info
46
-
47
- unless failed_info.nil? || failed_info.empty?
48
- page_analyzer = PageAnalyzer.new(page_source, report_data[:platform].to_s) rescue nil
49
- all_page_elements = page_analyzer ? (page_analyzer.analyze || []) : []
50
- similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
51
- alternative_xpaths = generate_alternative_xpaths(similar_elements, doc)
52
- unified_element_map = ElementRepository.load_all rescue {}
53
- de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
54
- code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
55
-
56
- report_data.merge!(
57
- similar_elements: similar_elements,
58
- alternative_xpaths: alternative_xpaths,
59
- de_para_analysis: de_para_result,
60
- code_search_results: code_search_results,
61
- all_page_elements: all_page_elements
62
- )
33
+ page_source = @driver.page_source
34
+ doc = Nokogiri::XML(page_source)
35
+
36
+ failed_info = Analyzer.extract_failure_details(@exception) || {}
37
+ if failed_info.empty?
38
+ failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
63
39
  end
64
40
 
65
- ReportGenerator.new(@output_folder, report_data).generate_all
66
- Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
41
+ if failed_info.empty?
42
+ report_data[:triage_result] = :unidentified_locator_issue
43
+ else
44
+ page_analyzer = PageAnalyzer.new(page_source, platform)
45
+ all_page_elements = page_analyzer.analyze || []
46
+
47
+ best_candidate_analysis = Analyzer.perform_advanced_analysis(failed_info, all_page_elements, platform)
48
+
49
+ alternative_xpaths = []
50
+ if best_candidate_analysis
51
+ if best_candidate_analysis[:attributes] && (target_path = best_candidate_analysis[:attributes][:path])
52
+ target_node = doc.at_xpath(target_path)
53
+ if target_node
54
+ alternative_xpaths = XPathFactory.generate_for_node(target_node)
55
+ end
56
+ end
57
+ end
58
+
59
+ report_data.merge!({
60
+ page_source: page_source,
61
+ failed_element: failed_info,
62
+ best_candidate_analysis: best_candidate_analysis,
63
+ alternative_xpaths: alternative_xpaths,
64
+ all_page_elements: all_page_elements
65
+ })
66
+ end
67
67
  end
68
68
 
69
+ ReportGenerator.new(@output_folder, report_data).generate_all
70
+ Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
69
71
  rescue => e
70
- Utils.logger.error("Erro fatal na GEM de diagnóstico: #{e.message}\n#{e.backtrace.join("\n")}")
71
- report_data = { exception: @exception, triage_result: :error } if report_data.nil? || report_data.empty?
72
- ensure
73
- return report_data
72
+ puts "--- ERRO FATAL NA GEM ---"
73
+ puts "CLASSE: #{e.class}, MENSAGEM: #{e.message}"
74
+ puts e.backtrace.join("\n")
75
+ puts "-------------------------"
74
76
  end
77
+ report_data
75
78
  end
76
79
 
77
80
  private
@@ -79,32 +82,26 @@ module AppiumFailureHelper
79
82
  def fetch_failed_element
80
83
  msg = @exception&.message.to_s
81
84
 
82
- # 1) pattern: using "type" with value "value"
83
85
  if (m = msg.match(/using\s+["']?([^"']+)["']?\s+with\s+value\s+["']([^"']+)["']/i))
84
86
  return { selector_type: m[1], selector_value: m[2] }
85
87
  end
86
88
 
87
- # 2) JSON-like: {"method":"id","selector":"btn"}
88
89
  if (m = msg.match(/"method"\s*:\s*"([^"]+)"[\s,}].*"selector"\s*:\s*"([^"]+)"/i))
89
90
  return { selector_type: m[1], selector_value: m[2] }
90
91
  end
91
92
 
92
- # 3) generic quoted token "value" or 'value'
93
93
  if (m = msg.match(/["']([^"']+)["']/))
94
94
  maybe_value = m[1]
95
- # try lookup in repo by that value
96
95
  unified_map = ElementRepository.load_all rescue {}
97
96
  found = find_in_element_repository_by_value(maybe_value, unified_map)
98
97
  if found
99
98
  return found
100
99
  end
101
100
 
102
- # guess type from message heuristics
103
101
  guessed_type = msg[/\b(xpath|id|accessibility id|css)\b/i] ? $&.downcase : nil
104
102
  return { selector_type: guessed_type || 'unknown', selector_value: maybe_value }
105
103
  end
106
104
 
107
- # 4) try SourceCodeAnalyzer
108
105
  begin
109
106
  code_info = SourceCodeAnalyzer.extract_from_exception(@exception) rescue {}
110
107
  unless code_info.nil? || code_info.empty?
@@ -112,15 +109,12 @@ module AppiumFailureHelper
112
109
  end
113
110
  rescue => _; end
114
111
 
115
- # 5) fallback: try to inspect unified map for likely candidates (keys or inner values)
116
112
  unified_map = ElementRepository.load_all rescue {}
117
- # try to match any key that looks like an identifier present in the message
118
113
  unified_map.each do |k, v|
119
114
  k_str = k.to_s.downcase
120
115
  if msg.downcase.include?(k_str)
121
116
  return normalize_repo_element(v)
122
117
  end
123
- # inspect value fields
124
118
  vals = []
125
119
  if v.is_a?(Hash)
126
120
  vals << v['valor'] if v.key?('valor')
@@ -20,28 +20,20 @@ module AppiumFailureHelper
20
20
  @platform = platform
21
21
  end
22
22
 
23
- def analyze
24
- seen_elements = {}
23
+ def analyze
25
24
  all_elements_suggestions = []
26
25
  @doc.xpath('//*').each do |node|
27
- next if ['hierarchy', 'AppiumAUT'].include?(node.name)
28
- attrs = node.attributes.transform_values(&:value)
29
-
30
- unique_key = node.path
31
- next if seen_elements[unique_key]
26
+ next if ['hierarchy', 'AppiumAUT'].include?(node.name)
32
27
 
33
- name = suggest_name(node.name, attrs)
34
-
35
- locators = XPathFactory.generate_for_node(node)
36
-
37
- all_elements_suggestions << {
38
- name: name,
39
- locators: locators,
40
- attributes: attrs.merge(tag: node.name, path: node.path)
41
- }
42
- seen_elements[unique_key] = true
28
+ attrs = node.attribute_nodes.to_h { |attr| [attr.name, attr.value] }
29
+
30
+ attrs['tag'] = node.name
31
+ name = suggest_name(node.name, attrs)
32
+ locators = XPathFactory.generate_for_node(node)
33
+
34
+ all_elements_suggestions << { name: name, locators: locators, attributes: attrs.merge(path: node.path) }
43
35
  end
44
- all_elements_suggestions
36
+ all_elements_suggestions.uniq { |s| s[:attributes][:path] }
45
37
  end
46
38
 
47
39
  private
@@ -3,7 +3,7 @@ module AppiumFailureHelper
3
3
  def initialize(output_folder, report_data)
4
4
  @output_folder = output_folder
5
5
  @data = report_data
6
- @page_source = report_data[:page_source] # Pega o page_source de dentro do hash
6
+ @page_source = report_data[:page_source]
7
7
  end
8
8
 
9
9
  def generate_all
@@ -18,7 +18,7 @@ module AppiumFailureHelper
18
18
  File.write("#{@output_folder}/page_source_#{@data[:timestamp]}.xml", @page_source)
19
19
  end
20
20
 
21
- def generate_yaml_reports
21
+ def generate_yaml_reports
22
22
  analysis_report = {
23
23
  triage_result: @data[:triage_result],
24
24
  exception_class: @data[:exception].class.to_s,
@@ -27,86 +27,97 @@ module AppiumFailureHelper
27
27
  best_candidate_analysis: @data[:best_candidate_analysis]
28
28
  }
29
29
  File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
30
-
30
+
31
31
  if @data[:all_page_elements]
32
32
  File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
33
33
  end
34
34
  end
35
35
 
36
36
  def generate_html_report
37
- html_content = case @data[:triage_result]
38
- when :locator_issue
37
+ html_content = if @data[:triage_result] == :locator_issue && !(@data[:failed_element] || {}).empty?
39
38
  build_full_report
40
39
  else
41
40
  build_simple_diagnosis_report(
42
41
  title: "Diagnóstico Rápido de Falha",
43
- message: "A falha não foi causada por um seletor não encontrado ou a análise do seletor falhou. Verifique a mensagem de erro original e o stack trace para a causa raiz."
42
+ message: "A análise profunda do seletor não foi executada ou falhou. Verifique a mensagem de erro original e o stack trace."
44
43
  )
45
44
  end
46
-
47
45
  File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
48
46
  end
49
47
 
50
48
  def build_full_report
51
49
  failed_info = @data[:failed_element] || {}
52
- similar_elements = @data[:similar_elements] || []
53
50
  all_suggestions = @data[:all_page_elements] || []
54
- de_para_analysis = @data[:de_para_analysis]
55
- code_search_results = @data[:code_search_results] || []
51
+ best_candidate = @data[:best_candidate_analysis]
56
52
  alternative_xpaths = @data[:alternative_xpaths] || []
57
53
  timestamp = @data[:timestamp]
58
54
  platform = @data[:platform]
59
- screenshot_base64 = @data[:screenshot_base64]
55
+ screenshot_base64 = @data[:screenshot_base_64]
60
56
 
61
57
  locators_html = lambda do |locators|
62
- (locators || []).map do |loc|
63
- strategy_text = loc[:strategy].to_s.upcase.gsub('_', ' ')
64
- "<li class='flex justify-between items-center bg-gray-50 p-2 rounded-md mb-1 text-xs font-mono'><span class='font-bold text-indigo-600'>#{CGI.escapeHTML(strategy_text)}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{CGI.escapeHTML(loc[:locator])}</span></li>"
65
- end.join
58
+ (locators || []).map { |loc| "<li class='flex justify-between items-center bg-gray-50 p-2 rounded-md mb-1 text-xs font-mono'><span class='font-bold text-indigo-600'>#{CGI.escapeHTML(loc[:strategy].to_s.upcase.gsub('_', ' '))}:</span><span class='text-gray-700 ml-2 overflow-auto max-w-[70%]'>#{CGI.escapeHTML(loc[:locator])}</span></li>" }.join
66
59
  end
67
60
 
68
61
  all_elements_html = lambda do |elements|
69
62
  (elements || []).map { |el| "<details class='border-b border-gray-200 py-3'><summary class='font-semibold text-sm text-gray-800 cursor-pointer'>#{CGI.escapeHTML(el[:name])}</summary><ul class='text-xs space-y-1 mt-2'>#{locators_html.call(el[:locators])}</ul></details>" }.join
70
63
  end
71
-
72
- failed_info_content = "<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{CGI.escapeHTML(failed_info[:selector_type].to_s)}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-all'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
73
64
 
74
- code_search_html = "" # (Sua lógica code_search_html)
75
- failed_info_content = if failed_info && !failed_info.empty?
76
- "<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{CGI.escapeHTML(failed_info[:selector_type].to_s)}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-all'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
77
- else
78
- "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
79
- end
65
+ failed_info_content = "<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{CGI.escapeHTML(failed_info[:selector_type].to_s)}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-words'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
80
66
 
81
- code_search_html = ""
82
- unless code_search_results.empty?
83
- suggestions_list = code_search_results.map do |match|
84
- score_percent = (match[:score] * 100).round(1)
85
- "<div class='border border-sky-200 bg-sky-50 p-3 rounded-lg mb-2'><p class='text-sm text-gray-600'>Encontrado em: <strong class='font-mono'>#{match[:file]}:#{match[:line_number]}</strong></p><pre class='bg-gray-800 text-white p-2 rounded mt-2 text-xs overflow-auto'><code>#{CGI.escapeHTML(match[:code])}</code></pre><p class='text-xs text-green-600 mt-1'>Similaridade: #{score_percent}%</p></div>"
86
- end.join
87
- code_search_html = "<div class='bg-white p-4 rounded-lg shadow-md'><h2 class='text-xl font-bold text-sky-700 mb-4'>Sugestões Encontradas no Código</h2>#{suggestions_list}</div>"
88
- end
67
+ advanced_analysis_html = if best_candidate.nil?
68
+ "<p class='text-gray-500'>Nenhum candidato provável foi encontrado na tela atual para uma análise detalhada.</p>"
69
+ else
70
+ analysis_details = (best_candidate[:analysis] || {}).map do |key, data|
71
+ status_color = 'bg-gray-400'
72
+ status_icon = '⚪'
73
+ status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Não verificado</span>"
89
74
 
90
- # --- LÓGICA RESTAURADA: ELEMENTO COM FALHA ---
91
- failed_info_content = if failed_info && !failed_info.empty?
92
- "<p class='text-sm text-gray-700 font-medium mb-2'>Tipo de Seletor: <span class='font-mono text-xs bg-red-100 p-1 rounded'>#{CGI.escapeHTML(failed_info[:selector_type].to_s)}</span></p><p class='text-sm text-gray-700 font-medium'>Valor Buscado: <span class='font-mono text-xs bg-red-100 p-1 rounded break-all'>#{CGI.escapeHTML(failed_info[:selector_value].to_s)}</span></p>"
93
- else
94
- "<p class='text-sm text-gray-500'>O localizador exato não pôde ser extraído.</p>"
95
- end
75
+ if data[:match] == true || (data[:similarity] && data[:similarity] == 1.0)
76
+ status_color = 'bg-green-500'
77
+ status_icon = ''
78
+ status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Correspondência Exata!</span>"
79
+ elsif data[:similarity] && data[:similarity] > 0.7
80
+ status_color = 'bg-yellow-500'
81
+ status_icon = '⚠️'
82
+ status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Parecido (Encontrado: '#{CGI.escapeHTML(data[:actual])}')</span>"
83
+ else
84
+ status_color = 'bg-red-500'
85
+ status_icon = '❌'
86
+ status_text = "<b>#{key.capitalize}:</b><span class='ml-2 text-gray-700'>Diferente! Esperado: '#{CGI.escapeHTML(data[:expected].to_s)}'</span>"
87
+ end
88
+
89
+ "<li class='flex items-center text-sm'><span class='w-4 h-4 rounded-full #{status_color} mr-3 flex-shrink-0 flex items-center justify-center text-white text-xs'>#{status_icon}</span><div class='truncate'>#{status_text}</div></li>"
90
+ end.join
91
+
92
+ suggestion_text = "O `resource-id` pode ter mudado ou o `text` está diferente. Considere usar um seletor mais robusto baseado nos atributos que corresponderam."
93
+ if (best_candidate[:analysis][:id] || {})[:match] == true && (best_candidate[:analysis][:text] || {})[:similarity].to_f < 0.7
94
+ suggestion_text = "O `resource-id` corresponde, mas o texto é diferente. **Recomendamos fortemente usar o `resource-id` para este seletor.**"
95
+ end
96
96
 
97
- repair_suggestions_content = if alternative_xpaths.empty?
98
- "<p class='text-gray-500'>Nenhuma estratégia de localização alternativa pôde ser gerada.</p>"
99
- else
100
- pages = alternative_xpaths.each_slice(6).to_a
101
- carousel_items = pages.map do |page_strategies|
102
- strategy_list_html = page_strategies.map do |strategy|
103
- reliability_color = case strategy[:reliability]
104
- when :alta then 'bg-green-100 text-green-800'
105
- when :media then 'bg-yellow-100 text-yellow-800'
106
- else 'bg-red-100 text-red-800'
107
- end
108
- # CORREÇÃO: Adiciona o tipo de estratégia (ID, XPATH) ao lado do seletor
109
- <<~STRATEGY_ITEM
97
+ <<~HTML
98
+ <div class='border border-sky-200 bg-sky-50 p-4 rounded-lg'>
99
+ <h4 class='font-bold text-sky-800 mb-3'>Candidato Mais Provável Encontrado: <span class='font-mono bg-sky-100 text-sky-900 rounded px-2 py-1 text-sm'>#{CGI.escapeHTML(best_candidate[:name])}</span></h4>
100
+ <ul class='space-y-2 mb-4'>#{analysis_details}</ul>
101
+ <div class='bg-sky-100 border-l-4 border-sky-500 text-sky-900 text-sm p-3 rounded-r-lg'>
102
+ <p><b>Sugestão:</b> #{suggestion_text}</p>
103
+ </div>
104
+ </div>
105
+ HTML
106
+ end
107
+
108
+ repair_strategies_content = if alternative_xpaths.empty?
109
+ "<p class='text-gray-500'>Nenhuma estratégia de localização alternativa pôde ser gerada.</p>"
110
+ else
111
+ pages = alternative_xpaths.each_slice(6).to_a
112
+ carousel_items = pages.map do |page_strategies|
113
+ strategy_list_html = page_strategies.map do |strategy|
114
+ reliability_color = case strategy[:reliability]
115
+ when :alta then 'bg-green-100 text-green-800'
116
+ when :media then 'bg-yellow-100 text-yellow-800'
117
+ else 'bg-red-100 text-red-800'
118
+ end
119
+ # CORREÇÃO: Adiciona o tipo de estratégia (ID, XPATH) ao lado do seletor
120
+ <<~STRATEGY_ITEM
110
121
  <li class='border-b border-gray-200 py-3 last:border-b-0'>
111
122
  <div class='flex justify-between items-center mb-1'>
112
123
  <p class='font-semibold text-indigo-800 text-sm'>#{CGI.escapeHTML(strategy[:name])}</p>
@@ -118,11 +129,11 @@ module AppiumFailureHelper
118
129
  </div>
119
130
  </li>
120
131
  STRATEGY_ITEM
121
- end.join
122
- "<div class='carousel-item w-full flex-shrink-0'><ul>#{strategy_list_html}</ul></div>"
123
- end.join
132
+ end.join
133
+ "<div class='carousel-item w-full flex-shrink-0'><ul>#{strategy_list_html}</ul></div>"
134
+ end.join
124
135
 
125
- <<~CAROUSEL
136
+ <<~CAROUSEL
126
137
  <div id="xpath-carousel" class="relative">
127
138
  <div class="overflow-hidden">
128
139
  <div class="carousel-track flex transition-transform duration-300 ease-in-out">
@@ -140,34 +151,7 @@ module AppiumFailureHelper
140
151
  </div>
141
152
  </div>
142
153
  CAROUSEL
143
- end
144
-
145
- similar_elements_content = if similar_elements.empty?
146
- "<p class='text-gray-500'>Nenhuma alternativa semelhante foi encontrada na tela atual.</p>"
147
- else
148
- carousel_items = similar_elements.map do |el|
149
- score_percent = (el[:score] * 100).round(1)
150
- <<~ITEM
151
- <div class="carousel-item w-full flex-shrink-0">
152
- <div class='border border-indigo-100 p-4 rounded-lg bg-indigo-50'>
153
- <p class='font-bold text-indigo-800 mb-2'>#{CGI.escapeHTML(el[:name])} <span class='text-xs font-normal text-green-600 bg-green-100 rounded-full px-2 py-1 ml-2'>Similaridade: #{score_percent}%</span></p>
154
- <ul>#{locators_html.call(el[:locators])}</ul>
155
- </div>
156
- </div>
157
- ITEM
158
- end.join
159
- <<~CAROUSEL
160
- <div id="similar-elements-carousel" class="relative">
161
- <div class="overflow-hidden rounded-lg bg-white"><div class="carousel-track flex transition-transform duration-300 ease-in-out">#{carousel_items}</div></div>
162
- <div class="flex items-center justify-center space-x-4 mt-4">
163
- <button class="carousel-prev-footer bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded-lg disabled:opacity-50"> &lt; Anterior </button>
164
- <div class="carousel-counter text-center text-sm text-gray-600 font-medium"></div>
165
- <button class="carousel-next-footer bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-4 rounded-lg disabled:opacity-50"> Próximo &gt; </button>
166
- </div>
167
- </div>
168
- CAROUSEL
169
- end
170
-
154
+ end
171
155
  <<~HTML_REPORT
172
156
  <!DOCTYPE html>
173
157
  <html lang="pt-BR">
@@ -189,7 +173,6 @@ module AppiumFailureHelper
189
173
  <h2 class="text-xl font-bold text-red-600 mb-4">Elemento com Falha</h2>
190
174
  #{failed_info_content}
191
175
  </div>
192
- #{code_search_html}
193
176
  <div class="bg-white p-4 rounded-lg shadow-md">
194
177
  <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
195
178
  <img src="data:image/png;base64,#{screenshot_base64}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
@@ -198,13 +181,14 @@ module AppiumFailureHelper
198
181
  <div class="lg:col-span-2">
199
182
  <div class="bg-white rounded-lg shadow-md">
200
183
  <div class="flex border-b border-gray-200">
201
- <button class="tab-button active px-4 py-3 text-sm" data-tab="strategies">Estratégias de Reparo (#{alternative_xpaths.size})</button>
184
+ <button class="tab-button active px-4 py-3 text-sm" data-tab="analysis">Análise Avançada</button>
202
185
  <button class="tab-button px-4 py-3 text-sm text-gray-600" data-tab="all">Dump Completo (#{all_suggestions.size})</button>
203
186
  </div>
204
187
  <div class="p-6">
205
- <div id="strategies" class="tab-content active">
206
- <h3 class="text-lg font-semibold text-indigo-700 mb-4">Estratégias de Localização Alternativas</h3>
207
- #{repair_suggestions_content}
188
+ <div id="analysis" class="tab-content active">
189
+ <h3 class="text-lg font-semibold text-indigo-700 mb-4">Diagnóstico por Atributos Ponderados</h3>
190
+ #{advanced_analysis_html}
191
+ #{repair_strategies_content}
208
192
  </div>
209
193
  <div id="all" class="tab-content">
210
194
  <h3 class="text-lg font-semibold text-gray-700 mb-4">Dump de Todos os Elementos da Tela</h3>
@@ -215,7 +199,7 @@ module AppiumFailureHelper
215
199
  </div>
216
200
  </div>
217
201
  </div>
218
- <script>
202
+ <script>
219
203
  document.addEventListener('DOMContentLoaded', () => {
220
204
  const tabs = document.querySelectorAll('.tab-button');
221
205
  tabs.forEach(tab => {
@@ -228,43 +212,56 @@ module AppiumFailureHelper
228
212
  document.getElementById(target).classList.add('active');
229
213
  });
230
214
  });
231
-
232
- const carousel = document.getElementById('xpath-carousel');
233
- if (carousel) {
234
- const track = carousel.querySelector('.carousel-track');
235
- const items = carousel.querySelectorAll('.carousel-item');
236
- const prevButton = carousel.querySelector('.carousel-prev-footer');
237
- const nextButton = carousel.querySelector('.carousel-next-footer');
238
- const counter = carousel.querySelector('.carousel-counter');
239
- const totalItems = items.length;
240
- let currentIndex = 0;
241
-
242
- function updateCarousel() {
243
- if (totalItems === 0) {
244
- if(counter) counter.textContent = "";
245
- return;
246
- };
247
- track.style.transform = `translateX(-${currentIndex * 100}%)`;
248
- if (counter) { counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`; }
249
- if (prevButton) { prevButton.disabled = currentIndex === 0; }
250
- if (nextButton) { nextButton.disabled = currentIndex === totalItems - 1; }
251
- }
252
-
253
- if (nextButton) {
254
- nextButton.addEventListener('click', () => {
255
- if (currentIndex < totalItems - 1) { currentIndex++; updateCarousel(); }
256
- });
257
- }
258
-
259
- if (prevButton) {
260
- prevButton.addEventListener('click', () => {
261
- if (currentIndex > 0) { currentIndex--; updateCarousel(); }
262
- });
263
- }
264
-
265
- if (totalItems > 0) { updateCarousel(); }
266
- }
267
215
  });
216
+ document.addEventListener('DOMContentLoaded', () => {
217
+ const tabs = document.querySelectorAll('.tab-button');
218
+ tabs.forEach(tab => {
219
+ tab.addEventListener('click', (e) => {
220
+ e.preventDefault();
221
+ const target = tab.getAttribute('data-tab');
222
+ tabs.forEach(t => t.classList.remove('active'));
223
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
224
+ tab.classList.add('active');
225
+ document.getElementById(target).classList.add('active');
226
+ });
227
+ });
228
+
229
+ const carousel = document.getElementById('xpath-carousel');
230
+ if (carousel) {
231
+ const track = carousel.querySelector('.carousel-track');
232
+ const items = carousel.querySelectorAll('.carousel-item');
233
+ const prevButton = carousel.querySelector('.carousel-prev-footer');
234
+ const nextButton = carousel.querySelector('.carousel-next-footer');
235
+ const counter = carousel.querySelector('.carousel-counter');
236
+ const totalItems = items.length;
237
+ let currentIndex = 0;
238
+
239
+ function updateCarousel() {
240
+ if (totalItems === 0) {
241
+ if(counter) counter.textContent = "";
242
+ return;
243
+ };
244
+ track.style.transform = `translateX(-${currentIndex * 100}%)`;
245
+ if (counter) { counter.textContent = `Página ${currentIndex + 1} de ${totalItems}`; }
246
+ if (prevButton) { prevButton.disabled = currentIndex === 0; }
247
+ if (nextButton) { nextButton.disabled = currentIndex === totalItems - 1; }
248
+ }
249
+
250
+ if (nextButton) {
251
+ nextButton.addEventListener('click', () => {
252
+ if (currentIndex < totalItems - 1) { currentIndex++; updateCarousel(); }
253
+ });
254
+ }
255
+
256
+ if (prevButton) {
257
+ prevButton.addEventListener('click', () => {
258
+ if (currentIndex > 0) { currentIndex--; updateCarousel(); }
259
+ });
260
+ }
261
+
262
+ if (totalItems > 0) { updateCarousel(); }
263
+ }
264
+ });
268
265
  </script>
269
266
  </body>
270
267
  </html>
@@ -273,6 +270,7 @@ module AppiumFailureHelper
273
270
 
274
271
  def build_simple_diagnosis_report(title:, message:)
275
272
  exception = @data[:exception]
273
+ screenshot = @data[:screenshot_base_64]
276
274
  error_message_html = CGI.escapeHTML(exception.message.to_s)
277
275
  backtrace_html = CGI.escapeHTML(exception.backtrace.join("\n"))
278
276
 
@@ -294,7 +292,7 @@ module AppiumFailureHelper
294
292
  <div class="md:col-span-1">
295
293
  <div class="bg-white p-4 rounded-lg shadow-md">
296
294
  <h2 class="text-xl font-bold text-gray-800 mb-4">Screenshot da Falha</h2>
297
- <img src="data:image/png;base64,#{@data[:screenshot_base64]}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
295
+ <img src="data:image/png;base64,#{screenshot}" alt="Screenshot da Falha" class="w-full rounded-md shadow-lg border border-gray-200">
298
296
  </div>
299
297
  </div>
300
298
  <div class="md:col-span-2 space-y-6">
@@ -1,3 +1,3 @@
1
1
  module AppiumFailureHelper
2
- VERSION = "1.5.0"
2
+ VERSION = "1.5.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appium_failure_helper
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Nascimento
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-02 00:00:00.000000000 Z
11
+ date: 2025-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -90,11 +90,15 @@ executables: []
90
90
  extensions: []
91
91
  extra_rdoc_files: []
92
92
  files:
93
+ - ".idea/.gitignore"
94
+ - ".idea/Appium_failure_helper.iml"
95
+ - ".idea/misc.xml"
96
+ - ".idea/modules.xml"
97
+ - ".idea/vcs.xml"
93
98
  - ".rspec"
94
99
  - LICENSE.txt
95
100
  - README.md
96
101
  - Rakefile
97
- - appium_failure_helper-1.4.0.gem
98
102
  - elements/cadastro.yaml
99
103
  - elements/login.yaml
100
104
  - img/fluxo_appium_failure_helper.png
Binary file