rwebspec-webdriver 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +11 -12
- data/lib/rwebspec-webdriver/assert.rb +445 -0
- data/lib/{rwebspec → rwebspec-webdriver}/clickJSDialog.rb +0 -0
- data/lib/{rwebspec → rwebspec-webdriver}/context.rb +13 -11
- data/lib/rwebspec-webdriver/database_checker.rb +76 -0
- data/lib/rwebspec-webdriver/driver.rb +1011 -0
- data/lib/rwebspec-webdriver/element_locator.rb +89 -0
- data/lib/rwebspec-webdriver/load_test_helper.rb +174 -0
- data/lib/{rwebspec → rwebspec-webdriver}/matchers/contains_text.rb +0 -0
- data/lib/rwebspec-webdriver/popup.rb +149 -0
- data/lib/rwebspec-webdriver/rspec_helper.rb +101 -0
- data/lib/rwebspec-webdriver/test_script.rb +10 -0
- data/lib/rwebspec-webdriver/test_utils.rb +219 -0
- data/lib/rwebspec-webdriver/testwise_plugin.rb +85 -0
- data/lib/rwebspec-webdriver/using_pages.rb +51 -0
- data/lib/rwebspec-webdriver/web_browser.rb +744 -0
- data/lib/rwebspec-webdriver/web_page.rb +110 -0
- data/lib/rwebspec-webdriver/web_testcase.rb +40 -0
- data/lib/rwebspec-webdriver.rb +12 -12
- metadata +22 -22
- data/lib/rwebspec/assert.rb +0 -443
- data/lib/rwebspec/database_checker.rb +0 -74
- data/lib/rwebspec/driver.rb +0 -1009
- data/lib/rwebspec/element_locator.rb +0 -87
- data/lib/rwebspec/load_test_helper.rb +0 -172
- data/lib/rwebspec/popup.rb +0 -147
- data/lib/rwebspec/rspec_helper.rb +0 -99
- data/lib/rwebspec/test_script.rb +0 -8
- data/lib/rwebspec/test_utils.rb +0 -217
- data/lib/rwebspec/testwise_plugin.rb +0 -83
- data/lib/rwebspec/using_pages.rb +0 -49
- data/lib/rwebspec/web_browser.rb +0 -742
- data/lib/rwebspec/web_page.rb +0 -108
- data/lib/rwebspec/web_testcase.rb +0 -38
@@ -1,87 +0,0 @@
|
|
1
|
-
module RWebSpec
|
2
|
-
|
3
|
-
module ElementLocator
|
4
|
-
|
5
|
-
BUTTON_VALID_TYPES = %w[button reset submit image]
|
6
|
-
def button_elements
|
7
|
-
find_elements(:xpath, ".//button | .//input[#{attribute_expression :type => BUTTON_VALID_TYPES}]")
|
8
|
-
end
|
9
|
-
|
10
|
-
CHECK_BOX_TYPES = %w(checkbox)
|
11
|
-
def check_box_elements(how, what, opts = [])
|
12
|
-
find_elements(:xpath, ".//input[#{attribute_expression :type => CHECK_BOX_TYPES}]")
|
13
|
-
end
|
14
|
-
|
15
|
-
RADIO_TYPES = %w(radio)
|
16
|
-
def radio_elements(how, what, opts = [])
|
17
|
-
find_elements(:xpath, ".//input[#{attribute_expression :type => RADIO_TYPES}]")
|
18
|
-
end
|
19
|
-
|
20
|
-
def select_elements(how, what, opts = [])
|
21
|
-
find_elements(:xpath, ".//input[#{attribute_expression :type => RADIO_TYPES}]")
|
22
|
-
end
|
23
|
-
|
24
|
-
# TextField, TextArea
|
25
|
-
TEXT_FILED_TYPES = %w(text)
|
26
|
-
def text_field_elements
|
27
|
-
find_elements(:xpath, ".//input[#{attribute_expression :type => TEXT_FILED_TYPES}]")
|
28
|
-
end
|
29
|
-
|
30
|
-
def text_area_elements
|
31
|
-
find_elements(:xpath, ".//textarea")
|
32
|
-
end
|
33
|
-
|
34
|
-
FILE_FIELD_TYPES = %w(file)
|
35
|
-
def file_field_elements
|
36
|
-
find_elements(:xpath, ".//input[#{attribute_expression :type => FILE_FIELD_TYPES}]")
|
37
|
-
end
|
38
|
-
|
39
|
-
HIDDEN_TYPES = %w(hidden)
|
40
|
-
def hidden_elements
|
41
|
-
find_elements(:xpath, ".//input[#{attribute_expression :type => HIDDEN_TYPES}]")
|
42
|
-
end
|
43
|
-
|
44
|
-
#---
|
45
|
-
def find_by_tag(tag)
|
46
|
-
find_elements(:tag_name, tag)
|
47
|
-
end
|
48
|
-
|
49
|
-
def should_use_label_element?
|
50
|
-
@selector[:tag_name] != "option" rescue false
|
51
|
-
end
|
52
|
-
|
53
|
-
def equal_pair(key, value)
|
54
|
-
# we assume :label means a corresponding label element, not the attribute
|
55
|
-
if key == :label && should_use_label_element?
|
56
|
-
"@id=//label[normalize-space()='#{value}']/@for"
|
57
|
-
else
|
58
|
-
"#{lhs_for(key)}='#{value}'"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def lhs_for(key)
|
63
|
-
case key
|
64
|
-
when :text, 'text'
|
65
|
-
'normalize-space()'
|
66
|
-
when :href
|
67
|
-
# TODO: change this behaviour?
|
68
|
-
'normalize-space(@href)'
|
69
|
-
else
|
70
|
-
"@#{key.to_s.gsub("_", "-")}"
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
|
75
|
-
def attribute_expression(selectors)
|
76
|
-
selectors.map do |key, val|
|
77
|
-
if val.kind_of?(Array)
|
78
|
-
"(" + val.map { |v| equal_pair(key, v) }.join(" or ") + ")"
|
79
|
-
else
|
80
|
-
equal_pair(key, val)
|
81
|
-
end
|
82
|
-
end.join(" and ")
|
83
|
-
end
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
end
|
@@ -1,172 +0,0 @@
|
|
1
|
-
|
2
|
-
module RWebSpec
|
3
|
-
module LoadTestHelper
|
4
|
-
|
5
|
-
include RWebSpec::Utils
|
6
|
-
include RWebSpec::Assert
|
7
|
-
|
8
|
-
MAX_VU = 1000
|
9
|
-
|
10
|
-
# only support firefox or Celerity
|
11
|
-
def open_browser(base_url, options = {})
|
12
|
-
default_options = {:resynchronize => false, :firefox => false }
|
13
|
-
options = default_options.merge(options)
|
14
|
-
options[:firefox] ||= (ENV['LOADWISE_PREVIEW'] || $LOADWISE_PREVIEW)
|
15
|
-
RWebSpec::WebBrowser.new(base_url, nil, options)
|
16
|
-
end
|
17
|
-
|
18
|
-
# maybe attach_browser
|
19
|
-
|
20
|
-
# Does not provide real function, other than make enhancing test syntax
|
21
|
-
#
|
22
|
-
# Example:
|
23
|
-
# allow { click_button('Register') }
|
24
|
-
def allow(&block)
|
25
|
-
yield
|
26
|
-
end
|
27
|
-
alias shall_allow allow
|
28
|
-
alias allowing allow
|
29
|
-
|
30
|
-
# try operation, ignore if errors occur
|
31
|
-
#
|
32
|
-
# Example:
|
33
|
-
# failsafe { click_link("Logout") } # try logout, but it still OK if not being able to (already logout))
|
34
|
-
def failsafe(&block)
|
35
|
-
begin
|
36
|
-
yield
|
37
|
-
rescue =>e
|
38
|
-
end
|
39
|
-
end
|
40
|
-
alias fail_safe failsafe
|
41
|
-
|
42
|
-
# Try the operation up to specified timeout (in seconds), and sleep given interval (in seconds).
|
43
|
-
# Error will be ignored until timeout
|
44
|
-
# Example
|
45
|
-
# try { click_link('waiting')}
|
46
|
-
# try(10, 2) { click_button('Search' } # try to click the 'Search' button upto 10 seconds, try every 2 seconds
|
47
|
-
# try { click_button('Search' }
|
48
|
-
def try(timeout = @@default_timeout, polling_interval = @@default_polling_interval || 1, &block)
|
49
|
-
start_time = Time.now
|
50
|
-
|
51
|
-
last_error = nil
|
52
|
-
until (duration = Time.now - start_time) > timeout
|
53
|
-
begin
|
54
|
-
return if yield
|
55
|
-
last_error = nil
|
56
|
-
rescue => e
|
57
|
-
last_error = e
|
58
|
-
end
|
59
|
-
sleep polling_interval
|
60
|
-
end
|
61
|
-
|
62
|
-
raise "Timeout after #{duration.to_i} seconds with error: #{last_error}." if last_error
|
63
|
-
raise "Timeout after #{duration.to_i} seconds."
|
64
|
-
end
|
65
|
-
alias try_upto try
|
66
|
-
|
67
|
-
##
|
68
|
-
# Convert :first to 1, :second to 2, and so on...
|
69
|
-
def symbol_to_sequence(symb)
|
70
|
-
value = { :zero => 0,
|
71
|
-
:first => 1,
|
72
|
-
:second => 2,
|
73
|
-
:third => 3,
|
74
|
-
:fourth => 4,
|
75
|
-
:fifth => 5,
|
76
|
-
:sixth => 6,
|
77
|
-
:seventh => 7,
|
78
|
-
:eighth => 8,
|
79
|
-
:ninth => 9,
|
80
|
-
:tenth => 10 }[symb]
|
81
|
-
return value || symb.to_i
|
82
|
-
end
|
83
|
-
|
84
|
-
# monitor current execution using
|
85
|
-
#
|
86
|
-
# Usage
|
87
|
-
# log_time { browser.click_button('Confirm') }
|
88
|
-
def log_time(msg, &block)
|
89
|
-
start_time = Time.now
|
90
|
-
yield
|
91
|
-
end_time = Time.now
|
92
|
-
|
93
|
-
Thread.current[:log] ||= []
|
94
|
-
Thread.current[:log] << {:file => File.basename(__FILE__),
|
95
|
-
:message => msg,
|
96
|
-
:start_time => Time.now,
|
97
|
-
:duration => Time.now - start_time}
|
98
|
-
|
99
|
-
if $LOADWISE_MONITOR
|
100
|
-
begin
|
101
|
-
require 'java'
|
102
|
-
puts "Calling Java 1"
|
103
|
-
java_import com.loadwise.db.MemoryDatabase
|
104
|
-
#puts "Calling Java 2: #{MemoryDatabase.count}"
|
105
|
-
MemoryDatabase.addEntry(1, "zdfa01", "a_spec.rb", msg, start_time, end_time);
|
106
|
-
puts "Calling Java Ok: #{MemoryDatabase.count}"
|
107
|
-
rescue NameError => ne
|
108
|
-
puts "Name Error: #{ne}"
|
109
|
-
# failed to load Java class
|
110
|
-
rescue => e
|
111
|
-
puts "Failed to calling Java: #{e.class.name}"
|
112
|
-
end
|
113
|
-
end
|
114
|
-
# How to notify LoadWise at real time
|
115
|
-
# LoadWise to collect CPU
|
116
|
-
end
|
117
|
-
|
118
|
-
def run_with_virtual_users(virtual_user_count = 2, preview = false, &block)
|
119
|
-
raise "too many virtual users" if virtual_user_count > MAX_VU
|
120
|
-
|
121
|
-
begin
|
122
|
-
if defined?(LOADWISE_PREVIEW)
|
123
|
-
preview = LOADWISE_PREVIEW
|
124
|
-
end
|
125
|
-
rescue => e1
|
126
|
-
end
|
127
|
-
|
128
|
-
if preview
|
129
|
-
virtual_user_count = 1
|
130
|
-
$LOADWISE_PREVIEW = true
|
131
|
-
end
|
132
|
-
|
133
|
-
if (virtual_user_count <= 1)
|
134
|
-
yield
|
135
|
-
else
|
136
|
-
threads = []
|
137
|
-
vu_reports = {}
|
138
|
-
virtual_user_count.times do |idx|
|
139
|
-
threads[idx] = Thread.new do
|
140
|
-
start_time = Time.now
|
141
|
-
vu_reports[idx] ||= []
|
142
|
-
begin
|
143
|
-
yield
|
144
|
-
vu_reports[idx] = Thread.current[:log]
|
145
|
-
rescue => e
|
146
|
-
vu_reports[idx] = Thread.current[:log]
|
147
|
-
vu_reports[idx] ||= []
|
148
|
-
vu_reports[idx] << { :error => e }
|
149
|
-
end
|
150
|
-
vu_reports[idx] ||= []
|
151
|
-
vu_reports[idx] << { :message => "Total Duration", :duration => Time.now - start_time }
|
152
|
-
puts "VU[#{idx+1}] #{Time.now - start_time}s"
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
threads.each {|t| t.join; }
|
157
|
-
vu_reports.each do |key, value|
|
158
|
-
value.each do |entry|
|
159
|
-
if entry[:error] then
|
160
|
-
puts "Error: #{entry[:error]}"
|
161
|
-
else
|
162
|
-
puts "[#{key}] #{entry[:message]}, #{entry[:duration]}"
|
163
|
-
end
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
|
-
return vu_reports
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
end
|
172
|
-
end
|
data/lib/rwebspec/popup.rb
DELETED
@@ -1,147 +0,0 @@
|
|
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/ || RUBY_PLATFORM =~ /mingw/ 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,99 +0,0 @@
|
|
1
|
-
require 'uri'
|
2
|
-
require File.dirname(__FILE__) + "/driver"
|
3
|
-
require File.dirname(__FILE__) + "/web_page"
|
4
|
-
require File.dirname(__FILE__) + "/assert"
|
5
|
-
|
6
|
-
# ZZ patches to RSpec 1.1.2 - 1.1.4
|
7
|
-
# - add to_s method to example_group
|
8
|
-
module Spec
|
9
|
-
module Example
|
10
|
-
class ExampleGroup
|
11
|
-
def to_s
|
12
|
-
@_defined_description
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
# example
|
19
|
-
# should link_by_text(text, options).size > 0
|
20
|
-
|
21
|
-
module RWebSpec
|
22
|
-
module RSpecHelper
|
23
|
-
include RWebSpec::Driver
|
24
|
-
include RWebSpec::Assert
|
25
|
-
include RWebSpec::Utils
|
26
|
-
|
27
|
-
# --
|
28
|
-
# Content
|
29
|
-
# --
|
30
|
-
|
31
|
-
def table_source(table_id)
|
32
|
-
table(:id, table_id).innerHTML
|
33
|
-
# elem = @web_browser.document.getElementById(table_id)
|
34
|
-
# raise "The element '#{table_id}' is not a table or there are multple elements with same id" unless elem.name.uppercase == "TABLE"
|
35
|
-
# elem.innerHTML
|
36
|
-
end
|
37
|
-
alias table_source_by_id table_source
|
38
|
-
|
39
|
-
def element_text(elem_id)
|
40
|
-
@web_browser.element_value(elem_id)
|
41
|
-
end
|
42
|
-
alias element_text_by_id element_text
|
43
|
-
|
44
|
-
#TODO: is it working?
|
45
|
-
def element_source(elem_id)
|
46
|
-
@web_browser.get_html_in_element(elem_id)
|
47
|
-
end
|
48
|
-
|
49
|
-
|
50
|
-
def button_by_id(button_id)
|
51
|
-
button(:id, button_id)
|
52
|
-
end
|
53
|
-
|
54
|
-
def buttons_by_caption(text)
|
55
|
-
button(:text, text)
|
56
|
-
end
|
57
|
-
alias buttons_by_text buttons_by_caption
|
58
|
-
|
59
|
-
def link_by_id(link_id)
|
60
|
-
link(:id, link_id)
|
61
|
-
end
|
62
|
-
|
63
|
-
# default options: exact => true
|
64
|
-
def links_by_text(link_text, options = {})
|
65
|
-
options.merge!({:exact=> true})
|
66
|
-
matching_links = []
|
67
|
-
links.each { |link|
|
68
|
-
matching_links << link if (options[:exact] ? link.text == link_text : link.text.include?(link_text))
|
69
|
-
}
|
70
|
-
return matching_links
|
71
|
-
end
|
72
|
-
alias links_with_text links_by_text
|
73
|
-
|
74
|
-
def save_page(file_name = nil)
|
75
|
-
@web_browser.save_page(file_name)
|
76
|
-
end
|
77
|
-
|
78
|
-
def save_content_to_file(content, file_name = nil)
|
79
|
-
file_name ||= Time.now.strftime("%Y%m%d%H%M%S") + ".html"
|
80
|
-
puts "about to save page: #{File.expand_path(file_name)}"
|
81
|
-
File.open(file_name, "w").puts content
|
82
|
-
end
|
83
|
-
|
84
|
-
# When running
|
85
|
-
def debugging?
|
86
|
-
$ITEST2_DEBUGGING && $ITEST2_RUNNING_AS == "test_case"
|
87
|
-
end
|
88
|
-
|
89
|
-
# RSpec Matchers
|
90
|
-
#
|
91
|
-
# Example,
|
92
|
-
# a_number.should be_odd_number
|
93
|
-
def be_odd_number
|
94
|
-
simple_matcher("must be odd number") { |actual| actual && actual.to_id % 2 == 1}
|
95
|
-
end
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
end
|