mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:27:14 +01:00
Speak formula names, fit sheet value
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c9c30b1ee5
commit
2dc502bbb6
@ -167,7 +167,13 @@ class _DatedValueCardState extends State<DatedValueCard> {
|
|||||||
if (latest == null)
|
if (latest == null)
|
||||||
Icon(Icons.block, size: 18, color: scheme.outline)
|
Icon(Icons.block, size: 18, color: scheme.outline)
|
||||||
else
|
else
|
||||||
Text('${_fmt(latest.$2)} ${latest.$1}'),
|
// Scales down instead of clipping on a narrow sheet.
|
||||||
|
Flexible(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text('${_fmt(latest.$2)} ${latest.$1}', maxLines: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/// A kcal formula shown as a glyph + number; its proper name is only a
|
/// A kcal formula shown as a glyph + number; its proper name is only the
|
||||||
/// long-press tooltip.
|
/// glyph's spoken label.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:diet_guard_app/services/activity_kcal.dart';
|
import 'package:diet_guard_app/services/activity_kcal.dart';
|
||||||
@ -13,7 +13,7 @@ const Map<String, IconData> formulaIcons = {
|
|||||||
'Per-km': Icons.straighten,
|
'Per-km': Icons.straighten,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The glyph for formula [name], tooltipped with the formula's proper name.
|
/// The glyph for formula [name], spoken as the formula's proper name.
|
||||||
class FormulaGlyph extends StatelessWidget {
|
class FormulaGlyph extends StatelessWidget {
|
||||||
/// Creates a glyph for [name].
|
/// Creates a glyph for [name].
|
||||||
const FormulaGlyph(this.name, {this.size = 16, super.key});
|
const FormulaGlyph(this.name, {this.size = 16, super.key});
|
||||||
@ -24,14 +24,13 @@ class FormulaGlyph extends StatelessWidget {
|
|||||||
/// Glyph size.
|
/// Glyph size.
|
||||||
final double size;
|
final double size;
|
||||||
|
|
||||||
|
// The proper name is spoken only: a tooltip paints on long-press.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Tooltip(
|
Widget build(BuildContext context) => Icon(
|
||||||
message: name,
|
formulaIcons[name],
|
||||||
child: Icon(
|
size: size,
|
||||||
formulaIcons[name],
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
size: size,
|
semanticLabel: name,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:diet_guard_app/services/body_energy.dart';
|
import 'package:diet_guard_app/services/body_energy.dart';
|
||||||
|
import 'package:diet_guard_app/widgets/body/formula_figure.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Lose = down arrow, maintain = level bar, gain = up arrow.
|
/// Lose = down arrow, maintain = level bar, gain = up arrow.
|
||||||
@ -20,24 +21,25 @@ IconData timeUnitIcon(String unit) => switch (unit) {
|
|||||||
_ => Icons.event_repeat,
|
_ => Icons.event_repeat,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The five levels ramp from a chair to a barbell; measured is a gauge.
|
/// The five levels ramp from a chair to a barbell; a measured choice wears
|
||||||
|
/// its formula's glyph (the same one the Activity sheet shows next to kcal).
|
||||||
IconData activityIcon(String key) => switch (key) {
|
IconData activityIcon(String key) => switch (key) {
|
||||||
'sedentary' => Icons.chair,
|
'sedentary' => Icons.chair,
|
||||||
'light' => Icons.directions_walk,
|
'light' => Icons.directions_walk,
|
||||||
'moderate' => Icons.directions_bike,
|
'moderate' => Icons.directions_bike,
|
||||||
'very' => Icons.directions_run,
|
'very' => Icons.directions_run,
|
||||||
'extra' => Icons.fitness_center,
|
'extra' => Icons.fitness_center,
|
||||||
|
_ when key.startsWith(measuredPrefix) =>
|
||||||
|
formulaIcons[key.substring(measuredPrefix.length)] ?? Icons.speed,
|
||||||
_ => Icons.speed,
|
_ => Icons.speed,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Short non-word caption under an activity icon: the TDEE multiplier of a
|
/// Numeric caption under an activity icon: the TDEE multiplier of a level.
|
||||||
/// level, or the proper name of the measured formula.
|
/// Null for a measured formula (its glyph names it; the proper name is only
|
||||||
String activityCaption(String key) {
|
/// spoken) and for an unknown key.
|
||||||
if (key.startsWith(measuredPrefix)) {
|
String? activityCaption(String key) {
|
||||||
return key.substring(measuredPrefix.length);
|
|
||||||
}
|
|
||||||
for (final level in activityLevels) {
|
for (final level in activityLevels) {
|
||||||
if (level.key == key) return formatG(level.factor);
|
if (level.key == key) return formatG(level.factor);
|
||||||
}
|
}
|
||||||
return key;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
90
app/test/no_words_body_test.dart
Normal file
90
app/test/no_words_body_test.dart
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// Guard: the Body screen, every tile sheet and the activity-method chooser
|
||||||
|
// paint no words (only numbers, unit symbols, MET and workout labels).
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'label_finders.dart';
|
||||||
|
import 'no_words_support.dart';
|
||||||
|
import 'screens/body_screen_support.dart';
|
||||||
|
|
||||||
|
/// Workout labels the PC published (content, see `seedPerson`), plus the
|
||||||
|
/// proper names of the BMI / ideal-weight / BMR formulas that label their
|
||||||
|
/// figures (DOUBT rows in keep.md: kept from the first pilot, outside the
|
||||||
|
/// activity sheet). Activity formulas (ACSM, Per-km) are NOT here: that sheet
|
||||||
|
/// and its chooser show glyphs only.
|
||||||
|
const Set<String> _workouts = {
|
||||||
|
'Evening run',
|
||||||
|
'StrongLifts A',
|
||||||
|
'Harris-Benedict (revised 1984)',
|
||||||
|
'Peterson (BMI 22)',
|
||||||
|
'Mifflin-St Jeor',
|
||||||
|
'Katch-McArdle',
|
||||||
|
'Cunningham',
|
||||||
|
'BMI Prime',
|
||||||
|
'Trefethen',
|
||||||
|
'Devine',
|
||||||
|
'Robinson',
|
||||||
|
'Miller',
|
||||||
|
};
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late MemoryDocStore store;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
store = MemoryDocStore();
|
||||||
|
await setUpBodyServices(store);
|
||||||
|
});
|
||||||
|
tearDown(tearDownBodyServices);
|
||||||
|
|
||||||
|
test('the checker flags an injected word (canary)', () {
|
||||||
|
expect(wordsIn('70.0 kg'), isEmpty);
|
||||||
|
expect(wordsIn('265 MET'), isEmpty);
|
||||||
|
expect(wordsIn('Measured (ACSM)'), isNotEmpty);
|
||||||
|
expect(wordsIn('Evening run 4 km', content: _workouts), ['km']);
|
||||||
|
expect(wordsIn('Evening run', content: _workouts), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('canary: a painted word fails the guard', (tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(home: Text('Activity')));
|
||||||
|
expect(() => expectNoWords(tester), throwsA(isA<TestFailure>()));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final seeded in [false, true]) {
|
||||||
|
testWidgets('body tiles and sheets paint no words (seeded: $seeded)', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
if (seeded) await seedPerson(store);
|
||||||
|
await pumpBody(tester);
|
||||||
|
final found = wordyStrings(
|
||||||
|
tester,
|
||||||
|
content: _workouts,
|
||||||
|
minStrings: 5,
|
||||||
|
where: 'tiles',
|
||||||
|
);
|
||||||
|
for (final title in tileTitles(tester)) {
|
||||||
|
await openTile(tester, title);
|
||||||
|
found.addAll(wordyStrings(tester, content: _workouts, where: title));
|
||||||
|
await closeSheet(tester);
|
||||||
|
}
|
||||||
|
expect(found, isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('the activity-method chooser paints no words', (tester) async {
|
||||||
|
await seedPerson(store);
|
||||||
|
await pumpBody(tester);
|
||||||
|
await openTile(tester, 'Daily calorie target');
|
||||||
|
await tester.tap(bySemLabel('Moderate').first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expectNoWords(
|
||||||
|
tester,
|
||||||
|
content: _workouts,
|
||||||
|
minStrings: 8,
|
||||||
|
where: 'activity chooser',
|
||||||
|
);
|
||||||
|
await tester.tap(bySemLabel('Measured (ACSM)').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expectNoWords(tester, content: _workouts, where: 'measured goal');
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../../label_finders.dart';
|
||||||
|
|
||||||
Future<void> _pumpAt(WidgetTester tester, Size dp, Widget child) async {
|
Future<void> _pumpAt(WidgetTester tester, Size dp, Widget child) async {
|
||||||
tester.view.physicalSize = dp;
|
tester.view.physicalSize = dp;
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
@ -35,9 +37,7 @@ bool _paintsFormulaWord(Widget w) =>
|
|||||||
].any((word) => (w.data ?? '').contains(word));
|
].any((word) => (w.data ?? '').contains(word));
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('activity formulas are icons; names only in tooltips', (
|
testWidgets('activity formulas are icons; names only spoken', (tester) async {
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await _pumpAt(
|
await _pumpAt(
|
||||||
tester,
|
tester,
|
||||||
const Size(360, 2400),
|
const Size(360, 2400),
|
||||||
@ -48,34 +48,19 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(find.byWidgetPredicate(_paintsFormulaWord), findsNothing);
|
expect(find.byWidgetPredicate(_paintsFormulaWord), findsNothing);
|
||||||
|
// A tooltip paints on long-press: none may carry a formula name.
|
||||||
|
expect(find.byType(Tooltip), findsNothing);
|
||||||
for (final name in ['MET', 'ACSM', 'Per-km']) {
|
for (final name in ['MET', 'ACSM', 'Per-km']) {
|
||||||
// One in the mean row, one in the session tile.
|
// One in the mean row, one in the session tile.
|
||||||
expect(find.byTooltip(name), findsNWidgets(2));
|
expect(byIconLabel(name), findsNWidgets(2));
|
||||||
}
|
}
|
||||||
expect(find.text('300'), findsOneWidget);
|
expect(find.text('300'), findsOneWidget);
|
||||||
expect(find.text('310'), findsOneWidget);
|
expect(find.text('310'), findsOneWidget);
|
||||||
expect(find.text('320'), findsOneWidget);
|
expect(find.text('320'), findsOneWidget);
|
||||||
// Distinct glyphs, so the three are told apart without words.
|
// Distinct glyphs, so the three are told apart without words.
|
||||||
final icons = tester
|
|
||||||
.widgetList<Icon>(
|
|
||||||
find.descendant(
|
|
||||||
of: find.byTooltip('MET').first,
|
|
||||||
matching: find.byType(Icon),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.map((i) => i.icon)
|
|
||||||
.toSet();
|
|
||||||
expect(icons, hasLength(1));
|
|
||||||
final glyphs = {
|
final glyphs = {
|
||||||
for (final name in ['MET', 'ACSM', 'Per-km'])
|
for (final name in ['MET', 'ACSM', 'Per-km'])
|
||||||
(tester
|
tester.widget<Icon>(byIconLabel(name).first).icon,
|
||||||
.widget<Icon>(
|
|
||||||
find.descendant(
|
|
||||||
of: find.byTooltip(name).first,
|
|
||||||
matching: find.byType(Icon),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.icon),
|
|
||||||
};
|
};
|
||||||
expect(glyphs, hasLength(3));
|
expect(glyphs, hasLength(3));
|
||||||
});
|
});
|
||||||
@ -98,11 +83,12 @@ void main() {
|
|||||||
expect(find.byWidgetPredicate(_paintsFormulaWord), findsNothing);
|
expect(find.byWidgetPredicate(_paintsFormulaWord), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (final width in [360.0, 411.0]) {
|
// A 360 dp phone, the Pixel's 411 dp, and a 4K web window.
|
||||||
testWidgets('a weight tile value fits whole at $width dp', (tester) async {
|
for (final size in const [Size(360, 800), Size(411, 800), Size(3840, 2160)]) {
|
||||||
|
testWidgets('a weight tile value fits whole at $size', (tester) async {
|
||||||
await _pumpAt(
|
await _pumpAt(
|
||||||
tester,
|
tester,
|
||||||
Size(width, 800),
|
size,
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: BodyTileGrid(
|
child: BodyTileGrid(
|
||||||
@ -130,11 +116,14 @@ void main() {
|
|||||||
expect(paragraph.didExceedMaxLines, isFalse, reason: value);
|
expect(paragraph.didExceedMaxLines, isFalse, reason: value);
|
||||||
final card = find.ancestor(of: text, matching: find.byType(Card));
|
final card = find.ancestor(of: text, matching: find.byType(Card));
|
||||||
final inside = tester.getRect(card).inflate(0.5);
|
final inside = tester.getRect(card).inflate(0.5);
|
||||||
expect(inside.contains(tester.getRect(text).topLeft), isTrue);
|
// The painted rect (after FittedBox scaling) sits inside the tile.
|
||||||
expect(inside.contains(tester.getRect(text).bottomRight), isTrue);
|
final painted = tester.getRect(text);
|
||||||
|
expect(inside.contains(painted.topLeft), isTrue, reason: value);
|
||||||
|
expect(inside.contains(painted.bottomRight), isTrue, reason: value);
|
||||||
|
expect(painted.width, lessThanOrEqualTo(inside.width), reason: value);
|
||||||
// The full text is laid out (no ellipsis clipping): its width is
|
// The full text is laid out (no ellipsis clipping): its width is
|
||||||
// the natural width of "70.0 kg" at this font.
|
// the natural width of "70.0 kg" at this font.
|
||||||
expect(tester.getSize(text).width, greaterThan(40));
|
expect(paragraph.size.width, greaterThan(40), reason: value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user