blob: e74a292244073f37f0fc9d05e78f42c8465035c4 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:ansicolor/ansicolor.dart';
import 'package:devtools_app/src/common_widgets.dart';
import 'package:devtools_app/src/debugger/console.dart';
import 'package:devtools_app/src/debugger/controls.dart';
import 'package:devtools_app/src/debugger/debugger_controller.dart';
import 'package:devtools_app/src/debugger/debugger_model.dart';
import 'package:devtools_app/src/debugger/debugger_screen.dart';
import 'package:devtools_app/src/globals.dart';
import 'package:devtools_app/src/service_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:vm_service/vm_service.dart';
import 'support/mocks.dart';
import 'support/utils.dart';
import 'support/wrappers.dart';
void main() {
DebuggerScreen screen;
FakeServiceManager fakeServiceManager;
MockDebuggerController debuggerController;
group('DebuggerScreen', () {
Future<void> pumpDebuggerScreen(
WidgetTester tester, DebuggerController controller) async {
await tester.pumpWidget(wrapWithControllers(
const DebuggerScreenBody(),
debugger: controller,
));
}
setUp(() async {
fakeServiceManager = FakeServiceManager();
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(false);
setGlobal(ServiceConnectionManager, fakeServiceManager);
when(fakeServiceManager.errorBadgeManager.errorCountNotifier(any))
.thenReturn(ValueNotifier<int>(0));
screen = const DebuggerScreen();
debuggerController = MockDebuggerController();
when(debuggerController.isPaused).thenReturn(ValueNotifier(false));
when(debuggerController.resuming).thenReturn(ValueNotifier(false));
when(debuggerController.breakpoints).thenReturn(ValueNotifier([]));
when(debuggerController.isSystemIsolate).thenReturn(false);
when(debuggerController.breakpointsWithLocation)
.thenReturn(ValueNotifier([]));
when(debuggerController.librariesVisible)
.thenReturn(ValueNotifier(false));
when(debuggerController.currentScriptRef).thenReturn(ValueNotifier(null));
when(debuggerController.sortedScripts).thenReturn(ValueNotifier([]));
when(debuggerController.selectedBreakpoint)
.thenReturn(ValueNotifier(null));
when(debuggerController.stackFramesWithLocation)
.thenReturn(ValueNotifier([]));
when(debuggerController.selectedStackFrame)
.thenReturn(ValueNotifier(null));
when(debuggerController.hasTruncatedFrames)
.thenReturn(ValueNotifier(false));
when(debuggerController.stdio)
.thenReturn(ValueNotifier([ConsoleLine.text('')]));
when(debuggerController.scriptLocation).thenReturn(ValueNotifier(null));
when(debuggerController.exceptionPauseMode)
.thenReturn(ValueNotifier('Unhandled'));
when(debuggerController.variables).thenReturn(ValueNotifier([]));
when(debuggerController.currentParsedScript)
.thenReturn(ValueNotifier<ParsedScript>(null));
});
testWidgets('builds its tab', (WidgetTester tester) async {
await tester.pumpWidget(wrap(Builder(builder: screen.buildTab)));
expect(find.text('Debugger'), findsOneWidget);
});
testWidgets('has Console / stdio area', (WidgetTester tester) async {
when(debuggerController.stdio)
.thenReturn(ValueNotifier([ConsoleLine.text('test stdio')]));
await pumpDebuggerScreen(tester, debuggerController);
expect(find.text('Console'), findsOneWidget);
// test for stdio output.
expect(find.selectableText('test stdio'), findsOneWidget);
});
testWidgets('Console area shows processed ansi text',
(WidgetTester tester) async {
when(debuggerController.stdio)
.thenReturn(ValueNotifier([ConsoleLine.text(_ansiCodesOutput())]));
await pumpDebuggerScreen(tester, debuggerController);
final finder =
find.selectableText('Ansi color codes processed for console');
expect(finder, findsOneWidget);
finder.evaluate().forEach((element) {
final selectableText = element.widget as SelectableText;
final textSpan = selectableText.textSpan;
final secondSpan = textSpan.children[1] as TextSpan;
expect(
secondSpan.text,
'console',
reason: 'Text with ansi code should be in separate span',
);
expect(
secondSpan.style.backgroundColor,
const Color.fromRGBO(215, 95, 135, 1),
);
});
});
group('ConsoleControls', () {
testWidgets('Console Controls are rendered disabled when stdio is empty',
(WidgetTester tester) async {
when(debuggerController.stdio).thenReturn(ValueNotifier([]));
await pumpDebuggerScreen(tester, debuggerController);
expect(find.byKey(DebuggerConsole.clearStdioButtonKey), findsOneWidget);
expect(find.byKey(DebuggerConsole.copyToClipboardButtonKey),
findsOneWidget);
final clearStdioElement =
find.byKey(DebuggerConsole.clearStdioButtonKey).evaluate().first;
final clearStdioButton = clearStdioElement.widget as ToolbarAction;
expect(clearStdioButton.onPressed, isNull);
});
testWidgets('Tapping the Console Clear button clears stdio.',
(WidgetTester tester) async {
when(debuggerController.stdio)
.thenReturn(ValueNotifier([ConsoleLine.text(_ansiCodesOutput())]));
await pumpDebuggerScreen(tester, debuggerController);
final clearButton = find.byKey(DebuggerConsole.clearStdioButtonKey);
expect(clearButton, findsOneWidget);
await tester.tap(clearButton);
verify(debuggerController.clearStdio());
});
group('Clipboard', () {
var _clipboardContents = '';
final _stdio = ['First line', _ansiCodesOutput(), 'Third line']
.map((text) => ConsoleLine.text(text))
.toList();
final _expected = _stdio.join('\n');
setUp(() {
// This intercepts the Clipboard.setData SystemChannel message,
// and stores the contents that were (attempted) to be copied.
SystemChannels.platform.setMockMethodCallHandler((MethodCall call) {
switch (call.method) {
case 'Clipboard.setData':
_clipboardContents = call.arguments['text'];
return Future.value(true);
break;
case 'Clipboard.getData':
return Future.value(<String, dynamic>{});
break;
default:
break;
}
return Future.value(true);
});
});
tearDown(() {
// Cleanup the SystemChannel
SystemChannels.platform.setMockMethodCallHandler(null);
});
testWidgets(
'Tapping the Copy to Clipboard button attempts to copy stdio to clipboard.',
(WidgetTester tester) async {
when(debuggerController.stdio).thenReturn(ValueNotifier(_stdio));
await pumpDebuggerScreen(tester, debuggerController);
final copyButton =
find.byKey(DebuggerConsole.copyToClipboardButtonKey);
expect(copyButton, findsOneWidget);
expect(_clipboardContents, isEmpty);
await tester.tap(copyButton);
expect(_clipboardContents, equals(_expected));
});
});
});
testWidgets('Libraries hidden', (WidgetTester tester) async {
final scripts = [
ScriptRef(uri: 'package:/test/script.dart', id: 'test-script')
];
when(debuggerController.sortedScripts).thenReturn(ValueNotifier(scripts));
// Libraries view is hidden
when(debuggerController.librariesVisible)
.thenReturn(ValueNotifier(false));
await pumpDebuggerScreen(tester, debuggerController);
expect(find.text('Libraries'), findsNothing);
});
testWidgets('Libraries visible', (WidgetTester tester) async {
final scripts = [
ScriptRef(uri: 'package:test/script.dart', id: 'test-script')
];
when(debuggerController.sortedScripts).thenReturn(ValueNotifier(scripts));
// Libraries view is shown
when(debuggerController.librariesVisible).thenReturn(ValueNotifier(true));
await pumpDebuggerScreen(tester, debuggerController);
expect(find.text('Libraries'), findsOneWidget);
// test for items in the libraries tree
expect(find.text(scripts.first.uri.split('/').first), findsOneWidget);
});
testWidgets('Breakpoints show items', (WidgetTester tester) async {
final breakpoints = [
Breakpoint(
breakpointNumber: 1,
id: 'bp1',
resolved: false,
location: UnresolvedSourceLocation(
scriptUri: 'package:test/script.dart',
line: 10,
),
)
];
final breakpointsWithLocation = [
BreakpointAndSourcePosition.create(
breakpoints.first,
SourcePosition(line: 10, column: 1),
)
];
when(debuggerController.breakpoints)
.thenReturn(ValueNotifier(breakpoints));
when(debuggerController.breakpointsWithLocation)
.thenReturn(ValueNotifier(breakpointsWithLocation));
when(debuggerController.sortedScripts).thenReturn(ValueNotifier([]));
when(debuggerController.stdio).thenReturn(ValueNotifier([]));
when(debuggerController.scriptLocation).thenReturn(ValueNotifier(null));
await pumpDebuggerScreen(tester, debuggerController);
expect(find.text('Breakpoints'), findsOneWidget);
// test for items in the breakpoint list
expect(
find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text.toPlainText().contains('script.dart:10')),
findsOneWidget,
);
});
testWidgetsWithWindowSize(
'Call Stack shows items', const Size(1000.0, 4000.0),
(WidgetTester tester) async {
final stackFrames = [
Frame(
index: 0,
code: CodeRef(
name: 'testCodeRef', id: 'testCodeRef', kind: CodeKind.kDart),
location: SourceLocation(
script:
ScriptRef(uri: 'package:test/script.dart', id: 'script.dart'),
tokenPos: 10,
),
kind: FrameKind.kRegular,
),
Frame(
index: 1,
location: SourceLocation(
script:
ScriptRef(uri: 'package:test/script1.dart', id: 'script1.dart'),
tokenPos: 10,
),
kind: FrameKind.kRegular,
),
Frame(
index: 2,
code: CodeRef(
name: '[Unoptimized] testCodeRef2',
id: 'testCodeRef2',
kind: CodeKind.kDart,
),
location: SourceLocation(
script:
ScriptRef(uri: 'package:test/script2.dart', id: 'script2.dart'),
tokenPos: 10,
),
kind: FrameKind.kRegular,
),
Frame(
index: 3,
code: CodeRef(
name: 'testCodeRef3.<anonymous closure>',
id: 'testCodeRef3.closure',
kind: CodeKind.kDart,
),
location: SourceLocation(
script:
ScriptRef(uri: 'package:test/script3.dart', id: 'script3.dart'),
tokenPos: 10,
),
kind: FrameKind.kRegular,
),
Frame(
index: 4,
location: SourceLocation(
script:
ScriptRef(uri: 'package:test/script4.dart', id: 'script4.dart'),
tokenPos: 10,
),
kind: FrameKind.kAsyncSuspensionMarker,
),
];
final stackFramesWithLocation =
stackFrames.map<StackFrameAndSourcePosition>((frame) {
return StackFrameAndSourcePosition(
frame,
position: SourcePosition(
line: stackFrames.indexOf(frame),
column: 10,
),
);
}).toList();
when(debuggerController.stackFramesWithLocation)
.thenReturn(ValueNotifier(stackFramesWithLocation));
when(debuggerController.isPaused).thenReturn(ValueNotifier(true));
await pumpDebuggerScreen(tester, debuggerController);
expect(find.text('Call Stack'), findsOneWidget);
// Stack frame 0
expect(
find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text.toPlainText().contains('testCodeRef() script.dart 0')),
findsOneWidget,
);
// verify that the frame has a tooltip
expect(
find.byTooltip('testCodeRef() script.dart 0'),
findsOneWidget,
);
// Stack frame 1
expect(
find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text.toPlainText().contains('<none> script1.dart 1')),
findsOneWidget,
);
// Stack frame 2
expect(
find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text
.toPlainText()
.contains('testCodeRef2() script2.dart 2')),
findsOneWidget,
);
// Stack frame 3
expect(
find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text
.toPlainText()
.contains('testCodeRef3.<closure>() script3.dart 3')),
findsOneWidget,
);
// Stack frame 4
expect(find.text('<async break>'), findsOneWidget);
});
testWidgetsWithWindowSize(
'Variables shows items', const Size(1000.0, 4000.0),
(WidgetTester tester) async {
when(debuggerController.variables)
.thenReturn(ValueNotifier(testVariables));
await pumpDebuggerScreen(tester, debuggerController);
expect(find.text('Variables'), findsOneWidget);
final listFinder = find.selectableText('Root 1: _GrowableList (2 items)');
// expect a tooltip for the list value
expect(
find.byTooltip('_GrowableList (2 items)'),
findsOneWidget,
);
final mapFinder = find
.selectableTextContaining('Root 2: _InternalLinkedHashmap (2 items)');
final mapElement1Finder = find.selectableTextContaining("['key1']: 1.0");
final mapElement2Finder = find.selectableTextContaining("['key2']: 1.1");
expect(listFinder, findsOneWidget);
expect(mapFinder, findsOneWidget);
expect(
find.selectableTextContaining("Root 3: 'test str...'"),
findsOneWidget,
);
expect(
find.selectableTextContaining('Root 4: true'),
findsOneWidget,
);
// Expand list.
expect(find.selectableTextContaining('0: 3'), findsNothing);
expect(find.selectableTextContaining('1: 4'), findsNothing);
await tester.tap(listFinder);
await tester.pumpAndSettle();
expect(find.selectableTextContaining('0: 3'), findsOneWidget);
expect(find.selectableTextContaining('1: 4'), findsOneWidget);
// Expand map.
expect(mapElement1Finder, findsNothing);
expect(mapElement2Finder, findsNothing);
await tester.tap(mapFinder);
await tester.pumpAndSettle();
expect(mapElement1Finder, findsOneWidget);
expect(mapElement2Finder, findsOneWidget);
});
WidgetPredicate createDebuggerButtonPredicate(String title) {
return (Widget widget) {
if (widget is DebuggerButton && widget.title == title) {
return true;
}
return false;
};
}
testWidgets('debugger controls running', (WidgetTester tester) async {
await tester.pumpWidget(wrapWithControllers(
Builder(builder: screen.build),
debugger: debuggerController,
));
expect(find.byWidgetPredicate(createDebuggerButtonPredicate('Pause')),
findsOneWidget);
final DebuggerButton pause = getWidgetFromFinder(
find.byWidgetPredicate(createDebuggerButtonPredicate('Pause')));
expect(pause.onPressed, isNotNull);
expect(find.byWidgetPredicate(createDebuggerButtonPredicate('Resume')),
findsOneWidget);
final DebuggerButton resume = getWidgetFromFinder(
find.byWidgetPredicate(createDebuggerButtonPredicate('Resume')));
expect(resume.onPressed, isNull);
});
testWidgets('debugger controls paused', (WidgetTester tester) async {
when(debuggerController.isPaused).thenReturn(ValueNotifier(true));
when(debuggerController.stackFramesWithLocation)
.thenReturn(ValueNotifier([
StackFrameAndSourcePosition(
Frame(
index: 0,
code: CodeRef(
name: 'testCodeRef', id: 'testCodeRef', kind: CodeKind.kDart),
location: SourceLocation(
script:
ScriptRef(uri: 'package:test/script.dart', id: 'script.dart'),
tokenPos: 10,
),
kind: FrameKind.kRegular,
),
position: SourcePosition(
line: 1,
column: 10,
),
)
]));
await tester.pumpWidget(wrapWithControllers(
Builder(builder: screen.build),
debugger: debuggerController,
));
expect(find.byWidgetPredicate(createDebuggerButtonPredicate('Pause')),
findsOneWidget);
final DebuggerButton pause = getWidgetFromFinder(
find.byWidgetPredicate(createDebuggerButtonPredicate('Pause')));
expect(pause.onPressed, isNull);
expect(find.byWidgetPredicate(createDebuggerButtonPredicate('Resume')),
findsOneWidget);
final DebuggerButton resume = getWidgetFromFinder(
find.byWidgetPredicate(createDebuggerButtonPredicate('Resume')));
expect(resume.onPressed, isNotNull);
});
testWidgets('debugger controls break on exceptions',
(WidgetTester tester) async {
await tester.pumpWidget(wrapWithControllers(
Builder(builder: screen.build),
debugger: debuggerController,
));
expect(find.text('Ignore'), findsOneWidget);
});
});
group('FloatingDebuggerControls', () {
setUp(() {
debuggerController = MockDebuggerController();
when(debuggerController.isPaused).thenReturn(ValueNotifier<bool>(true));
});
Future<void> pumpControls(WidgetTester tester) async {
await tester.pumpWidget(wrapWithControllers(
FloatingDebuggerControls(),
debugger: debuggerController,
));
await tester.pumpAndSettle();
}
testWidgets('display as expected', (WidgetTester tester) async {
await pumpControls(tester);
final animatedOpacityFinder = find.byType(AnimatedOpacity);
expect(animatedOpacityFinder, findsOneWidget);
final AnimatedOpacity animatedOpacity =
animatedOpacityFinder.evaluate().first.widget;
expect(animatedOpacity.opacity, equals(1.0));
expect(
find.text('Main isolate is paused in the debugger'), findsOneWidget);
expect(find.byTooltip('Resume'), findsOneWidget);
expect(find.byTooltip('Step over'), findsOneWidget);
});
testWidgets('can resume', (WidgetTester tester) async {
bool didResume = false;
Future<Success> resume() {
didResume = true;
return Future.value(Success());
}
when(debuggerController.resume()).thenAnswer((_) => resume());
await pumpControls(tester);
expect(didResume, isFalse);
await tester.tap(find.byTooltip('Resume'));
await tester.pumpAndSettle();
expect(didResume, isTrue);
});
testWidgets('can step over', (WidgetTester tester) async {
bool didStep = false;
Future<Success> stepOver() {
didStep = true;
return Future.value(Success());
}
when(debuggerController.stepOver()).thenAnswer((_) => stepOver());
await pumpControls(tester);
expect(didStep, isFalse);
await tester.tap(find.byTooltip('Step over'));
await tester.pumpAndSettle();
expect(didStep, isTrue);
});
testWidgets('are hidden when app is not paused',
(WidgetTester tester) async {
when(debuggerController.isPaused).thenReturn(ValueNotifier<bool>(false));
await pumpControls(tester);
final animatedOpacityFinder = find.byType(AnimatedOpacity);
expect(animatedOpacityFinder, findsOneWidget);
final AnimatedOpacity animatedOpacity =
animatedOpacityFinder.evaluate().first.widget;
expect(animatedOpacity.opacity, equals(0.0));
});
});
}
Widget getWidgetFromFinder(Finder finder) {
return finder.first.evaluate().first.widget;
}
String _ansiCodesOutput() {
final sb = StringBuffer();
sb.write('Ansi color codes processed for ');
final pen = AnsiPen()..rgb(r: 0.8, g: 0.3, b: 0.4, bg: true);
sb.write(pen('console'));
return sb.toString();
}
final testVariables = [
Variable.create(BoundVariable(
name: 'Root 1',
value: InstanceRef(
id: 'ref1',
kind: InstanceKind.kList,
classRef: ClassRef(
name: '_GrowableList',
id: 'ref2',
),
length: 2,
identityHashCode: null,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
))
..addAllChildren([
Variable.create(BoundVariable(
name: '0',
value: InstanceRef(
id: 'ref3',
kind: InstanceKind.kInt,
classRef: ClassRef(name: 'Integer', id: 'ref4'),
valueAsString: '3',
valueAsStringIsTruncated: false,
identityHashCode: null,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
)),
Variable.create(BoundVariable(
name: '1',
value: InstanceRef(
id: 'ref5',
kind: InstanceKind.kInt,
classRef: ClassRef(name: 'Integer', id: 'ref6'),
valueAsString: '4',
valueAsStringIsTruncated: false,
identityHashCode: null,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
)),
]),
Variable.create(BoundVariable(
name: 'Root 2',
value: InstanceRef(
id: 'ref7',
kind: InstanceKind.kMap,
classRef: ClassRef(name: '_InternalLinkedHashmap', id: 'ref8'),
length: 2,
identityHashCode: null,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
))
..addAllChildren([
Variable.create(BoundVariable(
name: "['key1']",
value: InstanceRef(
id: 'ref9',
kind: InstanceKind.kDouble,
classRef: ClassRef(name: 'Double', id: 'ref10'),
valueAsString: '1.0',
valueAsStringIsTruncated: false,
identityHashCode: null,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
)),
Variable.create(BoundVariable(
name: "['key2']",
value: InstanceRef(
id: 'ref11',
kind: InstanceKind.kDouble,
classRef: ClassRef(name: 'Double', id: 'ref12'),
valueAsString: '1.1',
valueAsStringIsTruncated: false,
identityHashCode: null,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
)),
]),
Variable.create(BoundVariable(
name: 'Root 3',
value: InstanceRef(
id: 'ref13',
kind: InstanceKind.kString,
classRef: ClassRef(name: 'String', id: 'ref14'),
valueAsString: 'test str',
valueAsStringIsTruncated: true,
identityHashCode: null,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
)),
Variable.create(BoundVariable(
name: 'Root 4',
value: InstanceRef(
id: 'ref15',
kind: InstanceKind.kBool,
classRef: ClassRef(name: 'Boolean', id: 'ref16'),
valueAsString: 'true',
valueAsStringIsTruncated: false,
identityHashCode: null,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
)),
];