| /// Debugger custom formatter tests. | |
| /// If the tests fail, paste the expected output into the golden file and audit | |
| /// the diff to ensure changes are expected. | |
| /// | |
| /// Currently only DDC supports debugging objects with custom formatters | |
| /// but it is reasonable to add support to Dart2JS in the future. | |
| @JS() | |
| library debugger_test; | |
| import 'dart:html'; | |
| import 'dart:js_interop'; | |
| import 'dart:js_interop_unsafe'; | |
| import 'package:expect/async_helper.dart'; | |
| import 'package:expect/legacy/minitest.dart'; // ignore: deprecated_member_use_from_same_package | |
| import 'package:js/js.dart' as pkgJs; | |
| import 'dart:_debugger' as _debugger; | |
| import 'dart:_foreign_helper' as _foreign_helper; | |
| class TestClass { | |
| String name = 'test class'; | |
| int date; | |
| static List<int> foo = [1, 2, 3, 4]; | |
| static String greeting = 'Hello world'; | |
| static Object bar = new Object(); | |
| static exampleStaticMethod(x) => x * 2; | |
| TestClass(this.date); | |
| String nameAndDate() => '$name on day $date'; | |
| int last(List<int> list) => list.last; | |
| void addOne(String name) { | |
| name = '${name}1'; | |
| } | |
| get someInt => 42; | |
| get someString => "Hello world"; | |
| get someObject => this; | |
| Object returnObject() => bar; | |
| } | |
| class TestGenericClass<X, Y> { | |
| TestGenericClass(this.x); | |
| X x; | |
| } | |
| class FormattedObject { | |
| FormattedObject(this.object, this.config); | |
| JSAny? object; | |
| JSAny? config; | |
| } | |
| @JS() | |
| external JSAny? get dartDevEmbedder; | |
| @JS('JSON.stringify') | |
| external String? stringify(JSAny? value, [JSFunction replacer, int space]); | |
| @JS('Object.getOwnPropertyNames') | |
| external JSArray<JSString> getOwnPropertyNames(JSObject obj); | |
| @JS() | |
| external JSArray? get devtoolsFormatters; | |
| @JS('Object.getPrototypeOf') | |
| external Prototype getPrototypeOf(JSAny obj); | |
| extension type FormattedJSObject._(JSObject _) implements JSObject { | |
| external JSAny? get object; | |
| external JSAny? get config; | |
| } | |
| // We use `JSAny` here since we're using this to interop with the prototype of a | |
| // Dart class, which isn't a `JSObject`. | |
| extension type Prototype._(JSAny _) implements JSAny { | |
| external JSAny get constructor; | |
| } | |
| extension type FooBar._(JSObject _) implements JSObject { | |
| external FooBar({String foo}); | |
| } | |
| @pkgJs.JS() | |
| class PackageJSClass<T> { | |
| external factory PackageJSClass(T x); | |
| } | |
| T unsafeCast<T extends JSAny?>(Object? object) { | |
| // This is improper interop code. However, this test mixes Dart and JS values. | |
| // Since this is only ever run on DDC, this is okay, but we should be | |
| // deliberate about where we're mixing Dart and JS values. | |
| return object as T; | |
| } | |
| // Replacer normalizes file names that could vary depending on the test runner | |
| // styles. | |
| JSAny? replacer(String key, JSAny? externalValue) { | |
| // The values for keys with name 'object' may be arbitrary Dart nested | |
| // Objects so are not safe to stringify. | |
| if (key == 'object') return '<OBJECT>'.toJS; | |
| if (externalValue.isA<JSString>()) { | |
| final value = (externalValue as JSString).toDart; | |
| if (value.contains('dart_sdk.js')) return '<DART_SDK>'.toJS; | |
| if (new RegExp(r'[.](js|dart|html)').hasMatch(value)) { | |
| return '<FILE>'.toJS; | |
| } | |
| } | |
| return externalValue; | |
| } | |
| String? format(JSAny? value) { | |
| // Avoid double-escaping strings. | |
| if (value.isA<JSString>()) return (value as JSString).toDart; | |
| return stringify(value, replacer.toJS, 4); | |
| } | |
| /// Extract all object tags from a json ml expression to enable | |
| /// calling the custom formatter on the extracted object tag. | |
| List<FormattedObject> extractNestedFormattedObjects(JSAny json) { | |
| var ret = <FormattedObject>[]; | |
| if (json.isA<JSString>() || json.isA<JSBoolean>() || json.isA<JSNumber>()) { | |
| return ret; | |
| } | |
| if (json.isA<JSArray>()) { | |
| for (var i = 0; i < (json as JSArray<JSAny>).length; i++) { | |
| ret.addAll(extractNestedFormattedObjects(json[i])); | |
| } | |
| return ret; | |
| } | |
| // Must be a JS object. See JsonMLElement in dart:_debugger. | |
| final jsObject = json as FormattedJSObject; | |
| final propertyNames = getOwnPropertyNames(jsObject); | |
| for (var i = 0; i < propertyNames.length; i++) { | |
| final name = propertyNames[i].toDart; | |
| if (name == 'object') { | |
| // Found a nested formatted object. | |
| ret.add(new FormattedObject(jsObject.object, jsObject.config)); | |
| return ret; | |
| } | |
| ret.addAll(extractNestedFormattedObjects(jsObject[name]!)); | |
| } | |
| return ret; | |
| } | |
| JSObject getCurrentLibrary() => | |
| // With the new module format, we can't get the current library, so this is | |
| // a workaround to fetch it. Note that this is run in the top-level scope. | |
| // We can't use interop for this, as the lowering would be emitted as | |
| // `dart.global.eval('this')`, which does not evaluate to the same value as | |
| // `eval('this')`. | |
| _foreign_helper.JS('', 'this'); | |
| main() async { | |
| asyncStart(); | |
| if (devtoolsFormatters == null) { | |
| print("Warning: no devtools custom formatters specified. Skipping tests."); | |
| return; | |
| } | |
| // Cache blocker is a workaround for: | |
| // https://code.google.com/p/dart/issues/detail?id=11834 | |
| var cacheBlocker = new DateTime.now().millisecondsSinceEpoch; | |
| var embedder_suffix = dartDevEmbedder != null ? '_ddc' : ''; | |
| var goldenUrl = | |
| '/root_dart/tests/dartdevc/debugger/' | |
| 'debugger${embedder_suffix}_test_golden.txt?cacheBlock=$cacheBlocker'; | |
| String? golden; | |
| try { | |
| golden = (await HttpRequest.getString(goldenUrl)).trim(); | |
| } catch (e) { | |
| print("Warning: couldn't load golden file from $goldenUrl"); | |
| } | |
| document.body!.append( | |
| new ScriptElement() | |
| ..type = 'text/javascript' | |
| ..innerHtml = r""" | |
| window.PackageJSClass = function PackageJSClass(x) { | |
| this.x = x; | |
| }; | |
| """, | |
| ); | |
| var _devtoolsFormatter = | |
| (devtoolsFormatters![0] | |
| as ExternalDartReference<_debugger.JsonMLFormatter>) | |
| .toDartObject; | |
| var actual = new StringBuffer(); | |
| // Accumulate the entire expected custom formatted data as a single | |
| // massive string buffer so it is simple to update expectations when | |
| // modifying the formatting code. | |
| // Otherwise a small formatting change would result in tweaking lots | |
| // of expectations. | |
| // The verify golden match test cases does the final comparison of golden | |
| // to expected output. | |
| void addGolden(String name, JSAny value) { | |
| var text = format(value); | |
| actual.write( | |
| 'Test: $name\n' | |
| 'Value:\n' | |
| '$text\n' | |
| '-----------------------------------\n', | |
| ); | |
| } | |
| void addFormatterGoldens(String name, Object? object, [Object? config]) { | |
| addGolden( | |
| '$name formatting header', | |
| _devtoolsFormatter.header(object, config), | |
| ); | |
| addGolden('$name formatting body', _devtoolsFormatter.body(object, config)); | |
| } | |
| // Include goldens for the nested [[class]] definition field. | |
| void addNestedFormatterGoldens(String name, Object obj) { | |
| addGolden('$name instance header', _devtoolsFormatter.header(obj, null)); | |
| var body = _devtoolsFormatter.body(obj, null); | |
| addGolden('$name instance body', body); | |
| var nestedObjects = extractNestedFormattedObjects(body); | |
| var clazz = nestedObjects.last; | |
| // By convention assume last nested object is the [[class]] definition | |
| // describing the object's static members and inheritance hierarchy | |
| addFormatterGoldens('$name definition', clazz.object, clazz.config); | |
| } | |
| // Include goldens for the nested [[class]] definition field. | |
| void addAllNestedFormatterGoldens(String name, Object obj) { | |
| addGolden('$name header', _devtoolsFormatter.header(obj, null)); | |
| // The cast to `JSAny` is safe as `header` and `body` should always return | |
| // JS values. | |
| var body = _devtoolsFormatter.body(obj, null) as JSAny; | |
| addGolden('$name body', body); | |
| var nestedObjects = extractNestedFormattedObjects(body); | |
| var i = 0; | |
| for (var nested in nestedObjects) { | |
| addFormatterGoldens('$name child $i', nested.object, nested.config); | |
| i++; | |
| } | |
| } | |
| group('Iterable formatting', () { | |
| var list = ['foo', 'bar', 'baz']; | |
| var iterable = list.map((x) => x * 5); | |
| addFormatterGoldens('List<String>', list); | |
| var listOfObjects = <Object>[42, 'bar', true]; | |
| addNestedFormatterGoldens('List<Object>', listOfObjects); | |
| var largeList = <int>[]; | |
| for (var i = 0; i < 200; ++i) { | |
| largeList.add(i * 10); | |
| } | |
| addNestedFormatterGoldens('List<int> large', largeList); | |
| addNestedFormatterGoldens('Iterable', iterable); | |
| var s = new Set() | |
| ..add("foo") | |
| ..add(42) | |
| ..add(true); | |
| addNestedFormatterGoldens('Set', s); | |
| }); | |
| group('Map formatting', () { | |
| Map<String, int> foo = new Map(); | |
| foo = {'1': 2, 'x': 4, '5': 6}; | |
| addFormatterGoldens('Map<String, int>', foo); | |
| test('hasBody', () { | |
| expect(_devtoolsFormatter.hasBody(foo, null), isTrue); | |
| }); | |
| Map<dynamic, dynamic> dynamicMap = new Map(); | |
| dynamicMap = {1: 2, 'x': 4, true: "truthy"}; | |
| addNestedFormatterGoldens('Map<dynamic, dynamic>', dynamicMap); | |
| }); | |
| group('Function formatting', () { | |
| adder(int a, int b) => a + b; | |
| addFormatterGoldens('Function', adder); | |
| test('hasBody', () { | |
| expect(_devtoolsFormatter.hasBody(adder, null), isTrue); | |
| }); | |
| addEventListener(String name, bool callback(Event e)) => null; | |
| addFormatterGoldens('Function with function arguments', addEventListener); | |
| // Closure | |
| addGolden('dart:html method', unsafeCast(window.addEventListener)); | |
| // Get a reference to the JS constructor for a Dart class. | |
| // This tracks a regression bug where overly verbose and confusing output | |
| // was shown for this case. | |
| var testClass = new TestClass(17); | |
| var dartConstructor = getPrototypeOf(unsafeCast(testClass)).constructor; | |
| addFormatterGoldens('Raw reference to dart constructor', dartConstructor); | |
| }); | |
| group('Object formatting', () { | |
| var object = new Object(); | |
| addFormatterGoldens('Object', object); | |
| test('hasBody', () { | |
| expect(_devtoolsFormatter.hasBody(object, null), isTrue); | |
| }); | |
| }); | |
| group('Type formatting', () { | |
| addFormatterGoldens('Type TestClass', TestClass); | |
| addFormatterGoldens('Type HttpRequest', HttpRequest); | |
| }); | |
| group('JS interop object formatting', () { | |
| var object = FooBar(foo: 'bar'); | |
| // Make sure we don't apply the Dart custom formatter to JS interop objects. | |
| expect(_devtoolsFormatter.header(object, null), isNull); | |
| }); | |
| group('Library formatting', () { | |
| final lib = getCurrentLibrary(); | |
| addFormatterGoldens( | |
| 'Test library', | |
| // TODO(srujzs): We have to construct a `Library` manually here, | |
| // whereas the `LibraryModuleFormatter` does that for us automatically | |
| // when we format the module. We should add properties to libraries so | |
| // that we can detect them as a library. Once we have support for that, | |
| // revisit this and the formatter code. | |
| _debugger.Library('debugger_test', lib), | |
| ); | |
| }); | |
| group('StackTrace formatting', () { | |
| StackTrace stack; | |
| try { | |
| throw new Error(); | |
| } catch (exception, stackTrace) { | |
| stack = stackTrace; | |
| } | |
| addFormatterGoldens('StackTrace', stack); | |
| test('hasBody', () { | |
| expect(_devtoolsFormatter.hasBody(stack, null), isTrue); | |
| }); | |
| }); | |
| group('Class formatting', () { | |
| addNestedFormatterGoldens('TestClass', new TestClass(17)); | |
| // TODO(jmesserly): this includes a timeStamp, so it varies each run. | |
| //addNestedFormatterGoldens('MouseEvent', new MouseEvent("click")); | |
| // This is a good class to test as it has statics and a deep inheritance hierarchy | |
| addNestedFormatterGoldens('HttpRequest', new HttpRequest()); | |
| }); | |
| group('Generics formatting', () { | |
| addNestedFormatterGoldens( | |
| 'TestGenericClass', | |
| new TestGenericClass<int, List>(42), | |
| ); | |
| addNestedFormatterGoldens( | |
| 'TestGenericClassJSInterop', | |
| new TestGenericClass<PackageJSClass<JSString>, int>( | |
| new PackageJSClass("Hello".toJS), | |
| ), | |
| ); | |
| }); | |
| test('verify golden match', () { | |
| // Warning: all other test groups must have run for this test to be | |
| // meaningful | |
| var actualStr = actual.toString().trim(); | |
| if (actualStr != golden) { | |
| var helpMessage = | |
| 'Debugger output does not match the golden data found in:\n' | |
| 'tests/dartdevc/debugger/debugger_test_golden.txt.\n' | |
| 'The new golden data is copied to the clipboard when you click on ' | |
| 'this window.\n' | |
| 'Please update the golden file with the following output and review ' | |
| 'the diff using your favorite diff tool to make sure the custom ' | |
| 'formatting output has not regressed.'; | |
| print(helpMessage); | |
| // Copy text to clipboard on page click. We can't copy to the clipboard | |
| // without a click due to Chrome security. | |
| var textField = new Element.tag('textarea') as TextAreaElement; | |
| textField.maxLength = 100000000; | |
| textField.text = actualStr; | |
| textField.style | |
| ..width = '800px' | |
| ..height = '400px'; | |
| document.body!.append( | |
| new Element.tag('h3')..innerHtml = helpMessage.replaceAll('\n', '<br>'), | |
| ); | |
| document.body!.append(textField); | |
| document.body!.onClick.listen((_) { | |
| textField.select(); | |
| var result = document.execCommand('copy'); | |
| if (result) { | |
| print("Copy to clipboard successful"); | |
| } else { | |
| print("Copy to clipboard failed"); | |
| } | |
| }); | |
| } | |
| expect(actualStr == golden, isTrue); | |
| asyncEnd(); | |
| }); | |
| } |