blob: 57a3bb6ced46244790778723d70a4746e4c3fc61 [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(useFakeService: true);
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(false);
setGlobal(ServiceConnectionManager, fakeServiceManager);
screen = const DebuggerScreen();
debuggerController = MockDebuggerController();
when(debuggerController.isPaused).thenReturn(ValueNotifier(false));
when(debuggerController.resuming).thenReturn(ValueNotifier(false));
when(debuggerController.hasFrames).thenReturn(ValueNotifier(false));
when(debuggerController.breakpoints).thenReturn(ValueNotifier([]));
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.stdio).thenReturn(ValueNotifier(['']));
when(debuggerController.scriptLocation).thenReturn(ValueNotifier(null));
when(debuggerController.exceptionPauseMode)
.thenReturn(ValueNotifier('Unhandled'));
when(debuggerController.variables).thenReturn(ValueNotifier([]));
});
testWidgets('builds its tab', (WidgetTester tester) async {
await tester.pumpWidget(wrap(Builder(builder: screen.buildTab)));
expect(find.text('Debugger'), findsOneWidget);
});
testWidgets('builds disabled message when disabled for profile mode',
(WidgetTester tester) async {
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(true);
await tester.pumpWidget(wrap(Builder(builder: screen.build)));
expect(find.byType(DebuggerScreenBody), findsNothing);
expect(find.byType(DisabledForProfileBuildMessage), findsOneWidget);
});
testWidgets('has Console / stdio area', (WidgetTester tester) async {
when(debuggerController.stdio).thenReturn(ValueNotifier(['test stdio']));
await pumpDebuggerScreen(tester, debuggerController);
expect(find.text('Console'), findsOneWidget);
// test for stdio output.
expect(find.richText('test stdio'), findsOneWidget);
});
testWidgets('Console area shows processed ansi text',
(WidgetTester tester) async {
when(debuggerController.stdio)
.thenReturn(ValueNotifier([_ansiCodesOutput()]));
await pumpDebuggerScreen(tester, debuggerController);
final finder = find.richText('Ansi color codes processed for console');
expect(finder, findsOneWidget);
finder.evaluate().forEach((element) {
final richText = element.widget as RichText;
final textSpan = richText.text as 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([_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'];
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'];
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 and Classes'), 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 list
expect(find.text(scripts.first.uri), 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,
);
// 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.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text
.toPlainText()
.contains('Root 1: _GrowableList (2 items)'));
final listChild1Finder = find.byWidgetPredicate((Widget widget) =>
widget is RichText && widget.text.toPlainText().contains('0: 3'));
final listChild2Finder = find.byWidgetPredicate((Widget widget) =>
widget is RichText && widget.text.toPlainText().contains('1: 4'));
final mapFinder = find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text
.toPlainText()
.contains('Root 2: _InternalLinkedHashmap (2 items)'));
final mapElement1Finder = find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text.toPlainText().contains("['key1']: 1.0"));
final mapElement2Finder = find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text.toPlainText().contains("['key2']: 1.1"));
expect(listFinder, findsOneWidget);
expect(mapFinder, findsOneWidget);
expect(
find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text.toPlainText().contains("Root 3: 'test str...'")),
findsOneWidget,
);
expect(
find.byWidgetPredicate((Widget widget) =>
widget is RichText &&
widget.text.toPlainText().contains('Root 4: true')),
findsOneWidget,
);
// Expand list.
expect(listChild1Finder, findsNothing);
expect(listChild2Finder, findsNothing);
await tester.tap(listFinder);
await tester.pumpAndSettle();
expect(listChild1Finder, findsOneWidget);
expect(listChild2Finder, 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.hasFrames).thenReturn(ValueNotifier(true));
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);
});
});
}
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,
),
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,
),
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,
),
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,
),
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,
),
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,
),
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,
),
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,
),
declarationTokenPos: null,
scopeEndTokenPos: null,
scopeStartTokenPos: null,
)),
];