mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-11 14:27:48 +01:00
Text-free UI: glyph chips, icon-only actions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a30e7b34a9
commit
091e68a2d9
@ -44,18 +44,24 @@ Future<List<XFile>> 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);
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -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<void>(
|
||||
appRoute(context, (_) => CloudImagesScreen(httpClient: httpClient)),
|
||||
@ -129,7 +128,9 @@ class _CloudImagesScreenState extends State<CloudImagesScreen> {
|
||||
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<CloudImagesScreen> {
|
||||
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<CloudImagesScreen> {
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
58
lib/ui/confirm_delete.dart
Normal file
58
lib/ui/confirm_delete.dart
Normal file
@ -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<bool> confirmDeleteNote(BuildContext context) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
105
lib/ui/enum_glyphs.dart
Normal file
105
lib/ui/enum_glyphs.dart
Normal file
@ -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<T> 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<T> values;
|
||||
|
||||
/// Values currently selected.
|
||||
final Set<T> selected;
|
||||
|
||||
/// Builds the glyph for one value.
|
||||
final Widget Function(T) glyphOf;
|
||||
|
||||
/// Called with the tapped value.
|
||||
final ValueChanged<T> 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -145,7 +145,12 @@ class _GitHubMirrorScreenState extends State<GitHubMirrorScreen> {
|
||||
@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<GitHubMirrorScreen> {
|
||||
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<GitHubMirrorScreen> {
|
||||
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<GitHubMirrorScreen> {
|
||||
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<GitHubMirrorScreen> {
|
||||
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<GitHubMirrorScreen> {
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -111,8 +111,10 @@ class _FileEmbedState extends State<FileEmbed> {
|
||||
?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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<NoteActionsSheet> {
|
||||
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<Status>(
|
||||
label: 'Status',
|
||||
GlyphChips<Status>(
|
||||
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<Priority>(
|
||||
label: 'Priority',
|
||||
GlyphChips<Priority>(
|
||||
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<NoteActionsSheet> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Single-select chip row for an enum (used for per-note priority/status).
|
||||
class _EnumChips<T> extends StatelessWidget {
|
||||
const _EnumChips({
|
||||
required this.label,
|
||||
required this.values,
|
||||
required this.selected,
|
||||
required this.labelOf,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final List<T> values;
|
||||
final Set<T> selected;
|
||||
final String Function(T) labelOf;
|
||||
final ValueChanged<T> 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<NoteDetailScreen> {
|
||||
_persist(_note.copyWith(text: text, updatedAt: DateTime.now()));
|
||||
|
||||
Future<void> _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<NoteDetailScreen> {
|
||||
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<NoteDetailScreen> {
|
||||
onPressed: _copy,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Delete note',
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
semanticLabel: 'Delete note',
|
||||
),
|
||||
onPressed: _delete,
|
||||
),
|
||||
],
|
||||
|
||||
@ -43,7 +43,7 @@ class NoteEditorChrome extends StatelessWidget {
|
||||
DropdownButtonFormField<String>(
|
||||
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},
|
||||
|
||||
@ -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(),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -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<NoteEditorWizard> {
|
||||
|
||||
@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<NoteEditorWizard> {
|
||||
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<NoteEditorWizard> {
|
||||
DropdownButtonFormField<Priority>(
|
||||
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<NoteEditorWizard> {
|
||||
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<NoteEditorWizard> {
|
||||
DropdownButtonFormField<String>(
|
||||
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<NoteEditorWizard> {
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@ -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<T> 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<T> extends StatelessWidget {
|
||||
/// Every selectable value.
|
||||
final List<T> 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<T> onChanged;
|
||||
@ -43,7 +49,7 @@ class MetaDropdown<T> 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<T> extends StatelessWidget {
|
||||
isExpanded: true,
|
||||
items: [
|
||||
for (final v in values)
|
||||
DropdownMenuItem<T>(value: v, child: Text(labelOf(v))),
|
||||
DropdownMenuItem<T>(value: v, child: glyphOf(v)),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) onChanged(v);
|
||||
@ -98,20 +104,22 @@ class NoteMetaRow extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: MetaDropdown<Priority>(
|
||||
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<Status>(
|
||||
label: 'Status',
|
||||
heading: kStatusHeadingIcon,
|
||||
semanticLabel: 'Status',
|
||||
value: status,
|
||||
values: Status.values,
|
||||
labelOf: (s) => s.label,
|
||||
glyphOf: statusGlyph,
|
||||
onChanged: onStatusChanged,
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<FilterSheet> {
|
||||
|
||||
@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<FilterSheet> {
|
||||
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<Status>(
|
||||
label: 'Status',
|
||||
GlyphChips<Status>(
|
||||
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<Priority>(
|
||||
label: 'Priority',
|
||||
GlyphChips<Priority>(
|
||||
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<FilterSheet> {
|
||||
),
|
||||
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<FilterSheet> {
|
||||
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<FilterSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Multi-select chip group for an enum (used by the filter sheet).
|
||||
class _MultiChips<T> extends StatelessWidget {
|
||||
const _MultiChips({
|
||||
required this.label,
|
||||
required this.values,
|
||||
required this.selected,
|
||||
required this.labelOf,
|
||||
required this.onToggle,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final List<T> values;
|
||||
final Set<T> selected;
|
||||
final String Function(T) labelOf;
|
||||
final ValueChanged<T> 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
|
||||
|
||||
@ -134,7 +134,7 @@ class _NotesListScreenState extends State<NotesListScreen> {
|
||||
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<NotesListScreen> {
|
||||
onPressed: _toggleClaudeNotes,
|
||||
),
|
||||
PopupMenuButton<NoteSort>(
|
||||
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<NotesListScreen> {
|
||||
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<NotesListScreen> {
|
||||
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('');
|
||||
|
||||
@ -105,11 +105,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
/// 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<void> _setAdvancedMode(bool value) async {
|
||||
final analytics = widget.analytics;
|
||||
if (analytics != null) {
|
||||
@ -206,7 +203,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
@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<SettingsScreen> {
|
||||
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<void> Function() open) => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: icon,
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => unawaited(open()),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<TextField>(find.byType(TextField).first);
|
||||
expect(field.controller?.text, contains('my important idea'));
|
||||
},
|
||||
);
|
||||
final field = tester.widget<TextField>(find.byType(TextField).first);
|
||||
expect(field.controller?.text, contains('my important idea'));
|
||||
});
|
||||
}
|
||||
|
||||
57
test/confirm_delete_test.dart
Normal file
57
test/confirm_delete_test.dart
Normal file
@ -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<bool? Function()> _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);
|
||||
});
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
});
|
||||
|
||||
@ -61,7 +61,7 @@ void main() {
|
||||
find.byWidgetPredicate((w) => w is DropdownButton<Priority>),
|
||||
);
|
||||
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<Status>),
|
||||
);
|
||||
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<Priority>));
|
||||
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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<TextField>(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<TextField>(find.byType(TextField));
|
||||
|
||||
@ -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<Priority>), 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<String>), 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<Priority>));
|
||||
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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -10,7 +10,7 @@ import 'notes_list_harness.dart';
|
||||
|
||||
/// Opens a date field's menu and picks [entry].
|
||||
Future<void> 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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -53,7 +53,8 @@ Future<FakeNoteRepository> pumpList(
|
||||
/// sheet, so on a short viewport it legitimately sits below the fold:
|
||||
/// scroll it into view first, as a user would.
|
||||
Future<void> 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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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'));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<void> 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 = <AttachmentPickSource>[];
|
||||
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');
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -33,7 +33,7 @@ void main() {
|
||||
|
||||
Future<void> 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<TextField>(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));
|
||||
|
||||
@ -58,7 +58,7 @@ void main() {
|
||||
await tester.pump();
|
||||
tester.widget<TextField>(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));
|
||||
|
||||
@ -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']);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user