awetestlib 0.1.29pre3 → 0.1.29pre4
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.
- data/README.md +16 -8
- data/awetestlib.windows.gemspec +42 -41
- data/awetestlib_osx.gemspec +41 -47
- data/bin/awetestlib-android-setup.rb +2 -1
- data/bin/awetestlib-cucumber-setup.rb +2 -1
- data/bin/awetestlib-driver-setup.rb +1 -0
- data/bin/awetestlib-helpers.rb +2 -2
- data/bin/awetestlib-mobile-app-setup.rb +1 -0
- data/bin/awetestlib-netbeans-setup.rb +2 -1
- data/bin/awetestlib-regression-setup.rb +26 -12
- data/bin/awetestlib-rubymine-setup.rb +9 -4
- data/images/netbeans1.jpg +0 -0
- data/images/netbeans2.jpg +0 -0
- data/images/netbeans3.jpg +0 -0
- data/images/netbeans4.jpg +0 -0
- data/images/netbeans5.jpg +0 -0
- data/images/rubymine1.jpg +0 -0
- data/images/rubymine2.jpg +0 -0
- data/images/rubymine3.jpg +0 -0
- data/images/rubymine4.jpg +0 -0
- data/images/scripting1.png +0 -0
- data/images/scripting2.png +0 -0
- data/images/scripting3.png +0 -0
- data/images/scripting4.png +0 -0
- data/lib/awetestlib/logging.rb +6 -6
- data/lib/awetestlib/regression/browser.rb +15 -12
- data/lib/awetestlib/regression/drag_and_drop.rb +421 -421
- data/lib/awetestlib/regression/runner.rb +311 -307
- data/lib/awetestlib/regression/tables.rb +627 -627
- data/lib/awetestlib/regression/user_input.rb +576 -576
- data/lib/awetestlib/regression/utilities.rb +1056 -1046
- data/lib/awetestlib/regression/validations.rb +2 -1
- data/lib/version.rb +2 -2
- data/netbeans_setup.md +30 -30
- data/rubymine_setup.md +24 -24
- data/setup_samples/sample_cucumber/features/step_definitions/predefined_steps.rb +303 -25
- metadata +160 -34
- checksums.yaml +0 -7
@@ -1,421 +1,421 @@
|
|
1
|
-
module Awetestlib
|
2
|
-
module Regression
|
3
|
-
# Methods for moving and resizing elements, manipulating the mouse, and checking for relative positioning of elements,
|
4
|
-
# including overlap, overlay, etc.
|
5
|
-
# @note Still experimental. Works with IE but not fully tested with Firefox or Chrome in Windows using Watir-webdriver.
|
6
|
-
# Not compatible with Mac
|
7
|
-
# Rdoc is work in progress
|
8
|
-
module DragAndDrop
|
9
|
-
|
10
|
-
# Verify that specified *inner_element* is fully enclosed by *outer_element*.
|
11
|
-
# @param [Watir::Element] inner_element A reference to a DOM element
|
12
|
-
# @param [Watir::Element] outer_element A reference to a DOM element
|
13
|
-
# @param [String] desc Contains a message or description intended to appear in the log and/or report output
|
14
|
-
def verify_element_inside(inner_element, outer_element, desc = '')
|
15
|
-
mark_testlevel("#{__method__.to_s.titleize}", 3)
|
16
|
-
msg = build_message("#{inner_element.class.to_s} (:id=#{inner_element.id}) is fully enclosed by "+
|
17
|
-
"#{outer_element.class.to_s} (:id=#{outer_element.id}).", desc)
|
18
|
-
if overlay?(inner_element, outer_element, :inside)
|
19
|
-
failed_to_log(msg)
|
20
|
-
else
|
21
|
-
passed_to_log(msg)
|
22
|
-
true
|
23
|
-
end
|
24
|
-
rescue
|
25
|
-
failed_to_log("Unable to verify that #{msg} '#{$!}'")
|
26
|
-
end
|
27
|
-
|
28
|
-
# Verify that two elements, identified by specified attribute and value, do not overlap on a given *side*.
|
29
|
-
# @param [Symbol] above_element The element type for the first element, e.g. :div, :span, etc.
|
30
|
-
# @param [Symbol] above_how The element attribute used to identify the *above_element*.
|
31
|
-
# Valid values depend on the kind of element.
|
32
|
-
# Common values: :text, :id, :title, :name, :class, :href (:link only)
|
33
|
-
# @param [String, Regexp] above_what A string or a regular expression to be found in the *above_how* attribute that uniquely identifies the element.
|
34
|
-
# @param [Symbol] below_element The element type for the second element, e.g. :div, :span, etc.
|
35
|
-
# @param [Symbol] below_how The element attribute used to identify the *below_element*.
|
36
|
-
# @param [String, Regexp] below_what A string or a regular expression to be found in the *below_how* attribute that uniquely identifies the element.
|
37
|
-
# @param [Symbol] side :top, :bottom, :left, :right, :inside, or :outside
|
38
|
-
# @param [String] desc Contains a message or description intended to appear in the log and/or report output
|
39
|
-
def verify_no_element_overlap(browser, above_element, above_how, above_what, below_element, below_how, below_what, side, desc = '')
|
40
|
-
mark_testlevel("#{__method__.to_s.titleize}", 3)
|
41
|
-
msg = build_message("#{above_element.to_s.titleize} #{above_how}=>#{above_what} does not overlap "+
|
42
|
-
"#{below_element.to_s.titleize} #{below_how}=>#{below_what} at the #{side}.", desc)
|
43
|
-
above = browser.element(above_how, above_what)
|
44
|
-
below = browser.element(below_how, below_what)
|
45
|
-
if overlay?(above, below, side)
|
46
|
-
failed_to_log(msg)
|
47
|
-
else
|
48
|
-
passed_to_log(msg)
|
49
|
-
true
|
50
|
-
end
|
51
|
-
rescue
|
52
|
-
failed_to_log("Unable to verify that #{msg} '#{$!}'")
|
53
|
-
end
|
54
|
-
|
55
|
-
def overlay?(inner, outer, side = :bottom)
|
56
|
-
#mark_testlevel("#{__method__.to_s.titleize}", 3)
|
57
|
-
inner_t, inner_b, inner_l, inner_r = inner.bounding_rectangle_offsets
|
58
|
-
outer_t, outer_b, outer_l, outer_r = outer.bounding_rectangle_offsets
|
59
|
-
#overlay = false
|
60
|
-
case side
|
61
|
-
when :bottom
|
62
|
-
overlay = inner_b > outer_t
|
63
|
-
when :top
|
64
|
-
overlay = inner_t > outer_t
|
65
|
-
when :left
|
66
|
-
overlay = inner_l < outer_r
|
67
|
-
when :right
|
68
|
-
overlay = inner_r > outer_r
|
69
|
-
when :inside
|
70
|
-
overlay = !(inner_t > outer_t and
|
71
|
-
inner_r < outer_r and
|
72
|
-
inner_l > outer_l and
|
73
|
-
inner_b < outer_b)
|
74
|
-
else
|
75
|
-
overlay = (inner_t > outer_b or
|
76
|
-
inner_r > outer_l or
|
77
|
-
inner_l < outer_r or
|
78
|
-
inner_b < outer_t)
|
79
|
-
end
|
80
|
-
overlay
|
81
|
-
rescue
|
82
|
-
failed_to_log("Unable to determine overlay. '#{$!}'")
|
83
|
-
end
|
84
|
-
|
85
|
-
def hover(browser, element, wait = 2)
|
86
|
-
w1, h1, x1, y1, xc1, yc1, xlr1, ylr1 = get_element_coordinates(browser, element, true)
|
87
|
-
@ai.MoveMouse(xc1, yc1)
|
88
|
-
sleep_for(1)
|
89
|
-
end
|
90
|
-
|
91
|
-
def move_element_with_handle(browser, element, handle_id, dx, dy)
|
92
|
-
# msg = "Move element "
|
93
|
-
# w1, h1, x1, y1, xc1, yc1, xlr1, ylr1 = get_element_coordinates(browser, element, true)
|
94
|
-
# newx = w1 + dx
|
95
|
-
# newy = h1 + dy
|
96
|
-
# msg << " by [#{dx}, #{dy}] to expected [[#{newx}, #{newy}] "
|
97
|
-
# handle = get_resize_handle(element, handle_id)
|
98
|
-
# hw, hh, hx, hy, hxc, hyc, hxlr, hylr = get_element_coordinates(browser, handle, true)
|
99
|
-
|
100
|
-
# drag_and_drop(hxc, hyc, dx, dy)
|
101
|
-
|
102
|
-
# w2, h2, x2, y2, xc2, yc2, xlr2, ylr2 = get_element_coordinates(browser, element, true)
|
103
|
-
|
104
|
-
# xerr = x2 - newx
|
105
|
-
# yerr = y2 - newy
|
106
|
-
# xdsp = (x1 - x2).abs
|
107
|
-
# ydsp = (y1 - y2).abs
|
108
|
-
|
109
|
-
# if x2 == newx and y2 == newy
|
110
|
-
# msg << "succeeded."
|
111
|
-
# passed_to_log(msg)
|
112
|
-
# else
|
113
|
-
# msg << "failed. "
|
114
|
-
# failed_to_log(msg)
|
115
|
-
# debug_to_log("x: actual #{x2}, error #{xerr}, displace #{xdsp}. y: actual #{y2}, error #{yerr}, displace #{ydsp}.")
|
116
|
-
# end
|
117
|
-
|
118
|
-
end
|
119
|
-
|
120
|
-
def resize_element_with_handle(browser, element, target, dx, dy=nil)
|
121
|
-
#TODO enhance to accept differing percentages in each direction
|
122
|
-
msg = "Resize element "
|
123
|
-
w1, h1, x1, y1, xc1, yc1, xlr1, ylr1 = get_element_coordinates(browser, element, true)
|
124
|
-
if dy
|
125
|
-
deltax = dx
|
126
|
-
deltay = dy
|
127
|
-
neww = w1 + dx
|
128
|
-
newh = h1 + dy
|
129
|
-
msg << " by [#{dx}, #{dy}] " #"" to expected dimension [#{neww}, #{newh}] "
|
130
|
-
else
|
131
|
-
deltax, deltay, neww, newh = adjust_dimensions_by_percent(w1, h1, dx, true)
|
132
|
-
msg << "by #{dx} percent " #"" to expected dimension [#{neww}, #{newh}] "
|
133
|
-
end
|
134
|
-
handle = get_resize_handle_by_class(element, target) #, true)
|
135
|
-
sleep_for(0.5)
|
136
|
-
hw, hh, hx, hy, hxc, hyc, hxlr, hylr = get_element_coordinates(browser, handle, true)
|
137
|
-
hxlr_diff = 0
|
138
|
-
hylr_diff = 0
|
139
|
-
|
140
|
-
# TODO These adjustments are adhoc and empirical. Need to be derived more rigorously
|
141
|
-
if @browserAbbrev == 'IE'
|
142
|
-
hxlr_diff = (xlr1 - hxlr)
|
143
|
-
hylr_diff = (ylr1 - hylr)
|
144
|
-
x_start = hxlr - 2
|
145
|
-
y_start = hylr - 2
|
146
|
-
else
|
147
|
-
hxlr_diff = (xlr1 - hxlr) / 2 unless (xlr1 - hxlr) == 0
|
148
|
-
hylr_diff = (ylr1 - hylr) / 2 unless (ylr1 - hylr) == 0
|
149
|
-
x_start = hxlr
|
150
|
-
y_start = hylr
|
151
|
-
end
|
152
|
-
|
153
|
-
newxlr = xlr1 + deltax
|
154
|
-
newylr = ylr1 + deltay
|
155
|
-
# msg << ", lower right [#{newxlr}, #{newylr}] - "
|
156
|
-
sleep_for(0.5)
|
157
|
-
|
158
|
-
drag_and_drop(x_start, y_start, deltax, deltay)
|
159
|
-
|
160
|
-
sleep_for(1.5)
|
161
|
-
w2, h2, x2, y2, xc2, yc2, xlr2, ylr2 = get_element_coordinates(browser, element, true)
|
162
|
-
|
163
|
-
werr = w2 - neww
|
164
|
-
herr = h2 - newh
|
165
|
-
|
166
|
-
# TODO This adjustment is adhoc and empirical. Needs to be derived more rigorously
|
167
|
-
xlrerr = xlr2 - newxlr + hxlr_diff
|
168
|
-
ylrerr = ylr2 - newylr + hylr_diff
|
169
|
-
|
170
|
-
xlrdsp = (xlr1 - xlr2).abs
|
171
|
-
ylrdsp = (ylr1 - ylr2).abs
|
172
|
-
|
173
|
-
debug_to_log("\n" +
|
174
|
-
"\t\t hxlr_diff: #{hxlr_diff}\n" +
|
175
|
-
"\t\t hylr_diff: #{hylr_diff}\n" +
|
176
|
-
"\t\t werr: #{werr}\n" +
|
177
|
-
"\t\t herr: #{herr}\n" +
|
178
|
-
"\t\t xlrerr: #{xlrerr}\n" +
|
179
|
-
"\t\t ylrerr: #{ylrerr}\n" +
|
180
|
-
"\t\t xlrdsp: #{xlrdsp}\n" +
|
181
|
-
"\t\t ylrdsp: #{ylrdsp}\n" +
|
182
|
-
"\t\t @min_width: #{@min_width}\n" +
|
183
|
-
"\t\t@min_height: #{@min_height}\n" +
|
184
|
-
"\t\t x tol: #{@x_tolerance}\n" +
|
185
|
-
"\t\t y tol: #{@y_tolerance}\n"
|
186
|
-
)
|
187
|
-
|
188
|
-
#TODO Add check that window _was_ resized.
|
189
|
-
x_ok, x_msg = validate_move(w2, xlrerr, @x_tolerance, @min_width, xlr2)
|
190
|
-
y_ok, y_msg = validate_move(h2, ylrerr, @y_tolerance, @min_height, ylr2)
|
191
|
-
msg = msg + "x: #{x_msg}, y: #{y_msg}"
|
192
|
-
|
193
|
-
if x_ok and y_ok
|
194
|
-
passed_to_log(msg)
|
195
|
-
else
|
196
|
-
failed_to_log(msg)
|
197
|
-
debug_to_log("x - actual #{xlr2}, error #{xlrerr}, displace #{xlrdsp}, y - actual #{ylr2}, error #{ylrerr}, displace #{ylrdsp}.")
|
198
|
-
end
|
199
|
-
sleep_for(1)
|
200
|
-
rescue
|
201
|
-
failed_to_log("Unable to validate resize. #{$!} (#{__LINE__})")
|
202
|
-
sleep_for(1)
|
203
|
-
end
|
204
|
-
|
205
|
-
# :category: GUI
|
206
|
-
def get_resize_handle_by_id(element, id, dbg=nil)
|
207
|
-
handle = get_div_by_id(element, id, dbg)
|
208
|
-
sleep_for(1)
|
209
|
-
handle.flash(5)
|
210
|
-
return handle
|
211
|
-
end
|
212
|
-
|
213
|
-
# :category: GUI
|
214
|
-
def get_resize_handle_by_class(element, strg, dbg=nil)
|
215
|
-
handle = get_div_by_class(element, strg, dbg)
|
216
|
-
sleep_for(0.5)
|
217
|
-
handle.flash(5)
|
218
|
-
return handle
|
219
|
-
end
|
220
|
-
|
221
|
-
# :category: GUI
|
222
|
-
def get_element_coordinates(browser, element, dbg=nil)
|
223
|
-
bx, by, bw, bh = get_browser_coord(browser, dbg)
|
224
|
-
if @browserAbbrev == 'IE'
|
225
|
-
x_hack = @horizontal_hack_ie
|
226
|
-
y_hack = @vertical_hack_ie
|
227
|
-
elsif @browserAbbrev == 'FF'
|
228
|
-
x_hack = @horizontal_hack_ff
|
229
|
-
y_hack = @vertical_hack_ff
|
230
|
-
end
|
231
|
-
sleep_for(1)
|
232
|
-
w, h = element.dimensions.to_a
|
233
|
-
xc, yc = element.client_offset.to_a
|
234
|
-
# xcc, ycc = element.client_center.to_a
|
235
|
-
xcc = xc + w/2
|
236
|
-
ycc = yc + h/2
|
237
|
-
# screen offset:
|
238
|
-
xs = bx + x_hack + xc - 1
|
239
|
-
ys = by + y_hack + yc - 1
|
240
|
-
# screen center:
|
241
|
-
xsc = xs + w/2
|
242
|
-
ysc = ys + h/2
|
243
|
-
xslr = xs + w
|
244
|
-
yslr = ys + h
|
245
|
-
if dbg
|
246
|
-
debug_to_log(
|
247
|
-
"\n\t\tElement: #{element.inspect}"+
|
248
|
-
"\n\t\tbrowser screen offset: x: #{bx} y: #{by}"+
|
249
|
-
"\n\t\t dimensions: x: #{w} y: #{h}"+
|
250
|
-
"\n\t\t client offset x: #{xc} y: #{yc}"+
|
251
|
-
"\n\t\t screen offset x: #{xs} y: #{ys}"+
|
252
|
-
"\n\t\t client center x: #{xcc} y: #{ycc}"+
|
253
|
-
"\n\t\t screen center x: #{xsc} y: #{ysc}"+
|
254
|
-
"\n\t\t screen lower right x: #{xslr} y: #{yslr}")
|
255
|
-
end
|
256
|
-
[w, h, xs, ys, xsc, ysc, xslr, yslr]
|
257
|
-
end
|
258
|
-
|
259
|
-
def adjust_dimensions_by_percent(w, h, p, returnnew=nil)
|
260
|
-
p += 100
|
261
|
-
nw = (w * (p/100.0)).to_i
|
262
|
-
nh = (h * (p/100.0)).to_i
|
263
|
-
deltaw = nw - w
|
264
|
-
deltah = nh - h
|
265
|
-
if returnnew
|
266
|
-
[deltaw, deltah, nw, nh]
|
267
|
-
else
|
268
|
-
[deltaw, deltah]
|
269
|
-
end
|
270
|
-
end
|
271
|
-
|
272
|
-
def get_browser_coord(browser=nil, dbg=nil)
|
273
|
-
browser = @myBrowser if not browser
|
274
|
-
title = browser.title
|
275
|
-
x = @ai.WinGetPosX(title)
|
276
|
-
y = @ai.WinGetPosY(title)
|
277
|
-
w = @ai.WinGetPosWidth(title)
|
278
|
-
h = @ai.WinGetPosHeight(title)
|
279
|
-
if dbg
|
280
|
-
debug_to_log("\n\t\tBrowser #{browser.inspect}\n"+
|
281
|
-
"\t\tdimensions: x: #{w} y: #{h}"+
|
282
|
-
"\t\tscreen offset x: #{x} y: #{y}")
|
283
|
-
end
|
284
|
-
[x, y, w, h]
|
285
|
-
end
|
286
|
-
|
287
|
-
def drag_and_drop_div(browser, how, what, delta_x, delta_y, desc = '')
|
288
|
-
#TODO: webdriver
|
289
|
-
#TODO: assumes element is div
|
290
|
-
msg = "Drag and drop element :#{how}=>#{what} by x=>#{delta_x} y=>#{delta_y}."
|
291
|
-
msg << " #{desc}" if desc.length > 0
|
292
|
-
drag_me = browser.div(how, what)
|
293
|
-
drag_me.drag_and_drop_by(delta_x, delta_y)
|
294
|
-
passed_to_log(msg)
|
295
|
-
true
|
296
|
-
rescue
|
297
|
-
failed_to_log(unable_to)
|
298
|
-
end
|
299
|
-
|
300
|
-
def drag_and_drop(x1, y1, dx, dy, speed=nil)
|
301
|
-
speed = 10 if not speed
|
302
|
-
x2 = x1 + dx
|
303
|
-
y2 = y1 + dy
|
304
|
-
debug_to_log("drag_and_drop: start: [#{x1}, #{y1}] end: [#{x2}, #{y2}]")
|
305
|
-
|
306
|
-
@ai.MouseMove(x1, y1, speed)
|
307
|
-
@ai.MouseClick("primary", x1, y1)
|
308
|
-
sleep_for(0.5)
|
309
|
-
@ai.MouseClick("primary", x1, y1)
|
310
|
-
sleep_for(0.5)
|
311
|
-
@ai.MouseClickDrag("primary", x1, y1, x2, y2, speed)
|
312
|
-
end
|
313
|
-
|
314
|
-
def drag_and_drop_element(browser, element, dx, dy, speed = nil)
|
315
|
-
speed = 10 if not speed
|
316
|
-
w1, h1, x1, y1, xc1, yc1, xlr1, ylr1 = get_element_coordinates(browser, element, true)
|
317
|
-
msg = "Move #{element} by [#{dx}, #{dy}] from center[#{xc1}, #{yc1}] "
|
318
|
-
newxc = xc1 + dx
|
319
|
-
newyc = yc1 + dy
|
320
|
-
msg << "to center[[#{newxc}, #{newyc}]"
|
321
|
-
sleep_for(0.5)
|
322
|
-
|
323
|
-
drag_and_drop(xc1, yc1, dx, dy)
|
324
|
-
|
325
|
-
sleep_for(1)
|
326
|
-
w2, h2, x2, y2, xc2, yc2, xlr2, ylr2 = get_element_coordinates(browser, element, true)
|
327
|
-
|
328
|
-
# TODO This adjustment is adhoc and empirical. Needs to be derived more rigorously
|
329
|
-
xcerr = xc2 - xc1
|
330
|
-
ycerr = yc2 - yc1
|
331
|
-
|
332
|
-
debug_to_log("\n" +
|
333
|
-
"\t\t xc1: #{xc1}\n" +
|
334
|
-
"\t\t yc1: #{yc1}\n" +
|
335
|
-
"\t\t xc2: #{xc2}\n" +
|
336
|
-
"\t\t yc2: #{yc2}\n" +
|
337
|
-
"\t\t xcerr: #{xlrerr}\n" +
|
338
|
-
"\t\t ycerr: #{ylrerr}\n" +
|
339
|
-
"\t\t x tol: #{@x_tolerance}\n" +
|
340
|
-
"\t\t y tol: #{@y_tolerance}\n"
|
341
|
-
)
|
342
|
-
|
343
|
-
#TODO Add check that window _was_ resized.
|
344
|
-
x_ok, x_msg = validate_drag_drop(xcerr, @x_tolerance, newxc, xc2)
|
345
|
-
y_ok, y_msg = validate_drag_drop(ycerr, @y_tolerance, newyc, yc2)
|
346
|
-
msg = msg + "x: #{x_msg}, y: #{y_msg}"
|
347
|
-
|
348
|
-
if x_ok and y_ok
|
349
|
-
passed_to_log(msg)
|
350
|
-
else
|
351
|
-
failed_to_log(msg)
|
352
|
-
end
|
353
|
-
sleep_for(1)
|
354
|
-
rescue
|
355
|
-
failed_to_log("Unable to validate drag and drop. #{$!} (#{__LINE__})")
|
356
|
-
sleep_for(1)
|
357
|
-
end
|
358
|
-
|
359
|
-
def right_click(element)
|
360
|
-
x = element.left_edge_absolute + 2
|
361
|
-
y = element.top_edge_absolute + 2
|
362
|
-
@ai.MouseClick("secondary", x, y)
|
363
|
-
end
|
364
|
-
|
365
|
-
def left_click(element)
|
366
|
-
x = element.left_edge_absolute + 2
|
367
|
-
y = element.top_edge_absolute + 2
|
368
|
-
@ai.MouseClick("primary", x, y)
|
369
|
-
end
|
370
|
-
|
371
|
-
def screen_offset(element, browser=nil)
|
372
|
-
bx, by, bw, bh = get_browser_coord(browser)
|
373
|
-
ex = element.left_edge
|
374
|
-
ey = element.top_edge
|
375
|
-
[bx + ex, by + ey]
|
376
|
-
end
|
377
|
-
|
378
|
-
def screen_center(element, browser=nil)
|
379
|
-
bx, by, bw, bh = get_browser_coord(browser)
|
380
|
-
w, h = element.dimensions.to_a
|
381
|
-
cx = bx + w/2
|
382
|
-
cy = by + h/2
|
383
|
-
[cx, cy]
|
384
|
-
end
|
385
|
-
|
386
|
-
def screen_lower_right(element, browser=nil)
|
387
|
-
bx, by, bw, bh = get_browser_coord(browser)
|
388
|
-
w, h = element.dimensions.to_a
|
389
|
-
[bx + w, by + h]
|
390
|
-
end
|
391
|
-
|
392
|
-
def verify_resize(d, err, tol, min, act)
|
393
|
-
ary = [false, "failed, actual #{act} err #{err}"]
|
394
|
-
if err == 0
|
395
|
-
ary = [true, 'succeeded ']
|
396
|
-
#TODO need to find way to calculate this adjustment
|
397
|
-
elsif d <= min + 4
|
398
|
-
ary = [true, "reached minimum (#{min}) "]
|
399
|
-
elsif err.abs <= tol
|
400
|
-
ary = [true, "within tolerance (+-#{tol}px) "]
|
401
|
-
end
|
402
|
-
ary
|
403
|
-
end
|
404
|
-
|
405
|
-
alias validate_move verify_resize
|
406
|
-
alias validate_resize verify_resize
|
407
|
-
|
408
|
-
def validate_drag_drop(err, tol, exp, act)
|
409
|
-
ary = [false, "failed, expected: #{exp}, actual: #{act}, err: #{err}"]
|
410
|
-
if err == 0
|
411
|
-
ary = [true, 'succeeded ']
|
412
|
-
elsif err.abs <= tol
|
413
|
-
ary = [true, "within tolerance (+-#{tol}px) "]
|
414
|
-
end
|
415
|
-
ary
|
416
|
-
end
|
417
|
-
|
418
|
-
end
|
419
|
-
end
|
420
|
-
end
|
421
|
-
|
1
|
+
module Awetestlib
|
2
|
+
module Regression
|
3
|
+
# Methods for moving and resizing elements, manipulating the mouse, and checking for relative positioning of elements,
|
4
|
+
# including overlap, overlay, etc.
|
5
|
+
# @note Still experimental. Works with IE but not fully tested with Firefox or Chrome in Windows using Watir-webdriver.
|
6
|
+
# Not compatible with Mac
|
7
|
+
# Rdoc is work in progress
|
8
|
+
module DragAndDrop
|
9
|
+
|
10
|
+
# Verify that specified *inner_element* is fully enclosed by *outer_element*.
|
11
|
+
# @param [Watir::Element] inner_element A reference to a DOM element
|
12
|
+
# @param [Watir::Element] outer_element A reference to a DOM element
|
13
|
+
# @param [String] desc Contains a message or description intended to appear in the log and/or report output
|
14
|
+
def verify_element_inside(inner_element, outer_element, desc = '')
|
15
|
+
mark_testlevel("#{__method__.to_s.titleize}", 3)
|
16
|
+
msg = build_message("#{inner_element.class.to_s} (:id=#{inner_element.id}) is fully enclosed by "+
|
17
|
+
"#{outer_element.class.to_s} (:id=#{outer_element.id}).", desc)
|
18
|
+
if overlay?(inner_element, outer_element, :inside)
|
19
|
+
failed_to_log(msg)
|
20
|
+
else
|
21
|
+
passed_to_log(msg)
|
22
|
+
true
|
23
|
+
end
|
24
|
+
rescue
|
25
|
+
failed_to_log("Unable to verify that #{msg} '#{$!}'")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Verify that two elements, identified by specified attribute and value, do not overlap on a given *side*.
|
29
|
+
# @param [Symbol] above_element The element type for the first element, e.g. :div, :span, etc.
|
30
|
+
# @param [Symbol] above_how The element attribute used to identify the *above_element*.
|
31
|
+
# Valid values depend on the kind of element.
|
32
|
+
# Common values: :text, :id, :title, :name, :class, :href (:link only)
|
33
|
+
# @param [String, Regexp] above_what A string or a regular expression to be found in the *above_how* attribute that uniquely identifies the element.
|
34
|
+
# @param [Symbol] below_element The element type for the second element, e.g. :div, :span, etc.
|
35
|
+
# @param [Symbol] below_how The element attribute used to identify the *below_element*.
|
36
|
+
# @param [String, Regexp] below_what A string or a regular expression to be found in the *below_how* attribute that uniquely identifies the element.
|
37
|
+
# @param [Symbol] side :top, :bottom, :left, :right, :inside, or :outside
|
38
|
+
# @param [String] desc Contains a message or description intended to appear in the log and/or report output
|
39
|
+
def verify_no_element_overlap(browser, above_element, above_how, above_what, below_element, below_how, below_what, side, desc = '')
|
40
|
+
mark_testlevel("#{__method__.to_s.titleize}", 3)
|
41
|
+
msg = build_message("#{above_element.to_s.titleize} #{above_how}=>#{above_what} does not overlap "+
|
42
|
+
"#{below_element.to_s.titleize} #{below_how}=>#{below_what} at the #{side}.", desc)
|
43
|
+
above = browser.element(above_how, above_what)
|
44
|
+
below = browser.element(below_how, below_what)
|
45
|
+
if overlay?(above, below, side)
|
46
|
+
failed_to_log(msg)
|
47
|
+
else
|
48
|
+
passed_to_log(msg)
|
49
|
+
true
|
50
|
+
end
|
51
|
+
rescue
|
52
|
+
failed_to_log("Unable to verify that #{msg} '#{$!}'")
|
53
|
+
end
|
54
|
+
|
55
|
+
def overlay?(inner, outer, side = :bottom)
|
56
|
+
#mark_testlevel("#{__method__.to_s.titleize}", 3)
|
57
|
+
inner_t, inner_b, inner_l, inner_r = inner.bounding_rectangle_offsets
|
58
|
+
outer_t, outer_b, outer_l, outer_r = outer.bounding_rectangle_offsets
|
59
|
+
#overlay = false
|
60
|
+
case side
|
61
|
+
when :bottom
|
62
|
+
overlay = inner_b > outer_t
|
63
|
+
when :top
|
64
|
+
overlay = inner_t > outer_t
|
65
|
+
when :left
|
66
|
+
overlay = inner_l < outer_r
|
67
|
+
when :right
|
68
|
+
overlay = inner_r > outer_r
|
69
|
+
when :inside
|
70
|
+
overlay = !(inner_t > outer_t and
|
71
|
+
inner_r < outer_r and
|
72
|
+
inner_l > outer_l and
|
73
|
+
inner_b < outer_b)
|
74
|
+
else
|
75
|
+
overlay = (inner_t > outer_b or
|
76
|
+
inner_r > outer_l or
|
77
|
+
inner_l < outer_r or
|
78
|
+
inner_b < outer_t)
|
79
|
+
end
|
80
|
+
overlay
|
81
|
+
rescue
|
82
|
+
failed_to_log("Unable to determine overlay. '#{$!}'")
|
83
|
+
end
|
84
|
+
|
85
|
+
def hover(browser, element, wait = 2)
|
86
|
+
w1, h1, x1, y1, xc1, yc1, xlr1, ylr1 = get_element_coordinates(browser, element, true)
|
87
|
+
@ai.MoveMouse(xc1, yc1)
|
88
|
+
sleep_for(1)
|
89
|
+
end
|
90
|
+
|
91
|
+
def move_element_with_handle(browser, element, handle_id, dx, dy)
|
92
|
+
# msg = "Move element "
|
93
|
+
# w1, h1, x1, y1, xc1, yc1, xlr1, ylr1 = get_element_coordinates(browser, element, true)
|
94
|
+
# newx = w1 + dx
|
95
|
+
# newy = h1 + dy
|
96
|
+
# msg << " by [#{dx}, #{dy}] to expected [[#{newx}, #{newy}] "
|
97
|
+
# handle = get_resize_handle(element, handle_id)
|
98
|
+
# hw, hh, hx, hy, hxc, hyc, hxlr, hylr = get_element_coordinates(browser, handle, true)
|
99
|
+
|
100
|
+
# drag_and_drop(hxc, hyc, dx, dy)
|
101
|
+
|
102
|
+
# w2, h2, x2, y2, xc2, yc2, xlr2, ylr2 = get_element_coordinates(browser, element, true)
|
103
|
+
|
104
|
+
# xerr = x2 - newx
|
105
|
+
# yerr = y2 - newy
|
106
|
+
# xdsp = (x1 - x2).abs
|
107
|
+
# ydsp = (y1 - y2).abs
|
108
|
+
|
109
|
+
# if x2 == newx and y2 == newy
|
110
|
+
# msg << "succeeded."
|
111
|
+
# passed_to_log(msg)
|
112
|
+
# else
|
113
|
+
# msg << "failed. "
|
114
|
+
# failed_to_log(msg)
|
115
|
+
# debug_to_log("x: actual #{x2}, error #{xerr}, displace #{xdsp}. y: actual #{y2}, error #{yerr}, displace #{ydsp}.")
|
116
|
+
# end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
def resize_element_with_handle(browser, element, target, dx, dy=nil)
|
121
|
+
#TODO enhance to accept differing percentages in each direction
|
122
|
+
msg = "Resize element "
|
123
|
+
w1, h1, x1, y1, xc1, yc1, xlr1, ylr1 = get_element_coordinates(browser, element, true)
|
124
|
+
if dy
|
125
|
+
deltax = dx
|
126
|
+
deltay = dy
|
127
|
+
neww = w1 + dx
|
128
|
+
newh = h1 + dy
|
129
|
+
msg << " by [#{dx}, #{dy}] " #"" to expected dimension [#{neww}, #{newh}] "
|
130
|
+
else
|
131
|
+
deltax, deltay, neww, newh = adjust_dimensions_by_percent(w1, h1, dx, true)
|
132
|
+
msg << "by #{dx} percent " #"" to expected dimension [#{neww}, #{newh}] "
|
133
|
+
end
|
134
|
+
handle = get_resize_handle_by_class(element, target) #, true)
|
135
|
+
sleep_for(0.5)
|
136
|
+
hw, hh, hx, hy, hxc, hyc, hxlr, hylr = get_element_coordinates(browser, handle, true)
|
137
|
+
hxlr_diff = 0
|
138
|
+
hylr_diff = 0
|
139
|
+
|
140
|
+
# TODO These adjustments are adhoc and empirical. Need to be derived more rigorously
|
141
|
+
if @browserAbbrev == 'IE'
|
142
|
+
hxlr_diff = (xlr1 - hxlr)
|
143
|
+
hylr_diff = (ylr1 - hylr)
|
144
|
+
x_start = hxlr - 2
|
145
|
+
y_start = hylr - 2
|
146
|
+
else
|
147
|
+
hxlr_diff = (xlr1 - hxlr) / 2 unless (xlr1 - hxlr) == 0
|
148
|
+
hylr_diff = (ylr1 - hylr) / 2 unless (ylr1 - hylr) == 0
|
149
|
+
x_start = hxlr
|
150
|
+
y_start = hylr
|
151
|
+
end
|
152
|
+
|
153
|
+
newxlr = xlr1 + deltax
|
154
|
+
newylr = ylr1 + deltay
|
155
|
+
# msg << ", lower right [#{newxlr}, #{newylr}] - "
|
156
|
+
sleep_for(0.5)
|
157
|
+
|
158
|
+
drag_and_drop(x_start, y_start, deltax, deltay)
|
159
|
+
|
160
|
+
sleep_for(1.5)
|
161
|
+
w2, h2, x2, y2, xc2, yc2, xlr2, ylr2 = get_element_coordinates(browser, element, true)
|
162
|
+
|
163
|
+
werr = w2 - neww
|
164
|
+
herr = h2 - newh
|
165
|
+
|
166
|
+
# TODO This adjustment is adhoc and empirical. Needs to be derived more rigorously
|
167
|
+
xlrerr = xlr2 - newxlr + hxlr_diff
|
168
|
+
ylrerr = ylr2 - newylr + hylr_diff
|
169
|
+
|
170
|
+
xlrdsp = (xlr1 - xlr2).abs
|
171
|
+
ylrdsp = (ylr1 - ylr2).abs
|
172
|
+
|
173
|
+
debug_to_log("\n" +
|
174
|
+
"\t\t hxlr_diff: #{hxlr_diff}\n" +
|
175
|
+
"\t\t hylr_diff: #{hylr_diff}\n" +
|
176
|
+
"\t\t werr: #{werr}\n" +
|
177
|
+
"\t\t herr: #{herr}\n" +
|
178
|
+
"\t\t xlrerr: #{xlrerr}\n" +
|
179
|
+
"\t\t ylrerr: #{ylrerr}\n" +
|
180
|
+
"\t\t xlrdsp: #{xlrdsp}\n" +
|
181
|
+
"\t\t ylrdsp: #{ylrdsp}\n" +
|
182
|
+
"\t\t @min_width: #{@min_width}\n" +
|
183
|
+
"\t\t@min_height: #{@min_height}\n" +
|
184
|
+
"\t\t x tol: #{@x_tolerance}\n" +
|
185
|
+
"\t\t y tol: #{@y_tolerance}\n"
|
186
|
+
)
|
187
|
+
|
188
|
+
#TODO Add check that window _was_ resized.
|
189
|
+
x_ok, x_msg = validate_move(w2, xlrerr, @x_tolerance, @min_width, xlr2)
|
190
|
+
y_ok, y_msg = validate_move(h2, ylrerr, @y_tolerance, @min_height, ylr2)
|
191
|
+
msg = msg + "x: #{x_msg}, y: #{y_msg}"
|
192
|
+
|
193
|
+
if x_ok and y_ok
|
194
|
+
passed_to_log(msg)
|
195
|
+
else
|
196
|
+
failed_to_log(msg)
|
197
|
+
debug_to_log("x - actual #{xlr2}, error #{xlrerr}, displace #{xlrdsp}, y - actual #{ylr2}, error #{ylrerr}, displace #{ylrdsp}.")
|
198
|
+
end
|
199
|
+
sleep_for(1)
|
200
|
+
rescue
|
201
|
+
failed_to_log("Unable to validate resize. #{$!} (#{__LINE__})")
|
202
|
+
sleep_for(1)
|
203
|
+
end
|
204
|
+
|
205
|
+
# :category: GUI
|
206
|
+
def get_resize_handle_by_id(element, id, dbg=nil)
|
207
|
+
handle = get_div_by_id(element, id, dbg)
|
208
|
+
sleep_for(1)
|
209
|
+
handle.flash(5)
|
210
|
+
return handle
|
211
|
+
end
|
212
|
+
|
213
|
+
# :category: GUI
|
214
|
+
def get_resize_handle_by_class(element, strg, dbg=nil)
|
215
|
+
handle = get_div_by_class(element, strg, dbg)
|
216
|
+
sleep_for(0.5)
|
217
|
+
handle.flash(5)
|
218
|
+
return handle
|
219
|
+
end
|
220
|
+
|
221
|
+
# :category: GUI
|
222
|
+
def get_element_coordinates(browser, element, dbg=nil)
|
223
|
+
bx, by, bw, bh = get_browser_coord(browser, dbg)
|
224
|
+
if @browserAbbrev == 'IE'
|
225
|
+
x_hack = @horizontal_hack_ie
|
226
|
+
y_hack = @vertical_hack_ie
|
227
|
+
elsif @browserAbbrev == 'FF'
|
228
|
+
x_hack = @horizontal_hack_ff
|
229
|
+
y_hack = @vertical_hack_ff
|
230
|
+
end
|
231
|
+
sleep_for(1)
|
232
|
+
w, h = element.dimensions.to_a
|
233
|
+
xc, yc = element.client_offset.to_a
|
234
|
+
# xcc, ycc = element.client_center.to_a
|
235
|
+
xcc = xc + w/2
|
236
|
+
ycc = yc + h/2
|
237
|
+
# screen offset:
|
238
|
+
xs = bx + x_hack + xc - 1
|
239
|
+
ys = by + y_hack + yc - 1
|
240
|
+
# screen center:
|
241
|
+
xsc = xs + w/2
|
242
|
+
ysc = ys + h/2
|
243
|
+
xslr = xs + w
|
244
|
+
yslr = ys + h
|
245
|
+
if dbg
|
246
|
+
debug_to_log(
|
247
|
+
"\n\t\tElement: #{element.inspect}"+
|
248
|
+
"\n\t\tbrowser screen offset: x: #{bx} y: #{by}"+
|
249
|
+
"\n\t\t dimensions: x: #{w} y: #{h}"+
|
250
|
+
"\n\t\t client offset x: #{xc} y: #{yc}"+
|
251
|
+
"\n\t\t screen offset x: #{xs} y: #{ys}"+
|
252
|
+
"\n\t\t client center x: #{xcc} y: #{ycc}"+
|
253
|
+
"\n\t\t screen center x: #{xsc} y: #{ysc}"+
|
254
|
+
"\n\t\t screen lower right x: #{xslr} y: #{yslr}")
|
255
|
+
end
|
256
|
+
[w, h, xs, ys, xsc, ysc, xslr, yslr]
|
257
|
+
end
|
258
|
+
|
259
|
+
def adjust_dimensions_by_percent(w, h, p, returnnew=nil)
|
260
|
+
p += 100
|
261
|
+
nw = (w * (p/100.0)).to_i
|
262
|
+
nh = (h * (p/100.0)).to_i
|
263
|
+
deltaw = nw - w
|
264
|
+
deltah = nh - h
|
265
|
+
if returnnew
|
266
|
+
[deltaw, deltah, nw, nh]
|
267
|
+
else
|
268
|
+
[deltaw, deltah]
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def get_browser_coord(browser=nil, dbg=nil)
|
273
|
+
browser = @myBrowser if not browser
|
274
|
+
title = browser.title
|
275
|
+
x = @ai.WinGetPosX(title)
|
276
|
+
y = @ai.WinGetPosY(title)
|
277
|
+
w = @ai.WinGetPosWidth(title)
|
278
|
+
h = @ai.WinGetPosHeight(title)
|
279
|
+
if dbg
|
280
|
+
debug_to_log("\n\t\tBrowser #{browser.inspect}\n"+
|
281
|
+
"\t\tdimensions: x: #{w} y: #{h}"+
|
282
|
+
"\t\tscreen offset x: #{x} y: #{y}")
|
283
|
+
end
|
284
|
+
[x, y, w, h]
|
285
|
+
end
|
286
|
+
|
287
|
+
def drag_and_drop_div(browser, how, what, delta_x, delta_y, desc = '')
|
288
|
+
#TODO: webdriver
|
289
|
+
#TODO: assumes element is div
|
290
|
+
msg = "Drag and drop element :#{how}=>#{what} by x=>#{delta_x} y=>#{delta_y}."
|
291
|
+
msg << " #{desc}" if desc.length > 0
|
292
|
+
drag_me = browser.div(how, what)
|
293
|
+
drag_me.drag_and_drop_by(delta_x, delta_y)
|
294
|
+
passed_to_log(msg)
|
295
|
+
true
|
296
|
+
rescue
|
297
|
+
failed_to_log(unable_to)
|
298
|
+
end
|
299
|
+
|
300
|
+
def drag_and_drop(x1, y1, dx, dy, speed=nil)
|
301
|
+
speed = 10 if not speed
|
302
|
+
x2 = x1 + dx
|
303
|
+
y2 = y1 + dy
|
304
|
+
debug_to_log("drag_and_drop: start: [#{x1}, #{y1}] end: [#{x2}, #{y2}]")
|
305
|
+
|
306
|
+
@ai.MouseMove(x1, y1, speed)
|
307
|
+
@ai.MouseClick("primary", x1, y1)
|
308
|
+
sleep_for(0.5)
|
309
|
+
@ai.MouseClick("primary", x1, y1)
|
310
|
+
sleep_for(0.5)
|
311
|
+
@ai.MouseClickDrag("primary", x1, y1, x2, y2, speed)
|
312
|
+
end
|
313
|
+
|
314
|
+
def drag_and_drop_element(browser, element, dx, dy, speed = nil)
|
315
|
+
speed = 10 if not speed
|
316
|
+
w1, h1, x1, y1, xc1, yc1, xlr1, ylr1 = get_element_coordinates(browser, element, true)
|
317
|
+
msg = "Move #{element} by [#{dx}, #{dy}] from center[#{xc1}, #{yc1}] "
|
318
|
+
newxc = xc1 + dx
|
319
|
+
newyc = yc1 + dy
|
320
|
+
msg << "to center[[#{newxc}, #{newyc}]"
|
321
|
+
sleep_for(0.5)
|
322
|
+
|
323
|
+
drag_and_drop(xc1, yc1, dx, dy)
|
324
|
+
|
325
|
+
sleep_for(1)
|
326
|
+
w2, h2, x2, y2, xc2, yc2, xlr2, ylr2 = get_element_coordinates(browser, element, true)
|
327
|
+
|
328
|
+
# TODO This adjustment is adhoc and empirical. Needs to be derived more rigorously
|
329
|
+
xcerr = xc2 - xc1
|
330
|
+
ycerr = yc2 - yc1
|
331
|
+
|
332
|
+
debug_to_log("\n" +
|
333
|
+
"\t\t xc1: #{xc1}\n" +
|
334
|
+
"\t\t yc1: #{yc1}\n" +
|
335
|
+
"\t\t xc2: #{xc2}\n" +
|
336
|
+
"\t\t yc2: #{yc2}\n" +
|
337
|
+
"\t\t xcerr: #{xlrerr}\n" +
|
338
|
+
"\t\t ycerr: #{ylrerr}\n" +
|
339
|
+
"\t\t x tol: #{@x_tolerance}\n" +
|
340
|
+
"\t\t y tol: #{@y_tolerance}\n"
|
341
|
+
)
|
342
|
+
|
343
|
+
#TODO Add check that window _was_ resized.
|
344
|
+
x_ok, x_msg = validate_drag_drop(xcerr, @x_tolerance, newxc, xc2)
|
345
|
+
y_ok, y_msg = validate_drag_drop(ycerr, @y_tolerance, newyc, yc2)
|
346
|
+
msg = msg + "x: #{x_msg}, y: #{y_msg}"
|
347
|
+
|
348
|
+
if x_ok and y_ok
|
349
|
+
passed_to_log(msg)
|
350
|
+
else
|
351
|
+
failed_to_log(msg)
|
352
|
+
end
|
353
|
+
sleep_for(1)
|
354
|
+
rescue
|
355
|
+
failed_to_log("Unable to validate drag and drop. #{$!} (#{__LINE__})")
|
356
|
+
sleep_for(1)
|
357
|
+
end
|
358
|
+
|
359
|
+
def right_click(element)
|
360
|
+
x = element.left_edge_absolute + 2
|
361
|
+
y = element.top_edge_absolute + 2
|
362
|
+
@ai.MouseClick("secondary", x, y)
|
363
|
+
end
|
364
|
+
|
365
|
+
def left_click(element)
|
366
|
+
x = element.left_edge_absolute + 2
|
367
|
+
y = element.top_edge_absolute + 2
|
368
|
+
@ai.MouseClick("primary", x, y)
|
369
|
+
end
|
370
|
+
|
371
|
+
def screen_offset(element, browser=nil)
|
372
|
+
bx, by, bw, bh = get_browser_coord(browser)
|
373
|
+
ex = element.left_edge
|
374
|
+
ey = element.top_edge
|
375
|
+
[bx + ex, by + ey]
|
376
|
+
end
|
377
|
+
|
378
|
+
def screen_center(element, browser=nil)
|
379
|
+
bx, by, bw, bh = get_browser_coord(browser)
|
380
|
+
w, h = element.dimensions.to_a
|
381
|
+
cx = bx + w/2
|
382
|
+
cy = by + h/2
|
383
|
+
[cx, cy]
|
384
|
+
end
|
385
|
+
|
386
|
+
def screen_lower_right(element, browser=nil)
|
387
|
+
bx, by, bw, bh = get_browser_coord(browser)
|
388
|
+
w, h = element.dimensions.to_a
|
389
|
+
[bx + w, by + h]
|
390
|
+
end
|
391
|
+
|
392
|
+
def verify_resize(d, err, tol, min, act)
|
393
|
+
ary = [false, "failed, actual #{act} err #{err}"]
|
394
|
+
if err == 0
|
395
|
+
ary = [true, 'succeeded ']
|
396
|
+
#TODO need to find way to calculate this adjustment
|
397
|
+
elsif d <= min + 4
|
398
|
+
ary = [true, "reached minimum (#{min}) "]
|
399
|
+
elsif err.abs <= tol
|
400
|
+
ary = [true, "within tolerance (+-#{tol}px) "]
|
401
|
+
end
|
402
|
+
ary
|
403
|
+
end
|
404
|
+
|
405
|
+
alias validate_move verify_resize
|
406
|
+
alias validate_resize verify_resize
|
407
|
+
|
408
|
+
def validate_drag_drop(err, tol, exp, act)
|
409
|
+
ary = [false, "failed, expected: #{exp}, actual: #{act}, err: #{err}"]
|
410
|
+
if err == 0
|
411
|
+
ary = [true, 'succeeded ']
|
412
|
+
elsif err.abs <= tol
|
413
|
+
ary = [true, "within tolerance (+-#{tol}px) "]
|
414
|
+
end
|
415
|
+
ary
|
416
|
+
end
|
417
|
+
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|