[ermine] Refactor AppModel for unittest
This change adds a unittest for AppModel, the main
respository of application level state in Ermine. The AppModel class
is refactored to be able to take mock depedencies injected into its
constructor.
- minor renamings and tweaks.
- tighten logic to confirm to expected behavior, with test coverage.
Bug: 49908
Testing: Adds a unittest for almost 100% coverage on AppModel class.
Change-Id: I953c7418b42872f225663cb23d250b2f3f66e4f0
Reviewed-on: https://fuchsia-review.googlesource.com/c/experiences/+/379582
Commit-Queue: Sanjay Chouksey <sanjayc@google.com>
Reviewed-by: Chase Latta <chaselatta@google.com>
Reviewed-by: Yeonhee Lee <yhlee@google.com>
diff --git a/session_shells/ermine/keyboard_shortcuts/BUILD.gn b/session_shells/ermine/keyboard_shortcuts/BUILD.gn
index 0a06f79..534688f 100644
--- a/session_shells/ermine/keyboard_shortcuts/BUILD.gn
+++ b/session_shells/ermine/keyboard_shortcuts/BUILD.gn
@@ -18,6 +18,7 @@
"//sdk/fidl/fuchsia.ui.input2",
"//sdk/fidl/fuchsia.ui.shortcut",
"//sdk/fidl/fuchsia.ui.views",
+ "//topaz/public/dart/fuchsia_services",
"//third_party/dart-pkg/git/flutter/packages/flutter",
]
}
diff --git a/session_shells/ermine/keyboard_shortcuts/lib/src/keyboard_shortcuts.dart b/session_shells/ermine/keyboard_shortcuts/lib/src/keyboard_shortcuts.dart
index b1535b9..b00e231 100644
--- a/session_shells/ermine/keyboard_shortcuts/lib/src/keyboard_shortcuts.dart
+++ b/session_shells/ermine/keyboard_shortcuts/lib/src/keyboard_shortcuts.dart
@@ -9,8 +9,9 @@
import 'package:fidl_fuchsia_ui_input2/fidl_async.dart';
import 'package:fidl_fuchsia_ui_shortcut/fidl_async.dart' as ui_shortcut
- show Registry, Shortcut, Trigger, Listener, ListenerBinding;
+ show Registry, RegistryProxy, Shortcut, Trigger, Listener, ListenerBinding;
import 'package:fidl_fuchsia_ui_views/fidl_async.dart' show ViewRef;
+import 'package:fuchsia_services/services.dart' show StartupContext;
import 'package:zircon/zircon.dart' show EventPairPair;
/// Listens for keyboard shortcuts and triggers callbacks when they occur.
@@ -36,7 +37,25 @@
shortcuts.forEach(registry.registerShortcut);
}
+ factory KeyboardShortcuts.fromStartupContext(
+ StartupContext startupContext, {
+ Map<String, VoidCallback> actions,
+ String bindings,
+ }) {
+ final shortcutRegistry = ui_shortcut.RegistryProxy();
+ startupContext.incoming.connectToService(shortcutRegistry);
+ return KeyboardShortcuts(
+ registry: shortcutRegistry,
+ actions: actions,
+ bindings: bindings,
+ );
+ }
+
void dispose() {
+ if (registry is ui_shortcut.RegistryProxy) {
+ ui_shortcut.RegistryProxy proxy = registry;
+ proxy.ctrl.close();
+ }
shortcuts.clear();
_listenerBinding.close();
}
diff --git a/session_shells/ermine/shell/BUILD.gn b/session_shells/ermine/shell/BUILD.gn
index dbf2951..d8e51f6 100644
--- a/session_shells/ermine/shell/BUILD.gn
+++ b/session_shells/ermine/shell/BUILD.gn
@@ -169,6 +169,7 @@
flutter_test("ermine_unittests") {
sources = [
+ "app_model_test.dart",
"app_widget_test.dart",
"ask_model_test.dart",
"ask_widget_test.dart",
diff --git a/session_shells/ermine/shell/lib/src/models/app_model.dart b/session_shells/ermine/shell/lib/src/models/app_model.dart
index 662a172..a321fd6 100644
--- a/session_shells/ermine/shell/lib/src/models/app_model.dart
+++ b/session_shells/ermine/shell/lib/src/models/app_model.dart
@@ -6,14 +6,10 @@
import 'dart:io';
import 'package:fidl_fuchsia_intl/fidl_async.dart';
-import 'package:fidl_fuchsia_ui_input/fidl_async.dart' as input;
-import 'package:fidl_fuchsia_ui_shortcut/fidl_async.dart' as ui_shortcut
- show RegistryProxy;
import 'package:fidl_fuchsia_ui_policy/fidl_async.dart';
import 'package:flutter/material.dart';
import 'package:fuchsia_internationalization_flutter/internationalization.dart';
import 'package:fuchsia_inspect/inspect.dart' as inspect;
-
import 'package:fuchsia_services/services.dart' show StartupContext;
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'
show KeyboardShortcuts;
@@ -27,13 +23,16 @@
import 'topbar_model.dart';
/// Model that manages all the application state of this session shell.
+///
+/// Its primary responsibility is to manage visibility of top level UI widgets
+/// like Overview, Recents, Ask and Status.
class AppModel {
- final _presentation = PresentationProxy();
- final _pointerEventsListener = PointerEventsListener();
- final _shortcutRegistry = ui_shortcut.RegistryProxy();
+ KeyboardShortcuts _keyboardShortcuts;
+ PointerEventsListener _pointerEventsListener;
+ SuggestionService _suggestionService;
+
final _intl = PropertyProviderProxy();
- SuggestionService _suggestionService;
PresenterService _presenterService;
/// The [GlobalKey] associated with [Ask] widget.
@@ -52,30 +51,60 @@
ValueNotifier<bool> peekNotifier = ValueNotifier(false);
ValueNotifier<bool> recentsVisibility = ValueNotifier(false);
Stream<Locale> _localeStream;
- KeyboardShortcuts _keyboardShortcuts;
+
ClustersModel clustersModel;
- StatusModel status;
+ StatusModel statusModel;
TopbarModel topbarModel;
- String keyboardShortcuts = 'Help Me!';
+ String keyboardShortcutsHelpText = 'Help Me!';
- AppModel() {
- _startupContext.incoming.connectToService(_shortcutRegistry);
- _startupContext.incoming.connectToService(_intl);
- _startupContext.incoming.connectToService(_presentation);
+ AppModel({
+ KeyboardShortcuts keyboardShortcuts,
+ PointerEventsListener pointerEventsListener,
+ LocaleSource localeSource,
+ SuggestionService suggestionService,
+ this.statusModel,
+ this.clustersModel,
+ }) : _keyboardShortcuts = keyboardShortcuts,
+ _pointerEventsListener = pointerEventsListener,
+ _suggestionService = suggestionService {
+ // Setup child models.
+ topbarModel = TopbarModel(appModel: this);
- _localeStream = LocaleSource(_intl).stream().asBroadcastStream();
+ statusModel ??= StatusModel.fromStartupContext(_startupContext, onLogout);
- clustersModel = ClustersModel();
+ clustersModel ??= ClustersModel();
- _suggestionService = SuggestionService.fromStartupContext(
+ // Setup keyboard shortcuts.
+ _keyboardShortcuts ??= KeyboardShortcuts.fromStartupContext(
+ _startupContext,
+ actions: actions,
+ bindings: keyboardBindings,
+ );
+ keyboardShortcutsHelpText = _keyboardShortcuts.helpText();
+
+ // Setup pointer events listener.
+ _pointerEventsListener ??=
+ _PointerEventsListener.fromStartupContext(_startupContext);
+
+ // Setup locale stream.
+ if (localeSource == null) {
+ _startupContext.incoming.connectToService(_intl);
+ localeSource = LocaleSource(_intl);
+ }
+ _localeStream = localeSource.stream().asBroadcastStream();
+
+ // Suggestion service.
+ _suggestionService ??= SuggestionService.fromStartupContext(
startupContext: _startupContext,
onSuggestion: clustersModel.storySuggested,
);
- topbarModel = TopbarModel(appModel: this);
+ // Expose PresenterService to the environment.
+ advertise();
+ }
- status = StatusModel.fromStartupContext(_startupContext, onLogout);
-
+ @visibleForTesting
+ void advertise() {
// Expose the presenter service to the environment.
_presenterService = PresenterService(clustersModel.presentStory);
_startupContext.outgoing
@@ -93,33 +122,9 @@
/// Called after runApp which initializes flutter's gesture system.
Future<void> onStarted() async {
// Capture pointer events directly from Scenic.
- _pointerEventsListener.listen(_presentation);
-
- // Capture key pressess for key bindings in keyboard_shortcuts.json.
- File file = File('/pkg/data/keyboard_shortcuts.json');
- if (file.existsSync()) {
- final bindings = await file.readAsString();
- _keyboardShortcuts = KeyboardShortcuts(
- registry: _shortcutRegistry,
- actions: {
- 'shortcuts': onKeyboard,
- 'ask': onMeta,
- 'overview': onOverview,
- 'recents': onRecents,
- 'fullscreen': onFullscreen,
- 'cancel': onCancel,
- 'close': onClose,
- 'status': onStatus,
- 'nextCluster': clustersModel.nextCluster,
- 'previousCluster': clustersModel.previousCluster,
- 'logout': onLogout,
- },
- bindings: bindings,
- );
- keyboardShortcuts = _keyboardShortcuts.helpText();
- } else {
- throw ArgumentError.value(
- 'keyboard_shortcuts.json', 'fileName', 'File does not exist');
+ if (_pointerEventsListener is _PointerEventsListener) {
+ _PointerEventsListener listener = _pointerEventsListener;
+ listener.listen(listener.presentation);
}
// Update the current time every second.
@@ -141,6 +146,28 @@
inspect.Inspect.onDemand('ermine', _onInspect);
}
+ // Map key shortcuts to corresponding actions.
+ Map<String, VoidCallback> get actions => {
+ 'shortcuts': onKeyboard,
+ 'ask': onAsk,
+ 'overview': onOverview,
+ 'recents': onRecents,
+ 'fullscreen': onFullscreen,
+ 'cancel': onCancel,
+ 'close': onClose,
+ 'status': onStatus,
+ 'nextCluster': clustersModel.nextCluster,
+ 'previousCluster': clustersModel.previousCluster,
+ 'logout': onLogout,
+ };
+
+ // Returns key bindings in keyboard_shortcuts.json. Throws a fatal exception
+ // if not found.
+ String get keyboardBindings {
+ File file = File('/pkg/data/keyboard_shortcuts.json');
+ return file.readAsStringSync();
+ }
+
void onFullscreen() {
if (clustersModel.fullscreenStory != null) {
clustersModel.fullscreenStory.restore();
@@ -151,9 +178,9 @@
}
}
- /// Toggles the Ask bar.
- void onMeta() {
- if (!hasStories) {
+ /// Toggles the Ask bar when Overview is not visible.
+ void onAsk() {
+ if (!hasStories || overviewVisibility.value == true) {
return;
}
if (askVisibility.value == false) {
@@ -176,9 +203,9 @@
overviewVisibility.value = !overviewVisibility.value;
}
- /// Toggles recents.
+ /// Toggles recents when Overview is not visible.
void onRecents() {
- if (!hasStories) {
+ if (!hasStories || overviewVisibility.value == true) {
return;
}
if (recentsVisibility.value == false) {
@@ -189,9 +216,9 @@
recentsVisibility.value = !recentsVisibility.value;
}
- /// Toggles the Status menu on/off.
+ /// Toggles the Status menu on/off when Overview is not visible.
void onStatus() {
- if (!hasStories) {
+ if (!hasStories || overviewVisibility.value == true) {
return;
}
if (statusVisibility.value == false) {
@@ -204,22 +231,25 @@
/// Called when tapped behind Ask bar, quick settings, notifications or the
/// Escape key was pressed.
void onCancel() {
- status.reset();
+ statusModel.reset();
askVisibility.value = false;
statusVisibility.value = false;
helpVisibility.value = false;
recentsVisibility.value = false;
- overviewVisibility.value = !hasStories;
+ overviewVisibility.value = overviewVisibility.value || !hasStories;
}
/// Called when the user wants to delete the story.
void onClose() {
- clustersModel.focusedStory?.delete();
+ // Close is allowed when not in Overview.
+ if (overviewVisibility.value == false) {
+ clustersModel.focusedStory?.delete();
+ }
}
/// Called when the keyboard help button is tapped.
void onKeyboard() {
- if (!hasStories) {
+ if (overviewVisibility.value == true) {
return;
}
if (helpVisibility.value == false) {
@@ -232,51 +262,14 @@
/// Called when the user initiates logout (using keyboard or UI).
void onLogout() {
onCancel();
+ _keyboardShortcuts.dispose();
_pointerEventsListener.stop();
- _intl.ctrl.close();
+ _intl?.ctrl?.close();
_suggestionService.dispose();
- status.dispose();
- _keyboardShortcuts.dispose();
- _shortcutRegistry.ctrl.close();
- _presentation.ctrl.close();
+ statusModel.dispose();
}
- void injectTap(Offset offset) {
- _presentation
- ..injectPointerEventHack(_createPointerEvent(
- phase: input.PointerEventPhase.add,
- offset: offset,
- ))
- ..injectPointerEventHack(_createPointerEvent(
- phase: input.PointerEventPhase.down,
- offset: offset,
- ))
- ..injectPointerEventHack(_createPointerEvent(
- phase: input.PointerEventPhase.up,
- offset: offset,
- ))
- ..injectPointerEventHack(_createPointerEvent(
- phase: input.PointerEventPhase.remove,
- offset: offset,
- ));
- }
-
- input.PointerEvent _createPointerEvent({
- input.PointerEventPhase phase,
- Offset offset,
- }) =>
- input.PointerEvent(
- eventTime: 0,
- deviceId: 0,
- pointerId: 0,
- type: input.PointerEventType.touch,
- phase: phase,
- x: offset.dx,
- y: offset.dy,
- buttons: 0,
- );
-
void _onInspect(inspect.Node node) {
// Session.
node.stringProperty('session').setValue('started');
@@ -285,9 +278,27 @@
askKey.currentState?.onInspect(node.child('ask'));
// Status.
- status.onInspect(node.child('status'));
+ statusModel.onInspect(node.child('status'));
// Topbar.
topbarModel.onInspect(node.child('topbar'));
}
}
+
+class _PointerEventsListener extends PointerEventsListener {
+ final PresentationProxy presentation;
+
+ _PointerEventsListener(this.presentation) : super();
+
+ factory _PointerEventsListener.fromStartupContext(StartupContext context) {
+ final presentation = PresentationProxy();
+ context.incoming.connectToService(presentation);
+ return _PointerEventsListener(presentation);
+ }
+
+ @override
+ void stop() {
+ super.stop();
+ presentation.ctrl.close();
+ }
+}
diff --git a/session_shells/ermine/shell/lib/src/models/cluster_model.dart b/session_shells/ermine/shell/lib/src/models/cluster_model.dart
index fa33de6..e9d9bd7 100644
--- a/session_shells/ermine/shell/lib/src/models/cluster_model.dart
+++ b/session_shells/ermine/shell/lib/src/models/cluster_model.dart
@@ -157,8 +157,12 @@
}
// If the story was also focused, set focus on next story in same cluster.
- if (focusedStory == story && currentCluster.value.stories.isNotEmpty) {
- currentCluster.value.stories.last.focus();
+ if (focusedStory == story) {
+ if (currentCluster.value.stories.isNotEmpty) {
+ currentCluster.value.stories.last.focus();
+ } else {
+ focusedStoryNotifier.value = null;
+ }
}
}
diff --git a/session_shells/ermine/shell/lib/src/models/topbar_model.dart b/session_shells/ermine/shell/lib/src/models/topbar_model.dart
index 96bd789..439d40e 100644
--- a/session_shells/ermine/shell/lib/src/models/topbar_model.dart
+++ b/session_shells/ermine/shell/lib/src/models/topbar_model.dart
@@ -43,7 +43,7 @@
void showRecents() => appModel.onRecents();
/// Display the Ask bar. Called by Ask Button.
- void showAsk() => appModel.onMeta();
+ void showAsk() => appModel.onAsk();
/// Display the keyboard help panel.
void showKeyboardHelp() => appModel.onKeyboard();
diff --git a/session_shells/ermine/shell/lib/src/widgets/status/status_container.dart b/session_shells/ermine/shell/lib/src/widgets/status/status_container.dart
index be83201..6f41c19 100644
--- a/session_shells/ermine/shell/lib/src/widgets/status/status_container.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/status/status_container.dart
@@ -26,7 +26,7 @@
ErmineStyle.kTopBarHeight -
ErmineStyle.kStoryTitleHeight;
final status = Material(
- key: model.status.key,
+ key: model.statusModel.key,
color: ErmineStyle.kBackgroundColor,
elevation: Elevations.systemOverlayElevation,
child: Container(
@@ -36,7 +36,7 @@
decoration: BoxDecoration(
border: Border.all(color: ErmineStyle.kOverlayBorderColor),
),
- child: Status(model: model.status),
+ child: Status(model: model.statusModel),
),
);
return RepaintBoundary(
diff --git a/session_shells/ermine/shell/lib/src/widgets/support/keyboard_help.dart b/session_shells/ermine/shell/lib/src/widgets/support/keyboard_help.dart
index 0b61c8c..3ab3807 100644
--- a/session_shells/ermine/shell/lib/src/widgets/support/keyboard_help.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/support/keyboard_help.dart
@@ -51,7 +51,7 @@
height: 500,
child: SingleChildScrollView(
child: Text(
- model.keyboardShortcuts,
+ model.keyboardShortcutsHelpText,
style: TextStyle(
fontFamily: 'RobotoMono',
fontSize: 14.0,
diff --git a/session_shells/ermine/shell/lib/src/widgets/support/overview.dart b/session_shells/ermine/shell/lib/src/widgets/support/overview.dart
index 792be7d..f415555 100644
--- a/session_shells/ermine/shell/lib/src/widgets/support/overview.dart
+++ b/session_shells/ermine/shell/lib/src/widgets/support/overview.dart
@@ -74,7 +74,7 @@
Expanded(
child: Container(
padding: ErmineStyle.kOverviewElementPadding,
- child: Status(model: model.status),
+ child: Status(model: model.statusModel),
),
),
],
diff --git a/session_shells/ermine/shell/test/app_model_test.dart b/session_shells/ermine/shell/test/app_model_test.dart
new file mode 100644
index 0000000..4177393
--- /dev/null
+++ b/session_shells/ermine/shell/test/app_model_test.dart
@@ -0,0 +1,213 @@
+// 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:ui';
+
+import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'
+ show KeyboardShortcuts;
+import 'package:lib.widgets/utils.dart' show PointerEventsListener;
+import 'package:fuchsia_internationalization_flutter/internationalization.dart';
+import 'package:test/test.dart';
+import 'package:mockito/mockito.dart';
+
+// ignore_for_file: implementation_imports
+import 'package:ermine/src/models/cluster_model.dart';
+import 'package:ermine/src/models/ermine_story.dart';
+import 'package:ermine/src/models/status_model.dart';
+import 'package:ermine/src/utils/suggestions.dart';
+import 'package:ermine/src/models/app_model.dart';
+
+void main() {
+ AppModel appModel;
+ KeyboardShortcuts keyboardShortcuts;
+ PointerEventsListener pointerEventsListener;
+ LocaleSource localeSource;
+ SuggestionService suggestionService;
+ StatusModel statusModel;
+ ClustersModel clustersModel;
+
+ setUp(() async {
+ keyboardShortcuts = MockKeyboardShortcuts();
+ pointerEventsListener = MockPointerEventsListener();
+ localeSource = MockLocaleSource();
+ suggestionService = MockSuggestionService();
+ statusModel = MockStatusModel();
+ clustersModel = MockClustersModel();
+
+ when(localeSource.stream()).thenAnswer((_) => Stream<Locale>.empty());
+
+ appModel = _TestAppModel(
+ keyboardShortcuts: keyboardShortcuts,
+ pointerEventsListener: pointerEventsListener,
+ localeSource: localeSource,
+ suggestionService: suggestionService,
+ statusModel: statusModel,
+ clustersModel: clustersModel,
+ );
+ await appModel.onStarted();
+ });
+
+ tearDown(() {
+ when(clustersModel.hasStories).thenReturn(false);
+
+ appModel.onLogout();
+
+ verify(keyboardShortcuts.dispose()).called(1);
+ verify(pointerEventsListener.stop()).called(1);
+ verify(suggestionService.dispose()).called(1);
+ verify(statusModel.dispose()).called(1);
+ });
+
+ test('Should start in Overview state', () async {
+ expect(appModel.overviewVisibility.value, true);
+ expect(appModel.askVisibility.value, false);
+ });
+
+ test('Toggle Overview state with or without stories', () async {
+ when(clustersModel.hasStories).thenReturn(false);
+ appModel.onOverview();
+ expect(appModel.overviewVisibility.value, true);
+
+ when(clustersModel.hasStories).thenReturn(true);
+ appModel.onOverview();
+ expect(appModel.overviewVisibility.value, false);
+ });
+
+ test('Should not toggle from Overview on Ask', () async {
+ when(clustersModel.hasStories).thenReturn(true);
+
+ appModel.overviewVisibility.value = true;
+ appModel.onAsk();
+ expect(appModel.askVisibility.value, false);
+ expect(appModel.overviewVisibility.value, true);
+ });
+
+ test('Allow toggling Ask when NOT in Overview', () async {
+ when(clustersModel.hasStories).thenReturn(true);
+ appModel.overviewVisibility.value = false;
+
+ appModel.onAsk();
+ expect(appModel.askVisibility.value, true);
+ appModel.onAsk();
+ expect(appModel.askVisibility.value, false);
+ });
+
+ test('Allow toggling Recents when NOT in Overview', () async {
+ when(clustersModel.hasStories).thenReturn(true);
+ appModel.overviewVisibility.value = false;
+
+ appModel.onRecents();
+ expect(appModel.recentsVisibility.value, true);
+ appModel.onRecents();
+ expect(appModel.recentsVisibility.value, false);
+ });
+
+ test('Allow toggling Status when NOT in Overview', () async {
+ when(clustersModel.hasStories).thenReturn(true);
+ appModel.overviewVisibility.value = false;
+
+ appModel.onStatus();
+ expect(appModel.statusVisibility.value, true);
+ appModel.onStatus();
+ expect(appModel.statusVisibility.value, false);
+ });
+
+ test('Escape key should dismiss top level widgets.', () async {
+ // When no stories are present, onCancel should display Overview.
+ when(clustersModel.hasStories).thenReturn(false);
+ appModel.onCancel();
+ expect(appModel.overviewVisibility.value, true);
+ expect(appModel.askVisibility.value, false);
+ expect(appModel.statusVisibility.value, false);
+ expect(appModel.helpVisibility.value, false);
+ expect(appModel.recentsVisibility.value, false);
+
+ // When stories are present, onCancel should not toggle Overview.
+ when(clustersModel.hasStories).thenReturn(true);
+ appModel.overviewVisibility.value = false;
+
+ appModel.onCancel();
+ expect(appModel.overviewVisibility.value, false);
+ expect(appModel.askVisibility.value, false);
+ expect(appModel.statusVisibility.value, false);
+ expect(appModel.helpVisibility.value, false);
+ expect(appModel.recentsVisibility.value, false);
+ });
+
+ test('Close should remove focused story.', () async {
+ final story = MockErmineStory();
+ when(clustersModel.focusedStory).thenReturn(story);
+
+ // Close is not allowed in Overview.
+ appModel.overviewVisibility.value = true;
+ appModel.onClose();
+ verifyNever(story.delete());
+
+ // But allowed from cluster view.
+ appModel.overviewVisibility.value = false;
+ appModel.onClose();
+ verify(story.delete()).called(1);
+ });
+
+ test('Keyboard help can be visible when not in Overview.', () async {
+ when(clustersModel.hasStories).thenReturn(true);
+
+ appModel.overviewVisibility.value = false;
+ appModel.onKeyboard();
+ expect(appModel.helpVisibility.value, true);
+ });
+
+ test('Should toggle fullscreen for focused story.', () async {
+ final story = MockErmineStory();
+ when(clustersModel.focusedStory).thenReturn(story);
+
+ appModel.onFullscreen();
+ verify(clustersModel.maximize(any)).called(1);
+
+ when(clustersModel.fullscreenStory).thenReturn(story);
+ expect(appModel.isFullscreen, true);
+ appModel.onFullscreen();
+ verify(story.restore()).called(1);
+ });
+
+ test('Should not go fullscreen if no story is in focus.', () async {
+ appModel.onFullscreen();
+ expect(appModel.isFullscreen, false);
+ });
+}
+
+class _TestAppModel extends AppModel {
+ _TestAppModel({
+ KeyboardShortcuts keyboardShortcuts,
+ PointerEventsListener pointerEventsListener,
+ LocaleSource localeSource,
+ SuggestionService suggestionService,
+ StatusModel statusModel,
+ ClustersModel clustersModel,
+ }) : super(
+ keyboardShortcuts: keyboardShortcuts,
+ pointerEventsListener: pointerEventsListener,
+ localeSource: localeSource,
+ suggestionService: suggestionService,
+ statusModel: statusModel,
+ clustersModel: clustersModel,
+ );
+
+ @override
+ void advertise() {}
+}
+
+class MockKeyboardShortcuts extends Mock implements KeyboardShortcuts {}
+
+class MockPointerEventsListener extends Mock implements PointerEventsListener {}
+
+class MockLocaleSource extends Mock implements LocaleSource {}
+
+class MockSuggestionService extends Mock implements SuggestionService {}
+
+class MockStatusModel extends Mock implements StatusModel {}
+
+class MockClustersModel extends Mock implements ClustersModel {}
+
+class MockErmineStory extends Mock implements ErmineStory {}