[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.',
+      );
 }