blob: 7305cdb9a3160db1434722c3a8d4dee9f2d00eac [file] [log] [blame]
// Copyright 2019 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.
@TestOn('vm')
import 'dart:async';
import 'package:ansicolor/ansicolor.dart';
import 'package:devtools_app/src/common_widgets.dart';
import 'package:devtools_app/src/console.dart';
import 'package:devtools_app/src/globals.dart';
import 'package:devtools_app/src/logging/logging_screen.dart';
import 'package:devtools_app/src/logging/logging_controller.dart';
import 'package:devtools_app/src/service_extensions.dart';
import 'package:devtools_app/src/service_manager.dart';
import 'package:devtools_app/src/ui/service_extension_widgets.dart';
import 'package:devtools_app/src/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'support/mocks.dart';
import 'support/utils.dart';
import 'support/wrappers.dart';
void main() {
LoggingScreen screen;
MockLoggingController mockLoggingController;
group('Logging Screen', () {
FakeServiceManager fakeServiceManager;
Future<void> pumpLoggingScreen(WidgetTester tester) async {
await tester.pumpWidget(wrapWithControllers(
const LoggingScreenBody(),
logging: mockLoggingController,
));
}
setUp(() async {
await ensureInspectorDependencies();
mockLoggingController = MockLoggingController();
when(mockLoggingController.data).thenReturn([]);
when(mockLoggingController.filteredData).thenReturn([]);
when(mockLoggingController.onLogsUpdated).thenReturn(Reporter());
fakeServiceManager = FakeServiceManager(useFakeService: true);
when(fakeServiceManager.connectedApp.isFlutterWebAppNow)
.thenReturn(false);
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(false);
setGlobal(ServiceConnectionManager, fakeServiceManager);
screen = const LoggingScreen();
});
testWidgets('builds its tab', (WidgetTester tester) async {
await tester.pumpWidget(wrap(Builder(builder: screen.buildTab)));
expect(find.text('Logging'), findsOneWidget);
});
testWidgets('builds disabled message when disabled for flutter web app',
(WidgetTester tester) async {
when(fakeServiceManager.connectedApp.isFlutterWebAppNow).thenReturn(true);
when(fakeServiceManager.connectedApp.isProfileBuildNow).thenReturn(true);
await tester.pumpWidget(wrap(Builder(builder: screen.build)));
expect(find.byType(LoggingScreenBody), findsNothing);
expect(find.byType(DisabledForFlutterWebProfileBuildMessage),
findsOneWidget);
});
testWidgets('builds with no data', (WidgetTester tester) async {
await pumpLoggingScreen(tester);
expect(find.byType(LoggingScreenBody), findsOneWidget);
expect(find.byType(LogsTable), findsOneWidget);
expect(find.byType(LogDetails), findsOneWidget);
expect(find.text('Clear'), findsOneWidget);
expect(find.byType(TextField), findsOneWidget);
expect(find.byType(StructuredErrorsToggle), findsOneWidget);
});
testWidgets('can clear logs', (WidgetTester tester) async {
await pumpLoggingScreen(tester);
verifyNever(mockLoggingController.clear());
await tester.tap(find.text('Clear'));
verify(mockLoggingController.clear()).called(1);
});
testWidgets('can enter filter text', (WidgetTester tester) async {
await pumpLoggingScreen(tester);
verifyNever(mockLoggingController.clear());
await tester.enterText(find.byType(TextField), 'abc');
verify(mockLoggingController.filterText = 'abc');
});
testWidgets('can toggle structured errors', (WidgetTester tester) async {
final serviceManager = FakeServiceManager();
when(serviceManager.connectedApp.isFlutterWebAppNow).thenReturn(false);
when(serviceManager.connectedApp.isProfileBuildNow).thenReturn(false);
setGlobal(
ServiceConnectionManager,
serviceManager,
);
await pumpLoggingScreen(tester);
Switch toggle = tester.widget(find.byType(Switch));
expect(toggle.value, false);
serviceManager.serviceExtensionManager
.fakeServiceExtensionStateChanged(structuredErrors.extension, 'true');
await tester.pumpAndSettle();
toggle = tester.widget(find.byType(Switch));
expect(toggle.value, true);
// TODO(djshuckerow): Hook up fake extension state querying.
});
group('with data', () {
setUp(() {
when(mockLoggingController.data).thenReturn(fakeLogData);
when(mockLoggingController.filteredData).thenReturn(fakeLogData);
});
testWidgets('shows log items', (WidgetTester tester) async {
await pumpLoggingScreen(tester);
await tester.pumpAndSettle();
expect(find.byType(LogsTable), findsOneWidget);
expect(
find.byKey(ValueKey(fakeLogData.first)),
findsOneWidget,
);
expect(
find.byKey(ValueKey(fakeLogData.last)),
findsOneWidget,
);
});
testWidgets('can show non-computing log data',
(WidgetTester tester) async {
await pumpLoggingScreen(tester);
await tester.pumpAndSettle();
await tester.tap(find.byKey(ValueKey(fakeLogData[6])));
await tester.pumpAndSettle();
expect(
find.richText('log event 6'),
findsNWidgets(2),
reason: 'The log details should be visible both in the table and the '
'details section.',
);
});
testWidgets('can show null log data', (WidgetTester tester) async {
await pumpLoggingScreen(tester);
await tester.pumpAndSettle();
await tester.tap(find.byKey(ValueKey(fakeLogData[7])));
await tester.pumpAndSettle();
});
testWidgets('Copy to clipboard button enables/disables correctly',
(WidgetTester tester) async {
await pumpLoggingScreen(tester);
// Locates the copy to clipboard button's IconButton.
final copyButton = () => find
.byKey(LogDetails.copyToClipboardButtonKey)
.evaluate()
.first
.widget as ToolbarAction;
expect(
copyButton().onPressed,
isNull,
reason:
'Copy to clipboard button should be disabled when no logs are selected',
);
await tester.tap(find.byKey(ValueKey(fakeLogData[5])));
await tester.pumpAndSettle();
expect(
copyButton().onPressed,
isNotNull,
reason:
'Copy to clipboard button should be enabled when a log with content is selected',
);
await tester.tap(find.byKey(ValueKey(fakeLogData[7])));
await tester.pumpAndSettle();
expect(
copyButton().onPressed,
isNull,
reason:
'Copy to clipboard button should be disabled when the log details are null',
);
});
testWidgets('can compute details of non-json log data',
(WidgetTester tester) async {
const index = 8;
final log = fakeLogData[index];
await pumpLoggingScreen(tester);
await tester.pumpAndSettle();
await tester.tap(find.byKey(ValueKey(log)));
await tester.pump();
expect(
find.richText(nonJsonOutput),
findsNothing,
reason:
"The details of the log haven't computed yet, so they shouldn't "
'be available.',
);
await tester.pumpAndSettle();
expect(find.richText(nonJsonOutput), findsOneWidget);
});
testWidgets('can show details of json log data',
(WidgetTester tester) async {
const index = 9;
bool containsJson(Widget widget) {
if (widget is! Console) return false;
final content = (widget as Console).textContent.trim();
return content.startsWith('{') && content.endsWith('}');
}
final findJson = find.descendant(
of: find.byType(LogDetails),
matching: find.byWidgetPredicate(containsJson),
);
await pumpLoggingScreen(tester);
await tester.pumpAndSettle();
await tester.tap(find.byKey(ValueKey(fakeLogData[index])));
await tester.pump();
expect(
findJson,
findsNothing,
reason:
"The details of the log haven't computed yet, so they shouldn't be available.",
);
await tester.pumpAndSettle();
expect(findJson, findsOneWidget);
});
testWidgets('can process Ansi codes', (WidgetTester tester) async {
await pumpLoggingScreen(tester);
await tester.pumpAndSettle();
await tester.tap(find.byKey(ValueKey(fakeLogData[5])));
await tester.pumpAndSettle();
final finder = find.richText('Ansi color codes processed for log 5');
expect(
finder,
findsNWidgets(2),
reason: 'Processed text without ansi codes should exist in logs and '
'details sections.',
);
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,
'log 5',
reason: 'Text with ansi code should be in separate span',
);
expect(
secondSpan.style.backgroundColor,
const Color.fromRGBO(215, 95, 135, 1),
);
});
});
});
});
}
const totalLogs = 10;
final fakeLogData = List<LogData>.generate(totalLogs, _generate);
LogData _generate(int i) {
String details = 'log event $i';
String kind = 'kind $i';
String computedDetails;
switch (i) {
case 9:
computedDetails = jsonOutput;
break;
case 8:
computedDetails = nonJsonOutput;
break;
case 7:
details = null;
break;
case 5:
kind = 'stdout';
details = _ansiCodesOutput();
break;
default:
break;
}
final detailsComputer = computedDetails == null
? null
: () => Future.delayed(const Duration(seconds: 1), () => computedDetails);
return LogData(kind, details, i, detailsComputer: detailsComputer);
}
const nonJsonOutput = 'Non-json details for log number 8';
const jsonOutput = '{\n"Details": "of log event 9",\n"logEvent": "9"\n}\n';
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('log 5'));
return sb.toString();
}