rwebunit 0.2.0 → 0.7.2

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.
@@ -3,408 +3,374 @@
3
3
  #* Distributed open-source, see full license in MIT-LICENSE
4
4
  #***********************************************************
5
5
 
6
- require 'watir/watir_simple.rb'
7
- require 'runit/assert'
8
-
9
- module RWebUnit
10
-
11
- ##
12
- # Wrapping WATIR and provide assertions
13
- #
14
- class WebTester
15
- include Watir::Simple
16
- include RUNIT::Assert
17
-
18
- attr_accessor :test_context
19
-
20
- def initialize
21
- @test_context = TestContext.new
22
- end
23
-
24
- def beginAt(relativeURL)
25
- url = @test_context.base_url + relativeURL
26
- new_browser_at(url) # create @@browser
27
- end
28
-
29
- def closeBrowser
30
- begin
31
- close_browser
32
- rescue
33
- end
34
- end
35
-
36
- def browser_opened?
37
- begin
38
- @@browser != nil
39
- rescue => e
40
- return false
41
- end
42
- end
43
-
44
- def goBack
45
- go_back()
46
- end
47
-
48
- def goForward
49
- go_forward()
50
- end
51
-
52
- def gotoPage(page)
53
- navigate_to(page)
54
- end
55
-
56
- # assertions
57
- def assertTitleEquals(title)
58
- assert_equals(title, html_title())
59
- end
60
-
61
- # text fields
62
- def setFormElement(elementName, elementValue)
63
- enter_text_into_field_with_name(elementName, elementValue)
64
- end
65
- alias enterText setFormElement
66
-
67
- def assertTextPresentInTextField(textfieldName, text, msg = nil)
68
- assert_text_in_field(textfieldName, text, msg)
69
- end
70
-
71
- #links
72
- def clickLink(linkId)
73
- click_link_with_id(linkId)
74
- end
75
-
76
- def clickLinkWithText(linkText)
77
- click_link_with_text(linkText)
78
- end
79
-
80
- def assertLinkPresentWithExactText(linkText)
81
- html_links.each { |link|
82
- return if linkText == link.text
83
- }
84
- fail( "can't find the link with text: #{linkText}")
85
- end
86
-
87
- def assertLinkNotPresentWithExactText(linkText)
88
- html_links.each { |link|
89
- assert(linkText != link.text, "unexpected link (exact): #{linkText} found")
90
- }
91
- end
92
-
93
- def assertLinkPresentWithText(linkText)
94
- html_links.each { |link|
95
- return if link.text.include?(linkText)
96
- }
97
- fail( "can't find the link containing text: #{linkText}")
98
- end
99
-
100
- def assertLinkNotPresentWithText(linkText)
101
- html_links.each { |link|
102
- assert(!link.Text.include?(linkText), "unexpected link containing: #{linkText} found")
103
- }
104
- end
105
-
106
-
107
- ##
108
- # buttons
109
-
110
- # submit first submit button
111
- def submit(buttonName = nil)
112
- if (buttonName.nil?) then
113
- html_buttons.each { |button|
114
- next if button.type != 'submit'
115
- button.click
116
- return
117
- }
118
- else
119
- click_button_with_name(buttonName)
120
- end
121
- end
122
-
123
- def clickButton(buttonId)
124
- click_button_with_id(buttonId)
125
- end
126
-
127
- def clickButtonWithCaption(caption)
128
- click_button_with_caption(caption)
129
- end
130
-
131
- def clickButtonWithValue(value)
132
- click_button_with_value(value)
133
- end
134
-
135
- def assertButtonNotPresent(buttonId)
136
- html_buttons.each { |button|
137
- assert(button.id != buttonId, "unexpected button id: #{buttonId} found")
138
- }
139
- end
140
-
141
- def assertButtonNotPresentWithText(text)
142
- html_buttons.each { |button|
143
- assert(button.value != text, "unexpected button id: #{text} found")
144
- }
145
- end
146
-
147
- def assertButtonPresent(buttonID)
148
- html_buttons.each { |button|
149
- return if buttonID == button.id
150
- }
151
- assert(false, "can't find the button with id: #{buttonID}")
152
- end
153
-
154
- def assertButtonPresentWithText(buttonText)
155
- html_buttons.each { |button|
156
- return if buttonText == button.value
157
- }
158
- assert(false, "can't find the button with text: #{buttonText}")
159
- end
160
-
161
- # checkbox
162
- def checkCheckbox(checkBoxName)
163
- @@browser.checkbox(:name, checkBoxName).set
164
- end
165
-
166
- def uncheckCheckbox(checkBoxName)
167
- @@browser.checkbox(:name, checkBoxName).clear
168
- end
169
-
170
- def assertCheckboxNotSelected(checkBoxName)
171
- html_checkboxes.each { |checkbox|
172
- if (checkbox.name == checkBoxName) then
173
- assert(!checkbox.isSet?, "Checkbox #{checkBoxName} checked unexpected")
174
- end
175
- }
176
- end
177
-
178
- def assertCheckboxSelected(checkBoxName)
179
- html_checkboxes.each { |checkbox|
180
- if (checkbox.name == checkBoxName) then
181
- assert(checkbox.isSet?, "Checkbox #{checkBoxName} not checked")
182
- end
183
- }
184
- end
185
-
186
- # combo box
187
- def selectOption(selectName, option)
188
- @@browser.select_from_combobox_with_name(selectName, option)
189
- end
190
-
191
- def assertOptionValueNotPresent(selectName, optionValue)
192
- html_selects.each { |select|
193
- continue unless select.name == selectName
194
- select.o.each do |option| # items in the list
195
- assert(!(option.value == optionValue), "unexpected select option: #{optionValue} for #{selectName} found")
196
- end
197
- }
198
- end
199
-
200
- def assertOptionNotPresent(selectName, optionLabel)
201
- html_selects.each { |select|
202
- next unless select.name == selectName
203
- select.o.each do |option| # items in the list
204
- assert(!(option.text == optionLabel), "unexpected select option: #{optionLabel} for #{selectName} found")
205
- end
206
- }
207
- end
208
-
209
-
210
- def assertOptionValuePresent(selectName, optionValue)
211
- html_selects.each { |select|
212
- next unless select.name == selectName
213
- select.o.each do |option| # items in the list
214
- return if option.value == optionValue
215
- end
216
- }
217
- assert(false, "can't find the combob box with value: #{optionValue}")
218
- end
219
-
220
- def assertOptionPresent(selectName, optionLabel)
221
- html_selects.each { |select|
222
- next unless select.name == selectName
223
- select.o.each do |option| # items in the list
224
- return if option.text == optionLabel
225
- end
226
- }
227
- assert(false, "can't find the combob box: #{selectName} with value: #{optionLabel}")
228
- end
229
-
230
- def assertOptionEquals(selectName, optionLabel)
231
- html_selects.each { |select|
232
- next unless select.name == selectName
233
- select.o.each do |option| # items in the list
234
- if (option.text == optionLabel) then
235
- assert_equal(select.value, option.value, "Select #{selectName}'s value is not equal to expected option label: '#{optionLabel}'")
236
- end
237
- end
238
- }
239
- end
240
-
241
-
242
- def assertOptionValueEquals(selectName, optionValue)
243
- html_selects.each { |select|
244
- next unless select.name == selectName
245
- assert_equal(select.value, optionValue, "Select #{selectName}'s value is not equal to expected: '#{optionValue}'")
246
- }
247
- end
248
-
249
-
250
- #radio
251
-
252
- # radioGroup is the name field, radio options 'value' field
253
- def assertRadioOptionNotPresent(radioGroup, radioOption)
254
- html_radios.each { |radio|
255
- if (radio.name == radioGroup) then
256
- assert(!(radioOption == radio.value), "unexpected radio option: " + radioOption + " found")
257
- end
258
- }
259
- end
260
-
261
- def assertRadioOptionPresent(radioGroup, radioOption)
262
- html_radios.each { |radio|
263
- return if (radio.name == radioGroup) and (radioOption == radio.value)
264
- }
265
- fail("can't find the radio option : '#{radioOption}'")
266
- end
267
-
268
- def assertRadioOptionSelected(radioGroup, radioOption)
269
- html_radios.each { |radio|
270
- if (radio.name == radioGroup and radioOption == radio.value) then
271
- assert(radio.isSet?, "Radio button #{radioGroup}-[#{radioOption}] not checked")
272
- end
273
- }
274
- end
275
-
276
- def assertRadioOptionNotSelected(radioGroup, radioOption)
277
- html_radios.each { |radio|
278
- if (radio.name == radioGroup and radioOption == radio.value) then
279
- assert(!radio.isSet?, "Radio button #{radioGroup}-[#{radioOption}] checked unexpected")
280
- end
281
- }
282
- end
283
-
284
- # the method is protected in JWebUnit
285
- def clickRadioOption(radioGroup, radioOption)
286
- html_radios.each { |radio|
287
- if (radio.name == radioGroup and radioOption == radio.value) then
288
- radio.set
289
- end
290
- }
291
- end
292
-
293
- #text
294
- def assertTextPresent(text)
295
- assert((html_body().include? text), 'expected text: ' + text + ' not found')
296
- end
297
-
298
- def assertTextNotPresent(text)
299
- assert(!(html_body().include? text), 'expected text: ' + text + ' found')
300
- end
301
-
302
-
303
- def assertTextInTable(tableId, text)
304
- elem = @@browser.document.getElementById(tableId)
305
- assert_not_nil(elem, "tableId #{tableId} not exists")
306
- assert_equal(elem.tagName, "TABLE")
307
- puts elem.innerHTML if $DEBUG
308
- assert(elem.innerHTML.include?(text), "the text #{text} not found in table #{tableId}")
309
- end
310
-
311
- def assertTextNotInTable(tableId, text)
312
- elem = @@browser.document.getElementById(tableId)
313
- assert_equal(elem.name.uppercase, "TABLE")
314
- assert_not_nil(elem, "tableId #{tableId} not exists")
315
- assert(!elem.innerHTML.include?(text), "unexpected text #{text} found in table #{tableId}")
316
- end
317
-
318
- def assertElementPresent(elementID)
319
- assert_not_nil(@@browser.document.getElementById(elementID), "element with id #{elementID} not found")
320
- end
321
-
322
- def assertElementNotPresent(elementID)
323
- assert_nil(@@browser.document.getElementById(elementID), "unexpected element with id #{elementID} found")
324
- end
325
-
326
- def assertTextInElement(elementID, text)
327
- assertElementPresent(elementID)
328
- elem = @@browser.document.getElementById(elementID)
329
- assert_not_nil(elem.innerText, "element #{elementID} has no text")
330
- assert(elem.innerText.include?(text), "the text #{text} not found in element #{elementID}")
331
- end
332
-
333
- def getElementValue(elementId)
334
- assertElementPresent(elementId)
335
- elem = @@browser.document.getElementById(elementId)
336
- elem_val = elem.invoke('innerText')
337
- end
338
-
339
- # new API
340
- def getHtmlInElement(elementId)
341
- elem = @@browser.document.getElementById(elementId)
342
- assert_not_nil(elem, "HTML element: #{elementId} not exists")
343
- elem.innerHTML
344
- end
345
-
346
- def getElementById(elementId)
347
- elem = @@browser.document.getElementById(elementId)
348
- end
349
-
350
-
351
- # ---
352
- # For deubgging
353
- # ---
354
- def dumpResponse(stream = nil)
355
- if stream.nil?
356
- puts html_body # std out
357
- else
358
- stream.puts html_body
359
- end
360
- end
361
-
362
- private
363
- def html_body
364
- @@browser.html()
365
- end
366
-
367
- def html_title
368
- @@browser.document.title
369
- end
370
-
371
- def html_links
372
- @@browser.links
373
- end
374
-
375
- def html_buttons
376
- @@browser.buttons
377
- end
378
-
379
- def html_selects
380
- @@browser.select_lists
381
- end
382
-
383
- def html_checkboxes
384
- @@browser.checkboxes
385
- end
386
-
387
- def html_radios
388
- @@browser.radios
389
- end
390
-
391
- def assert_equals(expected, actual, msg=nil)
392
- assert(expected == actual, (msg.nil?) ? "Expected: #{expected} diff from actual: #{actual}" : msg)
393
- end
394
- alias assert_equal assert_equals
6
+ begin
7
+ require 'watir'
8
+ require 'watir/contrib/enabled_popup'
9
+ require 'watir/close_all'
10
+ $watir_loaded = true
11
+ rescue LoadError => e
12
+ $watir_loaded = false
13
+ end
395
14
 
396
- def assert_nil(actual, msg="")
397
- assert(actual.nil?, msg)
398
- end
15
+ begin
16
+ require "firewatir";
17
+ $firewatir_loaded = true
18
+ rescue LoadError => e
19
+ $firewatir_loaded = false
20
+ end
399
21
 
400
- def assert_not_nil(actual, msg="")
401
- assert(!actual.nil?, msg)
402
- end
22
+ raise "You have must at least Watir or Firewatir installed" unless $watir_loaded || $firewatir_loaded
403
23
 
404
- def fail(message)
405
- assert(false, message)
406
- end
24
+ module RWebUnit
407
25
 
408
- end
26
+ ##
27
+ # Wrapping WATIR and provide *assertions*
28
+ #
29
+ class WebTester
30
+
31
+ attr_accessor :test_context
32
+
33
+ def initialize(base_url = nil, options = {})
34
+ default_options = {:speed => "fast",
35
+ :visible => true,
36
+ :highlight_colour => 'yellow',
37
+ :close_others => true}
38
+ options = default_options.merge options
39
+ @test_context = TestContext.new base_url if base_url
40
+
41
+ if (options[:firefox] && $firewatir_loaded) || ($firewatir_loaded and !$watir_loaded)
42
+ @@browser = FireWatir::Firefox.start(base_url)
43
+ elsif $watir_loaded
44
+ @@browser = Watir::IE.new
45
+ if options[:speed] == 'slow' then @@browser.set_slow_speed else @@browser.set_fast_speed end
46
+ @@browser.activeObjectHighLightColor = options[:highlight_colour]
47
+ @@browser.visible = options[:visible]
48
+ @@browser.close_others if options[:close_others]
49
+ else
50
+ raise "rWebUnit initialiazation error, most likely Watir or Firewatir not present"
51
+ end
52
+ end
53
+
54
+ ##
55
+ # Delegate to Watir
56
+ #
57
+ def area(*args); @@browser.area(*args); end
58
+ def button(*args); @@browser.button(*args); end
59
+ def cell(*args); @@browser.cell(*args); end
60
+ alias td cell
61
+ def checkbox(*args); @@browser.checkbox(*args); end
62
+ alias check_box checkbox # seems watir doc is wrong, checkbox not check_box
63
+ def div(*args); @@browser.div(*args); end
64
+ def form(*args); @@browser.form(*args); end
65
+ def frame(*args); @@browser.frame(*args); end
66
+ def h1(*args); @@browser.h1(*args); end
67
+ def h2(*args); @@browser.h2(*args); end
68
+ def h3(*args); @@browser.h3(*args); end
69
+ def h4(*args); @@browser.h4(*args); end
70
+ def h5(*args); @@browser.h5(*args); end
71
+ def h6(*args); @@browser.h6(*args); end
72
+ def hidden(*args); @@browser.hidden(*args); end
73
+ def image(*args); @@browser.image(*args); end
74
+ def li(*args); @@browser.li(*args); end
75
+ def link(*args); @@browser.link(*args); end
76
+ def map(*args); @@browser.map(*args); end
77
+ def pre(*args); @@browser.pre(*args); end
78
+ def radio(*args); @@browser.radio(*args); end
79
+ def row(*args); @@browser.row(*args); end
80
+ alias tr row
81
+ def select_list(*args); @@browser.select_list(*args); end
82
+ def span(*args); @@browser.span(*args); end
83
+ def table(*args); @@browser.table(*args); end
84
+ def text_field(*args); @@browser.text_field(*args); end
85
+
86
+ def paragraph(*args); @@browser.paragraph(*args); end
87
+ def file_field(*args); @@browser.file_field(*args); end
88
+ def label(*args); @@browser.span(*args); end
89
+
90
+ def contains_text(text); @@browser.contains_text(text); end
91
+
92
+ def page_source
93
+ @@browser.html()
94
+ #@@browser.document.body
95
+ end
96
+ alias html_body page_source
97
+
98
+ def page_title
99
+ if is_firefox?
100
+ @@browser.title
101
+ else
102
+ @@browser.document.title
103
+ end
104
+ end
105
+
106
+ def links; @@browser.links; end
107
+ def buttons; @@browser.buttons; end
108
+ def select_lists; @@browser.select_lists; end
109
+ def checkboxes; @@browser.checkboxes; end
110
+ def radios; @@browser.radios; end
111
+ def text_fields; @@browser.text_fields; end
112
+
113
+ # current url
114
+ def url; @@browser.url; end
115
+
116
+ def base_url=(new_base_url)
117
+ if @test_context
118
+ @test_conext.base_url = new_base_url
119
+ return
120
+ end
121
+ @test_context = TestContext.new base_url
122
+ end
123
+
124
+ def is_firefox?
125
+ begin
126
+ @@browser.class == FireWatir::Firefox
127
+ rescue => e
128
+ return false
129
+ end
130
+ end
131
+
132
+ # Close the browser window. Useful for automated test suites to reduce
133
+ # test interaction.
134
+ def close_browser
135
+ if is_firefox? then
136
+ puts "[debug] about to close firefox"
137
+ @@browser.close
138
+ else
139
+ @@browser.getIE.quit
140
+ end
141
+ sleep 2
142
+ end
143
+
144
+ def self.close_all_browsers
145
+ if is_firefox? then
146
+ @@browser.close_all
147
+ else
148
+ Watir::IE.close_all
149
+ end
150
+ end
151
+
152
+ def full_url(relative_url)
153
+ if @test_context && @test_context.base_url
154
+ @test_context.base_url + relative_url
155
+ else
156
+ relative_url
157
+ end
158
+ end
159
+
160
+ def begin_at(relative_url)
161
+ @@browser.goto full_url(relative_url)
162
+ end
163
+
164
+ def browser_opened?
165
+ begin
166
+ @@browser != nil
167
+ rescue => e
168
+ return false
169
+ end
170
+ end
171
+
172
+ # Some browsers (i.e. IE) need to be waited on before more actions can be
173
+ # performed. Most action methods in Watir::Simple already call this before
174
+ # and after.
175
+ def wait_for_browser
176
+ @@browser.waitForIE unless is_firefox?
177
+ end
178
+
179
+
180
+ # A convenience method to wait at both ends of an operation for the browser
181
+ # to catch up.
182
+ def wait_before_and_after
183
+ wait_for_browser
184
+ yield
185
+ wait_for_browser
186
+ end
187
+
188
+
189
+ def go_back; @@browser.back; end
190
+ def go_forward; @@browser.forward; end
191
+ def goto_page(page); @@browser.goto full_url(page); end
192
+ def refresh; @@browser.refresh; end
193
+
194
+ def focus; @@browser.focus; end
195
+ def close_others; @@browser.close_others; end
196
+
197
+ # text fields
198
+ def enter_text_into_field_with_name(name, text)
199
+ wait_before_and_after { text_field(:name, name).set(text) }
200
+ end
201
+ alias set_form_element enter_text_into_field_with_name
202
+ alias enter_text enter_text_into_field_with_name
203
+
204
+ #links
205
+ def click_link(link_id)
206
+ wait_before_and_after { link(:id, link_id).click }
207
+ end
208
+ alias click_link_with_id click_link
209
+
210
+ def click_link_with_text(text)
211
+ wait_before_and_after { link(:text, text).click }
212
+ end
213
+
214
+ ##
215
+ # buttons
216
+
217
+ def click_button_with_id(id)
218
+ wait_before_and_after { button(:id, id).click }
219
+ end
220
+ alias click_button click_button_with_id
221
+
222
+ def click_button_with_name(name)
223
+ wait_before_and_after { button(:name, name).click }
224
+ end
225
+
226
+ def click_button_with_caption(caption)
227
+ wait_before_and_after { button(:caption, caption).click }
228
+ end
229
+
230
+ def click_button_with_value(value)
231
+ wait_before_and_after { button(:value, value).click }
232
+ end
233
+
234
+ def select_option(selectName, option)
235
+ # @@browser.selectBox(:name, selectName).select(option)
236
+ select_list(:name, selectName).select(option)
237
+ end
238
+
239
+ # submit first submit button
240
+ def submit(buttonName = nil)
241
+ if (buttonName.nil?) then
242
+ buttons.each { |button|
243
+ next if button.type != 'submit'
244
+ button.click
245
+ return
246
+ }
247
+ else
248
+ click_button_with_name(buttonName)
249
+ end
250
+ end
251
+
252
+ # checkbox
253
+ def check_checkbox(checkBoxName, values=nil)
254
+ if values
255
+ values.class == Array ? arys = values : arys = [values]
256
+ arys.each {|cbx_value|
257
+ checkbox(:name, checkBoxName, cbx_value).set
258
+ }
259
+ else
260
+ checkbox(:name, checkBoxName).set
261
+ end
262
+ end
263
+
264
+ def uncheck_checkbox(checkBoxName, values = nil)
265
+ if values
266
+ values.class == Array ? arys = values : arys = [values]
267
+ arys.each {|cbx_value|
268
+ checkbox(:name, checkBoxName, cbx_value).clear
269
+ }
270
+ else
271
+ checkbox(:name, checkBoxName).clear
272
+ end
273
+ end
274
+
275
+
276
+ # the method is protected in JWebUnit
277
+ def click_radio_option(radio_group, radio_option)
278
+ radio(:name, radio_group, radio_option).set
279
+ end
280
+
281
+ def clear_radio_option(radio_group, radio_option)
282
+ radio(:name, radio_group, radio_option).clear
283
+ end
284
+
285
+ def element_by_id(elem_id)
286
+ elem = @@browser.document.getElementById(elem_id)
287
+ end
288
+
289
+ def element_value(elementId)
290
+ if is_firefox? then
291
+ elem = element_by_id(elementId)
292
+ elem ? elem.invoke('innerText') : nil
293
+ else
294
+ elem = element_by_id(elementId)
295
+ elem ? elem.invoke('innerText') : nil
296
+ end
297
+ end
298
+
299
+ def element_source(elementId)
300
+ elem = element_by_id(elementId)
301
+ assert_not_nil(elem, "HTML element: #{elementId} not exists")
302
+ elem.innerHTML
303
+ end
304
+
305
+ def select_file_for_upload(file_field, file_path)
306
+ file_field(:name, file_field).set(file_path)
307
+ end
308
+
309
+ def start_window(url = nil); @@browser.start_window(url); end
310
+ def self.attach_browser(how, what)
311
+ if is_firefox? then
312
+ raise "not implemented yet"
313
+ else
314
+ Watir::IE.attach(how, what)
315
+ end
316
+ end
317
+
318
+ # Attach a Watir::IE instance to a popup window.
319
+ #
320
+ # Typical usage
321
+ # new_popup_window(:url => "http://www.google.com/a.pdf")
322
+ def new_popup_window(options, browser = "ie")
323
+ if is_firefox?
324
+ raise "not implemented"
325
+ else
326
+ Watir::IE.attach(:url, options[:url])
327
+ end
328
+ end
329
+
330
+ # ---
331
+ # For deubgging
332
+ # ---
333
+ def dump_response(stream = nil)
334
+ if stream.nil?
335
+ puts page_source # std out
336
+ else
337
+ stream.puts page_source
338
+ end
339
+ end
340
+
341
+ # A Better Popup Handler using the latest Watir version. Posted by Mark_cain@rl.gov
342
+ #
343
+ # http://wiki.openqa.org/display/WTR/FAQ#FAQ-HowdoIattachtoapopupwindow%3F
344
+ #
345
+ def start_clicker( button , waitTime= 9, user_input=nil)
346
+ # get a handle if one exists
347
+ hwnd = @@browser.enabled_popup(waitTime)
348
+ if (hwnd) # yes there is a popup
349
+ w = WinClicker.new
350
+ if ( user_input )
351
+ w.setTextValueForFileNameField( hwnd, "#{user_input}" )
352
+ end
353
+ # I put this in to see the text being input it is not necessary to work
354
+ sleep 3
355
+ # "OK" or whatever the name on the button is
356
+ w.clickWindowsButton_hwnd( hwnd, "#{button}" )
357
+ #
358
+ # this is just cleanup
359
+ w = nil
360
+ end
361
+ end
362
+
363
+ # return underlying browser
364
+ def ie
365
+ raise "can't call this as it is configured to use Firefox" if is_firefox?
366
+ @@browser
367
+ end
368
+
369
+ def firefox
370
+ raise "can't call this as it is configured to use IE" unless is_firefox?
371
+ @@browser
372
+ end
373
+
374
+ end
409
375
 
410
376
  end