marta 0.37396 → 0.41245
Sign up to get free protection for your applications and to get access to all the features.
- 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
|