Merge branch 'train-days' into integration
# Conflicts: # apps/train/halkyon_train_flutter/lib/src/app/train_paths.dart # docs/adr/0030-halkyon-train.md
This commit is contained in:
commit
81adeb91cc
7 changed files with 25705 additions and 410 deletions
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/model/day.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/widget/night_chart.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets("a phase starting at the night's end or after it paints "
|
||||
'nothing out of its row rather than throwing mid-paint, which left '
|
||||
'the rows and the charts below drawn out of place (3 Nov 2026)', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 320,
|
||||
child: NightChart(
|
||||
phases: [
|
||||
(Duration.zero, SleepState.light),
|
||||
(Duration(hours: 2), SleepState.rem),
|
||||
(Duration(hours: 3), SleepState.deep),
|
||||
// Polar's last phase, at the night's very end, and one past
|
||||
// it.
|
||||
(Duration(hours: 4), SleepState.wake),
|
||||
(Duration(hours: 5), SleepState.rem),
|
||||
],
|
||||
length: Duration(hours: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/model/day.dart';
|
||||
|
||||
void main() {
|
||||
test("Polar's marks for no value (-1, -100, 0) are no value, the "
|
||||
"person's own figures kept", () {
|
||||
const fromBand = Recharge(
|
||||
ansStatus: -100,
|
||||
recovery: -1,
|
||||
recoverySubLevel: -1,
|
||||
rri: 1000,
|
||||
rmssd: 69,
|
||||
breathInterval: 4380,
|
||||
baselineRri: 0,
|
||||
baselineRmssd: -1,
|
||||
);
|
||||
expect(
|
||||
fromBand.known,
|
||||
const Recharge(rri: 1000, rmssd: 69, breathInterval: 4380),
|
||||
);
|
||||
});
|
||||
|
||||
test('figures in their ranges stay as they are', () {
|
||||
const fromBand = Recharge(
|
||||
ansStatus: -2.5,
|
||||
recovery: 4,
|
||||
recoverySubLevel: 2,
|
||||
rmssd: 48,
|
||||
baselineRmssd: 45,
|
||||
);
|
||||
expect(fromBand.known, fromBand);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_train_flutter/src/core/privacy/train_keys.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_connect.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_platform.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_store.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/weight/data/weight_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/weight/model/weight.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/data/workout_repository.dart';
|
||||
|
||||
import '../../support/fake_health_platform.dart';
|
||||
import '../../support/memory_content_key_gateway.dart';
|
||||
import '../../support/memory_weight_gateway.dart';
|
||||
import '../../support/memory_workout_gateway.dart';
|
||||
|
||||
/// Other apps' weights brought in from Health Connect (ADR 0030, D64): off
|
||||
/// until the person turns it on, then each weight once, as a weight of
|
||||
/// Train's own store; one the person entered in Train already is a double
|
||||
/// and stays out, as does one deleted in Train.
|
||||
void main() {
|
||||
final morning = DateTime.utc(2026, 11, 3, 6, 30);
|
||||
const scale = 'com.example.scale';
|
||||
|
||||
late Directory folder;
|
||||
late TrainKeys keys;
|
||||
late WorkoutRepository workouts;
|
||||
late WeightRepository weights;
|
||||
late FakeHealthPlatform platform;
|
||||
final opened = <HealthConnect>[];
|
||||
|
||||
HealthWeight measured(String id, double kilograms, {int days = 0}) =>
|
||||
HealthWeight(
|
||||
id: id,
|
||||
at: morning.add(Duration(days: days)),
|
||||
kilograms: kilograms,
|
||||
app: scale,
|
||||
);
|
||||
|
||||
HealthConnect open() {
|
||||
final health = HealthConnect(
|
||||
platform,
|
||||
HealthStore(File('${folder.path}/health_connect.json')),
|
||||
workouts,
|
||||
weights: weights,
|
||||
)..start();
|
||||
opened.add(health);
|
||||
return health;
|
||||
}
|
||||
|
||||
Future<void> settled(HealthConnect health) async {
|
||||
await pumpEventQueue();
|
||||
await health.idle;
|
||||
}
|
||||
|
||||
Future<List<WeightContent>> kept() async => [
|
||||
for (final weight in await weights.all()) weight.content!,
|
||||
];
|
||||
|
||||
setUp(() {
|
||||
folder = Directory.systemTemp.createTempSync('health');
|
||||
keys = TrainKeys(
|
||||
MemoryContentKeyGateway(),
|
||||
FakeContentCipher(),
|
||||
() async => FakeAccountCrypto().keysNumbered(1),
|
||||
);
|
||||
workouts = WorkoutRepository(MemoryWorkoutGateway(), keys);
|
||||
weights = WeightRepository(MemoryWeightGateway(), keys);
|
||||
platform = FakeHealthPlatform();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
for (final health in opened) {
|
||||
await health.dispose();
|
||||
}
|
||||
opened.clear();
|
||||
await workouts.dispose();
|
||||
await weights.dispose();
|
||||
keys.dispose();
|
||||
folder.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('nothing is read until the person turns it on, which asks Health '
|
||||
'Connect for the weights alone', () async {
|
||||
platform.otherWeights.add(measured('a', 74.3));
|
||||
final health = open();
|
||||
await settled(health);
|
||||
expect(await weights.all(), isEmpty);
|
||||
expect(platform.asked, isEmpty);
|
||||
|
||||
expect(await health.setReadingWeights(on: true), isTrue);
|
||||
await settled(health);
|
||||
expect(platform.asked, [
|
||||
{HealthAccess.weight},
|
||||
]);
|
||||
expect(await kept(), [
|
||||
WeightContent(
|
||||
at: morning,
|
||||
kilograms: 74.3,
|
||||
source: WeightSource.healthConnect,
|
||||
healthId: 'a',
|
||||
app: scale,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('each weight comes in once, read again or not', () async {
|
||||
platform.otherWeights.add(measured('a', 74.3));
|
||||
final health = open();
|
||||
await health.setReadingWeights(on: true);
|
||||
await settled(health);
|
||||
platform.otherWeights.add(measured('b', 74.1, days: 1));
|
||||
await health.read();
|
||||
await settled(health);
|
||||
await health.read();
|
||||
await settled(health);
|
||||
|
||||
expect([for (final weight in await kept()) weight.healthId], ['a', 'b']);
|
||||
});
|
||||
|
||||
test('one the person entered in Train already, within ten minutes and a '
|
||||
'tenth of a kilogram, stays out', () async {
|
||||
await weights.save(
|
||||
WeightContent(
|
||||
at: morning.add(const Duration(minutes: 4)),
|
||||
kilograms: 74.35,
|
||||
),
|
||||
);
|
||||
platform.otherWeights
|
||||
..add(measured('double', 74.3))
|
||||
..add(measured('other', 74.9, days: 1));
|
||||
final health = open();
|
||||
await health.setReadingWeights(on: true);
|
||||
await settled(health);
|
||||
|
||||
expect(
|
||||
[for (final weight in await kept()) weight.healthId],
|
||||
[null, 'other'],
|
||||
);
|
||||
});
|
||||
|
||||
test('one deleted in Train stays out', () async {
|
||||
platform.otherWeights.add(measured('a', 74.3));
|
||||
final health = open();
|
||||
await health.setReadingWeights(on: true);
|
||||
await settled(health);
|
||||
await weights.delete((await weights.all()).single.id);
|
||||
|
||||
await health.read();
|
||||
await settled(health);
|
||||
expect(await weights.all(), isEmpty);
|
||||
});
|
||||
|
||||
test('refused, it stays off and says so; turned off, nothing more '
|
||||
'comes', () async {
|
||||
platform = FakeHealthPlatform(grants: {});
|
||||
platform.otherWeights.add(measured('a', 74.3));
|
||||
final health = open();
|
||||
|
||||
expect(await health.setReadingWeights(on: true), isFalse);
|
||||
expect(health.settings.readsWeights, isFalse);
|
||||
expect(health.settings.weightsRefused, isTrue);
|
||||
expect(await weights.all(), isEmpty);
|
||||
});
|
||||
|
||||
test("the switch and what came in are kept with the phone's settings", () {
|
||||
final store = HealthStore(File('${folder.path}/health_connect.json'));
|
||||
const settings = HealthSettings(
|
||||
readsWeights: true,
|
||||
weightsRefused: true,
|
||||
takenWeights: {'a', 'b'},
|
||||
);
|
||||
store.save(settings);
|
||||
expect(store.read(), settings);
|
||||
});
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import 'package:halkyon_train_flutter/src/feature/music/data/music_downloads.dar
|
|||
import 'package:halkyon_train_flutter/src/feature/music/data/music_library.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/music/data/music_store.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/music/widget/music_screen.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../support/fake_jellyfin.dart';
|
||||
|
|
@ -34,7 +35,10 @@ void main() {
|
|||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
localizationsDelegates: const [
|
||||
...AppLocalizations.localizationsDelegates,
|
||||
HalkyonUiLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: const Locale('en'),
|
||||
home: BlocProvider(
|
||||
|
|
@ -112,7 +116,10 @@ void main() {
|
|||
addTearDown(kept.dispose);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
localizationsDelegates: const [
|
||||
...AppLocalizations.localizationsDelegates,
|
||||
HalkyonUiLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: const Locale('en'),
|
||||
home: BlocProvider(
|
||||
|
|
@ -217,7 +224,10 @@ void main() {
|
|||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
localizationsDelegates: const [
|
||||
...AppLocalizations.localizationsDelegates,
|
||||
HalkyonUiLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: const Locale('en'),
|
||||
home: BlocProvider(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_flutter/src/core/privacy/train_keys.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/weight/data/weight_payload.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/weight/data/weight_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/weight/model/weight.dart';
|
||||
|
||||
import '../../support/memory_content_key_gateway.dart';
|
||||
import '../../support/memory_weight_gateway.dart';
|
||||
|
||||
/// The person's weights, sealed on the way to the instance and opened on
|
||||
/// the way back (ADR 0030, D7, D64), with libsodium.
|
||||
void main() {
|
||||
final crypto = SodiumAccountCrypto();
|
||||
late UnlockedAccountKeys alice;
|
||||
late UnlockedAccountKeys bob;
|
||||
late MemoryContentKeyGateway keyRows;
|
||||
late MemoryWeightGateway rows;
|
||||
final opened = <TrainKeys>[];
|
||||
|
||||
final monday = WeightContent(
|
||||
at: DateTime.utc(2026, 11, 2, 7),
|
||||
kilograms: 74.3,
|
||||
);
|
||||
final tuesday = WeightContent(
|
||||
at: DateTime.utc(2026, 11, 3, 7),
|
||||
kilograms: 74.05,
|
||||
source: WeightSource.healthConnect,
|
||||
healthId: 'hc-1',
|
||||
app: 'com.example.scale',
|
||||
);
|
||||
|
||||
Future<WeightRepository> device(UnlockedAccountKeys? account) async {
|
||||
final keys = TrainKeys(keyRows, SodiumContentCipher(), () async => account);
|
||||
opened.add(keys);
|
||||
await keys.refresh();
|
||||
return WeightRepository(rows, keys);
|
||||
}
|
||||
|
||||
setUpAll(() async {
|
||||
alice = (await crypto.newAccount('alice password')).keys;
|
||||
bob = (await crypto.newAccount('bob password')).keys;
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
keyRows = MemoryContentKeyGateway();
|
||||
rows = MemoryWeightGateway();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
for (final keys in opened) {
|
||||
keys.dispose();
|
||||
}
|
||||
opened.clear();
|
||||
});
|
||||
|
||||
test('weights entered on one device read on another of the account, the '
|
||||
'earliest first, and are deleted there', () async {
|
||||
final phone = await device(alice);
|
||||
final second = await phone.save(tuesday);
|
||||
final first = await phone.save(monday);
|
||||
|
||||
final laptop = await device(alice);
|
||||
expect(await laptop.all(), [
|
||||
StoredWeight(first, monday),
|
||||
StoredWeight(second, tuesday),
|
||||
]);
|
||||
|
||||
await laptop.delete(first);
|
||||
expect(await phone.all(), [StoredWeight(second, tuesday)]);
|
||||
});
|
||||
|
||||
test('nothing the instance gets is readable: no weight, no JSON', () async {
|
||||
await (await device(alice)).save(tuesday);
|
||||
|
||||
final sealed = rows.rows.values.single;
|
||||
expect(sealed.keyId, isNotNull);
|
||||
final text = utf8.decode(
|
||||
Uint8List.sublistView(sealed.payload),
|
||||
allowMalformed: true,
|
||||
);
|
||||
expect(text, isNot(contains('74050')));
|
||||
expect(text, isNot(contains('scale')));
|
||||
});
|
||||
|
||||
test("another person's weight, a readable one, or one moved to another "
|
||||
'row shows locked, never as theirs', () async {
|
||||
final id = await (await device(alice)).save(monday);
|
||||
rows.rows
|
||||
..['moved'] = rows.rows[id]!
|
||||
..['readable'] = (
|
||||
keyId: null,
|
||||
nonce: null,
|
||||
payload: encodeWeight(monday),
|
||||
);
|
||||
|
||||
expect(
|
||||
(await (await device(bob)).all()).where((w) => w.content != null),
|
||||
isEmpty,
|
||||
);
|
||||
expect(
|
||||
{for (final w in await (await device(alice)).all()) w.id: w.content},
|
||||
{id: monday, 'moved': null, 'readable': null},
|
||||
);
|
||||
});
|
||||
|
||||
test('a device without the keys reads nothing and saves nothing', () async {
|
||||
await (await device(alice)).save(monday);
|
||||
final stranger = await device(null);
|
||||
|
||||
expect((await stranger.all()).single.content, isNull);
|
||||
await expectLater(stranger.save(monday), throwsA(isA<KeysMissing>()));
|
||||
});
|
||||
|
||||
group('the payload', () {
|
||||
test('keeps the weight to the gram, its time in UTC, and where it came '
|
||||
'from', () {
|
||||
expect(decodeWeight(encodeWeight(tuesday)), tuesday);
|
||||
expect(decodeWeight(encodeWeight(monday)), monday);
|
||||
final local = WeightContent(
|
||||
at: DateTime(2026, 11, 3, 8),
|
||||
kilograms: 80.0004,
|
||||
);
|
||||
expect(
|
||||
decodeWeight(encodeWeight(local)),
|
||||
WeightContent(at: local.at.toUtc(), kilograms: 80),
|
||||
);
|
||||
});
|
||||
|
||||
test('one of a newer schema, or none at all, reads as nothing', () {
|
||||
ByteData of(String json) =>
|
||||
ByteData.sublistView(Uint8List.fromList(utf8.encode(json)));
|
||||
expect(
|
||||
decodeWeight(
|
||||
of('{"schemaVersion":2,"at":"2026-11-03T07:00:00Z","grams":1}'),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(decodeWeight(of('{"schemaVersion":1}')), isNull);
|
||||
expect(
|
||||
decodeWeight(
|
||||
of('{"schemaVersion":1,"at":"2026-11-03T07:00:00Z","grams":0}'),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
import 'package:halkyon_train_flutter/src/core/privacy/train_keys.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/profile/data/profile_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/profile/model/profile.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/weight/bloc/weights_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/weight/data/weight_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/weight/model/weight.dart';
|
||||
|
||||
import '../../support/memory_content_key_gateway.dart';
|
||||
import '../../support/memory_profile_gateway.dart';
|
||||
import '../../support/memory_weight_gateway.dart';
|
||||
|
||||
void main() {
|
||||
late TrainKeys keys;
|
||||
late WeightRepository weights;
|
||||
late ProfileRepository profile;
|
||||
late WeightsCubit cubit;
|
||||
|
||||
setUp(() async {
|
||||
keys = TrainKeys(
|
||||
MemoryContentKeyGateway(),
|
||||
FakeContentCipher(),
|
||||
() async => FakeAccountCrypto().keysNumbered(1),
|
||||
);
|
||||
weights = WeightRepository(MemoryWeightGateway(), keys);
|
||||
profile = ProfileRepository(MemoryProfileGateway(), keys, person: 'alice');
|
||||
await profile.save(const ProfileContent(heightCm: 180, weightKg: 76));
|
||||
cubit = WeightsCubit(weights, profile);
|
||||
await cubit.open();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await cubit.close();
|
||||
await weights.dispose();
|
||||
await profile.dispose();
|
||||
keys.dispose();
|
||||
});
|
||||
|
||||
test('a weight entered shows, the earliest first, and the latest sets '
|
||||
"Your body's weight, the other figures kept", () async {
|
||||
final monday = DateTime.utc(2026, 11, 2, 7);
|
||||
await cubit.add(75.2, monday.add(const Duration(days: 1)));
|
||||
await pumpEventQueue();
|
||||
expect(
|
||||
await profile.read(),
|
||||
const ProfileContent(heightCm: 180, weightKg: 75.2),
|
||||
);
|
||||
|
||||
// An earlier one, entered late, changes nothing of the profile.
|
||||
await cubit.add(75.8, monday);
|
||||
await pumpEventQueue();
|
||||
expect(
|
||||
[for (final weight in cubit.state.weights) weight.content!.kilograms],
|
||||
[75.8, 75.2],
|
||||
);
|
||||
expect(cubit.state.latest?.kilograms, 75.2);
|
||||
expect((await profile.read())?.weightKg, 75.2);
|
||||
expect(cubit.state.weights.first.content!.source, WeightSource.own);
|
||||
});
|
||||
|
||||
test('a weight deleted goes', () async {
|
||||
await cubit.add(75.2, DateTime.utc(2026, 11, 3, 7));
|
||||
await pumpEventQueue();
|
||||
await cubit.delete(cubit.state.weights.single.id);
|
||||
await pumpEventQueue();
|
||||
expect(cubit.state.weights, isEmpty);
|
||||
expect(cubit.state.loaded, isTrue);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/widget/workout_chart.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
/// A heart rate from 60 at second 0 to 120 at second 100, in steps of 10
|
||||
/// a second.
|
||||
Future<void> pumpChart(WidgetTester tester) => tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SizedBox(
|
||||
width: 400,
|
||||
child: WorkoutChart(
|
||||
title: 'Heart rate',
|
||||
points: [
|
||||
for (var second = 0; second <= 100; second += 10)
|
||||
(x: second.toDouble(), value: 60 + second * 0.6),
|
||||
],
|
||||
label: (value) => '${value.round()} bpm',
|
||||
xLabel: (x) => 'at ${x.round()} s',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('held, the value and its moment under the finger show beside '
|
||||
'the title, following the finger, and go when it lifts', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpChart(tester);
|
||||
final plot = find.byKey(WorkoutChart.plotKey);
|
||||
final box = tester.getRect(plot);
|
||||
expect(find.text('at 0 s'), findsNothing);
|
||||
|
||||
// Held at the plot's start: the first point.
|
||||
final gesture = await tester.startGesture(
|
||||
box.centerLeft + const Offset(1, 0),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
expect(find.text('60 bpm · at 0 s'), findsOneWidget);
|
||||
|
||||
// Dragged to the middle: second 50, 90 bpm.
|
||||
await gesture.moveTo(box.center);
|
||||
await tester.pump();
|
||||
expect(find.text('90 bpm · at 50 s'), findsOneWidget);
|
||||
expect(find.text('60 bpm · at 0 s'), findsNothing);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
expect(find.textContaining(' · at '), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a tap shows nothing: only a long press inspects', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpChart(tester);
|
||||
await tester.tap(find.byKey(WorkoutChart.plotKey));
|
||||
await tester.pump();
|
||||
expect(find.textContaining(' · at '), findsNothing);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue