Skip to content

Commit

Permalink
Session manager unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
stasgora committed May 4, 2021
1 parent 2cc630d commit a732e7c
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 21 deletions.
10 changes: 5 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# 0.3.1

- **FIX**: `PageView` config was not correctly detected by the `Detector`
- **FIX**: `PageView` config not correctly detected by the `Detector`

# 0.3.0

- **BREAKING**: reworked `Detector` implementation - should be placed around the scrolling widgets
- **NEW**: support for lazy loaded scroll widgets like `ListView.builder()`
- **BREAKING**: reworked `Detector` implementation - should be placed around the scrollable widgets
- **NEW**: support for lazy loaded scrollable widgets like `ListView.builder()`
- **FIX**: events misreported during scrolling
- **NEW**: support for custom scroll widgets - using `Detector.custom()` constructor
- **NEW**: scrolling heat maps smart cropping - saves on image size
- **NEW**: support for custom scrollable widgets - using `Detector.custom()` constructor
- **NEW**: scrollable heat maps smart cropping - saving on image size
- **REFACTOR**: removed `ListDetector` - not needed anymore
- **FIX**: rendering bug when `Config.heatMapPixelRatio` was changed during session recording

Expand Down
1 change: 1 addition & 0 deletions lib/round_spot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
/// Widgets that dynamically change their size, position, dis/appear
/// or otherwise cause other widgets to shift their positions relative to
/// the viewport (for example in response to a user action) are not supported.
///
/// When that happens during a session recording it will cause
/// some of the gathered interactions to be displaced in relation
/// to the background image and the rest of data making
Expand Down
14 changes: 8 additions & 6 deletions lib/src/components/session_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:ui';

import 'package:enum_to_string/enum_to_string.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';

import '../../round_spot.dart';
Expand Down Expand Up @@ -45,7 +45,7 @@ class SessionManager {

/// Handles application lifecycle state changes
void onLifecycleStateChanged(AppLifecycleState state) {
if (state == AppLifecycleState.paused) _endSessions();
if (state == AppLifecycleState.paused) endSessions();
}

/// Handles application [PageRoute] changes
Expand All @@ -67,9 +67,9 @@ class SessionManager {
);
return;
}
if (_currentPage == null || _processedEventIDs.contains(event.id)) return;
if (_processedEventIDs.contains(event.id)) return;
if (!_config.enabled) {
_endSessions();
endSessions();
return;
}
if (_currentPageDisabled && !status.hasGlobalScope) return;
Expand All @@ -90,7 +90,7 @@ class SessionManager {
_idleTimer?.cancel();
_idleTimer = Timer(
Duration(seconds: _config.maxSessionIdleTime!),
_endSessions,
endSessions,
);
}
return session;
Expand All @@ -107,7 +107,9 @@ class SessionManager {
);
}

void _endSessions() {
/// Triggers the session processing
@visibleForTesting
void endSessions() {
bool skipSession(Session session) =>
session.events.length < _config.minSessionEventCount;
for (var key in _sessions.keys) {
Expand Down
4 changes: 2 additions & 2 deletions lib/src/models/event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import '../utils/utils.dart';
/// Holds information about a single user interaction
class Event {
/// Equals [PointerEvent.localPosition]
Offset location;
final Offset location;

/// Timestamp of when the event fired
final int timestamp;
Expand All @@ -17,7 +17,7 @@ class Event {

/// Creates an [Event] with a given [location] and [id]
@visibleForTesting
Event({required this.location, required this.id})
Event({this.location = Offset.zero, this.id = 0})
: timestamp = getTimestamp();

/// Creates an [Event] from Flutters [PointerEvent]
Expand Down
2 changes: 0 additions & 2 deletions lib/src/widgets/detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ import '../utils/components.dart';
/// and cannot put the [Detector] directly around one of the standard Flutter
/// widgets, use the [Detector.custom()] constructor.
///
/// This widget relies on [NotificationListener] to track the
///
/// ### Not supported / untested
/// * Nested scroll views
/// * Widgets changing their size
Expand Down
2 changes: 1 addition & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ packages:
source: hosted
version: "1.1.0"
collection:
dependency: transitive
dependency: "direct dev"
description:
name: collection
url: "https://pub.dartlang.org"
Expand Down
9 changes: 7 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Customizable, easy to use heat map interface analysis library
repository: https://github.com/stasgora/round-spot
issue_tracker: https://github.com/stasgora/round-spot/issues

version: 0.3.0
version: 0.3.1

environment:
sdk: '>=2.12.0 <3.0.0'
Expand All @@ -13,16 +13,21 @@ dependencies:
flutter:
sdk: flutter

meta: ^1.3.0
get_it: ^7.0.0
logging: ^1.0.0
simple_cluster: ^0.3.0

# Utils
meta: ^1.3.0
enum_to_string: ^2.0.1

dev_dependencies:
flutter_test:
sdk: flutter

mocktail: ^0.1.2
collection: ^1.15.0

# Lints
effective_dart: ^1.3.1
pedantic: ^1.11.0
8 changes: 5 additions & 3 deletions test/detector_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import 'package:round_spot/src/widgets/detector.dart';
class MockSessionManager extends Mock implements SessionManager {}

void main() {
S.registerSingleton<SessionManager>(MockSessionManager());
registerFallbackValue<Event>(Event(location: Offset.zero, id: 0));
registerFallbackValue<DetectorStatus>(DetectorStatus(areaKey: GlobalKey()));
setUpAll(() {
S.registerSingleton<SessionManager>(MockSessionManager());
registerFallbackValue<Event>(Event());
registerFallbackValue<DetectorStatus>(DetectorStatus(areaKey: GlobalKey()));
});

group('Detector', () {
setUp(() {
Expand Down
173 changes: 173 additions & 0 deletions test/session_manager_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:collection/collection.dart';

import 'package:round_spot/src/components/processors/graphical_processor.dart';
import 'package:round_spot/src/components/processors/raw_data_processor.dart';
import 'package:round_spot/src/components/background_manager.dart';
import 'package:round_spot/src/components/session_manager.dart';
import 'package:round_spot/src/models/config/config.dart';
import 'package:round_spot/src/models/detector_status.dart';
import 'package:round_spot/src/models/event.dart';
import 'package:round_spot/src/models/session.dart';
import 'package:round_spot/src/utils/components.dart';

class MockBackgroundManager extends Mock implements BackgroundManager {}

class MockGraphicalProcessor extends Mock implements GraphicalProcessor {}

class MockRawDataProcessor extends Mock implements RawDataProcessor {}

late SessionManager _manager;

class EventDescriptor {
final Event event;
final DetectorStatus status;

EventDescriptor(this.event, this.status);
EventDescriptor.global(this.event) : status = detectorStatus();
}

void main() {
setUpAll(() {
S.registerSingleton<Config>(Config());
S.registerSingleton<BackgroundManager>(MockBackgroundManager());
S.registerSingleton<GraphicalProcessor>(MockGraphicalProcessor());
S.registerSingleton<RawDataProcessor>(MockRawDataProcessor());

registerFallbackValue<Session>(Session(page: '', area: '', pixelRatio: 1));
registerFallbackValue<GlobalKey>(GlobalKey());
});

group('Session Manager', () {
setUp(() {
reset(S.get<BackgroundManager>());
reset(S.get<GraphicalProcessor>());
reset(S.get<RawDataProcessor>());

_manager = SessionManager((_, __) {}, (_, __) {});
_manager.onRouteOpened(settings: RouteSettings(name: ''));
});

group('Event processing', () {
test('reported events are included in the session', () {
var events = List.generate(4, (i) => Event(id: i));
var sessions = _simpleProcessEvents(events);
expect(sessions.first.events, equals(events));
});
test('events are skipped once they have been recorded once', () {
var event = Event();
var area = 'area';
var list = [
EventDescriptor(event, detectorStatus(areaID: area)),
EventDescriptor.global(event),
];
var sessions = _processEvents(list);
expect(sessions.first.events, equals([event]));
expect(sessions.first.area, equals(area));
});
test('events captured by global Detector are registered twice', () {
var event = Event();
var global = 'global';
var enclosing = 'area';
var list = [
EventDescriptor(event, detectorStatus(areaID: global, global: true)),
EventDescriptor(event, detectorStatus(areaID: enclosing)),
EventDescriptor.global(event),
];
var sessions = _processEvents(list, count: 2);
expect(sessions, hasLength(2));
expect(sessions.withAreaID(global), isNotNull);
expect(sessions.withAreaID(enclosing), isNotNull);
});
group('Routes', () {
test('page route changes are registered', () {
var page = 'other';
_registerEvent();
_manager.onRouteOpened(settings: RouteSettings(name: page));
var sessions = _simpleProcessEvents([Event(id: 1)], count: 2);
_expectEventsByIDs(sessions.withPage()!.events, [0]);
_expectEventsByIDs(sessions.withPage(page)!.events, [1]);
});
test('session is continued once the page route is reopened', () {
_registerEvent();
_manager.onRouteOpened(settings: RouteSettings(name: 'other'));
_manager.onRouteOpened(settings: RouteSettings(name: ''));
var sessions = _simpleProcessEvents([Event(id: 1)], count: 1);
expect(sessions.first.events, hasLength(2));
});
});
});
group('Other events', () {
test('events are ignored if no route is set', () {
_manager.onRouteOpened();
_simpleProcessEvents([Event()], count: 0);
});
test('all sessions are ended when app goes into paused state', () {
_registerEvent();
_manager.onLifecycleStateChanged(AppLifecycleState.paused);
verify(() => S.get<GraphicalProcessor>().process(any()))
.called(equals(1));
});
});
test('scroll events are forwarded to the Background Manager', () {
_manager.onSessionScroll(detectorStatus());
verify(() => S.get<BackgroundManager>().onScroll(any(), any())).called(1);
});
});
}

DetectorStatus detectorStatus({String areaID = '', bool global = false}) {
return DetectorStatus(
areaKey: GlobalKey(),
areaID: areaID,
hasGlobalScope: global,
);
}

List<Session> _simpleProcessEvents(List<Event> events, {int count = 1}) {
return _processEvents(
events.map((e) => EventDescriptor.global(e)).toList(),
count: count,
);
}

List<Session> _processEvents(List<EventDescriptor> events, {int count = 1}) {
for (var event in events) {
_manager.onEvent(event: event.event, status: event.status);
}
_manager.endSessions();
if (count == 0) {
verifyNever(() => S.get<GraphicalProcessor>().process(any()));
return [];
}
var args = verify(() {
return S.get<GraphicalProcessor>().process(captureAny());
})
..called(equals(count));
return args.captured.map((e) => e as Session).toList();
}

void _registerEvent({Event? event, DetectorStatus? status}) {
event ??= Event();
status ??= detectorStatus();
_manager.onEvent(event: event, status: status);
}

void _expectEventsByIDs(List<Event> actual, List<int> expectedIDs) {
expect(actual, hasLength(expectedIDs.length));
for (var i = 0; i < actual.length; i++) {
expect(actual[i].id, equals(expectedIDs[i]));
}
}

extension SessionByFiled on List<Session> {
Session? withAreaID([String areaID = '']) {
return firstWhereOrNull((s) => s.area == areaID);
}

Session? withPage([String page = '']) {
return firstWhereOrNull((s) => s.page == page);
}
}

0 comments on commit a732e7c

Please sign in to comment.