33// BSD-style license that can be found in the LICENSE file.
44
55import 'dart:async' ;
6+ import 'dart:convert' ;
67import 'dart:io' ;
78
9+ import 'package:coverage/coverage.dart' ;
10+ import 'package:http/http.dart' as http;
11+ import 'package:path/path.dart' as p;
812import 'package:pedantic/pedantic.dart' ;
913import 'package:test_api/src/backend/runtime.dart' ; // ignore: implementation_imports
1014import 'package:test_core/src/util/io.dart' ; // ignore: implementation_imports
15+ import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' ;
1116
1217import '../executable_settings.dart' ;
1318import 'browser.dart' ;
1419import 'default_settings.dart' ;
1520
16- // TODO(nweiz): move this into its own package?
1721/// A class for running an instance of Chrome.
1822///
1923/// Most of the communication with the browser is expected to happen via HTTP,
@@ -28,11 +32,16 @@ class Chrome extends Browser {
2832 @override
2933 final Future <Uri > remoteDebuggerUrl;
3034
35+ final Future <WipConnection > _tabConnection;
36+ final Map <String , String > _idToUrl;
37+
3138 /// Starts a new instance of Chrome open to the given [url] , which may be a
3239 /// [Uri] or a [String] .
3340 factory Chrome (Uri url, {ExecutableSettings settings, bool debug = false }) {
3441 settings ?? = defaultSettings[Runtime .chrome];
3542 var remoteDebuggerCompleter = Completer <Uri >.sync ();
43+ var connectionCompleter = Completer <WipConnection >();
44+ var idToUrl = < String , String > {};
3645 return Chrome ._(() async {
3746 var tryPort = ([int port]) async {
3847 var dir = createTempDir ();
@@ -73,6 +82,8 @@ class Chrome extends Browser {
7382 if (port != null ) {
7483 remoteDebuggerCompleter.complete (
7584 getRemoteDebuggerUrl (Uri .parse ('http://localhost:$port ' )));
85+
86+ connectionCompleter.complete (_connect (process, port, idToUrl));
7687 } else {
7788 remoteDebuggerCompleter.complete (null );
7889 }
@@ -85,9 +96,83 @@ class Chrome extends Browser {
8596
8697 if (! debug) return tryPort ();
8798 return getUnusedPort <Process >(tryPort);
88- }, remoteDebuggerCompleter.future);
99+ }, remoteDebuggerCompleter.future, connectionCompleter.future, idToUrl );
89100 }
90101
91- Chrome ._(Future <Process > Function () startBrowser, this .remoteDebuggerUrl)
102+ /// Returns a Dart based hit-map containing coverage report, suitable for use
103+ /// with `package:coverage` .
104+ Future <Map <String , dynamic >> gatherCoverage () async {
105+ var tabConnection = await _tabConnection;
106+ var response = await tabConnection.debugger.connection
107+ .sendCommand ('Profiler.takePreciseCoverage' , {});
108+ var result = response.result['result' ];
109+ var coverage = await parseChromeCoverage (
110+ (result as List ).cast (),
111+ _sourceProvider,
112+ _sourceMapProvider,
113+ _sourceUriProvider,
114+ );
115+ return coverage;
116+ }
117+
118+ Chrome ._(Future <Process > Function () startBrowser, this .remoteDebuggerUrl,
119+ this ._tabConnection, this ._idToUrl)
92120 : super (startBrowser);
121+
122+ Future <Uri > _sourceUriProvider (String sourceUrl, String scriptId) async {
123+ var script = _idToUrl[scriptId];
124+ if (script == null ) return null ;
125+ var uri = Uri .parse (script);
126+ var path = p.join (
127+ p.joinAll (uri.pathSegments.sublist (1 , uri.pathSegments.length - 1 )),
128+ sourceUrl);
129+ return path.contains ('/packages/' )
130+ ? Uri (scheme: 'package' , path: path.split ('/packages/' ).last)
131+ : Uri .file (p.absolute (path));
132+ }
133+
134+ Future <String > _sourceMapProvider (String scriptId) async {
135+ var script = _idToUrl[scriptId];
136+ if (script == null ) return null ;
137+ var mapResponse = await http.get ('$script .map' );
138+ if (mapResponse.statusCode != HttpStatus .ok) return null ;
139+ return mapResponse.body;
140+ }
141+
142+ Future <String > _sourceProvider (String scriptId) async {
143+ var script = _idToUrl[scriptId];
144+ if (script == null ) return null ;
145+ var scriptResponse = await http.get (script);
146+ if (scriptResponse.statusCode != HttpStatus .ok) return null ;
147+ return scriptResponse.body;
148+ }
149+ }
150+
151+ Future <WipConnection > _connect (
152+ Process process, int port, Map <String , String > idToUrl) async {
153+ // Wait for Chrome to be in a ready state.
154+ await process.stderr
155+ .transform (utf8.decoder)
156+ .transform (LineSplitter ())
157+ .firstWhere ((line) => line.startsWith ('DevTools listening' ));
158+
159+ var chromeConnection = ChromeConnection ('localhost' , port);
160+ var tab = (await chromeConnection.getTabs ()).first;
161+ var tabConnection = await tab.connect ();
162+
163+ // Enable debugging.
164+ await tabConnection.debugger.enable ();
165+
166+ // Coverage reports are in terms of scriptIds so keep note of URLs.
167+ tabConnection.debugger.onScriptParsed.listen ((data) {
168+ var script = data.script;
169+ if (script.url.isNotEmpty) idToUrl[script.scriptId] = script.url;
170+ });
171+
172+ // Enable coverage collection.
173+ await tabConnection.debugger.connection.sendCommand ('Profiler.enable' , {});
174+ await tabConnection.debugger.connection.sendCommand (
175+ 'Profiler.startPreciseCoverage' , {'detailed' : true , 'callCount' : false });
176+
177+ return tabConnection;
93178}
0 commit comments