kuali-test-factory 0.5.3.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.
@@ -0,0 +1,67 @@
1
+ # Copyright 2012-2014 The rSmart Group, Inc.
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ # =================
16
+ # CollectionsFactory
17
+ # =================
18
+ #
19
+ # Use this as the superclass for your data object collection classes.
20
+ class CollectionsFactory < Array
21
+
22
+ def initialize(browser)
23
+ @browser=browser
24
+ end
25
+
26
+ # Defines the class of objects contained in the collection
27
+ #
28
+ def self.contains klass
29
+
30
+ # Creates a method called "add" that will create the specified data
31
+ # object and then add it as an item in the collection.
32
+ #
33
+ # Note that it's assumed that the target data object will have a
34
+ # create method defined. If not, this will not work properly.
35
+ define_method 'add' do |opts={}|
36
+ element = klass.new @browser, opts
37
+ element.create
38
+ self << element
39
+ end
40
+
41
+ end
42
+
43
+ # Makes a "deep copy" of the Collection. See the #data_object_copy
44
+ # method description in the DataObject class for more information.
45
+ #
46
+ def copy
47
+ new_collection = self.class.new(@browser)
48
+ self.each do |item|
49
+ new_collection << item.data_object_copy
50
+ end
51
+ new_collection
52
+ end
53
+
54
+ # Used in conjunction with the Parent object containing
55
+ # the collection.
56
+ #
57
+ # The parent sends updated information to the collection(s)
58
+ # using #notify_collections
59
+ #
60
+ def notify_members *updates
61
+ self.each { |member| member.update_from_parent *updates }
62
+ end
63
+
64
+ end
65
+
66
+ # Just an alias class name.
67
+ class CollectionFactory < CollectionsFactory; end
@@ -0,0 +1,133 @@
1
+ # Copyright 2012-2014 The rSmart Group, Inc.
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ class Time
16
+
17
+ # Using the :year_range option (or no option), this method creates a
18
+ # Time object of a random value, within
19
+ # the year range specified (default is 5 years in the past).
20
+ #
21
+ # Using the :series option, this method returns an array
22
+ # containing a randomized Time object as its first element (limited by
23
+ # the specified :year_range value). Subsequent elements will be Time objects
24
+ # with values putting them later than the prior element, within the specified
25
+ # range value (see examples).
26
+ #
27
+ # Usage Examples:
28
+ # @example
29
+ # a random date...
30
+ # ?> Time.random
31
+ # => Tue Aug 05 00:00:00 EDT 2007
32
+ #
33
+ # birthdays, anyone?...
34
+ # 5.times { p Time.random(:year_range=>80) }
35
+ # Wed Feb 06 00:00:00 EDT 1974
36
+ # Tue Dec 22 00:00:00 EST 1992
37
+ # Fri Apr 14 00:00:00 EWT 1944
38
+ # Thu Jul 01 00:00:00 EDT 1993
39
+ # Wed Oct 02 00:00:00 EDT 2002
40
+ #
41
+ # A series of dates are useful for account-related info...
42
+ # ?> Time.random(:series=>[20.days, 3.years])
43
+ # => [Sat Jan 22 00:00:00 EST 2005,
44
+ # Sat Jan 29 12:58:45 EST 2005,
45
+ # Fri Sep 08 09:34:58 EDT 2006]
46
+ #
47
+ # or maybe to simulate events during an hour?...
48
+ # ?> Time.random(:series=>[1.hour,1.hour,1.hour])
49
+ # => [Wed Apr 21 00:00:00 EDT 2004,
50
+ # Wed Apr 21 00:45:59 EDT 2004,
51
+ # Wed Apr 21 01:02:47 EDT 2004,
52
+ # Wed Apr 21 01:31:00 EDT 2004]
53
+ #
54
+ def self.random(params={})
55
+ years_back = params[:year_range] || 5
56
+ year = (rand * (years_back)).ceil + (Time.now.year - years_back)
57
+ month = (rand * 12).ceil
58
+ day = (rand * 31).ceil
59
+ series = [date = Time.local(year, month, day)]
60
+ if params[:series]
61
+ params[:series].each do |some_time_after|
62
+ series << series.last + (rand * some_time_after).ceil
63
+ end
64
+ return series
65
+ end
66
+ date
67
+ end
68
+
69
+ end # Time
70
+
71
+ module Enumerable
72
+
73
+ # Use for getting a natural sort order instead of the ASCII
74
+ # sort order.
75
+ #
76
+ def alphabetize
77
+ sort { |a, b| grouped_compare(a, b) }
78
+ end
79
+
80
+ # Use for sorting an Enumerable object in place.
81
+ #
82
+ def alphabetize!
83
+ sort! { |a, b| grouped_compare(a, b) }
84
+ end
85
+
86
+ private
87
+
88
+ def grouped_compare(a, b)
89
+ loop {
90
+ a_chunk, a = extract_alpha_or_number_group(a)
91
+ b_chunk, b = extract_alpha_or_number_group(b)
92
+ ret = a_chunk <=> b_chunk
93
+ return -1 if a_chunk == ''
94
+ return ret if ret != 0
95
+ }
96
+ end
97
+
98
+ def extract_alpha_or_number_group(item)
99
+ test_item = item.downcase
100
+ matchdata = /([a-z]+|[\d]+)/.match(test_item)
101
+ if matchdata.nil?
102
+ ["", ""]
103
+ else
104
+ [matchdata[0], test_item = test_item[matchdata.offset(0)[1] .. -1]]
105
+ end
106
+ end
107
+
108
+ end # Enumerable
109
+
110
+ class Numeric
111
+
112
+ # Converts a number object to a string containing commas every 3rd digit. Adds
113
+ # trailing zeroes if necessary to match round currency amounts.
114
+ def commas
115
+ self.to_s =~ /([^\.]*)(\..*)?/
116
+ int, dec = $1.reverse, $2 ? ('%.2f' % $2).to_s[/.\d+$/] : ".00"
117
+ while int.gsub!(/(,|\.|^)(\d{3})(\d)/, '\1\2,\3')
118
+ end
119
+ int.reverse + dec
120
+ end
121
+
122
+ end # Numeric
123
+
124
+ class String
125
+
126
+ # Used to remove commas and dollar signs from long number strings,
127
+ # then converting the result to a Float so that it
128
+ # can be used in calculations.
129
+ def groom
130
+ self.gsub(/[$,]/,'').to_f
131
+ end
132
+
133
+ end # String
@@ -0,0 +1,334 @@
1
+ # Copyright 2012-2014 The rSmart Group, Inc.
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # The Superclass for all of your data objects.
16
+ class DataFactory
17
+
18
+ include Foundry
19
+ extend Forwardable
20
+
21
+ # Since Data Objects are not "Marshallable", and they generally
22
+ # contain lots of types of data in their instance variables,
23
+ # we have this method. This will create and return a 'deep copy' of
24
+ # the data object as well as any and all nested data objects
25
+ # and collections it contains.
26
+ #
27
+ # Please note that this method will fail if you are putting
28
+ # Data Objects into Arrays or Hashes instead
29
+ # of into Collection classes
30
+ #
31
+ def data_object_copy
32
+ opts = {}
33
+ self.instance_variables.each do |var|
34
+ key = var.to_s.gsub('@','').to_sym
35
+ orig_val = instance_variable_get var
36
+ opts[key] = case
37
+ when orig_val.kind_of?(CollectionsFactory)
38
+ orig_val.copy
39
+ when orig_val.instance_of?(Array) || orig_val.instance_of?(Hash)
40
+ begin
41
+ Marshal::load(Marshal.dump(orig_val))
42
+ rescue TypeError
43
+ raise %{\nKey: #{key.inspect}\nValue: #{orig_val.inspect}\nClass: #{orig_val.class}\n\nThe copying of the Data Object has thrown a TypeError,\nwhich means the object detailed above is not "Marshallable".\nThe most likely cause is that you have put\na Data Object inside an\nArray or Hash.\nIf possible, put the Data Object into a Collection.\n\n}
44
+ end
45
+ when orig_val.kind_of?(DataFactory)
46
+ orig_val.data_object_copy
47
+ else
48
+ orig_val
49
+ end
50
+ end
51
+ self.class.new(@browser, opts)
52
+ end
53
+
54
+ # Add this to the bottom of your Data Object's initialize method.
55
+ # This method does 2 things:
56
+ # 1) Converts the contents of the hash into the class's instance variables.
57
+ # 2) Grabs the names of your collection class instance variables and stores
58
+ # them in an Array. This is to allow for the data object class to send
59
+ # any needed updates to its children. See #notify_coolections for more
60
+ # details.
61
+ # @param hash [Hash] Contains all options required for creating the needed Data Object
62
+ #
63
+ def set_options(hash)
64
+ @collections ||= []
65
+ hash.each do |key, value|
66
+ instance_variable_set("@#{key}", value)
67
+ @collections << key if value.kind_of?(CollectionsFactory)
68
+ end
69
+ end
70
+ alias update_options set_options
71
+
72
+ # Use for setting a data object's class instance variable as a nested collection class.
73
+ #
74
+ # This method assumes your collection class name ends with "Collection". Therefore,
75
+ # the string you pass in its parameter is the first part of the class name.
76
+ #
77
+ # E.g., your collection class is called "DataObjectCollection", so, inside your
78
+ # parent object's defaults, you'd set the instance variable like this:
79
+ #
80
+ # @example
81
+ # data_objects: collection('DataObject')
82
+ #
83
+ def collection(name)
84
+ Kernel.const_get("#{name}Collection").new(@browser)
85
+ end
86
+
87
+ # Items passed to this method are checked to ensure that the associated class instance variable
88
+ # is not nil. If it is, the script is aborted and an error is thrown. Use symbols separated
89
+ # by commas with this method. The symbol(s) should exactly match the name of the instance
90
+ # variable that must not be empty.
91
+ #
92
+ # NOTE: Currently this is backwards compatible with prior versions, which took the instance
93
+ # variables directly in the parameter. This backwards compatibility will be removed in
94
+ # some future update of the gem.
95
+ #
96
+ # @param elements [Array] the list of items that are required.
97
+ #
98
+ # @example
99
+ # requires :site, :assignment, :document_id
100
+ #
101
+ def requires(*elements)
102
+ elements.each do |inst_var|
103
+ if inst_var.kind_of? Symbol
104
+ string="@#{inst_var.to_s}"
105
+ if instance_variable_get(string)==nil
106
+ raise "You've neglected to define a required variable for your #{self.class}.\n\nPlease ensure you always specify a value for #{string} when you create the data object."
107
+ end
108
+ elsif inst_var.kind_of? String
109
+ warn "<<<<WARNING!>>>>\n\nPlease update the requires method in your\n#{self.class} class to refer to symbols\ninstead of directly referencing the class'\ninstance variables.\n\n Example:\n\n This...\n requires @document_id\n Should be updated to...\n requires :document_id\n\nIn future versions of TestFactory the 'requires'\nmethod will only support symbolized references\nto the instance variables. The backwards\ncompatibility will be removed.\n\n<<<<WARNING!>>>>"
110
+ elsif inst_var==nil
111
+ raise "You've neglected to define a required variable for your #{self.class}.\n\n<<<<WARNING!>>>>\n\nPlease update the requires method in your #{self} class to refer to symbols\ninstead of directly referencing the class'\ninstance variables.\n\nIn future versions of TestFactory the 'requires' method\nwill only support symbolized references\nto the instance variables. The backwards\ncompatibility will be removed.\n\n<<<<WARNING!>>>>"
112
+ end
113
+ end
114
+ end
115
+
116
+ # A shortcut method for filling out fields on a page. The
117
+ # method's first parameter is the page class that contains the fields
118
+ # you want to fill out. That is followed by the list of field name(s)
119
+ # (as Symbols).
120
+ #
121
+ # This method has a number of requirements:
122
+ #
123
+ # 1) The field name and the instance variable name in your data object
124
+ # must be identical. For this reason, this method can only
125
+ # be used in your data objects' create methods.
126
+ #
127
+ # 2) Your checkbox and radio button data object instance variables are
128
+ # either +nil+, +:set+, or +:clear+. Any other values will not be handled
129
+ # correctly.
130
+ #
131
+ # 3) Since the listed fields get filled out in random order, be sure that
132
+ # this is okay in the context of your page--in other words, if field A
133
+ # needs to be specified before field B then having them both in your
134
+ # fill_out step would be inappropriate. If you need a specific order,
135
+ # use #ordered_fill instead.
136
+ #
137
+ # 4) This method supports text fields, select lists, check boxes, and
138
+ # radio buttons, but only if their element definitions don't take a
139
+ # parameter. Please use the +#fill_out_item+ with elements that do need
140
+ # a parameter defined.
141
+ #
142
+ # @example
143
+ # on PageClass do |page|
144
+ # fill_out page, :text_field_name, :radio_name, :select_list_name, :checkbox_name
145
+ # end
146
+ #
147
+ def fill_out(page, *fields)
148
+ f_o_i true, nil, page, *fields
149
+ end
150
+
151
+ # Use when you need to specify the order that the fields should be
152
+ # updated.
153
+ #
154
+ # @example
155
+ # on PageClass do |page|
156
+ # ordered_fill page, :text_field_name, :radio_name, :select_list_name, :checkbox_name
157
+ # end
158
+ #
159
+ def ordered_fill(page, *fields)
160
+ f_o_i false, nil, page, *fields
161
+ end
162
+
163
+ # Same as #fill_out, but used with methods that take a
164
+ # parameter to identify the target element...
165
+ #
166
+ # @example
167
+ # on PageClass do |page|
168
+ # fill_out_item 'Joe Schmoe', page, :text_field_name, :radio_name, :select_list_name, :checkbox_name
169
+ # end
170
+ #
171
+ def fill_out_item(name, page, *fields)
172
+ f_o_i true, name, page, *fields
173
+ end
174
+
175
+ # Use instead of #fill_out_item when you need to
176
+ # specify the order that the fields should be
177
+ # updated.
178
+ #
179
+ # @example
180
+ # on PageClass do |page|
181
+ # ordered_item_fill 'Joe Schmoe', page, :text_field_name, :radio_name, :select_list_name, :checkbox_name
182
+ # end
183
+ #
184
+ def ordered_item_fill(name, page, *fields)
185
+ f_o_i false, name, page, *fields
186
+ end
187
+
188
+ # Equivalent to #ordered_fill, except that it's used
189
+ # in the context of a Data Object's #edit method(s). As such, it
190
+ # requires the #edit method's hash to be passed as its own
191
+ # first parameter.
192
+ #
193
+ # @example
194
+ # on PageClass do |page|
195
+ # edit_fields opts, page, :text_field_name, :radio_name, :select_list_name, :checkbox_name
196
+ # end
197
+ #
198
+ def edit_fields(opts, page, *fields)
199
+ edit_item_fields opts, nil, page, *fields
200
+ end
201
+
202
+ # Equivalent to #ordered_item_fill, except that it's used
203
+ # in the context of a Data Object's #edit method(s). As such, it
204
+ # requires the #edit method's hash to be passed as its own
205
+ # first parameter.
206
+ #
207
+ # @example
208
+ # on PageClass do |page|
209
+ # edit_item_fields opts, 'Joe Schmoe', page, :text_field_name, :radio_name, :select_list_name, :checkbox_name
210
+ # end
211
+ #
212
+ def edit_item_fields(opts, name, page, *fields)
213
+ parse_fields(opts, name, page, *fields)
214
+ end
215
+
216
+ # This is a specialized method for use with any select list boxes
217
+ # that exist in the site you're testing and will contain
218
+ # unpredictable default values.
219
+ #
220
+ # Admittedly, this is a bit unusual, but one example would be
221
+ # be a "due date" list that changes its default selection based
222
+ # on today's date. You're going to want to do one of two things
223
+ # with that select list:
224
+ #
225
+ # 1) Retrieve and store the select list's value
226
+ # 2) Specify a custom value to select
227
+ #
228
+ # Enter: +#get_or_select!+
229
+ #
230
+ # Assuming you just want to store the default value, then your
231
+ # Data Object's instance variable for the field will--initially--be
232
+ # nil. In that case, +#get_or_select!+ will grab the select list's
233
+ # current value and store it in your instance variable.
234
+ #
235
+ # On the other hand, if you want to update that field with your
236
+ # custom value, then your instance variable will not be nil, so
237
+ # +#get_or_select!+ will take that value and use it to update the
238
+ # select list.
239
+ #
240
+ # Note that this method *only* works with select lists that take
241
+ # a single selection. Multi-selects are not supported.
242
+ #
243
+ # Also note that the first parameter is *not* the instance variable
244
+ # you need to use/update. It is a *symbol* that otherwise matches
245
+ # the instance variable.
246
+ #
247
+ # @param inst_var_sym [Symbol] A Symbol that _must_ match the instance variable that
248
+ # will either be set or be used to update the page
249
+ # @param select_list [Watir::Select] The relevant select list element on the page
250
+ #
251
+ # @example
252
+ # get_or_select! :@num_resubmissions, page.num_resubmissions
253
+ #
254
+ def get_or_select!(inst_var_sym, select_list)
255
+ value = instance_variable_get inst_var_sym
256
+ if value==nil
257
+ instance_variable_set inst_var_sym, select_list.selected_options[0].text
258
+ else
259
+ select_list.select value
260
+ end
261
+ end
262
+
263
+ # This method accomplishes the same thing as #get_or_select! but
264
+ # is used specifically when the instance variable being used/updated
265
+ # is a Hash and you only need to update one of its key/value pairs.
266
+ #
267
+ # Pay close attention to the syntax differences between
268
+ # this method and #get_or_select!
269
+ #
270
+ # First, note that the returned value of this method must be explicitly
271
+ # passed to the relevant key in the Hash instance variable. Note also that, unlike
272
+ # #get_or_select!, this method does *not* take a symbolized representation
273
+ # of the instance variable.
274
+ #
275
+ # @example
276
+ # @open[:day] = get_or_select(@open[:day], page.open_day)
277
+ #
278
+ def get_or_select(hash_inst_var, select_list)
279
+ if hash_inst_var==nil
280
+ select_list.selected_options[0].text
281
+ else
282
+ select_list.select hash_inst_var
283
+ hash_inst_var
284
+ end
285
+ end
286
+
287
+ # Define this method in your data object when
288
+ # it has a parent, and that parent
289
+ # may periodically need to send
290
+ # it updated information about itself.
291
+ #
292
+ def update_from_parent(update)
293
+ raise %{
294
+ This method must be implemented in your data object
295
+ class if you plan to pass updates from a
296
+ parent object to the members of its
297
+ collections.
298
+ }
299
+ end
300
+
301
+ # =======
302
+ private
303
+ # =======
304
+
305
+ # Do not use this method directly.
306
+ #
307
+ def f_o_i(shuffle, name, page, *fields)
308
+ shuffle ? fields.shuffle! : fields
309
+ parse_fields(nil, name, page, *fields)
310
+ end
311
+
312
+ # Do not use this method directly.
313
+ #
314
+ def parse_fields(opts, name, page, *fields)
315
+ fields.each do |field|
316
+ lmnt = page.send(*[field, name].compact)
317
+ var = opts.nil? ? instance_variable_get("@#{field}") : opts[field]
318
+ lmnt.class.to_s == 'Watir::Select' ? lmnt.pick!(var) : lmnt.fit(var)
319
+ end
320
+ end
321
+
322
+ # Use this method in conjunction with your nested
323
+ # collection classes. The collections will notify
324
+ # their members about the passed parameters.
325
+ #
326
+ # Note: You must write a custom #update_from_parent
327
+ # method in your data object that will know what to
328
+ # do with the parameter(s) passed to it.
329
+ #
330
+ def notify_collections *updates
331
+ @collections.each {|coll| instance_variable_get("@#{coll}").notify_members *updates }
332
+ end
333
+
334
+ end