rtml 2.0.3 → 2.0.4

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 (124) hide show
  1. data/History.txt +3 -0
  2. data/Manifest.txt +51 -13
  3. data/Rakefile +6 -1
  4. data/builtin/controllers/rtml_controller.rb +12 -1
  5. data/builtin/models/rtml/document.rb +24 -18
  6. data/builtin/models/rtml/document_model_object.rb +6 -0
  7. data/builtin/models/rtml/dom/collections/element_set.rb +6 -0
  8. data/builtin/models/rtml/dom/collections/property_set.rb +11 -0
  9. data/builtin/models/rtml/dom/element.rb +79 -27
  10. data/builtin/models/rtml/dom/frontend_element.rb +41 -20
  11. data/builtin/models/rtml/dom/property.rb +43 -26
  12. data/builtin/models/rtml/dom/screen_element.rb +13 -0
  13. data/builtin/widgets/document_variable_processing.rb +42 -18
  14. data/builtin/widgets/element_builder.rb +4 -4
  15. data/builtin/widgets/screen_variable_processing.rb +2 -2
  16. data/builtin/widgets/screen_variants.rb +31 -4
  17. data/builtin/widgets/screens.rb +13 -1
  18. data/builtin/widgets/static_content.rb +20 -6
  19. data/do_profile.rb +15 -0
  20. data/lib/extensions/action_controller/response.rb +0 -18
  21. data/lib/extensions/action_controller/routing/route_set.rb +28 -18
  22. data/lib/extensions/hpricot/doc.rb +3 -3
  23. data/lib/extensions/hpricot/elem.rb +3 -3
  24. data/lib/extensions/string.rb +2 -18
  25. data/lib/rtml.rb +0 -12
  26. data/lib/rtml/assigns.rb +32 -0
  27. data/lib/rtml/controller/document_generator.rb +9 -0
  28. data/lib/rtml/controller/render_helpers.rb +42 -18
  29. data/lib/rtml/controller/state.rb +2 -1
  30. data/lib/rtml/dependencies.rb +20 -15
  31. data/lib/rtml/dsl.rb +10 -10
  32. data/lib/rtml/environment.rb +13 -1
  33. data/lib/rtml/errors/application_error.rb +5 -0
  34. data/lib/rtml/errors/simulation_error.rb +4 -0
  35. data/lib/rtml/errors/variable_error.rb +5 -0
  36. data/lib/rtml/high_level/variable_manager.rb +11 -7
  37. data/lib/rtml/inherited_instance_variables.rb +8 -1
  38. data/lib/rtml/links.rb +17 -0
  39. data/lib/rtml/rules/dom_validation.rb +1 -0
  40. data/lib/rtml/test/builtin_variables.rb +33 -0
  41. data/lib/rtml/test/resemblance_test.rb +97 -0
  42. data/lib/rtml/test/screen.rb +126 -0
  43. data/lib/rtml/test/simulator.rb +240 -0
  44. data/lib/rtml/test/simulator_post_processors/base.rb +7 -0
  45. data/lib/rtml/test/simulator_post_processors/card_parsers.rb +32 -0
  46. data/lib/rtml/test/simulator_post_processors/receipt.rb +15 -0
  47. data/lib/rtml/test/simulator_post_processors/submit.rb +15 -0
  48. data/lib/rtml/test/spec.rb +14 -7
  49. data/lib/rtml/test/spec/matchers.rb +24 -0
  50. data/lib/rtml/test/tml_application.rb +331 -0
  51. data/lib/rtml/test/unit.rb +13 -0
  52. data/lib/rtml/test/variable_scope.rb +146 -0
  53. data/lib/rtml/version.rb +1 -1
  54. data/lib/rtml/widget.rb +26 -14
  55. data/lib/rtml/widget_core/class_methods.rb +8 -4
  56. data/lib/rtml/widget_core/widget_accessor_instance_methods.rb +6 -6
  57. data/lib/rtml/widgets.rb +22 -3
  58. data/lib/rtml_routes.rb +1 -1
  59. data/rails_generators/rtml/rtml_generator.rb +3 -0
  60. data/rails_generators/rtml/templates/db/migrate/20100513165226_add_options_to_rtml_documents.rb +9 -0
  61. data/rails_generators/rtml/templates/db/migrate/20100513165242_remove_dom_elements_mirror.rb +16 -0
  62. data/rails_generators/rtml/templates/db/migrate/20100513165249_remove_dom_properties_mirror.rb +16 -0
  63. data/rails_generators/rtml/templates/lib/tasks/rtml.rake +1 -1
  64. data/rtml.gemspec +65 -0
  65. data/spec/controllers/rtml_controller_spec.rb +1 -1
  66. data/spec/integration/post_tests_spec.rb +8 -0
  67. data/spec/lib/rtml/high_level/variable_manager_spec.rb +8 -0
  68. data/spec/lib/rtml/routes_spec.rb +23 -22
  69. data/spec/lib/rtml/test/simulator/receipt_spec.rb +18 -0
  70. data/spec/lib/rtml/test/simulator_spec.rb +185 -0
  71. data/spec/lib/rtml/test/tml_application_spec.rb +119 -0
  72. data/spec/lib/rtml/test/variable_scope_spec.rb +65 -0
  73. data/spec/lib/rtml/widget_spec.rb +1 -0
  74. data/spec/lib/rtml/widgets_spec.rb +30 -0
  75. data/spec/models/rtml/document_spec.rb +8 -0
  76. data/spec/models/rtml/dom/screen_element_spec.rb +15 -0
  77. data/spec/models/rtml/instruction_spec.rb +2 -2
  78. data/spec/rtml_action_spec.rb +25 -0
  79. data/spec/spec_helper.rb +31 -1
  80. data/spec/support/app/controllers/post_tests_controller.rb +11 -0
  81. data/spec/support/app/views/inherited/instance_variables_test/display.rtml.erb +1 -0
  82. data/spec/support/config/boot.rb +1 -0
  83. data/spec/support/config/routes.rb +3 -2
  84. data/spec/support/db/rtml_test_db.sqlite3 +0 -0
  85. data/spec/support/raw_tml/avs.tml +27 -0
  86. data/spec/support/raw_tml/document_level_events.tml +18 -0
  87. data/spec/support/raw_tml/empty_screen.tml +15 -0
  88. data/spec/support/raw_tml/enter_amount.tml +40 -0
  89. data/spec/support/raw_tml/foreign_receiver.tml +10 -0
  90. data/spec/support/raw_tml/foreign_reference.tml +10 -0
  91. data/spec/support/raw_tml/hello_world.tml +13 -0
  92. data/spec/support/raw_tml/loop_x_times.tml +39 -0
  93. data/spec/support/raw_tml/one_screen_with_setvar.tml +8 -0
  94. data/spec/support/raw_tml/receipt.tml +15 -0
  95. data/spec/support/raw_tml/simulator.tml +122 -0
  96. data/spec/support/raw_tml/tmlvar_reference.tml +34 -0
  97. data/spec/support/raw_tml/user_input.tml +47 -0
  98. data/spec/support/raw_tml/valid_document.tml +6 -0
  99. data/spec/support/rspec/example_groups.rb +1 -1
  100. data/spec/support/rspec/matchers.rb +0 -11
  101. data/spec/widgets/document_variable_processing_spec.rb +25 -39
  102. data/spec/widgets/element_builder_spec.rb +4 -0
  103. data/spec/widgets/event_listener_spec.rb +9 -0
  104. data/spec/widgets/highlevel_variable_processing_spec.rb +27 -2
  105. data/spec/widgets/screen_variable_processing_spec.rb +34 -0
  106. data/spec/widgets/screens_spec.rb +22 -0
  107. data/spec/widgets/simulator_post_processors/card_parsers_spec.rb +70 -0
  108. data/spec/widgets/simulator_post_processors/submit_spec.rb +44 -0
  109. data/tasks/stats.rake +10 -0
  110. data/test/test_rtml_generator.rb +3 -0
  111. metadata +55 -49
  112. data/builtin/widgets/subroutine.rb +0 -54
  113. data/lib/rtml/high_level/subroutine.rb +0 -22
  114. data/lib/rtml/reverse_engineering/crawler.rb +0 -58
  115. data/lib/rtml/reverse_engineering/simulator.rb +0 -269
  116. data/lib/rtml/reverse_engineering/simulator/casting.rb +0 -9
  117. data/lib/rtml/reverse_engineering/simulator/snapshot.rb +0 -18
  118. data/lib/rtml/reverse_engineering/simulator/variable_lookup.rb +0 -32
  119. data/lib/rtml/reverse_engineering/simulator/variable_value.rb +0 -105
  120. data/spec/lib/rtml/reverse_engineering/crawler_spec.rb +0 -24
  121. data/spec/lib/rtml/reverse_engineering/simulator/variable_value_spec.rb +0 -120
  122. data/spec/lib/rtml/reverse_engineering/simulator_spec.rb +0 -96
  123. data/spec/support/config/tml_dom_ruleset.rb +0 -82
  124. data/spec/widgets/subroutine_spec.rb +0 -109
@@ -2,7 +2,14 @@ module Rtml::InheritedInstanceVariables
2
2
  def copy_ivars_from(source)
3
3
  ivars = source.instance_variables
4
4
  ivars -= source.protected_instance_variables if source.respond_to?(:protected_instance_variables)
5
- ivars.each { |ivar| instance_variable_set(ivar, source.instance_variable_get(ivar)) }
5
+ ivars -= source.rtml_protected_instance_variables if source.respond_to?(:rtml_protected_instance_variables)
6
+ ivars.each do |ivar|
7
+ # as time consuming as this may be, it's still faster than eval("defined?(#{ivar})")
8
+ # -- and easier to read, too.
9
+ if !instance_variables.include?(ivar)
10
+ instance_variable_set(ivar, source.instance_variable_get(ivar))
11
+ end
12
+ end
6
13
  rescue
7
14
  # fail silently if they can't be copied
8
15
  end
@@ -0,0 +1,17 @@
1
+ module Rtml::Links
2
+ # str_or_sym is the string or symbol to find
3
+ # id_ref is a boolean: if true, then # is prepended where necessary
4
+ def id_for(str_or_sym)
5
+ tml_id = Rtml::Dom::Element.find_or_create_tml_id(str_or_sym)
6
+
7
+ if local?(tml_id)
8
+ "##{tml_id}"
9
+ else
10
+ tml_id
11
+ end
12
+ end
13
+
14
+ def local?(str_or_sym)
15
+ !(str_or_sym.kind_of?(String) && (str_or_sym =~ /\// || str_or_sym =~ /#/ || str_or_sym =~ /:/) || str_or_sym.blank?)
16
+ end
17
+ end
@@ -139,6 +139,7 @@ module Rtml::Rules::DomValidation
139
139
  tag_rules = rules.tag_rules(tag_name)
140
140
  prev_index = -1
141
141
  element.children.each do |child|
142
+ next if child.kind_of?(Hpricot::Text) || child.kind_of?(Hpricot::Comment)
142
143
  chn = child.name
143
144
  prn = '[no element]'
144
145
  index = tag_rules.order.index(child.name)
@@ -0,0 +1,33 @@
1
+ module Rtml
2
+ module Test
3
+ module BuiltinVariables
4
+ def self.add_variable(name, options) #:nodoc:
5
+ options = { :type => options } if !options.kind_of?(Hash)
6
+ BUILTIN_VARIABLES[name] = options
7
+ end
8
+
9
+ BUILTIN_VARIABLES = {}
10
+ add_variable('card.cardholder_name', :string)
11
+ add_variable('card.effective_date', :date)
12
+ add_variable('card.expiry_date', :date)
13
+ add_variable('card.input_type', :integer)
14
+ add_variable('card.issue_number', :integer)
15
+ add_variable('card.issuer_name', :string)
16
+ add_variable('card.scheme', :string)
17
+ add_variable('card.pan', :integer)
18
+ add_variable('card.parser.type', :string)
19
+ add_variable('card.parser.verdict', :string)
20
+ add_variable('card.parser.reject_reason', :string)
21
+
22
+ add_variable('payment.amount', :integer)
23
+ add_variable('payment.amount_other', :integer)
24
+ BUILTIN_VARIABLES.freeze
25
+
26
+ def declare_builtin_variables!
27
+ BUILTIN_VARIABLES.each do |name, options|
28
+ declare_variable(name, options)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,97 @@
1
+ # Used to see if a specified TML structure (given in the form of a Hash) resembles the
2
+ # overall structure of the given TML document.
3
+ #
4
+ # This is used to validate that the document conforms to a particular set of expectations
5
+ # without the overhead of loading it into the simulator.
6
+ #
7
+ # This class is tied into RSpec with the #resemble_tml method, as well as into Test::Unit
8
+ # with the #assert_tml_resembles method:
9
+ #
10
+ # RSpec Example
11
+ #
12
+ # get :index
13
+ # response.should resemble_tml(:tml => { :screen => [ :setvar, :submit => { :getvar } ] })
14
+ #
15
+ # response.should_not resemble_tml(. . .) # negative test
16
+ #
17
+ # Test::Unit Examples
18
+ #
19
+ # get :index
20
+ # assert_tml_resembles(:tml => { :screen => [ :setvar, :submit => { :getvar } ] }, response)
21
+ #
22
+ # assert_tml_not_resemble(. . .) # negative test
23
+ #
24
+ class Rtml::Test::ResemblanceTest
25
+ attr_reader :tml, :expectation
26
+
27
+ def pass?
28
+ pass_recursively?
29
+ end
30
+
31
+ def initialize(expectation, tml)
32
+ build_expectation(expectation)
33
+ find_tml(tml)
34
+ @xml = Hpricot::XML(@tml).root
35
+ end
36
+
37
+ private
38
+ def pass_recursively?(exp = @expectation.deep_dup, level = @xml)
39
+ case exp
40
+ when Hash
41
+ pass_recursively_with_hash?(exp, level)
42
+ when Array
43
+ pass_with_array?(exp, level)
44
+ else
45
+ pass_with_object?(exp, level)
46
+ end
47
+ end
48
+
49
+ def pass_recursively_with_hash?(exp, level)
50
+ if key_matches = pass_with_array?(exp.keys, level)
51
+ key_matches.each do |match|
52
+ if array = pass_with_array?(exp.values, match)
53
+ return array
54
+ end
55
+ end
56
+ end
57
+ false
58
+ end
59
+
60
+ def pass_with_array?(exp, level)
61
+ matches = []
62
+ exp.each do |item|
63
+ return false unless object_matches = pass_recursively?(item, level)
64
+ matches.concat object_matches
65
+ end
66
+ matches
67
+ end
68
+
69
+ def pass_with_object?(exp, level)
70
+ if exp.to_s == level.name
71
+ [level]
72
+ elsif (matches = (level / exp)) && !matches.empty?
73
+ matches
74
+ else
75
+ false
76
+ end
77
+ end
78
+
79
+ def build_expectation(hash)
80
+ @expectation = hash
81
+ end
82
+
83
+ def find_tml(object)
84
+ @tml = case object
85
+ when ActionController::TestResponse
86
+ object.body
87
+ when String
88
+ object
89
+ when Hpricot::Doc, Hpricot::Elem
90
+ object.to_s
91
+ when ActiveRecord::Base, Rtml::DocumentModelObject
92
+ object.to_tml
93
+ else
94
+ object.to_s
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,126 @@
1
+ # Used for modeling the current screen by Rtml::Test::TmlApplication.
2
+ class Rtml::Test::Screen
3
+ delegate :[], :[]=, :/, :to_s, :to => :screen
4
+
5
+ def initialize(tml)
6
+ @screen = tml
7
+ end
8
+
9
+ def ==(screen)
10
+ id == screen.id
11
+ end
12
+
13
+ def id
14
+ @screen['id']
15
+ end
16
+
17
+ def display?
18
+ !((@screen / "display") || []).empty?
19
+ end
20
+
21
+ def variant_with_hotkey(which)
22
+ possible_variants.each { |variant| return variant[:uri] if variant[:key] == which }
23
+ nil
24
+ end
25
+
26
+ def card_readers
27
+ ((@screen / "card") || [])
28
+ end
29
+
30
+ def uri_for_hotkey(which)
31
+ if variant = variant_with_hotkey(which)
32
+ return variant
33
+ end
34
+ return @screen[which] if @screen[which]
35
+ nil
36
+ end
37
+
38
+ # Returns the +next+ element for this screen, or nil if there is none.
39
+ def next_element
40
+ ((@screen / "next") || []).shift
41
+ end
42
+
43
+ # Returns the hyperlink Hpricot element, if there's only one; nil if there are none; and raises an
44
+ # Rtml::Errors::ApplicationError if there are multiple links (because there's no inference as to which link to
45
+ # follow).
46
+ def autoselect_hyperlink
47
+ if !(links = hyperlinks).empty?
48
+ if links.length > 1
49
+ raise Rtml::Errors::ApplicationError, "Cannot auto-select hyperlink: too many choices on screen #{id.inspect}"
50
+ end
51
+ links.shift
52
+ else
53
+ nil
54
+ end
55
+ end
56
+
57
+ def hyperlinks
58
+ (@screen / "a") || []
59
+ end
60
+
61
+ # Returns true if this screen is an input screen (that is, if it will halt program flow
62
+ # waiting for user input).
63
+ def input?
64
+ return true if !choices.empty?
65
+ Rtml::Test::TmlApplication::USER_INPUT_ELEMENTS.each { |ele| return true if !(@screen / ele).empty? }
66
+ false
67
+ end
68
+
69
+ def timeout
70
+ return @screen['timeout'].to_i if @screen['timeout']
71
+ nxt = ((@screen / "next") || []).shift
72
+ if nxt
73
+ return nxt['timeout'].to_i if nxt['timeout']
74
+ ((nxt / "variant") || []).each do |var|
75
+ return var['timeout'].to_i if var['timeout']
76
+ end
77
+ end
78
+ 0
79
+ end
80
+
81
+ def setvars
82
+ ((@screen / "setvar") || [])
83
+ end
84
+
85
+ # Returns an array of URIs that can be chosen by the user at this time, or an empty
86
+ # array if none can be chosen. This inversely coincides with #next_screen in that choices
87
+ # are only available if #next_screen returns an empty array, just as #next_screen will only
88
+ # return an empty array if choices are available.
89
+ def choices
90
+ choices = (@screen / "a").collect do |a|
91
+ a['href']
92
+ end
93
+ ((@screen / "variant") || []).each do |variant|
94
+ choices << variant['uri'] if variant['key']
95
+ end
96
+ choices
97
+ end
98
+
99
+
100
+ # Returns an array of Hashes representing all possible non-user interaction-requiring variants
101
+ # from this screen. These are returned regardless of whether user interation is required at this time.
102
+ #
103
+ # The hashes are laid out thusly:
104
+ # { :uri => "destination_uri", :key => "hotkey", :timeout => "seconds", :lo => "left_operand",
105
+ # :op => "operation", :ro => "right_operand" }
106
+ #
107
+ # Note that some of these keys may be omitted, according to the TML rules for the +variant+ element.
108
+ #
109
+ # For TML +next+ elements, all keys except :uri are omitted.
110
+ #
111
+ def possible_variants
112
+ variants = ((@screen / "variant") || []).collect do |variant|
113
+ { :uri => variant['uri'], :key => variant['key'], :timeout => variant['timeout'],
114
+ :lo => variant['lo'], :op => variant['op'], :ro => variant['ro'] }.optionalize
115
+ end
116
+ if !(nxt = (@screen / "next") || []).empty?
117
+ variants << { :uri => nxt.first['uri'] }
118
+ else
119
+ variants << { :uri => @screen['next'] }
120
+ end
121
+ variants
122
+ end
123
+
124
+ private
125
+ attr_reader :screen
126
+ end
@@ -0,0 +1,240 @@
1
+ # FIXME: Is there an easier way to load these other than just force feeding the filenames?
2
+ Rtml::Test::SimulatorPostProcessors::CardParsers
3
+ Rtml::Test::SimulatorPostProcessors::Submit
4
+ Rtml::Test::SimulatorPostProcessors::Receipt
5
+
6
+ class Rtml::Test::Simulator
7
+ delegate :response, :request, :controller, :to => :session
8
+ delegate :screens, :current_screen, :current_screen_id, :next_screen, :next_screen_id,
9
+ :waiting_for_input?, :find_screen, :process, :continue, :receipt,
10
+ :to => :app
11
+ attr_reader :app, :session
12
+
13
+ # Accepts a path to the TML document which should be requested. To pass raw TML,
14
+ # use this syntax:
15
+ # Rtml::Test::Simulator.new(:tml => my_tml_code)
16
+ def initialize(start_at = nil)
17
+ @session = ActionController::Integration::Session.new
18
+ @variables = Rtml::Test::VariableScope.new
19
+ @headers = HashWithIndifferentAccess.new
20
+ if start_at.kind_of?(Hash)
21
+ load_tml! start_at[:tml]
22
+ else
23
+ visit(start_at) unless start_at.nil?
24
+ end
25
+ end
26
+
27
+ def post_data(location, data = {})
28
+ post(location, data, @headers)
29
+ load_tml!(response.body)
30
+ end
31
+
32
+ def header(name, value)
33
+ @headers[name] = value
34
+ end
35
+
36
+ def headers
37
+ @headers.dup
38
+ end
39
+
40
+ # Instead of using "visit", this just force-feeds some TML into the simulator. Useful for
41
+ # hard-coded, quick-and-dirty debug situations, but far less useful for testing a real app
42
+ # or a large document.
43
+ def load_tml!(tml)
44
+ @path = nil
45
+ build_app(tml)
46
+ process
47
+ end
48
+
49
+ def process(forward = false, &block)
50
+ # FIXME: TODO: Refactor, this is too ugly!
51
+ if block_given?
52
+ uri = catch(:new_document) { yield; nil }
53
+ else
54
+ uri = catch(:new_document) do
55
+ if forward
56
+ app.continue_forward
57
+ else
58
+ app.continue
59
+ end
60
+ nil
61
+ end
62
+ end
63
+
64
+ if uri
65
+ if uri =~ /^tmlvar\:(.*)$/ # it's a TML variable referencing a screen ID
66
+ uri = variables[$~[1]]
67
+ if uri.blank?
68
+ raise Rtml::Errors::SimulationError, "Empty screen reference: tmlvar #{$~[1]} on screen #{current_screen_id}"
69
+ end
70
+ end
71
+ if uri == @path && current_screen_id == app.screens.first['id']
72
+ raise Rtml::Errors::SimulationError,
73
+ "Circular foreign reference: #{@path}##{current_screen_id}"
74
+ end
75
+ visit_uri(uri)
76
+ end
77
+
78
+ do_post_processing!
79
+ end
80
+
81
+ def visit(path, options = {})
82
+ options.reverse_merge! default_options
83
+ path = "##{path}" if path.kind_of?(Symbol)
84
+ if File.file?(path)
85
+ build_app File.read(@path = path)
86
+ else
87
+ path, screen = path_and_screen_from path
88
+ if !path.blank? && @path != path
89
+ get_and_build(path)
90
+ else
91
+ app.screenflow.clear
92
+ end
93
+ if screen && !screen.blank? && screen != '#'
94
+ app.jump_to_screen screen
95
+ end
96
+ end
97
+ process if options[:process]
98
+ end
99
+
100
+ # Visits the specified URI.
101
+ # This merely wraps a call to #visit, but is called internally by #process when following
102
+ # a direction to visit a new URI outside of the current document. This makes it useful for
103
+ # setting up test expectations because you can make sure that #visit_uri is (or isn't)
104
+ # called without worrying about breaking #visit.
105
+ def visit_uri(uri)
106
+ visit(uri)
107
+ end
108
+
109
+ def follow_link(path_or_text)
110
+ on_current_screen("a").each do |hyperlink|
111
+ if hyperlink['href'] == path_or_text || hyperlink.inner_text =~ /#{Regexp::escape path_or_text}/i
112
+ visit hyperlink['href']
113
+ return
114
+ end
115
+ end
116
+ raise Rtml::Errors::SimulationError,
117
+ "Hyperlink not found on screen #{current_screen_id.inspect}: #{path_or_text.inspect}"
118
+ end
119
+
120
+ def press(which_button)
121
+ process do
122
+ app.trigger_button_press(which_button)
123
+ process # FIXME: is this even necessary?
124
+ end
125
+ end
126
+
127
+ # carrier can be any of :visa, :mastercard, :jcb, :amex, :discover, and :debit.
128
+ # if it's anything else, the 'card.pan' and 'card.scheme' options must be specified.
129
+ def swipe_card(carrier, options = {})
130
+ options.reverse_merge!('card.cardholder_name' => 'Cardholder',
131
+ 'card.effective_date' => Time.now,
132
+ 'card.expiry_date' => 1.year.from_now,
133
+ 'card.input_type' => 1,
134
+ 'card.issue_number' => 0,
135
+ 'card.issuer_name' => 'NA',
136
+ 'card.scheme' => carrier.to_s.upcase,
137
+ 'card.pan' => case carrier
138
+ when :visa then '4111111111111111'
139
+ when :mastercard then '5454545454545454'
140
+ when :amex then '3434343434343434'
141
+ when :discover then '6011000012345678'
142
+ when :jcb then '3083000012345678'
143
+ when :debit then '1000000012345678'
144
+ else nil
145
+ end
146
+ )
147
+
148
+ unless options['card.pan'] && options['card.scheme']
149
+ raise Rtml::Errors::SimulationError, "Carrier not recognized and/or options are invalid"
150
+ end
151
+
152
+ card_parsers = on_current_screen("card")
153
+ card_parsers.each do |card|
154
+ if card['parser'] == 'mag' && card['parser_params'] == 'read_data'
155
+ app.update_variables(options)
156
+ process(:forward)
157
+ return
158
+ end
159
+ end
160
+ raise Rtml::Errors::SimulationError,
161
+ "Could not find a suitable card parser (with parser 'mag' and parser_params 'read_data') on screen #{current_screen_id.inspect}"
162
+ end
163
+
164
+ def fill_in(variable_name, value)
165
+ variable_name = variable_name.to_s unless variable_name.kind_of?(String)
166
+ on_current_screen("input").each do |field|
167
+ if field['name'] == variable_name
168
+ variables[variable_name] = value
169
+ return
170
+ end
171
+ end
172
+ raise Rtml::Errors::SimulationError,
173
+ "Form element for variable #{variable_name.inspect} does not appear on screen #{current_screen_id.inspect}"
174
+ end
175
+
176
+ def variables
177
+ app.variable_scope
178
+ end
179
+
180
+ private
181
+ delegate :get, :post, :put, :delete, :to => :session
182
+
183
+ def default_options
184
+ {
185
+ :process => true
186
+ }
187
+ end
188
+
189
+ def on_current_screen(field_name)
190
+ current_screen ? ((current_screen / field_name) || []) :
191
+ raise(Rtml::Errors::SimulationError, "Current screen was expected to not be nil")
192
+ end
193
+
194
+ def build_app(tml)
195
+ @app = Rtml::Test::TmlApplication.new(tml)
196
+ end
197
+
198
+ def get_and_build(path)
199
+ # set up state mocking
200
+ unless path =~ /ignore_state/
201
+ if path =~ /\?/
202
+ path.concat "&ignore_state=true"
203
+ else
204
+ path.concat "?ignore_state=true"
205
+ end
206
+ end
207
+
208
+ get @path = path, {}, @headers
209
+ if response # actually the only time there's no response should be during tests which stub #get
210
+ format = response.template.template_format
211
+ unless format == :rtml
212
+ raise Rtml::Errors::InvalidFormat, "Response must be in :rtml format; found #{format.inspect}:\n#{response.body}"
213
+ end
214
+ build_app(response.body)
215
+ end
216
+ end
217
+
218
+ def path_and_screen_from(path)
219
+ path, screen = path.split(/#/)
220
+ # query string
221
+ if screen =~ /\?/
222
+ screen, query = screen.split(/\?/)
223
+ path = "#{path}?#{query}"
224
+ end
225
+ [path, screen]
226
+ end
227
+
228
+ include Rtml::Widgets
229
+
230
+ # we're almost done. App is likely in a "waiting" state; we need to see if it's "waiting"
231
+ # for something that the simulator can, er, simulate, such as card risk management or
232
+ # EMV processing. If it is, then we need to fill out whatever variables are expected, and
233
+ # then send another call to #process.
234
+ def do_post_processing!
235
+ return unless current_screen
236
+ self.widget_entry_points.each do |entry_point|
237
+ send(entry_point)
238
+ end
239
+ end
240
+ end