[simple_browser][widget_tests] Added widget tests for the widgets in src/widgets/
- Widget tests for ErrorPage, HistoryButtons, NavigationBar,
NavigationField and TabsWidget.
- Added 'NEW TAB' to simple_browser_internationalization/Strings.dart
- Removed unused fuchsia_modular dependency from BUILD.
Change-Id: I20169660039ea78c3c53bd7d8fdf5c16e9feec5d
diff --git a/bin/simple_browser/BUILD.gn b/bin/simple_browser/BUILD.gn
index a4dfc47..4000ace 100644
--- a/bin/simple_browser/BUILD.gn
+++ b/bin/simple_browser/BUILD.gn
@@ -38,7 +38,6 @@
"//third_party/dart-pkg/pub/http",
"//third_party/dart/third_party/pkg/intl",
"//topaz/public/dart/fuchsia_logger",
- "//topaz/public/dart/fuchsia_modular",
"//topaz/public/dart/fuchsia_scenic_flutter",
"//topaz/public/dart/fuchsia_services",
"//topaz/public/dart/widgets:lib.widgets",
@@ -61,6 +60,11 @@
"tabs_bloc_test.dart",
"tld_checker_test.dart",
"webpage_bloc_test.dart",
+ "widgets/error_page_test.dart",
+ "widgets/history_buttons_test.dart",
+ "widgets/navigation_bar_test.dart",
+ "widgets/navigation_field_test.dart",
+ "widgets/tabs_widget_test.dart",
]
deps = [
diff --git a/bin/simple_browser/lib/src/widgets/tabs_widget.dart b/bin/simple_browser/lib/src/widgets/tabs_widget.dart
index 5d833e4..9f050ba 100644
--- a/bin/simple_browser/lib/src/widgets/tabs_widget.dart
+++ b/bin/simple_browser/lib/src/widgets/tabs_widget.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
+import 'package:internationalization/strings.dart';
import '../blocs/tabs_bloc.dart';
import '../blocs/webpage_bloc.dart';
import '../models/tabs_action.dart';
@@ -11,7 +12,23 @@
const _kMinTabWidth = 120.0;
const _kSeparatorWidth = 1.0;
const _kTabPadding = EdgeInsets.symmetric(horizontal: _kTabBarHeight);
-const _kScrollToMargin = _kMinTabWidth * 0.333;
+const _kScrollToMargin = _kMinTabWidth / 3;
+const _kCloseMark = '×';
+
+@visibleForTesting
+double get kTabBarHeight => _kTabBarHeight;
+
+@visibleForTesting
+double get kMinTabWidth => _kMinTabWidth;
+
+@visibleForTesting
+double get kSeparatorWidth => _kSeparatorWidth;
+
+@visibleForTesting
+double get kScrollToMargin => _kScrollToMargin;
+
+@visibleForTesting
+String get kCloseMark => _kCloseMark;
class TabsWidget extends StatefulWidget {
final TabsBloc bloc;
@@ -53,7 +70,7 @@
void _onCurrentTabChanged() {
if (_scrollController.hasClients) {
final viewportWidth = _scrollController.position.viewportDimension;
- final currentTabIndex = widget.bloc.tabs.indexOf(widget.bloc.currentTab);
+ final currentTabIndex = widget.bloc.currentTabIdx;
final currentTabPosition =
currentTabIndex * (_kMinTabWidth + _kSeparatorWidth);
@@ -166,7 +183,7 @@
child: AnimatedBuilder(
animation: widget.bloc.pageTitleNotifier,
builder: (_, __) => Text(
- widget.bloc.pageTitle ?? 'NEW TAB',
+ widget.bloc.pageTitle ?? Strings.newtab.toUpperCase(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -190,7 +207,7 @@
child: Container(
color: Colors.transparent,
alignment: Alignment.center,
- child: Text('×'),
+ child: Text(_kCloseMark),
),
),
),
diff --git a/bin/simple_browser/test/widgets/error_page_test.dart b/bin/simple_browser/test/widgets/error_page_test.dart
new file mode 100644
index 0000000..5e8062c
--- /dev/null
+++ b/bin/simple_browser/test/widgets/error_page_test.dart
@@ -0,0 +1,75 @@
+// Copyright 2019 The Fuchsia 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:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fuchsia_logger/logger.dart';
+
+// ignore_for_file: implementation_imports
+import 'package:simple_browser/src/widgets/error_page.dart';
+
+void main() {
+ setupLogger(name: 'error_page_test');
+
+ double bodyWidth;
+ double bodyHeight;
+
+ setUpAll(() {
+ bodyWidth = 800.0;
+ bodyHeight = 600.0;
+ });
+
+ testWidgets('There should be 5 text widgets: E,R,R,O,R.',
+ (WidgetTester tester) async {
+ await _setUpErrorPage(tester, bodyWidth, bodyHeight);
+
+ // Sees if there are one ‘E’, one ‘O’ and three ‘R’ texts.
+ expect(find.text('E'), findsOneWidget,
+ reason: 'Expected an E on the error page.');
+ expect(find.text('O'), findsOneWidget,
+ reason: 'Expected an O on the error page.');
+ expect(find.text('R'), findsNWidgets(3),
+ reason: 'Expected three Rs on the error page.');
+ });
+
+ testWidgets('There should be 5 Positioned widgets in the intended order.',
+ (WidgetTester tester) async {
+ await _setUpErrorPage(tester, bodyWidth, bodyHeight);
+
+ // Sees if there are 5 Positioned widgets
+ expect(find.byType(Positioned), findsNWidgets(5),
+ reason: 'Expected 5 Positioned widgets on the error page.');
+
+ // Sees if all those Positioned widgets are positioned on the intended locations.
+
+ // Verifies the left offsets of the widgets.
+ final e = find.text('E');
+ final r = find.text('R');
+ final o = find.text('O');
+
+ // Sees if each character is displayed in the correct order.
+ _expectAToBeFollowedByB(tester, e, r.at(0));
+ _expectAToBeFollowedByB(tester, r.at(0), r.at(1));
+ _expectAToBeFollowedByB(tester, r.at(1), o);
+ _expectAToBeFollowedByB(tester, o, r.at(2));
+ });
+}
+
+Future<void> _setUpErrorPage(
+ WidgetTester tester, double width, double height) async {
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: Container(
+ width: width,
+ height: height,
+ child: ErrorPage(),
+ ),
+ ),
+ ));
+}
+
+void _expectAToBeFollowedByB(WidgetTester tester, Finder a, Finder b) {
+ expect(tester.getTopLeft(a).dx < tester.getTopLeft(b).dx, true,
+ reason: 'Expected $a to be followed by $b when an error page created.');
+}
diff --git a/bin/simple_browser/test/widgets/history_buttons_test.dart b/bin/simple_browser/test/widgets/history_buttons_test.dart
new file mode 100644
index 0000000..f955e4b
--- /dev/null
+++ b/bin/simple_browser/test/widgets/history_buttons_test.dart
@@ -0,0 +1,164 @@
+// Copyright 2019 The Fuchsia 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:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:mockito/mockito.dart';
+
+// ignore_for_file: implementation_imports
+import 'package:simple_browser/src/blocs/webpage_bloc.dart';
+import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
+import 'package:simple_browser/src/services/simple_browser_web_service.dart';
+import 'package:simple_browser/src/widgets/history_buttons.dart';
+
+enum ButtonType {
+ back,
+ forward,
+ refresh,
+}
+
+void main() {
+ setupLogger(name: 'history_buttons_test');
+
+ WebPageBloc webPageBloc;
+ MockSimpleBrowserWebService mockWebService;
+ MockSimpleBrowserNavigationEventListener mockEventListener;
+
+ ValueNotifier backStateNotifier = ValueNotifier<bool>(false);
+ ValueNotifier forwardStateNotifier = ValueNotifier<bool>(false);
+ ValueNotifier urlNotifier = ValueNotifier<String>('');
+ ValueNotifier pageTypeNotifier = ValueNotifier<PageType>(PageType.empty);
+
+ setUpAll(() {
+ mockEventListener = MockSimpleBrowserNavigationEventListener();
+ when(mockEventListener.backStateNotifier)
+ .thenAnswer((_) => backStateNotifier);
+ when(mockEventListener.forwardStateNotifier)
+ .thenAnswer((_) => forwardStateNotifier);
+ when(mockEventListener.urlNotifier).thenAnswer((_) => urlNotifier);
+
+ when(mockEventListener.backState)
+ .thenAnswer((_) => backStateNotifier.value);
+ when(mockEventListener.forwardState)
+ .thenAnswer((_) => forwardStateNotifier.value);
+ when(mockEventListener.pageType).thenAnswer((_) => pageTypeNotifier.value);
+
+ mockWebService = MockSimpleBrowserWebService();
+ when(mockWebService.navigationEventListener)
+ .thenAnswer((_) => mockEventListener);
+ webPageBloc = WebPageBloc(
+ webService: mockWebService,
+ );
+ });
+
+ testWidgets('There should be 3 text widgets: BCK, FWD, and RFRSH.',
+ (WidgetTester tester) async {
+ await _setUpHistoryButtons(tester, webPageBloc);
+
+ // Sees if there are a ‘BCK’, a 'FWD' and a 'RFRSH' texts.
+ expect(find.text('BCK'), findsOneWidget);
+ expect(find.text('FWD'), findsOneWidget);
+ expect(find.text('RFRSH'), findsOneWidget);
+ });
+
+ group('Buttons are all disabled', () {
+ testWidgets('A disalbed button should not work when tapped.',
+ (WidgetTester tester) async {
+ await _setUpHistoryButtons(tester, webPageBloc);
+
+ final historyButtons = _findHistoryButtons();
+
+ // Taps the back button and sees whether it works or not.
+ await _tapHistoryButton(tester, historyButtons, ButtonType.back);
+ _verifyAllNeverWork(webPageBloc);
+
+ // Taps the forward button and sees whether it works or not.
+ await _tapHistoryButton(tester, historyButtons, ButtonType.forward);
+ _verifyAllNeverWork(webPageBloc);
+
+ // Taps the refresh button and sees whether it works or not.
+ await _tapHistoryButton(tester, historyButtons, ButtonType.refresh);
+ _verifyAllNeverWork(webPageBloc);
+ });
+ });
+
+ group('Buttons are all enabled', () {
+ String testUrl;
+
+ // Set-ups for enabling the history buttons.
+ setUp(() {
+ testUrl = 'https://www.google.com';
+ backStateNotifier.value = true;
+ forwardStateNotifier.value = true;
+ urlNotifier.value = testUrl;
+ pageTypeNotifier.value = PageType.normal;
+ });
+
+ testWidgets('An enabled button should work when tapped.',
+ (WidgetTester tester) async {
+ await _setUpHistoryButtons(tester, webPageBloc);
+
+ final historyButtons = _findHistoryButtons();
+
+ // Taps the back button and sees whether it works or not.
+ await _tapHistoryButton(tester, historyButtons, ButtonType.back);
+ verify(webPageBloc.webService.goBack());
+ verifyNever(webPageBloc.webService.goForward());
+ verifyNever(webPageBloc.webService.refresh());
+
+ // Taps the forward button and sees whether it works or not.
+ await _tapHistoryButton(tester, historyButtons, ButtonType.forward);
+ verifyNever(webPageBloc.webService.goBack());
+ verify(webPageBloc.webService.goForward());
+ verifyNever(webPageBloc.webService.refresh());
+
+ // Taps the refresh button and sees whether it works or not.
+ await _tapHistoryButton(tester, historyButtons, ButtonType.refresh);
+ verifyNever(webPageBloc.webService.goBack());
+ verifyNever(webPageBloc.webService.goForward());
+ verify(webPageBloc.webService.refresh());
+ });
+ });
+}
+
+Future<void> _setUpHistoryButtons(
+ WidgetTester tester,
+ WebPageBloc bloc,
+) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: HistoryButtons(
+ bloc: bloc,
+ ),
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(_findHistoryButtons(), findsNWidgets(3),
+ reason: 'Expected 3 history buttons on the HistoryButtons widget.');
+}
+
+Finder _findHistoryButtons() => find.byType(GestureDetector);
+
+Future<void> _tapHistoryButton(
+ WidgetTester tester, Finder buttons, ButtonType target) async {
+ int index = target.index;
+
+ await tester.tap(buttons.at(index));
+ await tester.pumpAndSettle();
+}
+
+void _verifyAllNeverWork(WebPageBloc bloc) {
+ verifyNever(bloc.webService.goBack());
+ verifyNever(bloc.webService.goForward());
+ verifyNever(bloc.webService.refresh());
+}
+
+class MockSimpleBrowserNavigationEventListener extends Mock
+ implements SimpleBrowserNavigationEventListener {}
+
+class MockSimpleBrowserWebService extends Mock
+ implements SimpleBrowserWebService {}
diff --git a/bin/simple_browser/test/widgets/navigation_bar_test.dart b/bin/simple_browser/test/widgets/navigation_bar_test.dart
new file mode 100644
index 0000000..f49b59f
--- /dev/null
+++ b/bin/simple_browser/test/widgets/navigation_bar_test.dart
@@ -0,0 +1,168 @@
+// Copyright 2019 The Fuchsia 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 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:mockito/mockito.dart';
+
+// ignore_for_file: implementation_imports
+import 'package:simple_browser/src/blocs/tabs_bloc.dart';
+import 'package:simple_browser/src/blocs/webpage_bloc.dart';
+import 'package:simple_browser/src/models/tabs_action.dart';
+import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
+import 'package:simple_browser/src/services/simple_browser_web_service.dart';
+import 'package:simple_browser/src/widgets/history_buttons.dart';
+import 'package:simple_browser/src/widgets/navigation_bar.dart';
+import 'package:simple_browser/src/widgets/navigation_field.dart';
+
+void main() {
+ setupLogger(name: 'navigation_bar_test');
+
+ WebPageBloc webPageBloc;
+ MockSimpleBrowserWebService mockWebService;
+ MockSimpleBrowserNavigationEventListener mockEventListener;
+
+ ValueNotifier backStateNotifier = ValueNotifier<bool>(false);
+ ValueNotifier forwardStateNotifier = ValueNotifier<bool>(false);
+ ValueNotifier urlNotifier = ValueNotifier<String>('');
+ ValueNotifier pageTypeNotifier = ValueNotifier<PageType>(PageType.empty);
+ ValueNotifier isLoadedStateNotifier = ValueNotifier<bool>(true);
+
+ group('The bloc is null.', () {
+ testWidgets('Should create two empty containers and one + button.',
+ (WidgetTester tester) async {
+ await _setUpNavigationBar(tester, webPageBloc, () {});
+
+ const reasonSuffix =
+ 'when the NavigationBar widget has a null webPageBloc.';
+
+ expect(find.byType(Container), findsNWidgets(3),
+ reason: 'Expected three containers $reasonSuffix');
+
+ // Sees if there are two empty Containers.
+ expect(
+ find.byWidgetPredicate(
+ (Widget widget) => widget is Container && widget.child == null,
+ description: 'Empty containers.',
+ ),
+ findsNWidgets(2),
+ reason: 'Expected two of those containers were empty $reasonSuffix');
+
+ // Sees if there are one + button.
+ expect(_findNewTabButton(), findsOneWidget,
+ reason:
+ 'Expected one of those containers was a + button $reasonSuffix');
+ });
+ });
+
+ group('The bloc is not null.', () {
+ setUp(() {
+ mockEventListener = MockSimpleBrowserNavigationEventListener();
+ when(mockEventListener.backStateNotifier)
+ .thenAnswer((_) => backStateNotifier);
+ when(mockEventListener.forwardStateNotifier)
+ .thenAnswer((_) => forwardStateNotifier);
+ when(mockEventListener.urlNotifier).thenAnswer((_) => urlNotifier);
+ when(mockEventListener.isLoadedStateNotifier)
+ .thenAnswer((_) => isLoadedStateNotifier);
+
+ when(mockEventListener.backState)
+ .thenAnswer((_) => backStateNotifier.value);
+ when(mockEventListener.forwardState)
+ .thenAnswer((_) => forwardStateNotifier.value);
+ when(mockEventListener.pageType)
+ .thenAnswer((_) => pageTypeNotifier.value);
+ when(mockEventListener.isLoadedState)
+ .thenAnswer((_) => isLoadedStateNotifier.value);
+
+ mockWebService = MockSimpleBrowserWebService();
+ when(mockWebService.navigationEventListener)
+ .thenAnswer((_) => mockEventListener);
+ webPageBloc = WebPageBloc(
+ webService: mockWebService,
+ );
+ });
+
+ testWidgets(
+ 'Should create one HistoryButtons, one URL field and one + button.',
+ (WidgetTester tester) async {
+ await _setUpNavigationBar(tester, webPageBloc, () {});
+
+ const reasonSuffix =
+ 'when the NavigationBar widget has a non-null webPageBloc.';
+
+ expect(find.byType(HistoryButtons), findsOneWidget,
+ reason: 'Expected a HistoryButtons widget $reasonSuffix');
+ expect(find.byType(NavigationField), findsOneWidget,
+ reason: 'Expected a NavigationField widget $reasonSuffix');
+ expect(_findNewTabButton(), findsOneWidget,
+ reason: 'Expected a + button $reasonSuffix');
+ });
+
+ testWidgets('''Should show a progress bar when the page has not been loaded,
+ and should not show it anymore when the page loading is complete.''',
+ (WidgetTester tester) async {
+ await _setUpNavigationBar(tester, webPageBloc, () {});
+ isLoadedStateNotifier.value = false;
+ await tester.pump();
+ expect(find.byType(LinearProgressIndicator), findsOneWidget,
+ reason: 'Expected a progress bar when loading has not finished.');
+
+ isLoadedStateNotifier.value = true;
+ await tester.pumpAndSettle();
+ expect(find.byType(LinearProgressIndicator), findsNothing,
+ reason: 'Expected no progress bars when loading has completed.');
+ });
+
+ testWidgets('Should call the newTab callback when the + button is tapped.',
+ (WidgetTester tester) async {
+ TabsBloc tb = TabsBloc(
+ tabFactory: () => MockWebPageBloc(),
+ disposeTab: (tab) => tab.dispose(),
+ );
+
+ await _setUpNavigationBar(
+ tester, webPageBloc, () => tb.request.add(NewTabAction()));
+
+ expect(tb.tabs.length, 0,
+ reason:
+ 'Expected no tabs in the tabsBloc when none has been added to it.');
+
+ final newTabBtn = _findNewTabButton();
+ expect(newTabBtn, findsOneWidget,
+ reason: 'Expected one + button on the NavigationBar.');
+ await tester.tap(newTabBtn);
+ await tester.pumpAndSettle();
+ expect(tb.tabs.length, 1,
+ reason:
+ 'Expected a tab in the tabsBloc when the + button was tapped.');
+ });
+ });
+}
+
+Future<void> _setUpNavigationBar(
+ WidgetTester tester, WebPageBloc bloc, Function callback) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: NavigationBar(
+ bloc: bloc,
+ newTab: callback,
+ ),
+ ),
+ ),
+ );
+}
+
+Finder _findNewTabButton() => find.text('+');
+
+class MockSimpleBrowserNavigationEventListener extends Mock
+ implements SimpleBrowserNavigationEventListener {}
+
+class MockSimpleBrowserWebService extends Mock
+ implements SimpleBrowserWebService {}
+
+class MockWebPageBloc extends Mock implements WebPageBloc {}
diff --git a/bin/simple_browser/test/widgets/navigation_field_test.dart b/bin/simple_browser/test/widgets/navigation_field_test.dart
new file mode 100644
index 0000000..3c338a2
--- /dev/null
+++ b/bin/simple_browser/test/widgets/navigation_field_test.dart
@@ -0,0 +1,116 @@
+// Copyright 2019 The Fuchsia 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:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:mockito/mockito.dart';
+
+// ignore_for_file: implementation_imports
+import 'package:simple_browser/src/blocs/webpage_bloc.dart';
+import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
+import 'package:simple_browser/src/services/simple_browser_web_service.dart';
+import 'package:simple_browser/src/widgets/navigation_field.dart';
+
+void main() {
+ setupLogger(name: 'navigation_field_test');
+
+ SimpleBrowserWebService mockWebService;
+ SimpleBrowserNavigationEventListener mockEventListener;
+ WebPageBloc webPageBloc;
+
+ setUpAll(() {
+ mockWebService = MockSimpleBrowserWebService();
+ mockEventListener = MockSimpleBrowserNavigationEventListener();
+ webPageBloc = WebPageBloc(webService: mockWebService);
+ });
+
+ group('Default textfield (without URL)', () {
+ ValueNotifier urlNotifier = ValueNotifier<String>('');
+
+ setUp(() {
+ when(mockEventListener.urlNotifier).thenAnswer((_) => urlNotifier);
+ when(mockEventListener.url).thenAnswer((_) => urlNotifier.value);
+ when(mockWebService.navigationEventListener)
+ .thenAnswer((_) => mockEventListener);
+ });
+
+ const whenSuffix = 'when created it empty.';
+ testWidgets('Should focus on the textfield $whenSuffix',
+ (WidgetTester tester) async {
+ await _setUpNavigationField(tester, webPageBloc);
+
+ final textField = _findTextField();
+ expect(tester.widget<TextField>(textField).autofocus, true,
+ reason:
+ 'Expected the TextField to be focused by default $whenSuffix');
+ });
+
+ testWidgets('Should call the callback when a valid url is entered.',
+ (WidgetTester tester) async {
+ await _setUpNavigationField(tester, webPageBloc);
+
+ String testUrl = 'https://www.google.com';
+ final textField = _findTextField();
+
+ // Enters the testUrl to the text field and submit it.
+ await tester.enterText(textField, testUrl);
+ await tester.testTextInput.receiveAction(TextInputAction.go);
+ await tester.pump();
+
+ // Sees if the corresponding callback is called.
+ verify(webPageBloc.webService.loadUrl(testUrl)).called(1);
+ });
+ });
+
+ group('Textfield with a URL', () {
+ ValueNotifier urlNotifier = ValueNotifier<String>('');
+
+ setUp(() {
+ when(mockEventListener.urlNotifier).thenAnswer((_) => urlNotifier);
+ when(mockEventListener.url).thenAnswer((_) => urlNotifier.value);
+ when(mockWebService.navigationEventListener)
+ .thenAnswer((_) => mockEventListener);
+ });
+
+ const whenSuffix = 'when created it with a url.';
+
+ testWidgets('Should not focus on the TextField $whenSuffix',
+ (WidgetTester tester) async {
+ urlNotifier.value = 'https://www.google.com';
+
+ await _setUpNavigationField(tester, webPageBloc);
+
+ final textField = _findTextField();
+ expect(tester.widget<TextField>(textField).autofocus, false,
+ reason: 'Expected the textfield not focused by default $whenSuffix');
+ });
+ });
+}
+
+Future<void> _setUpNavigationField(
+ WidgetTester tester, WebPageBloc bloc) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: NavigationField(
+ bloc: bloc,
+ ),
+ ),
+ ),
+ );
+}
+
+Finder _findTextField() {
+ final textField = find.byType(TextField);
+ expect(textField, findsOneWidget,
+ reason: 'Expected a TextField on the NavigationField widget.');
+
+ return textField;
+}
+
+class MockSimpleBrowserNavigationEventListener extends Mock
+ implements SimpleBrowserNavigationEventListener {}
+
+class MockSimpleBrowserWebService extends Mock
+ implements SimpleBrowserWebService {}
diff --git a/bin/simple_browser/test/widgets/tabs_widget_test.dart b/bin/simple_browser/test/widgets/tabs_widget_test.dart
new file mode 100644
index 0000000..c48e2a6
--- /dev/null
+++ b/bin/simple_browser/test/widgets/tabs_widget_test.dart
@@ -0,0 +1,345 @@
+// Copyright 2019 The Fuchsia 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 'dart:async';
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:fuchsia_logger/logger.dart';
+import 'package:mockito/mockito.dart';
+
+// ignore_for_file: implementation_imports
+import 'package:simple_browser/src/blocs/tabs_bloc.dart';
+import 'package:simple_browser/src/blocs/webpage_bloc.dart';
+import 'package:simple_browser/src/models/tabs_action.dart';
+import 'package:simple_browser/src/services/simple_browser_navigation_event_listener.dart';
+import 'package:simple_browser/src/services/simple_browser_web_service.dart';
+import 'package:simple_browser/src/widgets/tabs_widget.dart';
+
+const _emptyTitle = 'NEW TAB';
+
+void main() {
+ setupLogger(name: 'tabs_widget_test');
+
+ TabsBloc tabsBloc;
+ SimpleBrowserWebService mockWebService;
+ SimpleBrowserNavigationEventListener mockListener;
+ ValueNotifier titleNotifier = ValueNotifier<String>('');
+
+ setUp(() {
+ mockWebService = MockSimpleBrowserWebService();
+ mockListener = MockSimpleBrowserNavigationEventListener();
+
+ when(mockWebService.navigationEventListener)
+ .thenAnswer((_) => mockListener);
+ when(mockListener.pageTitleNotifier).thenAnswer((_) => titleNotifier);
+
+ tabsBloc = TabsBloc(
+ tabFactory: () => WebPageBloc(
+ webService: mockWebService,
+ ),
+ disposeTab: (tab) => tab.dispose(),
+ );
+ });
+
+ testWidgets('Should create tab widgets when more than two tabs added.',
+ (WidgetTester tester) async {
+ // Initial state: Tabsbloc does not have any tabs.
+ expect(tabsBloc.tabs.length, 0);
+ await _setUpTabsWidget(tester, tabsBloc);
+
+ // Sees if there is no tab widgets on the screen.
+ expect(_findNewTabWidgets(), findsNothing,
+ reason: 'Expected no tab widgets when no tabs added.');
+
+ // Adds one tab and sees if there is a tab in the tabsBloc, but still no tab widgets
+ // on the screen.
+ await _addNTabsToTabsBloc(tester, tabsBloc, 1);
+ expect(_findNewTabWidgets(), findsNothing,
+ reason: 'Expected no tab widgets when only 1 tab added.');
+
+ // Adds one more tab and sees if there are two tabs in the tabsBloc.
+ await _addNTabsToTabsBloc(tester, tabsBloc, 1);
+
+ // Sees if there are two Expanded widgets for the tab widgets.
+ // A tabsBloc should be wrapped by an Expanded widget when the total widths of
+ // the currently displayed tabs is smaller than the browser width.
+ expect(find.byType(Expanded), findsNWidgets(2),
+ reason: 'Expected 2 Expanded widgets when 2 tabs added.');
+
+ // See if the tab widgets have the default title.
+ expect(_findNewTabWidgets(), findsNWidgets(2),
+ reason: '''Expected 2 tab widgets with the title, $_emptyTitle,
+ when 2 tabs added.''');
+ });
+
+ testWidgets(
+ 'Should create tab widgets with a custom title when one is given.',
+ (WidgetTester tester) async {
+ // Gives a custom title, 'TAB_WIDGET_TEST'.
+ String expectedTitle = 'TAB_WIDGET_TEST';
+ when(mockListener.pageTitle).thenReturn(expectedTitle);
+
+ await _setUpTabsWidget(tester, tabsBloc);
+
+ // Adds two tabs since tab widgets are only created when there are more than one tab.
+ await _addNTabsToTabsBloc(tester, tabsBloc, 2);
+
+ // Sees if there are two tab widgets with the given title.
+ expect(find.text(expectedTitle), findsNWidgets(2),
+ reason: '''Expected 2 tab widgets with the title $expectedTitle
+ when 2 new tabs with it have been added.''');
+ });
+
+ testWidgets('''Should give the minimum width to the tab widgets
+ when the total sum of the tab widths is larger than the browser width.''',
+ (WidgetTester tester) async {
+ await _setUpTabsWidget(tester, tabsBloc);
+
+ // Adds seven tabs.
+ await _addNTabsToTabsBloc(tester, tabsBloc, 7);
+
+ // Sees if there are seven SizedBox widgets with the minimum width for the tab widgets.
+ // A tabsBloc should be wrapped by a SizedBox widget and have the minimum width
+ // when the total widths of the currently displayed tabs is larger than the browser width.
+ expect(_findMinTabWidgets(), findsNWidgets(7));
+ });
+
+ testWidgets('Should change the focus to it when an unfocused tab is tapped.',
+ (WidgetTester tester) async {
+ await _setUpTabsWidget(tester, tabsBloc);
+
+ await _addNTabsToTabsBloc(tester, tabsBloc, 3);
+
+ expect(tabsBloc.currentTabIdx, 2,
+ reason: 'Expected the 3rd tab widget was focused by default.');
+
+ final tabs = _findNewTabWidgets();
+ await tester.tap(tabs.at(0));
+ await tester.pumpAndSettle();
+ expect(tabsBloc.currentTabIdx, 0,
+ reason: 'Expected the 1st tab widget is focused when tapped on it.');
+ });
+
+ testWidgets(
+ 'Should show close buttons on the focused tab and a hovered tab if any.',
+ (WidgetTester tester) async {
+ await _setUpTabsWidget(tester, tabsBloc);
+ await _addNTabsToTabsBloc(tester, tabsBloc, 3);
+ final tabs = _findNewTabWidgets();
+ expect(tabs, findsNWidgets(3),
+ reason: 'Expected to find 3 tab widgets when 3 tabs added.');
+
+ // Sees if there is only one tab that has a close button on it.
+ expect(_findClose(), findsOneWidget,
+ reason: 'Expected to find 1 close button when hovering no tabs.');
+
+ // Configures a gesture.
+ final TestGesture gesture =
+ await tester.createGesture(kind: PointerDeviceKind.mouse);
+ addTearDown(gesture.removePointer);
+
+ // Mouse Enters onto the first tab.
+ final Offset firstTabCenter = tester.getCenter(tabs.at(0));
+ await gesture.moveTo(firstTabCenter);
+ await tester.pumpAndSettle();
+
+ // Sees if there are two tabs that have a close button on it.
+ expect(_findClose(), findsNWidgets(2),
+ reason: 'Expected to find 2 close buttons when hovering a tab.');
+
+ // Mouse is out from the first tab and moves to the currently focused tab.
+ final Offset focusedTabCenter =
+ tester.getCenter(tabs.at(tabsBloc.currentTabIdx));
+ await gesture.moveTo(focusedTabCenter);
+ await tester.pumpAndSettle();
+
+ // Sees if there is only one tab that has a close button on it.
+ expect(_findClose(), findsOneWidget,
+ reason:
+ 'Expected to find 1 close button when hovering the focused tab.');
+ });
+
+ testWidgets('Should close the tab when its close button is tapped.',
+ (WidgetTester tester) async {
+ await _setUpTabsWidget(tester, tabsBloc);
+ await _addNTabsToTabsBloc(tester, tabsBloc, 4);
+ expect(_findNewTabWidgets(), findsNWidgets(4),
+ reason: 'Expected to find 4 tab widgets when 4 tabs added.');
+ expect(tabsBloc.currentTabIdx, 3,
+ reason: 'Expected the 4th tab widget was focused by default.');
+
+ Finder closeButtons = _findClose();
+ expect(closeButtons, findsOneWidget,
+ reason: 'Expected to find 1 close button when hovering no tabs.');
+
+ await tester.tap(closeButtons);
+ await tester.pumpAndSettle();
+ expect(tabsBloc.tabs.length, 3,
+ reason: 'Expected 3 tabs in tabsBloc after tapped the close.');
+ expect(_findNewTabWidgets(), findsNWidgets(3),
+ reason: 'Expected to find 3 tab widgets after tapped the close.');
+ expect(tabsBloc.currentTabIdx, 2,
+ reason:
+ 'Expected the 3rd tab widget was focused after tapped the close.');
+ });
+
+ testWidgets(
+ 'The tab widget list should scroll if needed depending on the offset of the selected tab.',
+ (WidgetTester tester) async {
+ final rightScrollMargin = 800.0 - kScrollToMargin - kMinTabWidth;
+ final rightMinMargin = 800.0 - kMinTabWidth;
+ final leftScrollMargin = kScrollToMargin;
+ const leftMinMargin = 0.0;
+
+ await _setUpTabsWidget(tester, tabsBloc);
+
+ // Creates 8 tabs.
+ await _addNTabsToTabsBloc(tester, tabsBloc, 8);
+
+ // The screen(container) width: 800.0
+ // The total width of the tab widgets: 120.0 * 8 = 960.
+ // The expected display of the tab widgets (* is the currently focused tab):
+ // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7* -|
+ // |- SCREEN -|
+
+ // See if the last tab is currenly focused.
+ expect(tabsBloc.currentTabIdx, 7,
+ reason: 'Expected the 8th tab widget is focused by default.');
+
+ final tabsOnScreen = _findMinTabWidgets();
+
+ // Sees if there are 7 tab widgets on the screen.
+ expect(tabsOnScreen, findsNWidgets(7),
+ reason: 'Expected 7 out of 8 tab widgets on the 800-wide viewport.');
+
+ final lastTab = tabsOnScreen.last;
+ final fixedY = tester.getTopLeft(lastTab).dy;
+ final expectedLastPosition = Offset(rightMinMargin, fixedY);
+
+ expect(tester.getTopLeft(lastTab), expectedLastPosition,
+ reason: '''Expected the initial X position of the last tab widget
+ to be $expectedLastPosition.''');
+
+ // The fifth tab in the tabsBloc is the forth tab widget on the current screen.
+ final fifthTab = tabsOnScreen.at(3);
+ final expectedFifthPosition = tester.getTopLeft(fifthTab);
+
+ // Taps on the fifth tab to change the focus.
+ await tester.tap(fifthTab);
+ await tester.pumpAndSettle();
+
+ // The expected display of the tab widgets (* is the currently focused tab):
+ // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4* -| |- 5 -| |- 6 -| |- 7 -|
+ // |- SCREEN -|
+
+ expect(tabsBloc.currentTabIdx, 4,
+ reason: 'Expected the 5th tab to be focused when tapped on it.');
+ expect(tester.getTopLeft(fifthTab), expectedFifthPosition,
+ reason: '''Expected the tab widget list stay still when tapped on
+ the 5th tab, which is the 4th tab widget on the screen.''');
+
+ // The second tab in the tabsBloc is the first tab widget on the current screen.
+ final secondTab = tabsOnScreen.at(0);
+
+ final expectedSecondPosition = Offset(leftScrollMargin, fixedY);
+ await tester.tap(secondTab);
+ await tester.pumpAndSettle();
+
+ // The expected display of the tab widgets (* is the currently focused tab):
+ // |- 0 -| |- 1* -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
+ // |- SCREEN -|
+
+ expect(tabsOnScreen, findsNWidgets(8),
+ reason: '''Expected 8 tab widgets on the screen when tapped on the
+ 2nd tab, which was the 1st tab widget on the screen.''');
+ expect(tabsBloc.currentTabIdx, 1,
+ reason: '''Expected the 2nd tab to be focused when tapped on it.''');
+ expect(tester.getTopLeft(tabsOnScreen.at(1)), expectedSecondPosition,
+ reason: '''Expected the tab widget list to shift from left to right
+ and the 2nd tab widget to be fully revealed on the screen when
+ tapped on it.''');
+
+ final expectedFirstPosition = Offset(leftMinMargin, fixedY);
+ // Directly adds the FocusTabAction to the tabsBloc since tester.tap() does not
+ // work on the widget whose center is offscreen.
+ tabsBloc.request.add(FocusTabAction(tab: tabsBloc.tabs[0]));
+ await tester.pumpAndSettle();
+
+ // The expected display of the tab widgets (* is the currently focused tab):
+ // |- 0* -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
+ // |- SCREEN -|
+
+ expect(tabsOnScreen, findsNWidgets(7),
+ reason:
+ 'Expected 7 tab widgets on the screen when 1st tab is focused.');
+
+ expect(tabsBloc.currentTabIdx, 0,
+ reason: 'Expected the 1st tab to be focused when moved focus on it.');
+ expect(tester.getTopLeft(tabsOnScreen.at(0)), expectedFirstPosition,
+ reason: '''Expected the tab widget list to shift from left to right
+ and the 1st tab widget to be fully revealed on the screen when
+ tapped on it.''');
+
+ final seventhTab = tabsOnScreen.at(6);
+ final expectedSeventhPosition = Offset(rightScrollMargin, fixedY);
+ await tester.tap(seventhTab);
+ await tester.pumpAndSettle();
+
+ // The expected display of the tab widgets (* is the currently focused tab):
+ // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6* -| |- 7 -|
+ // |- SCREEN -|
+
+ expect(tabsOnScreen, findsNWidgets(8),
+ reason: '''Expected 8 tab widgets on the screen when tapped on the
+ 7th tab widget.''');
+
+ expect(tabsBloc.currentTabIdx, 6,
+ reason: '''Expected the 7th tab to be focused when tapped on it.''');
+ expect(tester.getTopLeft(seventhTab), expectedSeventhPosition,
+ reason: '''Expected the tab widget list to shift from right to left
+ and the 7th tab widget to be fully revealed on the screen when
+ tapped on it.''');
+ });
+}
+
+Future<void> _setUpTabsWidget(WidgetTester tester, TabsBloc tabsBloc) async {
+ await tester.pumpWidget(MaterialApp(
+ home: Scaffold(
+ body: Container(
+ width: 800,
+ child: TabsWidget(
+ bloc: tabsBloc,
+ ),
+ ),
+ ),
+ ));
+
+ await tester.pumpAndSettle();
+}
+
+Future<void> _addNTabsToTabsBloc(
+ WidgetTester tester, TabsBloc tabsBloc, int n) async {
+ int currentNumTabs = tabsBloc.tabs.length;
+ for (int i = 0; i < n; i++) {
+ tabsBloc.request.add(NewTabAction());
+ await tester.pumpAndSettle();
+ }
+ expect(tabsBloc.tabs.length, currentNumTabs + n);
+}
+
+Finder _findMinTabWidgets() => find.byWidgetPredicate(
+ (Widget widget) => widget is SizedBox && (widget.width) == kMinTabWidth);
+
+Finder _findNewTabWidgets() => find.text(_emptyTitle);
+
+Finder _findClose() => find.text(kCloseMark);
+
+class MockSimpleBrowserNavigationEventListener extends Mock
+ implements SimpleBrowserNavigationEventListener {}
+
+class MockSimpleBrowserWebService extends Mock
+ implements SimpleBrowserWebService {}
+
+class MockWebPageBloc extends Mock implements WebPageBloc {}
diff --git a/bin/simple_browser_internationalization/lib/strings.dart b/bin/simple_browser_internationalization/lib/strings.dart
index 7c4b6ba..6e206ad 100644
--- a/bin/simple_browser_internationalization/lib/strings.dart
+++ b/bin/simple_browser_internationalization/lib/strings.dart
@@ -45,4 +45,9 @@
name: 'browser',
desc: 'As in: web browser',
);
+ static String get newtab => Intl.message(
+ 'New Tab',
+ name: 'newtab',
+ desc: 'A default title for a newly created empty tab.',
+ );
}