blob: 32808e30343a1c29e43c6177e144a91ae70b0d9a [file] [log] [blame] [edit]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// reduced-test-set:
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
@TestOn('!chrome')
library;
import 'dart:convert';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker/leak_tracker.dart';
import '../impeller_test_helpers.dart';
import 'widget_inspector_test_utils.dart';
// Start of block of code where widget creation location line numbers and
// columns will impact whether tests pass.
class ClockDemo extends StatelessWidget {
const ClockDemo({ super.key });
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('World Clock'),
makeClock('Local', DateTime.now().timeZoneOffset.inHours),
makeClock('UTC', 0),
makeClock('New York, NY', -4),
makeClock('Chicago, IL', -5),
makeClock('Denver, CO', -6),
makeClock('Los Angeles, CA', -7),
],
),
);
}
Widget makeClock(String label, int utcOffset) {
return Stack(
children: <Widget>[
const Icon(Icons.watch),
Text(label),
ClockText(utcOffset: utcOffset),
],
);
}
}
class ClockText extends StatefulWidget {
const ClockText({
super.key,
this.utcOffset = 0,
});
final int utcOffset;
@override
State<ClockText> createState() => _ClockTextState();
}
class _ClockTextState extends State<ClockText> {
DateTime? currentTime = DateTime.now();
void updateTime() {
setState(() {
currentTime = DateTime.now();
});
}
void stopClock() {
setState(() {
currentTime = null;
});
}
@override
Widget build(BuildContext context) {
if (currentTime == null) {
return const Text('stopped');
}
return Text(
currentTime!
.toUtc()
.add(Duration(hours: widget.utcOffset))
.toIso8601String(),
);
}
}
// End of block of code where widget creation location line numbers and
// columns will impact whether tests pass.
// Class to enable building trees of nodes with cycles between properties of
// nodes and the properties of those properties.
// This exposed a bug in code serializing DiagnosticsNode objects that did not
// handle these sorts of cycles robustly.
class CyclicDiagnostic extends DiagnosticableTree {
CyclicDiagnostic(this.name);
// Field used to create cyclic relationships.
CyclicDiagnostic? related;
final List<DiagnosticsNode> children = <DiagnosticsNode>[];
final String name;
@override
String toStringShort() => '${objectRuntimeType(this, 'CyclicDiagnostic')}-$name';
// We have to override toString to avoid the toString call itself triggering a
// stack overflow.
@override
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return toStringShort();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<CyclicDiagnostic>('related', related));
}
@override
List<DiagnosticsNode> debugDescribeChildren() => children;
}
class _CreationLocation {
_CreationLocation({
required this.id,
required this.file,
required this.line,
required this.column,
required this.name,
});
final int id;
final String file;
final int line;
final int column;
String? name;
}
class RenderRepaintBoundaryWithDebugPaint extends RenderRepaintBoundary {
@override
void debugPaintSize(PaintingContext context, Offset offset) {
super.debugPaintSize(context, offset);
assert(() {
// Draw some debug paint UI interleaving creating layers and drawing
// directly to the context's canvas.
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = Colors.red;
{
final PictureLayer pictureLayer = PictureLayer(Offset.zero & size);
final ui.PictureRecorder recorder = ui.PictureRecorder();
final Canvas pictureCanvas = Canvas(recorder);
pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
pictureLayer.picture = recorder.endRecording();
context.addLayer(
OffsetLayer()
..offset = offset
..append(pictureLayer),
);
}
context.canvas.drawLine(
offset,
offset.translate(size.width, size.height),
paint,
);
{
final PictureLayer pictureLayer = PictureLayer(Offset.zero & size);
final ui.PictureRecorder recorder = ui.PictureRecorder();
final Canvas pictureCanvas = Canvas(recorder);
pictureCanvas.drawCircle(const Offset(20.0, 20.0), 20.0, paint);
pictureLayer.picture = recorder.endRecording();
context.addLayer(
OffsetLayer()
..offset = offset
..append(pictureLayer),
);
}
paint.color = Colors.blue;
context.canvas.drawLine(
offset,
offset.translate(size.width * 0.5, size.height * 0.5),
paint,
);
return true;
}());
}
}
class RepaintBoundaryWithDebugPaint extends RepaintBoundary {
/// Creates a widget that isolates repaints.
const RepaintBoundaryWithDebugPaint({
super.key,
super.child,
});
@override
RenderRepaintBoundary createRenderObject(BuildContext context) {
return RenderRepaintBoundaryWithDebugPaint();
}
}
Widget _applyConstructor(Widget Function() constructor) => constructor();
class _TrivialWidget extends StatelessWidget {
const _TrivialWidget() : super(key: const Key('singleton'));
@override
Widget build(BuildContext context) => const Text('Hello, world!');
}
int getChildLayerCount(OffsetLayer layer) {
Layer? child = layer.firstChild;
int count = 0;
while (child != null) {
count++;
child = child.nextSibling;
}
return count;
}
extension TextFromString on String {
@widgetFactory
Widget text() {
return Text(this);
}
}
final List<Object> _weakValueTests = <Object>[1, 1.0, 'hello', true, false, Object(), <int>[3, 4], DateTime(2023)];
void main() {
group('$InspectorReferenceData', (){
for (final Object item in _weakValueTests) {
test('can be created for any type but $Record, $item', () async {
final InspectorReferenceData weakValue = InspectorReferenceData(item, 'id');
expect(weakValue.value, item);
});
}
test('throws for $Record', () async {
expect(()=> InspectorReferenceData((1, 2), 'id'), throwsA(isA<ArgumentError>()));
});
});
group('$WeakMap', (){
for (final Object item in _weakValueTests) {
test('assigns and removes value, $item', () async {
final WeakMap<Object, Object> weakMap = WeakMap<Object, Object>();
weakMap[item] = 1;
expect(weakMap[item], 1);
expect(weakMap.remove(item), 1);
expect(weakMap[item], null);
});
}
for (final Object item in _weakValueTests) {
test('returns null for absent value, $item', () async {
final WeakMap<Object, Object> weakMap = WeakMap<Object, Object>();
expect(weakMap[item], null);
});
}
});
_TestWidgetInspectorService.runTests();
}
class _TestWidgetInspectorService extends TestWidgetInspectorService {
// These tests need access to protected members of WidgetInspectorService.
static void runTests() {
final TestWidgetInspectorService service = TestWidgetInspectorService();
WidgetInspectorService.instance = service;
setUp(() {
WidgetInspectorService.instance.isSelectMode.value = true;
});
tearDown(() async {
service.resetAllState();
if (WidgetInspectorService.instance.isWidgetCreationTracked()) {
await service.testBoolExtension(
WidgetInspectorServiceExtensions.trackRebuildDirtyWidgets.name,
<String, String>{'enabled': 'false'},
);
}
});
test ('objectToDiagnosticsNode returns null for non-diagnosticable', () {
expect(WidgetInspectorService.objectToDiagnosticsNode(Alignment.bottomCenter), isNull);
});
test('WidgetInspector does not hold objects from GC', () async {
List<DateTime>? someObject = <DateTime>[DateTime.now(), DateTime.now()];
final String? id = service.toId(someObject, 'group_name');
expect(id, isNotNull);
final WeakReference<Object> ref = WeakReference<Object>(someObject);
someObject = null;
// 1 should be enough for [fullGcCycles], but it is 3 to make sure tests are not flaky.
await forceGC(fullGcCycles: 3);
expect(ref.target, null);
});
testWidgets('WidgetInspector smoke test', (WidgetTester tester) async {
// This is a smoke test to verify that adding the inspector doesn't crash.
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
selectButtonBuilder: null,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
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 = GlobalKey();
final GlobalKey inspectorKey = GlobalKey();
final GlobalKey topButtonKey = GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null));
}
String paragraphText(RenderParagraph paragraph) {
final TextSpan textSpan = paragraph.text as TextSpan;
return textSpan.text!;
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: Material(
child: ListView(
children: <Widget>[
ElevatedButton(
key: topButtonKey,
onPressed: () {
log.add('top');
},
child: const Text('TOP'),
),
ElevatedButton(
onPressed: () {
log.add('bottom');
},
child: const Text('BOTTOM'),
),
],
),
),
),
),
);
expect(WidgetInspectorService.instance.selection.current, isNull);
await tester.tap(find.text('TOP'), warnIfMissed: false);
await tester.pump();
// Tap intercepted by the inspector
expect(log, equals(<String>[]));
expect(
paragraphText(
WidgetInspectorService.instance.selection.current! as RenderParagraph,
),
equals('TOP'),
);
final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject!;
expect(
WidgetInspectorService.instance.selection.candidates,
contains(topButton),
);
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(
WidgetInspectorService.instance.selection.current! as RenderParagraph,
),
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'), warnIfMissed: false);
expect(log, equals(<String>[]));
log.clear();
expect(
paragraphText(
WidgetInspectorService.instance.selection.current! as RenderParagraph,
),
equals('BOTTOM'),
);
});
testWidgets('WidgetInspector non-invertible transform regression test', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
selectButtonBuilder: null,
child: Transform(
transform: Matrix4.identity()..scale(0.0),
child: const Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
),
),
);
await tester.tap(find.byType(Transform), warnIfMissed: false);
expect(true, isTrue); // Expect that we reach here without crashing.
});
testWidgets('WidgetInspector scroll test', (WidgetTester tester) async {
final Key childKey = UniqueKey();
final GlobalKey selectButtonKey = GlobalKey();
final GlobalKey inspectorKey = GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null));
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: ListView(
dragStartBehavior: DragStartBehavior.down,
children: <Widget>[
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, warnIfMissed: false);
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, warnIfMissed: false);
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), warnIfMissed: false);
await tester.pump();
expect(WidgetInspectorService.instance.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(
Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
selectButtonBuilder: null,
child: GestureDetector(
onLongPress: () {
expect(didLongPress, isFalse);
didLongPress = true;
},
child: const Text('target', textDirection: TextDirection.ltr),
),
),
),
);
await tester.longPress(find.text('target'), warnIfMissed: false);
// The inspector will swallow the long press.
expect(didLongPress, isFalse);
});
testWidgets('WidgetInspector offstage', (WidgetTester tester) async {
final GlobalKey inspectorKey = GlobalKey();
final GlobalKey clickTarget = GlobalKey();
Widget createSubtree({ double? width, Key? key }) {
return Stack(
children: <Widget>[
Positioned(
key: key,
left: 0.0,
top: 0.0,
width: width,
height: 100.0,
child: Text(width.toString(), textDirection: TextDirection.ltr),
),
],
);
}
late final OverlayEntry entry1;
addTearDown(() => entry1..remove()..dispose());
late final OverlayEntry entry2;
addTearDown(() => entry2..remove()..dispose());
late final OverlayEntry entry3;
addTearDown(() => entry3..remove()..dispose());
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
key: inspectorKey,
selectButtonBuilder: null,
child: Overlay(
initialEntries: <OverlayEntry>[
entry1 = OverlayEntry(
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 94.0),
),
entry2 = OverlayEntry(
opaque: true,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 95.0),
),
entry3 = OverlayEntry(
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 96.0, key: clickTarget),
),
],
),
),
),
);
await tester.longPress(find.byKey(clickTarget), warnIfMissed: false);
// The object with width 95.0 wins over the object with width 94.0 because
// the subtree with width 94.0 is offstage.
expect(
WidgetInspectorService.instance.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(
WidgetInspectorService.instance.selection.candidates
.whereType<RenderParagraph>()
.length,
equals(2),
);
});
testWidgets('WidgetInspector with Transform above', (WidgetTester tester) async {
final GlobalKey childKey = GlobalKey();
final GlobalKey repaintBoundaryKey = GlobalKey();
final Matrix4 mainTransform = Matrix4.identity()
..translate(50.0, 30.0)
..scale(0.8, 0.8)
..translate(100.0, 50.0);
await tester.pumpWidget(
RepaintBoundary(
key: repaintBoundaryKey,
child: ColoredBox(
color: Colors.grey,
child: Transform(
transform: mainTransform,
child: Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
selectButtonBuilder: null,
child: ColoredBox(
color: Colors.white,
child: Center(
child: Container(
key: childKey,
height: 100.0,
width: 50.0,
color: Colors.red,
),
),
),
),
),
),
),
),
);
await tester.tap(find.byKey(childKey), warnIfMissed: false);
await tester.pump();
await expectLater(
find.byKey(repaintBoundaryKey),
matchesGoldenFile('inspector.overlay_positioning_with_transform.png'),
);
});
testWidgets('Multiple widget inspectors', (WidgetTester tester) async {
// This test verifies that interacting with different inspectors
// works correctly. This use case may be an app that displays multiple
// apps inside (i.e. a storyboard).
final GlobalKey selectButton1Key = GlobalKey();
final GlobalKey selectButton2Key = GlobalKey();
final GlobalKey inspector1Key = GlobalKey();
final GlobalKey inspector2Key = GlobalKey();
final GlobalKey child1Key = GlobalKey();
final GlobalKey child2Key = GlobalKey();
InspectorSelectButtonBuilder selectButtonBuilder(Key key) {
return (BuildContext context, VoidCallback onPressed) {
return Material(child: ElevatedButton(onPressed: onPressed, key: key, child: null));
};
}
String paragraphText(RenderParagraph paragraph) {
final TextSpan textSpan = paragraph.text as TextSpan;
return textSpan.text!;
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
Flexible(
child: WidgetInspector(
key: inspector1Key,
selectButtonBuilder: selectButtonBuilder(selectButton1Key),
child: Container(
key: child1Key,
child: const Text('Child 1'),
),
),
),
Flexible(
child: WidgetInspector(
key: inspector2Key,
selectButtonBuilder: selectButtonBuilder(selectButton2Key),
child: Container(
key: child2Key,
child: const Text('Child 2'),
),
),
),
],
),
),
);
await tester.tap(find.text('Child 1'), warnIfMissed: false);
await tester.pump();
expect(
paragraphText(
WidgetInspectorService.instance.selection.current! as RenderParagraph,
),
equals('Child 1'),
);
// Re-enable select mode since it's state is shared between the
// WidgetInspectors
WidgetInspectorService.instance.isSelectMode.value = true;
await tester.tap(find.text('Child 2'), warnIfMissed: false);
await tester.pump();
expect(
paragraphText(
WidgetInspectorService.instance.selection.current! as RenderParagraph,
),
equals('Child 2'),
);
});
test('WidgetInspectorService null id', () {
service.disposeAllGroups();
expect(service.toObject(null), isNull);
expect(service.toId(null, 'test-group'), isNull);
});
test('WidgetInspectorService dispose group', () {
service.disposeAllGroups();
final Object a = 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', () {
service.disposeAllGroups();
final Object a = Object();
final Object b = 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 = Text('a', textDirection: TextDirection.ltr);
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 = Text('a', textDirection: TextDirection.ltr);
const Text b = Text('b', textDirection: TextDirection.ltr);
const Text c = Text('c', textDirection: TextDirection.ltr);
const Text d = Text('d', textDirection: TextDirection.ltr);
const String group1 = 'group-1';
const String group2 = 'group-2';
const String group3 = 'group-3';
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(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
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! as 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 defunct selection regression test', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA);
expect(service.selection.currentElement, equals(elementA));
expect(service.selection.current, equals(elementA.renderObject));
await tester.pumpWidget(
const SizedBox(
child: Text('b', textDirection: TextDirection.ltr),
),
);
// Selection is now empty as the element is defunct.
expect(service.selection.currentElement, equals(null));
expect(service.selection.current, equals(null));
// Verify that getting the debug creation location of the defunct element
// does not crash.
expect(debugIsLocalCreationLocation(elementA), isFalse);
// Verify that generating json for a defunct element does not crash.
expect(
elementA.toDiagnosticsNode().toJsonMap(
InspectorSerializationDelegate(
service: service,
includeProperties: true,
),
),
isNotNull,
);
final Element elementB = find.text('b').evaluate().first;
service.setSelection(elementB);
expect(service.selection.currentElement, equals(elementB));
expect(service.selection.current, equals(elementB.renderObject));
// Set selection back to a defunct element.
service.setSelection(elementA);
expect(service.selection.currentElement, equals(null));
expect(service.selection.current, equals(null));
});
testWidgets('WidgetInspectorService getParentChain', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
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! as List<Object?>;
final List<Element> expectedChain = elementB.debugGetDiagnosticChain().reversed.toList();
// Sanity check that the chain goes back to the root.
expect(expectedChain.first, tester.binding.rootElement);
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]! as Map<String, Object?>;
final Element element = expectedChain[i];
expect(chainNode['node'], isMap);
final Map<String, Object?> jsonNode = chainNode['node']! as Map<String, Object?>;
expect(service.toObject(jsonNode['valueId']! as String), equals(element));
expect(chainNode['children'], isList);
final List<Object?> jsonChildren = chainNode['children']! as List<Object?>;
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]! as Map<String, Object?>;
expect(service.toObject(childJson['valueId']! as String), equals(childrenElements[j]));
}
}
});
test('WidgetInspectorService getProperties', () {
const Diagnosticable diagnosticable = Text('a', textDirection: TextDirection.ltr);
const String group = 'group';
service.disposeAllGroups();
final String id = service.toId(diagnosticable, group)!;
final List<Object?> propertiesJson = json.decode(service.getProperties(id, group)) as List<Object?>;
final List<DiagnosticsNode> properties = diagnosticable.toDiagnosticsNode().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]! as Map<String, Object?>;
expect(service.toObject(propertyJson['valueId'] as String?), equals(properties[i].value));
}
});
testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode();
service.disposeAllGroups();
final String id = service.toId(diagnostic, group)!;
final List<Object?> propertiesJson = json.decode(service.getChildren(id, group)) as List<Object?>;
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]! as Map<String, Object?>;
expect(service.toObject(propertyJson['valueId']! as String), equals(children[i].value));
}
});
testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
const Text('a'),
const Text('b', textDirection: TextDirection.ltr),
'c'.text(),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
final Element elementC = find.text('c').evaluate().first;
service.disposeAllGroups();
service.resetPubRootDirectories();
service.setSelection(elementA, 'my-group');
final Map<String, Object?> jsonA = json.decode(service.getSelectedWidget(null, 'my-group')) as Map<String, Object?>;
final Map<String, Object?> creationLocationA = jsonA['creationLocation']! as Map<String, Object?>;
expect(creationLocationA, isNotNull);
final String fileA = creationLocationA['file']! as String;
final int lineA = creationLocationA['line']! as int;
final int columnA = creationLocationA['column']! as int;
final String nameA = creationLocationA['name']! as String;
expect(nameA, equals('Text'));
service.setSelection(elementB, 'my-group');
final Map<String, Object?> jsonB = json.decode(service.getSelectedWidget(null, 'my-group')) as Map<String, Object?>;
final Map<String, Object?> creationLocationB = jsonB['creationLocation']! as Map<String, Object?>;
expect(creationLocationB, isNotNull);
final String fileB = creationLocationB['file']! as String;
final int lineB = creationLocationB['line']! as int;
final int columnB = creationLocationB['column']! as int;
final String? nameB = creationLocationB['name'] as String?;
expect(nameB, equals('Text'));
service.setSelection(elementC, 'my-group');
final Map<String, Object?> jsonC = json.decode(service.getSelectedWidget(null, 'my-group')) as Map<String, Object?>;
final Map<String, Object?> creationLocationC = jsonC['creationLocation']! as Map<String, Object?>;
expect(creationLocationC, isNotNull);
final String fileC = creationLocationC['file']! as String;
final int lineC = creationLocationC['line']! as int;
final int columnC = creationLocationC['column']! as int;
final String? nameC = creationLocationC['name'] as String?;
expect(nameC, equals('TextFromString|text'));
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(fileA, equals(fileB));
expect(fileA, equals(fileC));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(lineA + 1, equals(lineB));
expect(lineB + 1, equals(lineC));
// Column numbers are more stable than line numbers.
expect(columnA, equals(21));
expect(columnA, equals(columnB));
expect(columnC, equals(19));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
testWidgets('WidgetInspectorService setSelection notifiers for an Element',
(WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.disposeAllGroups();
setupDefaultPubRootDirectory(service);
// Select the widget
service.setSelection(elementA, 'my-group');
// ensure that developer.inspect was called on the widget
final List<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<Element>[elementA]));
// ensure that a navigate event was sent for the element
final List<Map<Object, Object?>> navigateEventsPosted
= service.dispatchedEvents('navigate', stream: 'ToolEvent',);
expect(navigateEventsPosted.length, equals(1));
final Map<Object,Object?> event = navigateEventsPosted[0];
final String file = event['fileUri']! as String;
final int line = event['line']! as int;
final int column = event['column']! as int;
expect(file, endsWith('widget_inspector_test.dart'));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(line, isNotNull);
// Column numbers are more stable than line numbers.
expect(column, equals(15));
},
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag.
);
testWidgets(
'WidgetInspectorService setSelection notifiers for a RenderObject',
(WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.disposeAllGroups();
setupDefaultPubRootDirectory(service);
// Select the render object for the widget.
service.setSelection(elementA.renderObject, 'my-group');
// ensure that developer.inspect was called on the widget
final List<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<RenderObject?>[elementA.renderObject]));
// ensure that a navigate event was sent for the renderObject
final List<Map<Object, Object?>> navigateEventsPosted
= service.dispatchedEvents('navigate', stream: 'ToolEvent',);
expect(navigateEventsPosted.length, equals(1));
final Map<Object,Object?> event = navigateEventsPosted[0];
final String file = event['fileUri']! as String;
final int line = event['line']! as int;
final int column = event['column']! as int;
expect(file, endsWith('widget_inspector_test.dart'));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(line, isNotNull);
// Column numbers are more stable than line numbers.
expect(column, equals(17));
},
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(), // [intended] Test requires --track-widget-creation flag.
);
testWidgets(
'WidgetInspector selectButton inspection for tap',
(WidgetTester tester) async {
final GlobalKey selectButtonKey = GlobalKey();
final GlobalKey inspectorKey = GlobalKey();
setupDefaultPubRootDirectory(service);
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null));
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: const Text('Child 1'),
),
),
);
final Finder child = find.text('Child 1');
final Element childElement = child.evaluate().first;
await tester.tap(child, warnIfMissed: false);
await tester.pump();
// ensure that developer.inspect was called on the widget
final List<Object?> objectsInspected = service.inspectedObjects();
expect(objectsInspected, equals(<RenderObject?>[childElement.renderObject]));
// ensure that a navigate event was sent for the renderObject
final List<Map<Object, Object?>> navigateEventsPosted
= service.dispatchedEvents('navigate', stream: 'ToolEvent',);
expect(navigateEventsPosted.length, equals(1));
final Map<Object,Object?> event = navigateEventsPosted[0];
final String file = event['fileUri']! as String;
final int line = event['line']! as int;
final int column = event['column']! as int;
expect(file, endsWith('widget_inspector_test.dart'));
// We don't hardcode the actual lines the widgets are created on as that
// would make this test fragile.
expect(line, isNotNull);
// Column numbers are more stable than line numbers.
expect(column, equals(28));
},
skip: !WidgetInspectorService.instance.isWidgetCreationTracked() // [intended] Test requires --track-widget-creation flag.
);
testWidgets('test transformDebugCreator will re-order if after stack trace', (WidgetTester tester) async {
final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked();
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
late String pubRootTest;
if (widgetTracked) {
final Map<String, Object?> jsonObject = json.decode(
service.getSelectedWidget(null, 'my-group'),
) as Map<String, Object?>;
final Map<String, Object?> creationLocation = jsonObject['creationLocation']! as Map<String, Object?>;
expect(creationLocation, isNotNull);
final String fileA = creationLocation['file']! as String;
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.
pubRootTest = '/${segments.take(segments.length - 2).join('/')}';
service.resetPubRootDirectories();
service.addPubRootDirectories(<String>[pubRootTest]);
}
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
builder.add(StringProperty('dummy1', 'value'));
builder.add(StringProperty('dummy2', 'value'));
builder.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', null));
builder.add(DiagnosticsDebugCreator(DebugCreator(elementA)));
final List<DiagnosticsNode> nodes = List<DiagnosticsNode>.from(debugTransformDebugCreator(builder.properties));
expect(nodes.length, 5);
expect(nodes[0].runtimeType, StringProperty);
expect(nodes[0].name, 'dummy1');
expect(nodes[1].runtimeType, StringProperty);
expect(nodes[1].name, 'dummy2');
// transformed node should come in front of stack trace.
if (widgetTracked) {
expect(nodes[2].runtimeType, DiagnosticsBlock);
final DiagnosticsBlock node = nodes[2] as DiagnosticsBlock;
final List<DiagnosticsNode> children = node.getChildren();
expect(children.length, 1);
final ErrorDescription child = children[0] as ErrorDescription;
expect(child.valueToString(), contains(Uri.parse(pubRootTest).path));
} else {
expect(nodes[2].runtimeType, ErrorDescription);
final ErrorDescription node = nodes[2] as ErrorDescription;
expect(node.valueToString().startsWith('Widget creation tracking is currently disabled.'), true);
}
expect(nodes[3].runtimeType, ErrorSpacer);
expect(nodes[4].runtimeType, DiagnosticsStackTrace);
});
testWidgets('test transformDebugCreator will not re-order if before stack trace', (WidgetTester tester) async {
final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked();
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
late String pubRootTest;
if (widgetTracked) {
final Map<String, Object?> jsonObject = json.decode(
service.getSelectedWidget(null, 'my-group'),
) as Map<String, Object?>;
final Map<String, Object?> creationLocation = jsonObject['creationLocation']! as Map<String, Object?>;
expect(creationLocation, isNotNull);
final String fileA = creationLocation['file']! as String;
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.
pubRootTest = '/${segments.take(segments.length - 2).join('/')}';
service.resetPubRootDirectories();
service.addPubRootDirectories(<String>[pubRootTest]);
}
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
builder.add(StringProperty('dummy1', 'value'));
builder.add(DiagnosticsDebugCreator(DebugCreator(elementA)));
builder.add(StringProperty('dummy2', 'value'));
builder.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', null));
final List<DiagnosticsNode> nodes = List<DiagnosticsNode>.from(debugTransformDebugCreator(builder.properties));
expect(nodes.length, 5);
expect(nodes[0].runtimeType, StringProperty);
expect(nodes[0].name, 'dummy1');
// transformed node stays at original place.
if (widgetTracked) {
expect(nodes[1].runtimeType, DiagnosticsBlock);
final DiagnosticsBlock node = nodes[1] as DiagnosticsBlock;
final List<DiagnosticsNode> children = node.getChildren();
expect(children.length, 1);
final ErrorDescription child = children[0] as ErrorDescription;
expect(child.valueToString(), contains(Uri.parse(pubRootTest).path));
} else {
expect(nodes[1].runtimeType, ErrorDescription);
final ErrorDescription node = nodes[1] as ErrorDescription;
expect(node.valueToString().startsWith('Widget creation tracking is currently disabled.'), true);
}
expect(nodes[2].runtimeType, ErrorSpacer);
expect(nodes[3].runtimeType, StringProperty);
expect(nodes[3].name, 'dummy2');
expect(nodes[4].runtimeType, DiagnosticsStackTrace);
}, skip: WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --no-track-widget-creation flag.
testWidgets('test transformDebugCreator will add DevToolsDeepLinkProperty for overflow errors', (WidgetTester tester) async {
activeDevToolsServerAddress = 'http://127.0.0.1:9100';
connectedVmServiceUri = 'http://127.0.0.1:55269/798ay5al_FM=/';
setupDefaultPubRootDirectory(service);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
builder.add(ErrorSummary('A RenderFlex overflowed by 273 pixels on the bottom'));
builder.add(DiagnosticsDebugCreator(DebugCreator(elementA)));
builder.add(StringProperty('dummy2', 'value'));
final List<DiagnosticsNode> nodes = List<DiagnosticsNode>.from(debugTransformDebugCreator(builder.properties));
expect(nodes.length, 6);
expect(nodes[0].runtimeType, ErrorSummary);
expect(nodes[1].runtimeType, DiagnosticsBlock);
expect(nodes[2].runtimeType, ErrorSpacer);
expect(nodes[3].runtimeType, DevToolsDeepLinkProperty);
expect(nodes[4].runtimeType, ErrorSpacer);
expect(nodes[5].runtimeType, StringProperty);
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
testWidgets('test transformDebugCreator will not add DevToolsDeepLinkProperty for non-overflow errors', (WidgetTester tester) async {
activeDevToolsServerAddress = 'http://127.0.0.1:9100';
connectedVmServiceUri = 'http://127.0.0.1:55269/798ay5al_FM=/';
setupDefaultPubRootDirectory(service);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
builder.add(ErrorSummary('some other error'));
builder.add(DiagnosticsDebugCreator(DebugCreator(elementA)));
builder.add(StringProperty('dummy2', 'value'));
final List<DiagnosticsNode> nodes = List<DiagnosticsNode>.from(debugTransformDebugCreator(builder.properties));
expect(nodes.length, 4);
expect(nodes[0].runtimeType, ErrorSummary);
expect(nodes[1].runtimeType, DiagnosticsBlock);
expect(nodes[2].runtimeType, ErrorSpacer);
expect(nodes[3].runtimeType, StringProperty);
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
testWidgets('test transformDebugCreator will not add DevToolsDeepLinkProperty if devtoolsServerAddress is unavailable', (WidgetTester tester) async {
activeDevToolsServerAddress = null;
connectedVmServiceUri = 'http://127.0.0.1:55269/798ay5al_FM=/';
setupDefaultPubRootDirectory(service);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
builder.add(ErrorSummary('A RenderFlex overflowed by 273 pixels on the bottom'));
builder.add(DiagnosticsDebugCreator(DebugCreator(elementA)));
builder.add(StringProperty('dummy2', 'value'));
final List<DiagnosticsNode> nodes = List<DiagnosticsNode>.from(debugTransformDebugCreator(builder.properties));
expect(nodes.length, 4);
expect(nodes[0].runtimeType, ErrorSummary);
expect(nodes[1].runtimeType, DiagnosticsBlock);
expect(nodes[2].runtimeType, ErrorSpacer);
expect(nodes[3].runtimeType, StringProperty);
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
// TODO(CoderDake): Clean up pubRootDirectory tests https://github.com/flutter/flutter/issues/107186
group('pubRootDirectory', () {
const String directoryA = '/a/b/c';
const String directoryB = '/d/e/f';
const String directoryC = '/g/h/i';
setUp(() {
service.resetPubRootDirectories();
});
group('addPubRootDirectories', () {
test('can add multiple directories', () async {
const List<String> directories = <String>[directoryA, directoryB];
service.addPubRootDirectories(directories);
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, unorderedEquals(directories));
});
test('can add multiple directories separately', () async {
service.addPubRootDirectories(<String>[directoryA]);
service.addPubRootDirectories(<String>[directoryB]);
service.addPubRootDirectories(<String>[]);
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, unorderedEquals(<String>[
directoryA,
directoryB,
]));
});
test('handles duplicates', () async {
const List<String> directories = <String>[
directoryA,
'file://$directoryA',
directoryB,
directoryB
];
service.addPubRootDirectories(directories);
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, unorderedEquals(<String>[
directoryA,
directoryB,
]));
});
});
group('removePubRootDirectories', () {
setUp(() {
service.resetPubRootDirectories();
service.addPubRootDirectories(<String>[directoryA, directoryB, directoryC]);
});
test('removes multiple directories', () async {
service.removePubRootDirectories(<String>[directoryA, directoryB,]);
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, equals(<String>[directoryC]));
});
test('removes multiple directories separately', () async {
service.removePubRootDirectories(<String>[directoryA]);
service.removePubRootDirectories(<String>[directoryB]);
service.removePubRootDirectories(<String>[]);
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, equals(<String>[directoryC]));
});
test('handles duplicates', () async {
service.removePubRootDirectories(<String>[
'file://$directoryA',
directoryA,
directoryB,
directoryB,
]);
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, equals(<String>[directoryC]));
});
test("does nothing if the directories doesn't exist ", () async {
service.removePubRootDirectories(<String>['/x/y/z']);
final List<String> pubRoots = await service.currentPubRootDirectories;
expect(pubRoots, unorderedEquals(<String>[
directoryA,
directoryB,
directoryC,
]));
});
});
});
group(
'WidgetInspectorService',
() {
late final String pubRootTest;
setUpAll(() {
pubRootTest = generateTestPubRootDirectory(service);
});
setUp(() {
service.disposeAllGroups();
service.resetPubRootDirectories();
});
group('addPubRootDirectories', () {
testWidgets(
'does not have createdByLocalProject when there are no pubRootDirectories',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
final Map<String, Object?> jsonObject =
json.decode(service.getSelectedWidget(null, 'my-group'))
as Map<String, Object?>;
final Map<String, Object?> creationLocation =
jsonObject['creationLocation']! as Map<String, Object?>;
expect(creationLocation, isNotNull);
final String fileA = creationLocation['file']! as String;
expect(fileA, endsWith('widget_inspector_test.dart'));
expect(jsonObject, isNot(contains('createdByLocalProject')));
},
);
testWidgets(
'has createdByLocalProject when the element is part of the pubRootDirectory',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.addPubRootDirectories(<String>[pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
contains('createdByLocalProject'),
);
},
);
testWidgets(
'does not have createdByLocalProject when widget package directory is a suffix of a pubRootDirectory',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
service.addPubRootDirectories(<String>['/invalid/$pubRootTest']);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
isNot(contains('createdByLocalProject')),
);
},
);
testWidgets(
'has createdByLocalProject when the pubRootDirectory is prefixed with file://',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
service.addPubRootDirectories(<String>['file://$pubRootTest']);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
contains('createdByLocalProject'),
);
},
);
testWidgets(
'does not have createdByLocalProject when thePubRootDirectory has a different suffix',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
service.addPubRootDirectories(<String>['$pubRootTest/different']);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
isNot(contains('createdByLocalProject')),
);
},
);
testWidgets(
'has createdByLocalProject even if another pubRootDirectory does not match',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
service.addPubRootDirectories(<String>[
'/invalid/$pubRootTest',
pubRootTest,
]);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
contains('createdByLocalProject'),
);
},
);
testWidgets(
'widget is part of core framework and is the child of a widget in the package pubRootDirectories',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
final Element richText = find
.descendant(
of: find.text('a'),
matching: find.byType(RichText),
)
.evaluate()
.first;
service.setSelection(richText, 'my-group');
service.addPubRootDirectories(<String>[pubRootTest]);
final Map<String, Object?> jsonObject =
json.decode(service.getSelectedWidget(null, 'my-group'))
as Map<String, Object?>;
expect(jsonObject, isNot(contains('createdByLocalProject')));
final Map<String, Object?> creationLocation =
jsonObject['creationLocation']! as Map<String, Object?>;
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']! as String).pathSegments;
expect(
pathSegmentsFramework.join('/'),
endsWith('/flutter/lib/src/widgets/text.dart'),
);
// Strip off /src/widgets/text.dart.
final String pubRootFramework =
'/${pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/')}';
service.resetPubRootDirectories();
service.addPubRootDirectories(<String>[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(<String>[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'),
);
},
);
});
group('createdByLocalProject', () {
setUp(() {
service.resetPubRootDirectories();
});
testWidgets(
'reacts to add and removing pubRootDirectories',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.addPubRootDirectories(<String>[
pubRootTest,
'file://$pubRootTest',
'/unrelated/$pubRootTest',
]);
service.setSelection(elementA, 'my-group');
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
contains('createdByLocalProject'),
);
service.removePubRootDirectories(<String>[pubRootTest]);
service.setSelection(elementA, 'my-group');
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
isNot(contains('createdByLocalProject')),
);
},
);
testWidgets(
'does not match when the package directory does not match',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
service.addPubRootDirectories(<String>[
'$pubRootTest/different',
'/unrelated/$pubRootTest',
]);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
isNot(contains('createdByLocalProject')),
);
},
);
testWidgets(
'has createdByLocalProject when the pubRootDirectory is prefixed with file://',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
service.addPubRootDirectories(<String>['file://$pubRootTest']);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
contains('createdByLocalProject'),
);
},
);
testWidgets(
'can handle consecutive calls to add',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
service.addPubRootDirectories(<String>[
pubRootTest,
]);
service.addPubRootDirectories(<String>[
'/invalid/$pubRootTest',
]);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
contains('createdByLocalProject'),
);
},
);
testWidgets(
'can handle removing an unrelated pubRootDirectory',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
service.setSelection(elementA, 'my-group');
service.addPubRootDirectories(<String>[
pubRootTest,
'/invalid/$pubRootTest',
]);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
contains('createdByLocalProject'),
);
service.removePubRootDirectories(<String>[
'/invalid/$pubRootTest',
]);
expect(
json.decode(service.getSelectedWidget(null, 'my-group')),
contains('createdByLocalProject'),
);
},
);
testWidgets(
'can handle parent widget being part of a separate package',
(WidgetTester tester) async {
const Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
);
await tester.pumpWidget(widget);
final Element elementA = find.text('a').evaluate().first;
final Element richText = find
.descendant(
of: find.text('a'),
matching: find.byType(RichText),
)
.evaluate()
.first;
service.setSelection(richText, 'my-group');
service.addPubRootDirectories(<String>[pubRootTest]);
final Map<String, Object?> jsonObject =
json.decode(service.getSelectedWidget(null, 'my-group'))
as Map<String, Object?>;
expect(jsonObject, isNot(contains('createdByLocalProject')));
final Map<String, Object?> creationLocation =
jsonObject['creationLocation']! as Map<String, Object?>;
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']! as String).pathSegments;
expect(
pathSegmentsFramework.join('/'),
endsWith('/flutter/lib/src/widgets/text.dart'),
);
// Strip off /src/widgets/text.dart.
final String pubRootFramework =
'/${pathSegmentsFramework.take(pathSegmentsFramework.length - 3).join('/')}';
service.resetPubRootDirectories();
service.addPubRootDirectories(<String>[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.resetPubRootDirectories();
service
.addPubRootDirectories(<String>[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(), // [intended] Test requires --track-widget-creation flag.
);
group('InspectorSelection', () {
testWidgets('receives notifications when selection changes',
(WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a'),
Text('b'),
],
),
),
);
final InspectorSelection selection = InspectorSelection();
addTearDown(selection.dispose);
int count = 0;
selection.addListener(() {
count++;
});
final RenderParagraph renderObjectA =
tester.renderObject<RenderParagraph>(find.text('a'));
final RenderParagraph renderObjectB =
tester.renderObject<RenderParagraph>(find.text('b'));
final Element elementA = find.text('a').evaluate().first;
selection.candidates = <RenderObject>[renderObjectA, renderObjectB];
await tester.pump();
expect(count, equals(1));
selection.index = 1;
await tester.pump();
expect(count, equals(2));
selection.clear();
await tester.pump();
expect(count, equals(3));
selection.current = renderObjectA;
await tester.pump();
expect(count, equals(4));
selection.currentElement = elementA;
expect(count, equals(5));
});
});
test('ext.flutter.inspector.disposeGroup', () async {
final Object a = 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));
await service.testExtension(
WidgetInspectorServiceExtensions.disposeGroup.name,
<String, String>{'objectGroup': group1},
);
await service.testExtension(
WidgetInspectorServiceExtensions.disposeGroup.name,
<String, String>{'objectGroup': group2},
);
expect(service.toObject(aId), equals(a));
await service.testExtension(
WidgetInspectorServiceExtensions.disposeGroup.name,
<String, String>{'objectGroup': group3},
);
expect(() => service.toObject(aId), throwsFlutterError);
});
test('ext.flutter.inspector.disposeId', () async {
final Object a = Object();
final Object b = 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));
await service.testExtension(
WidgetInspectorServiceExtensions.disposeId.name,
<String, String>{'arg': bId, 'objectGroup': group1},
);
expect(() => service.toObject(bId), throwsFlutterError);
await service.testExtension(
WidgetInspectorServiceExtensions.disposeId.name,
<String, String>{'arg': aId, 'objectGroup': group1},
);
expect(service.toObject(aId), equals(a));
await service.testExtension(
WidgetInspectorServiceExtensions.disposeId.name,
<String, String>{'arg': aId, 'objectGroup': group2},
);
expect(() => service.toObject(aId), throwsFlutterError);
});
testWidgets('ext.flutter.inspector.setSelection', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final Element elementB = find.text('b').evaluate().first;
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! as DebugCreator).element));
service.setSelection('invalid selection');
expect(selectionChangedCount, equals(2));
expect(service.selection.current, equals(elementB.renderObject));
await service.testExtension(
WidgetInspectorServiceExtensions.setSelectionById.name,
<String, String>{'arg': service.toId(elementA, 'my-group')!, 'objectGroup': '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('ext.flutter.inspector.getParentChain', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementB = find.text('b').evaluate().first;
final String bId = service.toId(elementB, group)!;
final Object? jsonList = await service.testExtension(
WidgetInspectorServiceExtensions.getParentChain.name,
<String, String>{'arg': bId, 'objectGroup': group},
);
expect(jsonList, isList);
final List<Object?> chainElements = jsonList! as List<Object?>;
final List<Element> expectedChain = elementB.debugGetDiagnosticChain().reversed.toList();
// Sanity check that the chain goes back to the root.
expect(expectedChain.first, tester.binding.rootElement);
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]! as Map<String, Object?>;
final Element element = expectedChain[i];
expect(chainNode['node'], isMap);
final Map<String, Object?> jsonNode = chainNode['node']! as Map<String, Object?>;
expect(service.toObject(jsonNode['valueId']! as String), equals(element));
expect(chainNode['children'], isList);
final List<Object?> jsonChildren = chainNode['children']! as List<Object?>;
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]! as Map<String, Object?>;
expect(service.toObject(childJson['valueId']! as String), equals(childrenElements[j]));
}
}
});
test('ext.flutter.inspector.getProperties', () async {
const Diagnosticable diagnosticable = Text('a', textDirection: TextDirection.ltr);
const String group = 'group';
final String id = service.toId(diagnosticable, group)!;
final List<Object?> propertiesJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getProperties.name,
<String, String>{'arg': id, 'objectGroup': group},
))! as List<Object?>;
final List<DiagnosticsNode> properties = diagnosticable.toDiagnosticsNode().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]! as Map<String, Object?>;
expect(service.toObject(propertyJson['valueId'] as String?), equals(properties[i].value));
}
});
testWidgets('ext.flutter.inspector.getChildren', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final DiagnosticsNode diagnostic = find.byType(Stack).evaluate().first.toDiagnosticsNode();
final String id = service.toId(diagnostic, group)!;
final List<Object?> propertiesJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildren.name,
<String, String>{'arg': id, 'objectGroup': group},
))! as List<Object?>;
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]! as Map<String, Object?>;
expect(service.toObject(propertyJson['valueId']! as String), equals(children[i].value));
}
});
testWidgets('ext.flutter.inspector.getChildrenDetailsSubtree', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Diagnosticable diagnosticable = find.byType(Stack).evaluate().first;
final String id = service.toId(diagnosticable, group)!;
final List<Object?> childrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenDetailsSubtree.name,
<String, String>{'arg': id, 'objectGroup': group},
))! as List<Object?>;
final List<DiagnosticsNode> children = diagnosticable.toDiagnosticsNode().getChildren();
expect(children.length, equals(3));
expect(childrenJson.length, equals(children.length));
for (int i = 0; i < childrenJson.length; ++i) {
final Map<String, Object?> childJson = childrenJson[i]! as Map<String, Object?>;
expect(service.toObject(childJson['valueId']! as String), equals(children[i].value));
final List<Object?> propertiesJson = childJson['properties']! as List<Object?>;
final Element element = service.toObject(childJson['valueId']! as String)! as Element;
final List<DiagnosticsNode> expectedProperties = element.toDiagnosticsNode().getProperties();
final Iterable<Object?> propertyValues = expectedProperties.map((DiagnosticsNode e) => e.value.toString());
for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) {
final String id = propertyJson['valueId']! as String;
final String property = service.toObject(id)!.toString();
expect(propertyValues, contains(property));
}
}
});
testWidgets('WidgetInspectorService getDetailsSubtree', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Diagnosticable diagnosticable = find.byType(Stack).evaluate().first;
final String id = service.toId(diagnosticable, group)!;
final Map<String, Object?> subtreeJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getDetailsSubtree.name,
<String, String>{'arg': id, 'objectGroup': group},
))! as Map<String, Object?>;
expect(subtreeJson['valueId'], equals(id));
final List<Object?> childrenJson = subtreeJson['children']! as List<Object?>;
final List<DiagnosticsNode> children = diagnosticable.toDiagnosticsNode().getChildren();
expect(children.length, equals(3));
expect(childrenJson.length, equals(children.length));
for (int i = 0; i < childrenJson.length; ++i) {
final Map<String, Object?> childJson = childrenJson[i]! as Map<String, Object?>;
expect(service.toObject(childJson['valueId']! as String), equals(children[i].value));
final List<Object?> propertiesJson = childJson['properties']! as List<Object?>;
for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) {
expect(propertyJson, isNot(contains('children')));
}
final Element element = service.toObject(childJson['valueId']! as String)! as Element;
final List<DiagnosticsNode> expectedProperties = element.toDiagnosticsNode().getProperties();
final Iterable<Object?> propertyValues = expectedProperties.map((DiagnosticsNode e) => e.value.toString());
for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) {
final String id = propertyJson['valueId']! as String;
final String property = service.toObject(id)!.toString();
expect(propertyValues, contains(property));
}
}
final Map<String, Object?> deepSubtreeJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getDetailsSubtree.name,
<String, String>{'arg': id, 'objectGroup': group, 'subtreeDepth': '3'},
))! as Map<String, Object?>;
final List<Object?> deepChildrenJson = deepSubtreeJson['children']! as List<Object?>;
for (final Map<String, Object?> childJson in deepChildrenJson.cast<Map<String, Object?>>()) {
final List<Object?> propertiesJson = childJson['properties']! as List<Object?>;
for (final Map<String, Object?> propertyJson in propertiesJson.cast<Map<String, Object?>>()) {
expect(propertyJson, contains('children'));
}
}
});
testWidgets('cyclic diagnostics regression test', (WidgetTester tester) async {
const String group = 'test-group';
final CyclicDiagnostic a = CyclicDiagnostic('a');
final CyclicDiagnostic b = CyclicDiagnostic('b');
a.related = b;
a.children.add(b.toDiagnosticsNode());
b.related = a;
final String id = service.toId(a, group)!;
final Map<String, Object?> subtreeJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getDetailsSubtree.name,
<String, String>{'arg': id, 'objectGroup': group},
))! as Map<String, Object?>;
expect(subtreeJson['valueId'], equals(id));
expect(subtreeJson, contains('children'));
final List<Object?> propertiesJson = subtreeJson['properties']! as List<Object?>;
expect(propertiesJson.length, equals(1));
final Map<String, Object?> relatedProperty = propertiesJson.first! as Map<String, Object?>;
expect(relatedProperty['name'], equals('related'));
expect(relatedProperty['description'], equals('CyclicDiagnostic-b'));
expect(relatedProperty, contains('isDiagnosticableValue'));
expect(relatedProperty, isNot(contains('children')));
expect(relatedProperty, contains('properties'));
final List<Object?> relatedWidgetProperties = relatedProperty['properties']! as List<Object?>;
expect(relatedWidgetProperties.length, equals(1));
final Map<String, Object?> nestedRelatedProperty = relatedWidgetProperties.first! as Map<String, Object?>;
expect(nestedRelatedProperty['name'], equals('related'));
// Make sure we do not include properties or children for diagnostic a
// which we already included as the root node as that would indicate a
// cycle.
expect(nestedRelatedProperty['description'], equals('CyclicDiagnostic-a'));
expect(nestedRelatedProperty, contains('isDiagnosticableValue'));
expect(nestedRelatedProperty, isNot(contains('properties')));
expect(nestedRelatedProperty, isNot(contains('children')));
});
testWidgets('ext.flutter.inspector.getRootWidgetSummaryTree', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service.disposeAllGroups();
service.resetPubRootDirectories();
service.setSelection(elementA, 'my-group');
final Map<String, dynamic> jsonA = (await service.testExtension(
WidgetInspectorServiceExtensions.getSelectedWidget.name,
<String, String>{'objectGroup': 'my-group'},
))! as Map<String, dynamic>;
service.resetPubRootDirectories();
Map<String, Object?> rootJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name,
<String, String>{'objectGroup': group},
))! as Map<String, Object?>;
// We haven't yet properly specified which directories are summary tree
// directories so we get an empty tree other than the root that is always
// included.
final Object? rootWidget = service.toObject(rootJson['valueId']! as String);
expect(rootWidget, equals(WidgetsBinding.instance.rootElement));
List<Object?> childrenJson = rootJson['children']! as List<Object?>;
// There are no summary tree children.
expect(childrenJson.length, equals(0));
final Map<String, Object?> creationLocation = jsonA['creationLocation']! as Map<String, Object?>;
expect(creationLocation, isNotNull);
final String testFile = creationLocation['file']! as String;
expect(testFile, endsWith('widget_inspector_test.dart'));
final List<String> segments = Uri.parse(testFile).pathSegments;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final String pubRootTest = '/${segments.take(segments.length - 2).join('/')}';
service.resetPubRootDirectories();
await service.testExtension(
WidgetInspectorServiceExtensions.addPubRootDirectories.name,
<String, String>{'arg0': pubRootTest},
);
rootJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name,
<String, String>{'objectGroup': group},
))! as Map<String, Object?>;
childrenJson = rootJson['children']! as List<Object?>;
// The tree of nodes returned contains all widgets created directly by the
// test.
childrenJson = rootJson['children']! as List<Object?>;
expect(childrenJson.length, equals(1));
List<Object?> alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': rootJson['valueId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(1));
Map<String, Object?> childJson = childrenJson[0]! as Map<String, Object?>;
Map<String, Object?> alternateChildJson = alternateChildrenJson[0]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Directionality'));
expect(alternateChildJson['description'], startsWith('Directionality'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
childrenJson = childJson['children']! as List<Object?>;
alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': childJson['valueId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(1));
expect(childrenJson.length, equals(1));
alternateChildJson = alternateChildrenJson[0]! as Map<String, Object?>;
childJson = childrenJson[0]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Stack'));
expect(alternateChildJson['description'], startsWith('Stack'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
childrenJson = childJson['children']! as List<Object?>;
childrenJson = childJson['children']! as List<Object?>;
alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': childJson['valueId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(3));
expect(childrenJson.length, equals(3));
alternateChildJson = alternateChildrenJson[2]! as Map<String, Object?>;
childJson = childrenJson[2]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Text'));
expect(alternateChildJson['description'], startsWith('Text'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': childJson['valueId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length , equals(0));
// Tests are failing when this typo is fixed.
expect(childJson['chidlren'], isNull);
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector.getRootWidgetSummaryTreeWithPreviews', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
service
..disposeAllGroups()
..resetPubRootDirectories()
..setSelection(elementA, 'my-group');
final Map<String, dynamic> jsonA = (await service.testExtension(
WidgetInspectorServiceExtensions.getSelectedWidget.name,
<String, String>{'objectGroup': 'my-group'},
))! as Map<String, dynamic>;
final Map<String, Object?> creationLocation = jsonA['creationLocation']! as Map<String, Object?>;
expect(creationLocation, isNotNull);
final String testFile = creationLocation['file']! as String;
expect(testFile, endsWith('widget_inspector_test.dart'));
final List<String> segments = Uri.parse(testFile).pathSegments;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final String pubRootTest = '/${segments.take(segments.length - 2).join('/')}';
service
..resetPubRootDirectories()
..addPubRootDirectories(<String>[pubRootTest]);
final Map<String, Object?> rootJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getRootWidgetSummaryTreeWithPreviews.name,
<String, String>{'groupName': group},
))! as Map<String, Object?>;
List<Object?> childrenJson = rootJson['children']! as List<Object?>;
// The tree of nodes returned contains all widgets created directly by the
// test.
childrenJson = rootJson['children']! as List<Object?>;
expect(childrenJson.length, equals(1));
List<Object?> alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': rootJson['valueId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(1));
Map<String, Object?> childJson = childrenJson[0]! as Map<String, Object?>;
Map<String, Object?> alternateChildJson = alternateChildrenJson[0]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Directionality'));
expect(alternateChildJson['description'], startsWith('Directionality'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
childrenJson = childJson['children']! as List<Object?>;
alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': childJson['valueId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(1));
expect(childrenJson.length, equals(1));
alternateChildJson = alternateChildrenJson[0]! as Map<String, Object?>;
childJson = childrenJson[0]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Stack'));
expect(alternateChildJson['description'], startsWith('Stack'));
expect(alternateChildJson['valueId'], equals(childJson['valueId']));
childrenJson = childJson['children']! as List<Object?>;
childrenJson = childJson['children']! as List<Object?>;
alternateChildrenJson = (await service.testExtension(
WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
<String, String>{'arg': childJson['valueId']! as String, 'objectGroup': group},
))! as List<Object?>;
expect(alternateChildrenJson.length, equals(3));
expect(childrenJson.length, equals(3));
alternateChildJson = alternateChildrenJson[2]! as Map<String, Object?>;
childJson = childrenJson[2]! as Map<String, Object?>;
expect(childJson['description'], startsWith('Text'));
// [childJson] contains the 'textPreview' key since the tree was requested
// with previews [getRootWidgetSummaryTreeWithPreviews].
expect(childJson['textPreview'], equals('c'));
}, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // [intended] Test requires --track-widget-creation flag.
testWidgets('ext.flutter.inspector.getSelectedSummaryWidget', (WidgetTester tester) async {
const String group = 'test-group';
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Text('a', textDirection: TextDirection.ltr),
Text('b', textDirection: TextDirection.ltr),
Text('c', textDirection: TextDirection.ltr),
],
),
),
);
final Element elementA = find.text('a').evaluate().first;
final List<DiagnosticsNode> children = elementA.debugDescribeChildren();
expect(children.length, equals(1));
final DiagnosticsNode richTextDiagnostic = children.first;
service.disposeAllGroups();
service.resetPubRootDirectories();
service.setSelection(elementA, 'my-group');
final Map<String, Object?> jsonA = (await service.testExtension(
WidgetInspectorServiceExtensions.getSelectedWidget.name,
<String, String>{'objectGroup': 'my-group'},
))! as Map<String, Object?>;
service.setSelection(richTextDiagnostic.value, 'my-group');
service.resetPubRootDirectories();
Map<String, Object?>? summarySelection = await service.testExtension(
WidgetInspectorServiceExtensions.getSelectedSummaryWidget.name,
<String, String>{'objectGroup': group},
) as Map<String, Object?>?;
// No summary selection because we haven't set the pub root directories
// yet to indicate what directories are in the summary tree.
expect(summarySelection, isNull);
final Map<String, Object?> creationLocation = jsonA['creationLocation']! as Map<String, Object?>;
expect(creationLocation, isNotNull);
final String testFile = creationLocation['file']! as String;
expect(testFile, endsWith('widget_inspector_test.dart'));
final List<String> segments = Uri.parse(testFile).pathSegments;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.</