tapestry 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ module Tapestry
2
+ module Errors
3
+ NoUrlForDefinition = Class.new(StandardError)
4
+ NoUrlMatchForDefinition = Class.new(StandardError)
5
+ NoTitleForDefinition = Class.new(StandardError)
6
+ NoUrlMatchPossible = Class.new(StandardError)
7
+ PageNotValidatedError = Class.new(StandardError)
8
+ NoBlockForWhenReady = Class.new(StandardError)
9
+
10
+ class PageURLFromFactoryNotVerified < StandardError
11
+ def message
12
+ 'The page URL was not verified during a factory setup.'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,106 @@
1
+ class Object
2
+ # This method is necessary to dynamically chain method calls. The reason
3
+ # this is necessary the data setter initially has no idea of the actual
4
+ # object it's going to be dealing with, particularly because part of its
5
+ # job is to find that object and map a data string to it. Not only this,
6
+ # but that element will have been called on a specific instance of a
7
+ # interface class. With the example provide in the comments below for the
8
+ # `using` method, the following would be the case:
9
+ #
10
+ # method_chain: warp_factor.set
11
+ # o (object): <WarpTravel:0x007f8b23224218>
12
+ # m (method): warp_factor
13
+ # data: 1
14
+ #
15
+ # Thus what you end up with is:
16
+ #
17
+ # <WarpTravel:0x007f8b23224218>.warp_factor.set 1
18
+ def chain(method_chain, data = nil)
19
+ return self if method_chain.empty?
20
+ method_chain.split('.').inject(self) do |o, m|
21
+ if data.nil?
22
+ o.send(m.intern)
23
+ else
24
+ o.send(m.intern, data)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ module Tapestry
31
+ module DataSetter
32
+ # The `using` method tells Tapestry to match up whatever data is passed
33
+ # in via the action with element definitions. If those elements are found,
34
+ # they will be populated with the specified data. Consider the following:
35
+ #
36
+ # class WarpTravel
37
+ # include Tapestry
38
+ #
39
+ # text_field :warp_factor, id: 'warpInput'
40
+ # text_field :velocity, id: 'velocityInput'
41
+ # text_field :distance, id: 'distInput'
42
+ # end
43
+ #
44
+ # Assuming an instance of this class called `page`, you could do the
45
+ # following:
46
+ #
47
+ # page.using_data(warp_factor: 1, velocity: 1, distance: 4.3)
48
+ #
49
+ # This is based on conventions. The idea is that element definitions are
50
+ # written in the form of "snake case" -- meaning, underscores between
51
+ # each separate word. In the above example, "warp_factor: 1" would be
52
+ # matched to the `warp_factor` element and the value used for that
53
+ # element would be "1". The default operation for a text field is to
54
+ # enter the value in. It is also possible to use strings:
55
+ #
56
+ # page.using_data("warp factor": 1, velocity: 1, distance: 4.3)
57
+ #
58
+ # Here "warp factor" would be converted to "warp_factor".
59
+ def using(data)
60
+ data.each do |key, value|
61
+ use_data_with(key, value) if object_enabled_for(key)
62
+ end
63
+ end
64
+
65
+ alias using_data using
66
+ alias use_data using
67
+ alias using_values using
68
+ alias use_values using
69
+
70
+ private
71
+
72
+ # This is the method that is delegated to in order to make sure that
73
+ # elements are interacted with appropriately. This will in turn delegate
74
+ # to `set_and_select` and `check_and_uncheck`, which determines what
75
+ # actions are viable based on the type of element that is being dealt
76
+ # with. These aspects are what tie this particular implementation to
77
+ # Watir.
78
+ def use_data_with(key, value)
79
+ element = send(key.to_s.tr(' ', '_'))
80
+ set_and_select(key, element, value)
81
+ check_and_uncheck(key, element, value)
82
+ end
83
+
84
+ def set_and_select(key, element, value)
85
+ key = key.to_s.tr(' ', '_')
86
+ chain("#{key}.set", value) if element.class == Watir::TextField
87
+ chain("#{key}.set") if element.class == Watir::Radio
88
+ chain("#{key}.select", value) if element.class == Watir::Select
89
+ end
90
+
91
+ def check_and_uncheck(key, element, value)
92
+ key = key.to_s.tr(' ', '_')
93
+ return chain("#{key}.check") if element.class == Watir::CheckBox && value
94
+ chain("#{key}.uncheck") if element.class == Watir::CheckBox
95
+ end
96
+
97
+ # This is a sanity check method to make sure that whatever element is
98
+ # being used as part of the data setting, it exists in the DOM, is
99
+ # visible (meaning, display is not 'none'), and is capable of accepting
100
+ # input, thus being enabled.
101
+ def object_enabled_for(key)
102
+ web_element = send(key.to_s.tr(' ', '_'))
103
+ web_element.enabled? && web_element.visible?
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,78 @@
1
+ /*
2
+ This functionality will only work for browsers that support it.
3
+ See: http://caniuse.com/#feat=mutationobserver
4
+ */
5
+
6
+ // WebDriver arguments, which are passed to the MutationObserver.
7
+ var element = arguments[0];
8
+ var delay = arguments[1] * 1000;
9
+ var callback = arguments[2];
10
+
11
+ /*
12
+ The two functions below are similar in what they are doing. Both are
13
+ disconneting the observer. Both are also invoking WebDriver's callback
14
+ function.
15
+
16
+ notStartedUpdating passes true to the callback, which indicates that the
17
+ DOM has not yet begun updating.
18
+
19
+ startedUpdating passes false to the callback, which indicates that the
20
+ DOM has begun updating.
21
+
22
+ The disconnect is important. You only want to be listening (observing) for the
23
+ period required, removing the listeners when done. Since there be many DOM
24
+ operations, you want to disconnet when there is interaction with the page by
25
+ the automated scripts.
26
+
27
+ When observing a node for changes, the callback will not be fired until the
28
+ DOM has finished changing. That is the only granularity that is required for
29
+ the Tapestry implementation. What specific events occurred is not important
30
+ because the goal is not to conditionally respond to them; rather just to know
31
+ when the process has completed.
32
+ */
33
+ var notStartedUpdating = function() {
34
+ return setTimeout(function() {
35
+ observer.disconnect();
36
+ callback(true);
37
+ }, 1000);
38
+ };
39
+
40
+ var startedUpdating = function() {
41
+ clearTimeout(notStartedUpdating);
42
+ observer.disconnect();
43
+ callback(false);
44
+ };
45
+
46
+ /*
47
+ Mutation Observer
48
+ The W3C DOM4 specification initially introduced mutation observers as a
49
+ replacement for the deprecated mutation events.
50
+
51
+ The MutationObserver is a JavaScript native object that allows for observing
52
+ a change on any node-like DOM Element. "Mutation" means the addition or the
53
+ removal of a node as well as changes to the node's attribute and data.
54
+
55
+ The general approach is to create a MutationObserver object with a defined
56
+ callback function. The function will execute on every mutation observed by
57
+ the MutationObserver. The MutationObserver must be bound to a target, which
58
+ for Tapestry would mean the element whose context it is being called upon.
59
+
60
+ A MutationObserver can be provided with a set of options, which indicate
61
+ what kind of events should be observed.
62
+
63
+ The childList option checks for additions and removals of the target node's
64
+ child elements, including text nodes. This is basically looking for any
65
+ nodes added or removed from documentElement.
66
+
67
+ The subtree option checks for mutations to the target as well the target's
68
+ descendants. So that means every child node of documentElement.
69
+
70
+ The attribute option checks for mutations to the target's attributes.
71
+
72
+ The characterData option checks for mutations to the target's data.
73
+ */
74
+ var observer = new MutationObserver(startedUpdating);
75
+ var config = { attributes: true, childList: true, characterData: true, subtree: true };
76
+ observer.observe(element, config);
77
+
78
+ var notStartedUpdating = notStartedUpdating();
@@ -0,0 +1,74 @@
1
+ module Watir
2
+ class Element
3
+ OBSERVER_FILE = "/dom_observer.js".freeze
4
+ DOM_OBSERVER = File.read("#{File.dirname(__FILE__)}#{OBSERVER_FILE}").freeze
5
+
6
+ # This method makes a call to `execute_async_script` which means that the
7
+ # DOM observer script must explicitly signal that it is finished by
8
+ # invoking a callback. In this case, the callback is nothing more than
9
+ # a delay. The delay is being used to allow the DOM to be updated before
10
+ # script actions continue.
11
+ #
12
+ # The method returns true if the DOM has been changed within the element
13
+ # context, while false means that the DOM has not yet finished changing.
14
+ # Note the wording: "has not finished changing." It's known that the DOM
15
+ # is changing because the observer has recognized that. So the question
16
+ # this method is helping to answer is "has it finished?"
17
+ #
18
+ # Consider the following element definition:
19
+ #
20
+ # p :page_list, id: 'navlist'
21
+ #
22
+ # You could then do this:
23
+ #
24
+ # page_list.dom_updated?
25
+ #
26
+ # That would return true if the DOM content for page_list has finished
27
+ # updating. If the DOM was in the process of being updated, this would
28
+ # return false. You could also do this:
29
+ #
30
+ # page_list.wait_until(&:dom_updated?).click
31
+ #
32
+ # This will use Watir's wait until functionality to wait for the DOM to
33
+ # be updated within the context of the element. Note that the "&:" is
34
+ # that the object that `dom_updated?` is being called on (in this case
35
+ # `page_list`) substitutes the ampersand. You can also structure it like
36
+ # this:
37
+ #
38
+ # page_list.wait_until do |element|
39
+ # element.dom_updated?
40
+ # end
41
+ #
42
+ # The default delay of waiting for the DOM to start updating is 1.1
43
+ # second. However, you can pass a delay value when you call the method
44
+ # to set your own value, which can be useful for particular sensitivities
45
+ # in the application you are testing.
46
+ def dom_updated?(delay: 1.1)
47
+ driver.manage.timeouts.script_timeout = delay + 1
48
+ driver.execute_async_script(DOM_OBSERVER, wd, delay)
49
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError
50
+ # This situation can occur when the DOM changes between two calls to
51
+ # some element or aspect of the page. In this case, we are expecting
52
+ # the DOM to be different so what's being handled here are those hard
53
+ # to anticipate race conditions when "weird things happen" and DOM
54
+ # updating plus script execution get interleaved.
55
+ retry
56
+ rescue Selenium::WebDriver::Error::JavascriptError => e
57
+ # This situation can occur if the script execution has started before
58
+ # a new page is fully loaded. The specific error being checked for here
59
+ # is one that occurs when a new page is loaded as that page is trying
60
+ # to execute a JavaScript function.
61
+ retry if e.message.include?('document unloaded while waiting for result')
62
+ raise
63
+ ensure
64
+ # Note that this setting here means any user-defined timeout would
65
+ # effectively be overwritten.
66
+ driver.manage.timeouts.script_timeout = 1
67
+ end
68
+
69
+ alias dom_has_updated? dom_updated?
70
+ alias dom_has_changed? dom_updated?
71
+ alias when_dom_updated dom_updated?
72
+ alias when_dom_changed dom_updated?
73
+ end
74
+ end
@@ -0,0 +1,16 @@
1
+ module Watir
2
+ class CheckBox
3
+ alias check set
4
+ alias uncheck clear
5
+ alias checked? set?
6
+ end
7
+
8
+ class Radio
9
+ alias choose set
10
+ alias chosen? set?
11
+ end
12
+
13
+ class TextField
14
+ alias enter set
15
+ end
16
+ end
@@ -0,0 +1,92 @@
1
+ module Tapestry
2
+ module Factory
3
+ # Creates a definition context for actions and establishes the context
4
+ # for execution. Given an interface definition for a page like this:
5
+ #
6
+ # class TestPage
7
+ # include Tapestry
8
+ #
9
+ # url_is "http://localhost:9292"
10
+ # end
11
+ #
12
+ # You can do the following:
13
+ #
14
+ # on_view(TestPage)
15
+ #
16
+ # Note that the actual factory creation is handled by `on`. This method
17
+ # exists as a way to differentiate when an interface needs to be
18
+ # visited.
19
+ def on_view(definition, &block)
20
+ on(definition, true, &block)
21
+ end
22
+
23
+ alias on_visit on_view
24
+
25
+ # Creates a definition context for actions. If an existing context
26
+ # exists, that context will be re-used. You can use this simply to keep
27
+ # the context for a script clear. For example, say you have the following
28
+ # interface definitions for pages:
29
+ #
30
+ # class Home
31
+ # include Tapestry
32
+ # url_is "http://localhost:9292"
33
+ # end
34
+ #
35
+ # class Navigation
36
+ # include Tapestry
37
+ # end
38
+ #
39
+ # You could then do this:
40
+ #
41
+ # on_view(Home)
42
+ # on(Navigation)
43
+ #
44
+ # The Home definition needs the url_is attribute in order for the on_view
45
+ # factory to work. But Navigation does not because the `on` method is not
46
+ # attempting to visit, simply to reference. Note that you can use `on`
47
+ # to visit, just by doing this:
48
+ #
49
+ # on(Home, true)
50
+ def on(definition, visit = false, &block)
51
+ unless @context.is_a?(definition)
52
+ @context = definition.new(@browser) if @browser
53
+ @context = definition.new unless @browser
54
+ @context.visit if visit
55
+ end
56
+
57
+ verify_page(@context)
58
+
59
+ yield @context if block
60
+ @context
61
+ end
62
+
63
+ # Creates a definition context for actions. Unlike the `on` factory, the
64
+ # `on_new` factory will always create a new context and will never re-use
65
+ # an existing one. The reason for using this factory might be that you
66
+ # are on the same page, but a given action has changed it so much that
67
+ # you want to reference it as a new version of that page, meaning a new
68
+ # context is established.
69
+ #
70
+ # It's doubtful that you will want to rely on this factory too much.
71
+ def on_new(definition, &block)
72
+ @context = nil
73
+ on(definition, &block)
74
+ end
75
+
76
+ alias on_page on
77
+ alias while_on on
78
+
79
+ private
80
+
81
+ # This method is used to provide a means for checking if a page has been
82
+ # navigated to correctly as part of a context. This is useful because
83
+ # the context signature should remain highly readable, and checks for
84
+ # whether a given page has been reached would make the context definition
85
+ # look sloppy.
86
+ def verify_page(context)
87
+ return if context.url_match_attribute.nil?
88
+ return if context.has_correct_url?
89
+ raise Tapestry::Errors::PageURLFromFactoryNotVerified
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,203 @@
1
+ require "tapestry/situation"
2
+
3
+ module Tapestry
4
+ module Interface
5
+ module Page
6
+ include Situation
7
+
8
+ # The `visit` method provides navigation to a specific page by passing
9
+ # in the URL. If no URL is passed in, this method will attempt to use
10
+ # the `url_is` attribute from the interface it is being called on.
11
+ def visit(url = nil)
12
+ no_url_provided if url.nil? && url_attribute.nil?
13
+ @browser.goto(url) unless url.nil?
14
+ @browser.goto(url_attribute) if url.nil?
15
+ self
16
+ end
17
+
18
+ alias view visit
19
+ alias navigate_to visit
20
+ alias goto visit
21
+ alias perform visit
22
+
23
+ # A call to `url_attribute` returns what the value of the `url_is`
24
+ # attribute is for the given interface. It's important to note that
25
+ # this is not grabbing the URL that is displayed in the browser;
26
+ # rather it's the one declared in the interface, if any.
27
+ def url_attribute
28
+ self.class.url_attribute
29
+ end
30
+
31
+ # A call to `url_match_attribute` returns what the value of the
32
+ # `url_matches` attribute is for the given interface. It's important
33
+ # to note that the URL matching mechanism is effectively a regular
34
+ # expression check.
35
+ def url_match_attribute
36
+ value = self.class.url_match_attribute
37
+ return if value.nil?
38
+ value = Regexp.new(value) unless value.is_a?(Regexp)
39
+ value
40
+ end
41
+
42
+ # A call to `title_attribute` returns what the value of the `title_is`
43
+ # attribute is for the given definition. It's important to note that
44
+ # this is not grabbing the title that is displayed in the browser;
45
+ # rather it's the one declared in the interface, if any.
46
+ def title_attribute
47
+ self.class.title_attribute
48
+ end
49
+
50
+ # A call to `has_correct_url?`returns true or false if the actual URL
51
+ # found in the browser matches the `url_matches` assertion. This is
52
+ # important to note. It's not using the `url_is` attribute nor the URL
53
+ # displayed in the browser. It's using the `url_matches` attribute.
54
+ def has_correct_url?
55
+ if url_attribute.nil? && url_match_attribute.nil?
56
+ no_url_match_is_possible
57
+ end
58
+ !(url =~ url_match_attribute).nil?
59
+ end
60
+
61
+ alias displayed? has_correct_url?
62
+
63
+ # A call to `has_correct_title?` returns true or false if the actual
64
+ # title of the current page in the browser matches the `title_is`
65
+ # attribute. Notice that this check is done as part of a match rather
66
+ # than a direct check. This allows for regular expressions to be used.
67
+ def has_correct_title?
68
+ no_title_is_provided if title_attribute.nil?
69
+ !title.match(title_attribute).nil?
70
+ end
71
+
72
+ # A call to `secure?` returns true if the page is secure and false
73
+ # otherwise. This is a simple check that looks for whether or not the
74
+ # current URL begins with 'https'.
75
+ def secure?
76
+ !url.match(/^https/).nil?
77
+ end
78
+
79
+ # A call to `url` returns the actual URL of the page that is displayed
80
+ # in the browser.
81
+ def url
82
+ @browser.url
83
+ end
84
+
85
+ alias page_url url
86
+ alias current_url url
87
+
88
+ # A call to `title` returns the actual title of the page that is
89
+ # displayed in the browser.
90
+ def title
91
+ @browser.title
92
+ end
93
+
94
+ alias page_title title
95
+
96
+ # A call to `markup` returns all markup on a page. Generally you don't
97
+ # just want the entire markup but rather want to parse the output of
98
+ # the `markup` call.
99
+ def markup
100
+ browser.html
101
+ end
102
+
103
+ alias html markup
104
+
105
+ # A call to `text` returns all text on a page. Note that this is text
106
+ # that is taken out of the markup context. It is unlikely you will just
107
+ # want the entire text but rather want to parse the output of the
108
+ # `text` call.
109
+ def text
110
+ browser.text
111
+ end
112
+
113
+ alias page_text text
114
+
115
+ # This method sends a standard "browser refresh" message to the browser.
116
+ def refresh
117
+ browser.refresh
118
+ end
119
+
120
+ alias refresh_page refresh
121
+
122
+ # This method provides a call to the browser window to resize that
123
+ # window to the specified width and height values.
124
+ def resize(width, height)
125
+ browser.window.resize_to(width, height)
126
+ end
127
+
128
+ alias resize_to resize
129
+
130
+ # This method provides a call to the browser window to move the
131
+ # window to the specified x and y screen coordinates.
132
+ def move_to(x, y)
133
+ browser.window.move_to(x, y)
134
+ end
135
+
136
+ # This method provides a call to the synchronous `execute_script`
137
+ # action on the browser, passing in JavaScript that you want to have
138
+ # executed against the current page. For example:
139
+ #
140
+ # result = page.run_script("alert('Tapestry ran a script.')")
141
+ #
142
+ # You can also run full JavaScript snippets.
143
+ #
144
+ # script = <<-JS
145
+ # return arguments[0].innerHTML
146
+ # JS
147
+ #
148
+ # page.run_script(script, page.account)
149
+ #
150
+ # Here you pass two arguments to `run_script`. One is the script itself
151
+ # and the other are some arguments that you want to pass as part of
152
+ # of the execution. In this case, an element definition (`account`) is
153
+ # being passed in.
154
+ def run_script(script, *args)
155
+ browser.execute_script(script, *args)
156
+ end
157
+
158
+ alias execute_script run_script
159
+
160
+ # A call to `screenshot` saves a screenshot of the current browser
161
+ # page. Note that this will grab the entire browser page, even portions
162
+ # of it that are off panel and need to be scrolled to. You can pass in
163
+ # the path and filename of the image that you want the screenshot
164
+ # saved to.
165
+ def screenshot(file)
166
+ browser.save.screenshot(file)
167
+ end
168
+
169
+ alias save_screenshot screenshot
170
+
171
+ # A call to `screen_width` returns the width of the browser screen as
172
+ # reported by the browser API, using a JavaScript call to the `screen`
173
+ # object.
174
+ def screen_width
175
+ run_script("return screen.width;")
176
+ end
177
+
178
+ # A call to `screen_height` returns the height of the browser screen as
179
+ # reported by the browser API, using a JavaScript call to the `screen`
180
+ # object.
181
+ def screen_height
182
+ run_script("return screen.height;")
183
+ end
184
+
185
+ # A call to `get_cookie` allows you to specify a particular cookie, by
186
+ # name, and return the information specified in the cookie.
187
+ def get_cookie(name)
188
+ browser.cookies.to_a.each do |cookie|
189
+ return cookie[:value] if cookie[:name] == name
190
+ end
191
+ nil
192
+ end
193
+
194
+ # A call to `clear_cookies` removes all the cookies from the current
195
+ # instance of the browser that is being controlled by WebDriver.
196
+ def clear_cookies
197
+ browser.cookies.clear
198
+ end
199
+
200
+ alias remove_cookies clear_cookies
201
+ end
202
+ end
203
+ end