rwebspec 1.4.0 → 1.4.0.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.
- data/CHANGELOG +300 -300
- data/MIT-LICENSE +21 -21
- data/README +32 -32
- data/Rakefile +94 -94
- data/lib/rspec_extensions.rb +50 -50
- data/lib/rwebspec/assert.rb +361 -361
- data/lib/rwebspec/clickJSDialog.rb +15 -15
- data/lib/rwebspec/context.rb +24 -24
- data/lib/rwebspec/driver.rb +734 -734
- data/lib/rwebspec/itest_plugin.rb +68 -68
- data/lib/rwebspec/matchers/contains_text.rb +37 -37
- data/lib/rwebspec/popup.rb +147 -147
- data/lib/rwebspec/rspec_helper.rb +96 -96
- data/lib/rwebspec/test_script.rb +8 -8
- data/lib/rwebspec/test_utils.rb +171 -171
- data/lib/rwebspec/using_pages.rb +49 -49
- data/lib/rwebspec/web_browser.rb +528 -528
- data/lib/rwebspec/web_page.rb +94 -94
- data/lib/rwebspec/web_testcase.rb +36 -36
- data/lib/rwebspec.rb +31 -31
- data/lib/rwebunit.rb +3 -3
- data/lib/watir_extensions.rb +69 -69
- metadata +6 -6
@@ -1,68 +1,68 @@
|
|
1
|
-
require 'socket'
|
2
|
-
|
3
|
-
module RWebSpec
|
4
|
-
module ITestPlugin
|
5
|
-
|
6
|
-
def connect_to_itest(message_type, body)
|
7
|
-
begin
|
8
|
-
the_message = message_type + "|" + body
|
9
|
-
if @last_message == the_message then # ignore the message same as preivous one
|
10
|
-
return
|
11
|
-
end
|
12
|
-
itest_port = $ITEST2_TRACE_PORT || 7025
|
13
|
-
itest_socket = Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)
|
14
|
-
itest_socket.connect(Socket.pack_sockaddr_in(itest_port, '127.0.0.1'))
|
15
|
-
itest_socket.puts(the_message)
|
16
|
-
@last_message = the_message
|
17
|
-
itest_socket.close
|
18
|
-
rescue => e
|
19
|
-
end
|
20
|
-
end
|
21
|
-
alias connect_to_itest2 connect_to_itest
|
22
|
-
|
23
|
-
def debug(message)
|
24
|
-
connect_to_itest(" DEBUG", message + "\r\n") if $RUN_IN_ITEST
|
25
|
-
end
|
26
|
-
|
27
|
-
|
28
|
-
# Support of iTest to ajust the intervals between keystroke/mouse operations
|
29
|
-
def operation_delay
|
30
|
-
begin
|
31
|
-
if $ITEST2_OPERATION_DELAY && $ITEST2_OPERATION_DELAY > 0 &&
|
32
|
-
$ITEST2_OPERATION_DELAY && $ITEST2_OPERATION_DELAY < 30000 then # max 30 seconds
|
33
|
-
sleep($ITEST2_OPERATION_DELAY / 1000)
|
34
|
-
end
|
35
|
-
|
36
|
-
while $ITEST2_PAUSE
|
37
|
-
debug("Paused, waiting ...")
|
38
|
-
sleep 1
|
39
|
-
end
|
40
|
-
rescue => e
|
41
|
-
puts "Error on delaying: #{e}"
|
42
|
-
# ignore
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# find out the line (and file) the execution is on, and notify iTest via Socket
|
47
|
-
def dump_caller_stack
|
48
|
-
return unless $ITEST2_TRACE_EXECUTION
|
49
|
-
begin
|
50
|
-
caller.each_with_index do |position, idx|
|
51
|
-
next unless position =~ /\A(.*?):(\d+)/
|
52
|
-
file = $1
|
53
|
-
# TODO: send multiple trace to be parse with pages.rb
|
54
|
-
# next if file =~ /example\/example_methods\.rb$/ or file =~ /example\/example_group_methods\.rb$/ or file =~ /driver\.rb$/ or file =~ /timeout\.rb$/ # don't include rspec or ruby trace
|
55
|
-
|
56
|
-
if file.include?("_spec.rb") || file.include?("_test.rb") || file.include?("_cmd.rb")
|
57
|
-
connect_to_itest(" TRACE", position)
|
58
|
-
end
|
59
|
-
|
60
|
-
break if idx > 4 or file =~ /"_spec\.rb$/
|
61
|
-
end
|
62
|
-
rescue => e
|
63
|
-
puts "failed to capture log: #{e}"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
end
|
68
|
-
end
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module RWebSpec
|
4
|
+
module ITestPlugin
|
5
|
+
|
6
|
+
def connect_to_itest(message_type, body)
|
7
|
+
begin
|
8
|
+
the_message = message_type + "|" + body
|
9
|
+
if @last_message == the_message then # ignore the message same as preivous one
|
10
|
+
return
|
11
|
+
end
|
12
|
+
itest_port = $ITEST2_TRACE_PORT || 7025
|
13
|
+
itest_socket = Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)
|
14
|
+
itest_socket.connect(Socket.pack_sockaddr_in(itest_port, '127.0.0.1'))
|
15
|
+
itest_socket.puts(the_message)
|
16
|
+
@last_message = the_message
|
17
|
+
itest_socket.close
|
18
|
+
rescue => e
|
19
|
+
end
|
20
|
+
end
|
21
|
+
alias connect_to_itest2 connect_to_itest
|
22
|
+
|
23
|
+
def debug(message)
|
24
|
+
connect_to_itest(" DEBUG", message + "\r\n") if $RUN_IN_ITEST
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Support of iTest to ajust the intervals between keystroke/mouse operations
|
29
|
+
def operation_delay
|
30
|
+
begin
|
31
|
+
if $ITEST2_OPERATION_DELAY && $ITEST2_OPERATION_DELAY > 0 &&
|
32
|
+
$ITEST2_OPERATION_DELAY && $ITEST2_OPERATION_DELAY < 30000 then # max 30 seconds
|
33
|
+
sleep($ITEST2_OPERATION_DELAY / 1000)
|
34
|
+
end
|
35
|
+
|
36
|
+
while $ITEST2_PAUSE
|
37
|
+
debug("Paused, waiting ...")
|
38
|
+
sleep 1
|
39
|
+
end
|
40
|
+
rescue => e
|
41
|
+
puts "Error on delaying: #{e}"
|
42
|
+
# ignore
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# find out the line (and file) the execution is on, and notify iTest via Socket
|
47
|
+
def dump_caller_stack
|
48
|
+
return unless $ITEST2_TRACE_EXECUTION
|
49
|
+
begin
|
50
|
+
caller.each_with_index do |position, idx|
|
51
|
+
next unless position =~ /\A(.*?):(\d+)/
|
52
|
+
file = $1
|
53
|
+
# TODO: send multiple trace to be parse with pages.rb
|
54
|
+
# next if file =~ /example\/example_methods\.rb$/ or file =~ /example\/example_group_methods\.rb$/ or file =~ /driver\.rb$/ or file =~ /timeout\.rb$/ # don't include rspec or ruby trace
|
55
|
+
|
56
|
+
if file.include?("_spec.rb") || file.include?("_test.rb") || file.include?("_cmd.rb")
|
57
|
+
connect_to_itest(" TRACE", position)
|
58
|
+
end
|
59
|
+
|
60
|
+
break if idx > 4 or file =~ /"_spec\.rb$/
|
61
|
+
end
|
62
|
+
rescue => e
|
63
|
+
puts "failed to capture log: #{e}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -1,37 +1,37 @@
|
|
1
|
-
class ContainsText
|
2
|
-
|
3
|
-
# this is what the matcher is called on.
|
4
|
-
# In this case:
|
5
|
-
# foo.should contains(:bars)
|
6
|
-
# foo would be passed to the +initialize+
|
7
|
-
def initialize(expected)
|
8
|
-
@expected = expected
|
9
|
-
end
|
10
|
-
|
11
|
-
def matches?(actual)
|
12
|
-
@actual = actual
|
13
|
-
return actual && actual.include?(@expected)
|
14
|
-
end
|
15
|
-
|
16
|
-
def actual_text
|
17
|
-
@actual.to_s.length > 1000000 ? @actual[0, 1000] : @actual
|
18
|
-
end
|
19
|
-
|
20
|
-
# error message for should
|
21
|
-
def failure_message
|
22
|
-
"expected #{actual_text} not to contains #{@expected}, but it did't"
|
23
|
-
end
|
24
|
-
|
25
|
-
# error message for should_not
|
26
|
-
def negative_failure_message
|
27
|
-
"expected #{actual_text} not to contains #{@expected}, but it did"
|
28
|
-
end
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
|
33
|
-
# This method is the one you use with should/should_not
|
34
|
-
def contains_text?(expected)
|
35
|
-
ContainsText.new(expected)
|
36
|
-
end
|
37
|
-
alias contains? contains_text?
|
1
|
+
class ContainsText
|
2
|
+
|
3
|
+
# this is what the matcher is called on.
|
4
|
+
# In this case:
|
5
|
+
# foo.should contains(:bars)
|
6
|
+
# foo would be passed to the +initialize+
|
7
|
+
def initialize(expected)
|
8
|
+
@expected = expected
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches?(actual)
|
12
|
+
@actual = actual
|
13
|
+
return actual && actual.include?(@expected)
|
14
|
+
end
|
15
|
+
|
16
|
+
def actual_text
|
17
|
+
@actual.to_s.length > 1000000 ? @actual[0, 1000] : @actual
|
18
|
+
end
|
19
|
+
|
20
|
+
# error message for should
|
21
|
+
def failure_message
|
22
|
+
"expected #{actual_text} not to contains #{@expected}, but it did't"
|
23
|
+
end
|
24
|
+
|
25
|
+
# error message for should_not
|
26
|
+
def negative_failure_message
|
27
|
+
"expected #{actual_text} not to contains #{@expected}, but it did"
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# This method is the one you use with should/should_not
|
34
|
+
def contains_text?(expected)
|
35
|
+
ContainsText.new(expected)
|
36
|
+
end
|
37
|
+
alias contains? contains_text?
|
data/lib/rwebspec/popup.rb
CHANGED
@@ -1,147 +1,147 @@
|
|
1
|
-
module RWebSpec
|
2
|
-
module Popup
|
3
|
-
|
4
|
-
#= Popup
|
5
|
-
#
|
6
|
-
|
7
|
-
# Start background thread to click popup windows
|
8
|
-
# Warning:
|
9
|
-
# Make browser window active
|
10
|
-
# Don't mouse your mouse to focus other window during test execution
|
11
|
-
def check_for_popups
|
12
|
-
autoit = WIN32OLE.new('AutoItX3.Control')
|
13
|
-
#
|
14
|
-
# Do forever - assumes popups could occur anywhere/anytime in your
|
15
|
-
# application.
|
16
|
-
loop do
|
17
|
-
# Look for window with given title. Give up after 1 second.
|
18
|
-
ret = autoit.WinWait('Windows Internet Explorer', '', 1)
|
19
|
-
#
|
20
|
-
# If window found, send appropriate keystroke (e.g. {enter}, {Y}, {N}).
|
21
|
-
if (ret==1) then
|
22
|
-
autoit.Send('{enter}')
|
23
|
-
end
|
24
|
-
#
|
25
|
-
# Take a rest to avoid chewing up cycles and give another thread a go.
|
26
|
-
# Then resume the loop.
|
27
|
-
sleep(3)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
##
|
32
|
-
# Check for "Security Information" and "Security Alert" alert popup, click 'Yes'
|
33
|
-
#
|
34
|
-
# Usage: For individual test suite
|
35
|
-
#
|
36
|
-
# before(:all) do
|
37
|
-
# $popup = Thread.new { check_for_alerts }
|
38
|
-
# open_in_browser
|
39
|
-
# ...
|
40
|
-
# end
|
41
|
-
#
|
42
|
-
# after(:all) do
|
43
|
-
# close_browser
|
44
|
-
# Thread.kill($popup)
|
45
|
-
# end
|
46
|
-
#
|
47
|
-
# or for all tests,
|
48
|
-
# $popup = Thread.new { check_for_alerts }
|
49
|
-
# at_exit{ Thread.kill($popup) }
|
50
|
-
def check_for_security_alerts
|
51
|
-
autoit = WIN32OLE.new('AutoItX3.Control')
|
52
|
-
loop do
|
53
|
-
["Security Alert", "Security Information"].each do |win_title|
|
54
|
-
ret = autoit.WinWait(win_title, '', 1)
|
55
|
-
if (ret==1) then
|
56
|
-
autoit.Send('{Y}')
|
57
|
-
end
|
58
|
-
end
|
59
|
-
sleep(3)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def verify_alert(title = "Microsoft Internet Explorer", button = "OK")
|
64
|
-
if is_windows? && !is_firefox?
|
65
|
-
WIN32OLE.new('AutoItX3.Control').ControlClick(title, '', button)
|
66
|
-
else
|
67
|
-
raise "This function only supports IE"
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def click_button_in_security_information_popup(button = "&Yes")
|
72
|
-
verify_alert("Security Information", "", button)
|
73
|
-
end
|
74
|
-
alias click_security_information_popup click_button_in_security_information_popup
|
75
|
-
|
76
|
-
def click_button_in_security_alert_popup(button = "&Yes")
|
77
|
-
verify_alert("Security Alert", "", button)
|
78
|
-
end
|
79
|
-
alias click_security_alert_popup click_button_in_security_alert_popup
|
80
|
-
|
81
|
-
def click_button_in_javascript_popup(button = "OK")
|
82
|
-
verify_alert()
|
83
|
-
end
|
84
|
-
alias click_javascript_popup click_button_in_javascript_popup
|
85
|
-
|
86
|
-
##
|
87
|
-
# This only works for IEs
|
88
|
-
# Cons:
|
89
|
-
# - Slow
|
90
|
-
# - only works in IE
|
91
|
-
# - does not work for security alert ?
|
92
|
-
def ie_popup_clicker(button_name = "OK", max_wait = 15)
|
93
|
-
require 'watir/contrib/enabled_popup'
|
94
|
-
require 'win32ole'
|
95
|
-
hwnd = ie.enabled_popup(15)
|
96
|
-
if (hwnd) #yeah! a popup
|
97
|
-
popup = WinClicker.new
|
98
|
-
popup.makeWindowActive(hwnd) #Activate the window.
|
99
|
-
popup.clickWindowsButton_hwnd(hwnd, button_name) #Click the button
|
100
|
-
#popup.clickWindowsButton(/Internet/,button_name,30)
|
101
|
-
popup = nil
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def click_popup_window(button, wait_time= 9, user_input=nil )
|
106
|
-
@web_browser.start_clicker(button, wait_time, user_input)
|
107
|
-
sleep 0.5
|
108
|
-
end
|
109
|
-
# run a separate process waiting for the popup window to click
|
110
|
-
#
|
111
|
-
#
|
112
|
-
def prepare_to_click_button_in_popup(button = "OK", wait_time = 3)
|
113
|
-
# !@web_browser.is_firefox?
|
114
|
-
# TODO: firefox is OK
|
115
|
-
if RUBY_PLATFORM =~ /mswin/ then
|
116
|
-
start_checking_js_dialog(button, wait_time)
|
117
|
-
else
|
118
|
-
raise "this only support on Windows and on IE"
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# Start a background process to click the button on a javascript popup window
|
123
|
-
def start_checking_js_dialog(button = "OK", wait_time = 3)
|
124
|
-
w = WinClicker.new
|
125
|
-
longName = File.expand_path(File.dirname(__FILE__)).gsub("/", "\\" )
|
126
|
-
shortName = w.getShortFileName(longName)
|
127
|
-
c = "start ruby #{shortName}\\clickJSDialog.rb #{button} #{wait_time} "
|
128
|
-
w.winsystem(c)
|
129
|
-
w = nil
|
130
|
-
end
|
131
|
-
|
132
|
-
# Click the button in javascript popup dialog
|
133
|
-
# Usage:
|
134
|
-
# click_button_in_popup_after { click_link('Cancel')}
|
135
|
-
# click_button_in_popup_after("OK") { click_link('Cancel')}
|
136
|
-
#
|
137
|
-
def click_button_in_popup_after(options = {:button => "OK", :wait_time => 3}, &block)
|
138
|
-
if is_windows? then
|
139
|
-
start_checking_js_dialog(options[:button], options[:wait_time])
|
140
|
-
yield
|
141
|
-
else
|
142
|
-
raise "this only support on Windows and on IE"
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
end
|
147
|
-
end
|
1
|
+
module RWebSpec
|
2
|
+
module Popup
|
3
|
+
|
4
|
+
#= Popup
|
5
|
+
#
|
6
|
+
|
7
|
+
# Start background thread to click popup windows
|
8
|
+
# Warning:
|
9
|
+
# Make browser window active
|
10
|
+
# Don't mouse your mouse to focus other window during test execution
|
11
|
+
def check_for_popups
|
12
|
+
autoit = WIN32OLE.new('AutoItX3.Control')
|
13
|
+
#
|
14
|
+
# Do forever - assumes popups could occur anywhere/anytime in your
|
15
|
+
# application.
|
16
|
+
loop do
|
17
|
+
# Look for window with given title. Give up after 1 second.
|
18
|
+
ret = autoit.WinWait('Windows Internet Explorer', '', 1)
|
19
|
+
#
|
20
|
+
# If window found, send appropriate keystroke (e.g. {enter}, {Y}, {N}).
|
21
|
+
if (ret==1) then
|
22
|
+
autoit.Send('{enter}')
|
23
|
+
end
|
24
|
+
#
|
25
|
+
# Take a rest to avoid chewing up cycles and give another thread a go.
|
26
|
+
# Then resume the loop.
|
27
|
+
sleep(3)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Check for "Security Information" and "Security Alert" alert popup, click 'Yes'
|
33
|
+
#
|
34
|
+
# Usage: For individual test suite
|
35
|
+
#
|
36
|
+
# before(:all) do
|
37
|
+
# $popup = Thread.new { check_for_alerts }
|
38
|
+
# open_in_browser
|
39
|
+
# ...
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# after(:all) do
|
43
|
+
# close_browser
|
44
|
+
# Thread.kill($popup)
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# or for all tests,
|
48
|
+
# $popup = Thread.new { check_for_alerts }
|
49
|
+
# at_exit{ Thread.kill($popup) }
|
50
|
+
def check_for_security_alerts
|
51
|
+
autoit = WIN32OLE.new('AutoItX3.Control')
|
52
|
+
loop do
|
53
|
+
["Security Alert", "Security Information"].each do |win_title|
|
54
|
+
ret = autoit.WinWait(win_title, '', 1)
|
55
|
+
if (ret==1) then
|
56
|
+
autoit.Send('{Y}')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
sleep(3)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def verify_alert(title = "Microsoft Internet Explorer", button = "OK")
|
64
|
+
if is_windows? && !is_firefox?
|
65
|
+
WIN32OLE.new('AutoItX3.Control').ControlClick(title, '', button)
|
66
|
+
else
|
67
|
+
raise "This function only supports IE"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def click_button_in_security_information_popup(button = "&Yes")
|
72
|
+
verify_alert("Security Information", "", button)
|
73
|
+
end
|
74
|
+
alias click_security_information_popup click_button_in_security_information_popup
|
75
|
+
|
76
|
+
def click_button_in_security_alert_popup(button = "&Yes")
|
77
|
+
verify_alert("Security Alert", "", button)
|
78
|
+
end
|
79
|
+
alias click_security_alert_popup click_button_in_security_alert_popup
|
80
|
+
|
81
|
+
def click_button_in_javascript_popup(button = "OK")
|
82
|
+
verify_alert()
|
83
|
+
end
|
84
|
+
alias click_javascript_popup click_button_in_javascript_popup
|
85
|
+
|
86
|
+
##
|
87
|
+
# This only works for IEs
|
88
|
+
# Cons:
|
89
|
+
# - Slow
|
90
|
+
# - only works in IE
|
91
|
+
# - does not work for security alert ?
|
92
|
+
def ie_popup_clicker(button_name = "OK", max_wait = 15)
|
93
|
+
require 'watir/contrib/enabled_popup'
|
94
|
+
require 'win32ole'
|
95
|
+
hwnd = ie.enabled_popup(15)
|
96
|
+
if (hwnd) #yeah! a popup
|
97
|
+
popup = WinClicker.new
|
98
|
+
popup.makeWindowActive(hwnd) #Activate the window.
|
99
|
+
popup.clickWindowsButton_hwnd(hwnd, button_name) #Click the button
|
100
|
+
#popup.clickWindowsButton(/Internet/,button_name,30)
|
101
|
+
popup = nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def click_popup_window(button, wait_time= 9, user_input=nil )
|
106
|
+
@web_browser.start_clicker(button, wait_time, user_input)
|
107
|
+
sleep 0.5
|
108
|
+
end
|
109
|
+
# run a separate process waiting for the popup window to click
|
110
|
+
#
|
111
|
+
#
|
112
|
+
def prepare_to_click_button_in_popup(button = "OK", wait_time = 3)
|
113
|
+
# !@web_browser.is_firefox?
|
114
|
+
# TODO: firefox is OK
|
115
|
+
if RUBY_PLATFORM =~ /mswin/ then
|
116
|
+
start_checking_js_dialog(button, wait_time)
|
117
|
+
else
|
118
|
+
raise "this only support on Windows and on IE"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Start a background process to click the button on a javascript popup window
|
123
|
+
def start_checking_js_dialog(button = "OK", wait_time = 3)
|
124
|
+
w = WinClicker.new
|
125
|
+
longName = File.expand_path(File.dirname(__FILE__)).gsub("/", "\\" )
|
126
|
+
shortName = w.getShortFileName(longName)
|
127
|
+
c = "start ruby #{shortName}\\clickJSDialog.rb #{button} #{wait_time} "
|
128
|
+
w.winsystem(c)
|
129
|
+
w = nil
|
130
|
+
end
|
131
|
+
|
132
|
+
# Click the button in javascript popup dialog
|
133
|
+
# Usage:
|
134
|
+
# click_button_in_popup_after { click_link('Cancel')}
|
135
|
+
# click_button_in_popup_after("OK") { click_link('Cancel')}
|
136
|
+
#
|
137
|
+
def click_button_in_popup_after(options = {:button => "OK", :wait_time => 3}, &block)
|
138
|
+
if is_windows? then
|
139
|
+
start_checking_js_dialog(options[:button], options[:wait_time])
|
140
|
+
yield
|
141
|
+
else
|
142
|
+
raise "this only support on Windows and on IE"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|