diff --git a/lib/ui/attachment_picking.dart b/lib/ui/attachment_picking.dart index 0ac2ea0..afd269d 100644 --- a/lib/ui/attachment_picking.dart +++ b/lib/ui/attachment_picking.dart @@ -44,18 +44,24 @@ Future> pickAttachments( mainAxisSize: MainAxisSize.min, children: [ ListTile( - leading: const Icon(Icons.photo_camera_outlined), - title: const Text('Camera'), + leading: const Icon( + Icons.photo_camera_outlined, + semanticLabel: 'Camera', + ), onTap: () => Navigator.pop(context, AttachmentPickSource.camera), ), ListTile( - leading: const Icon(Icons.photo_library_outlined), - title: const Text('Gallery'), + leading: const Icon( + Icons.photo_library_outlined, + semanticLabel: 'Gallery', + ), onTap: () => Navigator.pop(context, AttachmentPickSource.gallery), ), ListTile( - leading: const Icon(Icons.insert_drive_file_outlined), - title: const Text('File'), + leading: const Icon( + Icons.insert_drive_file_outlined, + semanticLabel: 'File', + ), onTap: () => Navigator.pop(context, AttachmentPickSource.file), ), ], @@ -102,8 +108,7 @@ class AttachButton extends StatelessWidget { @override Widget build(BuildContext context) { return IconButton( - tooltip: 'Attach', - icon: const Icon(Icons.attach_file), + icon: const Icon(Icons.attach_file, semanticLabel: 'Attach'), onPressed: () async { final files = await (picking ?? pickAttachments)(context); if (files.isNotEmpty) onFiles(files); diff --git a/lib/ui/capture_app_bar.dart b/lib/ui/capture_app_bar.dart index 21868d8..52eb0f0 100644 --- a/lib/ui/capture_app_bar.dart +++ b/lib/ui/capture_app_bar.dart @@ -86,13 +86,11 @@ class CaptureAppBar extends StatelessWidget implements PreferredSizeWidget { // Not gated on advanced mode: attaching is basic capture. AttachButton(onFiles: onAttachFiles, picking: attachmentPicking), IconButton( - tooltip: 'New note', onPressed: onNewNote, - icon: const Icon(Icons.check), + icon: const Icon(Icons.check, semanticLabel: 'New note'), ), if (advanced) IconButton( - tooltip: 'Sync', onPressed: syncing ? null : onSync, icon: syncing ? const SizedBox( @@ -100,12 +98,11 @@ class CaptureAppBar extends StatelessWidget implements PreferredSizeWidget { height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Icon(Icons.sync), + : const Icon(Icons.sync, semanticLabel: 'Sync'), ), IconButton( - tooltip: 'Notes', onPressed: onOpenList, - icon: const Icon(Icons.list), + icon: const Icon(Icons.list, semanticLabel: 'Notes'), ), // Live count of stored notes, proving local persistence. Sits directly // right of the Notes button it belongs to, and carries no icon of its @@ -122,9 +119,8 @@ class CaptureAppBar extends StatelessWidget implements PreferredSizeWidget { ), CaptureConfirmMark(captured: captured), IconButton( - tooltip: 'Settings', onPressed: onOpenSettings, - icon: const Icon(Icons.settings), + icon: const Icon(Icons.settings, semanticLabel: 'Settings'), ), ], ); diff --git a/lib/ui/cloud_images_settings.dart b/lib/ui/cloud_images_settings.dart index bb90281..2527fd3 100644 --- a/lib/ui/cloud_images_settings.dart +++ b/lib/ui/cloud_images_settings.dart @@ -34,8 +34,7 @@ class CloudImagesTile extends StatelessWidget { @override Widget build(BuildContext context) => ListTile( contentPadding: EdgeInsets.zero, - title: const Text('Cloud images'), - subtitle: const Text('Where attached images and files upload (dufs)'), + leading: const Icon(Icons.cloud, semanticLabel: 'Cloud images'), trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( appRoute(context, (_) => CloudImagesScreen(httpClient: httpClient)), @@ -129,7 +128,9 @@ class _CloudImagesScreenState extends State { Widget build(BuildContext context) { final status = _status; return Scaffold( - appBar: AppBar(title: const Text('Cloud images')), + appBar: AppBar( + title: const Icon(Icons.cloud_outlined, semanticLabel: 'Cloud images'), + ), body: ListView( padding: const EdgeInsets.all(16), children: [ @@ -138,13 +139,16 @@ class _CloudImagesScreenState extends State { controller: _password, obscureText: true, decoration: const InputDecoration( - labelText: 'Password for the "todo" dufs login', + prefixIcon: Icon( + Icons.lock_outline, + semanticLabel: 'Password for the "todo" dufs login', + ), ), ), const SizedBox(height: 12), FilledButton( onPressed: _checking ? null : _saveAndTest, - child: const Text('Save & test'), + child: const Icon(Icons.save, semanticLabel: 'Save & test'), ), if (_checkResult case final result?) ...[ const SizedBox(height: 8), @@ -153,21 +157,45 @@ class _CloudImagesScreenState extends State { const SizedBox(height: 16), ], if (status != null) ...[ - Text( - status.configured + // Shape + colour: a filled check, or a red error ring followed by + // the command (user-typed text, so it stays) that fixes it. + Icon( + status.configured ? Icons.check_circle : Icons.error_outline, + color: status.configured + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.error, + semanticLabel: status.configured ? 'Login: configured' : 'Login: missing — create it on the PC with', ), if (!status.configured) const SelectableText(kAddLoginCommand), const SizedBox(height: 8), - Text('Waiting to upload: ${status.pending}'), + // Icon + the count itself: the number is the information. + Row( + children: [ + const Icon(Icons.cloud_upload_outlined), + const SizedBox(width: 8), + Text( + '${status.pending}', + semanticsLabel: 'Waiting to upload: ${status.pending}', + ), + ], + ), ], const SizedBox(height: 16), - Text( - 'Images are shrunk to 2560px JPEGs without location data before ' - 'they leave this device. Other files upload unchanged, at any ' - 'size.', - style: Theme.of(context).textTheme.bodySmall, + // Shrunk + no location, as glyphs; the sentence is spoken only. + const Row( + children: [ + Icon( + Icons.photo_size_select_large, + semanticLabel: + 'Images are shrunk to 2560px JPEGs without location data ' + 'before they leave this device. Other files upload ' + 'unchanged, at any size.', + ), + SizedBox(width: 8), + Icon(Icons.location_off), + ], ), ], ), diff --git a/lib/ui/confirm_delete.dart b/lib/ui/confirm_delete.dart new file mode 100644 index 0000000..b1405ec --- /dev/null +++ b/lib/ui/confirm_delete.dart @@ -0,0 +1,58 @@ +/// Icon-only confirmation before deleting a note. +/// +/// Replaces design_system's `confirmDestructive` here because that dialog +/// paints its title, message and button labels as text. The safety contract +/// is kept: Cancel is autofocused (a reflexive Return keeps the note), Escape +/// and the barrier answer "no", and the confirm glyph wears the error colour. +library; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Asks whether to delete a note; true only when the delete glyph is pressed. +Future confirmDeleteNote(BuildContext context) async { + final confirmed = await showDialog( + context: context, + builder: (_) => const _ConfirmDeleteDialog(), + ); + return confirmed ?? false; +} + +class _ConfirmDeleteDialog extends StatelessWidget { + const _ConfirmDeleteDialog(); + + @override + Widget build(BuildContext context) { + final error = Theme.of(context).colorScheme.error; + return CallbackShortcuts( + bindings: { + const SingleActivator(LogicalKeyboardKey.escape): () => + Navigator.of(context).pop(false), + }, + child: AlertDialog( + // Spoken when the dialog opens; the old written body. + semanticLabel: 'This cannot be undone.', + title: Icon( + Icons.delete_forever, + size: 40, + color: error, + semanticLabel: 'Delete note?', + ), + actionsAlignment: MainAxisAlignment.spaceEvenly, + actions: [ + IconButton( + key: const ValueKey('confirm-cancel'), + autofocus: true, + onPressed: () => Navigator.of(context).pop(false), + icon: const Icon(Icons.close, semanticLabel: 'Cancel'), + ), + IconButton( + key: const ValueKey('confirm-delete'), + onPressed: () => Navigator.of(context).pop(true), + icon: Icon(Icons.delete, color: error, semanticLabel: 'Delete'), + ), + ], + ), + ); + } +} diff --git a/lib/ui/enum_glyphs.dart b/lib/ui/enum_glyphs.dart new file mode 100644 index 0000000..0867c0e --- /dev/null +++ b/lib/ui/enum_glyphs.dart @@ -0,0 +1,105 @@ +/// Glyphs for the Priority and Status enums, and the chip row that picks +/// them, so every picker shows the same shape + colour for the same value. +/// +/// The enum's `label` survives only as the spoken (semantics) label. +library; + +import 'package:flutter/material.dart'; + +import 'package:todo/data/note.dart'; +import 'package:todo/ui/note_grid_tile.dart'; + +/// Shape channel for a priority: up for high, level for medium, down for low. +/// Colour ([priorityColor]) is the second channel. +IconData priorityIcon(Priority p) => switch (p) { + Priority.high => Icons.keyboard_double_arrow_up, + Priority.medium => Icons.drag_handle, + Priority.low => Icons.keyboard_double_arrow_down, +}; + +/// Heading glyph standing for "status" (matches the bulk-status button). +const IconData kStatusHeadingIcon = Icons.task_alt; + +/// Heading glyph standing for "priority" (matches the bulk-priority button). +const IconData kPriorityHeadingIcon = Icons.flag_outlined; + +/// [s] as its glyph, spoken as its label. +Widget statusGlyph(Status s, {double? size}) => + Icon(statusIcon(s), size: size, semanticLabel: s.label); + +/// [p] as its tinted glyph, spoken as its label. +Widget priorityGlyph(BuildContext context, Priority p, {double? size}) => Icon( + priorityIcon(p), + size: size, + color: priorityColor(p, Theme.of(context).colorScheme), + semanticLabel: p.label, +); + +/// A heading glyph followed by one chip per value, each chip a glyph. +/// +/// [multi] switches between FilterChips (toggle many) and ChoiceChips (pick +/// one); either way the selected chips are filled and checked. +class GlyphChips extends StatelessWidget { + /// Creates a chip row for [values]. + const GlyphChips({ + required this.heading, + required this.semanticLabel, + required this.values, + required this.selected, + required this.glyphOf, + required this.onTap, + this.multi = false, + super.key, + }); + + /// Glyph shown in front of the chips in place of a written heading. + final IconData heading; + + /// Spoken name of the row (the old written heading). + final String semanticLabel; + + /// Every value offered. + final List values; + + /// Values currently selected. + final Set selected; + + /// Builds the glyph for one value. + final Widget Function(T) glyphOf; + + /// Called with the tapped value. + final ValueChanged onTap; + + /// Whether this is a multi-select (filter) row. + final bool multi; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon(heading, semanticLabel: semanticLabel), + const SizedBox(width: 12), + Expanded( + child: Wrap( + spacing: 8, + children: [ + for (final v in values) + if (multi) + FilterChip( + label: glyphOf(v), + selected: selected.contains(v), + onSelected: (_) => onTap(v), + ) + else + ChoiceChip( + label: glyphOf(v), + selected: selected.contains(v), + onSelected: (_) => onTap(v), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/ui/github_mirror_screen.dart b/lib/ui/github_mirror_screen.dart index e23428b..c3fbb34 100644 --- a/lib/ui/github_mirror_screen.dart +++ b/lib/ui/github_mirror_screen.dart @@ -145,7 +145,12 @@ class _GitHubMirrorScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Advanced sync (GitHub)')), + appBar: AppBar( + title: const Icon( + Icons.cloud_sync, + semanticLabel: 'Advanced sync (GitHub)', + ), + ), body: ListView( padding: const EdgeInsets.all(16), children: [ @@ -157,17 +162,16 @@ class _GitHubMirrorScreenState extends State { const SizedBox(height: 12), Align( alignment: Alignment.centerLeft, - child: FilledButton.icon( + child: IconButton.filled( onPressed: _connectGitHub, - icon: const Icon(Icons.login), - label: const Text('Connect GitHub'), + icon: const Icon(Icons.login, semanticLabel: 'Connect GitHub'), ), ), const SizedBox(height: 16), TextField( controller: _owner, decoration: const InputDecoration( - labelText: 'GitHub owner', + prefixIcon: Icon(Icons.person, semanticLabel: 'GitHub owner'), border: OutlineInputBorder(), ), ), @@ -175,7 +179,7 @@ class _GitHubMirrorScreenState extends State { TextField( controller: _repo, decoration: const InputDecoration( - labelText: 'Repository name', + prefixIcon: Icon(Icons.folder, semanticLabel: 'Repository name'), border: OutlineInputBorder(), ), ), @@ -183,8 +187,7 @@ class _GitHubMirrorScreenState extends State { TextField( controller: _clientId, decoration: const InputDecoration( - labelText: 'OAuth App client id', - helperText: 'Leave as the baked-in default unless self-hosting', + prefixIcon: Icon(Icons.key, semanticLabel: 'OAuth App client id'), border: OutlineInputBorder(), ), ), @@ -193,15 +196,17 @@ class _GitHubMirrorScreenState extends State { controller: _token, obscureText: true, decoration: const InputDecoration( - labelText: 'Access token (fallback)', - helperText: 'Contents: read/write on the sync repo only', + prefixIcon: Icon( + Icons.vpn_key, + semanticLabel: 'Access token (fallback)', + ), border: OutlineInputBorder(), ), ), const SizedBox(height: 12), Row( children: [ - OutlinedButton.icon( + IconButton.outlined( onPressed: _testing ? null : _test, icon: _testing ? const SizedBox( @@ -209,14 +214,15 @@ class _GitHubMirrorScreenState extends State { height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) - : const Icon(Icons.wifi_tethering), - label: const Text('Test connection'), + : const Icon( + Icons.wifi_tethering, + semanticLabel: 'Test connection', + ), ), const SizedBox(width: 12), - FilledButton.icon( + IconButton.filled( onPressed: _save, - icon: const Icon(Icons.save), - label: const Text('Save'), + icon: const Icon(Icons.save, semanticLabel: 'Save'), ), ], ), diff --git a/lib/ui/images/file_embed.dart b/lib/ui/images/file_embed.dart index 339d8ba..edc3a17 100644 --- a/lib/ui/images/file_embed.dart +++ b/lib/ui/images/file_embed.dart @@ -111,8 +111,10 @@ class _FileEmbedState extends State { ?pending, ], ); + // Spoken only: nothing paints this string. + final semanticLabel = 'Attached file ${attachment.name}, $detail'; return Semantics( - label: 'Attached file ${attachment.name}, $detail', + label: semanticLabel, button: size != null, child: Material( color: theme.colorScheme.surfaceContainerHighest, diff --git a/lib/ui/images/image_embed.dart b/lib/ui/images/image_embed.dart index b377ec5..dc075a2 100644 --- a/lib/ui/images/image_embed.dart +++ b/lib/ui/images/image_embed.dart @@ -33,8 +33,10 @@ class ImageEmbed extends StatelessWidget { final store = ImageStoreScope.of(context); final source = store.sourceFor(ref); final picture = imageFor(source, fit: fit); + // Spoken only: nothing paints this string. + const semanticLabel = 'Attached image'; return Semantics( - label: 'Attached image', + label: semanticLabel, button: source is! MissingImageSource, child: GestureDetector( onTap: source is MissingImageSource diff --git a/lib/ui/note_actions_sheet.dart b/lib/ui/note_actions_sheet.dart index 76cd166..bc93374 100644 --- a/lib/ui/note_actions_sheet.dart +++ b/lib/ui/note_actions_sheet.dart @@ -6,10 +6,11 @@ library; import 'dart:async'; -import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:todo/data/note.dart'; import 'package:todo/data/note_template.dart'; +import 'package:todo/ui/confirm_delete.dart'; +import 'package:todo/ui/enum_glyphs.dart'; import 'package:todo/ui/note_sheet_details.dart'; import 'package:todo/ui/note_task_details.dart'; @@ -61,53 +62,54 @@ class _NoteActionsSheetState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - firstLine.isEmpty ? '(empty)' : firstLine, - style: Theme.of(context).textTheme.titleMedium, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + // User content; untitled shows nothing, not a placeholder. + if (firstLine.isNotEmpty) + Text( + firstLine, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), NoteSheetDetails(note: widget.note), const SizedBox(height: 16), - _EnumChips( - label: 'Status', + GlyphChips( + heading: kStatusHeadingIcon, + semanticLabel: 'Status', values: Status.values, selected: {_status}, - labelOf: (s) => s.label, - onSelected: (s) { + glyphOf: statusGlyph, + onTap: (s) { setState(() => _status = s); unawaited(_persist()); }, ), const SizedBox(height: 12), - _EnumChips( - label: 'Priority', + GlyphChips( + heading: kPriorityHeadingIcon, + semanticLabel: 'Priority', values: Priority.values, selected: {_priority}, - labelOf: (p) => p.label, - onSelected: (p) { + glyphOf: (p) => priorityGlyph(context, p), + onTap: (p) { setState(() => _priority = p); unawaited(_persist()); }, ), NoteTaskDetails(note: widget.note), const SizedBox(height: 16), - OutlinedButton.icon( - icon: const Icon(Icons.delete_outline), - label: const Text('Delete note'), - style: OutlinedButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.error, + Align( + child: IconButton.outlined( + color: Theme.of(context).colorScheme.error, + icon: const Icon( + Icons.delete_outline, + semanticLabel: 'Delete note', + ), + onPressed: () async { + if (!await confirmDeleteNote(context)) return; + await widget.onDelete(); + if (context.mounted) Navigator.of(context).pop(); + }, ), - onPressed: () async { - final confirmed = await confirmDestructive( - context, - title: 'Delete note?', - message: 'This cannot be undone.', - ); - if (!confirmed) return; - await widget.onDelete(); - if (context.mounted) Navigator.of(context).pop(); - }, ), ], ), @@ -116,42 +118,3 @@ class _NoteActionsSheetState extends State { ); } } - -/// Single-select chip row for an enum (used for per-note priority/status). -class _EnumChips extends StatelessWidget { - const _EnumChips({ - required this.label, - required this.values, - required this.selected, - required this.labelOf, - required this.onSelected, - }); - - final String label; - final List values; - final Set selected; - final String Function(T) labelOf; - final ValueChanged onSelected; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: Theme.of(context).textTheme.labelLarge), - const SizedBox(height: 8), - Wrap( - spacing: 8, - children: [ - for (final v in values) - ChoiceChip( - label: Text(labelOf(v)), - selected: selected.contains(v), - onSelected: (_) => onSelected(v), - ), - ], - ), - ], - ); - } -} diff --git a/lib/ui/note_date_range_field.dart b/lib/ui/note_date_range_field.dart index fd6e01e..b0e27c1 100644 --- a/lib/ui/note_date_range_field.dart +++ b/lib/ui/note_date_range_field.dart @@ -9,19 +9,29 @@ import 'dart:async'; import 'package:flutter/material.dart'; +/// Glyph for "created" (a note added), shared by the filter and sort menus. +const IconData kCreatedIcon = Icons.post_add; + +/// Glyph for "last updated", shared by the filter and sort menus. +const IconData kUpdatedIcon = Icons.update; + /// A labelled date range shown as one preset menu plus a custom picker. class DateRangeField extends StatelessWidget { /// Creates a labelled date-range control. const DateRangeField({ - required this.label, + required this.icon, + required this.semanticLabel, required this.from, required this.to, required this.onChanged, super.key, }); - /// Heading shown above the control (e.g. 'Created'). - final String label; + /// Glyph shown in the control's border in place of a written heading. + final IconData icon; + + /// Spoken name of the control (e.g. 'Created'); never painted. + final String semanticLabel; /// Start of the selected range, or null when unbounded. final DateTime? from; @@ -86,7 +96,8 @@ class DateRangeField extends StatelessWidget { // A popup menu rather than DropdownButton: re-picking "Custom…" while a // custom range is shown must still reopen the picker. return PopupMenuButton<_Preset>( - tooltip: label, + // Empty, not the label: a tooltip is painted on long-press. + tooltip: '', onSelected: (p) => _select(context, p), itemBuilder: (_) => [ for (final p in _Preset.values) @@ -94,7 +105,7 @@ class DateRangeField extends StatelessWidget { ], child: InputDecorator( decoration: InputDecoration( - labelText: label, + prefixIcon: Icon(icon, semanticLabel: semanticLabel), isDense: true, border: const OutlineInputBorder(), contentPadding: const EdgeInsets.symmetric( diff --git a/lib/ui/note_detail_screen.dart b/lib/ui/note_detail_screen.dart index a106803..cfe6683 100644 --- a/lib/ui/note_detail_screen.dart +++ b/lib/ui/note_detail_screen.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:todo/data/app_settings.dart'; @@ -8,6 +7,7 @@ import 'package:todo/data/note.dart'; import 'package:todo/data/note_repository.dart'; import 'package:todo/data/note_template.dart'; import 'package:todo/ui/attachment_picking.dart'; +import 'package:todo/ui/confirm_delete.dart'; import 'package:todo/ui/image_attach.dart'; import 'package:todo/ui/image_input.dart'; import 'package:todo/ui/note_form.dart'; @@ -85,12 +85,7 @@ class _NoteDetailScreenState extends State { _persist(_note.copyWith(text: text, updatedAt: DateTime.now())); Future _delete() async { - final confirmed = await confirmDestructive( - context, - title: 'Delete note?', - message: 'This cannot be undone.', - ); - if (!confirmed) return; + if (!await confirmDeleteNote(context)) return; await widget.repository.delete(_note.id); if (mounted) Navigator.of(context).pop(); } @@ -100,7 +95,8 @@ class _NoteDetailScreenState extends State { final title = noteTitle(_note.text); return Scaffold( appBar: AppBar( - title: Text(title.isEmpty ? '(empty)' : title), + // An untitled note shows no title rather than a written placeholder. + title: title.isEmpty ? null : Text(title), actions: [ AttachButton( onFiles: _attachFiles, @@ -114,8 +110,10 @@ class _NoteDetailScreenState extends State { onPressed: _copy, ), IconButton( - tooltip: 'Delete note', - icon: const Icon(Icons.delete_outline), + icon: const Icon( + Icons.delete_outline, + semanticLabel: 'Delete note', + ), onPressed: _delete, ), ], diff --git a/lib/ui/note_editor_chrome.dart b/lib/ui/note_editor_chrome.dart index 18c8f6e..2c36dd6 100644 --- a/lib/ui/note_editor_chrome.dart +++ b/lib/ui/note_editor_chrome.dart @@ -43,7 +43,7 @@ class NoteEditorChrome extends StatelessWidget { DropdownButtonFormField( initialValue: template.id, decoration: const InputDecoration( - labelText: 'Template', + label: Icon(Icons.description, semanticLabel: 'Template'), isDense: true, border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -54,9 +54,7 @@ class NoteEditorChrome extends StatelessWidget { ], onChanged: (id) { if (id == null) return; - onTemplateSelected( - NoteTemplate.all.firstWhere((t) => t.id == id), - ); + onTemplateSelected(NoteTemplate.all.firstWhere((t) => t.id == id)); }, ), const SizedBox(height: 8), @@ -67,8 +65,7 @@ class NoteEditorChrome extends StatelessWidget { segments: [ const ButtonSegment( value: NoteEditorMode.preview, - icon: Icon(Icons.visibility_outlined), - label: Text('View'), + icon: Icon(Icons.visibility_outlined, semanticLabel: 'View'), ), // Guided is offered for any structured template; tapping it // on an empty draft opens the wizard, and is blocked at @@ -76,13 +73,11 @@ class NoteEditorChrome extends StatelessWidget { if (!template.isFreeform) const ButtonSegment( value: NoteEditorMode.guided, - icon: Icon(Icons.checklist), - label: Text('Guided'), + icon: Icon(Icons.checklist, semanticLabel: 'Guided'), ), const ButtonSegment( value: NoteEditorMode.raw, - icon: Icon(Icons.notes), - label: Text('Raw'), + icon: Icon(Icons.notes, semanticLabel: 'Raw'), ), ], selected: {mode}, diff --git a/lib/ui/note_editor_raw_field.dart b/lib/ui/note_editor_raw_field.dart index 13b1a17..d4f3ac6 100644 --- a/lib/ui/note_editor_raw_field.dart +++ b/lib/ui/note_editor_raw_field.dart @@ -44,10 +44,8 @@ class NoteEditorRawField extends StatelessWidget { textAlignVertical: TextAlignVertical.top, keyboardType: TextInputType.multiline, style: Theme.of(context).textTheme.bodyLarge, - decoration: const InputDecoration( - border: InputBorder.none, - hintText: 'Write your idea…', - ), + // No hint: the cursor in an empty full-height box says "type". + decoration: const InputDecoration(border: InputBorder.none), onChanged: (_) => onChanged(), ), ), diff --git a/lib/ui/note_editor_step_page.dart b/lib/ui/note_editor_step_page.dart index 9e8497e..b170f3d 100644 --- a/lib/ui/note_editor_step_page.dart +++ b/lib/ui/note_editor_step_page.dart @@ -66,8 +66,7 @@ class NoteEditorStepPage extends StatelessWidget { Align( alignment: Alignment.centerLeft, child: IconButton( - icon: const Icon(Icons.arrow_back), - tooltip: 'Exit guided', + icon: const Icon(Icons.arrow_back, semanticLabel: 'Exit guided'), onPressed: onExit, ), ), @@ -96,10 +95,12 @@ class NoteEditorStepPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - section.isTitle ? 'title' : section.label, - style: theme.textTheme.titleMedium, - ), + // Section labels are template content (they land in the note); + // the title step has none, so it gets a glyph, not a word. + if (section.isTitle) + const Icon(Icons.title, semanticLabel: 'title') + else + Text(section.label, style: theme.textTheme.titleMedium), const SizedBox(height: 4), Text( section.helper, @@ -140,16 +141,22 @@ class NoteEditorStepPage extends StatelessWidget { if (index > 0) TextButton( onPressed: () => onGoToStep(index - 1), - child: const Text('Back'), + child: const Icon( + Icons.navigate_before, + semanticLabel: 'Back', + ), ), const Spacer(), if (index < total - 1) FilledButton( onPressed: () => onGoToStep(index + 1), - child: const Text('Next'), + child: const Icon(Icons.navigate_next, semanticLabel: 'Next'), ) else - FilledButton(onPressed: onExit, child: const Text('Done')), + FilledButton( + onPressed: onExit, + child: const Icon(Icons.check, semanticLabel: 'Done'), + ), ], ), ), diff --git a/lib/ui/note_editor_wizard.dart b/lib/ui/note_editor_wizard.dart index 03f9330..b86aebc 100644 --- a/lib/ui/note_editor_wizard.dart +++ b/lib/ui/note_editor_wizard.dart @@ -13,6 +13,7 @@ import 'package:flutter/material.dart'; import 'package:todo/data/note.dart'; import 'package:todo/data/note_template.dart'; +import 'package:todo/ui/enum_glyphs.dart'; /// Asks for a priority, then a template, then hands both back via [onStart]. class NoteEditorWizard extends StatefulWidget { @@ -48,7 +49,7 @@ class _NoteEditorWizardState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final semanticLabel = 'Step ${_step + 1} of 2'; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, @@ -56,11 +57,24 @@ class _NoteEditorWizardState extends State { Row( children: [ IconButton( - icon: const Icon(Icons.close), - tooltip: 'Cancel', + icon: const Icon(Icons.close, semanticLabel: 'Cancel'), onPressed: widget.onCancel, ), - Text('Step ${_step + 1} of 2', style: theme.textTheme.labelLarge), + // Two step dots, the current one filled: position by shape. + Semantics( + label: semanticLabel, + child: ExcludeSemantics( + child: Row( + children: [ + for (var i = 0; i < 2; i++) + Icon( + i == _step ? Icons.circle : Icons.circle_outlined, + size: 12, + ), + ], + ), + ), + ), ], ), const SizedBox(height: 8), @@ -74,14 +88,14 @@ class _NoteEditorWizardState extends State { DropdownButtonFormField( initialValue: _priority, decoration: const InputDecoration( - labelText: 'Priority', + prefixIcon: Icon(kPriorityHeadingIcon, semanticLabel: 'Priority'), isDense: true, border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), items: [ for (final p in Priority.values) - DropdownMenuItem(value: p, child: Text(p.label)), + DropdownMenuItem(value: p, child: priorityGlyph(context, p)), ], onChanged: (p) { if (p != null) setState(() => _priority = p); @@ -92,7 +106,7 @@ class _NoteEditorWizardState extends State { alignment: Alignment.centerRight, child: FilledButton( onPressed: () => setState(() => _step = 1), - child: const Text('Next'), + child: const Icon(Icons.navigate_next, semanticLabel: 'Next'), ), ), ]; @@ -104,7 +118,7 @@ class _NoteEditorWizardState extends State { DropdownButtonFormField( initialValue: _template.id, decoration: const InputDecoration( - labelText: 'Template', + prefixIcon: Icon(Icons.description, semanticLabel: 'Template'), isDense: true, border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -124,12 +138,12 @@ class _NoteEditorWizardState extends State { children: [ TextButton( onPressed: () => setState(() => _step = 0), - child: const Text('Back'), + child: const Icon(Icons.arrow_back, semanticLabel: 'Back'), ), const SizedBox(width: 8), FilledButton( onPressed: () => widget.onStart(_priority, _template), - child: const Text('Start'), + child: const Icon(Icons.play_arrow, semanticLabel: 'Start'), ), ], ), diff --git a/lib/ui/note_meta_row.dart b/lib/ui/note_meta_row.dart index 94ac395..68dd2be 100644 --- a/lib/ui/note_meta_row.dart +++ b/lib/ui/note_meta_row.dart @@ -8,24 +8,30 @@ library; import 'package:flutter/material.dart'; import 'package:todo/data/note.dart'; +import 'package:todo/ui/enum_glyphs.dart'; -/// A compact labelled dropdown for picking an enum value (priority/status). +/// A compact dropdown for picking an enum value (priority/status), headed by +/// a glyph and showing each value as its glyph. /// /// Generic over the enum type [T] so the same control drives both pickers -/// without duplication; [labelOf] maps a value to its display string. +/// without duplication; [glyphOf] maps a value to its glyph. class MetaDropdown extends StatelessWidget { /// Creates a dropdown showing [value] out of [values]. const MetaDropdown({ - required this.label, + required this.heading, + required this.semanticLabel, required this.value, required this.values, - required this.labelOf, + required this.glyphOf, required this.onChanged, super.key, }); - /// Label shown above the control. - final String label; + /// Glyph in the control's border in place of a written heading. + final IconData heading; + + /// Spoken name of the control; never painted. + final String semanticLabel; /// Currently selected value. final T value; @@ -33,8 +39,8 @@ class MetaDropdown extends StatelessWidget { /// Every selectable value. final List values; - /// Maps a value to its display string. - final String Function(T) labelOf; + /// Maps a value to its glyph. + final Widget Function(T) glyphOf; /// Called with the newly picked value. final ValueChanged onChanged; @@ -43,7 +49,7 @@ class MetaDropdown extends StatelessWidget { Widget build(BuildContext context) { return InputDecorator( decoration: InputDecoration( - labelText: label, + prefixIcon: Icon(heading, semanticLabel: semanticLabel), isDense: true, border: const OutlineInputBorder(), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -55,7 +61,7 @@ class MetaDropdown extends StatelessWidget { isExpanded: true, items: [ for (final v in values) - DropdownMenuItem(value: v, child: Text(labelOf(v))), + DropdownMenuItem(value: v, child: glyphOf(v)), ], onChanged: (v) { if (v != null) onChanged(v); @@ -98,20 +104,22 @@ class NoteMetaRow extends StatelessWidget { children: [ Expanded( child: MetaDropdown( - label: 'Priority', + heading: kPriorityHeadingIcon, + semanticLabel: 'Priority', value: priority, values: Priority.values, - labelOf: (p) => p.label, + glyphOf: (p) => priorityGlyph(context, p), onChanged: onPriorityChanged, ), ), const SizedBox(width: 12), Expanded( child: MetaDropdown( - label: 'Status', + heading: kStatusHeadingIcon, + semanticLabel: 'Status', value: status, values: Status.values, - labelOf: (s) => s.label, + glyphOf: statusGlyph, onChanged: onStatusChanged, ), ), diff --git a/lib/ui/note_tile.dart b/lib/ui/note_tile.dart index 82796f8..9f514f8 100644 --- a/lib/ui/note_tile.dart +++ b/lib/ui/note_tile.dart @@ -56,8 +56,7 @@ class NoteTile extends StatelessWidget { ), subtitle: Text(meta), trailing: IconButton( - icon: const Icon(Icons.more_vert), - tooltip: 'Quick actions', + icon: const Icon(Icons.more_vert, semanticLabel: 'Quick actions'), onPressed: onActions, ), onTap: onTap, diff --git a/lib/ui/notes_filter_sheet.dart b/lib/ui/notes_filter_sheet.dart index 5582904..5810e0e 100644 --- a/lib/ui/notes_filter_sheet.dart +++ b/lib/ui/notes_filter_sheet.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:todo/data/note.dart'; import 'package:todo/data/note_repository.dart'; +import 'package:todo/ui/enum_glyphs.dart'; import 'package:todo/ui/note_date_range_field.dart'; import 'package:todo/ui/notes_list_defaults.dart'; @@ -66,7 +67,6 @@ class _FilterSheetState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); return SafeArea( child: Padding( padding: EdgeInsets.fromLTRB( @@ -82,33 +82,38 @@ class _FilterSheetState extends State { children: [ Row( children: [ - Text('Filters', style: theme.textTheme.titleLarge), + const Icon(Icons.filter_list, semanticLabel: 'Filters'), const Spacer(), TextButton( onPressed: _clearAll, - child: const Text('Clear all'), + child: const Icon(Icons.clear, semanticLabel: 'Clear all'), ), ], ), const SizedBox(height: 8), - _MultiChips( - label: 'Status', + GlyphChips( + multi: true, + heading: kStatusHeadingIcon, + semanticLabel: 'Status', values: Status.values, selected: _statuses, - labelOf: (s) => s.label, - onToggle: (s) => _toggle(_statuses, s), + glyphOf: statusGlyph, + onTap: (s) => _toggle(_statuses, s), ), const SizedBox(height: 12), - _MultiChips( - label: 'Priority', + GlyphChips( + multi: true, + heading: kPriorityHeadingIcon, + semanticLabel: 'Priority', values: Priority.values, selected: _priorities, - labelOf: (p) => p.label, - onToggle: (p) => _toggle(_priorities, p), + glyphOf: (p) => priorityGlyph(context, p), + onTap: (p) => _toggle(_priorities, p), ), const SizedBox(height: 12), DateRangeField( - label: 'Created', + icon: kCreatedIcon, + semanticLabel: 'Created', from: _createdFrom, to: _createdTo, onChanged: (from, to) => setState(() { @@ -118,7 +123,8 @@ class _FilterSheetState extends State { ), const SizedBox(height: 12), DateRangeField( - label: 'Last updated', + icon: kUpdatedIcon, + semanticLabel: 'Last updated', from: _updatedFrom, to: _updatedTo, onChanged: (from, to) => setState(() { @@ -129,7 +135,7 @@ class _FilterSheetState extends State { const SizedBox(height: 20), FilledButton( onPressed: () => Navigator.of(context).pop(_build()), - child: const Text('Apply'), + child: const Icon(Icons.check, semanticLabel: 'Apply'), ), ], ), @@ -139,45 +145,6 @@ class _FilterSheetState extends State { } } -/// Multi-select chip group for an enum (used by the filter sheet). -class _MultiChips extends StatelessWidget { - const _MultiChips({ - required this.label, - required this.values, - required this.selected, - required this.labelOf, - required this.onToggle, - }); - - final String label; - final List values; - final Set selected; - final String Function(T) labelOf; - final ValueChanged onToggle; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: Theme.of(context).textTheme.labelLarge), - const SizedBox(height: 8), - Wrap( - spacing: 8, - children: [ - for (final v in values) - FilterChip( - label: Text(labelOf(v)), - selected: selected.contains(v), - onSelected: (_) => onToggle(v), - ), - ], - ), - ], - ); - } -} - /// A date-range control offering quick presets plus a custom range picker. /// /// Reports the chosen [from]/[to] (day granularity, both inclusive) back to diff --git a/lib/ui/notes_list_screen.dart b/lib/ui/notes_list_screen.dart index a23d71a..be5b8eb 100644 --- a/lib/ui/notes_list_screen.dart +++ b/lib/ui/notes_list_screen.dart @@ -134,7 +134,7 @@ class _NotesListScreenState extends State { final badgeCount = userFacetCount(_filter); return Scaffold( appBar: AppBar( - title: const Text('Notes'), + title: const Icon(Icons.note, semanticLabel: 'Notes'), actions: [ _selectButton(), // Eye, no label: open = Claude-made notes shown, slashed = hidden. @@ -148,26 +148,27 @@ class _NotesListScreenState extends State { onPressed: _toggleClaudeNotes, ), PopupMenuButton( - tooltip: 'Sort', - icon: const Icon(Icons.sort), + // Empty, not omitted: null falls back to the painted "Show menu". + tooltip: '', + icon: const Icon(Icons.sort, semanticLabel: 'Sort'), initialValue: _sort, onSelected: _setSort, itemBuilder: (_) => const [ PopupMenuItem( value: NoteSort.modifiedDesc, - child: Text('Last updated'), + child: Icon(Icons.update, semanticLabel: 'Last updated'), ), PopupMenuItem( value: NoteSort.createdDesc, - child: Text('Newest created'), + child: Icon(Icons.post_add, semanticLabel: 'Newest created'), ), PopupMenuItem( value: NoteSort.alphabetical, - child: Text('Alphabetical'), + child: Icon(Icons.sort_by_alpha, semanticLabel: 'Alphabetical'), ), PopupMenuItem( value: NoteSort.priorityDesc, - child: Text('Priority'), + child: Icon(Icons.flag_outlined, semanticLabel: 'Priority'), ), ], ), @@ -181,8 +182,7 @@ class _NotesListScreenState extends State { label: Text('$badgeCount'), offset: const Offset(-8, 4), child: IconButton( - tooltip: 'Filter', - icon: const Icon(Icons.filter_list), + icon: const Icon(Icons.filter_list, semanticLabel: 'Filter'), onPressed: _openFilters, ), ), @@ -198,15 +198,16 @@ class _NotesListScreenState extends State { onChanged: _onSearchChanged, textInputAction: TextInputAction.search, decoration: InputDecoration( - hintText: 'Search notes…', prefixIcon: const Icon(Icons.search), isDense: true, border: const OutlineInputBorder(), suffixIcon: _searchController.text.isEmpty ? null : IconButton( - tooltip: 'Clear search', - icon: const Icon(Icons.clear), + icon: const Icon( + Icons.clear, + semanticLabel: 'Clear search', + ), onPressed: () { _searchController.clear(); _onSearchChanged(''); diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index a7efec0..5c3ab15 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -105,11 +105,8 @@ class _SettingsScreenState extends State { /// session is available, best-effort mirrors it so the choice follows the /// user to their other devices. /// - /// The local write always lands — a signed-in-as-wrong-account device - /// throwing [FirebaseAuthError] out of `openFirebase()` must not take the - /// toggle down with it, so opening the client has its own try/catch, - /// separate from (and preceding) the try that guards the local write and - /// mirror push. + /// The local write always lands: a [FirebaseAuthError] from opening the + /// client (wrong account) is caught on its own, before the write. Future _setAdvancedMode(bool value) async { final analytics = widget.analytics; if (analytics != null) { @@ -206,7 +203,9 @@ class _SettingsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Settings')), + appBar: AppBar( + title: const Icon(Icons.settings, semanticLabel: 'Settings'), + ), body: ListView( padding: const EdgeInsets.all(16), children: [ @@ -214,33 +213,38 @@ class _SettingsScreenState extends State { valueListenable: widget.appSettings, builder: (context, settings, _) => SwitchListTile( contentPadding: EdgeInsets.zero, - title: const Text('Enable advanced'), - subtitle: const Text( - 'Priority/status, templates, view modes, and sync details ' - 'in the capture screen', - ), + secondary: const Icon(Icons.tune, semanticLabel: _advanced), value: settings.advancedMode, onChanged: (value) => unawaited(_setAdvancedMode(value)), ), ), const Divider(), - ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Sync settings'), - subtitle: const Text('Firebase sync and notes backup'), - trailing: const Icon(Icons.chevron_right), - onTap: () => unawaited(_openSyncSettings()), + _link( + const Icon(Icons.sync, semanticLabel: 'Sync settings: Firebase'), + _openSyncSettings, ), CloudImagesTile(httpClient: widget.httpClient), - ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Advanced sync (GitHub)'), - subtitle: const Text('Cutover mirror — not recommended'), - trailing: const Icon(Icons.chevron_right), - onTap: () => unawaited(_openGitHubMirror()), + _link( + Icon( + Icons.cloud_sync, + color: Theme.of(context).colorScheme.outline, + semanticLabel: 'Advanced sync (GitHub): not recommended', + ), + _openGitHubMirror, ), ], ), ); } + + // Spoken only: what the advanced switch turns on. + static const _advanced = 'Enable advanced: priority, status, view modes'; + + /// A row that opens a sub-screen: its glyph (muted = not recommended). + Widget _link(Icon icon, Future Function() open) => ListTile( + contentPadding: EdgeInsets.zero, + leading: icon, + trailing: const Icon(Icons.chevron_right), + onTap: () => unawaited(open()), + ); } diff --git a/pubspec.yaml b/pubspec.yaml index a123c84..fdeee76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.72+1 +version: 1.0.73+1 environment: sdk: ^3.12.2 diff --git a/test/capture_screen_editing_test.dart b/test/capture_screen_editing_test.dart index 7633dcb..149eb2b 100644 --- a/test/capture_screen_editing_test.dart +++ b/test/capture_screen_editing_test.dart @@ -29,18 +29,18 @@ void main() { // Defaults to Raw: a single text field, no note persisted yet. expect(find.byType(TextField), findsOneWidget); - expect(find.text('Guided'), findsOneWidget); + expect(find.bySemanticsLabel('Guided'), findsOneWidget); expect(find.text('0'), findsOneWidget); // Tapping Guided on the empty draft opens the priority+template wizard // rather than jumping straight into the stepper. - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); - expect(find.text('Step 1 of 2'), findsOneWidget); + expect(find.bySemanticsLabel('Step 1 of 2'), findsOneWidget); - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); - await tester.tap(find.text('Start')); + await tester.tap(find.bySemanticsLabel('Start')); await tester.pump(); // Now on step 1 of the full-screen step page. @@ -55,7 +55,7 @@ void main() { testWidgets('saving the untouched template creates no note', (tester) async { final repo = await pumpCapture(tester); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(); expect(await repo.listNotes(), isEmpty); @@ -83,7 +83,7 @@ void main() { await tester.enterText(find.byType(TextField).first, 'A real idea'); await tester.pump(); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(); expect(await repo.listNotes(), hasLength(1)); @@ -98,7 +98,7 @@ void main() { ) async { await pumpCapture(tester); // empty prefs → no token → not configured - await tester.tap(find.byTooltip('Sync')); + await tester.tap(find.bySemanticsLabel('Sync')); await tester.pump(); // settings load + snackbar await tester.pump(); @@ -114,11 +114,11 @@ void main() { ) async { await pumpCapture(tester); - await tester.tap(find.byTooltip('Notes')); + await tester.tap(find.bySemanticsLabel('Notes')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); // route transition - expect(find.text('Notes'), findsOneWidget); // list screen app bar title + expect(find.byIcon(Icons.note), findsOneWidget); // list screen title }); testWidgets('changing the priority dropdown updates the saved note', ( @@ -134,7 +134,7 @@ void main() { ); await tester.pump(); await tester.pump(const Duration(milliseconds: 400)); // menu open - await tester.tap(find.text('High').last); + await tester.tap(find.bySemanticsLabel('High').last); await tester.pump(); expect((await repo.listNotes()).single.priority, Priority.high); @@ -153,7 +153,7 @@ void main() { ); await tester.pump(); await tester.pump(const Duration(milliseconds: 400)); // menu open - await tester.tap(find.text('In progress').last); + await tester.tap(find.bySemanticsLabel('In progress').last); await tester.pump(); expect((await repo.listNotes()).single.status, Status.inProgress); diff --git a/test/capture_screen_sync_test.dart b/test/capture_screen_sync_test.dart index 864661e..7562db6 100644 --- a/test/capture_screen_sync_test.dart +++ b/test/capture_screen_sync_test.dart @@ -35,7 +35,7 @@ void main() { }); await pumpCapture(tester, prefs: configuredPrefs, httpClient: mock); - await tester.tap(find.byTooltip('Sync')); + await tester.tap(find.bySemanticsLabel('Sync')); await tester.pump(); // setState(_syncing = true) await tester.pump(); // service runs, snackbar scheduled await tester.pump(); // snackbar builds @@ -55,7 +55,7 @@ void main() { final mock = MockClient((_) async => throw Exception('offline')); await pumpCapture(tester, prefs: configuredPrefs, httpClient: mock); - await tester.tap(find.byTooltip('Sync')); + await tester.tap(find.bySemanticsLabel('Sync')); await tester.pump(); await tester.pump(); await tester.pump(); @@ -85,7 +85,7 @@ void main() { }); await pumpCapture(tester, prefs: configuredPrefs, httpClient: mock); - await tester.tap(find.byTooltip('Sync')); + await tester.tap(find.bySemanticsLabel('Sync')); await tester.pump(); await tester.pump(); await tester.pump(); @@ -101,7 +101,7 @@ void main() { ) async { await pumpCapture(tester); - await tester.tap(find.byTooltip('Settings')); + await tester.tap(find.bySemanticsLabel('Settings')); await tester.pumpAndSettle(); // route transition expect(find.byType(SettingsScreen), findsOneWidget); // settings is up @@ -112,11 +112,11 @@ void main() { // without popping a result at all, so a stale in-memory value would be // wrong there too). This exercises that reload-after-return path, then // Back returns to capture. - await tester.tap(find.text('Advanced sync (GitHub)')); + await tester.tap(find.byIcon(Icons.cloud_sync)); await tester.pumpAndSettle(); // route transition expect(find.byType(GitHubMirrorScreen), findsOneWidget); - await tester.tap(find.text('Save')); + await tester.tap(find.bySemanticsLabel('Save')); await tester.pumpAndSettle(); // save + pop transition back to Settings expect(find.byType(SettingsScreen), findsOneWidget); @@ -124,7 +124,7 @@ void main() { await tester.pumpAndSettle(); // pop transition back to capture expect(find.byType(SettingsScreen), findsNothing); // back on capture - expect(find.byTooltip('Settings'), findsOneWidget); + expect(find.bySemanticsLabel('Settings'), findsOneWidget); }); // A MockClient that records request methods and answers the sync flow: @@ -148,11 +148,11 @@ void main() { await tester.pump(); // The editor's own template/mode chrome is gone. - expect(find.text('Guided'), findsNothing); - expect(find.text('Template'), findsNothing); + expect(find.bySemanticsLabel('Guided'), findsNothing); + expect(find.bySemanticsLabel('Template'), findsNothing); // The capture screen's priority/status row is gone. - expect(find.text('Priority'), findsNothing); - expect(find.text('Status'), findsNothing); + expect(find.bySemanticsLabel('Priority'), findsNothing); + expect(find.bySemanticsLabel('Status'), findsNothing); // Routine status chatter is gone; only a badge count remains. expect(find.textContaining('Autosaves as you type'), findsNothing); expect(find.text('1'), findsOneWidget); @@ -161,29 +161,23 @@ void main() { }, ); - testWidgets( - 'draft text survives an advancedMode toggle mid-typing', - (tester) async { - final appSettings = ValueNotifier( - const AppSettings(advancedMode: true), - ); - await pumpCapture(tester, appSettings: appSettings); + testWidgets('draft text survives an advancedMode toggle mid-typing', ( + tester, + ) async { + final appSettings = ValueNotifier(const AppSettings(advancedMode: true)); + await pumpCapture(tester, appSettings: appSettings); - await tester.enterText( - find.byType(TextField).first, - 'my important idea', - ); - await tester.pump(); + await tester.enterText(find.byType(TextField).first, 'my important idea'); + await tester.pump(); - // NoteForm's metadata row is conditionally present, so toggling - // advancedMode changes the editor's position beneath it — a key one - // level too shallow doesn't survive that reposition and Flutter - // remounts the editor with an empty draft, losing the typing. - appSettings.value = const AppSettings(advancedMode: false); - await tester.pump(); + // NoteForm's metadata row is conditionally present, so toggling + // advancedMode changes the editor's position beneath it — a key one + // level too shallow doesn't survive that reposition and Flutter + // remounts the editor with an empty draft, losing the typing. + appSettings.value = const AppSettings(advancedMode: false); + await tester.pump(); - final field = tester.widget(find.byType(TextField).first); - expect(field.controller?.text, contains('my important idea')); - }, - ); + final field = tester.widget(find.byType(TextField).first); + expect(field.controller?.text, contains('my important idea')); + }); } diff --git a/test/confirm_delete_test.dart b/test/confirm_delete_test.dart new file mode 100644 index 0000000..18b7d72 --- /dev/null +++ b/test/confirm_delete_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:todo/ui/confirm_delete.dart'; + +/// Opens the dialog and returns a getter for its eventual answer. +Future _open(WidgetTester tester) async { + bool? answer; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) => Scaffold( + body: TextButton( + onPressed: () async => answer = await confirmDeleteNote(context), + child: const SizedBox.square(dimension: 40), + ), + ), + ), + ), + ); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + return () => answer; +} + +void main() { + testWidgets('the delete glyph answers yes', (tester) async { + final answer = await _open(tester); + expect(find.bySemanticsLabel('Delete note?'), findsOneWidget); + await tester.tap(find.byKey(const ValueKey('confirm-delete'))); + await tester.pumpAndSettle(); + expect(answer(), isTrue); + }); + + testWidgets('cancel is autofocused, so Return keeps the note', ( + tester, + ) async { + final answer = await _open(tester); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + expect(answer(), isFalse); + }); + + testWidgets('Escape answers no', (tester) async { + final answer = await _open(tester); + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + expect(answer(), isFalse); + }); + + testWidgets('tapping the barrier answers no', (tester) async { + final answer = await _open(tester); + await tester.tapAt(const Offset(5, 5)); + await tester.pumpAndSettle(); + expect(answer(), isFalse); + }); +} diff --git a/test/github_mirror_device_flow_test.dart b/test/github_mirror_device_flow_test.dart index 526640f..b788066 100644 --- a/test/github_mirror_device_flow_test.dart +++ b/test/github_mirror_device_flow_test.dart @@ -72,7 +72,7 @@ void main() { httpClient: mock, ); - await tester.tap(find.text('Connect GitHub')); + await tester.tap(find.bySemanticsLabel('Connect GitHub')); await tester.pump(); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); @@ -135,7 +135,7 @@ void main() { await tester.pump(const Duration(milliseconds: 400)); // route transition expect(find.byType(GitHubMirrorScreen), findsOneWidget); // screen is up - await tester.tap(find.text('Save')); + await tester.tap(find.bySemanticsLabel('Save')); await tester.pump(); // run _save (persist + pop) await tester.pump(const Duration(milliseconds: 400)); // pop transition @@ -195,7 +195,7 @@ void main() { httpClient: mock, ); - await tester.tap(find.text('Connect GitHub')); + await tester.tap(find.bySemanticsLabel('Connect GitHub')); await tester.pump(); // requestDeviceCode await tester.pump(); // dialog builds, poll starts (interval 0) expect(find.text('WXYZ-1234'), findsOneWidget); diff --git a/test/github_mirror_screen_test.dart b/test/github_mirror_screen_test.dart index d9515a0..751913b 100644 --- a/test/github_mirror_screen_test.dart +++ b/test/github_mirror_screen_test.dart @@ -43,7 +43,7 @@ void main() { tester, ) async { await pumpMirror(tester); - await tester.tap(find.text('Connect GitHub')); + await tester.tap(find.bySemanticsLabel('Connect GitHub')); await tester.pump(); expect( find.textContaining('Enter the OAuth App client id'), @@ -55,7 +55,7 @@ void main() { final mock = MockClient((_) async => http.Response('{}', 200)); await pumpMirror(tester, httpClient: mock); - await tester.tap(find.text('Test connection')); + await tester.tap(find.bySemanticsLabel('Test connection')); await tester.pump(); // start await tester.pump(); // resolve future + rebuild @@ -66,7 +66,7 @@ void main() { final mock = MockClient((_) async => http.Response('', 404)); await pumpMirror(tester, httpClient: mock); - await tester.tap(find.text('Test connection')); + await tester.tap(find.bySemanticsLabel('Test connection')); await tester.pump(); await tester.pump(); @@ -77,7 +77,7 @@ void main() { final mock = MockClient((_) async => throw Exception('offline')); await pumpMirror(tester, httpClient: mock); - await tester.tap(find.text('Test connection')); + await tester.tap(find.bySemanticsLabel('Test connection')); await tester.pump(); await tester.pump(); @@ -97,7 +97,7 @@ void main() { httpClient: mock, ); - await tester.tap(find.text('Connect GitHub')); + await tester.tap(find.bySemanticsLabel('Connect GitHub')); await tester.pump(); await tester.pump(); @@ -133,7 +133,7 @@ void main() { httpClient: mock, ); - await tester.tap(find.text('Connect GitHub')); + await tester.tap(find.bySemanticsLabel('Connect GitHub')); await tester.pump(); // requestDeviceCode await tester.pump(); // dialog builds, shows the user code expect(find.text('WXYZ-1234'), findsOneWidget); diff --git a/test/motion/capture_confirmation_test.dart b/test/motion/capture_confirmation_test.dart index ce5ca37..c11752d 100644 --- a/test/motion/capture_confirmation_test.dart +++ b/test/motion/capture_confirmation_test.dart @@ -38,7 +38,7 @@ void main() { await pumpCapture(tester); await _type(tester); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(); // one frame: the cue must not wait for anything expect(haptics, [_lightImpact]); @@ -52,7 +52,7 @@ void main() { await pumpCapture(tester); await _type(tester); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(_mark, findsOneWidget); @@ -74,7 +74,7 @@ void main() { final haptics = recordHaptics(tester); await pumpCapture(tester); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(const Duration(milliseconds: 100)); expect(haptics, isEmpty); @@ -87,16 +87,18 @@ void main() { final haptics = recordHaptics(tester); await pumpCapture(tester); await _type(tester); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(); // the ticker's clock starts on its first frame await tester.pump(const Duration(milliseconds: 700)); - await tester.tap(find.byTooltip('New note')); // nothing new to capture + await tester.tap( + find.bySemanticsLabel('New note'), + ); // nothing new to capture await tester.pump(const Duration(milliseconds: 100)); expect(haptics, hasLength(1)); expect(_mark, findsNothing); await _type(tester); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(const Duration(milliseconds: 100)); expect(haptics, hasLength(2)); expect(_mark, findsOneWidget); @@ -111,7 +113,7 @@ void main() { await pumpCapture(tester); await _type(tester); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(); expect(haptics, [_lightImpact]); @@ -130,7 +132,7 @@ void main() { await pumpCapture(tester); final before = tester.getTopLeft(find.text('0')); await _type(tester); - await tester.tap(find.byTooltip('New note')); + await tester.tap(find.bySemanticsLabel('New note')); await tester.pump(const Duration(milliseconds: 300)); expect(tester.getTopLeft(find.text('1')), before); await tester.pump(const Duration(milliseconds: 400)); diff --git a/test/motion/motion_tokens_test.dart b/test/motion/motion_tokens_test.dart index 63d3937..3953f56 100644 --- a/test/motion/motion_tokens_test.dart +++ b/test/motion/motion_tokens_test.dart @@ -73,7 +73,7 @@ void main() { final total = spec.sections.length; expect(_barValue(tester), closeTo(1 / total, 1e-9)); - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); await tester.pump(AppDuration.base ~/ 2); expect(_barValue(tester), inExclusiveRange(1 / total, 2 / total)); @@ -85,7 +85,7 @@ void main() { testWidgets('remove-animations: the progress bar jumps', (tester) async { disableAnimations(tester); await pumpEditor(tester, initialTemplate: spec); - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); expect(_barValue(tester), closeTo(2 / spec.sections.length, 1e-9)); }); diff --git a/test/note_detail_screen_test.dart b/test/note_detail_screen_test.dart index 9d9db74..e110ebb 100644 --- a/test/note_detail_screen_test.dart +++ b/test/note_detail_screen_test.dart @@ -61,7 +61,7 @@ void main() { find.byWidgetPredicate((w) => w is DropdownButton), ); await tester.pumpAndSettle(); - await tester.tap(find.text('High').last); + await tester.tap(find.bySemanticsLabel('High').last); await tester.pumpAndSettle(); expect((await repo.listNotes()).single.priority, Priority.high); @@ -74,7 +74,7 @@ void main() { find.byWidgetPredicate((w) => w is DropdownButton), ); await tester.pumpAndSettle(); - await tester.tap(find.text('Done').last); + await tester.tap(find.bySemanticsLabel('Done').last); await tester.pumpAndSettle(); expect((await repo.listNotes()).single.status, Status.done); @@ -85,7 +85,7 @@ void main() { ) async { final repo = await pumpDetail(tester, seedNote('# T\n\n## what\nold')); - await tester.tap(find.text('Raw')); + await tester.tap(find.bySemanticsLabel('Raw')); await tester.pump(); await tester.enterText(find.byType(TextField), '# T\n\n## what\nnew body'); await tester.pump(); @@ -101,20 +101,20 @@ void main() { await tester.enterText(find.byType(TextField), ''); await tester.pump(); - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); - expect(find.text('Step 1 of 2'), findsOneWidget); + expect(find.bySemanticsLabel('Step 1 of 2'), findsOneWidget); // Wizard hides the Status dropdown (onChromeVisibleChanged(false)). - expect(find.text('Status'), findsNothing); + expect(find.bySemanticsLabel('Status'), findsNothing); await tester.tap(find.byType(DropdownButtonFormField)); await tester.pumpAndSettle(); - await tester.tap(find.text('High').last); + await tester.tap(find.bySemanticsLabel('High').last); await tester.pumpAndSettle(); - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); - await tester.tap(find.text('Start')); + await tester.tap(find.bySemanticsLabel('Start')); await tester.pump(); expect((await repo.listNotes()).single.priority, Priority.high); @@ -134,11 +134,11 @@ void main() { advancedMode: false, ); - expect(find.text('Template'), findsNothing); - expect(find.text('Guided'), findsNothing); - expect(find.text('Raw'), findsNothing); - expect(find.text('Priority'), findsNothing); - expect(find.text('Status'), findsNothing); + expect(find.bySemanticsLabel('Template'), findsNothing); + expect(find.bySemanticsLabel('Guided'), findsNothing); + expect(find.bySemanticsLabel('Raw'), findsNothing); + expect(find.bySemanticsLabel('Priority'), findsNothing); + expect(find.bySemanticsLabel('Status'), findsNothing); // The body itself is still editable — only the chrome is gone. expect(find.byType(TextField), findsOneWidget); }); @@ -146,13 +146,13 @@ void main() { testWidgets('the delete action removes the note and pops', (tester) async { final repo = await pumpDetail(tester, seedNote('# Bye')); - await tester.tap(find.byTooltip('Delete note')); + await tester.tap(find.bySemanticsLabel('Delete note')); await tester.pumpAndSettle(); // Delete is now confirmed. Deleting a note is irreversible and there is no // undo, so a single Return on a focused control must not destroy it. - expect(find.text('Delete note?'), findsOneWidget); - await tester.tap(find.text('Delete')); + expect(find.bySemanticsLabel('Delete note?'), findsOneWidget); + await tester.tap(find.byKey(const ValueKey('confirm-delete'))); await tester.pumpAndSettle(); expect(await repo.listNotes(), isEmpty); @@ -162,9 +162,9 @@ void main() { testWidgets('cancelling the delete keeps the note', (tester) async { final repo = await pumpDetail(tester, seedNote('# Keep me')); - await tester.tap(find.byTooltip('Delete note')); + await tester.tap(find.bySemanticsLabel('Delete note')); await tester.pumpAndSettle(); - await tester.tap(find.text('Cancel')); + await tester.tap(find.byKey(const ValueKey('confirm-cancel'))); await tester.pumpAndSettle(); expect(await repo.listNotes(), hasLength(1)); diff --git a/test/note_editor_guided_test.dart b/test/note_editor_guided_test.dart index e0008b0..14c0ae1 100644 --- a/test/note_editor_guided_test.dart +++ b/test/note_editor_guided_test.dart @@ -13,7 +13,7 @@ void main() { ) async { final emitted = await pumpEditor(tester, initialTemplate: spec); - expect(find.byTooltip('Exit guided'), findsOneWidget); + expect(find.bySemanticsLabel('Exit guided'), findsOneWidget); await tester.enterText(find.byType(TextField).first, 'Dark mode'); await tester.pump(); @@ -28,11 +28,11 @@ void main() { expect(find.byType(LinearProgressIndicator), findsOneWidget); expect(find.text('1 / ${spec.sections.length}'), findsOneWidget); - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); expect(find.text('2 / ${spec.sections.length}'), findsOneWidget); - await tester.tap(find.text('Back')); + await tester.tap(find.bySemanticsLabel('Back')); await tester.pump(); expect(find.text('1 / ${spec.sections.length}'), findsOneWidget); expect(find.byType(LinearProgressIndicator), findsOneWidget); @@ -50,20 +50,22 @@ void main() { final section = spec.sections[i]; expect(find.text('${i + 1} / ${spec.sections.length}'), findsOneWidget); expect( - find.text(section.isTitle ? 'title' : section.label), + section.isTitle + ? find.bySemanticsLabel('title') + : find.text(section.label), findsWidgets, reason: 'step ${i + 1} should show "${section.label}"', ); expect(find.text(section.helper), findsOneWidget); if (i < spec.sections.length - 1) { - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); } } // The last step is `read first`; there is no Next past it. expect(spec.sections.last.label, 'read first'); - expect(find.text('Next'), findsNothing); + expect(find.bySemanticsLabel('Next'), findsNothing); }); testWidgets('pasting a bare link drops the draft to the blank template', ( @@ -84,7 +86,7 @@ void main() { // Freeform now, so there is no seven-step stepper to walk for a link. expect(find.text('Blank'), findsOneWidget); - expect(find.text('Guided'), findsNothing); + expect(find.bySemanticsLabel('Guided'), findsNothing); // Adding prose puts the spec template back — lossless in both directions. await tester.enterText( @@ -93,7 +95,7 @@ void main() { ); await tester.pump(); expect(find.text('LLM design spec'), findsOneWidget); - expect(find.text('Guided'), findsOneWidget); + expect(find.bySemanticsLabel('Guided'), findsOneWidget); }); testWidgets('an explicit template choice survives further typing', ( @@ -126,7 +128,7 @@ void main() { await tester.enterText(find.byType(TextField).first, '# Render me'); await tester.pump(); - await tester.tap(find.text('View')); + await tester.tap(find.bySemanticsLabel('View')); await tester.pump(); expect(find.byType(MarkdownView), findsOneWidget); diff --git a/test/note_editor_modes_test.dart b/test/note_editor_modes_test.dart index 24aa8e7..b173dc8 100644 --- a/test/note_editor_modes_test.dart +++ b/test/note_editor_modes_test.dart @@ -15,7 +15,7 @@ void main() { await tester.enterText(find.byType(TextField).first, 'T'); await tester.pump(); - await tester.tap(find.byTooltip('Exit guided')); + await tester.tap(find.bySemanticsLabel('Exit guided')); await tester.pump(); final raw = tester.widget(find.byType(TextField)); @@ -38,7 +38,7 @@ void main() { ); // Structured template + non-conforming raw text → switching is refused. - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); expect(find.textContaining("doesn't match the template"), findsOneWidget); @@ -56,25 +56,28 @@ void main() { // Bare guided -> raw via the back arrow makes the (still conforming) // body the source… - await tester.tap(find.byTooltip('Exit guided')); + await tester.tap(find.bySemanticsLabel('Exit guided')); await tester.pump(); expect(find.byType(LinearProgressIndicator), findsNothing); // …then raw -> guided re-parses the conforming body straight back into // the bare stepper — no wizard, since the content isn't empty. - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); expect(find.byType(LinearProgressIndicator), findsOneWidget); - expect(find.text('Step 1 of 2'), findsNothing); // wizard was skipped + expect( + find.bySemanticsLabel('Step 1 of 2'), + findsNothing, + ); // wizard was skipped }, ); testWidgets('freeform template offers only View and Raw', (tester) async { final emitted = await pumpEditor(tester, initialTemplate: blank); - expect(find.text('Guided'), findsNothing); - expect(find.text('View'), findsOneWidget); - expect(find.text('Raw'), findsOneWidget); + expect(find.bySemanticsLabel('Guided'), findsNothing); + expect(find.bySemanticsLabel('View'), findsOneWidget); + expect(find.bySemanticsLabel('Raw'), findsOneWidget); await tester.enterText(find.byType(TextField), 'free text'); await tester.pump(); @@ -89,7 +92,10 @@ void main() { initialTemplate: spec, initialMode: NoteEditorMode.raw, ); - expect(find.text('Guided'), findsOneWidget); // structured: Guided offered + expect( + find.bySemanticsLabel('Guided'), + findsOneWidget, + ); // structured: Guided offered await tester.tap(find.text('LLM design spec').first); // open the menu await tester.pumpAndSettle(); @@ -97,7 +103,7 @@ void main() { await tester.pumpAndSettle(); // Freeform now: Guided is no longer offered. - expect(find.text('Guided'), findsNothing); + expect(find.bySemanticsLabel('Guided'), findsNothing); }); testWidgets('detects a conforming note (no template given) as guided', ( @@ -110,7 +116,7 @@ void main() { find.byType(LinearProgressIndicator), findsOneWidget, ); // guided by default - expect(find.byTooltip('Exit guided'), findsOneWidget); + expect(find.bySemanticsLabel('Exit guided'), findsOneWidget); }); testWidgets('detects a legacy note (no template given) as freeform raw', ( @@ -120,7 +126,7 @@ void main() { // Non-conforming → blank/raw, no guided stepper offered. expect(find.byType(LinearProgressIndicator), findsNothing); - expect(find.text('Guided'), findsNothing); + expect(find.bySemanticsLabel('Guided'), findsNothing); }); testWidgets('initialMode preview opens directly in the rendered view', ( @@ -147,7 +153,7 @@ void main() { initialMode: NoteEditorMode.preview, ); - await tester.tap(find.text('Raw')); + await tester.tap(find.bySemanticsLabel('Raw')); await tester.pump(); final raw = tester.widget(find.byType(TextField)); diff --git a/test/note_editor_wizard_test.dart b/test/note_editor_wizard_test.dart index 5534f39..9542dda 100644 --- a/test/note_editor_wizard_test.dart +++ b/test/note_editor_wizard_test.dart @@ -17,10 +17,10 @@ void main() { initialMode: NoteEditorMode.raw, ); - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); - expect(find.text('Step 1 of 2'), findsOneWidget); + expect(find.bySemanticsLabel('Step 1 of 2'), findsOneWidget); expect(find.byType(DropdownButtonFormField), findsOneWidget); expect(find.byType(LinearProgressIndicator), findsNothing); }, @@ -34,17 +34,17 @@ void main() { initialTemplate: spec, initialMode: NoteEditorMode.raw, ); - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); - expect(find.text('Step 2 of 2'), findsOneWidget); + expect(find.bySemanticsLabel('Step 2 of 2'), findsOneWidget); expect(find.byType(DropdownButtonFormField), findsOneWidget); - await tester.tap(find.text('Back')); + await tester.tap(find.bySemanticsLabel('Back')); await tester.pump(); - expect(find.text('Step 1 of 2'), findsOneWidget); + expect(find.bySemanticsLabel('Step 1 of 2'), findsOneWidget); }, ); @@ -56,9 +56,9 @@ void main() { initialTemplate: spec, initialMode: NoteEditorMode.raw, ); - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); await tester.tap(find.text('LLM design spec').first); // open the menu @@ -68,7 +68,7 @@ void main() { // Select the (only) offered template, exercising the dropdown's onChanged. await tester.tap(find.text('LLM design spec').last); await tester.pumpAndSettle(); - expect(find.text('Step 2 of 2'), findsOneWidget); + expect(find.bySemanticsLabel('Step 2 of 2'), findsOneWidget); }); testWidgets( @@ -81,22 +81,22 @@ void main() { initialMode: NoteEditorMode.raw, onPriorityChanged: (p) => committed = p, ); - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); await tester.tap(find.byType(DropdownButtonFormField)); await tester.pumpAndSettle(); - await tester.tap(find.text('High').last); + await tester.tap(find.bySemanticsLabel('High').last); await tester.pumpAndSettle(); - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); - await tester.tap(find.text('Start')); + await tester.tap(find.bySemanticsLabel('Start')); await tester.pump(); expect(committed, Priority.high); expect(find.byType(LinearProgressIndicator), findsOneWidget); - expect(find.byTooltip('Exit guided'), findsOneWidget); + expect(find.bySemanticsLabel('Exit guided'), findsOneWidget); }, ); @@ -110,16 +110,19 @@ void main() { initialMode: NoteEditorMode.raw, onChromeVisibleChanged: chromeVisible.add, ); - await tester.tap(find.text('Guided')); + await tester.tap(find.bySemanticsLabel('Guided')); await tester.pump(); expect(chromeVisible.last, false); - await tester.tap(find.byTooltip('Cancel')); + await tester.tap(find.bySemanticsLabel('Cancel')); await tester.pump(); expect(chromeVisible.last, true); expect(find.byType(LinearProgressIndicator), findsNothing); - expect(find.text('Raw'), findsOneWidget); // chrome's mode selector is back + expect( + find.bySemanticsLabel('Raw'), + findsOneWidget, + ); // chrome's mode selector is back }); testWidgets('exiting bare Guided via the back arrow restores the chrome', ( @@ -135,11 +138,11 @@ void main() { ); expect(find.byType(LinearProgressIndicator), findsOneWidget); - await tester.tap(find.byTooltip('Exit guided')); + await tester.tap(find.bySemanticsLabel('Exit guided')); await tester.pump(); expect(chromeVisible, contains(true)); - expect(find.text('Raw'), findsOneWidget); + expect(find.bySemanticsLabel('Raw'), findsOneWidget); }); testWidgets('last step shows Done instead of Next', (tester) async { @@ -147,12 +150,12 @@ void main() { // Navigate to the final step. for (var i = 0; i < spec.sections.length - 1; i++) { - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); } - expect(find.text('Done'), findsOneWidget); - expect(find.text('Next'), findsNothing); + expect(find.bySemanticsLabel('Done'), findsOneWidget); + expect(find.bySemanticsLabel('Next'), findsNothing); expect( find.text('${spec.sections.length} / ${spec.sections.length}'), findsOneWidget, @@ -167,10 +170,10 @@ void main() { await tester.pump(); for (var i = 0; i < spec.sections.length - 1; i++) { - await tester.tap(find.text('Next')); + await tester.tap(find.bySemanticsLabel('Next')); await tester.pump(); } - await tester.tap(find.text('Done')); + await tester.tap(find.bySemanticsLabel('Done')); await tester.pump(); expect(find.byType(LinearProgressIndicator), findsNothing); diff --git a/test/notes_grid_test.dart b/test/notes_grid_test.dart index 05be803..b0234b3 100644 --- a/test/notes_grid_test.dart +++ b/test/notes_grid_test.dart @@ -103,8 +103,8 @@ void main() { await t.tap(find.text('Open me')); await t.pumpAndSettle(); - expect(find.text('Priority'), findsOneWidget); - expect(find.text('View'), findsOneWidget); + expect(find.bySemanticsLabel('Priority'), findsOneWidget); + expect(find.bySemanticsLabel('View'), findsOneWidget); }); testWidgets('long-press opens the sheet with body, dates and actions', ( @@ -121,10 +121,10 @@ void main() { expect(find.textContaining('Two litres of oat'), findsOneWidget); expect(find.textContaining('Edited'), findsOneWidget); expect(find.textContaining('Created'), findsOneWidget); - await t.tap(find.text('In progress')); + await t.tap(find.bySemanticsLabel('In progress')); await t.pump(); expect((await repo.listNotes()).single.status, Status.inProgress); - expect(find.text('Delete note'), findsOneWidget); + expect(find.bySemanticsLabel('Delete note'), findsOneWidget); }); testWidgets('a title-only note adds no body to the sheet', (t) async { @@ -142,9 +142,9 @@ void main() { final repo = await pumpList(t, seed: [note('a', 'Delete me')]); await _openSheet(t, 'Delete me'); - await t.tap(find.text('Delete note')); + await t.tap(find.bySemanticsLabel('Delete note')); await t.pumpAndSettle(); - await t.tap(find.text('Delete')); + await t.tap(find.byKey(const ValueKey('confirm-delete'))); await t.pumpAndSettle(); expect(await repo.listNotes(), isEmpty); diff --git a/test/notes_list_filter_test.dart b/test/notes_list_filter_test.dart index 040f389..f3fd2a4 100644 --- a/test/notes_list_filter_test.dart +++ b/test/notes_list_filter_test.dart @@ -10,7 +10,7 @@ import 'notes_list_harness.dart'; /// Opens a date field's menu and picks [entry]. Future pickRange(WidgetTester tester, String field, String entry) async { - await tester.tap(find.byTooltip(field)); + await tester.tap(find.bySemanticsLabel(field)); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); await tester.tap(find.text(entry).last); @@ -82,9 +82,9 @@ void main() { await tester.tap(find.byIcon(Icons.filter_list)); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('Done')); // add a non-default status + await tester.tap(find.bySemanticsLabel('Done')); // non-default status await tester.pump(); - await tester.tap(find.text('Clear all')); + await tester.tap(find.bySemanticsLabel('Clear all')); await tester.pump(); await tapApply(tester); await tester.pump(); @@ -139,7 +139,7 @@ void main() { await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('High')); // priority chip toggle + await tester.tap(find.bySemanticsLabel('High')); // priority chip toggle await pickRange(tester, 'Last updated', 'Today'); await tapApply(tester); await tester.pump(); @@ -178,10 +178,10 @@ void main() { testWidgets('per-note sheet changes priority via a chip', (tester) async { final repo = await pumpList(tester, seed: [note('a', 'Repriortise me')]); - await tester.tap(find.byTooltip('Quick actions')); + await tester.tap(find.bySemanticsLabel('Quick actions')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('Low')); // default is Medium → change to Low + await tester.tap(find.bySemanticsLabel('Low')); // Medium → Low await tester.pump(); expect((await repo.listNotes()).single.priority, Priority.low); @@ -196,7 +196,7 @@ void main() { // The detail screen shows the note title in its app bar plus the // Priority/Status meta dropdowns and the editor mode toggle. - expect(find.text('Priority'), findsOneWidget); - expect(find.text('View'), findsOneWidget); + expect(find.bySemanticsLabel('Priority'), findsOneWidget); + expect(find.bySemanticsLabel('View'), findsOneWidget); }); } diff --git a/test/notes_list_focus_test.dart b/test/notes_list_focus_test.dart index 8482b1d..90dba6e 100644 --- a/test/notes_list_focus_test.dart +++ b/test/notes_list_focus_test.dart @@ -37,7 +37,7 @@ void main() { ) async { await pumpList(tester, seed: [note('a', 'Buy milk')]); await editNoteAndSwipeBack(tester); - expect(find.text('Search notes…'), findsOneWidget); + expect(find.byIcon(Icons.search), findsOneWidget); expect(textFieldFocused(), isFalse); }); @@ -48,7 +48,7 @@ void main() { await tester.tap(find.byType(TextField).first); await tester.pump(); await editNoteAndSwipeBack(tester); - expect(find.text('Search notes…'), findsOneWidget); + expect(find.byIcon(Icons.search), findsOneWidget); expect(textFieldFocused(), isFalse); }); } diff --git a/test/notes_list_harness.dart b/test/notes_list_harness.dart index 80714d9..75e4e3a 100644 --- a/test/notes_list_harness.dart +++ b/test/notes_list_harness.dart @@ -53,7 +53,8 @@ Future pumpList( /// sheet, so on a short viewport it legitimately sits below the fold: /// scroll it into view first, as a user would. Future tapApply(WidgetTester tester) async { - await tester.ensureVisible(find.text('Apply')); + final apply = find.widgetWithIcon(FilledButton, Icons.check); + await tester.ensureVisible(apply); await tester.pump(); - await tester.tap(find.text('Apply')); + await tester.tap(apply); } diff --git a/test/notes_list_one_list_test.dart b/test/notes_list_one_list_test.dart index 8250233..ac3fafa 100644 --- a/test/notes_list_one_list_test.dart +++ b/test/notes_list_one_list_test.dart @@ -79,7 +79,10 @@ void main() { if (axisDirectionToAxis(s.axisDirection) == Axis.vertical) expect(s.position.maxScrollExtent, 0); } - expect(find.text('Apply').hitTestable(), findsOneWidget); + expect( + find.widgetWithIcon(FilledButton, Icons.check).hitTestable(), + findsOneWidget, + ); expect(find.text('Repo tasks'), findsNothing); expect(find.text('Ideas'), findsNothing); expect(find.byType(ActionChip), findsNothing); diff --git a/test/notes_list_screen_test.dart b/test/notes_list_screen_test.dart index a7745fd..f16cd23 100644 --- a/test/notes_list_screen_test.dart +++ b/test/notes_list_screen_test.dart @@ -51,7 +51,7 @@ void main() { await tester.tap(find.byIcon(Icons.sort)); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); // menu open - await tester.tap(find.text('Alphabetical').last); + await tester.tap(find.bySemanticsLabel('Alphabetical').last); await tester.pump(); expect(repo.lastSort, NoteSort.alphabetical); @@ -63,9 +63,9 @@ void main() { await tester.tap(find.byIcon(Icons.filter_list)); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); // sheet open - await tester.tap(find.text('Done')); + await tester.tap(find.bySemanticsLabel('Done')); await tester.pump(); - await tester.tap(find.text('Apply')); + await tapApply(tester); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); // sheet close @@ -77,16 +77,16 @@ void main() { testWidgets('per-note sheet deletes the note', (tester) async { final repo = await pumpList(tester, seed: [note('a', 'Delete me')]); - await tester.tap(find.byTooltip('Quick actions')); + await tester.tap(find.bySemanticsLabel('Quick actions')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); // sheet open - await tester.tap(find.text('Delete note')); + await tester.tap(find.bySemanticsLabel('Delete note')); await tester.pumpAndSettle(); // Delete is now confirmed on this path too: it is irreversible, has no // undo, and is one Tab from a Return in the sheet's chip rows. - expect(find.text('Delete note?'), findsOneWidget); - await tester.tap(find.text('Delete')); + expect(find.bySemanticsLabel('Delete note?'), findsOneWidget); + await tester.tap(find.byKey(const ValueKey('confirm-delete'))); await tester.pumpAndSettle(); expect(await repo.listNotes(), isEmpty); @@ -95,12 +95,12 @@ void main() { testWidgets('cancelling the sheet delete keeps the note', (tester) async { final repo = await pumpList(tester, seed: [note('a', 'Keep me')]); - await tester.tap(find.byTooltip('Quick actions')); + await tester.tap(find.bySemanticsLabel('Quick actions')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('Delete note')); + await tester.tap(find.bySemanticsLabel('Delete note')); await tester.pumpAndSettle(); - await tester.tap(find.text('Cancel')); + await tester.tap(find.byKey(const ValueKey('confirm-cancel'))); await tester.pumpAndSettle(); expect(await repo.listNotes(), hasLength(1)); @@ -109,10 +109,10 @@ void main() { testWidgets('per-note sheet changes status via a chip', (tester) async { final repo = await pumpList(tester, seed: [note('a', 'Change me')]); - await tester.tap(find.byTooltip('Quick actions')); + await tester.tap(find.bySemanticsLabel('Quick actions')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); - await tester.tap(find.text('In progress')); + await tester.tap(find.bySemanticsLabel('In progress')); await tester.pump(); expect((await repo.listNotes()).single.status, Status.inProgress); diff --git a/test/settings_screen_backup_test.dart b/test/settings_screen_backup_test.dart index 6638607..919775c 100644 --- a/test/settings_screen_backup_test.dart +++ b/test/settings_screen_backup_test.dart @@ -96,7 +96,7 @@ void main() { ], ); - await tester.tap(find.text('Sync settings')); + await tester.tap(find.byIcon(Icons.sync)); await tester.pumpAndSettle(); // _export does real file I/O on desktop, so drive it under runAsync. @@ -116,7 +116,7 @@ void main() { ) async { await pumpSettings(tester, repository: _ExplodingRepo()); - await tester.tap(find.text('Sync settings')); + await tester.tap(find.byIcon(Icons.sync)); await tester.pumpAndSettle(); await tester.runAsync(() async { @@ -149,7 +149,7 @@ void main() { final repo = await pumpSettings(tester); - await tester.tap(find.text('Sync settings')); + await tester.tap(find.byIcon(Icons.sync)); await tester.pumpAndSettle(); await tester.tap(find.text('Import notes')); @@ -165,7 +165,7 @@ void main() { FileSelectorPlatform.instance = _FakeFileSelector(null); // cancelled final repo = await pumpSettings(tester); - await tester.tap(find.text('Sync settings')); + await tester.tap(find.byIcon(Icons.sync)); await tester.pumpAndSettle(); await tester.tap(find.text('Import notes')); @@ -183,7 +183,7 @@ void main() { FileSelectorPlatform.instance = _ThrowingFileSelector(); final repo = await pumpSettings(tester); - await tester.tap(find.text('Sync settings')); + await tester.tap(find.byIcon(Icons.sync)); await tester.pumpAndSettle(); await tester.tap(find.text('Import notes')); diff --git a/test/settings_screen_test.dart b/test/settings_screen_test.dart index 497fafe..a982421 100644 --- a/test/settings_screen_test.dart +++ b/test/settings_screen_test.dart @@ -61,30 +61,25 @@ class _ThrowingFileSelector extends FileSelectorPlatform } void main() { - testWidgets( - 'Enable advanced toggles and persists locally with no client', - (tester) async { - final appSettings = ValueNotifier( - const AppSettings(advancedMode: false), - ); - await pumpSettings( - tester, - appSettings: appSettings, - firebaseFactory: () async => null, - ); - - await tester.tap(find.text('Enable advanced')); - await tester.pump(); - - expect(appSettings.value.advancedMode, isTrue); - final reloaded = await AppSettings.load(); - expect(reloaded.advancedMode, isTrue); - }, - ); - - testWidgets('Enable advanced mirrors the toggle to Firebase', ( + testWidgets('Enable advanced toggles and persists locally with no client', ( tester, ) async { + final appSettings = ValueNotifier(const AppSettings(advancedMode: false)); + await pumpSettings( + tester, + appSettings: appSettings, + firebaseFactory: () async => null, + ); + + await tester.tap(find.byIcon(Icons.tune)); + await tester.pump(); + + expect(appSettings.value.advancedMode, isTrue); + final reloaded = await AppSettings.load(); + expect(reloaded.advancedMode, isTrue); + }); + + testWidgets('Enable advanced mirrors the toggle to Firebase', (tester) async { String? putPath; final firebase = FirebaseRestClient( databaseUrl: 'https://x-rtdb.europe-west1.firebasedatabase.app', @@ -110,7 +105,7 @@ void main() { firebaseFactory: () async => firebase, ); - await tester.tap(find.text('Enable advanced')); + await tester.tap(find.byIcon(Icons.tune)); await tester.pump(); expect(appSettings.value.advancedMode, isTrue); @@ -120,16 +115,14 @@ void main() { testWidgets( 'Enable advanced still persists locally when opening Firebase throws', (tester) async { - final appSettings = ValueNotifier( - const AppSettings(advancedMode: false), - ); + final appSettings = ValueNotifier(const AppSettings(advancedMode: false)); await pumpSettings( tester, appSettings: appSettings, firebaseFactory: () async => throw FirebaseAuthError('wrong account'), ); - await tester.tap(find.text('Enable advanced')); + await tester.tap(find.byIcon(Icons.tune)); await tester.pump(); // A signed-in-as-wrong-account device must not lose the local toggle. @@ -137,9 +130,7 @@ void main() { }, ); - testWidgets('Enable advanced logs a settings_toggle event', ( - tester, - ) async { + testWidgets('Enable advanced logs a settings_toggle event', (tester) async { const analytics = AnalyticsService(nodeId: 'device-a'); final appSettings = ValueNotifier(const AppSettings(advancedMode: false)); await pumpSettings( @@ -149,7 +140,7 @@ void main() { firebaseFactory: () async => null, ); - await tester.tap(find.text('Enable advanced')); + await tester.tap(find.byIcon(Icons.tune)); await tester.pump(); final prefs = await SharedPreferences.getInstance(); @@ -162,9 +153,9 @@ void main() { await pumpSettings(tester); // The slim screen only links out; neither sync surface's own widgets // should be inlined here anymore. - expect(find.text('Sync settings'), findsOneWidget); - expect(find.text('Advanced sync (GitHub)'), findsOneWidget); - expect(find.text('Connect GitHub'), findsNothing); + expect(find.byIcon(Icons.sync), findsOneWidget); + expect(find.byIcon(Icons.cloud_sync), findsOneWidget); + expect(find.bySemanticsLabel('Connect GitHub'), findsNothing); expect(find.text('Sign in with Google'), findsNothing); }); @@ -173,7 +164,7 @@ void main() { ) async { await pumpSettings(tester); - await tester.tap(find.text('Sync settings')); + await tester.tap(find.byIcon(Icons.sync)); await tester.pumpAndSettle(); expect(find.byType(SyncSettingsScreen), findsOneWidget); @@ -186,7 +177,7 @@ void main() { const initial = SyncSettings(owner: 'o', repo: 'r', token: 'tok'); final repo = await pumpSettings(tester, initial: initial); - await tester.tap(find.text('Advanced sync (GitHub)')); + await tester.tap(find.byIcon(Icons.cloud_sync)); await tester.pumpAndSettle(); expect(find.byType(GitHubMirrorScreen), findsOneWidget); diff --git a/test/ui_images/attachment_picking_test.dart b/test/ui_images/attachment_picking_test.dart index a30e5b3..1e1e43a 100644 --- a/test/ui_images/attachment_picking_test.dart +++ b/test/ui_images/attachment_picking_test.dart @@ -15,11 +15,11 @@ void main() { ), ), ); - await tester.tap(find.byTooltip('Attach')); + await tester.tap(find.bySemanticsLabel('Attach')); await tester.pump(); expect(got, isEmpty); result = [XFile('/picked/a.pdf')]; - await tester.tap(find.byTooltip('Attach')); + await tester.tap(find.bySemanticsLabel('Attach')); await tester.pump(); expect(got.single.single.name, 'a.pdf'); }); @@ -55,7 +55,7 @@ void main() { Future choose(WidgetTester tester, String entry) async { await tester.tap(find.text('pick')); await tester.pumpAndSettle(); - await tester.tap(find.text(entry)); + await tester.tap(find.bySemanticsLabel(entry)); await tester.pumpAndSettle(); } @@ -64,9 +64,9 @@ void main() { ) async { final asked = []; await openSheet(tester, asked); - expect(find.text('Camera'), findsOneWidget); - expect(find.text('File'), findsOneWidget); - await tester.tap(find.text('Gallery')); + expect(find.bySemanticsLabel('Camera'), findsOneWidget); + expect(find.bySemanticsLabel('File'), findsOneWidget); + await tester.tap(find.bySemanticsLabel('Gallery')); await tester.pumpAndSettle(); await choose(tester, 'Camera'); await choose(tester, 'File'); diff --git a/test/ui_images/capture_images_test.dart b/test/ui_images/capture_images_test.dart index 1f6a25b..1cf6216 100644 --- a/test/ui_images/capture_images_test.dart +++ b/test/ui_images/capture_images_test.dart @@ -25,7 +25,7 @@ void main() { ], appSettings: ValueNotifier(const AppSettings(advancedMode: false)), ); - await tester.tap(find.byTooltip('Attach')); + await tester.tap(find.bySemanticsLabel('Attach')); await tester.pump(); await tester.pump(); expect(store.attachedFiles, hasLength(2)); @@ -47,7 +47,7 @@ void main() { await tester.enterText(find.byType(TextField), 'idea'); await tester.pump(); final id = (await repo.listNotes()).single.id; - await tester.tap(find.byTooltip('Attach')); + await tester.tap(find.bySemanticsLabel('Attach')); await tester.pump(); await tester.pump(); final text = (await repo.listNotes()).single.text; @@ -90,7 +90,7 @@ void main() { ); await tester.pump(); final before = store.reconciles.length; - await tester.tap(find.byTooltip('Sync')); + await tester.tap(find.bySemanticsLabel('Sync')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(store.reconciles.length, greaterThan(before)); diff --git a/test/ui_images/cloud_images_settings_test.dart b/test/ui_images/cloud_images_settings_test.dart index c98d302..1cf403f 100644 --- a/test/ui_images/cloud_images_settings_test.dart +++ b/test/ui_images/cloud_images_settings_test.dart @@ -33,7 +33,7 @@ void main() { Future saveWith(WidgetTester tester, String password) async { await tester.enterText(find.byType(TextField), password); - await tester.tap(find.text('Save & test')); + await tester.tap(find.bySemanticsLabel('Save & test')); await tester.pump(); await tester.pump(); } @@ -43,7 +43,7 @@ void main() { await tester.pumpWidget( const MaterialApp(home: Scaffold(body: CloudImagesTile())), ); - await tester.tap(find.text('Cloud images')); + await tester.tap(find.bySemanticsLabel('Cloud images')); await tester.pumpAndSettle(); expect(find.byType(CloudImagesScreen), findsOneWidget); }); @@ -55,8 +55,8 @@ void main() { tester.widget(find.byType(TextField)).controller!.text, 'saved', ); - expect(find.text('Login: configured'), findsOneWidget); - expect(find.text('Waiting to upload: 0'), findsOneWidget); + expect(find.bySemanticsLabel('Login: configured'), findsOneWidget); + expect(find.bySemanticsLabel('Waiting to upload: 0'), findsOneWidget); }); testWidgets('an accepted password is saved and the queue uploads', ( @@ -108,7 +108,7 @@ void main() { await pump(tester, field: false); expect(find.byType(TextField), findsNothing); expect( - find.text('Login: missing — create it on the PC with'), + find.bySemanticsLabel('Login: missing — create it on the PC with'), findsOneWidget, ); expect(find.text(kAddLoginCommand), findsOneWidget); @@ -120,7 +120,7 @@ void main() { final store = FakeImageStore(); await pump(tester, store: store, client: MockClient((_) => reply.future)); await tester.enterText(find.byType(TextField), 'pw'); - await tester.tap(find.text('Save & test')); + await tester.tap(find.bySemanticsLabel('Save & test')); await tester.pump(); await tester.pumpWidget(const SizedBox()); reply.complete(http.Response('', 207)); diff --git a/test/ui_images/detail_and_misc_images_test.dart b/test/ui_images/detail_and_misc_images_test.dart index 4294f02..5c6a3f3 100644 --- a/test/ui_images/detail_and_misc_images_test.dart +++ b/test/ui_images/detail_and_misc_images_test.dart @@ -58,7 +58,7 @@ void main() { await tester.pump(); tester.widget(find.byType(TextField)).controller!.selection = const TextSelection.collapsed(offset: 5); - await tester.tap(find.byTooltip('Attach')); + await tester.tap(find.bySemanticsLabel('Attach')); await tester.pump(); await tester.pump(); expect( @@ -81,7 +81,7 @@ void main() { await tester.pump(); expect((await repo.listNotes()).single.text, contains('[Raport.pdf](')); // View mode renders inline and hides the strip. - await tester.tap(find.text('View')); + await tester.tap(find.bySemanticsLabel('View')); await tester.pump(); expect(find.byType(AttachmentStrip), findsNothing); expect(find.byType(ImageEmbed), findsNWidgets(2)); diff --git a/test/ui_images/note_editor_images_test.dart b/test/ui_images/note_editor_images_test.dart index 6b48105..86dbd6f 100644 --- a/test/ui_images/note_editor_images_test.dart +++ b/test/ui_images/note_editor_images_test.dart @@ -88,7 +88,8 @@ void main() { template: NoteTemplate.llmDesignSpec, mode: NoteEditorMode.guided, ); - expect(find.text('Exit guided'), findsNothing); + // In the guided stepper: its exit arrow is present. + expect(find.bySemanticsLabel('Exit guided'), findsOneWidget); attach.insert(['IMG']); await tester.pump(); expect(field(tester).text, 'IMG\n'); @@ -103,7 +104,7 @@ void main() { template: NoteTemplate.blank, onModeChanged: modes.add, ); - await tester.tap(find.text('View')); + await tester.tap(find.bySemanticsLabel('View')); await tester.pump(); expect(modes, [NoteEditorMode.preview]); attach.insert(['IMG']);