blob: 0b380ce88dc9b759e70b98b0dd26d9457cfaa9ad [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('WidgetInspector smoke test', (WidgetTester tester) async {
// This is a smoke test to verify that adding the inspector doesn't crash.
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
);
expect(true, isTrue); // Expect that we reach here without crashing.
});
testWidgets('WidgetInspector interaction test', (WidgetTester tester) async {
final List<String> log = <String>[];
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
final GlobalKey topButtonKey = new GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
String paragraphText(RenderParagraph paragraph) => paragraph.text.text;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: new Material(
child: new ListView(
children: <Widget>[
new RaisedButton(
key: topButtonKey,
onPressed: () {
log.add('top');
},
child: const Text('TOP'),
),
new RaisedButton(
onPressed: () {
log.add('bottom');
},
child: const Text('BOTTOM'),
),
],
),
),
),
),
);
expect(getInspectorState().selection.current, isNull);
await tester.tap(find.text('TOP'));
await tester.pump();
// Tap intercepted by the inspector
expect(log, equals(<String>[]));
final InspectorSelection selection = getInspectorState().selection;
expect(paragraphText(selection.current), equals('TOP'));
final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject;
expect(selection.candidates.contains(topButton), isTrue);
await tester.tap(find.text('TOP'));
expect(log, equals(<String>['top']));
log.clear();
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>['bottom']));
log.clear();
// Ensure the inspector selection has not changed to bottom.
expect(paragraphText(getInspectorState().selection.current), equals('TOP'));
await tester.tap(find.byKey(selectButtonKey));
await tester.pump();
// We are now back in select mode so tapping the bottom button will have
// not trigger a click but will cause it to be selected.
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>[]));
log.clear();
expect(paragraphText(getInspectorState().selection.current), equals('BOTTOM'));
});
testWidgets('WidgetInspector non-invertible transform regression test', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new Transform(
transform: new Matrix4.identity()..scale(0.0),
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
),
),
);
await tester.tap(find.byType(Transform));
expect(true, isTrue); // Expect that we reach here without crashing.
});
testWidgets('WidgetInspector scroll test', (WidgetTester tester) async {
final Key childKey = new UniqueKey();
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: new ListView(
children: <Widget>[
new Container(
key: childKey,
height: 5000.0,
),
],
),
),
),
);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
// Fling does nothing as are in inspect mode.
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.fling(find.byType(ListView), const Offset(200.0, 0.0), 200.0);
await tester.pump();
// Fling still does nothing as are in inspect mode.
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.tap(find.byType(ListView));
await tester.pump();
expect(getInspectorState().selection.current, isNotNull);
// Now out of inspect mode due to the click.
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-200.0));
await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
});
testWidgets('WidgetInspector long press', (WidgetTester tester) async {
bool didLongPress = false;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
selectButtonBuilder: null,
child: new GestureDetector(
onLongPress: () {
expect(didLongPress, isFalse);
didLongPress = true;
},
child: const Text('target', textDirection: TextDirection.ltr),
),
),
),
);
await tester.longPress(find.text('target'));
// The inspector will swallow the long press.
expect(didLongPress, isFalse);
});
testWidgets('WidgetInspector offstage', (WidgetTester tester) async {
final GlobalKey inspectorKey = new GlobalKey();
final GlobalKey clickTarget = new GlobalKey();
Widget createSubtree({ double width, Key key }) {
return new Stack(
children: <Widget>[
new Positioned(
key: key,
left: 0.0,
top: 0.0,
width: width,
height: 100.0,
child: new Text(width.toString(), textDirection: TextDirection.ltr),
),
],
);
}
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: null,
child: new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 94.0),
),
new OverlayEntry(
opaque: true,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 95.0),
),
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 96.0, key: clickTarget),
),
],
),
),
),
);
await tester.longPress(find.byKey(clickTarget));
// State type is private, hence using dynamic.
final dynamic inspectorState = inspectorKey.currentState;
// The object with width 95.0 wins over the object with width 94.0 because
// the subtree with width 94.0 is offstage.
expect(inspectorState.selection.current.semanticBounds.width, equals(95.0));
// Exactly 2 out of the 3 text elements should be in the candidate list of
// objects to select as only 2 are onstage.
expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2));
});
test('WidgetInspectorService null id', () {
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
expect(service.toObject(null), isNull);
expect(service.toId(null, 'test-group'), isNull);
});
test('WidgetInspectorService dispose group', () {
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final Object a = new Object();
const String group1 = 'group-1';
const String group2 = 'group-2';
const String group3 = 'group-3';
final String aId = service.toId(a, group1);
expect(service.toId(a, group2), equals(aId));
expect(service.toId(a, group3), equals(aId));
service.disposeGroup(group1);
service.disposeGroup(group2);
expect(service.toObject(aId), equals(a));
service.disposeGroup(group3);
expect(() => service.toObject(aId), throwsFlutterError);
});
test('WidgetInspectorService dispose id', () {
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final Object a = new Object();
final Object b = new Object();
const String group1 = 'group-1';
const String group2 = 'group-2';
final String aId = service.toId(a, group1);
final String bId = service.toId(b, group1);
expect(service.toId(a, group2), equals(aId));
service.disposeId(bId, group1);
expect(() => service.toObject(bId), throwsFlutterError);
service.disposeId(aId, group1);
expect(service.toObject(aId), equals(a));
service.disposeId(aId, group2);
expect(() => service.toObject(aId), throwsFlutterError);
});
test('WidgetInspectorService toObjectForSourceLocation', () {
const String group = 'test-group';
const Text widget = const Text('a', textDirection: TextDirection.ltr);
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String id = service.toId(widget, group);
expect(service.toObjectForSourceLocation(id), equals(widget));
final Element element = widget.createElement();
final String elementId = service.toId(element, group);
expect(service.toObjectForSourceLocation(elementId), equals(widget));
expect(element, isNot(equals(widget)));
service.disposeGroup(group);
expect(() => service.toObjectForSourceLocation(elementId), throwsFlutterError);
});
test('WidgetInspectorService object id test', () {
const Text a = const Text('a', textDirection: TextDirection.ltr);
const Text b = const Text('b', textDirection: TextDirection.ltr);
const Text c = const Text('c', textDirection: TextDirection.ltr);
const Text d = const Text('d', textDirection: TextDirection.ltr);
const String group1 = 'group-1';
const String group2 = 'group-2';
const String group3 = 'group-3';
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String aId = service.toId(a, group1);
final String bId = service.toId(b, group2);
final String cId = service.toId(c, group3);
final String dId = service.toId(d, group1);
// Make sure we get a consistent id if we add the object to a group multiple
// times.
expect(aId, equals(service.toId(a, group1)));
expect(service.toObject(aId), equals(a));
expect(service.toObject(aId), isNot(equals(b)));
expect(service.toObject(bId), equals(b));
expect(service.toObject(cId), equals(c));
expect(service.toObject(dId), equals(d));
// Make sure we get a consistent id even if we add the object to a different
// group.
expect(aId, equals(service.toId(a, group3)));
expect(aId, isNot(equals(bId)));
expect(aId, isNot(equals(cId)));
service.disposeGroup(group3);
});
testWidgets('WidgetInspectorService maybeSetSelection', (WidgetTester tester) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
service.selection.clear();
int selectionChangedCount = 0;
service.selectionChangedCallback = () => selectionChangedCount++;
service.setSelection('invalid selection');
expect(selectionChangedCount, equals(0));
expect(service.selection.currentElement, isNull);
service.setSelection(elementA);
expect(selectionChangedCount, equals(1));
expect(service.selection.currentElement, equals(elementA));
expect(service.selection.current, equals(elementA.renderObject));
service.setSelection(elementB.renderObject);
expect(selectionChangedCount, equals(2));
expect(service.selection.current, equals(elementB.renderObject));
expect(service.selection.currentElement, equals(elementB.renderObject.debugCreator.element));
service.setSelection('invalid selection');
expect(selectionChangedCount, equals(2));
expect(service.selection.current, equals(elementB.renderObject));
service.setSelectionById(service.toId(elementA, 'my-group'));
expect(selectionChangedCount, equals(3));
expect(service.selection.currentElement, equals(elementA));
expect(service.selection.current, equals(elementA.renderObject));
service.setSelectionById(service.toId(elementA, 'my-group'));
expect(selectionChangedCount, equals(3));
expect(service.selection.currentElement, equals(elementA));
});
testWidgets('WidgetInspectorService getParentChain', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final Element elementB = find.text('b').evaluate().first;
final String bId = service.toId(elementB, group);
final Object jsonList = json.decode(service.getParentChain(bId, group));
expect(jsonList, isList);
final List<Object> chainElements = jsonList;
final List<Element> expectedChain = elementB.debugGetDiagnosticChain()?.reversed?.toList();
// Sanity check that the chain goes back to the root.
expect(expectedChain.first, tester.binding.renderViewElement);
expect(chainElements.length, equals(expectedChain.length));
for (int i = 0; i < expectedChain.length; i += 1) {
expect(chainElements[i], isMap);
final Map<String, Object> chainNode = chainElements[i];
final Element element = expectedChain[i];
expect(chainNode['node'], isMap);
final Map<String, Object> jsonNode = chainNode['node'];
expect(service.toObject(jsonNode['valueId']), equals(element));
expect(service.toObject(jsonNode['objectId']), const isInstanceOf<DiagnosticsNode>());
expect(chainNode['children'], isList);
final List<Object> jsonChildren = chainNode['children'];
final List<Element> childrenElements = <Element>[];
element.visitChildren(childrenElements.add);
expect(jsonChildren.length, equals(childrenElements.length));
if (i + 1 == expectedChain.length) {
expect(chainNode['childIndex'], isNull);
} else {
expect(chainNode['childIndex'], equals(childrenElements.indexOf(expectedChain[i+1])));
}
for (int j = 0; j < childrenElements.length; j += 1) {
expect(jsonChildren[j], isMap);
final Map<String, Object> childJson = jsonChildren[j];
expect(service.toObject(childJson['valueId']), equals(childrenElements[j]));
expect(service.toObject(childJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
}
});
test('WidgetInspectorService getProperties', () {
final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode();
const String group = 'group';
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String id = service.toId(diagnostic, group);
final List<Object> propertiesJson = json.decode(service.getProperties(id, group));
final List<DiagnosticsNode> properties = diagnostic.getProperties();
expect(properties, isNotEmpty);
expect(propertiesJson.length, equals(properties.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(properties[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a', textDirection: TextDirection.ltr),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode();
final WidgetInspectorService service = WidgetInspectorService.instance;
service.disposeAllGroups();
final String id = service.toId(diagnostic, group);
final List<Object> propertiesJson = json.decode(service.getChildren(id, group));
final List<DiagnosticsNode> children = diagnostic.getChildren();
expect(children.length, equals(3));
expect(propertiesJson.length, equals(children.length));
for (int i = 0; i < propertiesJson.length; ++i) {
final Map<String, Object> propertyJson = propertiesJson[i];
expect(service.toObject(propertyJson['valueId']), equals(children[i].value));
expect(service.toObject(propertyJson['objectId']), const isInstanceOf<DiagnosticsNode>());
}
});
testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
final WidgetInspectorService service = WidgetInspectorService.instance;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
service.disposeAllGroups();
service.setPubRootDirectories(<Object>[]);
service.setSelection(elementA, 'my-group');
final Map<String, Object> jsonA = json.decode(service.getSelectedWidget(null, 'my-group'));
final Map<String, Object> creationLocationA = jsonA['creationLocation'];
expect(creationLocationA, isNotNull);
final String fileA = creationLocationA['file'];
final int lineA = creationLocationA['line'];
final int columnA = creationLocationA['column'];
final List<Object> parameterLocationsA = creationLocationA['parameterLocations'];
service.setSelection(elementB, 'my-group');
final Map<String, Object> jsonB = json.decode(service.getSelectedWidget(null, 'my-group'));
final Map<String, Object> creationLocationB = jsonB['creationLocation'];
expect(creationLocationB, isNotNull);
final String fileB = creationLocationB['file'];
final int lineB = creationLocationB['line'];
final int columnB = creationLocationB['column'];
final List<Object> parameterLocationsB = creationLocationB['parameterLocations'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(fileA, equals(fileB));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(lineA + 1, equals(lineB));
// Column numbers are more stable than line numbers.
expect(columnA, equals(19));
expect(columnA, equals(columnB));
expect(parameterLocationsA.length, equals(1));
final Map<String, Object> paramA = parameterLocationsA[0];
expect(paramA['name'], equals('data'));
expect(paramA['line'], equals(lineA));
expect(paramA['column'], equals(24));
expect(parameterLocationsB.length, equals(2));
final Map<String, Object> paramB1 = parameterLocationsB[0];
expect(paramB1['name'], equals('data'));
expect(paramB1['line'], equals(lineB));
expect(paramB1['column'], equals(24));
final Map<String, Object> paramB2 = parameterLocationsB[1];
expect(paramB2['name'], equals('textDirection'));
expect(paramB2['line'], equals(lineB));
expect(paramB2['column'], equals(29));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async {
final WidgetInspectorService service = WidgetInspectorService.instance;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Stack(
children: const <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
const Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.disposeAllGroups();
service.setPubRootDirectories(<Object>[]);
service.setSelection(elementA, 'my-group');
Map<String, Object> jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
Map<String, Object> creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
final String fileA = creationLocation['file'];
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
final List<String> segments = Uri.parse(fileA).pathSegments;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
service.setPubRootDirectories(<Object>[pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['/invalid/$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>['file://$pubRootTest']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setPubRootDirectories(<Object>['$pubRootTest/different']);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>[
'/invalid/$pubRootTest',
pubRootTest,
]);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
// The RichText child of the Text widget is created by the core framework
// not the current package.
final Element richText = find.descendant(
of: find.text('a'),
matching: find.byType(RichText),
).evaluate().first;
service.setSelection(richText, 'my-group');
service.setPubRootDirectories(<Object>[pubRootTest]);
jsonObject = json.decode(service.getSelectedWidget(null, 'my-group'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
creationLocation = jsonObject['creationLocation'];
expect(creationLocation, isNotNull);
// This RichText widget is created by the build method of the Text widget
// thus the creation location is in text.dart not basic.dart
final List<String> pathSegmentsFramework = Uri.parse(creationLocation['file']).pathSegments;
expect(pathSegmentsFramework.join('/'), endsWith('/packages/flutter/lib/src/widgets/text.dart'));
// Strip off /src/widgets/text.dart.
final String pubRootFramework = '/' + pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/');
service.setPubRootDirectories(<Object>[pubRootFramework]);
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));
service.setPubRootDirectories(<Object>[pubRootFramework, pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
service.setSelection(richText, 'my-group');
expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.
}