marta 0.37396 → 0.41245
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/README.md +42 -19
- data/example_project/p_object/test_page.rb +2 -1
- data/example_project/project_itself/iframe.html +5 -0
- data/{lib/marta/data/test.html → example_project/project_itself/index.html} +1 -1
- data/example_project/spec/p_object/pageobjects/MartaTestPage.json +707 -124
- data/example_project/spec/p_object/pageobjects/TheIframe.json +70 -14
- data/example_project/spec/spec_helper.rb +3 -1
- data/lib/marta/black_magic.rb +59 -13
- data/lib/marta/data/element.html +4 -15
- data/lib/marta/data/element.js +25 -226
- data/lib/marta/data/style.css +2 -2
- data/lib/marta/dialogs.rb +52 -4
- data/lib/marta/element_information.rb +86 -0
- data/lib/marta/injector.rb +2 -2
- data/lib/marta/json_2_class.rb +1 -1
- data/lib/marta/lightning.rb +1 -0
- data/lib/marta/marta_app/content.js +8 -0
- data/lib/marta/marta_app/devtools.html +1 -0
- data/lib/marta/marta_app/devtools.js +5 -0
- data/lib/marta/marta_app/manifest.json +3 -2
- data/lib/marta/options_and_paths.rb +1 -10
- data/lib/marta/page_arithmetic.rb +85 -97
- data/lib/marta/public_methods.rb +5 -4
- data/lib/marta/read_write.rb +50 -1
- data/lib/marta/server.rb +0 -18
- data/lib/marta/simple_element_finder.rb +0 -19
- data/lib/marta/version.rb +1 -1
- data/lib/marta/x_path.rb +154 -139
- metadata +7 -3
data/lib/marta/public_methods.rb
CHANGED
@@ -6,7 +6,6 @@ module Marta
|
|
6
6
|
|
7
7
|
include OptionsAndPaths
|
8
8
|
|
9
|
-
|
10
9
|
#
|
11
10
|
# User can create pageobject class using SmartPage.new
|
12
11
|
#
|
@@ -44,10 +43,12 @@ module Marta
|
|
44
43
|
method_name = correct_name(name)
|
45
44
|
exact_name = method_name.to_s + "_exact"
|
46
45
|
data = user_method_dialogs(method_name)
|
47
|
-
define_singleton_method method_name.
|
48
|
-
|
46
|
+
define_singleton_method method_name.
|
47
|
+
to_sym do |meth_content=@data['meths'][method_name]|
|
48
|
+
marta_magic_finder(meth_content, method_name)
|
49
49
|
end
|
50
|
-
define_singleton_method exact_name.
|
50
|
+
define_singleton_method exact_name.
|
51
|
+
to_sym do |meth_content=@data['meths'][method_name]|
|
51
52
|
marta_simple_finder(meth_content)
|
52
53
|
end
|
53
54
|
public_send name.to_sym
|
data/lib/marta/read_write.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
require 'marta/options_and_paths'
|
2
|
+
require 'marta/element_information'
|
2
3
|
module Marta
|
3
4
|
|
4
5
|
# Marta has very simple read\write actions
|
5
6
|
module ReadWrite
|
6
7
|
|
8
|
+
include ElementInformation
|
9
|
+
|
7
10
|
private
|
8
11
|
|
9
12
|
#
|
@@ -11,7 +14,7 @@ module Marta
|
|
11
14
|
#
|
12
15
|
# @note It is believed that no user will use it
|
13
16
|
class ReaderWriter
|
14
|
-
include OptionsAndPaths
|
17
|
+
include OptionsAndPaths, ElementInformation
|
15
18
|
# Marta is writing to jsons from time to time
|
16
19
|
def self.file_write(name, data)
|
17
20
|
file_name = File.join(SettingMaster.pageobjects_folder, name + '.json')
|
@@ -27,10 +30,56 @@ module Marta
|
|
27
30
|
begin
|
28
31
|
file = File.read(json)
|
29
32
|
data = JSON.parse(file)
|
33
|
+
# If there are methods
|
34
|
+
if data['meths'] != {}
|
35
|
+
# If there are old methods
|
36
|
+
if !data['meths'].first[1]['options']['self'].nil? or !data['meths'].first[1]['options']['not_self'].nil?
|
37
|
+
data = treat_old_version(data)
|
38
|
+
File.open(json,"w") do |f|
|
39
|
+
f.write(JSON.pretty_generate(data))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
return data
|
30
44
|
rescue
|
31
45
|
nil
|
32
46
|
end
|
33
47
|
end
|
48
|
+
|
49
|
+
def self.treat_old_version(data)
|
50
|
+
result, result['meths'] = Hash.new, Hash.new
|
51
|
+
result['vars'] = data['vars']
|
52
|
+
# Taking all methods one by one
|
53
|
+
data['meths'].each_pair do |method, method_content|
|
54
|
+
result['meths'][method] = ElementHelper.method_structure
|
55
|
+
result['meths'][method]['options']['collection'] = method_content['options'].to_h['collection']
|
56
|
+
['self','pappy','granny'].each do |level|
|
57
|
+
if !method_content['options'].to_h[level].nil?
|
58
|
+
result['meths'][method]['positive'][level]['tag'] = [method_content['options'][level]] - ['*']
|
59
|
+
end
|
60
|
+
if !method_content[level].to_h['retrieved_by_marta_text'].nil?
|
61
|
+
result['meths'][method]['positive'][level]['text'] = [method_content[level]['retrieved_by_marta_text']]
|
62
|
+
end
|
63
|
+
method_content[level].to_h.each_pair do |name, value|
|
64
|
+
if name != "retrieved_by_marta_text"
|
65
|
+
result['meths'][method]['positive'][level]['attributes'][name] = value.class == String ? value.split(' ').uniq : value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
if !method_content['options'].to_h["not_#{level}"].nil?
|
69
|
+
result['meths'][method]['negative'][level]['tag'] = [method_content['options']["not_#{level}"]] - ['*']
|
70
|
+
end
|
71
|
+
if !method_content["not_#{level}"].to_h['retrieved_by_marta_text'].nil?
|
72
|
+
result['meths'][method]['negative'][level]['text'] = [method_content["not_#{level}"]['retrieved_by_marta_text']]
|
73
|
+
end
|
74
|
+
method_content["not_#{level}"].to_h.each_pair do |name, value|
|
75
|
+
if name != "retrieved_by_marta_text"
|
76
|
+
result['meths'][method]['negative'][level]['attributes'][name] = value.class == String ? value.split(' ').uniq : value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
result
|
82
|
+
end
|
34
83
|
end
|
35
84
|
|
36
85
|
def file_write(name, data)
|
data/lib/marta/server.rb
CHANGED
@@ -67,18 +67,6 @@ module Marta
|
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
70
|
-
#
|
71
|
-
# Test servlet. For example
|
72
|
-
#
|
73
|
-
# @note It is believed that no user will use it
|
74
|
-
class TestServlet < WEBrick::HTTPServlet::AbstractServlet
|
75
|
-
def do_GET (request, response)
|
76
|
-
response.status = 200
|
77
|
-
response.content_type = "text/html"
|
78
|
-
response.body = File.read(gem_libdir + "/data/test.html")
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
70
|
#
|
83
71
|
# Server control and logic is in the class.
|
84
72
|
#
|
@@ -112,7 +100,6 @@ module Marta
|
|
112
100
|
AccessLog: WEBrick::Log.new(File.open(File::NULL, 'w')))
|
113
101
|
the_server.mount "/dialog", DialogServlet
|
114
102
|
the_server.mount "/welcome", WelcomeServlet
|
115
|
-
the_server.mount "/test", TestServlet
|
116
103
|
the_server.mount_proc('/q') {|req, resp| the_server.shutdown; exit;}
|
117
104
|
the_server.start
|
118
105
|
end
|
@@ -140,11 +127,6 @@ module Marta
|
|
140
127
|
false
|
141
128
|
end
|
142
129
|
|
143
|
-
# There is a possibility to get the port of the server outside
|
144
|
-
def current_port
|
145
|
-
@port
|
146
|
-
end
|
147
|
-
|
148
130
|
# We are killing the server sometimes
|
149
131
|
def server_kill
|
150
132
|
# So nasty. But WEBrick knows what to do.
|
@@ -72,25 +72,6 @@ module Marta
|
|
72
72
|
subtype_of prefind
|
73
73
|
end
|
74
74
|
end
|
75
|
-
|
76
|
-
# Sometimes we need to find out what is hidden
|
77
|
-
def find_invisibles
|
78
|
-
result = Array.new
|
79
|
-
all = @engine.elements
|
80
|
-
all.each do |element|
|
81
|
-
if element.exists? and !element.visible?
|
82
|
-
result.push element
|
83
|
-
else
|
84
|
-
x, y = element.wd.location.x + 1, element.wd.location.y + 1
|
85
|
-
element_on_point = @engine.
|
86
|
-
execute_script "return document.elementFromPoint(#{x}, #{y})"
|
87
|
-
if element_on_point != element
|
88
|
-
result.push element
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
return result
|
93
|
-
end
|
94
75
|
end
|
95
76
|
|
96
77
|
# We can simply find something
|
data/lib/marta/version.rb
CHANGED
data/lib/marta/x_path.rb
CHANGED
@@ -31,181 +31,196 @@ module Marta
|
|
31
31
|
@requestor = requestor
|
32
32
|
end
|
33
33
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
34
|
+
# We are trying to understand here how much work should we do in order
|
35
|
+
# to generate all possible xpaths variants.
|
36
|
+
#
|
37
|
+
# depth is suggested amount of unstable xpath parts
|
38
|
+
# limit is the maximum amount of xpaths that we want to generate
|
39
|
+
# If we can try all the combinations of xpaths with considering
|
40
|
+
# depth elements unstable withou reaching the limit of tries,
|
41
|
+
# method will return that depth and precreated array_of_hashes
|
42
|
+
# If limit will be reached on creation of all xpaths, method
|
43
|
+
# is returning last acceptable depth and array_of_hashes
|
44
|
+
def analyze(depth, limit)
|
45
|
+
hashes = array_of_hashes
|
46
|
+
count = 1
|
47
|
+
real_depth = 0
|
48
|
+
variativity = 0
|
49
|
+
hashes.each do |hash|
|
50
|
+
variativity += (hash[:empty] - hash[:full]).count
|
43
51
|
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# Creating the granny part of xpath
|
47
|
-
def create_granny
|
48
|
-
# We are suggesting that granny is not a very first element
|
49
|
-
result = get_xpaths(@granny, 'granny')
|
50
|
-
result[0] = make_hash("//", "//")
|
51
|
-
result
|
52
|
-
end
|
53
|
-
|
54
|
-
# Creating the pappy part of xpath
|
55
|
-
def create_pappy
|
56
|
-
get_xpaths(@pappy, 'pappy')
|
57
|
-
end
|
58
|
-
|
59
|
-
# Creating self part of xpath
|
60
|
-
def create_self
|
61
|
-
get_xpaths(true, 'self')
|
62
|
-
end
|
63
|
-
|
64
|
-
# Full array of hashes to transform into xpath
|
65
|
-
def create_xpath
|
66
|
-
result_array = Array.new
|
67
|
-
result_array = create_granny + create_pappy + create_self
|
68
|
-
end
|
69
|
-
|
70
|
-
# Creating hash arrays from base array
|
71
|
-
def form_variants(depth)
|
72
|
-
work_array = [create_xpath]
|
73
52
|
depth.times do
|
74
|
-
|
75
|
-
|
76
|
-
|
53
|
+
count = count * variativity
|
54
|
+
if count < limit
|
55
|
+
real_depth += 1
|
77
56
|
end
|
78
|
-
work_array = (work_array + temp_array).uniq
|
79
57
|
end
|
80
|
-
|
58
|
+
return real_depth, hashes
|
81
59
|
end
|
82
60
|
|
83
|
-
#
|
84
|
-
#
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
form_variants(depth).each do |variant|
|
92
|
-
xpaths = [""]
|
93
|
-
variant.each do |part|
|
94
|
-
xpaths = tree_push(xpaths, part[:full])
|
61
|
+
# Generating not more than limit random xpaths variants considering
|
62
|
+
# that depth parts of xpath are unstable
|
63
|
+
def monte_carlo(hashes, depth, limit)
|
64
|
+
xpaths = Array.new
|
65
|
+
while xpaths.count < limit do
|
66
|
+
mask = Array.new hashes.count, :full
|
67
|
+
depth.times do
|
68
|
+
mask[rand(mask.count)] = :empty
|
95
69
|
end
|
96
|
-
|
70
|
+
final_array = Array.new
|
71
|
+
hashes.each_with_index do |hash, index|
|
72
|
+
final_array.push(hash[mask[index]].sample)
|
73
|
+
end
|
74
|
+
xpaths.push final_array.join
|
75
|
+
xpaths
|
97
76
|
end
|
98
|
-
|
77
|
+
xpaths
|
99
78
|
end
|
100
79
|
|
101
|
-
#
|
102
|
-
# are more
|
103
|
-
|
80
|
+
# We are generating masks like [:empty,:full,:full,:empty]
|
81
|
+
# They are used for more understandable logic of looping xpaths variants
|
82
|
+
# In fact they are lists with all the combinations of
|
83
|
+
# switches :full\:empty. Where the length of switches is the same as
|
84
|
+
# length of xpath parts. And amount of :empty switches is depth
|
85
|
+
def get_masks(masks, depth)
|
104
86
|
result = Array.new
|
105
|
-
|
106
|
-
|
107
|
-
|
87
|
+
masks.each do |mask|
|
88
|
+
result.push mask
|
89
|
+
for i in 0..mask.count-1
|
90
|
+
result.push(mask.map { |e| e.dup })
|
91
|
+
result.last[i] = :empty
|
108
92
|
end
|
109
93
|
end
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
generate_xpaths(0).join
|
94
|
+
if depth-1 == 0
|
95
|
+
result
|
96
|
+
else
|
97
|
+
get_masks(result, depth-1)
|
98
|
+
end
|
116
99
|
end
|
117
100
|
|
118
|
-
#
|
119
|
-
def
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
if
|
125
|
-
|
101
|
+
# We are forming xpath strings by masks and hashes with data
|
102
|
+
def xpaths_by_mask(mask, hashes)
|
103
|
+
xpaths = Array.new
|
104
|
+
final_array = [[]]
|
105
|
+
hashes.each_with_index do |hash, index|
|
106
|
+
hash[mask[index]].each_with_index do |hash_value, empty_index|
|
107
|
+
if empty_index == 0
|
108
|
+
final_array.each do |final_array_item|
|
109
|
+
final_array_item.push(hash_value)
|
110
|
+
end
|
126
111
|
else
|
127
|
-
|
112
|
+
alternative_final_array = []
|
113
|
+
final_array.each do |final_array_item|
|
114
|
+
alternative_final_array.push final_array_item.dup
|
115
|
+
end
|
116
|
+
alternative_final_array.each do |a_final_array_item|
|
117
|
+
a_final_array_item[-1] = hash_value
|
118
|
+
end
|
119
|
+
final_array = final_array + alternative_final_array
|
128
120
|
end
|
129
121
|
end
|
130
|
-
result_array = result_array.push temp_array
|
131
122
|
end
|
132
|
-
|
123
|
+
final_array.each do |final_array_item|
|
124
|
+
xpaths.push final_array_item.join
|
125
|
+
end
|
126
|
+
xpaths
|
127
|
+
end
|
128
|
+
|
129
|
+
# Full logic of xpath generating
|
130
|
+
# We are understanding can we find all the possible xpath variations
|
131
|
+
# We are creating all possible masks (arrays of switches) one by one
|
132
|
+
# If we know that we cannot find all variants we are generating some
|
133
|
+
# by more random algorithm
|
134
|
+
def generate_xpaths(depth, limit = 100000)
|
135
|
+
xpaths = Array.new
|
136
|
+
real_depth, hashes = analyze(depth, limit)
|
137
|
+
masks = get_masks([Array.new(hashes.count, :full)], real_depth)
|
138
|
+
masks.each do |mask|
|
139
|
+
xpaths = xpaths + xpaths_by_mask(mask, hashes)
|
140
|
+
end
|
141
|
+
if real_depth != depth
|
142
|
+
xpaths = xpaths + monte_carlo(hashes, depth, limit)
|
143
|
+
end
|
144
|
+
xpaths.uniq.map {|xpath| process_string(xpath, @requestor)}
|
133
145
|
end
|
134
146
|
|
135
|
-
#
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
result_array.push make_hash("[not(self::#{tag})]", "")
|
141
|
-
end
|
142
|
-
else
|
143
|
-
result_array.push make_hash("/", "//")
|
144
|
-
result_array.push make_hash(tag, "*")
|
147
|
+
# We can generate straight xpath by all known data
|
148
|
+
def generate_xpath
|
149
|
+
result = ''
|
150
|
+
array_of_hashes.each do |hash|
|
151
|
+
result = result + hash[:full][0]
|
145
152
|
end
|
146
|
-
|
153
|
+
process_string result, @requestor
|
147
154
|
end
|
148
155
|
|
149
|
-
#
|
150
|
-
def
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
negative)
|
162
|
-
end
|
156
|
+
# We are parsing positive part of element data to array of hashes
|
157
|
+
def positive_part_of_array_of_hashes(what)
|
158
|
+
result = Array.new
|
159
|
+
result.push make_hash(@meth['positive'][what]['tag'] != [] ? @meth['positive'][what]['tag'][0] : '*', '*')
|
160
|
+
if (@meth['positive'][what]['text'] != []) and (@meth['positive'][what]['text'] != [''])
|
161
|
+
result.push make_hash("[contains(text(),'#{@meth['positive'][what]['text'][0]}')]")
|
162
|
+
end
|
163
|
+
@meth['positive'][what]['attributes'].each_pair do |attribute, values|
|
164
|
+
if (values != []) and (values != ['']) and !values.nil?
|
165
|
+
values.each do |value|
|
166
|
+
result.push make_hash("[contains(@#{attribute},'#{value}')]",
|
167
|
+
["[@*[contains(.,'#{value}')]]", ""])
|
163
168
|
end
|
164
169
|
end
|
165
170
|
end
|
166
|
-
|
171
|
+
result
|
167
172
|
end
|
168
173
|
|
169
|
-
#
|
170
|
-
def
|
171
|
-
|
172
|
-
|
173
|
-
make_hash("[
|
174
|
-
else
|
175
|
-
make_hash("[#{not_start}@#{attribute}='#{value}'#{not_end}]", ["",
|
176
|
-
"[#{not_start}@*='#{value}'#{not_end}]"])
|
174
|
+
# We are parsing negative part of element data to array of hashes
|
175
|
+
def negative_part_of_array_of_hashes(what)
|
176
|
+
result = Array.new
|
177
|
+
@meth['negative'][what]['tag'].each do |not_tag|
|
178
|
+
result.push make_hash("[not(self::#{not_tag})]", '')
|
177
179
|
end
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
result_array = Array.new
|
187
|
-
not_start, not_end = get_nots_frames(negative)
|
188
|
-
value.each do |value_part|
|
189
|
-
if value_part.gsub(' ','') != ''
|
190
|
-
result_array.
|
191
|
-
push make_hash("[#{not_start}contains(@#{attribute},"\
|
192
|
-
"'#{value_part}')#{not_end}]", ["",
|
193
|
-
"[#{not_start}@*[contains"\
|
194
|
-
"(.,'#{value_part}')]#{not_end}]"])
|
180
|
+
@meth['negative'][what]['text'].each do |not_text|
|
181
|
+
result.push make_hash("[not(contains(text(),'#{not_text}'))]", '')
|
182
|
+
end
|
183
|
+
@meth['negative'][what]['attributes'].each_pair do |attribute, values|
|
184
|
+
if (values != []) and (values != ['']) and !values.nil?
|
185
|
+
values.each do |value|
|
186
|
+
result.push make_hash("[not(contains(@#{attribute},'#{value}'))]")
|
187
|
+
end
|
195
188
|
end
|
196
189
|
end
|
197
|
-
|
190
|
+
result
|
198
191
|
end
|
199
192
|
|
200
|
-
#
|
201
|
-
|
202
|
-
|
203
|
-
|
193
|
+
# We are parsing stored element data (tag, text and attributes)
|
194
|
+
# into the array of hashes
|
195
|
+
#
|
196
|
+
# Output looks like:
|
197
|
+
# [{full:["//"],empty["//"]},
|
198
|
+
# {full:["H1"],empty["*"]},
|
199
|
+
# {full:["[@id='x']"],empty["", "[@*[contains(.,'x')]]"]}]
|
200
|
+
def array_of_hashes
|
201
|
+
result = Array.new
|
202
|
+
result.push make_hash('//', '//')
|
203
|
+
if @granny
|
204
|
+
result = result +
|
205
|
+
positive_part_of_array_of_hashes('granny') +
|
206
|
+
negative_part_of_array_of_hashes('granny')
|
207
|
+
result.push make_hash('/', '//')
|
204
208
|
end
|
205
|
-
if
|
206
|
-
|
209
|
+
if @pappy
|
210
|
+
result = result +
|
211
|
+
positive_part_of_array_of_hashes('pappy') +
|
212
|
+
negative_part_of_array_of_hashes('pappy')
|
213
|
+
result.push make_hash('/', '//')
|
207
214
|
end
|
208
|
-
|
215
|
+
result = result +
|
216
|
+
positive_part_of_array_of_hashes('self') +
|
217
|
+
negative_part_of_array_of_hashes('self')
|
218
|
+
result
|
219
|
+
end
|
220
|
+
|
221
|
+
# Creating the smallest possible part of array hash
|
222
|
+
def make_hash(full, empty = '')
|
223
|
+
{full: [full], empty: empty.class != Array ? [empty] : empty}
|
209
224
|
end
|
210
225
|
end
|
211
226
|
end
|