gless 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|