// 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.

// @dart = 2.8

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:ui' as ui;

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/widgets.dart';
import 'package:flutter_test/flutter_test.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 StatefulWidget {
  const ClockDemo({ Key key }) : super(key: key);
  @override
  _ClockDemoState createState() => _ClockDemoState();
}

class _ClockDemoState extends State<ClockDemo> {
  @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({
    Key key,
    this.utcOffset = 0,
  }) : super(key: key);

  final int utcOffset;

  @override
  _ClockTextState 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() => '$runtimeType-$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 {
  const _CreationLocation({
    @required this.file,
    @required this.line,
    @required this.column,
    @required this.id,
  });

  final String file;
  final int line;
  final int column;
  final int id;
}

typedef InspectorServiceExtensionCallback = FutureOr<Map<String, Object>> Function(Map<String, String> parameters);

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({
    Key key,
    Widget child,
  }) : super(key: key, child: child);

  @override
  RenderRepaintBoundary createRenderObject(BuildContext context) {
    return RenderRepaintBoundaryWithDebugPaint();
  }
}

int getChildLayerCount(OffsetLayer layer) {
  Layer child = layer.firstChild;
  int count = 0;
  while (child != null) {
    count++;
    child = child.nextSibling;
  }
  return count;
}

void main() {
  _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;

    testWidgets('WidgetInspector smoke test', (WidgetTester tester) async {
      // This is a smoke test to verify that adding the inspector doesn't crash.
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <Widget>[
              Text('a', textDirection: TextDirection.ltr),
              Text('b', textDirection: TextDirection.ltr),
              Text('c', textDirection: TextDirection.ltr),
            ],
          ),
        ),
      );

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: WidgetInspector(
            selectButtonBuilder: null,
            child: Stack(
              children: const <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));
      }
      // State type is private, hence using dynamic.
      dynamic getInspectorState() => inspectorKey.currentState;
      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(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 as InspectorSelection;
      expect(paragraphText(selection.current as RenderParagraph), equals('TOP'));
      final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject;
      expect(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(getInspectorState().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'));
      expect(log, equals(<String>[]));
      log.clear();
      expect(paragraphText(getInspectorState().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: Stack(
                children: const <Widget>[
                  Text('a', textDirection: TextDirection.ltr),
                  Text('b', textDirection: TextDirection.ltr),
                  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 = 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));
      }
      // State type is private, hence using dynamic.
      dynamic getInspectorState() => inspectorKey.currentState;

      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);
      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(
        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'));
      // 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),
            ),
          ],
        );
      }
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: WidgetInspector(
            key: inspectorKey,
            selectButtonBuilder: null,
            child: Overlay(
              initialEntries: <OverlayEntry>[
                OverlayEntry(
                  opaque: false,
                  maintainState: true,
                  builder: (BuildContext _) => createSubtree(width: 94.0),
                ),
                OverlayEntry(
                  opaque: true,
                  maintainState: true,
                  builder: (BuildContext _) => createSubtree(width: 95.0),
                ),
                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));
    });

    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: Container(
            color: Colors.grey,
            child: Transform(
              transform: mainTransform,
              child: Directionality(
                textDirection: TextDirection.ltr,
                child: WidgetInspector(
                  selectButtonBuilder: null,
                  child: Container(
                    color: Colors.white,
                    child: Center(
                      child: Container(
                        key: childKey,
                        height: 100.0,
                        width: 50.0,
                        color: Colors.red,
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      );

      await tester.tap(find.byKey(childKey));
      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: RaisedButton(onPressed: onPressed, key: key));
        };
      }

      // State type is private, hence using dynamic.
      // The inspector state is static, so it's enough with reading one of them.
      dynamic getInspectorState() => inspector1Key.currentState;
      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'),
                  ),
                ),
              ),
            ],
          ),
        ),
      );

      final InspectorSelection selection = getInspectorState().selection as InspectorSelection;
      // The selection is static, so it may be initialized from previous tests.
      selection?.clear();

      await tester.tap(find.text('Child 1'));
      await tester.pump();
      expect(paragraphText(selection.current as RenderParagraph), equals('Child 1'));

      await tester.tap(find.text('Child 2'));
      await tester.pump();
      expect(paragraphText(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(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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.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(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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.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] 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(service.toObject(jsonNode['objectId'] as String), isA<DiagnosticsNode>());

        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]));
          expect(service.toObject(childJson['objectId'] as String), isA<DiagnosticsNode>());
        }
      }
    });

    test('WidgetInspectorService getProperties', () {
      final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode();
      const String group = 'group';
      service.disposeAllGroups();
      final String id = service.toId(diagnostic, group);
      final List<Object> propertiesJson = json.decode(service.getProperties(id, group)) as List<Object>;
      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] as Map<String, Object>;
        expect(service.toObject(propertyJson['valueId'] as String), equals(properties[i].value));
        expect(service.toObject(propertyJson['objectId'] as String), isA<DiagnosticsNode>());
      }
    });

    testWidgets('WidgetInspectorService getChildren', (WidgetTester tester) async {
      const String group = 'test-group';

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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));
        expect(service.toObject(propertyJson['objectId'] as String), isA<DiagnosticsNode>());
      }
    });

    testWidgets('WidgetInspectorService creationLocation', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <Widget>[
              Text('a'),
              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.setPubRootDirectories(<String>[]);
      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 List<Object> parameterLocationsA = creationLocationA['parameterLocations'] as List<Object>;

      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 List<Object> parameterLocationsB = creationLocationB['parameterLocations'] as List<Object>;
      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(15));
      expect(columnA, equals(columnB));
      expect(parameterLocationsA.length, equals(1));
      final Map<String, Object> paramA = parameterLocationsA[0] as Map<String, Object>;
      expect(paramA['name'], equals('data'));
      expect(paramA['line'], equals(lineA));
      expect(paramA['column'], equals(20));

      expect(parameterLocationsB.length, equals(2));
      final Map<String, Object> paramB1 = parameterLocationsB[0] as Map<String, Object>;
      expect(paramB1['name'], equals('data'));
      expect(paramB1['line'], equals(lineB));
      expect(paramB1['column'], equals(20));
      final Map<String, Object> paramB2 = parameterLocationsB[1] as Map<String, Object>;
      expect(paramB2['name'], equals('textDirection'));
      expect(paramB2['line'], equals(lineB));
      expect(paramB2['column'], equals(25));
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // 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(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <Widget>[
              Text('a'),
              Text('b', textDirection: TextDirection.ltr),
              Text('c', textDirection: TextDirection.ltr),
            ],
          ),
        ),
      );
      final Element elementA = find.text('a').evaluate().first;
      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.setPubRootDirectories(<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(transformDebugCreator(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(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <Widget>[
              Text('a'),
              Text('b', textDirection: TextDirection.ltr),
              Text('c', textDirection: TextDirection.ltr),
            ],
          ),
        ),
      );
      final Element elementA = find.text('a').evaluate().first;
      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.setPubRootDirectories(<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(transformDebugCreator(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()); // Test requires --no-track-widget-creation flag.

    testWidgets('WidgetInspectorService setPubRootDirectories', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <Widget>[
              Text('a'),
              Text('b', textDirection: TextDirection.ltr),
              Text('c', textDirection: TextDirection.ltr),
            ],
          ),
        ),
      );
      final Element elementA = find.text('a').evaluate().first;

      service.disposeAllGroups();
      service.setPubRootDirectories(<String>[]);
      service.setSelection(elementA, 'my-group');
      Map<String, Object> jsonObject = json.decode(service.getSelectedWidget(null, 'my-group')) as Map<String, Object>;
      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.
      final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
      service.setPubRootDirectories(<String>[pubRootTest]);

      service.setSelection(elementA, 'my-group');
      expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));

      service.setPubRootDirectories(<String>['/invalid/$pubRootTest']);
      expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));

      service.setPubRootDirectories(<String>['file://$pubRootTest']);
      expect(json.decode(service.getSelectedWidget(null, 'my-group')), contains('createdByLocalProject'));

      service.setPubRootDirectories(<String>['$pubRootTest/different']);
      expect(json.decode(service.getSelectedWidget(null, 'my-group')), isNot(contains('createdByLocalProject')));

      service.setPubRootDirectories(<String>[
        '/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(<String>[pubRootTest]);
      jsonObject = json.decode(service.getSelectedWidget(null, 'my-group')) as Map<String, Object>;
      expect(jsonObject, isNot(contains('createdByLocalProject')));
      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.setPubRootDirectories(<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'));
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked() || isBrowser); // Test requires --track-widget-creation flag.

    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('disposeGroup', <String, String>{'objectGroup': group1});
      await service.testExtension('disposeGroup', <String, String>{'objectGroup': group2});
      expect(service.toObject(aId), equals(a));
      await service.testExtension('disposeGroup', <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('disposeId', <String, String>{'arg': bId, 'objectGroup': group1});
      expect(() => service.toObject(bId), throwsFlutterError);
      await service.testExtension('disposeId', <String, String>{'arg': aId, 'objectGroup': group1});
      expect(service.toObject(aId), equals(a));
      await service.testExtension('disposeId', <String, String>{'arg': aId, 'objectGroup': group2});
      expect(() => service.toObject(aId), throwsFlutterError);
    });

    testWidgets('ext.flutter.inspector.setSelection', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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.element));

      service.setSelection('invalid selection');
      expect(selectionChangedCount, equals(2));
      expect(service.selection.current, equals(elementB.renderObject));

      await service.testExtension('setSelectionById', <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(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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('getParentChain', <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.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] 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(service.toObject(jsonNode['objectId'] as String), isA<DiagnosticsNode>());

        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]));
          expect(service.toObject(childJson['objectId'] as String), isA<DiagnosticsNode>());
        }
      }
    });

    test('ext.flutter.inspector.getProperties', () async {
      final DiagnosticsNode diagnostic = const Text('a', textDirection: TextDirection.ltr).toDiagnosticsNode();
      const String group = 'group';
      final String id = service.toId(diagnostic, group);
      final List<Object> propertiesJson = await service.testExtension('getProperties', <String, String>{'arg': id, 'objectGroup': group}) as List<Object>;
      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] as Map<String, Object>;
        expect(service.toObject(propertyJson['valueId'] as String), equals(properties[i].value));
        expect(service.toObject(propertyJson['objectId'] as String), isA<DiagnosticsNode>());
      }
    });

    testWidgets('ext.flutter.inspector.getChildren', (WidgetTester tester) async {
      const String group = 'test-group';

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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('getChildren', <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));
        expect(service.toObject(propertyJson['objectId'] as String), isA<DiagnosticsNode>());
      }
    });

    testWidgets('ext.flutter.inspector.getChildrenDetailsSubtree', (WidgetTester tester) async {
      const String group = 'test-group';

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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> childrenJson = await service.testExtension('getChildrenDetailsSubtree', <String, String>{'arg': id, 'objectGroup': group}) as List<Object>;
      final List<DiagnosticsNode> children = diagnostic.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));
        expect(service.toObject(childJson['objectId'] as String), isA<DiagnosticsNode>());
        final List<Object> propertiesJson = childJson['properties'] as List<Object>;
        final DiagnosticsNode diagnosticsNode = service.toObject(childJson['objectId'] as String) as DiagnosticsNode;
        final List<DiagnosticsNode> expectedProperties = diagnosticsNode.getProperties();
        for (final Map<String, Object> propertyJson in propertiesJson.cast<Map<String, Object>>()) {
          final Object property = service.toObject(propertyJson['objectId'] as String);
          expect(property, isA<DiagnosticsNode>());
          expect(expectedProperties, contains(property));
        }
      }
    });

    testWidgets('WidgetInspectorService getDetailsSubtree', (WidgetTester tester) async {
      const String group = 'test-group';

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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 Map<String, Object> subtreeJson = await service.testExtension('getDetailsSubtree', <String, String>{'arg': id, 'objectGroup': group}) as Map<String, Object>;
      expect(subtreeJson['objectId'], equals(id));
      final List<Object> childrenJson = subtreeJson['children'] as List<Object>;
      final List<DiagnosticsNode> children = diagnostic.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));
        expect(service.toObject(childJson['objectId'] as String), isA<DiagnosticsNode>());
        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 DiagnosticsNode diagnosticsNode = service.toObject(childJson['objectId'] as String) as DiagnosticsNode;
        final List<DiagnosticsNode> expectedProperties = diagnosticsNode.getProperties();
        for (final Map<String, Object> propertyJson in propertiesJson.cast<Map<String, Object>>()) {
          final Object property = service.toObject(propertyJson['objectId'] as String);
          expect(property, isA<DiagnosticsNode>());
          expect(expectedProperties, contains(property));
        }
      }

      final Map<String, Object> deepSubtreeJson = await service.testExtension(
        'getDetailsSubtree',
        <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 DiagnosticsNode diagnostic = a.toDiagnosticsNode();
      final String id = service.toId(diagnostic, group);
      final Map<String, Object> subtreeJson = await service.testExtension('getDetailsSubtree', <String, String>{'arg': id, 'objectGroup': group}) as Map<String, Object>;
      expect(subtreeJson['objectId'], 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(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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();
      await service.testExtension('setPubRootDirectories', <String, String>{});
      service.setSelection(elementA, 'my-group');
      final Map<String, Object> jsonA = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'}) as Map<String, Object>;

      await service.testExtension('setPubRootDirectories', <String, String>{});
      Map<String, Object> rootJson = await service.testExtension('getRootWidgetSummaryTree', <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?.renderViewElement));
      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('/');
      await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootTest});

      rootJson = await service.testExtension('getRootWidgetSummaryTree', <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('getChildrenSummaryTree', <String, String>{'arg': rootJson['objectId'] 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('getChildrenSummaryTree', <String, String>{'arg': childJson['objectId'] 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('getChildrenSummaryTree', <String, String>{'arg': childJson['objectId'] 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('getChildrenSummaryTree', <String, String>{'arg': childJson['objectId'] as String, 'objectGroup': group}) as List<Object>;
      expect(alternateChildrenJson.length , equals(0));
      expect(childJson['chidlren'], isNull);
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.

    testWidgets('ext.flutter.inspector.getSelectedSummaryWidget', (WidgetTester tester) async {
      const String group = 'test-group';

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <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();
      await service.testExtension('setPubRootDirectories', <String, String>{});
      service.setSelection(elementA, 'my-group');
      final Map<String, Object> jsonA = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'}) as Map<String, Object>;
      service.setSelection(richTextDiagnostic.value, 'my-group');

      await service.testExtension('setPubRootDirectories', <String, String>{});
      Map<String, Object> summarySelection = await service.testExtension('getSelectedSummaryWidget', <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.
      final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
      await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootTest});

      summarySelection = await service.testExtension('getSelectedSummaryWidget', <String, String>{'objectGroup': group}) as Map<String, Object>;
      expect(summarySelection['valueId'], isNotNull);
      // We got the Text element instead of the selected RichText element
      // because only the RichText element is part of the summary tree.
      expect(service.toObject(summarySelection['valueId'] as String), elementA);

      // Verify tha the regular getSelectedWidget method still returns
      // the RichText object not the Text element.
      final Map<String, Object> regularSelection = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'}) as Map<String, Object>;
      expect(service.toObject(regularSelection['valueId'] as String), richTextDiagnostic.value);
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.

    testWidgets('ext.flutter.inspector creationLocation', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <Widget>[
              Text('a'),
              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();
      await service.testExtension('setPubRootDirectories', <String, String>{});
      service.setSelection(elementA, 'my-group');
      final Map<String, Object> jsonA = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': '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 List<Object> parameterLocationsA = creationLocationA['parameterLocations'] as List<Object>;

      service.setSelection(elementB, 'my-group');
      final Map<String, Object> jsonB = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': '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 List<Object> parameterLocationsB = creationLocationB['parameterLocations'] as List<Object>;
      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(15));
      expect(columnA, equals(columnB));
      expect(parameterLocationsA.length, equals(1));
      final Map<String, Object> paramA = parameterLocationsA[0] as Map<String, Object>;
      expect(paramA['name'], equals('data'));
      expect(paramA['line'], equals(lineA));
      expect(paramA['column'], equals(20));

      expect(parameterLocationsB.length, equals(2));
      final Map<String, Object> paramB1 = parameterLocationsB[0] as Map<String, Object>;
      expect(paramB1['name'], equals('data'));
      expect(paramB1['line'], equals(lineB));
      expect(paramB1['column'], equals(20));
      final Map<String, Object> paramB2 = parameterLocationsB[1] as Map<String, Object>;
      expect(paramB2['name'], equals('textDirection'));
      expect(paramB2['line'], equals(lineB));
      expect(paramB2['column'], equals(25));
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.

    testWidgets('ext.flutter.inspector.setPubRootDirectories', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: const <Widget>[
              Text('a'),
              Text('b', textDirection: TextDirection.ltr),
              Text('c', textDirection: TextDirection.ltr),
            ],
          ),
        ),
      );
      final Element elementA = find.text('a').evaluate().first;

      await service.testExtension('setPubRootDirectories', <String, String>{});
      service.setSelection(elementA, 'my-group');
      Map<String, Object> jsonObject = await service.testExtension('getSelectedWidget', <String, String>{'arg': null, 'objectGroup': 'my-group'}) as Map<String, Object>;
      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.
      final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
      await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootTest});

      service.setSelection(elementA, 'my-group');
      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));

      await service.testExtension('setPubRootDirectories', <String, String>{'arg0': '/invalid/$pubRootTest'});
      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));

      await service.testExtension('setPubRootDirectories', <String, String>{'arg0': 'file://$pubRootTest'});
      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));

      await service.testExtension('setPubRootDirectories', <String, String>{'arg0': '$pubRootTest/different'});
      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));

      await service.testExtension('setPubRootDirectories', <String, String>{
        'arg0': '/unrelated/$pubRootTest',
        'arg1': 'file://$pubRootTest',
      });

      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': '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(<String>[pubRootTest]);
      jsonObject = json.decode(service.getSelectedWidget(null, 'my-group')) as Map<String, Object>;
      expect(jsonObject, isNot(contains('createdByLocalProject')));
      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('/');
      await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootFramework});
      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
      service.setSelection(elementA, 'my-group');
      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), isNot(contains('createdByLocalProject')));

      await service.testExtension('setPubRootDirectories', <String, String>{'arg0': pubRootFramework, 'arg1': pubRootTest});
      service.setSelection(elementA, 'my-group');
      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
      service.setSelection(richText, 'my-group');
      expect(await service.testExtension('getSelectedWidget', <String, String>{'objectGroup': 'my-group'}), contains('createdByLocalProject'));
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked() || isBrowser); // Test requires --track-widget-creation flag.

    testWidgets('ext.flutter.inspector.trackRebuildDirtyWidgets', (WidgetTester tester) async {
      service.rebuildCount = 0;

      await tester.pumpWidget(const ClockDemo());

      final Element clockDemoElement = find.byType(ClockDemo).evaluate().first;

      service.setSelection(clockDemoElement, 'my-group');
      final Map<String, Object> jsonObject = await service.testExtension(
        'getSelectedWidget',
        <String, String>{'arg': null, 'objectGroup': 'my-group'},
      ) as Map<String, Object>;
      final Map<String, Object> creationLocation = jsonObject['creationLocation'] as Map<String, Object>;
      expect(creationLocation, isNotNull);
      final String file = creationLocation['file'] as String;
      expect(file, endsWith('widget_inspector_test.dart'));
      final List<String> segments = Uri.parse(file).pathSegments;
      // Strip a couple subdirectories away to generate a plausible pub root
      // directory.
      final String pubRootTest =
          '/' + segments.take(segments.length - 2).join('/');
      await service.testExtension(
          'setPubRootDirectories', <String, String>{'arg0': pubRootTest});

      final List<Map<Object, Object>> rebuildEvents =
          service.getEventsDispatched('Flutter.RebuiltWidgets');
      expect(rebuildEvents, isEmpty);

      expect(service.rebuildCount, equals(0));
      expect(
        await service.testBoolExtension('trackRebuildDirtyWidgets', <String, String>{'enabled': 'true'}),
        equals('true'),
      );
      expect(service.rebuildCount, equals(1));
      await tester.pump();

      expect(rebuildEvents.length, equals(1));
      Map<Object, Object> event = rebuildEvents.removeLast();
      expect(event['startTime'], isA<int>());
      List<int> data = event['events'] as List<int>;
      expect(data.length, equals(14));
      final int numDataEntries = data.length ~/ 2;
      Map<String, List<int>> newLocations = event['newLocations'] as Map<String, List<int>>;
      expect(newLocations, isNotNull);
      expect(newLocations.length, equals(1));
      expect(newLocations.keys.first, equals(file));
      final List<int> locationsForFile = newLocations[file];
      expect(locationsForFile.length, equals(21));
      final int numLocationEntries = locationsForFile.length ~/ 3;
      expect(numLocationEntries, equals(numDataEntries));

      final Map<int, _CreationLocation> knownLocations = <int, _CreationLocation>{};
      addToKnownLocationsMap(
        knownLocations: knownLocations,
        newLocations: newLocations,
      );
      int totalCount = 0;
      int maxCount = 0;
      for (int i = 0; i < data.length; i += 2) {
        final int id = data[i];
        final int count = data[i + 1];
        totalCount += count;
        maxCount = max(maxCount, count);
        expect(knownLocations, contains(id));
      }
      expect(totalCount, equals(27));
      // The creation locations that were rebuilt the most were rebuilt 6 times
      // as there are 6 instances of the ClockText widget.
      expect(maxCount, equals(6));

      final List<Element> clocks = find.byType(ClockText).evaluate().toList();
      expect(clocks.length, equals(6));
      // Update a single clock.
      StatefulElement clockElement = clocks.first as StatefulElement;
      _ClockTextState state = clockElement.state as _ClockTextState;
      state.updateTime(); // Triggers a rebuild.
      await tester.pump();
      expect(rebuildEvents.length, equals(1));
      event = rebuildEvents.removeLast();
      expect(event['startTime'], isA<int>());
      data = event['events'] as List<int>;
      // No new locations were rebuilt.
      expect(event, isNot(contains('newLocations')));

      // There were two rebuilds: one for the ClockText element itself and one
      // for its child.
      expect(data.length, equals(4));
      int id = data[0];
      int count = data[1];
      _CreationLocation location = knownLocations[id];
      expect(location.file, equals(file));
      // ClockText widget.
      expect(location.line, equals(55));
      expect(location.column, equals(9));
      expect(count, equals(1));

      id = data[2];
      count = data[3];
      location = knownLocations[id];
      expect(location.file, equals(file));
      // Text widget in _ClockTextState build method.
      expect(location.line, equals(93));
      expect(location.column, equals(12));
      expect(count, equals(1));

      // Update 3 of the clocks;
      for (int i = 0; i < 3; i++) {
        clockElement = clocks[i] as StatefulElement;
        state = clockElement.state as _ClockTextState;
        state.updateTime(); // Triggers a rebuild.
      }

      await tester.pump();
      expect(rebuildEvents.length, equals(1));
      event = rebuildEvents.removeLast();
      expect(event['startTime'], isA<int>());
      data = event['events'] as List<int>;
      // No new locations were rebuilt.
      expect(event, isNot(contains('newLocations')));

      expect(data.length, equals(4));
      id = data[0];
      count = data[1];
      location = knownLocations[id];
      expect(location.file, equals(file));
      // ClockText widget.
      expect(location.line, equals(55));
      expect(location.column, equals(9));
      expect(count, equals(3)); // 3 clock widget instances rebuilt.

      id = data[2];
      count = data[3];
      location = knownLocations[id];
      expect(location.file, equals(file));
      // Text widget in _ClockTextState build method.
      expect(location.line, equals(93));
      expect(location.column, equals(12));
      expect(count, equals(3)); // 3 clock widget instances rebuilt.

      // Update one clock 3 times.
      clockElement = clocks.first as StatefulElement;
      state = clockElement.state as _ClockTextState;
      state.updateTime(); // Triggers a rebuild.
      state.updateTime(); // Triggers a rebuild.
      state.updateTime(); // Triggers a rebuild.

      await tester.pump();
      expect(rebuildEvents.length, equals(1));
      event = rebuildEvents.removeLast();
      expect(event['startTime'], isA<int>());
      data = event['events'] as List<int>;
      // No new locations were rebuilt.
      expect(event, isNot(contains('newLocations')));

      expect(data.length, equals(4));
      id = data[0];
      count = data[1];
      // Even though a rebuild was triggered 3 times, only one rebuild actually
      // occurred.
      expect(count, equals(1));

      // Trigger a widget creation location that wasn't previously triggered.
      state.stopClock();
      await tester.pump();
      expect(rebuildEvents.length, equals(1));
      event = rebuildEvents.removeLast();
      expect(event['startTime'], isA<int>());
      data = event['events'] as List<int>;
      newLocations = event['newLocations'] as Map<String, List<int>>;

      expect(data.length, equals(4));
      // The second pair in data is the previously unseen rebuild location.
      id = data[2];
      count = data[3];
      expect(count, equals(1));
      // Verify the rebuild location is new.
      expect(knownLocations, isNot(contains(id)));
      addToKnownLocationsMap(
        knownLocations: knownLocations,
        newLocations: newLocations,
      );
      // Verify the rebuild location was included in the newLocations data.
      expect(knownLocations, contains(id));

      // Turn off rebuild counts.
      expect(
        await service.testBoolExtension('trackRebuildDirtyWidgets', <String, String>{'enabled': 'false'}),
        equals('false'),
      );

      state.updateTime(); // Triggers a rebuild.
      await tester.pump();
      // Verify that rebuild events are not fired once the extension is disabled.
      expect(rebuildEvents, isEmpty);
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.

    testWidgets('ext.flutter.inspector.trackRepaintWidgets', (WidgetTester tester) async {
      service.rebuildCount = 0;

      await tester.pumpWidget(const ClockDemo());

      final Element clockDemoElement = find.byType(ClockDemo).evaluate().first;

      service.setSelection(clockDemoElement, 'my-group');
      final Map<String, Object> jsonObject = await service.testExtension(
          'getSelectedWidget',
          <String, String>{'arg': null, 'objectGroup': 'my-group'}) as Map<String, Object>;
      final Map<String, Object> creationLocation =
          jsonObject['creationLocation'] as Map<String, Object>;
      expect(creationLocation, isNotNull);
      final String file = creationLocation['file'] as String;
      expect(file, endsWith('widget_inspector_test.dart'));
      final List<String> segments = Uri.parse(file).pathSegments;
      // Strip a couple subdirectories away to generate a plausible pub root
      // directory.
      final String pubRootTest =
          '/' + segments.take(segments.length - 2).join('/');
      await service.testExtension(
          'setPubRootDirectories', <String, String>{'arg0': pubRootTest});

      final List<Map<Object, Object>> repaintEvents =
          service.getEventsDispatched('Flutter.RepaintWidgets');
      expect(repaintEvents, isEmpty);

      expect(service.rebuildCount, equals(0));
      expect(
          await service.testBoolExtension(
              'trackRepaintWidgets', <String, String>{'enabled': 'true'}),
          equals('true'));
      // Unlike trackRebuildDirtyWidgets, trackRepaintWidgets doesn't force a full
      // rebuild.
      expect(service.rebuildCount, equals(0));

      await tester.pump();

      expect(repaintEvents.length, equals(1));
      Map<Object, Object> event = repaintEvents.removeLast();
      expect(event['startTime'], isA<int>());
      List<int> data = event['events'] as List<int>;
      expect(data.length, equals(18));
      final int numDataEntries = data.length ~/ 2;
      final Map<String, List<int>> newLocations = event['newLocations'] as Map<String, List<int>>;
      expect(newLocations, isNotNull);
      expect(newLocations.length, equals(1));
      expect(newLocations.keys.first, equals(file));
      final List<int> locationsForFile = newLocations[file];
      expect(locationsForFile.length, equals(27));
      final int numLocationEntries = locationsForFile.length ~/ 3;
      expect(numLocationEntries, equals(numDataEntries));

      final Map<int, _CreationLocation> knownLocations =
          <int, _CreationLocation>{};
      addToKnownLocationsMap(
        knownLocations: knownLocations,
        newLocations: newLocations,
      );
      int totalCount = 0;
      int maxCount = 0;
      for (int i = 0; i < data.length; i += 2) {
        final int id = data[i];
        final int count = data[i + 1];
        totalCount += count;
        maxCount = max(maxCount, count);
        expect(knownLocations, contains(id));
      }
      expect(totalCount, equals(34));
      // The creation locations that were rebuilt the most were rebuilt 6 times
      // as there are 6 instances of the ClockText widget.
      expect(maxCount, equals(6));

      final List<Element> clocks = find.byType(ClockText).evaluate().toList();
      expect(clocks.length, equals(6));
      // Update a single clock.
      final StatefulElement clockElement = clocks.first as StatefulElement;
      final _ClockTextState state = clockElement.state as _ClockTextState;
      state.updateTime(); // Triggers a rebuild.
      await tester.pump();
      expect(repaintEvents.length, equals(1));
      event = repaintEvents.removeLast();
      expect(event['startTime'], isA<int>());
      data = event['events'] as List<int>;
      // No new locations were rebuilt.
      expect(event, isNot(contains('newLocations')));

      // Triggering a rebuild of one widget in this app causes the whole app
      // to repaint.
      expect(data.length, equals(18));

      // TODO(jacobr): add an additional repaint test that uses multiple repaint
      // boundaries to test more complex repaint conditions.

      // Turn off rebuild counts.
      expect(
          await service.testBoolExtension(
              'trackRepaintWidgets', <String, String>{'enabled': 'false'}),
          equals('false'));

      state.updateTime(); // Triggers a rebuild.
      await tester.pump();
      // Verify that repaint events are not fired once the extension is disabled.
      expect(repaintEvents, isEmpty);
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.

    testWidgets('ext.flutter.inspector.show', (WidgetTester tester) async {
      final Iterable<Map<Object, Object>> extensionChangedEvents = service.getServiceExtensionStateChangedEvents('ext.flutter.inspector.show');
      Map<Object, Object> extensionChangedEvent;

      service.rebuildCount = 0;
      expect(extensionChangedEvents, isEmpty);
      expect(await service.testBoolExtension('show', <String, String>{'enabled': 'true'}), equals('true'));
      expect(extensionChangedEvents.length, equals(1));
      extensionChangedEvent = extensionChangedEvents.last;
      expect(extensionChangedEvent['extension'], equals('ext.flutter.inspector.show'));
      expect(extensionChangedEvent['value'], isTrue);
      expect(service.rebuildCount, equals(1));
      expect(await service.testBoolExtension('show', <String, String>{}), equals('true'));
      expect(WidgetsApp.debugShowWidgetInspectorOverride, isTrue);
      expect(extensionChangedEvents.length, equals(1));
      expect(await service.testBoolExtension('show', <String, String>{'enabled': 'true'}), equals('true'));
      expect(extensionChangedEvents.length, equals(2));
      extensionChangedEvent = extensionChangedEvents.last;
      expect(extensionChangedEvent['extension'], equals('ext.flutter.inspector.show'));
      expect(extensionChangedEvent['value'], isTrue);
      expect(service.rebuildCount, equals(1));
      expect(await service.testBoolExtension('show', <String, String>{'enabled': 'false'}), equals('false'));
      expect(extensionChangedEvents.length, equals(3));
      extensionChangedEvent = extensionChangedEvents.last;
      expect(extensionChangedEvent['extension'], equals('ext.flutter.inspector.show'));
      expect(extensionChangedEvent['value'], isFalse);
      expect(await service.testBoolExtension('show', <String, String>{}), equals('false'));
      expect(extensionChangedEvents.length, equals(3));
      expect(service.rebuildCount, equals(2));
      expect(WidgetsApp.debugShowWidgetInspectorOverride, isFalse);
    });

    testWidgets('ext.flutter.inspector.screenshot', (WidgetTester tester) async {
      final GlobalKey outerContainerKey = GlobalKey();
      final GlobalKey paddingKey = GlobalKey();
      final GlobalKey redContainerKey = GlobalKey();
      final GlobalKey whiteContainerKey = GlobalKey();
      final GlobalKey sizedBoxKey = GlobalKey();

      // Complex widget tree intended to exercise features such as children
      // with rotational transforms and clipping without introducing platform
      // specific behavior as text rendering would.
      await tester.pumpWidget(
        Center(
          child: RepaintBoundaryWithDebugPaint(
            child: Container(
              key: outerContainerKey,
              color: Colors.white,
              child: Padding(
                key: paddingKey,
                padding: const EdgeInsets.all(100.0),
                child: SizedBox(
                  key: sizedBoxKey,
                  height: 100.0,
                  width: 100.0,
                  child: Transform.rotate(
                    angle: 1.0, // radians
                    child: ClipRRect(
                      borderRadius: const BorderRadius.only(
                        topLeft: Radius.elliptical(10.0, 20.0),
                        topRight: Radius.elliptical(5.0, 30.0),
                        bottomLeft: Radius.elliptical(2.5, 12.0),
                        bottomRight: Radius.elliptical(15.0, 6.0),
                      ),
                      child: Container(
                        key: redContainerKey,
                        color: Colors.red,
                        child: Container(
                          key: whiteContainerKey,
                          color: Colors.white,
                          child: RepaintBoundary(
                            child: Center(
                              child: Container(
                                color: Colors.black,
                                height: 10.0,
                                width: 10.0,
                              ),
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      );

      final Element repaintBoundary =
          find.byType(RepaintBoundaryWithDebugPaint).evaluate().single;

      final RenderRepaintBoundary renderObject = repaintBoundary.renderObject as RenderRepaintBoundary;

      final OffsetLayer layer = renderObject.debugLayer as OffsetLayer;
      final int expectedChildLayerCount = getChildLayerCount(layer);
      expect(expectedChildLayerCount, equals(2));
      await expectLater(
        layer.toImage(renderObject.semanticBounds.inflate(50.0)),
        matchesGoldenFile('inspector.repaint_boundary_margin.png'),
      );

      // Regression test for how rendering with a pixel scale other than 1.0
      // was handled.
      await expectLater(
        layer.toImage(
          renderObject.semanticBounds.inflate(50.0),
          pixelRatio: 0.5,
        ),
        matchesGoldenFile('inspector.repaint_boundary_margin_small.png'),
      );

      await expectLater(
        layer.toImage(
          renderObject.semanticBounds.inflate(50.0),
          pixelRatio: 2.0,
        ),
        matchesGoldenFile('inspector.repaint_boundary_margin_large.png'),
      );

      final Layer layerParent = layer.parent;
      final Layer firstChild = layer.firstChild;

      expect(layerParent, isNotNull);
      expect(firstChild, isNotNull);

      await expectLater(
        service.screenshot(
          repaintBoundary,
          width: 300.0,
          height: 300.0,
        ),
        matchesGoldenFile('inspector.repaint_boundary.png'),
      );

      // Verify that taking a screenshot didn't change the layers associated with
      // the renderObject.
      expect(renderObject.debugLayer, equals(layer));
      // Verify that taking a screenshot did not change the number of children
      // of the layer.
      expect(getChildLayerCount(layer), equals(expectedChildLayerCount));

      await expectLater(
        service.screenshot(
          repaintBoundary,
          width: 500.0,
          height: 500.0,
          margin: 50.0,
        ),
        matchesGoldenFile('inspector.repaint_boundary_margin.png'),
      );

      // Verify that taking a screenshot didn't change the layers associated with
      // the renderObject.
      expect(renderObject.debugLayer, equals(layer));
      // Verify that taking a screenshot did not change the number of children
      // of the layer.
      expect(getChildLayerCount(layer), equals(expectedChildLayerCount));

      // Make sure taking a screenshot didn't change the parent of the layer.
      expect(layer.parent, equals(layerParent));

      await expectLater(
        service.screenshot(
          repaintBoundary,
          width: 300.0,
          height: 300.0,
          debugPaint: true,
        ),
        matchesGoldenFile('inspector.repaint_boundary_debugPaint.png'),
      );
      // Verify that taking a screenshot with debug paint on did not change
      // the number of children the layer has.
      expect(getChildLayerCount(layer), equals(expectedChildLayerCount));

      // Ensure that creating screenshots including ones with debug paint
      // hasn't changed the regular render of the widget.
      await expectLater(
        find.byType(RepaintBoundaryWithDebugPaint),
        matchesGoldenFile('inspector.repaint_boundary.png'),
      );

      expect(renderObject.debugLayer, equals(layer));
      expect(layer.attached, isTrue);

      // Full size image
      await expectLater(
        service.screenshot(
          find.byKey(outerContainerKey).evaluate().single,
          width: 100.0,
          height: 100.0,
        ),
        matchesGoldenFile('inspector.container.png'),
      );

      await expectLater(
        service.screenshot(
          find.byKey(outerContainerKey).evaluate().single,
          width: 100.0,
          height: 100.0,
          debugPaint: true,
        ),
        matchesGoldenFile('inspector.container_debugPaint.png'),
      );

      {
        // Verify calling the screenshot method still works if the RenderObject
        // needs to be laid out again.
        final RenderObject container =
            find.byKey(outerContainerKey).evaluate().single.renderObject;
        container
          ..markNeedsLayout()
          ..markNeedsPaint();
        expect(container.debugNeedsLayout, isTrue);

        await expectLater(
          service.screenshot(
            find.byKey(outerContainerKey).evaluate().single,
            width: 100.0,
            height: 100.0,
            debugPaint: true,
          ),
          matchesGoldenFile('inspector.container_debugPaint.png'),
        );
        expect(container.debugNeedsLayout, isFalse);
      }

      // Small image
      await expectLater(
        service.screenshot(
          find.byKey(outerContainerKey).evaluate().single,
          width: 50.0,
          height: 100.0,
        ),
        matchesGoldenFile('inspector.container_small.png'),
      );

      await expectLater(
        service.screenshot(
          find.byKey(outerContainerKey).evaluate().single,
          width: 400.0,
          height: 400.0,
          maxPixelRatio: 3.0,
        ),
        matchesGoldenFile('inspector.container_large.png'),
      );

      // This screenshot will show the clip rect debug paint but no other
      // debug paint.
      await expectLater(
        service.screenshot(
          find.byType(ClipRRect).evaluate().single,
          width: 100.0,
          height: 100.0,
          debugPaint: true,
        ),
        matchesGoldenFile('inspector.clipRect_debugPaint.png'),
      );

      final Element clipRect = find.byType(ClipRRect).evaluate().single;

      final Future<ui.Image> clipRectScreenshot = service.screenshot(
        clipRect,
        width: 100.0,
        height: 100.0,
        margin: 20.0,
        debugPaint: true,
      );
      // Add a margin so that the clip icon shows up in the screenshot.
      // This golden image is platform dependent due to the clip icon.
      await expectLater(
        clipRectScreenshot,
        matchesGoldenFile('inspector.clipRect_debugPaint_margin.png'),
      );

      // Verify we get the same image if we go through the service extension
      // instead of invoking the screenshot method directly.
      final Future<Object> base64ScreenshotFuture = service.testExtension(
        'screenshot',
        <String, String>{
          'id': service.toId(clipRect, 'group'),
          'width': '100.0',
          'height': '100.0',
          'margin': '20.0',
          'debugPaint': 'true',
        },
      );

      final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
      final ui.Image screenshotImage = await binding.runAsync<ui.Image>(() async {
        final String base64Screenshot = await base64ScreenshotFuture as String;
        final ui.Codec codec = await ui.instantiateImageCodec(base64.decode(base64Screenshot));
        final ui.FrameInfo frame = await codec.getNextFrame();
        return frame.image;
      }, additionalTime: const Duration(seconds: 11));

      await expectLater(
        screenshotImage,
        matchesReferenceImage(await clipRectScreenshot),
        skip: !isLinux,
      );

      // Test with a very visible debug paint
      await expectLater(
        service.screenshot(
          find.byKey(paddingKey).evaluate().single,
          width: 300.0,
          height: 300.0,
          debugPaint: true,
        ),
        matchesGoldenFile('inspector.padding_debugPaint.png'),
      );

      // The bounds for this box crop its rendered content.
      await expectLater(
        service.screenshot(
          find.byKey(sizedBoxKey).evaluate().single,
          width: 300.0,
          height: 300.0,
          debugPaint: true,
        ),
        matchesGoldenFile('inspector.sizedBox_debugPaint.png'),
      );

      // Verify that setting a margin includes the previously cropped content.
      await expectLater(
        service.screenshot(
          find.byKey(sizedBoxKey).evaluate().single,
          width: 300.0,
          height: 300.0,
          margin: 50.0,
          debugPaint: true,
        ),
        matchesGoldenFile('inspector.sizedBox_debugPaint_margin.png'),
      );
    });

    test('ext.flutter.inspector.structuredErrors', () async {
      List<Map<Object, Object>> flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
      expect(flutterErrorEvents, isEmpty);

      final FlutterExceptionHandler oldHandler = FlutterError.presentError;

      try {
        // Enable structured errors.
        expect(await service.testBoolExtension(
            'structuredErrors', <String, String>{'enabled': 'true'}),
            equals('true'));

        // Create an error.
        FlutterError.reportError(FlutterErrorDetails(
          library: 'rendering library',
          context: ErrorDescription('during layout'),
          exception: StackTrace.current,
        ));

        // Validate that we received an error.
        flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
        expect(flutterErrorEvents, hasLength(1));

        // Validate the error contents.
        Map<Object, Object> error = flutterErrorEvents.first;
        expect(error['description'], 'Exception caught by rendering library');
        expect(error['children'], isEmpty);

        // Validate that we received an error count.
        expect(error['errorsSinceReload'], 0);
        expect(
            error['renderedErrorText'],
            startsWith(
                '══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞════════════'));

        // Send a second error.
        FlutterError.reportError(FlutterErrorDetails(
          library: 'rendering library',
          context: ErrorDescription('also during layout'),
          exception: StackTrace.current,
        ));

        // Validate that the error count increased.
        flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
        expect(flutterErrorEvents, hasLength(2));
        error = flutterErrorEvents.last;
        expect(error['errorsSinceReload'], 1);
        expect(error['renderedErrorText'], startsWith('Another exception was thrown:'));

        // Reloads the app.
        final FlutterExceptionHandler oldHandler = FlutterError.onError;
        final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
        // We need the runTest to setup the fake async in the test binding.
        await binding.runTest(() async {
          binding.reassembleApplication();
          await binding.pump();
        }, () { });
        // The run test overrides the flutter error handler, so we should
        // restore it back for the structure error to continue working.
        FlutterError.onError = oldHandler;
        // Cleans up the fake async so it does not bleed into next test.
        binding.postTest();

        // Send another error.
        FlutterError.reportError(FlutterErrorDetails(
          library: 'rendering library',
          context: ErrorDescription('during layout'),
          exception: StackTrace.current,
        ));

        // And, validate that the error count has been reset.
        flutterErrorEvents = service.getEventsDispatched('Flutter.Error');
        expect(flutterErrorEvents, hasLength(3));
        error = flutterErrorEvents.last;
        expect(error['errorsSinceReload'], 0);
      } finally {
        FlutterError.presentError = oldHandler;
      }
    });

    testWidgets('Screenshot of composited transforms - only offsets', (WidgetTester tester) async {
      // Composited transforms are challenging to take screenshots of as the
      // LeaderLayer and FollowerLayer classes used by CompositedTransformTarget
      // and CompositedTransformFollower depend on traversing ancestors of the
      // layer tree and mutating a [LayerLink] object when attaching layers to
      // the tree so that the FollowerLayer knows about the LeaderLayer.
      // 1. Finding the correct position for the follower layers requires
      // traversing the ancestors of the follow layer to find a common ancestor
      // with the leader layer.
      // 2. Creating a LeaderLayer and attaching it to a layer tree has side
      // effects as the leader layer will attempt to modify the mutable
      // LeaderLayer object shared by the LeaderLayer and FollowerLayer.
      // These tests verify that screenshots can still be taken and look correct
      // when the leader and follower layer are both in the screenshots and when
      // only the leader or follower layer is in the screenshot.
      final LayerLink link = LayerLink();
      final GlobalKey key = GlobalKey();
      final GlobalKey mainStackKey = GlobalKey();
      final GlobalKey transformTargetParent = GlobalKey();
      final GlobalKey stackWithTransformFollower = GlobalKey();

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: RepaintBoundary(
            child: Stack(
              key: mainStackKey,
              children: <Widget>[
                Stack(
                  key: transformTargetParent,
                  children: <Widget>[
                    Positioned(
                      left: 123.0,
                      top: 456.0,
                      child: CompositedTransformTarget(
                        link: link,
                        child: Container(height: 20.0, width: 20.0, color: const Color.fromARGB(128, 255, 0, 0)),
                      ),
                    ),
                  ],
                ),
                Positioned(
                  left: 787.0,
                  top: 343.0,
                  child: Stack(
                    key: stackWithTransformFollower,
                    children: <Widget>[
                      // Container so we can see how the follower layer was
                      // transformed relative to its initial location.
                      Container(height: 15.0, width: 15.0, color: const Color.fromARGB(128, 0, 0, 255)),
                      CompositedTransformFollower(
                        link: link,
                        child: Container(key: key, height: 10.0, width: 10.0, color: const Color.fromARGB(128, 0, 255, 0)),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
      final RenderBox box = key.currentContext.findRenderObject() as RenderBox;
      expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));

      await expectLater(
        find.byKey(mainStackKey),
        matchesGoldenFile('inspector.composited_transform.only_offsets.png'),
      );

      await expectLater(
        WidgetInspectorService.instance.screenshot(
          find.byKey(stackWithTransformFollower).evaluate().first,
          width: 5000.0,
          height: 500.0,
        ),
        matchesGoldenFile('inspector.composited_transform.only_offsets_follower.png'),
      );

      await expectLater(
        WidgetInspectorService.instance.screenshot(find.byType(Stack).evaluate().first, width: 300.0, height: 300.0),
        matchesGoldenFile('inspector.composited_transform.only_offsets_small.png'),
      );

      await expectLater(
        WidgetInspectorService.instance.screenshot(
          find.byKey(transformTargetParent).evaluate().first,
          width: 500.0,
          height: 500.0,
        ),
        matchesGoldenFile('inspector.composited_transform.only_offsets_target.png'),
      );
    });

    testWidgets('Screenshot composited transforms - with rotations', (WidgetTester tester) async {
      final LayerLink link = LayerLink();
      final GlobalKey key1 = GlobalKey();
      final GlobalKey key2 = GlobalKey();
      final GlobalKey rotate1 = GlobalKey();
      final GlobalKey rotate2 = GlobalKey();
      final GlobalKey mainStackKey = GlobalKey();
      final GlobalKey stackWithTransformTarget = GlobalKey();
      final GlobalKey stackWithTransformFollower = GlobalKey();

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            key: mainStackKey,
            children: <Widget>[
              Stack(
                key: stackWithTransformTarget,
                children: <Widget>[
                  Positioned(
                    top: 123.0,
                    left: 456.0,
                    child: Transform.rotate(
                      key: rotate1,
                      angle: 1.0, // radians
                      child: CompositedTransformTarget(
                        link: link,
                        child: Container(key: key1, height: 20.0, width: 20.0, color: const Color.fromARGB(128, 255, 0, 0)),
                      ),
                    ),
                  ),
                ],
              ),
              Positioned(
                top: 487.0,
                left: 243.0,
                child: Stack(
                  key: stackWithTransformFollower,
                  children: <Widget>[
                    Container(height: 15.0, width: 15.0, color: const Color.fromARGB(128, 0, 0, 255)),
                    Transform.rotate(
                      key: rotate2,
                      angle: -0.3, // radians
                      child: CompositedTransformFollower(
                        link: link,
                        child: Container(key: key2, height: 10.0, width: 10.0, color: const Color.fromARGB(128, 0, 255, 0)),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      );
      final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
      final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
      // Snapshot the positions of the two relevant boxes to ensure that taking
      // screenshots doesn't impact their positions.
      final Offset position1 = box1.localToGlobal(Offset.zero);
      final Offset position2 = box2.localToGlobal(Offset.zero);
      expect(position1.dx, moreOrLessEquals(position2.dx));
      expect(position1.dy, moreOrLessEquals(position2.dy));

      // Image of the full scene to use as reference to help validate that the
      // screenshots of specific subtrees are reasonable.
      await expectLater(
        find.byKey(mainStackKey),
        matchesGoldenFile('inspector.composited_transform.with_rotations.png'),
      );

      await expectLater(
        WidgetInspectorService.instance.screenshot(
          find.byKey(mainStackKey).evaluate().first,
          width: 500.0,
          height: 500.0,
        ),
        matchesGoldenFile('inspector.composited_transform.with_rotations_small.png'),
      );

      await expectLater(
        WidgetInspectorService.instance.screenshot(
          find.byKey(stackWithTransformTarget).evaluate().first,
          width: 500.0,
          height: 500.0,
        ),
        matchesGoldenFile('inspector.composited_transform.with_rotations_target.png'),
      );

      await expectLater(
        WidgetInspectorService.instance.screenshot(
          find.byKey(stackWithTransformFollower).evaluate().first,
          width: 500.0,
          height: 500.0,
        ),
        matchesGoldenFile('inspector.composited_transform.with_rotations_follower.png'),
      );

      // Make sure taking screenshots hasn't modified the positions of the
      // TransformTarget or TransformFollower layers.
      expect(identical(key1.currentContext.findRenderObject(), box1), isTrue);
      expect(identical(key2.currentContext.findRenderObject(), box2), isTrue);
      expect(box1.localToGlobal(Offset.zero), equals(position1));
      expect(box2.localToGlobal(Offset.zero), equals(position2));
    });

    testWidgets('getChildrenDetailsSubtree', (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          title: 'Hello, World',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: Scaffold(
            appBar: AppBar(
              title: const Text('Hello, World'),
            ),
            body: const Center(
              child: Text('Hello, World!'),
            ),
          ),
        ),
      );

      // Figure out the pubRootDirectory
      final Map<String, Object> jsonObject = await service.testExtension(
          'getSelectedWidget',
          <String, String>{'arg': null, 'objectGroup': 'my-group'},
      ) as Map<String, Object>;
      final Map<String, Object> creationLocation = jsonObject['creationLocation'] as Map<String, Object>;
      expect(creationLocation, isNotNull);
      final String file = creationLocation['file'] as String;
      expect(file, endsWith('widget_inspector_test.dart'));
      final List<String> segments = Uri.parse(file).pathSegments;
      // Strip a couple subdirectories away to generate a plausible pub rootdirectory.
      final String pubRootTest = '/' + segments.take(segments.length - 2).join('/');
      service.setPubRootDirectories(<String>[pubRootTest]);

      final String summary = service.getRootWidgetSummaryTree('foo1');
      final List<dynamic> childrenOfRoot = json.decode(summary)['children'] as List<dynamic>;
      final List<dynamic> childrenOfMaterialApp = childrenOfRoot.first['children'] as List<dynamic>;
      final Map<String, Object> scaffold = childrenOfMaterialApp.first as Map<String, Object>;
      expect(scaffold['description'], 'Scaffold');
      final String objectId = scaffold['objectId'] as String;
      final String details = service.getDetailsSubtree(objectId, 'foo2');
      final List<dynamic> detailedChildren = json.decode(details)['children'] as List<dynamic>;

      final List<Map<String, Object>> appBars = <Map<String, Object>>[];
      void visitChildren(List<dynamic> children) {
        for (final Map<String, Object> child in children.cast<Map<String, Object>>()) {
          if (child['description'] == 'AppBar') {
            appBars.add(child);
          }
          if (child.containsKey('children')) {
            visitChildren(child['children'] as List<dynamic>);
          }
        }
      }
      visitChildren(detailedChildren);
      expect(appBars.single, isNot(contains('children')));
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.

    testWidgets('InspectorSerializationDelegate addAdditionalPropertiesCallback', (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          title: 'Hello World!',
          home: Scaffold(
            appBar: AppBar(
              title: const Text('Hello World!'),
            ),
            body: Center(
              child: Column(
                children: const <Widget>[
                  Text('Hello World!'),
                ],
              ),
            ),
          ),
        ),
      );
      final Finder columnWidgetFinder = find.byType(Column);
      expect(columnWidgetFinder, findsOneWidget);
      final Element columnWidgetElement = columnWidgetFinder
        .evaluate()
        .first;
      final DiagnosticsNode node = columnWidgetElement.toDiagnosticsNode();
      final InspectorSerializationDelegate delegate =
        InspectorSerializationDelegate(
          service: service,
          summaryTree: false,
          includeProperties: true,
          addAdditionalPropertiesCallback:
            (DiagnosticsNode node, InspectorSerializationDelegate delegate) {
              final Map<String, Object> additionalJson = <String, Object>{};
              final Object value = node.value;
              if (value is Element) {
                additionalJson['renderObject'] =
                  value.renderObject.toDiagnosticsNode().toJsonMap(
                    delegate.copyWith(subtreeDepth: 0),
                  );
              }
              additionalJson['callbackExecuted'] = true;
              return additionalJson;
            },
        );
      final Map<String, Object> json = node.toJsonMap(delegate);
      expect(json['callbackExecuted'], true);
      expect(json.containsKey('renderObject'), true);
      expect(json['renderObject'], isA<Map<String, dynamic>>());
      final Map<String, dynamic> renderObjectJson = json['renderObject'] as Map<String, dynamic>;
      expect(renderObjectJson['description'], startsWith('RenderFlex'));

      final InspectorSerializationDelegate emptyDelegate =
        InspectorSerializationDelegate(
          service: service,
          summaryTree: false,
          includeProperties: true,
          addAdditionalPropertiesCallback:
            (DiagnosticsNode node, InspectorSerializationDelegate delegate) {
              return null;
            },
        );
      final InspectorSerializationDelegate defaultDelegate =
        InspectorSerializationDelegate(
          service: service,
          summaryTree: false,
          includeProperties: true,
          addAdditionalPropertiesCallback: null,
        );
      expect(node.toJsonMap(emptyDelegate), node.toJsonMap(defaultDelegate));
    });

    testWidgets('debugIsLocalCreationLocation test', (WidgetTester tester) async {

      final GlobalKey key = GlobalKey();

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: Container(
            padding: const EdgeInsets.all(8),
            child: Text('target', key: key, textDirection: TextDirection.ltr),
          ),
        ),
      );

      final Element element = key.currentContext as Element;

      expect(debugIsLocalCreationLocation(element), isTrue);
      expect(debugIsLocalCreationLocation(element.widget), isTrue);

      // Padding is inside container
      final Finder paddingFinder = find.byType(Padding);

      final Element paddingElement = paddingFinder.evaluate().first;

      expect(debugIsLocalCreationLocation(paddingElement), isFalse);
      expect(debugIsLocalCreationLocation(paddingElement.widget), isFalse);
    }, skip: !WidgetInspectorService.instance.isWidgetCreationTracked()); // Test requires --track-widget-creation flag.

  }
}

void addToKnownLocationsMap({
  @required Map<int, _CreationLocation> knownLocations,
  @required Map<String, List<int>> newLocations,
}) {
  newLocations.forEach((String file, List<int> entries) {
    assert(entries.length % 3 == 0);
    for (int i = 0; i < entries.length; i += 3) {
      final int id = entries[i];
      final int line = entries[i + 1];
      final int column = entries[i + 2];
      assert(!knownLocations.containsKey(id));
      knownLocations[id] =
          _CreationLocation(file: file, line: line, column: column, id: id);
    }
  });
}
