blob: 88d512abb7c888e7bf9a30f28b728a9073c27491 [file] [log] [blame] [edit]
/// 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();
});
}