webdrone 1.7.8 → 1.8.0
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 +5 -5
- data/.rubocop.relaxed.yml +165 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -1
- data/Rakefile +3 -3
- data/bin/console +3 -3
- data/lib/webdrone.rb +35 -27
- data/lib/webdrone/browser.rb +96 -82
- data/lib/webdrone/clic.rb +5 -3
- data/lib/webdrone/conf.rb +12 -9
- data/lib/webdrone/ctxt.rb +23 -21
- data/lib/webdrone/error.rb +57 -56
- data/lib/webdrone/exec.rb +5 -3
- data/lib/webdrone/find.rb +39 -36
- data/lib/webdrone/form.rb +41 -35
- data/lib/webdrone/html.rb +6 -4
- data/lib/webdrone/logg.rb +50 -40
- data/lib/webdrone/mark.rb +9 -6
- data/lib/webdrone/open.rb +7 -5
- data/lib/webdrone/shot.rb +7 -5
- data/lib/webdrone/text.rb +7 -5
- data/lib/webdrone/version.rb +3 -1
- data/lib/webdrone/vrfy.rb +19 -19
- data/lib/webdrone/wait.rb +8 -5
- data/lib/webdrone/xlsx.rb +53 -38
- data/lib/webdrone/xpath.rb +168 -0
- data/webdrone.gemspec +27 -28
- metadata +55 -24
data/lib/webdrone/shot.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Webdrone
|
2
4
|
class Browser
|
3
5
|
def shot
|
@@ -6,7 +8,7 @@ module Webdrone
|
|
6
8
|
end
|
7
9
|
|
8
10
|
class Shot
|
9
|
-
|
11
|
+
attr_reader :a0
|
10
12
|
|
11
13
|
def initialize(a0)
|
12
14
|
@a0 = a0
|
@@ -14,12 +16,12 @@ module Webdrone
|
|
14
16
|
|
15
17
|
def screen(name)
|
16
18
|
@counter = (@counter || 0) + 1
|
17
|
-
filename = sprintf "screenshot-%04d
|
19
|
+
filename = sprintf "screenshot-%04d-#{name}.png", @counter
|
18
20
|
filename = File.join(@a0.conf.outdir, filename)
|
19
|
-
|
21
|
+
::Webdrone::MethodLogger.screenshot = filename
|
20
22
|
@a0.driver.save_screenshot filename
|
21
|
-
rescue =>
|
22
|
-
Webdrone.report_error(@a0,
|
23
|
+
rescue StandardError => error
|
24
|
+
Webdrone.report_error(@a0, error)
|
23
25
|
end
|
24
26
|
end
|
25
27
|
end
|
data/lib/webdrone/text.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Webdrone
|
2
4
|
class Browser
|
3
5
|
def text
|
@@ -6,7 +8,7 @@ module Webdrone
|
|
6
8
|
end
|
7
9
|
|
8
10
|
class Text
|
9
|
-
|
11
|
+
attr_reader :a0
|
10
12
|
|
11
13
|
def initialize(a0)
|
12
14
|
@a0 = a0
|
@@ -19,8 +21,8 @@ module Webdrone
|
|
19
21
|
else
|
20
22
|
item.text
|
21
23
|
end
|
22
|
-
rescue =>
|
23
|
-
Webdrone.report_error(@a0,
|
24
|
+
rescue StandardError => error
|
25
|
+
Webdrone.report_error(@a0, error)
|
24
26
|
end
|
25
27
|
|
26
28
|
alias_method :id, :text
|
@@ -33,8 +35,8 @@ module Webdrone
|
|
33
35
|
|
34
36
|
def page_title
|
35
37
|
@a0.driver.title
|
36
|
-
rescue =>
|
37
|
-
Webdrone.report_error(@a0,
|
38
|
+
rescue StandardError => error
|
39
|
+
Webdrone.report_error(@a0, error)
|
38
40
|
end
|
39
41
|
|
40
42
|
protected :text
|
data/lib/webdrone/version.rb
CHANGED
data/lib/webdrone/vrfy.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Webdrone
|
2
4
|
class Browser
|
3
5
|
def vrfy
|
@@ -6,7 +8,7 @@ module Webdrone
|
|
6
8
|
end
|
7
9
|
|
8
10
|
class Vrfy
|
9
|
-
|
11
|
+
attr_reader :a0
|
10
12
|
|
11
13
|
def initialize(a0)
|
12
14
|
@a0 = a0
|
@@ -14,35 +16,33 @@ module Webdrone
|
|
14
16
|
|
15
17
|
def vrfy(text, n: 1, all: false, visible: true, scroll: false, parent: nil, attr: nil, eq: nil, contains: nil, mark: false)
|
16
18
|
item = @a0.find.send __callee__, text, n: n, all: all, visible: visible, scroll: scroll, parent: parent
|
19
|
+
@a0.mark.mark_item item if mark
|
17
20
|
if item.is_a? Array
|
18
|
-
@a0.mark.mark_item item if mark
|
19
21
|
item.each { |x| vrfy_item x, text: text, callee: __callee__, attr: attr, eq: eq, contains: contains }
|
20
22
|
else
|
21
|
-
@a0.mark.mark_item item if mark
|
22
23
|
vrfy_item item, text: text, callee: __callee__, attr: attr, eq: eq, contains: contains
|
23
24
|
end
|
24
|
-
rescue =>
|
25
|
-
Webdrone.report_error(@a0,
|
25
|
+
rescue StandardError => error
|
26
|
+
Webdrone.report_error(@a0, error)
|
26
27
|
end
|
27
28
|
|
28
29
|
def vrfy_item(item, text: nil, callee: nil, attr: nil, eq: nil, contains: nil)
|
29
|
-
if attr
|
30
|
-
r = item.attribute(attr) == eq if eq
|
31
|
-
r = item.attribute(attr).include? contains if contains
|
32
|
-
elsif eq
|
30
|
+
if !attr.nil?
|
31
|
+
r = item.attribute(attr) == eq if !eq.nil?
|
32
|
+
r = item.attribute(attr).include? contains if !contains.nil?
|
33
|
+
elsif !eq.nil?
|
33
34
|
r = item.text == eq
|
34
|
-
elsif contains
|
35
|
+
elsif !contains.nil?
|
35
36
|
r = item.text.include? contains
|
36
37
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
38
|
+
|
39
|
+
return unless r == false
|
40
|
+
|
41
|
+
targ = "eq: [#{eq}]" if eq
|
42
|
+
targ = "contains: [#{contains}]" if contains
|
43
|
+
|
44
|
+
raise "VRFY: #{callee} [#{text}] text value [#{item.text}] does not comply #{targ}" if attr.nil?
|
45
|
+
raise "VRFY: #{callee} [#{text}] attr [#{attr}] value [#{item.attribute(attr)}] does not comply #{targ}"
|
46
46
|
end
|
47
47
|
|
48
48
|
alias_method :id, :vrfy
|
data/lib/webdrone/wait.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Webdrone
|
2
4
|
class Browser
|
3
5
|
def wait
|
@@ -6,7 +8,8 @@ module Webdrone
|
|
6
8
|
end
|
7
9
|
|
8
10
|
class Wait
|
9
|
-
attr_accessor :
|
11
|
+
attr_accessor :ignore
|
12
|
+
attr_reader :a0
|
10
13
|
|
11
14
|
def initialize(a0)
|
12
15
|
@a0 = a0
|
@@ -25,14 +28,14 @@ module Webdrone
|
|
25
28
|
else
|
26
29
|
yield
|
27
30
|
end
|
28
|
-
rescue =>
|
29
|
-
Webdrone.report_error(@a0,
|
31
|
+
rescue StandardError => error
|
32
|
+
Webdrone.report_error(@a0, error)
|
30
33
|
end
|
31
34
|
|
32
35
|
def time(val)
|
33
36
|
sleep val
|
34
|
-
rescue =>
|
35
|
-
Webdrone.report_error(@a0,
|
37
|
+
rescue StandardError => error
|
38
|
+
Webdrone.report_error(@a0, error)
|
36
39
|
end
|
37
40
|
end
|
38
41
|
end
|
data/lib/webdrone/xlsx.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Webdrone
|
2
4
|
class Browser
|
3
5
|
def xlsx
|
@@ -6,7 +8,9 @@ module Webdrone
|
|
6
8
|
end
|
7
9
|
|
8
10
|
class Xlsx
|
9
|
-
attr_accessor :
|
11
|
+
attr_accessor :filename, :sheet
|
12
|
+
attr_reader :a0
|
13
|
+
attr_writer :dict, :rows, :both
|
10
14
|
|
11
15
|
def initialize(a0)
|
12
16
|
@a0 = a0
|
@@ -16,73 +20,80 @@ module Webdrone
|
|
16
20
|
|
17
21
|
def dict(sheet: nil, filename: nil)
|
18
22
|
update_sheet_filename(sheet, filename)
|
19
|
-
|
23
|
+
|
24
|
+
if @dict.nil?
|
20
25
|
reset
|
21
26
|
@dict = {}
|
22
27
|
workbook = RubyXL::Parser.parse(@filename)
|
23
28
|
worksheet = workbook[@sheet]
|
24
|
-
worksheet.sheet_data.rows.tap do |
|
29
|
+
worksheet.sheet_data.rows.tap do |_head, *body|
|
25
30
|
body.each do |row|
|
26
31
|
k, v = row[0].value, row[1].value
|
27
32
|
@dict[k] = v
|
28
33
|
end
|
29
34
|
end
|
30
35
|
end
|
36
|
+
|
31
37
|
@dict
|
32
|
-
rescue =>
|
33
|
-
Webdrone.report_error(@a0,
|
38
|
+
rescue StandardError => error
|
39
|
+
Webdrone.report_error(@a0, error)
|
34
40
|
end
|
35
41
|
|
36
42
|
def rows(sheet: nil, filename: nil)
|
37
43
|
update_sheet_filename(sheet, filename)
|
38
|
-
|
44
|
+
|
45
|
+
if @rows.nil?
|
39
46
|
reset
|
40
47
|
workbook = RubyXL::Parser.parse(@filename)
|
41
48
|
worksheet = workbook[@sheet]
|
42
49
|
@rows = worksheet.sheet_data.rows.collect do |row|
|
43
50
|
row.cells.collect do |cell|
|
44
|
-
cell
|
51
|
+
cell&.value
|
45
52
|
end
|
46
53
|
end
|
47
54
|
end
|
55
|
+
|
48
56
|
@rows
|
49
|
-
rescue =>
|
50
|
-
Webdrone.report_error(@a0,
|
57
|
+
rescue StandardError => error
|
58
|
+
Webdrone.report_error(@a0, error)
|
51
59
|
end
|
52
60
|
|
53
61
|
def both(sheet: nil, filename: nil)
|
54
62
|
update_sheet_filename(sheet, filename)
|
55
|
-
|
63
|
+
|
64
|
+
if @both.nil?
|
56
65
|
reset
|
57
66
|
workbook = RubyXL::Parser.parse(@filename)
|
58
67
|
worksheet = workbook[@sheet]
|
59
68
|
rows = worksheet.sheet_data.rows.collect do |row|
|
60
69
|
row.cells.collect do |cell|
|
61
|
-
cell
|
70
|
+
cell&.value
|
62
71
|
end
|
63
72
|
end
|
64
73
|
head = rows.shift
|
65
74
|
@both = rows.collect do |row|
|
66
75
|
dict = {}
|
67
76
|
row.each_with_index do |val, i|
|
68
|
-
dict[head[i]] = val if head[i]
|
77
|
+
dict[head[i]] = val if !head[i].nil?
|
69
78
|
end
|
70
79
|
dict
|
71
80
|
end
|
72
81
|
end
|
82
|
+
|
73
83
|
@both
|
74
|
-
rescue =>
|
75
|
-
Webdrone.report_error(@a0,
|
84
|
+
rescue StandardError => error
|
85
|
+
Webdrone.report_error(@a0, error)
|
76
86
|
end
|
77
87
|
|
78
|
-
|
79
88
|
def save(sheet: nil, filename: nil, dict: nil, rows: nil)
|
80
|
-
@filename = filename if filename
|
81
89
|
@sheet = sheet if sheet
|
90
|
+
@filename = filename if filename
|
91
|
+
@dict = dict if dict
|
92
|
+
@rows = rows if rows
|
82
93
|
workbook = RubyXL::Parser.parse(@filename)
|
83
94
|
worksheet = workbook[@sheet]
|
84
|
-
if
|
85
|
-
worksheet.sheet_data.rows.tap do |
|
95
|
+
if !@dict.nil?
|
96
|
+
worksheet.sheet_data.rows.tap do |_head, *body|
|
86
97
|
body.each do |row|
|
87
98
|
k = row[0].value
|
88
99
|
if @dict.include?(k)
|
@@ -90,20 +101,20 @@ module Webdrone
|
|
90
101
|
end
|
91
102
|
end
|
92
103
|
end
|
93
|
-
elsif
|
104
|
+
elsif !@rows.nil?
|
94
105
|
@rows.each_with_index do |row, rowi|
|
95
106
|
row.each_with_index do |data, coli|
|
96
|
-
if worksheet[rowi]
|
107
|
+
if worksheet[rowi].nil? || worksheet[rowi][coli].nil?
|
97
108
|
worksheet.add_cell(rowi, coli, data)
|
98
109
|
else
|
99
110
|
worksheet[rowi][coli].change_contents(data)
|
100
111
|
end
|
101
112
|
end
|
102
113
|
end
|
103
|
-
elsif
|
114
|
+
elsif !@both.nil?
|
104
115
|
rows = worksheet.sheet_data.rows.collect do |row|
|
105
116
|
row.cells.collect do |cell|
|
106
|
-
cell
|
117
|
+
cell&.value
|
107
118
|
end
|
108
119
|
end
|
109
120
|
head = rows.shift
|
@@ -111,7 +122,7 @@ module Webdrone
|
|
111
122
|
entry.each do |k, v|
|
112
123
|
coli = head.index(k)
|
113
124
|
if coli
|
114
|
-
if worksheet[rowi + 1]
|
125
|
+
if worksheet[rowi + 1].nil? || worksheet[rowi + 1][coli].nil?
|
115
126
|
worksheet.add_cell(rowi + 1, coli, v)
|
116
127
|
else
|
117
128
|
worksheet[rowi + 1][coli].change_contents(v)
|
@@ -120,28 +131,32 @@ module Webdrone
|
|
120
131
|
end
|
121
132
|
end
|
122
133
|
end
|
123
|
-
|
134
|
+
workbook.write(@filename)
|
124
135
|
reset
|
125
|
-
rescue =>
|
126
|
-
Webdrone.report_error(@a0,
|
136
|
+
rescue StandardError => error
|
137
|
+
Webdrone.report_error(@a0, error)
|
127
138
|
end
|
128
139
|
|
129
|
-
def reset
|
140
|
+
def reset
|
130
141
|
@dict = @rows = @both = nil
|
131
|
-
rescue =>
|
132
|
-
Webdrone.report_error(@a0,
|
142
|
+
rescue StandardError => error
|
143
|
+
Webdrone.report_error(@a0, error)
|
133
144
|
end
|
134
145
|
|
135
146
|
protected
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
if filename and filename != @filename
|
142
|
-
@filename = filename
|
143
|
-
reset
|
144
|
-
end
|
147
|
+
|
148
|
+
def update_sheet_filename(sheet, filename)
|
149
|
+
if sheet && sheet != @sheet
|
150
|
+
@sheet = sheet
|
151
|
+
reset
|
145
152
|
end
|
153
|
+
|
154
|
+
if filename && filename != @filename
|
155
|
+
@filename = filename
|
156
|
+
reset
|
157
|
+
end
|
158
|
+
|
159
|
+
nil
|
160
|
+
end
|
146
161
|
end
|
147
162
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Webdrone
|
4
|
+
# Code from teamcapybara/xpath
|
5
|
+
# Copyright (c) 2016 Jonas Nicklas - MIT LICENSE
|
6
|
+
module XPath
|
7
|
+
include ::XPath::DSL
|
8
|
+
extend self
|
9
|
+
|
10
|
+
# Match an `a` link element.
|
11
|
+
#
|
12
|
+
# @param [String] locator
|
13
|
+
# Text, id, title, or image alt attribute of the link
|
14
|
+
#
|
15
|
+
def link(locator)
|
16
|
+
locator = locator.to_s
|
17
|
+
link = descendant(:a)[attr(:href)]
|
18
|
+
link[attr(:id).equals(locator) | string.n.is(locator) | attr(:title).is(locator) | descendant(:img)[attr(:alt).is(locator)]]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Match a `submit`, `image`, or `button` element.
|
22
|
+
#
|
23
|
+
# @param [String] locator
|
24
|
+
# Value, title, id, or image alt attribute of the button
|
25
|
+
#
|
26
|
+
def button(locator)
|
27
|
+
locator = locator.to_s
|
28
|
+
button = descendant(:input)[attr(:type).one_of('submit', 'reset', 'image', 'button')][attr(:id).equals(locator) | attr(:value).is(locator) | attr(:title).is(locator)]
|
29
|
+
button += descendant(:button)[attr(:id).equals(locator) | attr(:value).is(locator) | string.n.is(locator) | attr(:title).is(locator)]
|
30
|
+
button += descendant(:input)[attr(:type).equals('image')][attr(:alt).is(locator)]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Match anything returned by either {#link} or {#button}.
|
34
|
+
#
|
35
|
+
# @param [String] locator
|
36
|
+
# Text, id, title, or image alt attribute of the link or button
|
37
|
+
#
|
38
|
+
def link_or_button(locator)
|
39
|
+
link(locator) + button(locator)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Match any `fieldset` element.
|
43
|
+
#
|
44
|
+
# @param [String] locator
|
45
|
+
# Legend or id of the fieldset
|
46
|
+
#
|
47
|
+
def fieldset(locator)
|
48
|
+
locator = locator.to_s
|
49
|
+
descendant(:fieldset)[attr(:id).equals(locator) | child(:legend)[string.n.is(locator)]]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Match any `input`, `textarea`, or `select` element that doesn't have a
|
53
|
+
# type of `submit`, `image`, or `hidden`.
|
54
|
+
#
|
55
|
+
# @param [String] locator
|
56
|
+
# Label, id, or name of field to match
|
57
|
+
#
|
58
|
+
def field(locator)
|
59
|
+
locator = locator.to_s
|
60
|
+
xpath = descendant(:input, :textarea, :select)[~attr(:type).one_of('submit', 'image', 'hidden')]
|
61
|
+
xpath = locate_field(xpath, locator)
|
62
|
+
xpath
|
63
|
+
end
|
64
|
+
|
65
|
+
# Match any `input` or `textarea` element that can be filled with text.
|
66
|
+
# This excludes any inputs with a type of `submit`, `image`, `radio`,
|
67
|
+
# `checkbox`, `hidden`, or `file`.
|
68
|
+
#
|
69
|
+
# @param [String] locator
|
70
|
+
# Label, id, or name of field to match
|
71
|
+
#
|
72
|
+
def fillable_field(locator)
|
73
|
+
locator = locator.to_s
|
74
|
+
xpath = descendant(:input, :textarea)[~attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
|
75
|
+
xpath = locate_field(xpath, locator)
|
76
|
+
xpath
|
77
|
+
end
|
78
|
+
|
79
|
+
# Match any `select` element.
|
80
|
+
#
|
81
|
+
# @param [String] locator
|
82
|
+
# Label, id, or name of the field to match
|
83
|
+
#
|
84
|
+
def select(locator)
|
85
|
+
locator = locator.to_s
|
86
|
+
locate_field(descendant(:select), locator)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Match any `input` element of type `checkbox`.
|
90
|
+
#
|
91
|
+
# @param [String] locator
|
92
|
+
# Label, id, or name of the checkbox to match
|
93
|
+
#
|
94
|
+
def checkbox(locator)
|
95
|
+
locator = locator.to_s
|
96
|
+
locate_field(descendant(:input)[attr(:type).equals('checkbox')], locator)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Match any `input` element of type `radio`.
|
100
|
+
#
|
101
|
+
# @param [String] locator
|
102
|
+
# Label, id, or name of the radio button to match
|
103
|
+
#
|
104
|
+
def radio_button(locator)
|
105
|
+
locator = locator.to_s
|
106
|
+
locate_field(descendant(:input)[attr(:type).equals('radio')], locator)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Match any `input` element of type `file`.
|
110
|
+
#
|
111
|
+
# @param [String] locator
|
112
|
+
# Label, id, or name of the file field to match
|
113
|
+
#
|
114
|
+
def file_field(locator)
|
115
|
+
locator = locator.to_s
|
116
|
+
locate_field(descendant(:input)[attr(:type).equals('file')], locator)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Match an `optgroup` element.
|
120
|
+
#
|
121
|
+
# @param [String] name
|
122
|
+
# Label for the option group
|
123
|
+
#
|
124
|
+
def optgroup(locator)
|
125
|
+
locator = locator.to_s
|
126
|
+
descendant(:optgroup)[attr(:label).is(locator)]
|
127
|
+
end
|
128
|
+
|
129
|
+
# Match an `option` element.
|
130
|
+
#
|
131
|
+
# @param [String] name
|
132
|
+
# Visible text of the option
|
133
|
+
#
|
134
|
+
def option(locator)
|
135
|
+
locator = locator.to_s
|
136
|
+
descendant(:option)[string.n.is(locator)]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Match any `table` element.
|
140
|
+
#
|
141
|
+
# @param [String] locator
|
142
|
+
# Caption or id of the table to match
|
143
|
+
# @option options [Array] :rows
|
144
|
+
# Content of each cell in each row to match
|
145
|
+
#
|
146
|
+
def table(locator)
|
147
|
+
locator = locator.to_s
|
148
|
+
descendant(:table)[attr(:id).equals(locator) | descendant(:caption).is(locator)]
|
149
|
+
end
|
150
|
+
|
151
|
+
# Match any 'dd' element.
|
152
|
+
#
|
153
|
+
# @param [String] locator
|
154
|
+
# Id of the 'dd' element or text from preciding 'dt' element content
|
155
|
+
def definition_description(locator)
|
156
|
+
locator = locator.to_s
|
157
|
+
descendant(:dd)[attr(:id).equals(locator) | previous_sibling(:dt)[string.n.equals(locator)] ]
|
158
|
+
end
|
159
|
+
|
160
|
+
protected
|
161
|
+
|
162
|
+
def locate_field(xpath, locator)
|
163
|
+
locate_field = xpath[attr(:id).equals(locator) | attr(:name).equals(locator) | attr(:placeholder).equals(locator) | attr(:id).equals(anywhere(:label)[string.n.is(locator)].attr(:for))]
|
164
|
+
locate_field += descendant(:label)[string.n.is(locator)].descendant(xpath)
|
165
|
+
locate_field
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|