Skip to content

Instantly share code, notes, and snippets.

@DanielHeath
Last active July 18, 2018 07:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielHeath/6d33e192dca1bd81814f440c50297371 to your computer and use it in GitHub Desktop.
Save DanielHeath/6d33e192dca1bd81814f440c50297371 to your computer and use it in GitHub Desktop.
Javascript coverage with selenium & chrome. Must not compile an asset bundle (or otherwise transpile assets) in test mode.
# TODO: Support headless-chrome (does not have chrome://version page)
# Probably needs 'sys/proctable' or similar as it's not exposed otherwise.
module ChromeTracing
def self.register_simplecov!
require 'simplecov'
SimpleCov.profiles.define "jscov" do
# ignore RB but not JS in vendor
filters.clear
add_filter "/vendor/bundle/**/*.rb"
load_profile "rails"
load_profile "root_filter"
track_files "{app,lib,public}/**/*.{rb,js}" # Track JS files
end
SimpleCov.start 'jscov'
end
def browser
super
initialize_coverage! unless @tracing
@browser
end
def visit(path)
save_coverage
super
start_coverage
end
def refresh
save_coverage
super
start_coverage
end
def go_back
save_coverage
super
start_coverage
end
def go_forward
save_coverage
super
start_coverage
end
def quit
save_coverage
serialize_coverage
super
end
def reset!
save_coverage
super
start_coverage
end
def initialize_coverage!
return if @initializing || @initialized
@initializing = true
visit "chrome://version/"
port = find_css("#command_line").first.visible_text.match(/remote-debugging-port=(\d+)\b/)
@tracing = ChromeRemote.client(port: port[1])
@stylesheets = {}
@tracing.on 'CSS.styleSheetAdded' do |resp|
@stylesheets[resp["header"]["styleSheetId"]] = resp["header"]["sourceURL"]
end
@results = {}
@initializing = false
@initialized = true
start_coverage
end
def asset_path_for_url(url)
return nil if url.blank?
uri = URI.parse(url)
unless uri.host == "localhost" || uri.host == "127.0.0.1"
# puts "skipping nonlocal url #{url}"
return nil
end
match = uri.path.match /assets\/(.+)((\.self)|(-[a-f0-9]))+.(js|css)/
return nil unless match
filename, ext = match[1], match[5]
# TODO: Handling sass/coffee/whatever requires source maps
# ext = "{css,sass,scss}" if ext == "css"
for path in Rails.application.config.assets.paths do
matches = Dir.glob("#{path}/#{filename}.#{ext}*")
return matches.first if matches.first
end
return nil
end
def byterange_to_linerange(lines, start, finish)
result = []
lines.each_with_index do |line, index|
if start > line.length || finish < 0
# haven't started yet or has finished.
start -= line.length
finish -= line.length
else
result << index
end
end
result.min..result.max
end
def set_coverage(filepath, start, finish, count=1)
lines = File.read(filepath).lines
@results[filepath] ||= [0] * lines.length
byterange_to_linerange(lines, start, finish).each do |i|
@results[filepath][i] = count
end
end
def save_coverage
initialize_coverage!
return if @initializing
css = @tracing.send_cmd "CSS.takeCoverageDelta"
@tracing.send_cmd "CSS.enable"
@tracing.send_cmd "CSS.startRuleUsageTracking"
css && css["coverage"] && css["coverage"].each do |css|
filepath = asset_path_for_url(@stylesheets[css["styleSheetId"]])
next if filepath.blank? || !css["used"]
start = css["startOffset"].to_i
finish = css["endOffset"].to_i
set_coverage(filepath, start, finish)
end
js = @tracing.send_cmd "Profiler.takePreciseCoverage"
js && js["result"] && js["result"].each do |r|
filepath = asset_path_for_url(r["url"])
next if filepath.blank?
r["functions"].each do |fn|
fn["ranges"].each do |range|
next unless range["count"].to_i > 0
start = range["startOffset"].to_i
finish = range["endOffset"].to_i
set_coverage(filepath, start, finish)
end
end
end
end
def start_coverage
return if @initializing
@tracing.send_cmd "DOM.enable"
@tracing.send_cmd "CSS.enable"
@tracing.send_cmd "CSS.startRuleUsageTracking"
@tracing.send_cmd "Profiler.enable"
@tracing.send_cmd "Profiler.start"
@tracing.send_cmd "Profiler.startPreciseCoverage", callCount: false, detailed: true
end
def serialize_coverage(to = "coverage/.resultset.json")
puts "Serializing coverage: #{object_id}"
json = JSON.parse(File.read(to)) rescue {}
json["frontend"] = {coverage: @results, timestamp: Time.now.to_i}
File.write(to, json.to_json)
end
end
require 'chrome_tracing'
ChromeTracing.register_simplecov!
Capybara.register_driver :chrome do |app|
drv = Capybara::Selenium::Driver.new app,
browser: :chrome,
options: Selenium::WebDriver::Chrome::Options.new()
drv.extend ChromeTracing
drv
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment