ams_layout 0.0.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.pryrc +8 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +3 -0
  6. data/Gemfile +12 -0
  7. data/Gemfile.lock +134 -0
  8. data/Guardfile +11 -0
  9. data/LICENSE +22 -0
  10. data/README.md +42 -0
  11. data/Rakefile +116 -0
  12. data/ams_layout.gemspec +32 -0
  13. data/bin/ams_layout +10 -0
  14. data/lib/ams_layout/browser_loader.rb +99 -0
  15. data/lib/ams_layout/cli/config.rb +250 -0
  16. data/lib/ams_layout/cli/generate.rb +145 -0
  17. data/lib/ams_layout/cli.rb +29 -0
  18. data/lib/ams_layout/client.rb +190 -0
  19. data/lib/ams_layout/core_ext/string.rb +30 -0
  20. data/lib/ams_layout/core_ext.rb +11 -0
  21. data/lib/ams_layout/delegate_writer.rb +348 -0
  22. data/lib/ams_layout/pages/login_page.rb +69 -0
  23. data/lib/ams_layout/pages/prequal_detail.rb +26 -0
  24. data/lib/ams_layout/pages.rb +36 -0
  25. data/lib/ams_layout/parser.rb +137 -0
  26. data/lib/ams_layout/version.rb +3 -0
  27. data/lib/ams_layout/writer.rb +124 -0
  28. data/lib/ams_layout.rb +198 -0
  29. data/spec/data/layout-small.yml +25 -0
  30. data/spec/data/layout-small.yml.aliases +22 -0
  31. data/spec/data/layout.yml +652 -0
  32. data/spec/data/layout.yml.aliases +613 -0
  33. data/spec/lib/ams_layout/ams_layout_spec.rb +7 -0
  34. data/spec/lib/ams_layout/cli/config_spec.rb +471 -0
  35. data/spec/lib/ams_layout/cli/generate_spec.rb +188 -0
  36. data/spec/lib/ams_layout/cli_spec.rb +35 -0
  37. data/spec/lib/ams_layout/client_spec.rb +93 -0
  38. data/spec/lib/ams_layout/delegate_writer_spec.rb +80 -0
  39. data/spec/lib/ams_layout/writer_spec.rb +64 -0
  40. data/spec/spec_helper.rb +7 -0
  41. data/spec/spec_helper_spec.rb +27 -0
  42. data/spec/support/asserts.rb +13 -0
  43. data/spec/support/dirs.rb +45 -0
  44. data/spec/support/helpers.rb +46 -0
  45. metadata +231 -0
@@ -0,0 +1,348 @@
1
+ ##############################################################################
2
+ # File:: delegate_writer.rb
3
+ # Purpose:: Generate a ruby source file that is a delegate class containing
4
+ # the fields on the Loan Entry screen.
5
+ #
6
+ # Author:: Jeff McAffee 06/23/2014
7
+ # Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
8
+ # Website:: http://ktechsystems.com
9
+ ##############################################################################
10
+
11
+ require 'ams_layout/core_ext'
12
+
13
+ module AmsLayout
14
+ class DelegateWriter
15
+
16
+ attr_writer :source_file_name
17
+ attr_writer :class_name
18
+ attr_writer :delegated_class_name
19
+ #attr_writer :aliases
20
+
21
+ ##
22
+ # Name of this class' source file
23
+ #
24
+
25
+ def source_file_name
26
+ class_name.ams_layout_snakecase + '.rb'
27
+ end
28
+
29
+ ##
30
+ # Name of this class
31
+ #
32
+
33
+ def class_name
34
+ @class_name ||= AmsLayout.configuration.delegate_class_name
35
+ end
36
+
37
+ ##
38
+ # Name of class we will delegate to
39
+ #
40
+
41
+ def delegated_class_name
42
+ @delegated_class_name ||= AmsLayout.configuration.layout_class_name
43
+ end
44
+
45
+ ##
46
+ # List of aliases for specified fields
47
+ #
48
+
49
+ def aliases
50
+ @aliases ||= {}
51
+ end
52
+
53
+ def aliases=(data)
54
+ @aliases = Hash(data)
55
+ end
56
+
57
+ ##
58
+ # Write a class file, based on the the layout.yml, to the provided stream
59
+ #
60
+
61
+ def write stream, layout
62
+ stream << header
63
+
64
+ layout.each do |section_label, fields|
65
+ stream << section(section_label)
66
+
67
+ fields.each do |fld|
68
+ stream << field(fld[:label], fld[:id], fld[:type])
69
+ write_aliases stream, fld[:label], fld[:id], fld[:type]
70
+ end # fields
71
+ end # layout
72
+
73
+ stream << footer
74
+ end
75
+
76
+ private
77
+
78
+ ##
79
+ # Write a field's aliases to the stream
80
+ #
81
+
82
+ def write_aliases stream, label, id, type
83
+ label_aliases = aliases.key?(label) ? aliases[label] : []
84
+ label_aliases.each do |al|
85
+ stream << field_alias(al, id, type)
86
+ end
87
+ end
88
+
89
+ ##
90
+ # Emit a file header
91
+ #
92
+
93
+ def header
94
+ text =<<TEXT
95
+ #############################################################################
96
+ # #{source_file_name}
97
+ #
98
+ # This file has been generated by AmsLayout.
99
+ # Do not modify this file manually.
100
+ #
101
+ #############################################################################
102
+
103
+ require_relative 'loan_entry_fields'
104
+ require 'nokogiri'
105
+
106
+ class #{class_name} < DelegateClass(#{delegated_class_name})
107
+
108
+ ##
109
+ # Capture and parse the page's raw HTML with Nokogiri
110
+ # Returns a Nokogiri::HTML document
111
+ #
112
+
113
+ def html_doc
114
+ @html_doc ||= Nokogiri::HTML(__getobj__.raw_html)
115
+ #Nokogiri::HTML(html).css("table#ctl00_ContentPlaceHolder1_tblMain").each do |tbl|
116
+ end
117
+
118
+ ##
119
+ # Return the first element matching the given ID
120
+ # Returns a Nokogiri Element
121
+ #
122
+
123
+ def noko_element(id)
124
+ html_doc.css(id).first
125
+ end
126
+
127
+ ##
128
+ # Returns true if element is checked
129
+ #
130
+
131
+ def checked?(id)
132
+ # el['type'] = 'checkbox'
133
+ is_checked = noko_element(id)['checked']
134
+ ! is_checked.nil?
135
+ end
136
+
137
+ ##
138
+ # Returns a text element's value
139
+ #
140
+
141
+ def get_text_element_value(id)
142
+ # el['type'] = 'text'
143
+ value = noko_element(id)['value']
144
+ end
145
+
146
+ ##
147
+ # Returns a textarea element's value
148
+ #
149
+
150
+ def get_textarea_element_value(id)
151
+ noko_element(id).text
152
+ end
153
+
154
+ ##
155
+ # Returns the selected option, '-- Please Select --' if no option is selected
156
+ #
157
+
158
+ def get_selected_option id
159
+ elem = noko_element(id)
160
+ elem.children.each do |c|
161
+ if c.attributes.key? 'selected'
162
+ return c.text
163
+ end
164
+ end
165
+
166
+ # Return the default value if nothing is selected
167
+ '-- Please Select --'
168
+ end
169
+
170
+ #
171
+ # Fields (ordered by section as seen on screen)
172
+ #
173
+ TEXT
174
+ end
175
+
176
+ ##
177
+ # Emit a section header
178
+ #
179
+
180
+ def section label
181
+ text =<<TEXT
182
+
183
+
184
+ # Section: #{label}
185
+ TEXT
186
+ end
187
+
188
+ ##
189
+ # Emit field methods
190
+ #
191
+
192
+ def field label, id, type
193
+ case type
194
+ when 'text'
195
+ return text_field_methods(label, id, type)
196
+ when 'textarea'
197
+ return textarea_field_methods(label, id, type)
198
+ when 'select'
199
+ return select_field_methods(label, id, type)
200
+ when 'checkbox'
201
+ return checkbox_field_methods(label, id, type)
202
+ else
203
+ return "\n# unknown_field_type: #{label}, #{id}, #{type}\n"
204
+ end
205
+ end
206
+
207
+ ##
208
+ # Emit field methods for aliased fields
209
+ #
210
+
211
+ def field_alias label, id, type
212
+ field label, id, type
213
+ end
214
+
215
+ ##
216
+ # Emit text field methods
217
+ #
218
+
219
+ def text_field_methods label, id, type
220
+ field_label = label.ams_layout_snakecase
221
+ field_id = '#' + id
222
+
223
+ if field_id.include? 'PlaceHolder1_cn'
224
+ return float_field_methods label, id, type
225
+ end
226
+
227
+ text =<<TEXT
228
+
229
+ def #{field_label}=(value)
230
+ if get_text_element_value('#{field_id}') != value
231
+ super(value)
232
+ # else skip sending the value.
233
+ end
234
+ end
235
+ TEXT
236
+ end
237
+
238
+ ##
239
+ # Emit float field methods
240
+ #
241
+
242
+ def float_field_methods label, id, type
243
+ field_label = label.ams_layout_snakecase
244
+ field_id = '#' + id
245
+
246
+ text =<<TEXT
247
+
248
+ def #{field_label}=(value)
249
+ current_value = get_text_element_value('#{field_id}')
250
+
251
+ current_value = '0' if current_value.empty?
252
+ value = '0' if value.empty?
253
+
254
+ if ((Float(current_value) * 1000) != (Float(value) * 1000))
255
+ super(value)
256
+ # else skip sending the value.
257
+ end
258
+ end
259
+ TEXT
260
+ end
261
+
262
+ ##
263
+ # Emit textarea field methods
264
+ #
265
+
266
+ def textarea_field_methods label, id, type
267
+ field_label = label.ams_layout_snakecase
268
+ field_id = '#' + id
269
+
270
+ text =<<TEXT
271
+
272
+ def #{field_label}=(value)
273
+ if get_textarea_element_value('#{field_id}') != value
274
+ super(value)
275
+ # else skip sending the value.
276
+ end
277
+ end
278
+ TEXT
279
+ end
280
+
281
+ ##
282
+ # Emit select field methods
283
+ #
284
+
285
+ def select_field_methods label, id, type
286
+ field_label = label.ams_layout_snakecase
287
+ field_id = '#' + id
288
+
289
+ text =<<TEXT
290
+
291
+ def #{field_label}=(value)
292
+ if get_selected_option('#{field_id}') != value
293
+ super(value)
294
+ # else skip sending the value.
295
+ end
296
+ end
297
+ TEXT
298
+ end
299
+
300
+ ##
301
+ # Emit checkbox field methods
302
+ #
303
+
304
+ def checkbox_field_methods label, id, type
305
+ field_label = label.ams_layout_snakecase
306
+ field_id = '#' + id
307
+
308
+ text =<<TEXT
309
+
310
+ def check_#{field_label}
311
+ if ! checked?('#{field_id}')
312
+ super()
313
+ # else skip sending the value.
314
+ end
315
+ end
316
+
317
+ def uncheck_#{field_label}
318
+ if checked?('#{field_id}')
319
+ super()
320
+ # else skip sending the value.
321
+ end
322
+ end
323
+ TEXT
324
+ end
325
+
326
+ ##
327
+ # Emit file footer
328
+ #
329
+
330
+ def footer
331
+ text =<<TEXT
332
+
333
+ end # #{class_name}
334
+
335
+ TEXT
336
+ end
337
+
338
+ ##
339
+ # Convert a field name to snake_case
340
+ #
341
+
342
+ def delme_snakecase str
343
+ snake = str.gsub /[^a-zA-Z0-9]/, '_'
344
+ snake = snake.gsub /_+/, '_'
345
+ snake.downcase
346
+ end
347
+ end # DelegateWriter
348
+ end # AmsLayout
@@ -0,0 +1,69 @@
1
+ ##############################################################################
2
+ # File:: login_page.rb
3
+ # Purpose:: AMS Login page
4
+ #
5
+ # Author:: Jeff McAffee 06/22/2014
6
+ # Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
7
+ # Website:: http://ktechsystems.com
8
+ ##############################################################################
9
+
10
+ require 'page-object'
11
+
12
+ module AmsLayout
13
+ module Pages
14
+
15
+ class LoginPage
16
+ include PageObject
17
+
18
+ page_url( AmsLayout.configuration.base_url )
19
+
20
+ text_field(:username, :id => "ctl00_ContentPlaceHolder1_Login1_txtUserName" )
21
+ text_field(:password_mask, :id => "ctl00_ContentPlaceHolder1_Login1_txtPasswordMask" )
22
+ text_field(:password, :id => "ctl00_ContentPlaceHolder1_Login1_txtPassword" )
23
+ button(:login, :id => "ctl00_ContentPlaceHolder1_Login1_btnLogin" )
24
+
25
+ # Part of the main menu visible after login
26
+ link(:pipelines, :text => "Pipelines")
27
+
28
+ def login_as(username, password)
29
+ self.username = username
30
+ allow_password_entry
31
+ self.password = password
32
+ login
33
+
34
+ trys = 0
35
+ # Make sure we wait until the (sometimes) slow login is finished.
36
+ while trys < 10 && !self.text.include?('LOAN PIPELINE')
37
+ trys += 1
38
+ sleep 1
39
+ end
40
+ end
41
+
42
+ def logout
43
+ navigate_to page_url_value + '/User/AppLogout.aspx'
44
+ end
45
+
46
+ def logged_in?
47
+ !self.username?
48
+ end
49
+
50
+ def allow_password_entry
51
+ # We used to have to click on the password mask before the page would let us enter the password itself:
52
+ #
53
+ # # For some unknown reason, we must click on a password mask input before
54
+ # # we can access the password field itself.
55
+ # password_mask_element.click
56
+ #
57
+ # Now, we have to use javascript to hide the mask and display the password field.
58
+ hide_mask_script = <<EOS
59
+ pwdmasks = document.getElementsByClassName('passwordmask');
60
+ pwdmasks[0].style.display = 'none';
61
+ pwds = document.getElementsByClassName('password');
62
+ pwds[0].style.display = 'block';
63
+ EOS
64
+
65
+ @browser.execute_script(hide_mask_script)
66
+ end
67
+ end # class LoginPage
68
+ end # Pages
69
+ end # AmsLayout
@@ -0,0 +1,26 @@
1
+ ##############################################################################
2
+ # File:: prequal_detail.rb
3
+ # Purpose:: Loan Entry (flow) for AMS Portal
4
+ #
5
+ # Author:: Jeff McAffee 2014-06-21
6
+ # Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
7
+ # Website:: http://ktechsystems.com
8
+ ##############################################################################
9
+
10
+ require 'page-object'
11
+
12
+ module AmsLayout
13
+ module Pages
14
+
15
+ class PrequalDetail
16
+ include PageObject
17
+
18
+ page_url(::AmsLayout.configuration.base_url + '/SubmitLoan/PrequalDetail.aspx')
19
+
20
+ def html
21
+ @browser.html
22
+ end
23
+ end
24
+ end # Pages
25
+ end # AmsLayout
26
+
@@ -0,0 +1,36 @@
1
+ ##############################################################################
2
+ # File:: pages.rb
3
+ # Purpose:: Require all Page classes
4
+ #
5
+ # Author:: Jeff McAffee 2014-06-21
6
+ # Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
7
+ # Website:: http://ktechsystems.com
8
+ ##############################################################################
9
+
10
+ require 'ams_layout/browser_loader'
11
+ require 'ams_layout/pages/login_page'
12
+ require 'ams_layout/pages/prequal_detail'
13
+
14
+ module AmsLayout
15
+ module Pages
16
+
17
+ ##
18
+ # Return a configured browser object. If a browser has already been created,
19
+ # this returns the existing browser.
20
+ #
21
+ # An +at_exit+ proc is created to close the browser when the program exits.
22
+
23
+ def browser
24
+ if @browser.nil?
25
+ BrowserLoader.user_data_path = AmsLayout.configuration.user_data_path
26
+ @browser = BrowserLoader.init_browser AmsLayout.configuration.browser_timeout
27
+
28
+ at_exit do
29
+ @browser.close unless @browser.nil?
30
+ end
31
+ end
32
+
33
+ @browser
34
+ end
35
+ end # Pages
36
+ end # AmsLayout
@@ -0,0 +1,137 @@
1
+ ##############################################################################
2
+ # File:: parser.rb
3
+ # Purpose:: Parse a Loan Entry screen and generate the layout data for the
4
+ # controls on the screen.
5
+ #
6
+ # Author:: Jeff McAffee 06/21/2014
7
+ # Copyright:: Copyright (c) 2014, kTech Systems LLC. All rights reserved.
8
+ # Website:: http://ktechsystems.com
9
+ ##############################################################################
10
+
11
+ require 'nokogiri'
12
+
13
+ module AmsLayout
14
+ class Parser
15
+
16
+ def layout
17
+ @layout ||= {}
18
+ end
19
+
20
+ def parse html
21
+ # Table: ctl00_ContentPlaceHolder1_tblMain
22
+ Nokogiri::HTML(html).css("table#ctl00_ContentPlaceHolder1_tblMain").each do |tbl|
23
+ tbl.css('tr').each do |row|
24
+ parse_row row
25
+ end
26
+ end
27
+
28
+ layout
29
+ end
30
+
31
+ private
32
+ def parse_row row
33
+ cells = row.css('td')
34
+
35
+ if cells[0]['class'] == 'sectionheader'
36
+ add_section cells[0].text
37
+ return
38
+ end
39
+
40
+ if cells.size >= 2
41
+ add_control build_control(cells[0], cells[1])
42
+ end
43
+
44
+ if cells.size >= 4
45
+ add_control build_control(cells[2], cells[3])
46
+ end
47
+ end
48
+
49
+ def add_section section_name
50
+ @current_section = section_name
51
+ end
52
+
53
+ def current_section
54
+ fail "@current_section not set" if @current_section.nil?
55
+
56
+ if layout[@current_section].nil?
57
+ layout[@current_section] = Array.new
58
+ end
59
+
60
+ layout[@current_section]
61
+ end
62
+
63
+ def add_control control
64
+ current_section << control.to_hash unless control.is_a?(NullControl)
65
+ end
66
+
67
+ def build_control label_cell, control_cell
68
+ assert_label_td label_cell
69
+ assert_control_td control_cell
70
+
71
+ label = label_cell.text.gsub("\u00A0", ' ').strip
72
+ # Return if this is a blank cell
73
+ return NullControl.new if label.empty?
74
+
75
+ # Actual control is nested INPUT field
76
+ ctrl = find_input_element(control_cell)
77
+ if ctrl.nil?
78
+ fail "Unable to determine input element.\n#{control_cell.inspect}"
79
+ end
80
+ id = ctrl['id']
81
+ type = ctrl['type']
82
+ type = ctrl.name if type.nil?
83
+ assert_control_type type
84
+ Control.new label, id, type
85
+ end
86
+
87
+ def find_input_element cell
88
+ ctrl = cell.css('input').first
89
+ ctrl = cell.css('select').first if ctrl.nil?
90
+ ctrl = cell.css('textarea').first if ctrl.nil?
91
+ ctrl
92
+ end
93
+
94
+ def assert_label_td cell
95
+ label_classes = %w[LabelTD Mandatory]
96
+ fail "TD does not contain class attribute of 'LabelTD'" unless label_classes.include?(cell['class'])
97
+ end
98
+
99
+ def assert_control_td cell
100
+ fail "TD does not contain class attribute of 'ControlTD'" unless cell['class'] == 'ControlTD'
101
+ end
102
+
103
+ def assert_control_type type
104
+ fail 'nil control type' if type.nil?
105
+ fail "unexpected control type: #{type}" unless %w[text textarea select checkbox].include?(type)
106
+ end
107
+
108
+ class Control
109
+ attr_reader :label, :id, :type
110
+
111
+ def initialize label, id, type
112
+ @label = label
113
+ @id = id
114
+ @type = type
115
+ end
116
+
117
+ def to_hash
118
+ {label: @label, id: @id, type: @type}
119
+ end
120
+ end # Control
121
+
122
+ class NullControl
123
+ attr_reader :label, :id, :type
124
+
125
+ def initialize
126
+ @label = ''
127
+ @id = ''
128
+ @type = ''
129
+ end
130
+
131
+ def to_hash
132
+ {label: @label, id: @id, type: @type}
133
+ end
134
+ end # Control
135
+ end # Parser
136
+ end # AmsLayout
137
+
@@ -0,0 +1,3 @@
1
+ module AmsLayout
2
+ VERSION = "0.0.2"
3
+ end