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.
@@ -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
@@ -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
@@ -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