Last active
July 18, 2018 07:05
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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