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 +4 -4
- data/.idea/.gitignore +8 -0
- data/.idea/Appium_failure_helper.iml +48 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/lib/appium_failure_helper/analyzer.rb +43 -62
- data/lib/appium_failure_helper/handler.rb +46 -52
- data/lib/appium_failure_helper/page_analyzer.rb +10 -18
- data/lib/appium_failure_helper/report_generator.rb +123 -125
- data/lib/appium_failure_helper/version.rb +1 -1
- metadata +7 -3
- data/appium_failure_helper-1.4.0.gem +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3ef5fd316590ddca6a0b9ccc21b06f947c3b439fb998668b2b965ae5f110702a
|
4
|
+
data.tar.gz: af17ce648f330a535cd5d0582fd4441f2cbcc881fd67626e8516a202655e747e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bec158f942080f960031179836e9830ea7f3fd2cc48117d5b803d5156d0ed89ee27bf1377c10a36f7ec4be92aeea7c9eac60e2232c77e75a5c19fd390b9fb306
|
7
|
+
data.tar.gz: 66a21888247d7bb983556428f3aad65dfa8fd9b3f576d670bc736a4db1e520b9874b2b01628b9fc7f3ad94dd06363d2d0faacb78c6ad042d373eb60c75ffcd84
|
data/.idea/.gitignore
ADDED
@@ -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
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
@@ -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
|
-
|
7
|
-
|
8
|
-
:
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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.
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
73
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
33
|
-
|
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
|
40
|
-
doc = Nokogiri::XML(page_source)
|
41
|
-
|
42
|
-
failed_info =
|
43
|
-
|
44
|
-
|
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
|
-
|
66
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
24
|
-
seen_elements = {}
|
23
|
+
def analyze
|
25
24
|
all_elements_suggestions = []
|
26
25
|
@doc.xpath('//*').each do |node|
|
27
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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]
|
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
|
-
|
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 =
|
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
|
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
|
-
|
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[:
|
55
|
+
screenshot_base64 = @data[:screenshot_base_64]
|
60
56
|
|
61
57
|
locators_html = lambda do |locators|
|
62
|
-
(locators || []).map
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
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
|
-
|
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"> < 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 > </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="
|
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="
|
206
|
-
<h3 class="text-lg font-semibold text-indigo-700 mb-4">
|
207
|
-
#{
|
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
|
-
|
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,#{
|
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">
|
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.
|
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-
|
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
|