gless 1.0.1

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