gless 1.0.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.
- data/.gitignore +18 -0
- data/.rvmrc +34 -0
- data/README.md +242 -0
- data/Rakefile +41 -0
- data/TODO +4 -0
- data/TODO-session +65 -0
- data/examples/test_github/features/support/env.rb +27 -0
- data/examples/test_github/features/support/step_definitions/test_github_steps.rb +27 -0
- data/examples/test_github/features/test_github/test_github.feature +12 -0
- data/examples/test_github/lib/config/development.yml +12 -0
- data/examples/test_github/lib/config/development.yml.example +12 -0
- data/examples/test_github/lib/pages/test_github/blog_page.rb +13 -0
- data/examples/test_github/lib/pages/test_github/explore_page.rb +13 -0
- data/examples/test_github/lib/pages/test_github/features_page.rb +13 -0
- data/examples/test_github/lib/pages/test_github/login_page.rb +15 -0
- data/examples/test_github/lib/pages/test_github/repo_page.rb +14 -0
- data/examples/test_github/lib/pages/test_github/search_page.rb +61 -0
- data/examples/test_github/lib/pages/test_github_base_page.rb +11 -0
- data/examples/test_github/lib/startup.rb +16 -0
- data/examples/test_github/lib/test_github.rb +68 -0
- data/gless.gemspec +25 -0
- data/lib/gless/base_page.rb +339 -0
- data/lib/gless/browser.rb +43 -0
- data/lib/gless/config.rb +80 -0
- data/lib/gless/logger.rb +122 -0
- data/lib/gless/session.rb +319 -0
- data/lib/gless/wrap_watir.rb +164 -0
- data/lib/gless.rb +54 -0
- metadata +173 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'gless'
|
3
|
+
|
4
|
+
module TestGithub
|
5
|
+
|
6
|
+
class TestGithub::Application
|
7
|
+
include RSpec::Matchers
|
8
|
+
|
9
|
+
attr_accessor :browser
|
10
|
+
attr_accessor :session
|
11
|
+
attr_accessor :site
|
12
|
+
attr_accessor :base_url
|
13
|
+
|
14
|
+
def initialize( browser, config, logger )
|
15
|
+
@logger = logger
|
16
|
+
|
17
|
+
@logger.debug "TestGithub Application: initializing with browser #{browser.inspect}"
|
18
|
+
|
19
|
+
@browser = browser
|
20
|
+
@config = config
|
21
|
+
|
22
|
+
@base_url = @config.get :global, :site, :url
|
23
|
+
@base_url.should be_true
|
24
|
+
|
25
|
+
# Create the session
|
26
|
+
@session = Gless::Session.new( @browser, @config, @logger, self )
|
27
|
+
|
28
|
+
@session.should be_true
|
29
|
+
|
30
|
+
@logger.info "TestGithub Application: going to github"
|
31
|
+
@session.enter TestGithub::LoginPage
|
32
|
+
end
|
33
|
+
|
34
|
+
def goto_repository_from_anywhere name, repo_pattern
|
35
|
+
@logger.info "TestGithub Application: going to repository #{name}"
|
36
|
+
|
37
|
+
@session.search.click
|
38
|
+
|
39
|
+
@session.search_for name
|
40
|
+
|
41
|
+
repodata = @session.find_repository repo_pattern
|
42
|
+
repodata.should be_true, "TestGithub Application: couldn't find repository #{name}"
|
43
|
+
|
44
|
+
@logger.info "TestGithub Application: found repository #{repodata[:name]}, which was at number #{repodata[:index] + 1} on the page, now opening it."
|
45
|
+
|
46
|
+
@session.goto_repository repo_pattern
|
47
|
+
end
|
48
|
+
|
49
|
+
def poke_headers
|
50
|
+
@logger.info "TestGithub Application: trying out all the header buttons."
|
51
|
+
|
52
|
+
@logger.info "TestGithub Application: clicking explore."
|
53
|
+
@session.explore.click
|
54
|
+
|
55
|
+
@logger.info "TestGithub Application: clicking search."
|
56
|
+
@session.search.click
|
57
|
+
|
58
|
+
@logger.info "TestGithub Application: clicking features."
|
59
|
+
@session.features.click
|
60
|
+
|
61
|
+
@logger.info "TestGithub Application: clicking blog."
|
62
|
+
@session.blog.click
|
63
|
+
|
64
|
+
@logger.info "TestGithub Application: clicking home."
|
65
|
+
@session.home.click
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/gless.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "gless"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "gless"
|
7
|
+
s.version = Gless::VERSION
|
8
|
+
s.authors = ["Robin Lee Powell"]
|
9
|
+
s.email = ["rlpowell@digitalkingdom.org"]
|
10
|
+
s.homepage = "http://github.com/rlpowell/gless"
|
11
|
+
s.summary = %q{A wrapper for Watir-WebDriver based on modelling web page and web site structure.}
|
12
|
+
s.description = %q{This gem attempts to provide a more robust model for web application testing, on top of Watir-WebDriver which already has significant improvements over just Selenium or WebDriver, based on describing pages and then interacting with the descriptions.}
|
13
|
+
|
14
|
+
s.add_dependency 'rspec'
|
15
|
+
s.add_dependency 'watir-webdriver'
|
16
|
+
s.add_dependency 'selenium-webdriver'
|
17
|
+
s.add_development_dependency 'debugger'
|
18
|
+
s.add_development_dependency 'yard'
|
19
|
+
s.add_development_dependency 'yard-tomdoc'
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split("\n")
|
22
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
end
|
@@ -0,0 +1,339 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
|
3
|
+
module Gless
|
4
|
+
#
|
5
|
+
# This class is intended to be the base class for all page classes
|
6
|
+
# used by the session object to represent individual pages on a
|
7
|
+
# website. In fact, if you *don't* subclass all your page classes
|
8
|
+
# from this one, something is likely to break.
|
9
|
+
#
|
10
|
+
# = Class Level Methods
|
11
|
+
#
|
12
|
+
# This class defines a bunch of class-level behaviour, so that we can
|
13
|
+
# have things like
|
14
|
+
#
|
15
|
+
# element :email_field, :text_field, :id => 'email'
|
16
|
+
#
|
17
|
+
# in the class definition itself.
|
18
|
+
#
|
19
|
+
# However, this is too early to do much of the initialization,
|
20
|
+
# which leads to some complexity in the real init method to
|
21
|
+
# basically make up for deferred computation.
|
22
|
+
#
|
23
|
+
# = Calling Back To The Session
|
24
|
+
#
|
25
|
+
# The session object needs to know all of the page object classes.
|
26
|
+
# This is accomplished by having an +inherited+ method on this
|
27
|
+
# (the +BasePage+) class that calls +add_page_class+ on the Session
|
28
|
+
# class; this only stores the subclass, it does no further
|
29
|
+
# processing, since complicated processing at class creation time
|
30
|
+
# tends to hit snags. When a session object is actually
|
31
|
+
# instantiated, the list of page classes is walked, and a page
|
32
|
+
# class instance is created for each for future use.
|
33
|
+
#
|
34
|
+
class Gless::BasePage
|
35
|
+
include RSpec::Matchers
|
36
|
+
|
37
|
+
#******************************
|
38
|
+
# Class Level
|
39
|
+
#******************************
|
40
|
+
|
41
|
+
class << self
|
42
|
+
# @return [String] A URL that can be used to come to this page
|
43
|
+
# directly, if that can be known at compile time; has no
|
44
|
+
# sensible default
|
45
|
+
attr_accessor :entry_url
|
46
|
+
|
47
|
+
# @return [Array<String>, Array<Regexp>] A list of strings or
|
48
|
+
# patterns to add to the Session dispatch list
|
49
|
+
attr_writer :url_patterns
|
50
|
+
|
51
|
+
# @return [Array] Just sets up a default (to wit, []) for url_patterns
|
52
|
+
def url_patterns
|
53
|
+
@url_patterns ||= []
|
54
|
+
end
|
55
|
+
|
56
|
+
# Calls back to Gless::Session. See overview documentation
|
57
|
+
# fro +Gless::BasePage+
|
58
|
+
def inherited(klass)
|
59
|
+
Gless::Session.add_page_class klass
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [Array<String>] An list of elements (actually just
|
63
|
+
# their method names) that should *always* exist if this
|
64
|
+
# page is loaded; used to wait for the page to load and
|
65
|
+
# validate correctness. The page is not considered fully
|
66
|
+
# loaded until all of these elements are found.
|
67
|
+
attr_writer :validator_elements
|
68
|
+
|
69
|
+
# @return [Array] Just sets up a default (to wit, []) for
|
70
|
+
# validator_elements
|
71
|
+
def validator_elements
|
72
|
+
@validator_elements ||= []
|
73
|
+
end
|
74
|
+
|
75
|
+
# Specifies the title that this page is expected to have.
|
76
|
+
#
|
77
|
+
# @param [String,Regexp] expected_title
|
78
|
+
def expected_title expected_title
|
79
|
+
define_method 'has_expected_title?' do
|
80
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, has_expected_title?: current is #{@browser.title}, expected is #{expected_title}"
|
81
|
+
expected_title.kind_of?(Regexp) ? @browser.title.should =~ expected_title : @browser.title.should == expected_title
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Specifies an element that might appear on the page.
|
86
|
+
# The goal is to be easy for users of this library to use, so
|
87
|
+
# there's some real complexity here so that the end user can
|
88
|
+
# just do stuff like:
|
89
|
+
#
|
90
|
+
# element :deleted_application , :div , :text => /Your application. \S+ has been deleted./
|
91
|
+
#
|
92
|
+
# and it comes out feeling very natural.
|
93
|
+
#
|
94
|
+
# A longer example:
|
95
|
+
#
|
96
|
+
# element :new_application_button , :element , :id => 'new_application' , :validator => true , :click_destination => :ApplicationNewPage
|
97
|
+
#
|
98
|
+
# That's about as complicated as it gets.
|
99
|
+
#
|
100
|
+
# The first two arguments (name and type) are required. The
|
101
|
+
# rest is a hash. +:validator_elements+ and +:click_destination+
|
102
|
+
# (see below) have special meaning.
|
103
|
+
#
|
104
|
+
# Anything else is taken to be a Watir selector. If no
|
105
|
+
# selector is forthcoming, the name is taken to be the element
|
106
|
+
# id.
|
107
|
+
#
|
108
|
+
# @param [Symbol] basename The name used in the Gless user's code
|
109
|
+
# to refer to this element. This page object ends up with a
|
110
|
+
# method of this name.
|
111
|
+
#
|
112
|
+
# @param [Symbol] type The Watir element type; used to
|
113
|
+
# dynamically pick which Watir element class to use for this
|
114
|
+
# element.
|
115
|
+
#
|
116
|
+
# @param [Hash] opts Further options for the element.
|
117
|
+
#
|
118
|
+
# @option opts [Boolean] :validator (false) Whether or not the element should
|
119
|
+
# be used to routinely validate the page's correctness
|
120
|
+
# (i.e., if the element is central to the page and always
|
121
|
+
# reliably is present). The page isn't considered loaded
|
122
|
+
# until all validator elements are present. Defaults to
|
123
|
+
# false.
|
124
|
+
#
|
125
|
+
# @option opts [Symbol] :click_destination (nil) A symbol giving the last
|
126
|
+
# bit of the class name of the page that clicking on this
|
127
|
+
# element leads to, if any.
|
128
|
+
#
|
129
|
+
# @option opts [Object] ANY All other opts keys are used as
|
130
|
+
# Watir selectors to find the element on the page.
|
131
|
+
def element basename, type, opts = {}
|
132
|
+
# No class-compile-time logging; it's way too much work, as this runs at *rake* time
|
133
|
+
# $master_logger.debug "In GenericBasePage for #{self.name}: element: initial opts: #{opts}"
|
134
|
+
|
135
|
+
# Promote various other things into selectors; do this before
|
136
|
+
# we add in the default below
|
137
|
+
non_selector_opts = [ :validator, :click_destination ]
|
138
|
+
if ! opts[:selector]
|
139
|
+
opts.keys.each do |key|
|
140
|
+
if ! non_selector_opts.member?(key)
|
141
|
+
opts[:selector] = { key => opts[key] }
|
142
|
+
opts.delete(key)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
opts = { :selector => { :id => basename.to_s }, :validator => false, :click_destination => nil }.merge(opts)
|
148
|
+
|
149
|
+
# No class-compile-time logging; it's way too much work, as this runs at *rake* time
|
150
|
+
# $master_logger.debug "In GenericBasePage for #{self.name}: element: final opts: #{opts}"
|
151
|
+
|
152
|
+
selector = opts[:selector]
|
153
|
+
click_destination = opts[:click_destination]
|
154
|
+
validator = opts[:validator]
|
155
|
+
|
156
|
+
methname = basename.to_s.tr('-', '_')
|
157
|
+
|
158
|
+
if validator
|
159
|
+
# No class-compile-time logging; it's way too much work, as this runs at *rake* time
|
160
|
+
# $master_logger.debug "In GenericBasePage, for #{self.name}, element: #{basename} is a validator"
|
161
|
+
validator_elements << methname
|
162
|
+
end
|
163
|
+
|
164
|
+
if click_destination
|
165
|
+
# No class-compile-time logging; it's way too much work, as this runs at *rake* time
|
166
|
+
# $master_logger.debug "In GenericBasePage, for #{self.name}, element: #{basename} has a special destination when clicked, #{click_destination}"
|
167
|
+
end
|
168
|
+
|
169
|
+
define_method methname do
|
170
|
+
Gless::WrapWatir.new(@browser, @session, type, selector, click_destination)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# @return [Rexexp,String] Used to give the URL string or pattern that matches this page; example:
|
175
|
+
#
|
176
|
+
# url %r{^:base_url/accounts/[0-9]+/apps$}
|
177
|
+
#
|
178
|
+
# +:base_url+ is replaced with the output of
|
179
|
+
# +@application.base_url+
|
180
|
+
def url( url )
|
181
|
+
if url.is_a?(String)
|
182
|
+
url_patterns << Regexp.new(Regexp.escape(url))
|
183
|
+
elsif url.is_a?(Regexp)
|
184
|
+
url_patterns << url
|
185
|
+
else
|
186
|
+
puts "INVALID URL class "+url.class.name+" for #{url.inspect}"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Set this page's entry url.
|
191
|
+
#
|
192
|
+
# @param [String] url
|
193
|
+
def set_entry_url( url )
|
194
|
+
@entry_url = url
|
195
|
+
end
|
196
|
+
|
197
|
+
end # class-level definitions
|
198
|
+
|
199
|
+
#******************************
|
200
|
+
# Instance Level
|
201
|
+
#******************************
|
202
|
+
|
203
|
+
|
204
|
+
# @return [Watir::Browser]
|
205
|
+
attr_accessor :browser
|
206
|
+
|
207
|
+
# The main application object. See the README for specifics.
|
208
|
+
attr_accessor :application
|
209
|
+
|
210
|
+
# @return [Gless::Session] The session object that uses/created
|
211
|
+
# this page.
|
212
|
+
attr_accessor :session
|
213
|
+
|
214
|
+
# Perform special variable substitution; used for url match
|
215
|
+
# patterns and entry urls.
|
216
|
+
def substitute str
|
217
|
+
if str.kind_of?(Regexp)
|
218
|
+
reg = str.source
|
219
|
+
reg.gsub!(/\:base_url/,@application.base_url)
|
220
|
+
return Regexp.new(reg)
|
221
|
+
else
|
222
|
+
return str.gsub(/\:base_url/,@application.base_url)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def initialize browser, session, application
|
227
|
+
# @session.log.debug "In GenericBasePage, for #{self.class.name}, init: #{browser}, #{session}, #{application}"
|
228
|
+
@browser = browser
|
229
|
+
@session = session
|
230
|
+
@application = application
|
231
|
+
|
232
|
+
# Couldn't do this any earlier, needed the application
|
233
|
+
if self.class.entry_url
|
234
|
+
self.class.entry_url = substitute self.class.entry_url
|
235
|
+
end
|
236
|
+
|
237
|
+
# Fake inheritance time
|
238
|
+
self.class.validator_elements = self.class.validator_elements + self.class.ancestors.map { |x| x.respond_to?( :validator_elements ) ? x.validator_elements : nil }
|
239
|
+
self.class.validator_elements = self.class.validator_elements.flatten.compact.uniq
|
240
|
+
|
241
|
+
self.class.url_patterns.map! { |x| substitute x }
|
242
|
+
|
243
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, init: class vars: #{self.class.entry_url}, #{self.class.url_patterns}, #{self.class.validator_elements}"
|
244
|
+
end
|
245
|
+
|
246
|
+
# Return true if the given url matches this page's patterns
|
247
|
+
def match_url( url )
|
248
|
+
self.class.url_patterns.each do |pattern|
|
249
|
+
if url =~ pattern
|
250
|
+
return true
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
return false
|
255
|
+
end
|
256
|
+
|
257
|
+
# Pass through anything we don't understand to the browser, just
|
258
|
+
# in case.
|
259
|
+
def method_missing sym, *args, &block
|
260
|
+
@browser.send sym, *args, &block
|
261
|
+
end
|
262
|
+
|
263
|
+
# Go to the page from who-cares-where, and make sure we're there
|
264
|
+
def enter
|
265
|
+
@session.log.debug "#{self.class.name}: enter"
|
266
|
+
|
267
|
+
arrived? do
|
268
|
+
@session.log.info "#{self.class.name}: about to goto #{self.class.entry_url} from #{@browser.url}"
|
269
|
+
@browser.goto self.class.entry_url
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# Make sure that we've actually gotten to this page, after clicking a button or whatever; used by Session
|
274
|
+
#
|
275
|
+
# Takes an optional block; if that block exists, it's run before
|
276
|
+
# the per-loop validation attempt.
|
277
|
+
def arrived?
|
278
|
+
all_validate = true
|
279
|
+
|
280
|
+
6.times do
|
281
|
+
if ! match_url( @browser.url )
|
282
|
+
yield if block_given?
|
283
|
+
end
|
284
|
+
|
285
|
+
self.class.validator_elements.each do |x|
|
286
|
+
begin
|
287
|
+
if self.send(x).wait_until_present(5)
|
288
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, arrived?: validator element #{x} found."
|
289
|
+
else
|
290
|
+
# Probably never reached
|
291
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, arrived?: validator element #{x} NOT found."
|
292
|
+
end
|
293
|
+
rescue Watir::Wait::TimeoutError => e
|
294
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, arrived?: validator element #{x} NOT found."
|
295
|
+
all_validate = false
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
if all_validate
|
300
|
+
if match_url( @browser.url )
|
301
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, arrived?: all validator elements found."
|
302
|
+
break
|
303
|
+
else
|
304
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, arrived?: all validator elements found, but the current URL (#{@browser.url}) doesn't match the expected URL(s) (#{self.class.url_patterns})"
|
305
|
+
end
|
306
|
+
else
|
307
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, arrived?: not all validator elements found, trying again."
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
begin
|
312
|
+
if respond_to? :has_expected_title?
|
313
|
+
has_expected_title?.should be_true
|
314
|
+
end
|
315
|
+
|
316
|
+
match_url( @browser.url ).should be_true
|
317
|
+
|
318
|
+
# We don't use all_validate here because we want to alert on the
|
319
|
+
# element with the problem
|
320
|
+
self.class.validator_elements.each do |x|
|
321
|
+
self.send(x).wait_until_present(5).should be_true
|
322
|
+
end
|
323
|
+
|
324
|
+
@session.log.debug "In GenericBasePage, for #{self.class.name}, arrived?: completed successfully."
|
325
|
+
return true
|
326
|
+
rescue Exception => e
|
327
|
+
if @session.get_config :global, :debug
|
328
|
+
@session.log.debug "GenericBasePage, for #{self.class.name}, arrived?: something doesn't match (url or title or expected elements), exception information follows, then giving you a debugger"
|
329
|
+
@session.log.debug "Gless::BasePage: Had an exception in debug mode: #{e.inspect}"
|
330
|
+
@session.log.debug "Gless::BasePage: Had an exception in debug mode: #{e.message}"
|
331
|
+
@session.log.debug "Gless::BasePage: Had an exception in debug mode: #{e.backtrace.join("\n")}"
|
332
|
+
debugger
|
333
|
+
else
|
334
|
+
return false
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'watir-webdriver'
|
2
|
+
require 'selenium-webdriver'
|
3
|
+
|
4
|
+
module Gless
|
5
|
+
# A very minor wrapper on the Watir::Browser class to use
|
6
|
+
# Gless's config file system. Other than that it just adds logging
|
7
|
+
# at this point. It might do more later.
|
8
|
+
class Gless::Browser
|
9
|
+
# The underlying Watir::Browser
|
10
|
+
attr_reader :browser
|
11
|
+
|
12
|
+
# Takes a Gless::EnvConfig object, which it uses to
|
13
|
+
# decide what kind of browser to launch, and launches a browser.
|
14
|
+
#
|
15
|
+
# @param [Gless::EnvConfig] config A Gless::EnvConfig which has
|
16
|
+
# :global => :browser => :type defined.
|
17
|
+
#
|
18
|
+
# @return [Gless::Browser]
|
19
|
+
def initialize( config, logger )
|
20
|
+
@config = config
|
21
|
+
@logger = logger
|
22
|
+
type=@config.get :global, :browser, :type
|
23
|
+
browser=@config.get :global, :browser, :browser
|
24
|
+
port=@config.get :global, :browser, :port
|
25
|
+
@logger.debug "Launching some browser; #{type}, #{port}, #{browser}"
|
26
|
+
|
27
|
+
if type == 'remote'
|
28
|
+
@logger.info "Launching remote browser #{browser} on port #{port}"
|
29
|
+
capabilities = Selenium::WebDriver::Remote::Capabilities.new( :browser_name => browser, :javascript_enabled=>true, :css_selectors_enabled=>true, :takes_screenshot=>true, :native_events=>true )
|
30
|
+
@browser = Watir::Browser.new(:remote, :url => "http://127.0.0.1:#{port}/wd/hub", :desired_capabilities => capabilities)
|
31
|
+
else
|
32
|
+
@logger.info "Launching local browser #{browser}"
|
33
|
+
@browser = Watir::Browser.new :browser
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Pass everything else through to the Watir::Browser
|
38
|
+
# underneath.
|
39
|
+
def method_missing(m, *args, &block)
|
40
|
+
@browser.send(m, *args, &block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/gless/config.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Gless
|
4
|
+
# Provides a bit of a wraper around yaml config files; nothing
|
5
|
+
# terribly complicated. Can merge multiple configs together.
|
6
|
+
# Expects all configs to be hashes.
|
7
|
+
class Gless::EnvConfig
|
8
|
+
# Bootstrapping method used to inform Gless as to where
|
9
|
+
# config files can be found.
|
10
|
+
#
|
11
|
+
# @param [String] dir The directory name that holds the config
|
12
|
+
# files (under lib/config in said directory).
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
#
|
16
|
+
# Gless::EnvConfig.env_dir = File.dirname(__FILE__)
|
17
|
+
#
|
18
|
+
def self.env_dir=(dir)
|
19
|
+
@@env_dir=dir
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets up the initial configuration environment. @@env_dir must
|
23
|
+
# be set before this, or things will go poorly.
|
24
|
+
# The file it wants to load is, loosely,
|
25
|
+
# @@env_dir/lib/config/ENVIRONMENT.yml, where ENVIRONMENT is the
|
26
|
+
# environment variable of that name.
|
27
|
+
#
|
28
|
+
# @return [Gless::EnvConfig]
|
29
|
+
def initialize
|
30
|
+
env = (ENV['ENVIRONMENT'] || 'development').to_sym
|
31
|
+
|
32
|
+
env_file = "#{@@env_dir}/config/#{env}.yml"
|
33
|
+
raise "You need to create a configuration file named '#{env}.yml' (generated from the ENVIRONMENT environment variable) under #{@@env_dir}/lib/config" unless File.exists? env_file
|
34
|
+
|
35
|
+
@config = YAML::load_file env_file
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add a file to those in use for configuration data.
|
39
|
+
# Simply merges in the new data, so each file should probably
|
40
|
+
# have its own top level singleton hash.
|
41
|
+
#
|
42
|
+
def add_file file
|
43
|
+
@config.merge!(YAML::load_file "#{@@env_dir}/#{file}")
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get an element from the configuration. Takes an arbitrary
|
47
|
+
# number of arguments; each is taken to be a hash key.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
#
|
51
|
+
# @config.get :global, :debug
|
52
|
+
#
|
53
|
+
# @return [Object] what's left after following each key; could be
|
54
|
+
# basically anything.
|
55
|
+
def get( *args )
|
56
|
+
return get_sub_tree( @config, *args )
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Recursively does all the heavy lifting for get
|
62
|
+
def get_sub_tree items, elem, *args
|
63
|
+
# Can't use debug logging here, as it maybe isn't turned on yet
|
64
|
+
# puts "In Gless::EnvConfig, get_sub_tree: items: #{items}, elem: #{elem}, args: #{args}"
|
65
|
+
|
66
|
+
if items.nil?
|
67
|
+
raise "Could not locate '#{elem}' in YAML config" if sub_tree.nil?
|
68
|
+
end
|
69
|
+
|
70
|
+
new_items = items[elem.to_sym]
|
71
|
+
raise "Could not locate '#{elem}' in YAML config" if new_items.nil?
|
72
|
+
|
73
|
+
if args.empty?
|
74
|
+
return new_items
|
75
|
+
else
|
76
|
+
return get_sub_tree( new_items, *args )
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/gless/logger.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
module Gless
|
2
|
+
# Provides some wrapping around the normal Logger class. In
|
3
|
+
# particular, Gless::Logger has a concept of a replay log, which
|
4
|
+
# is an attempt to lay out all the things that happened during its
|
5
|
+
# interactions with the browser, including screenshots and HTML
|
6
|
+
# source at each step.
|
7
|
+
#
|
8
|
+
# This does not improve performance. :)
|
9
|
+
#
|
10
|
+
# It also tries to simplify the maintenance of multiple logging
|
11
|
+
# streams, so that tests can be parallelized without too much
|
12
|
+
# trouble.
|
13
|
+
#
|
14
|
+
# The core system creates a log object with the tag :master for
|
15
|
+
# logging during setup and teardown. It is expected that each
|
16
|
+
# session object (i.e. each parallel browser instance) will create
|
17
|
+
# its own for logging of what happens during the actual session.
|
18
|
+
class Gless::Logger
|
19
|
+
# The log stream that goes to STDOUT. Here in case you need to
|
20
|
+
# bypass the normal multi-log semantics.
|
21
|
+
attr_reader :replay_log
|
22
|
+
# The log stream that goes to the replay directory. Here in
|
23
|
+
# case you need to bypass the normal multi-log semantics.
|
24
|
+
attr_reader :normal_log
|
25
|
+
|
26
|
+
# Sets up logging.
|
27
|
+
#
|
28
|
+
# @param [Symbol] tag A short tag describing this particular log
|
29
|
+
# stream (as opposed to other parallel ones that might exist).
|
30
|
+
#
|
31
|
+
# @param [Boolean] replay Whether or not to generate a replay
|
32
|
+
# log as part of this log stream.
|
33
|
+
#
|
34
|
+
# @param [String] replay_path The path to put the replay logs
|
35
|
+
# in. Passed through Kernel.sprintf with :home (ENV['HOME']),
|
36
|
+
# :tag, and :replay defined as you'd expect.
|
37
|
+
def initialize( tag, replay = true, replay_path = '%{home}/public_html/watir_replay/%{tag}' )
|
38
|
+
require 'logger'
|
39
|
+
require 'fileutils'
|
40
|
+
|
41
|
+
@ssnum = 0 # For snapshot pictures
|
42
|
+
|
43
|
+
@replay_path=sprintf(replay_path, { :home => ENV['HOME'], :tag => tag, :replay => replay })
|
44
|
+
FileUtils.rm_rf(@replay_path)
|
45
|
+
FileUtils.mkdir(@replay_path)
|
46
|
+
|
47
|
+
replay_log_file = File.open("#{@replay_path}/index.html", "w")
|
48
|
+
@replay_log = ::Logger.new replay_log_file
|
49
|
+
|
50
|
+
#@replay_log.formatter = proc do |severity, datetime, progname, msg|
|
51
|
+
# # I, [2012-08-14T15:30:10.736784 #14647] INFO -- : <p>Launching remote browser</p>
|
52
|
+
# "<p>#{severity[0]}, [#{datetime} #{progname}]: #{severity} -- : #{msg}</p>\n"
|
53
|
+
#end
|
54
|
+
|
55
|
+
original_formatter = ::Logger::Formatter.new
|
56
|
+
|
57
|
+
# Add in the tag and html-ify
|
58
|
+
@replay_log.formatter = proc { |severity, datetime, progname, msg|
|
59
|
+
# Can't flush after from here, so flush prior stuff
|
60
|
+
replay_log_file.flush
|
61
|
+
npn = "#{progname} #{tag} ".sub(/^\s*/,'').sub(/\s*$/,'')
|
62
|
+
stuff=original_formatter.call(severity, datetime, "#{progname} #{tag} ", msg)
|
63
|
+
#"<p>#{ERB::Util.html_escape(stuff.chomp)}</p>\n"
|
64
|
+
"<p>#{stuff.chomp}</p>\n"
|
65
|
+
}
|
66
|
+
@replay_log.level = ::Logger::WARN
|
67
|
+
|
68
|
+
@normal_log = ::Logger.new(STDOUT)
|
69
|
+
# Add in the tag
|
70
|
+
@normal_log.formatter = proc { |severity, datetime, progname, msg|
|
71
|
+
original_formatter.call(severity, datetime, "#{progname} #{tag} ", msg)
|
72
|
+
}
|
73
|
+
|
74
|
+
@normal_log.level = ::Logger::WARN
|
75
|
+
end
|
76
|
+
|
77
|
+
# Passes on all the normal Logger methods. By default, logs to
|
78
|
+
# both the normal log and the replay log.
|
79
|
+
def method_missing(m, *args, &block)
|
80
|
+
@replay_log.send(m, *args, &block)
|
81
|
+
@normal_log.send(m, *args, &block)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Adds a screenshot and HTML source into the replay log from the
|
85
|
+
# given browser.
|
86
|
+
#
|
87
|
+
# @param [Watir::Browser] browser
|
88
|
+
# @param [Gless::Session] session
|
89
|
+
def add_to_replay_log( browser, session )
|
90
|
+
@ssnum = @ssnum + 1
|
91
|
+
|
92
|
+
if session.get_config :global, :screenshots
|
93
|
+
begin
|
94
|
+
browser.driver.save_screenshot "#{@replay_path}/screenshot_#{@ssnum}.png"
|
95
|
+
|
96
|
+
if session.get_config :global, :thumbnails
|
97
|
+
require 'mini_magick'
|
98
|
+
|
99
|
+
image = MiniMagick::Image.open("#{@replay_path}/screenshot_#{@ssnum}.png")
|
100
|
+
image.resize "400"
|
101
|
+
image.write "#{@replay_path}/screenshot_#{@ssnum}_thumb.png"
|
102
|
+
FileUtils.chmod 0755, "#{@replay_path}/screenshot_#{@ssnum}_thumb.png"
|
103
|
+
|
104
|
+
@replay_log.debug "Screenshot: <a href='screenshot_#{@ssnum}.png'><img src='screenshot_#{@ssnum}_thumb.png' /></a>"
|
105
|
+
else
|
106
|
+
@replay_log.debug "Screenshot: <a href='screenshot_#{@ssnum}.png'>Screenshot</a>"
|
107
|
+
end
|
108
|
+
rescue Exception => e
|
109
|
+
@normal_log.warn "Screenshot failed with exception #{e}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
html=browser.html
|
114
|
+
htmlFile = File.new("#{@replay_path}/html_capture_#{@ssnum}.txt", "w")
|
115
|
+
htmlFile.write(html)
|
116
|
+
htmlFile.close
|
117
|
+
|
118
|
+
@replay_log.debug "<a href='html_capture_#{@ssnum}.txt'>HTML Source</a>"
|
119
|
+
@replay_log.debug "Force flush"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|