[simple_browser] Tab Rearrangement (3/4)
- Changed the UI and the behavior of the scrollable tab list with scroll buttons and enabled the tab rearrangement feature on it (fxb/45238)
- Modified existing widget tests for the scrollable tab list to fit the changed widget structure.
- Added new widget tests for the scroll buttons.
- Changed the color theme to meet the design spec.
Change-Id: I3686459fa7b071356ca33f6b794611b061522150
diff --git a/bin/simple_browser/lib/app.dart b/bin/simple_browser/lib/app.dart
index 9ac693b..36d20d1 100644
--- a/bin/simple_browser/lib/app.dart
+++ b/bin/simple_browser/lib/app.dart
@@ -19,10 +19,13 @@
import 'src/widgets/navigation_bar.dart';
import 'src/widgets/tabs_widget.dart';
-const _kBackgroundColor = Color(0xFFE5E5E5);
-const _kForegroundColor = Color(0xFF191919);
-const _kSelectionColor = Color(0x26191919);
-const _kTextStyle = TextStyle(color: _kForegroundColor, fontSize: 14.0);
+// TODO(fxb/45264): Replace these with colors from the central Ermine styles.
+const _kErmineColor100 = Color(0xFFE5E5E5);
+const _kErmineColor200 = Color(0xFFBDBDBD);
+const _kErmineColor300 = Color(0xFF282828);
+const _kErmineColor400 = Color(0xFF0C0C0C);
+
+const _kTextStyle = TextStyle(color: _kErmineColor400, fontSize: 14.0);
class App extends StatelessWidget {
final AppModel model;
@@ -44,13 +47,14 @@
title: Strings.browser,
theme: ThemeData(
fontFamily: 'RobotoMono',
- textSelectionColor: _kSelectionColor,
- textSelectionHandleColor: _kForegroundColor,
- hintColor: _kForegroundColor,
- cursorColor: _kForegroundColor,
- primaryColor: _kBackgroundColor,
- canvasColor: _kBackgroundColor,
- accentColor: _kForegroundColor,
+ textSelectionColor: _kErmineColor200,
+ textSelectionHandleColor: _kErmineColor400,
+ hintColor: _kErmineColor400,
+ cursorColor: _kErmineColor400,
+ primaryColor: _kErmineColor100,
+ canvasColor: _kErmineColor100,
+ accentColor: _kErmineColor400,
+ buttonColor: _kErmineColor300,
textTheme: TextTheme(
bodyText2: _kTextStyle,
subtitle1: _kTextStyle,
diff --git a/bin/simple_browser/lib/src/widgets/tabs_widget.dart b/bin/simple_browser/lib/src/widgets/tabs_widget.dart
index 7f470c2..22ac395 100644
--- a/bin/simple_browser/lib/src/widgets/tabs_widget.dart
+++ b/bin/simple_browser/lib/src/widgets/tabs_widget.dart
@@ -1,18 +1,23 @@
// 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:internationalization/strings.dart';
import '../blocs/tabs_bloc.dart';
import '../blocs/webpage_bloc.dart';
import '../models/tabs_action.dart';
+// TODO(fxb/45264): Make the common factors as part of Ermine central styles.
const _kTabBarHeight = 24.0;
const _kMinTabWidth = 120.0;
-const _kSeparatorWidth = 1.0;
+const _kBorderWidth = 1.0;
const _kTabPadding = EdgeInsets.symmetric(horizontal: _kTabBarHeight);
const _kScrollToMargin = _kMinTabWidth / 3;
-const _kCloseMark = '×';
+const _kScrollAnimationDuration = 300;
+const _kIconSize = 14.0;
+
+enum _ScrollButtonType { left, right }
@visibleForTesting
double get kTabBarHeight => _kTabBarHeight;
@@ -21,14 +26,11 @@
double get kMinTabWidth => _kMinTabWidth;
@visibleForTesting
-double get kSeparatorWidth => _kSeparatorWidth;
+double get kBorderWidth => _kBorderWidth;
@visibleForTesting
double get kScrollToMargin => _kScrollToMargin;
-@visibleForTesting
-String get kCloseMark => _kCloseMark;
-
/// The list of currently opened tabs in the browser.
///
/// Builds different widget trees for the tab list depending on the selected tab
@@ -59,7 +61,10 @@
AnimationController _ghostController;
AnimationController _leftNewGhostController;
AnimationController _rightNewGhostController;
+
final _scrollController = ScrollController();
+ final _leftScrollButton = _ScrollButton(_ScrollButtonType.left);
+ final _rightScrollButton = _ScrollButton(_ScrollButtonType.right);
ThemeData _browserTheme;
@@ -133,24 +138,18 @@
border: Border(
top: BorderSide(
color: _browserTheme.accentColor,
- width: 1.0,
+ width: _kBorderWidth,
),
bottom: BorderSide(
color: _browserTheme.accentColor,
- width: 1.0,
+ width: _kBorderWidth,
),
),
),
child: LayoutBuilder(
builder: (context, constraints) => _tabWidth > _kMinTabWidth
? _buildTabStacks()
- // TODO (fxb/45238): Change the ListView to a Custom scrollable list.
- // TODO (fxb/45239): Implement tab rearrangement for the scrollable tab list.
- : ListView(
- controller: _scrollController,
- scrollDirection: Axis.horizontal,
- children: _buildPageTabs(width: _kMinTabWidth),
- ),
+ : _buildScrollableTabListWithButtons(),
),
);
}
@@ -168,6 +167,50 @@
// BUILDERS
+ Widget _buildScrollableTabListWithButtons() => Row(
+ children: <Widget>[
+ _buildScrollButton(_leftScrollButton),
+ Expanded(child: _buildScrollableTabList()),
+ _buildScrollButton(_rightScrollButton),
+ ],
+ );
+
+ Widget _buildScrollableTabList() => NotificationListener<ScrollNotification>(
+ onNotification: (scrollNotification) {
+ if (scrollNotification is ScrollEndNotification) {
+ _onScrollEnd(scrollNotification.metrics);
+ }
+ return true;
+ },
+ child: SingleChildScrollView(
+ controller: _scrollController,
+ scrollDirection: Axis.horizontal,
+ physics: NeverScrollableScrollPhysics(),
+ child: _buildTabStacks(),
+ ),
+ );
+
+ Widget _buildScrollButton(_ScrollButton button) => GestureDetector(
+ onTap: () => _onScrollButtonTap(button),
+ child: Container(
+ width: _kTabBarHeight,
+ height: _kTabBarHeight,
+ color: _browserTheme.buttonColor,
+ child: Center(
+ child: AnimatedBuilder(
+ animation: button.isEnabled,
+ builder: (_, __) => Icon(
+ button.icon,
+ color: button.isEnabled.value
+ ? _browserTheme.primaryColor
+ : _browserTheme.primaryColor.withOpacity(0.2),
+ size: _kIconSize,
+ ),
+ ),
+ ),
+ ),
+ );
+
Widget _buildTabStacks() => Stack(
children: <Widget>[
Row(
@@ -261,6 +304,7 @@
renderingIndex ??= index;
return Container(
+ key: Key('tab'),
width: _tabWidth,
height: _kTabBarHeight,
decoration: BoxDecoration(
@@ -289,30 +333,10 @@
Border _buildBorder(bool hasBorder) => Border(
left: BorderSide(
color: hasBorder ? _browserTheme.accentColor : Colors.transparent,
- width: 1.0,
+ width: _kBorderWidth,
),
);
- // TODO(fxb/45239): This is an old code and will be removed.
- List<Widget> _buildPageTabs({@required double width}) => widget.bloc.tabs
- .map(_buildTab)
- // add a 1pip separator before every tab,
- // divide the rest of the space between tabs
- .expand((item) => [
- SizedBox(
- width: _kSeparatorWidth,
- child: Container(
- color: _browserTheme.accentColor,
- ),
- ),
- width == null
- ? Expanded(child: item, flex: 1)
- : SizedBox(child: item, width: width),
- ])
- // skip the first separator
- .skip(1)
- .toList();
-
Widget _buildTab(WebPageBloc tab) => _TabWidget(
bloc: tab,
selected: tab == widget.bloc.currentTab,
@@ -332,8 +356,7 @@
if (_scrollController.hasClients) {
final viewportWidth = _scrollController.position.viewportDimension;
final currentTabIndex = widget.bloc.currentTabIdx;
- final currentTabPosition =
- currentTabIndex * (_kMinTabWidth + _kSeparatorWidth);
+ final currentTabPosition = currentTabIndex * _kMinTabWidth;
final offsetForLeftEdge = currentTabPosition - _kScrollToMargin;
final offsetForRightEdge =
@@ -350,7 +373,7 @@
if (newOffset != null) {
_scrollController.animateTo(
newOffset,
- duration: Duration(milliseconds: 300),
+ duration: Duration(milliseconds: _kScrollAnimationDuration),
curve: Curves.ease,
);
}
@@ -374,8 +397,11 @@
void _onDragUpdate(DragUpdateDetails details) {
double dragOffsetX = details.globalPosition.dx - _dragStartX;
+ double dragXMax = (_scrollController.hasClients)
+ ? (_tabWidth * widget.bloc.tabs.length) - _tabWidth
+ : (_tabListWidth - _tabWidth);
_currentTabX.value = ((_tabWidth * widget.bloc.currentTabIdx) + dragOffsetX)
- .clamp(0.0, _tabListWidth - _tabWidth);
+ .clamp(0.0, dragXMax);
if (!_isAnimating) {
if (_isOverlappingLeftTabHalf()) {
@@ -437,6 +463,35 @@
_currentTabX.value = _tabWidth * _ghostIndex;
}
+ void _onScrollButtonTap(_ScrollButton button) {
+ if (!button.isEnabled.value) {
+ return;
+ }
+
+ final currentOffset = _scrollController.offset;
+ final newOffset = (_tabListWidth / 2) * button.directionFactor;
+
+ _scrollController.animateTo(
+ currentOffset + newOffset,
+ duration: Duration(milliseconds: _kScrollAnimationDuration),
+ curve: Curves.ease,
+ );
+ }
+
+ void _onScrollEnd(ScrollMetrics metrics) {
+ if (_canScrollTo(_ScrollButtonType.left)) {
+ _leftScrollButton.enable();
+ } else {
+ _leftScrollButton.disable();
+ }
+
+ if (_canScrollTo(_ScrollButtonType.right)) {
+ _rightScrollButton.enable();
+ } else {
+ _rightScrollButton.disable();
+ }
+ }
+
// CHECKERS
bool _isOverlappingLeftTabHalf() {
@@ -462,6 +517,25 @@
return false;
}
+ bool _canScrollTo(_ScrollButtonType direction) {
+ switch (direction) {
+ case _ScrollButtonType.left:
+ if (_scrollController.offset <= 0.0) {
+ return false;
+ }
+ return true;
+ case _ScrollButtonType.right:
+ if (_scrollController.offset >=
+ (_kMinTabWidth * widget.bloc.tabs.length -
+ _scrollController.position.viewportDimension)) {
+ return false;
+ }
+ return true;
+ default:
+ return true;
+ }
+ }
+
// ANIMATORS
void _shiftLeftToRight() {
@@ -535,23 +609,27 @@
),
),
Positioned(
- top: 0.0,
right: 0.0,
- bottom: 0.0,
child: AnimatedBuilder(
animation: _hovering,
builder: (_, child) => Offstage(
offstage: !(widget.selected || _hovering.value),
child: child,
),
- child: AspectRatio(
- aspectRatio: 1.0,
+ child: Padding(
+ padding: EdgeInsets.all(4.0),
child: GestureDetector(
onTap: widget.onClose,
child: Container(
color: Colors.transparent,
alignment: Alignment.center,
- child: Text(_kCloseMark),
+ child: Icon(
+ Icons.clear,
+ color: widget.selected
+ ? baseTheme.primaryColor
+ : baseTheme.accentColor,
+ size: _kIconSize,
+ ),
),
),
),
@@ -565,3 +643,28 @@
);
}
}
+
+class _ScrollButton {
+ final _ScrollButtonType type;
+ IconData icon;
+ final ValueNotifier<bool> isEnabled = ValueNotifier<bool>(true);
+ double directionFactor = 0.0;
+
+ _ScrollButton(this.type)
+ : assert(
+ type == _ScrollButtonType.left || type == _ScrollButtonType.right) {
+ switch (type) {
+ case _ScrollButtonType.left:
+ icon = Icons.keyboard_arrow_left;
+ directionFactor = -1.0;
+ break;
+ case _ScrollButtonType.right:
+ icon = Icons.keyboard_arrow_right;
+ directionFactor = 1.0;
+ break;
+ }
+ }
+
+ void disable() => isEnabled.value = false;
+ void enable() => isEnabled.value = true;
+}
diff --git a/bin/simple_browser/test/widgets/tabs_widget_test.dart b/bin/simple_browser/test/widgets/tabs_widget_test.dart
index c41d849..21981d6 100644
--- a/bin/simple_browser/test/widgets/tabs_widget_test.dart
+++ b/bin/simple_browser/test/widgets/tabs_widget_test.dart
@@ -18,6 +18,7 @@
import 'package:simple_browser/src/widgets/tabs_widget.dart';
const _emptyTitle = 'NEW TAB';
+const screenWidth = 800.0;
void main() {
setupLogger(name: 'tabs_widget_test');
@@ -106,14 +107,13 @@
await _addNTabsToTabsBloc(tester, tabsBloc, 3);
- expect(tabsBloc.currentTabIdx, 2,
- reason: 'Expected the 3rd tab widget was focused by default.');
+ _verifyFocusedTabIndex(tabsBloc, 2);
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.');
+
+ _verifyFocusedTabIndex(tabsBloc, 0);
});
testWidgets(
@@ -161,8 +161,7 @@
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.');
+ _verifyFocusedTabIndex(tabsBloc, 3);
Finder closeButtons = _findClose();
expect(closeButtons, findsOneWidget,
@@ -174,9 +173,7 @@
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.');
+ _verifyFocusedTabIndex(tabsBloc, 2);
});
testWidgets(
@@ -187,8 +184,7 @@
final tabs = _findNewTabWidgets();
expect(tabs, findsNWidgets(5),
reason: 'Expected to find 5 tab widgets when 5 tabs added.');
- expect(tabsBloc.currentTabIdx, 4,
- reason: 'Expected the 5th tab widget was focused by default.');
+ _verifyFocusedTabIndex(tabsBloc, 4);
WebPageBloc originalTab0 = tabsBloc.tabs[0];
WebPageBloc originalTab1 = tabsBloc.tabs[1];
@@ -223,9 +219,7 @@
await tester.pumpAndSettle();
// Sees if the tab just moved is focused.
- expect(tabsBloc.currentTabIdx, 2,
- reason: '''Expected the index of the focused tab to be rearranged to 2,
- but actually has been rearranged to ${tabsBloc.currentTabIdx}.''');
+ _verifyFocusedTabIndex(tabsBloc, 2);
// Sees if all tabs have been rarranged correctly.
expect(tabsBloc.tabs[0], originalTab0,
@@ -240,122 +234,179 @@
reason: 'Expected that the 5th tab used to the 4th.');
});
- 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;
+ group('Scrollable tab list', () {
+ final rightScrollMargin =
+ screenWidth - kTabBarHeight - kScrollToMargin - kMinTabWidth;
+ final rightMinMargin = screenWidth - kTabBarHeight - kMinTabWidth;
+ final leftScrollMargin = kScrollToMargin + kTabBarHeight;
+ final leftMinMargin = kTabBarHeight;
- await _setUpTabsWidget(tester, tabsBloc);
+ testWidgets('The tab widget list should scroll on a scroll button tapped.',
+ (WidgetTester tester) async {
+ await _setUpTabsWidget(tester, tabsBloc);
- // Creates 8 tabs.
- await _addNTabsToTabsBloc(tester, tabsBloc, 8);
+ // Creates 8 tabs.
+ await _addNTabsToTabsBloc(tester, tabsBloc, 10);
- // 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 -|
+ // The expected display of the initial tab list (*currently focused tab):
+ // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
+ // |- SCREEN -|
- // See if the last tab is currenly focused.
- expect(tabsBloc.currentTabIdx, 7,
- reason: 'Expected the 8th tab widget is focused by default.');
+ final tabs = _findMinTabWidgets();
- final tabsOnScreen = _findMinTabWidgets();
+ // Sees if there are 8 tab widgets created.
+ expect(tabs, findsNWidgets(10),
+ reason: 'Expected to find 10 tabs, but actually $tabs were found.');
- // 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.');
+ // Sees if the viewport of the list is on the right.
+ _verifyFocusedTabIndex(tabsBloc, 9);
+ _verifyFocusedTabMargin(tester, tabs, rightMinMargin);
+ _verifyPartlyOffscreenFromLeft(tester, tabs.at(3));
- final lastTab = tabsOnScreen.last;
- final fixedY = tester.getTopLeft(lastTab).dy;
- final expectedLastPosition = Offset(rightMinMargin, fixedY);
+ final leftScrollButton = find.byIcon(Icons.keyboard_arrow_left);
+ expect(leftScrollButton, findsOneWidget);
- expect(tester.getTopLeft(lastTab), expectedLastPosition,
- reason: '''Expected the initial X position of the last tab widget
- to be $expectedLastPosition.''');
+ final rightScrollButton = find.byIcon(Icons.keyboard_arrow_right);
+ expect(rightScrollButton, findsOneWidget);
- // 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 left scroll button.
+ await tester.tap(leftScrollButton);
+ await tester.pumpAndSettle();
- // Taps on the fifth tab to change the focus.
- await tester.tap(fifthTab);
- await tester.pumpAndSettle();
+ // The expected display of the tab list (*currently focused tab):
+ // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
+ // |- SCREEN -|
- // The expected display of the tab widgets (* is the currently focused tab):
- // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4* -| |- 5 -| |- 6 -| |- 7 -|
- // |- SCREEN -|
+ // Sees if the list has been scrolled to the left.
+ _verifyPartlyOffscreenFromLeft(tester, tabs.first);
+ _verifyEntirelyOffscreenFromRight(tester, tabs.last);
+ _verifyPartlyOffscreenFromRight(tester, tabs.at(6));
- 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.''');
+ await tester.tap(leftScrollButton);
+ await tester.pumpAndSettle();
- // The second tab in the tabsBloc is the first tab widget on the current screen.
- final secondTab = tabsOnScreen.at(0);
+ // The expected display of the tab list (*currently focused tab):
+ // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
+ // |- SCREEN -|
- final expectedSecondPosition = Offset(leftScrollMargin, fixedY);
- await tester.tap(secondTab);
- await tester.pumpAndSettle();
+ // Sees if the list has been scrolled to the left end.
+ final firstTabLeftX = tester.getTopLeft(tabs.first).dx;
+ expect(firstTabLeftX, leftMinMargin,
+ reason: '''Expected the scroll list have been scroll to its far left,
+ but actually it has not.''');
- // The expected display of the tab widgets (* is the currently focused tab):
- // |- 0 -| |- 1* -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
- // |- SCREEN -|
+ // Taps on the right scroll button.
+ await tester.tap(rightScrollButton);
+ await tester.pumpAndSettle();
- 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.''');
+ // The expected display of the tab list (*currently focused tab):
+ // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
+ // |- SCREEN -|
- 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();
+ // Sees if the list has been scrolled to the right.
+ _verifyPartlyOffscreenFromLeft(tester, tabs.at(3));
+ _verifyPartlyOffscreenFromRight(tester, tabs.last);
- // The expected display of the tab widgets (* is the currently focused tab):
- // |- 0* -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
- // |- SCREEN -|
+ await tester.tap(rightScrollButton);
+ await tester.pumpAndSettle();
- expect(tabsOnScreen, findsNWidgets(7),
- reason:
- 'Expected 7 tab widgets on the screen when 1st tab is focused.');
+ // The expected display of the tab list (*currently focused tab):
+ // |-0-||-1-||-2-||-3-||-4-||-5-||-6-||-7-||-8-||-9*-|
+ // |- SCREEN -|
- 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.''');
+ // Sees if the list has been scrolled to the right end.
+ _verifyFocusedTabMargin(tester, tabs, rightMinMargin);
+ _verifyPartlyOffscreenFromLeft(tester, tabs.at(3));
+ });
- final seventhTab = tabsOnScreen.at(6);
- final expectedSeventhPosition = Offset(rightScrollMargin, fixedY);
- await tester.tap(seventhTab);
- await tester.pumpAndSettle();
+ testWidgets(
+ 'The tab widget list should scroll if needed depending on the offset of the focused tab.',
+ (WidgetTester tester) async {
+ await _setUpTabsWidget(tester, tabsBloc);
- // The expected display of the tab widgets (* is the currently focused tab):
- // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6* -| |- 7 -|
- // |- SCREEN -|
+ // Creates 8 tabs.
+ await _addNTabsToTabsBloc(tester, tabsBloc, 8);
- expect(tabsOnScreen, findsNWidgets(8),
- reason: '''Expected 8 tab widgets on the screen when tapped on the
- 7th tab widget.''');
+ // The expected display of the initial tab list (*currently focused tab):
+ // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7* -|
+ // |- SCREEN -|
- 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.''');
+ final tabs = _findMinTabWidgets();
+
+ // Sees if there are 8 tab widgets created.
+ expect(tabs, findsNWidgets(8),
+ reason: 'Expected to find 8 tabs, but actually $tabs were found.');
+
+ // Sees if the expected tab is currently focused and its at the desired
+ // position.
+ _verifyFocusedTabIndex(tabsBloc, 7);
+ _verifyFocusedTabMargin(tester, tabs, rightMinMargin);
+
+ // Sees if certain tabs are partly/entire offscreen.
+ _verifyEntirelyOffscreenFromLeft(tester, tabs.first);
+ _verifyPartlyOffscreenFromLeft(tester, tabs.at(1));
+
+ // Finds the fifth tab and checks its current position.
+ final fifthTab = tabs.at(4);
+ final expectedFifthTabLeftX = tester.getTopLeft(fifthTab).dx;
+
+ // Taps on the fifth tab to change the focus.
+ await tester.tap(fifthTab);
+ await tester.pumpAndSettle();
+
+ // The expected display of the tab list (*currently focused tab):
+ // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4* -| |- 5 -| |- 6 -| |- 7 -|
+ // |- SCREEN -|
+
+ _verifyFocusedTabIndex(tabsBloc, 4);
+ _verifyFocusedTabMargin(tester, tabs, expectedFifthTabLeftX);
+
+ // Focuses on the second tab by directly adding 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[1]));
+ await tester.pumpAndSettle();
+
+ // The expected display of the tab list (*currently focused tab):
+ // |- 0 -| |- 1* -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
+ // |- SCREEN -|
+
+ _verifyFocusedTabIndex(tabsBloc, 1);
+ _verifyFocusedTabMargin(tester, tabs, leftScrollMargin);
+
+ _verifyPartlyOffscreenFromLeft(tester, tabs.first);
+ _verifyPartlyOffscreenFromRight(tester, tabs.at(5));
+ _verifyEntirelyOffscreenFromRight(tester, tabs.at(6));
+
+ // Focuses on the first tab.
+ tabsBloc.request.add(FocusTabAction(tab: tabsBloc.tabs[0]));
+ await tester.pumpAndSettle();
+
+ // The expected display of the tab list (*currently focused tab):
+ // |- 0* -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6 -| |- 7 -|
+ // |- SCREEN -|
+
+ _verifyFocusedTabIndex(tabsBloc, 0);
+ _verifyFocusedTabMargin(tester, tabs, leftMinMargin);
+
+ _verifyPartlyOffscreenFromRight(tester, tabs.at(5));
+ _verifyEntirelyOffscreenFromRight(tester, tabs.at(6));
+
+ // Focuses on the seventh tab.
+ tabsBloc.request.add(FocusTabAction(tab: tabsBloc.tabs[6]));
+ await tester.pumpAndSettle();
+
+ // The expected display of the tab list (*currently focused tab):
+ // |- 0 -| |- 1 -| |- 2 -| |- 3 -| |- 4 -| |- 5 -| |- 6* -| |- 7 -|
+ // |- SCREEN -|
+
+ _verifyFocusedTabIndex(tabsBloc, 6);
+ _verifyFocusedTabMargin(tester, tabs, rightScrollMargin);
+
+ _verifyEntirelyOffscreenFromLeft(tester, tabs.first);
+ _verifyPartlyOffscreenFromLeft(tester, tabs.at(1));
+ _verifyPartlyOffscreenFromRight(tester, tabs.at(6));
+ });
});
}
@@ -363,7 +414,7 @@
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Container(
- width: 800,
+ width: screenWidth,
child: TabsWidget(
bloc: tabsBloc,
),
@@ -384,12 +435,76 @@
expect(tabsBloc.tabs.length, currentNumTabs + n);
}
-Finder _findMinTabWidgets() => find.byWidgetPredicate(
- (Widget widget) => widget is SizedBox && (widget.width) == kMinTabWidth);
+Finder _findMinTabWidgets() => find.byWidgetPredicate((Widget widget) {
+ if (widget is Container && widget.key == Key('tab')) {
+ BoxConstraints width = widget.constraints.widthConstraints();
+ return (width.minWidth == width.maxWidth) &&
+ (width.minWidth == kMinTabWidth);
+ }
+ return false;
+ });
Finder _findNewTabWidgets() => find.text(_emptyTitle);
-Finder _findClose() => find.text(kCloseMark);
+Finder _findClose() => find.byIcon(Icons.clear);
+
+// Verifies if the currently focused tab is the expected tab.
+void _verifyFocusedTabIndex(TabsBloc tb, int expectedIndex) {
+ expect(tb.currentTabIdx, expectedIndex,
+ reason: '''Expected the index of the currently focused tab to be
+ $expectedIndex, but actually is ${tb.currentTabIdx}''');
+}
+
+// Verifies if the currently focused tab has the expected left margin.
+void _verifyFocusedTabMargin(
+ WidgetTester tester, Finder tabs, double expectedMargin) {
+ // The currently focused tab always become the last member of the finder
+ // since it is rendered on the top of the other tabs.
+ final actualMargin = tester.getTopLeft(tabs.last).dx;
+ expect(actualMargin, expectedMargin,
+ reason: '''Expected the left margin to the currently focused tab to be
+ $expectedMargin, but is actually $actualMargin.''');
+}
+
+void _verifyPartlyOffscreenFromLeft(WidgetTester tester, Finder tab) {
+ final leftBorderX = kTabBarHeight;
+ final tabLeftX = tester.getTopLeft(tab).dx;
+ final tabRightX = tester.getTopRight(tab).dx;
+ expect(tabLeftX < leftBorderX, true,
+ reason: '''Expected this tab's left edge to be offscreen,
+ but actually is onscreen.''');
+ expect(tabRightX >= leftBorderX, true,
+ reason: '''Expected this tab's right edge to be onscreen,
+ but actually is offscreen.''');
+}
+
+void _verifyEntirelyOffscreenFromLeft(WidgetTester tester, Finder tab) {
+ final leftBorderX = kTabBarHeight;
+ final tabRightX = tester.getTopRight(tab).dx;
+ expect(tabRightX < leftBorderX, true,
+ reason: '''Expected this tab's right edge to be offscreen,
+ but actually is onscreen.''');
+}
+
+void _verifyPartlyOffscreenFromRight(WidgetTester tester, Finder tab) {
+ final rightBorderX = screenWidth - kTabBarHeight;
+ final tabLeftX = tester.getTopLeft(tab).dx;
+ final tabRightX = tester.getTopRight(tab).dx;
+ expect(tabLeftX < rightBorderX, true,
+ reason: '''Expected this tab's left edge to be onscreen,
+ but actually is offscreen.''');
+ expect(tabRightX >= rightBorderX, true,
+ reason: '''Expected this tab's right edge to be offscreen,
+ but actually is onscreen.''');
+}
+
+void _verifyEntirelyOffscreenFromRight(WidgetTester tester, Finder tab) {
+ final rightBorderX = screenWidth - kTabBarHeight;
+ final tabLeftX = tester.getTopLeft(tab).dx;
+ expect(tabLeftX > rightBorderX, true,
+ reason: '''Expected this tab's left edge to be offscreen,
+ but actually is onscreen.''');
+}
class MockSimpleBrowserNavigationEventListener extends Mock
implements SimpleBrowserNavigationEventListener {}