feat(learn,app): explanations wait behind a question mark beside their title

What explains a setting or a section, rather than what is the person's
own, moves behind halkyon_ui's HelpButton, beside its title through the
new ExplainedTitle (the developer's review of 3 Nov 2026): in Learn's
settings (citations, the Pomodoro timer, grammar, the web search,
Jellyfin, the device's dictionaries, counting one's learning), the study
settings, export's progress, the Jellyfin tags and the study button's
back first; in halkyon_app, setting up a password, adding SimpleLogin,
own keys not allowed, the profile page's link back and the operator's
grant sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 22:24:39 +01:00
commit 830aea10fe
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
24 changed files with 279 additions and 115 deletions

View file

@ -33,9 +33,10 @@ class const AssistantSettingsCard({super.key}) extends StatelessWidget {
), ),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(l10n.settingsAskAssistant), title: ExplainedTitle(
subtitle: Text(l10n.settingsAskAssistantSubtitle), l10n.settingsAskAssistant,
isThreeLine: true, help: l10n.settingsAskAssistantSubtitle,
),
value: on, value: on,
onChanged: (on) => context.read<DeviceSettingsBloc>().add( onChanged: (on) => context.read<DeviceSettingsBloc>().add(
DeviceSettingsEventAssistantChosen(on), DeviceSettingsEventAssistantChosen(on),

View file

@ -186,15 +186,14 @@ class const _SettingsCard() extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( ExplainedTitle(
l10n.settingsDeviceDictionaryTitle, l10n.settingsDeviceDictionaryTitle,
help: l10n.settingsDeviceDictionaryAbout,
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 8),
Text(l10n.settingsDeviceDictionaryAbout),
const SizedBox(height: 12),
if (dictionaries.progress case final progress?) if (dictionaries.progress case final progress?)
DeviceDictionaryProgressView(progress: progress) DeviceDictionaryProgressView(progress: progress)
else if (installed != null) else if (installed != null)

View file

@ -256,8 +256,10 @@ class _ExportSheetState() extends State<_ExportSheet> {
value: _withProgress, value: _withProgress,
onChanged: (value) => onChanged: (value) =>
setState(() => _withProgress = value ?? _withProgress), setState(() => _withProgress = value ?? _withProgress),
title: Text(l10n.exportWithProgress), title: ExplainedTitle(
subtitle: Text(l10n.exportWithProgressSubtitle), l10n.exportWithProgress,
help: l10n.exportWithProgressSubtitle,
),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
), ),

View file

@ -159,15 +159,16 @@ class _JellyfinBrowserScreenState() extends State<JellyfinBrowserScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text(l10n.jellyfinSubtitle), ExplainedTitle(
const SizedBox(height: 4), l10n.jellyfinTitle,
Text( help:
l10n.jellyfinPrivacy, '${l10n.jellyfinSubtitle}\n\n'
style: theme.textTheme.bodySmall?.copyWith( '${l10n.jellyfinPrivacy}',
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 8),
const JellyfinConnectForm(), const JellyfinConnectForm(),
], ],
), ),

View file

@ -14,22 +14,14 @@ class const JellyfinSettingsCard({super.key}) extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( ExplainedTitle(
l10n.jellyfinTitle, l10n.jellyfinTitle,
help: '${l10n.jellyfinSubtitle}\n\n${l10n.jellyfinPrivacy}',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 8),
Text(l10n.jellyfinSubtitle),
const SizedBox(height: 4),
Text(
l10n.jellyfinPrivacy,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
const JellyfinAccountView(), const JellyfinAccountView(),
], ],
), ),

View file

@ -30,20 +30,14 @@ class const CitationSettingsCard({super.key}) extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Text( ExplainedTitle(
l10n.settingsCitations, l10n.settingsCitations,
help: l10n.settingsCitationsDescription,
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 8),
Text(
l10n.settingsCitationsDescription,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
DropdownButtonFormField<CitationStyle>( DropdownButtonFormField<CitationStyle>(
initialValue: style, initialValue: style,
// A style's name is cut short rather than overflowing a phone. // A style's name is cut short rather than overflowing a phone.

View file

@ -177,9 +177,10 @@ class const _PrivacyCard() extends StatelessWidget {
), ),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(l10n.settingsCountLearning), title: ExplainedTitle(
subtitle: Text(l10n.settingsCountLearningSubtitle), l10n.settingsCountLearning,
isThreeLine: true, help: l10n.settingsCountLearningSubtitle,
),
value: counted ?? false, value: counted ?? false,
onChanged: counted == null onChanged: counted == null
? null ? null
@ -221,8 +222,10 @@ class const _StudyCard() extends StatelessWidget {
), ),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(l10n.settingsStudyPomodoroTitle), title: ExplainedTitle(
subtitle: Text(l10n.settingsStudyPomodoroSubtitle), l10n.settingsStudyPomodoroTitle,
help: l10n.settingsStudyPomodoroSubtitle,
),
value: context.select<DeviceSettingsBloc, bool>( value: context.select<DeviceSettingsBloc, bool>(
(bloc) => bloc.state.pomodoroWhileStudying, (bloc) => bloc.state.pomodoroWhileStudying,
), ),
@ -270,9 +273,10 @@ class _WritingCardState() extends State<_WritingCard> {
), ),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(l10n.settingsGrammarTitle), title: ExplainedTitle(
subtitle: Text(l10n.settingsGrammarSubtitle), l10n.settingsGrammarTitle,
isThreeLine: true, help: l10n.settingsGrammarSubtitle,
),
value: context.select<DeviceSettingsBloc, bool>( value: context.select<DeviceSettingsBloc, bool>(
(bloc) => bloc.state.checkGrammar, (bloc) => bloc.state.checkGrammar,
), ),

View file

@ -346,8 +346,10 @@ class _StudyButtonDialogState() extends State<_StudyButtonDialog> {
), ),
SwitchListTile( SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(l10n.studyButtonBackFirst), title: ExplainedTitle(
subtitle: Text(l10n.studyButtonBackFirstHelper), l10n.studyButtonBackFirst,
help: l10n.studyButtonBackFirstHelper,
),
value: _backFirst, value: _backFirst,
onChanged: (value) => onChanged: (value) =>
setState(() => _backFirst = value), setState(() => _backFirst = value),

View file

@ -240,14 +240,16 @@ class const _Options({required final StudyOptions options})
value: mode, value: mode,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
dense: true, dense: true,
title: Text(switch (mode) { title: switch (mode) {
LearnMode.spaced => l10n.studyModeSpaced, LearnMode.spaced => ExplainedTitle(
LearnMode.cram => l10n.studyModeCram, l10n.studyModeSpaced,
}), help: l10n.studyModeSpacedHint,
subtitle: Text(switch (mode) { ),
LearnMode.spaced => l10n.studyModeSpacedHint, LearnMode.cram => ExplainedTitle(
LearnMode.cram => l10n.studyModeCramHint, l10n.studyModeCram,
}), help: l10n.studyModeCramHint,
),
},
), ),
], ],
), ),
@ -265,8 +267,12 @@ class const _Options({required final StudyOptions options})
), ),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
dense: true, dense: true,
title: Text(l10n.studySettingsQuizMc), title: ExplainedTitle(
subtitle: Text(l10n.studySettingsQuizMcDescription), l10n.studySettingsQuizMc,
help:
'${l10n.studySettingsQuizMcDescription}\n\n'
'${l10n.studySettingsAspectDescription}',
),
), ),
if (options.quizVocabulary) ...[ if (options.quizVocabulary) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
@ -289,13 +295,6 @@ class const _Options({required final StudyOptions options})
), ),
], ],
), ),
const SizedBox(height: 4),
Text(
l10n.studySettingsAspectDescription,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
], ],
SwitchListTile( SwitchListTile(
value: options.typeReadings, value: options.typeReadings,
@ -307,8 +306,10 @@ class const _Options({required final StudyOptions options})
), ),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
dense: true, dense: true,
title: Text(l10n.studySettingsTypeReading), title: ExplainedTitle(
subtitle: Text(l10n.studySettingsTypeReadingDescription), l10n.studySettingsTypeReading,
help: l10n.studySettingsTypeReadingDescription,
),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text(l10n.studySettingsCharacters, style: heading), Text(l10n.studySettingsCharacters, style: heading),
@ -318,8 +319,10 @@ class const _Options({required final StudyOptions options})
onChanged: (write) => change(options.copyWith(writeByHand: write)), onChanged: (write) => change(options.copyWith(writeByHand: write)),
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
dense: true, dense: true,
title: Text(l10n.studySettingsWriteByHand), title: ExplainedTitle(
subtitle: Text(l10n.studySettingsWriteByHandDescription), l10n.studySettingsWriteByHand,
help: l10n.studySettingsWriteByHandDescription,
),
), ),
], ],
); );

View file

@ -106,8 +106,10 @@ class const _HideJellyfinTags() extends StatelessWidget {
final l10n = AppLocalizations.of(context); final l10n = AppLocalizations.of(context);
return SwitchListTile( return SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(l10n.tagManageHideJellyfin), title: ExplainedTitle(
subtitle: Text(l10n.tagManageHideJellyfinHint), l10n.tagManageHideJellyfin,
help: l10n.tagManageHideJellyfinHint,
),
value: context.watch<JellyfinTagsBloc>().state ?? false, value: context.watch<JellyfinTagsBloc>().state ?? false,
onChanged: (hidden) => context.read<JellyfinTagsBloc>().add( onChanged: (hidden) => context.read<JellyfinTagsBloc>().add(
JellyfinTagsEventHidingChosen(hidden), JellyfinTagsEventHidingChosen(hidden),

View file

@ -419,6 +419,14 @@ void main() {
expect(copied, 'https://halkyon.eu/@raisondetredev'); expect(copied, 'https://halkyon.eu/@raisondetredev');
expect(find.text('Address copied.'), findsOneWidget); expect(find.text('Address copied.'), findsOneWidget);
// What linking back does, behind its question mark (decided 3 Nov
// 2026).
final checkmark = find.textContaining('gets a checkmark');
expect(checkmark, findsNothing);
await tester.tap(find.byTooltip("What's this?").last);
await tester.pumpAndSettle();
expect(checkmark, findsOneWidget);
}); });
testWidgets('a link saved is checked by itself, and gets a checkmark once ' testWidgets('a link saved is checked by itself, and gets a checkmark once '

View file

@ -206,6 +206,38 @@ void main() {
expect(brightnessAt(tester, find.text('Appearance')), Brightness.light); expect(brightnessAt(tester, find.text('Appearance')), Brightness.light);
}); });
testWidgets("every setting's explanation waits behind the question mark "
"beside its title, its values in view (the developer's review of 3 Nov "
'2026)', (tester) async {
await pumpApp(tester, testDependencies(), location: RoutePaths.settings);
for (final (title, explanation) in [
('Citations', 'How sources and studysets'),
('Show the Pomodoro timer while studying', 'Show the focus / break'),
('Check grammar while writing', 'Underline possible spelling'),
('Search the web from the app', 'Off unless you turn it on.'),
('Jellyfin', 'Watch shows and films'),
('Count my learning in Explore', 'Explore shows how many people'),
]) {
final titled = find.widgetWithText(ExplainedTitle, title);
await tester.scrollUntilVisible(
titled,
200,
scrollable: find.byType(Scrollable).first,
);
await tester.pumpAndSettle();
expect(find.textContaining(explanation), findsNothing, reason: title);
await tester.tap(
find.descendant(of: titled, matching: find.byType(HelpButton)),
);
await tester.pumpAndSettle();
expect(find.textContaining(explanation), findsOneWidget, reason: title);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
}
expect(find.text('APA (7th edition)'), findsOneWidget, reason: 'a value');
});
testWidgets('the citation style chosen shows on a sample at once and is ' testWidgets('the citation style chosen shows on a sample at once and is '
'kept', (tester) async { 'kept', (tester) async {
final store = MemoryDeviceSettingsStore(); final store = MemoryDeviceSettingsStore();

View file

@ -455,7 +455,11 @@ class _GrantSheetState() extends State<_GrantSheet> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (grantee == null) ...[ if (grantee == null) ...[
Text(l10n.assistantPeopleTitle, style: heading), ExplainedTitle(
l10n.assistantPeopleTitle,
help: l10n.assistantGrantNote,
style: heading,
),
const SizedBox(height: 8), const SizedBox(height: 8),
if (state.accounts.isNotEmpty) ...[ if (state.accounts.isNotEmpty) ...[
Wrap( Wrap(
@ -505,7 +509,14 @@ class _GrantSheetState() extends State<_GrantSheet> {
), ),
], ],
const SizedBox(height: 16), const SizedBox(height: 16),
Text(l10n.assistantGrantBudget, style: heading), ExplainedTitle(
l10n.assistantGrantBudget,
help: [
l10n.assistantGrantLimitHelper,
if (priced) l10n.assistantGrantMoneyNote,
].join(' '),
style: heading,
),
const SizedBox(height: 8), const SizedBox(height: 8),
if (priced) if (priced)
_LimitRow( _LimitRow(
@ -520,13 +531,6 @@ class _GrantSheetState() extends State<_GrantSheet> {
suffix: null, suffix: null,
enabled: !state.busy, enabled: !state.busy,
), ),
Text(
[
l10n.assistantGrantLimitHelper,
if (priced) l10n.assistantGrantMoneyNote,
].join(' '),
style: muted,
),
if (grantee == null && state.replaced.isNotEmpty) ...[ if (grantee == null && state.replaced.isNotEmpty) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
_Replaced(state.replaced, currency: setUp.currency), _Replaced(state.replaced, currency: setUp.currency),
@ -538,10 +542,6 @@ class _GrantSheetState() extends State<_GrantSheet> {
style: TextStyle(color: theme.colorScheme.error), style: TextStyle(color: theme.colorScheme.error),
), ),
], ],
if (grantee == null) ...[
const SizedBox(height: 16),
Text(l10n.assistantGrantNote, style: muted),
],
], ],
), ),
footer: FilledButton( footer: FilledButton(

View file

@ -72,7 +72,7 @@ class const _AssistantsView({
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final offer = state.offer; final offer = state.offer;
final muted = theme.textTheme.bodyMedium?.copyWith( final subtitle = theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
); );
return Padding( return Padding(
@ -141,19 +141,14 @@ class const _AssistantsView({
if (ownKept && (offer.ownKeys || state.own.isNotEmpty)) ...[ if (ownKept && (offer.ownKeys || state.own.isNotEmpty)) ...[
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text( // Why own keys go unasked, where they are not allowed,
// behind its question mark (decided 3 Nov 2026).
child: offer.ownKeys
? Text(l10n.assistantOwnTitle, style: subtitle)
: ExplainedTitle(
l10n.assistantOwnTitle, l10n.assistantOwnTitle,
style: theme.textTheme.titleSmall?.copyWith( help: l10n.assistantOwnNotAllowed(instance),
color: theme.colorScheme.onSurfaceVariant, style: subtitle,
),
),
),
if (!offer.ownKeys)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
l10n.assistantOwnNotAllowed(instance),
style: muted,
), ),
), ),
for (final own in state.own) for (final own in state.own)

View file

@ -79,8 +79,10 @@ class const _AccountSecurity() extends StatelessWidget {
ListTile( ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: const Icon(LucideIcons.keyRound), leading: const Icon(LucideIcons.keyRound),
title: Text(l10n.passwordSectionSetUp), title: ExplainedTitle(
subtitle: Text(l10n.passwordSectionSetUpHint), l10n.passwordSectionSetUp,
help: l10n.passwordSectionSetUpHint,
),
onTap: () => _setUp(context, security), onTap: () => _setUp(context, security),
), ),
] else if (!security.finished) ] else if (!security.finished)

View file

@ -731,9 +731,6 @@ class const _ProfilePage({required final String url}) extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = HalkyonAppLocalizations.of(context); final l10n = HalkyonAppLocalizations.of(context);
final theme = Theme.of(context); final theme = Theme.of(context);
final muted = theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
);
return Padding( return Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Column( child: Column(
@ -751,6 +748,9 @@ class const _ProfilePage({required final String url}) extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
// What linking back does, behind its question mark (decided
// 3 Nov 2026).
HelpButton(l10n.publicProfileVerifyHint),
IconButton( IconButton(
tooltip: l10n.publicProfilePageCopy, tooltip: l10n.publicProfilePageCopy,
icon: const Icon(LucideIcons.copy, size: 18), icon: const Icon(LucideIcons.copy, size: 18),
@ -766,7 +766,6 @@ class const _ProfilePage({required final String url}) extends StatelessWidget {
), ),
], ],
), ),
Text(l10n.publicProfileVerifyHint, style: muted),
], ],
), ),
); );

View file

@ -7,6 +7,7 @@ import 'package:halkyon_app/src/simplelogin/model/simplelogin_standing.dart';
import 'package:halkyon_app/src/simplelogin/widget/keys_reset.dart'; import 'package:halkyon_app/src/simplelogin/widget/keys_reset.dart';
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_sheet.dart'; import 'package:halkyon_app/src/simplelogin/widget/simplelogin_sheet.dart';
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_standing_scope.dart'; import 'package:halkyon_app/src/simplelogin/widget/simplelogin_standing_scope.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart'; import 'package:material_ui/material_ui.dart';
@ -64,8 +65,10 @@ class const _SimpleLoginSection({required final double gapAbove})
ListTile( ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
leading: const Icon(LucideIcons.link), leading: const Icon(LucideIcons.link),
title: Text(l10n.simpleLoginAddTitle), title: ExplainedTitle(
subtitle: Text(l10n.simpleLoginAddHint), l10n.simpleLoginAddTitle,
help: l10n.simpleLoginAddHint,
),
onTap: () => addSimpleLogin(context), onTap: () => addSimpleLogin(context),
), ),
], ],

View file

@ -144,6 +144,37 @@ void main() {
await show(tester); await show(tester);
await tap(tester, find.text('Grant assistants')); await tap(tester, find.text('Grant assistants'));
expect(tester.widget<FilledButton>(saveButton()).onPressed, isNull); expect(tester.widget<FilledButton>(saveButton()).onPressed, isNull);
// What a grant does and how its budget counts, behind the question
// marks beside People and Budget (decided 3 Nov 2026).
final note = find.textContaining('They agree once');
final counts = find.textContaining('Empty for none.');
expect(note, findsNothing);
expect(counts, findsNothing);
await tap(
tester,
find.descendant(
of: find.widgetWithText(ExplainedTitle, 'People'),
matching: find.byType(HelpButton),
),
);
expect(note, findsOneWidget);
await tap(tester, find.text('OK'));
await tester.ensureVisible(
find.widgetWithText(ExplainedTitle, 'Budget, for all of them together'),
);
await tester.pumpAndSettle();
await tap(
tester,
find.descendant(
of: find.widgetWithText(
ExplainedTitle,
'Budget, for all of them together',
),
matching: find.byType(HelpButton),
),
);
expect(counts, findsOneWidget);
await tap(tester, find.text('OK'));
await addPerson(tester, 'anna'); await addPerson(tester, 'anna');
await addPerson(tester, 'ben'); await addPerson(tester, 'ben');

View file

@ -406,14 +406,16 @@ void main() {
), ),
); );
await show(tester); await show(tester);
const why =
expect(
find.text(
"Whoever runs test.example.org doesn't allow own keys here, so the " "Whoever runs test.example.org doesn't allow own keys here, so the "
"app doesn't ask these.", "app doesn't ask these.";
),
findsOneWidget, expect(find.text(why), findsNothing, reason: 'behind its question mark');
); await tester.tap(find.byType(HelpButton));
await tester.pumpAndSettle();
expect(find.text(why), findsOneWidget);
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
expect(find.text('Add an assistant with your own key'), findsNothing); expect(find.text('Add an assistant with your own key'), findsNothing);
}); });

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart'; import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_app/testing.dart'; import 'package:halkyon_app/testing.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart'; import 'package:material_ui/material_ui.dart';
/// A signed-in person's app as a test makes one: whose it is. /// A signed-in person's app as a test makes one: whose it is.
@ -592,6 +593,20 @@ void main() {
expect(find.text('Set up a password'), findsOneWidget); expect(find.text('Set up a password'), findsOneWidget);
expect(find.text('Change password'), findsNothing); expect(find.text('Change password'), findsNothing);
// What it is, behind its question mark (decided 3 Nov 2026).
const what =
'Sign in with your email address, a password and an '
'authenticator app.';
expect(find.text(what), findsNothing);
await tester.tap(
find.descendant(
of: find.widgetWithText(ExplainedTitle, 'Set up a password'),
matching: find.byType(HelpButton),
),
);
await tester.pumpAndSettle();
expect(find.text(what), findsOneWidget);
}); });
testWidgets('offers none to one without an address: a code mailed to ' testWidgets('offers none to one without an address: a code mailed to '

Some files were not shown because too many files have changed in this diff Show more