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.
- data/History.txt +3 -0
- data/Manifest.txt +51 -13
- data/Rakefile +6 -1
- data/builtin/controllers/rtml_controller.rb +12 -1
- data/builtin/models/rtml/document.rb +24 -18
- data/builtin/models/rtml/document_model_object.rb +6 -0
- data/builtin/models/rtml/dom/collections/element_set.rb +6 -0
- data/builtin/models/rtml/dom/collections/property_set.rb +11 -0
- data/builtin/models/rtml/dom/element.rb +79 -27
- data/builtin/models/rtml/dom/frontend_element.rb +41 -20
- data/builtin/models/rtml/dom/property.rb +43 -26
- data/builtin/models/rtml/dom/screen_element.rb +13 -0
- data/builtin/widgets/document_variable_processing.rb +42 -18
- data/builtin/widgets/element_builder.rb +4 -4
- data/builtin/widgets/screen_variable_processing.rb +2 -2
- data/builtin/widgets/screen_variants.rb +31 -4
- data/builtin/widgets/screens.rb +13 -1
- data/builtin/widgets/static_content.rb +20 -6
- data/do_profile.rb +15 -0
- data/lib/extensions/action_controller/response.rb +0 -18
- data/lib/extensions/action_controller/routing/route_set.rb +28 -18
- data/lib/extensions/hpricot/doc.rb +3 -3
- data/lib/extensions/hpricot/elem.rb +3 -3
- data/lib/extensions/string.rb +2 -18
- data/lib/rtml.rb +0 -12
- data/lib/rtml/assigns.rb +32 -0
- data/lib/rtml/controller/document_generator.rb +9 -0
- data/lib/rtml/controller/render_helpers.rb +42 -18
- data/lib/rtml/controller/state.rb +2 -1
- data/lib/rtml/dependencies.rb +20 -15
- data/lib/rtml/dsl.rb +10 -10
- data/lib/rtml/environment.rb +13 -1
- data/lib/rtml/errors/application_error.rb +5 -0
- data/lib/rtml/errors/simulation_error.rb +4 -0
- data/lib/rtml/errors/variable_error.rb +5 -0
- data/lib/rtml/high_level/variable_manager.rb +11 -7
- data/lib/rtml/inherited_instance_variables.rb +8 -1
- data/lib/rtml/links.rb +17 -0
- data/lib/rtml/rules/dom_validation.rb +1 -0
- data/lib/rtml/test/builtin_variables.rb +33 -0
- data/lib/rtml/test/resemblance_test.rb +97 -0
- data/lib/rtml/test/screen.rb +126 -0
- data/lib/rtml/test/simulator.rb +240 -0
- data/lib/rtml/test/simulator_post_processors/base.rb +7 -0
- data/lib/rtml/test/simulator_post_processors/card_parsers.rb +32 -0
- data/lib/rtml/test/simulator_post_processors/receipt.rb +15 -0
- data/lib/rtml/test/simulator_post_processors/submit.rb +15 -0
- data/lib/rtml/test/spec.rb +14 -7
- data/lib/rtml/test/spec/matchers.rb +24 -0
- data/lib/rtml/test/tml_application.rb +331 -0
- data/lib/rtml/test/unit.rb +13 -0
- data/lib/rtml/test/variable_scope.rb +146 -0
- data/lib/rtml/version.rb +1 -1
- data/lib/rtml/widget.rb +26 -14
- data/lib/rtml/widget_core/class_methods.rb +8 -4
- data/lib/rtml/widget_core/widget_accessor_instance_methods.rb +6 -6
- data/lib/rtml/widgets.rb +22 -3
- data/lib/rtml_routes.rb +1 -1
- data/rails_generators/rtml/rtml_generator.rb +3 -0
- data/rails_generators/rtml/templates/db/migrate/20100513165226_add_options_to_rtml_documents.rb +9 -0
- data/rails_generators/rtml/templates/db/migrate/20100513165242_remove_dom_elements_mirror.rb +16 -0
- data/rails_generators/rtml/templates/db/migrate/20100513165249_remove_dom_properties_mirror.rb +16 -0
- data/rails_generators/rtml/templates/lib/tasks/rtml.rake +1 -1
- data/rtml.gemspec +65 -0
- data/spec/controllers/rtml_controller_spec.rb +1 -1
- data/spec/integration/post_tests_spec.rb +8 -0
- data/spec/lib/rtml/high_level/variable_manager_spec.rb +8 -0
- data/spec/lib/rtml/routes_spec.rb +23 -22
- data/spec/lib/rtml/test/simulator/receipt_spec.rb +18 -0
- data/spec/lib/rtml/test/simulator_spec.rb +185 -0
- data/spec/lib/rtml/test/tml_application_spec.rb +119 -0
- data/spec/lib/rtml/test/variable_scope_spec.rb +65 -0
- data/spec/lib/rtml/widget_spec.rb +1 -0
- data/spec/lib/rtml/widgets_spec.rb +30 -0
- data/spec/models/rtml/document_spec.rb +8 -0
- data/spec/models/rtml/dom/screen_element_spec.rb +15 -0
- data/spec/models/rtml/instruction_spec.rb +2 -2
- data/spec/rtml_action_spec.rb +25 -0
- data/spec/spec_helper.rb +31 -1
- data/spec/support/app/controllers/post_tests_controller.rb +11 -0
- data/spec/support/app/views/inherited/instance_variables_test/display.rtml.erb +1 -0
- data/spec/support/config/boot.rb +1 -0
- data/spec/support/config/routes.rb +3 -2
- data/spec/support/db/rtml_test_db.sqlite3 +0 -0
- data/spec/support/raw_tml/avs.tml +27 -0
- data/spec/support/raw_tml/document_level_events.tml +18 -0
- data/spec/support/raw_tml/empty_screen.tml +15 -0
- data/spec/support/raw_tml/enter_amount.tml +40 -0
- data/spec/support/raw_tml/foreign_receiver.tml +10 -0
- data/spec/support/raw_tml/foreign_reference.tml +10 -0
- data/spec/support/raw_tml/hello_world.tml +13 -0
- data/spec/support/raw_tml/loop_x_times.tml +39 -0
- data/spec/support/raw_tml/one_screen_with_setvar.tml +8 -0
- data/spec/support/raw_tml/receipt.tml +15 -0
- data/spec/support/raw_tml/simulator.tml +122 -0
- data/spec/support/raw_tml/tmlvar_reference.tml +34 -0
- data/spec/support/raw_tml/user_input.tml +47 -0
- data/spec/support/raw_tml/valid_document.tml +6 -0
- data/spec/support/rspec/example_groups.rb +1 -1
- data/spec/support/rspec/matchers.rb +0 -11
- data/spec/widgets/document_variable_processing_spec.rb +25 -39
- data/spec/widgets/element_builder_spec.rb +4 -0
- data/spec/widgets/event_listener_spec.rb +9 -0
- data/spec/widgets/highlevel_variable_processing_spec.rb +27 -2
- data/spec/widgets/screen_variable_processing_spec.rb +34 -0
- data/spec/widgets/screens_spec.rb +22 -0
- data/spec/widgets/simulator_post_processors/card_parsers_spec.rb +70 -0
- data/spec/widgets/simulator_post_processors/submit_spec.rb +44 -0
- data/tasks/stats.rake +10 -0
- data/test/test_rtml_generator.rb +3 -0
- metadata +55 -49
- data/builtin/widgets/subroutine.rb +0 -54
- data/lib/rtml/high_level/subroutine.rb +0 -22
- data/lib/rtml/reverse_engineering/crawler.rb +0 -58
- data/lib/rtml/reverse_engineering/simulator.rb +0 -269
- data/lib/rtml/reverse_engineering/simulator/casting.rb +0 -9
- data/lib/rtml/reverse_engineering/simulator/snapshot.rb +0 -18
- data/lib/rtml/reverse_engineering/simulator/variable_lookup.rb +0 -32
- data/lib/rtml/reverse_engineering/simulator/variable_value.rb +0 -105
- data/spec/lib/rtml/reverse_engineering/crawler_spec.rb +0 -24
- data/spec/lib/rtml/reverse_engineering/simulator/variable_value_spec.rb +0 -120
- data/spec/lib/rtml/reverse_engineering/simulator_spec.rb +0 -96
- data/spec/support/config/tml_dom_ruleset.rb +0 -82
- 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
|
|
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
|
data/lib/rtml/links.rb
ADDED
|
@@ -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
|