Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added punctuality display (#122) #473

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1c6cc8d
feat: added CAB start and end signaling
rawi-coding Dec 4, 2024
f4dc23a
feat: handled track equipment segments that start or end outside of t…
rawi-coding Dec 6, 2024
2cd599a
Merge branch 'main' into 82-streckenausruestung
rawi-coding Dec 6, 2024
0f2d05b
chore: fixed some issues after merge.
rawi-coding Dec 6, 2024
7babb87
chore: added UI integration test for CAB signaling.
rawi-coding Dec 6, 2024
3313065
chore: changes from review and fixed ordering of CABSignaling.
rawi-coding Dec 10, 2024
d707eca
chore: fixed integration test.
rawi-coding Dec 10, 2024
d868b47
chore: added train journey to test non standard track equipment.
rawi-coding Dec 11, 2024
6a6428a
Merge branch 'main' into 82-streckenausruestung
rawi-coding Dec 12, 2024
638c975
chore: updated integration test for track equipment to T1 train journey
rawi-coding Dec 12, 2024
2fe61ea
fix ui tests
Grodien Dec 12, 2024
924ea83
fix tests for real
Grodien Dec 12, 2024
8987de9
feat: added visualization of non standard track equipment (#82).
rawi-coding Dec 17, 2024
b9939e6
Merge branch 'main' into 82-streckenausruestung
rawi-coding Dec 17, 2024
7698872
chore: add integration tests for track equipment
rawi-coding Dec 17, 2024
351dd8e
fix rendering issue
Grodien Dec 18, 2024
2046c2f
merge main
Grodien Dec 18, 2024
aef8aa8
quick fix for not throwing exception on event message
Grodien Dec 18, 2024
dbf1762
fix something I broke :)
Grodien Dec 18, 2024
fc76192
feat: implement basic sfera event handling
Grodien Dec 19, 2024
5bb0c58
chore: fix test
Grodien Dec 19, 2024
58c4347
merge main
Grodien Dec 19, 2024
0d9a432
fix lint
Grodien Dec 19, 2024
b1ec409
remove unused file
Grodien Dec 19, 2024
3bb680b
feat: Fixed the delay model and added all the needed Sfera models. Al…
tobe-official Dec 20, 2024
6dd34d7
Merge branch 'main' into 122-puenktlichkeit-anzeige
tobe-official Dec 20, 2024
a999728
fix: fixed the mentioned points. Still need to do the tests. SBBLoadi…
tobe-official Dec 23, 2024
d33253b
fix: fixed the tests and fixed the mentioned points
tobe-official Jan 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions das_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,23 @@ fvm flutter test --flavor dev --dart-define=MQTT_USERNAME=${MQTT_USERNAME} --dar

## Architecture

TODO

### Test file structure
To prevent confusion, fictive train numbers with the prefix `T` are used for the test scenarios. It is desired to create new train journeys for different features.
The file structure in [test_resources](test_resources) for a test scenario looks as follows:
* base directory named `<train number>_<optional description>`
* journey profile named `SFERA_JP_<train number>_<optional postfix>`
* corresponding segment profiles named `SFERA_SP_<train number>_<segment number>`
* corresponding train characteristics named `SFERA_TC_<train number>_<tc number>`
An example test scenario for train number T1 could look like this:
* T1_demo_journey/
* SFERA_JP_T1
* SFERA_JP_T1_without_stop
* SFERA_SP_T1_1
* SFERA_SP_T1_2
* SFERA_TC_T1_1
<a name="localization"></a>


## Localization

The app is available in three languages:
Expand Down
127 changes: 125 additions & 2 deletions das_client/integration_test/test/train_journey_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:das_client/app/pages/journey/train_journey/widgets/header/header.dart';
import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart';
import 'package:flutter_test/flutter_test.dart';
Expand Down Expand Up @@ -28,7 +30,7 @@ void main() {
await tester.pumpAndSettle();

// check if station is present
expect(find.text('Solothurn'), findsOneWidget);
expect(find.text('Haltestelle B'), findsWidgets);

await tester.pumpAndSettle();
});
Expand All @@ -37,7 +39,6 @@ void main() {
// Load app widget.
await prepareAndStartApp(tester);

//
final trainNumberText = findTextFieldByLabel(l10n.p_train_selection_trainnumber_description);
expect(trainNumberText, findsOneWidget);

Expand All @@ -59,4 +60,126 @@ void main() {
await tester.pumpAndSettle();
});
});

testWidgets('find base value when no punctuality update comes', (tester) async {
// Load app widget.
await prepareAndStartApp(tester);

final trainNumberText = findTextFieldByLabel(l10n.p_train_selection_trainnumber_description);
expect(trainNumberText, findsOneWidget);

await enterText(tester, trainNumberText, 'T6');

final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first;
await tester.tap(primaryButton);

// wait for train journey to load
await tester.pumpAndSettle();

//find the header and check if it is existent
final headerFinder = find.byType(Header);
expect(headerFinder, findsOneWidget);

//Find the text in the header
expect(find.descendant(of: headerFinder, matching: find.text('+00:00')), findsOneWidget);

await tester.pumpAndSettle();
});

testWidgets('check if the displayed current time is correct', (tester) async {
// Load app widget.
await prepareAndStartApp(tester);

//Select the correct train number
final trainNumberText = findTextFieldByLabel(l10n.p_train_selection_trainnumber_description);
expect(trainNumberText, findsOneWidget);

await enterText(tester, trainNumberText, 'T6');

//Log into the journey
final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first;
await tester.tap(primaryButton);

// wait for train journey to load
await tester.pumpAndSettle();

//find the header and check if it is existent
final headerFinder = find.byType(Header);
expect(headerFinder, findsOneWidget);

final DateTime currentTime = DateTime.now();
final String currentHour = currentTime.hour <= 9 ? '0${currentTime.hour}' : (currentTime.hour).toString();
final String currentMinutes = currentTime.hour <= 9 ? '0${currentTime.minute}' : (currentTime.minute).toString();
final String currentSeconds = currentTime.hour <= 9 ? '0${currentTime.second}' : (currentTime.second).toString();
final String nextSecond =
currentTime.hour <= 9 ? '0${currentTime.second + 1}' : (currentTime.second + 1).toString();
final String currentWholeTime = '$currentHour:$currentMinutes:$currentSeconds';
final String nextSecondWholeTime = '$currentHour:$currentMinutes:$nextSecond';

if (!find.descendant(of: headerFinder, matching: find.text(currentWholeTime)).evaluate().isNotEmpty) {
expect(find.descendant(of: headerFinder, matching: find.text(nextSecondWholeTime)), findsOneWidget);
} else {
expect(find.descendant(of: headerFinder, matching: find.text(currentWholeTime)), findsOneWidget);
}

await tester.pumpAndSettle();
});

testWidgets('check if update sent is correct', (tester) async {
// Load app widget.
await prepareAndStartApp(tester);

// Select the correct train number
final trainNumberText = findTextFieldByLabel(l10n.p_train_selection_trainnumber_description);
expect(trainNumberText, findsOneWidget);

await enterText(tester, trainNumberText, 'T9999');

// Log into the journey
final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first;
await tester.tap(primaryButton);

// Wait for train journey to load
await tester.pumpAndSettle();

// Find the header and check if it is existent
final headerFinder = find.byType(Header);
expect(headerFinder, findsOneWidget);

// Timer logic: increase timer every second, rerun the base every 100 ms and check if the UI changed
int timer = 0;
const maxTime = 10;
int millisecondsCounter = 0;

final completer = Completer<void>();

expect(find.descendant(of: headerFinder, matching: find.text('+00:00')), findsOneWidget);

while (!completer.isCompleted) {
await tester.pumpAndSettle();

if (!find.descendant(of: headerFinder, matching: find.text('+00:00')).evaluate().isNotEmpty) {
expect(find.descendant(of: headerFinder, matching: find.text('+00:30')), findsOneWidget);
completer.complete();
break;
}

millisecondsCounter += 100;
if (millisecondsCounter % 1000 == 0) {
timer++;
}

if (timer > maxTime) {
completer
.completeError(Exception('UI did not change from the base value to the updated value (+00:00 -> +00:30)'));
break;
}

await Future.delayed(const Duration(milliseconds: 100));
}

await completer.future;

await tester.pumpAndSettle();
});
}
13 changes: 6 additions & 7 deletions das_client/integration_test/test/train_search_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import '../util/test_utils.dart';

void main() {
group('train search screen tests', () {

testWidgets('test default values', (tester) async {
// Load app widget.
await prepareAndStartApp(tester);
Expand Down Expand Up @@ -59,7 +58,6 @@ void main() {
// check that the primary button is disabled
final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first;
expect(tester.widget<SBBPrimaryButton>(primaryButton).onPressed, isNull);

});

testWidgets('test can select yesterday', (tester) async {
Expand All @@ -70,7 +68,8 @@ void main() {
final yesterday = today.add(Duration(days: -1));

final todayDateTextFinder = find.text(Format.date(today));
final yesterdayDateTextFinder = find.text('${Format.date(yesterday)} ${l10n.p_train_selection_date_not_today_warning}');
final yesterdayDateTextFinder =
find.text('${Format.date(yesterday)} ${l10n.p_train_selection_date_not_today_warning}');

// Verify that today is preselected
expect(todayDateTextFinder, findsOneWidget);
Expand All @@ -90,7 +89,6 @@ void main() {

expect(todayDateTextFinder, findsNothing);
expect(yesterdayDateTextFinder, findsOneWidget);

});

testWidgets('test can not select day before yesterday', (tester) async {
Expand All @@ -102,7 +100,8 @@ void main() {
final dayBeforeYesterday = today.add(Duration(days: -2));

final todayDateTextFinder = find.text(Format.date(today));
final yesterdayDateTextFinder = find.text('${Format.date(yesterday)} ${l10n.p_train_selection_date_not_today_warning}');
final yesterdayDateTextFinder =
find.text('${Format.date(yesterday)} ${l10n.p_train_selection_date_not_today_warning}');
final dayBeforeYesterdayDateTextFinder = find.text(Format.date(dayBeforeYesterday));

// Verify that today is preselected
Expand All @@ -114,7 +113,8 @@ void main() {
final sbbDatePickerFinder = find.byWidgetPredicate((widget) => widget is SBBDatePicker);
final yesterdayFinder = find.descendant(
of: sbbDatePickerFinder,
matching: find.byWidgetPredicate((widget) => widget is Text && widget.data == '${(dayBeforeYesterday.day)}.'));
matching:
find.byWidgetPredicate((widget) => widget is Text && widget.data == '${(dayBeforeYesterday.day)}.'));
await tapElement(tester, yesterdayFinder);

// tap outside dialog
Expand Down Expand Up @@ -149,6 +149,5 @@ void main() {

expect(find.text('${ErrorCode.sferaJpUnavailable.code}: ${l10n.c_error_sfera_jp_unavailable}'), findsOneWidget);
});

});
}
2 changes: 1 addition & 1 deletion das_client/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: d9dad56c0cd0b4fd8b4fe3034a53fd42a0b990f6

COCOAPODS: 1.16.1
COCOAPODS: 1.16.2
2 changes: 1 addition & 1 deletion das_client/l10n/strings_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"c_error_sfera_handshake_rejected": "Server hat die Verbindung abgelehnt",
"c_error_sfera_request_timeout": "Timeout bei der Anfrage",
"c_error_sfera_jp_unavailable": "Fahrordnung nicht vorhanden",
"c_error_sfera_sp_invalid": "Unvollständige Daten erhalten",
"c_error_sfera_invalid": "Unvollständige Daten erhalten",
"c_connection_track_weiche": "Weiche",
"c_button_confirm": "Übernehmen"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart';
import 'package:das_client/app/bloc/train_journey_cubit.dart';
import 'package:flutter/material.dart';
import 'package:das_client/model/journey/journey.dart';
import 'package:intl/intl.dart';
import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart';

class TimeContainer extends StatelessWidget {
const TimeContainer({super.key});
Expand All @@ -22,25 +25,56 @@ class TimeContainer extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'05:43:00',
style: SBBTextStyles.largeBold.copyWith(fontSize: 24.0),
),
Flexible(child: _currentTime()),
_divider(),
Text(
'+00:01:30',
style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0),
),
Flexible(child: _punctualityDisplay(context)),
],
),
),
);
}
}

Widget _divider() {
return const Padding(
padding: EdgeInsets.symmetric(vertical: sbbDefaultSpacing * 0.5),
child: Divider(height: 1.0, color: SBBColors.cloud),
);
}
Widget _divider() {
return const Padding(
padding: EdgeInsets.symmetric(vertical: sbbDefaultSpacing * 0.5),
child: Divider(height: 1.0, color: SBBColors.cloud),
);
}

Widget _punctualityDisplay(BuildContext context) {
final bloc = context.trainJourneyCubit;

return StreamBuilder<Journey?>(
stream: bloc.journeyStream,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null || snapshot.data!.metadata.delay == null) {
return Text('+00:00', style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0));
}

final Journey journey = snapshot.data!;
final Duration delay = journey.metadata.delay!;

final String minutes = NumberFormat('00').format(delay.inMinutes.abs() % 60);
final String seconds = NumberFormat('00').format(delay.inSeconds.abs() % 60);
tobe-official marked this conversation as resolved.
Show resolved Hide resolved
final String formattedDuration = '${delay.isNegative ? '-' : '+'}$minutes:$seconds';

return Text(
formattedDuration,
style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0),
);
},
);
}

StreamBuilder _currentTime() {
return StreamBuilder(
stream: Stream.periodic(const Duration(milliseconds: 200)),
builder: (context, snapshot) {
return Text(
DateFormat('HH:mm:ss').format(DateTime.now().toLocal()),
style: SBBTextStyles.largeBold.copyWith(fontSize: 24.0),
);
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class _TrainSelectionState extends State<TrainSelection> {
onChanged: (value) => context.trainJourneyCubit.updateTrainNumber(value),
controller: _trainNumberController,
labelText: context.l10n.p_train_selection_trainnumber_description,
keyboardType: TextInputType.number,
keyboardType: TextInputType.text,
),
);
}
Expand Down
2 changes: 2 additions & 0 deletions das_client/lib/model/journey/metadata.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Metadata {
this.currentPosition,
this.routeStart,
this.routeEnd,
this.delay,
this.breakSeries,
this.additionalSpeedRestrictions = const [],
this.nonStandardTrackEquipmentSegments = const [],
Expand All @@ -21,6 +22,7 @@ class Metadata {
final List<AdditionalSpeedRestriction> additionalSpeedRestrictions;
final BaseData? routeStart;
final BaseData? routeEnd;
final Duration? delay;
final List<NonStandardTrackEquipmentSegment> nonStandardTrackEquipmentSegments;
final BreakSeries? breakSeries;
final Set<BreakSeries> availableBreakSeries;
Expand Down
Loading
Loading