From 834f80d15b5f9d368a5c8473a504525ed7d31cdf Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Sat, 2 May 2020 00:05:31 -0700 Subject: [PATCH 1/2] add several debugger and runtime methods --- analysis_options.yaml | 4 + changelog.md | 7 + lib/src/console.dart | 15 ++- lib/src/debugger.dart | 191 ++++++++++++++++++++++++++-- lib/src/log.dart | 5 +- lib/src/page.dart | 12 +- lib/src/runtime.dart | 60 +++++++-- lib/src/target.dart | 10 +- lib/webkit_inspection_protocol.dart | 22 ++-- pubspec.yaml | 6 +- test/console_test.dart | 2 +- test/data/debugger_test.html | 16 +++ test/debugger_test.dart | 97 ++++++++++++++ test/test_setup.dart | 2 +- 14 files changed, 391 insertions(+), 58 deletions(-) create mode 100644 test/data/debugger_test.html create mode 100644 test/debugger_test.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index e6d99f9..1aa11af 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,4 +8,8 @@ linter: - avoid_init_to_null - directives_ordering - slash_for_doc_comments + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables - prefer_final_fields diff --git a/changelog.md b/changelog.md index 27a9bb7..1d831f5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # webkit_inspection_protocol.dart +## 0.5.1 +- add `Runtime.evaluate` +- add `Debugger.setBreakpoint` +- add `Debugger.removeBreakpoint` +- add `Debugger.evaluateOnCallFrame` +- add `Debugger.getPossibleBreakpoints` + ## 0.5.0+1 - fixed a bug in reading type of `WipScope` diff --git a/lib/src/console.dart b/lib/src/console.dart index e54f300..72d9e27 100644 --- a/lib/src/console.dart +++ b/lib/src/console.dart @@ -9,13 +9,16 @@ import '../webkit_inspection_protocol.dart'; class WipConsole extends WipDomain { WipConsole(WipConnection connection) : super(connection); - Future enable() => sendCommand('Console.enable'); - Future disable() => sendCommand('Console.disable'); - Future clearMessages() => sendCommand('Console.clearMessages'); + Future enable() => sendCommand('Console.enable'); + + Future disable() => sendCommand('Console.disable'); + + Future clearMessages() => sendCommand('Console.clearMessages'); Stream get onMessage => eventStream( 'Console.messageAdded', (WipEvent event) => new ConsoleMessageEvent(event)); + Stream get onCleared => eventStream( 'Console.messagesCleared', (WipEvent event) => new ConsoleClearedEvent(event)); @@ -27,7 +30,9 @@ class ConsoleMessageEvent extends WrappedWipEvent { Map get _message => params['message'] as Map; String get text => _message['text'] as String; + String get level => _message['level'] as String; + String get url => _message['url'] as String; Iterable getStackTrace() { @@ -52,8 +57,12 @@ class WipConsoleCallFrame { WipConsoleCallFrame.fromMap(this._map); int get columnNumber => _map['columnNumber'] as int; + String get functionName => _map['functionName'] as String; + int get lineNumber => _map['lineNumber'] as int; + String get scriptId => _map['scriptId'] as String; + String get url => _map['url'] as String; } diff --git a/lib/src/debugger.dart b/lib/src/debugger.dart index fa4c428..5f5d5c1 100644 --- a/lib/src/debugger.dart +++ b/lib/src/debugger.dart @@ -18,32 +18,140 @@ class WipDebugger extends WipDomain { }); } - Future enable() => sendCommand('Debugger.enable'); - Future disable() => sendCommand('Debugger.disable'); + Future enable() => sendCommand('Debugger.enable'); + + Future disable() => sendCommand('Debugger.disable'); Future getScriptSource(String scriptId) async => (await sendCommand('Debugger.getScriptSource', params: {'scriptId': scriptId})) .result['scriptSource'] as String; - Future pause() => sendCommand('Debugger.pause'); - Future resume() => sendCommand('Debugger.resume'); + Future pause() => sendCommand('Debugger.pause'); + + Future resume() => sendCommand('Debugger.resume'); + + Future stepInto() => sendCommand('Debugger.stepInto'); + + Future stepOut() => sendCommand('Debugger.stepOut'); - Future stepInto() => sendCommand('Debugger.stepInto'); - Future stepOut() => sendCommand('Debugger.stepOut'); - Future stepOver() => sendCommand('Debugger.stepOver'); + Future stepOver() => sendCommand('Debugger.stepOver'); - Future setPauseOnExceptions(PauseState state) => - sendCommand('Debugger.setPauseOnExceptions', - params: {'state': _pauseStateToString(state)}); + Future setPauseOnExceptions(PauseState state) { + return sendCommand('Debugger.setPauseOnExceptions', + params: {'state': _pauseStateToString(state)}); + } + + /// Sets JavaScript breakpoint at a given location. + /// + /// - `location`: Location to set breakpoint in + /// - `condition`: Expression to use as a breakpoint condition. When + /// specified, debugger will only stop on the breakpoint if this expression + /// evaluates to true. + Future setBreakpoint( + WipLocation location, { + String condition, + }) async { + Map params = { + 'location': location.toJsonMap(), + }; + if (condition != null) { + params['condition'] = condition; + } + + final WipResponse response = + await sendCommand('Debugger.setBreakpoint', params: params); + + if (response.result.containsKey('exceptionDetails')) { + throw new ExceptionDetails( + response.result['exceptionDetails'] as Map); + } else { + return new SetBreakpointResponse(response.json); + } + } + + /// Removes JavaScript breakpoint. + Future removeBreakpoint(String breakpointId) { + return sendCommand('Debugger.removeBreakpoint', + params: {'breakpointId': breakpointId}); + } + + /// Evaluates expression on a given call frame. + /// + /// - `callFrameId`: Call frame identifier to evaluate on + /// - `expression`: Expression to evaluate + /// - `returnByValue`: Whether the result is expected to be a JSON object that + /// should be sent by value + Future evaluateOnCallFrame( + String callFrameId, + String expression, { + bool returnByValue, + }) async { + Map params = { + 'callFrameId': callFrameId, + 'expression': expression, + }; + if (returnByValue != null) { + params['returnByValue'] = returnByValue; + } + + final WipResponse response = + await sendCommand('Debugger.evaluateOnCallFrame', params: params); + + if (response.result.containsKey('exceptionDetails')) { + throw new ExceptionDetails( + response.result['exceptionDetails'] as Map); + } else { + return new RemoteObject( + response.result['result'] as Map); + } + } + + /// Returns possible locations for breakpoint. scriptId in start and end range + /// locations should be the same. + /// + /// - `start`: Start of range to search possible breakpoint locations in + /// - `end`: End of range to search possible breakpoint locations in + /// (excluding). When not specified, end of scripts is used as end of range. + /// - `restrictToFunction`: Only consider locations which are in the same + /// (non-nested) function as start. + Future> getPossibleBreakpoints( + WipLocation start, { + WipLocation end, + bool restrictToFunction, + }) async { + Map params = { + 'start': start.toJsonMap(), + }; + if (end != null) { + params['end'] = end.toJsonMap(); + } + if (restrictToFunction != null) { + params['restrictToFunction'] = restrictToFunction; + } + + final WipResponse response = + await sendCommand('Debugger.getPossibleBreakpoints', params: params); + + if (response.result.containsKey('exceptionDetails')) { + throw new ExceptionDetails( + response.result['exceptionDetails'] as Map); + } else { + List locations = response.result['locations']; + return List.from(locations.map((map) => WipBreakLocation(map))); + } + } Stream get onPaused => eventStream( 'Debugger.paused', (WipEvent event) => new DebuggerPausedEvent(event)); + Stream get onGlobalObjectCleared => eventStream( 'Debugger.globalObjectCleared', (WipEvent event) => new GlobalObjectClearedEvent(event)); + Stream get onResumed => eventStream( 'Debugger.resumed', (WipEvent event) => new DebuggerResumedEvent(event)); + Stream get onScriptParsed => eventStream( 'Debugger.scriptParsed', (WipEvent event) => new ScriptParsedEvent(event)); @@ -72,6 +180,8 @@ class ScriptParsedEvent extends WrappedWipEvent { ScriptParsedEvent(WipEvent event) : this.script = new WipScript(event.params), super(event); + + String toString() => script.toString(); } class GlobalObjectClearedEvent extends WrappedWipEvent { @@ -86,6 +196,7 @@ class DebuggerPausedEvent extends WrappedWipEvent { DebuggerPausedEvent(WipEvent event) : super(event); String get reason => params['reason'] as String; + Object get data => params['data']; Iterable getCallFrames() => (params['callFrames'] as List) @@ -100,9 +211,12 @@ class WipCallFrame { WipCallFrame(this._map); String get callFrameId => _map['callFrameId'] as String; + String get functionName => _map['functionName'] as String; + WipLocation get location => new WipLocation(_map['location'] as Map); + WipRemoteObject get thisObject => new WipRemoteObject(_map['this'] as Map); @@ -117,9 +231,24 @@ class WipLocation { WipLocation(this._map); - int get columnNumber => _map['columnNumber'] as int; - int get lineNumber => _map['lineNumber'] as int; - String get scriptId => _map['scriptId'] as String; + WipLocation.fromValues(String scriptId, int lineNumber, {int columnNumber}) + : _map = {} { + _map['scriptId'] = scriptId; + _map['lineNumber'] = lineNumber; + if (columnNumber != null) { + _map['columnNumber'] = columnNumber; + } + } + + String get scriptId => _map['scriptId']; + + int get lineNumber => _map['lineNumber']; + + int get columnNumber => _map['columnNumber']; + + Map toJsonMap() { + return _map; + } String toString() => '[${scriptId}:${lineNumber}:${columnNumber}]'; } @@ -130,10 +259,15 @@ class WipRemoteObject { WipRemoteObject(this._map); String get className => _map['className'] as String; + String get description => _map['description'] as String; + String get objectId => _map['objectId'] as String; + String get subtype => _map['subtype'] as String; + String get type => _map['type'] as String; + Object get value => _map['value']; } @@ -143,12 +277,19 @@ class WipScript { WipScript(this._map); String get scriptId => _map['scriptId'] as String; + String get url => _map['url'] as String; + int get startLine => _map['startLine'] as int; + int get startColumn => _map['startColumn'] as int; + int get endLine => _map['endLine'] as int; + int get endColumn => _map['endColumn'] as int; + bool get isContentScript => _map['isContentScript'] as bool; + String get sourceMapURL => _map['sourceMapURL'] as String; String toString() => '[script ${scriptId}: ${url}]'; @@ -168,3 +309,27 @@ class WipScope { WipRemoteObject get object => new WipRemoteObject(_map['object'] as Map); } + +class WipBreakLocation extends WipLocation { + WipBreakLocation(Map map) : super(map); + + WipBreakLocation.fromValues(String scriptId, int lineNumber, + {int columnNumber, String type}) + : super.fromValues(scriptId, lineNumber, columnNumber: columnNumber) { + if (type != null) { + _map['type'] = type; + } + } + + /// Allowed Values: `debuggerStatement`, `call`, `return`. + String get type => _map['type']; +} + +/// The response from [WipDebugger.setBreakpoint]. +class SetBreakpointResponse extends WipResponse { + SetBreakpointResponse(Map json) : super(json); + + String get breakpointId => result['breakpointId']; + + WipLocation get actualLocation => WipLocation(result['actualLocation']); +} diff --git a/lib/src/log.dart b/lib/src/log.dart index eac0a42..772d453 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -8,8 +8,9 @@ import '../webkit_inspection_protocol.dart'; class WipLog extends WipDomain { WipLog(WipConnection connection) : super(connection); - Future enable() => sendCommand('Log.enable'); - Future disable() => sendCommand('Log.disable'); + Future enable() => sendCommand('Log.enable'); + + Future disable() => sendCommand('Log.disable'); Stream get onEntryAdded => eventStream('Log.entryAdded', (WipEvent event) => new LogEntry(event)); diff --git a/lib/src/page.dart b/lib/src/page.dart index e7e03f9..e8bf1e5 100644 --- a/lib/src/page.dart +++ b/lib/src/page.dart @@ -8,19 +8,19 @@ import '../webkit_inspection_protocol.dart'; class WipPage extends WipDomain { WipPage(WipConnection connection) : super(connection); - Future enable() => sendCommand('Page.enable'); - Future disable() => sendCommand('Page.disable'); + Future enable() => sendCommand('Page.enable'); - Future navigate(String url) => + Future disable() => sendCommand('Page.disable'); + + Future navigate(String url) => sendCommand('Page.navigate', params: {'url': url}); - Future reload({bool ignoreCache, String scriptToEvaluateOnLoad}) { + Future reload( + {bool ignoreCache, String scriptToEvaluateOnLoad}) { var params = {}; - if (ignoreCache != null) { params['ignoreCache'] = ignoreCache; } - if (scriptToEvaluateOnLoad != null) { params['scriptToEvaluateOnLoad'] = scriptToEvaluateOnLoad; } diff --git a/lib/src/runtime.dart b/lib/src/runtime.dart index f223fb9..d93eb03 100644 --- a/lib/src/runtime.dart +++ b/lib/src/runtime.dart @@ -9,15 +9,40 @@ import '../webkit_inspection_protocol.dart'; class WipRuntime extends WipDomain { WipRuntime(WipConnection connection) : super(connection); - Future enable() => sendCommand('Runtime.enable'); + Future enable() => sendCommand('Runtime.enable'); - Future disable() => sendCommand('Runtime.disable'); + Future disable() => sendCommand('Runtime.disable'); /// Evaluates expression on global object. - Future evaluate(String expression) async { - final WipResponse response = await sendCommand('Runtime.evaluate', params: { + /// + /// - `returnByValue`: Whether the result is expected to be a JSON object that + /// should be sent by value. + /// - `contextId`: Specifies in which execution context to perform evaluation. + /// If the parameter is omitted the evaluation will be performed in the + /// context of the inspected page. + /// - `awaitPromise`: Whether execution should await for resulting value and + /// return once awaited promise is resolved. + Future evaluate( + String expression, { + bool returnByValue, + int contextId, + bool awaitPromise, + }) async { + Map params = { 'expression': expression, - }); + }; + if (returnByValue != null) { + params['returnByValue'] = returnByValue; + } + if (contextId != null) { + params['contextId'] = contextId; + } + if (awaitPromise != null) { + params['awaitPromise'] = awaitPromise; + } + + final WipResponse response = + await sendCommand('Runtime.evaluate', params: params); if (response.result.containsKey('exceptionDetails')) { throw new ExceptionDetails( @@ -30,28 +55,37 @@ class WipRuntime extends WipDomain { /// Calls function with given declaration on the given object. Object group of /// the result is inherited from the target object. + /// + /// Each element in [arguments] must be either a [RemoteObject] or a primitive + /// object (int, String, double, bool). Future callFunctionOn( String functionDeclaration, { + List arguments, String objectId, + bool returnByValue, int executionContextId, - List arguments, }) async { Map params = { 'functionDeclaration': functionDeclaration, }; - if (objectId != null) { params['objectId'] = objectId; } - + if (returnByValue != null) { + params['returnByValue'] = returnByValue; + } if (executionContextId != null) { params['executionContextId'] = executionContextId; } - - if (objectId != null) { - // Convert to a ist of CallArguments. + if (arguments != null) { + // Convert to a list of RemoteObjects and primitive values to + // CallArguments. params['arguments'] = arguments.map((dynamic value) { - return {'value': value}; + if (value is RemoteObject) { + return {'objectId': value.objectId}; + } else { + return {'value': value}; + } }).toList(); } @@ -110,6 +144,8 @@ class ExceptionDetails { ExceptionDetails(this._map); + Map get json => _map; + /// Exception id. int get exceptionId => _map['exceptionId'] as int; diff --git a/lib/src/target.dart b/lib/src/target.dart index 7e1577d..5b51382 100644 --- a/lib/src/target.dart +++ b/lib/src/target.dart @@ -20,7 +20,7 @@ class WipTarget extends WipDomain { } /// Activates (focuses) the target. - Future activateTarget(String targetId) => + Future activateTarget(String targetId) => sendCommand('Target.activateTarget', params: {'targetId': targetId}); /// Closes the target. If the target is a page that gets closed too. @@ -41,19 +41,17 @@ class WipTarget extends WipDomain { /// - binding.onmessage = json => handleMessage(json) - a callback that will /// be called for the protocol notifications and command responses. @experimental - Future exposeDevToolsProtocol( + Future exposeDevToolsProtocol( String targetId, { String bindingName, - }) async { + }) { final Map params = {'targetId': targetId}; if (bindingName != null) { params['bindingName'] = bindingName; } - final WipResponse response = await sendCommand( + return sendCommand( 'Target.exposeDevToolsProtocol', params: params, ); - dynamic foo = await response.result['targetId']; - print(foo); } } diff --git a/lib/webkit_inspection_protocol.dart b/lib/webkit_inspection_protocol.dart index 358c951..cfc4257 100644 --- a/lib/webkit_inspection_protocol.dart +++ b/lib/webkit_inspection_protocol.dart @@ -69,7 +69,7 @@ class ChromeConnection { rethrow; } } - await new Future.delayed(new Duration(milliseconds: 25)); + await new Future.delayed(const Duration(milliseconds: 25)); } } @@ -244,10 +244,12 @@ class WipEvent { } class WipError { + final Map json; + final int id; final dynamic error; - WipError(Map json) + WipError(this.json) : id = json['id'] as int, error = json['error']; @@ -255,10 +257,12 @@ class WipError { } class WipResponse { + final Map json; + final int id; final Map result; - WipResponse(Map json) + WipResponse(this.json) : id = json['id'] as int, result = json['result'] as Map; @@ -290,12 +294,12 @@ abstract class WipDomain { .putIfAbsent( method, () => new StreamTransformer.fromHandlers( - handleData: (WipEvent event, EventSink sink) { - if (event.method == method) { - sink.add(transformer(event)); - } - }, - ).bind(connection.onNotification), + handleData: (WipEvent event, EventSink sink) { + if (event.method == method) { + sink.add(transformer(event)); + } + }, + ).bind(connection.onNotification), ) .cast(); } diff --git a/pubspec.yaml b/pubspec.yaml index 5676af6..3824d82 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,7 @@ name: webkit_inspection_protocol -version: 0.5.0+1 +version: 0.5.1 description: A client for the Chrome DevTools Protocol (previously called the Webkit Inspection Protocol). - homepage: https://github.com/google/webkit_inspection_protocol.dart -authors: -- Devon Carew -- Marc Fisher environment: sdk: '>=2.0.0 <3.0.0' diff --git a/test/console_test.dart b/test/console_test.dart index f0375a9..46c5abf 100644 --- a/test/console_test.dart +++ b/test/console_test.dart @@ -19,7 +19,7 @@ void main() { Future checkMessages(int expectedCount) async { // make sure all messages have been delivered - await new Future.delayed(new Duration(seconds: 1)); + await new Future.delayed(const Duration(seconds: 1)); expect(events, hasLength(expectedCount)); for (int i = 0; i < expectedCount; i++) { if (i == 0) { diff --git a/test/data/debugger_test.html b/test/data/debugger_test.html new file mode 100644 index 0000000..b40e0cd --- /dev/null +++ b/test/data/debugger_test.html @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/test/debugger_test.dart b/test/debugger_test.dart new file mode 100644 index 0000000..8c2c265 --- /dev/null +++ b/test/debugger_test.dart @@ -0,0 +1,97 @@ +// Copyright 2020 Google. All rights reserved. Use of this source code is +// governed by a BSD-style license that can be found in the LICENSE file. + +@TestOn('vm') +library wip.debugger_test; + +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; + +import 'test_setup.dart'; + +void main() { + group('WipDebugger', () { + WipDebugger debugger; + List subs = []; + + setUp(() async { + debugger = (await wipConnection).debugger; + }); + + tearDown(() async { + await debugger.disable(); + debugger = null; + + await closeConnection(); + subs.forEach((s) => s.cancel()); + subs.clear(); + }); + + test('gets script events', () async { + final controller = StreamController(); + subs.add(debugger.onScriptParsed.listen(controller.add)); + + await debugger.enable(); + await navigateToPage('debugger_test.html'); + + expect(controller.stream.first, isNotNull); + }); + + test('getScriptSource', () async { + final controller = StreamController(); + subs.add(debugger.onScriptParsed.listen(controller.add)); + + await debugger.enable(); + await navigateToPage('debugger_test.html'); + + final event = await controller.stream + .firstWhere((event) => event.script.url.endsWith('.html')); + expect(event.script.scriptId, isNotEmpty); + + final source = await debugger.getScriptSource(event.script.scriptId); + expect(source, isNotEmpty); + }); + + test('getPossibleBreakpoints', () async { + final controller = StreamController(); + subs.add(debugger.onScriptParsed.listen(controller.add)); + + await debugger.enable(); + await navigateToPage('debugger_test.html'); + + final event = await controller.stream + .firstWhere((event) => event.script.url.endsWith('.html')); + expect(event.script.scriptId, isNotEmpty); + + final script = event.script; + + final result = await debugger + .getPossibleBreakpoints(WipLocation.fromValues(script.scriptId, 0)); + expect(result, isNotEmpty); + expect(result.any((bp) => bp.lineNumber == 10), true); + }); + + test('setBreakpoint / removeBreakpoint', () async { + final controller = StreamController(); + subs.add(debugger.onScriptParsed.listen(controller.add)); + + await debugger.enable(); + await navigateToPage('debugger_test.html'); + + final event = await controller.stream + .firstWhere((event) => event.script.url.endsWith('.html')); + expect(event.script.scriptId, isNotEmpty); + + final script = event.script; + + final bpResult = await debugger + .setBreakpoint(WipLocation.fromValues(script.scriptId, 10)); + expect(bpResult.breakpointId, isNotEmpty); + + final result = await debugger.removeBreakpoint(bpResult.breakpointId); + expect(result.result, isEmpty); + }); + }); +} diff --git a/test/test_setup.dart b/test/test_setup.dart index 940a5cb..5f478b2 100644 --- a/test/test_setup.dart +++ b/test/test_setup.dart @@ -106,7 +106,7 @@ Future navigateToPage(String page) async { await (await wipConnection) .page .navigate((await _testServerUri).resolve(page).toString()); - await new Future.delayed(new Duration(seconds: 1)); + await new Future.delayed(const Duration(seconds: 1)); return wipConnection; } From 97fc143a6286cdeddbb55dfbe7fd1ae985d2e6f4 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Sat, 2 May 2020 00:08:08 -0700 Subject: [PATCH 2/2] fix comment --- lib/src/runtime.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/runtime.dart b/lib/src/runtime.dart index d93eb03..2c57014 100644 --- a/lib/src/runtime.dart +++ b/lib/src/runtime.dart @@ -78,8 +78,7 @@ class WipRuntime extends WipDomain { params['executionContextId'] = executionContextId; } if (arguments != null) { - // Convert to a list of RemoteObjects and primitive values to - // CallArguments. + // Convert a list of RemoteObjects and primitive values to CallArguments. params['arguments'] = arguments.map((dynamic value) { if (value is RemoteObject) { return {'objectId': value.objectId};