Add priority/template wizard and raw-as-default for guided mode

Entering Guided on an empty draft now runs a two-step wizard (priority,
then template) before showing the stepper. CaptureScreen defaults to Raw
mode so a quick capture stays in the single-field flow; switching to
Guided opens the wizard. Chrome (template/mode bar and priority/status
row) is hidden while Guided or the wizard is active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Cb7oE5Xsc8zSBHHtT6qwa
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-06-27 12:10:21 +02:00
parent f91311f3f9
commit 29f94e76a5
6 changed files with 606 additions and 148 deletions

View File

@ -81,6 +81,10 @@ class _CaptureScreenState extends State<CaptureScreen>
SyncSettings? _settings;
bool _syncing = false;
/// Hides the Priority/Status row while the editor's own bare-guided chrome
/// (template/mode selectors) is also hidden, so the two stay in lockstep.
bool _chromeVisible = true;
@override
void initState() {
super.initState();
@ -301,6 +305,7 @@ class _CaptureScreenState extends State<CaptureScreen>
_lastSavedAt = null;
_draftPriority = Priority.defaultValue;
_draftStatus = Status.todo;
_chromeVisible = true;
});
if (saved) {
ScaffoldMessenger.of(context).showSnackBar(
@ -359,7 +364,10 @@ class _CaptureScreenState extends State<CaptureScreen>
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Pickers sit above the editor so the bottom-right Save FAB
// never overlaps them.
// never overlaps them. Hidden together with the editor's own
// chrome while the bare guided stepper or its entry wizard is up,
// so the top of the screen stays free of noise.
if (_chromeVisible) ...[
Row(
children: [
Expanded(
@ -384,10 +392,16 @@ class _CaptureScreenState extends State<CaptureScreen>
],
),
const SizedBox(height: 12),
],
Expanded(
child: NoteEditor(
key: ValueKey(_editorGeneration),
initialTemplate: NoteTemplate.defaultTemplate,
initialMode: NoteEditorMode.raw,
priority: _draftPriority,
onPriorityChanged: _setPriority,
onChromeVisibleChanged: (visible) =>
setState(() => _chromeVisible = visible),
autofocus: true,
onChanged: _onChanged,
),

View File

@ -28,6 +28,10 @@ class NoteDetailScreen extends StatefulWidget {
class _NoteDetailScreenState extends State<NoteDetailScreen> {
late Note _note = widget.note;
/// Hides the Priority/Status row while the editor's own bare-guided chrome
/// (template/mode selectors) is also hidden, so the two stay in lockstep.
bool _chromeVisible = true;
Future<void> _persist(Note next) async {
setState(() => _note = next);
await widget.repository.upsert(next);
@ -60,6 +64,7 @@ class _NoteDetailScreenState extends State<NoteDetailScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_chromeVisible) ...[
Row(
children: [
Expanded(
@ -88,10 +93,17 @@ class _NoteDetailScreenState extends State<NoteDetailScreen> {
],
),
const SizedBox(height: 12),
],
Expanded(
child: NoteEditor(
initialText: _note.text,
initialMode: NoteEditorMode.preview,
initialMode: NoteEditorMode.raw,
priority: _note.priority,
onPriorityChanged: (p) => _persist(
_note.copyWith(priority: p, updatedAt: DateTime.now()),
),
onChromeVisibleChanged: (visible) =>
setState(() => _chromeVisible = visible),
onChanged: _onTextChanged,
),
),

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import '../data/note.dart';
import '../data/note_template.dart';
import 'markdown_view.dart';
@ -31,9 +32,19 @@ enum NoteEditorMode {
/// Non-conforming or freeform text never enters the guided stepper (we never
/// force it into the template), so for such text Guided is unavailable and the
/// editable source stays the raw body, preserving the user's content.
///
/// Entering Guided on an empty draft first runs a two-step wizard (priority,
/// then template) via [onPriorityChanged], since those choices only make
/// sense once, before there's anything to guide. Guided itself — wizard or
/// bare stepper hides the template/mode chrome entirely (just a back arrow
/// to return to Raw); [onChromeVisibleChanged] tells the parent screen to
/// hide its own priority/status row in sync.
class NoteEditor extends StatefulWidget {
const NoteEditor({
required this.onChanged,
required this.priority,
required this.onPriorityChanged,
required this.onChromeVisibleChanged,
this.initialText = '',
this.initialTemplate,
this.initialMode = NoteEditorMode.guided,
@ -44,6 +55,18 @@ class NoteEditor extends StatefulWidget {
/// Called with the freshly assembled note text on every edit.
final ValueChanged<String> onChanged;
/// The note's current priority, shown as the wizard's starting selection.
final Priority priority;
/// Called when the priority wizard step is confirmed (Guided "Start").
final ValueChanged<Priority> onPriorityChanged;
/// Called whenever the editor's own chrome (template dropdown, mode
/// selector) is shown/hidden, so the parent screen can hide its
/// priority/status row in sync while Guided (wizard or bare stepper) is
/// active.
final ValueChanged<bool> onChromeVisibleChanged;
/// Existing note text to load. Empty for a fresh draft.
final String initialText;
@ -75,6 +98,18 @@ class _NoteEditorState extends State<NoteEditor> {
int _currentStep = 0;
/// Whether the priority+template entry wizard is showing in place of the
/// normal chrome. True only between tapping Guided on an empty draft and
/// either "Start" (which flips to bare Guided) or "Cancel" (back to Raw).
bool _enteringGuided = false;
/// Which wizard step (0: priority, 1: template) is showing.
int _wizardStep = 0;
/// Working copies of the wizard's two choices, committed on "Start".
Priority _wizardPriority = Priority.defaultValue;
NoteTemplate _wizardTemplate = NoteTemplate.defaultTemplate;
/// One controller per structured section (keyed by section key).
final Map<String, TextEditingController> _section = {};
@ -94,14 +129,30 @@ class _NoteEditorState extends State<NoteEditor> {
final initial = widget.initialTemplate;
if (initial != null) {
_template = initial;
_loadSource(initial, widget.initialText, preferGuided: true);
// Only prefer the guided (sections) source for an empty draft when the
// caller actually wants to open in Guided an empty Raw draft (the new
// default) must keep the raw body as its source, or typing into the Raw
// field would silently update the (hidden, unused) section controllers
// instead of what's emitted via onChanged.
_loadSource(
initial,
widget.initialText,
preferGuided: widget.initialMode != NoteEditorMode.raw,
);
} else {
// Detect: does the text cleanly fit the design-spec template?
final parsed = parse(NoteTemplate.llmDesignSpec, widget.initialText);
if (parsed.conforms) {
_template = NoteTemplate.llmDesignSpec;
// Same Raw/source consistency rule as above: an explicit Raw request
// must keep the raw body as the source even for conforming text.
if (widget.initialMode == NoteEditorMode.raw) {
_rawSource = true;
_body.text = widget.initialText;
} else {
_rawSource = false;
_fillSections(parsed.values);
}
} else {
// Freeform / legacy / hand-mangled keep it as a raw body, untouched.
_template = NoteTemplate.blank;
@ -244,7 +295,18 @@ class _NoteEditorState extends State<NoteEditor> {
_rawSource = false;
}
_currentStep = 0;
if (_currentText().trim().isEmpty) {
// Fresh, empty draft: priority and template are meaningful
// choices only once, so ask for them before showing the stepper.
_enteringGuided = true;
_wizardStep = 0;
_wizardPriority = widget.priority;
_wizardTemplate = NoteTemplate.defaultTemplate;
} else {
// Existing content: priority/template are already settled, so
// skip straight to the bare stepper rather than re-asking.
_mode = NoteEditorMode.guided;
}
case NoteEditorMode.raw:
if (!_rawSource) {
// guided -> raw: materialise the assembled text into the body.
@ -257,14 +319,67 @@ class _NoteEditorState extends State<NoteEditor> {
_mode = NoteEditorMode.preview;
}
});
widget.onChromeVisibleChanged(
!_enteringGuided && _mode != NoteEditorMode.guided,
);
}
/// Commits the wizard's template choice and enters the bare stepper.
/// Distinct from [_switchTemplate]: that short-circuits when the template
/// is unchanged, which would skip flipping out of the wizard here.
void _enterGuidedWithTemplate(NoteTemplate template) {
final text = _currentText();
setState(() {
_template = template;
_currentStep = 0;
_loadSource(template, text, preferGuided: true);
_mode = NoteEditorMode.guided;
_enteringGuided = false;
});
_emit();
widget.onChromeVisibleChanged(false);
}
/// Aborts the wizard, returning to Raw with the chrome restored.
void _cancelWizard() {
setState(() => _enteringGuided = false);
widget.onChromeVisibleChanged(true);
}
/// Exits the bare stepper back to Raw, restoring the chrome.
void _exitGuided() {
setState(() {
if (!_rawSource) {
_body.text = _currentText();
_rawSource = true;
}
_mode = NoteEditorMode.raw;
});
widget.onChromeVisibleChanged(true);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (_enteringGuided) return _buildGuidedEntryWizard(theme);
// Guided (the bare stepper) hides the template/mode chrome entirely
// that's the point of "Guided": just the stepper, no top-bar noise. A
// single back arrow is the only way out, restoring the full chrome.
final bareGuided = _mode == NoteEditorMode.guided;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (bareGuided)
Align(
alignment: Alignment.centerLeft,
child: IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: 'Exit guided',
onPressed: _exitGuided,
),
)
else ...[
DropdownButtonFormField<String>(
initialValue: _template.id,
isDense: true,
@ -294,8 +409,9 @@ class _NoteEditorState extends State<NoteEditor> {
icon: Icon(Icons.visibility_outlined),
label: Text('View'),
),
// Guided is offered for any structured template; switching to it
// is blocked at switch time if the raw text no longer conforms.
// Guided is offered for any structured template; tapping it
// on an empty draft opens the wizard (see _setMode), and is
// blocked at switch time if the raw text no longer conforms.
if (!_template.isFreeform)
const ButtonSegment(
value: NoteEditorMode.guided,
@ -312,12 +428,116 @@ class _NoteEditorState extends State<NoteEditor> {
onSelectionChanged: (s) => _setMode(s.first),
),
),
],
const SizedBox(height: 12),
Expanded(child: _buildBody(theme)),
],
);
}
/// The two-step priority -> template wizard shown before a fresh draft
/// enters Guided. Replaces the normal chrome entirely (see [build]).
Widget _buildGuidedEntryWizard(ThemeData theme) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
IconButton(
icon: const Icon(Icons.close),
tooltip: 'Cancel',
onPressed: _cancelWizard,
),
Text(
'Step ${_wizardStep + 1} of 2',
style: theme.textTheme.labelLarge,
),
],
),
const SizedBox(height: 8),
if (_wizardStep == 0)
..._buildWizardPriorityStep()
else
..._buildWizardTemplateStep(),
],
);
}
List<Widget> _buildWizardPriorityStep() {
return [
DropdownButtonFormField<Priority>(
initialValue: _wizardPriority,
isDense: true,
decoration: const InputDecoration(
labelText: '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)),
],
onChanged: (p) {
if (p != null) setState(() => _wizardPriority = p);
},
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerRight,
child: FilledButton(
onPressed: () => setState(() => _wizardStep = 1),
child: const Text('Next'),
),
),
];
}
List<Widget> _buildWizardTemplateStep() {
final templates = NoteTemplate.all.where((t) => !t.isFreeform).toList();
return [
DropdownButtonFormField<String>(
initialValue: _wizardTemplate.id,
isDense: true,
decoration: const InputDecoration(
labelText: 'Template',
isDense: true,
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: [
for (final t in templates)
DropdownMenuItem(value: t.id, child: Text(t.label)),
],
onChanged: (id) {
if (id == null) return;
setState(
() => _wizardTemplate = templates.firstWhere((t) => t.id == id),
);
},
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => setState(() => _wizardStep = 0),
child: const Text('Back'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () {
widget.onPriorityChanged(_wizardPriority);
_enterGuidedWithTemplate(_wizardTemplate);
},
child: const Text('Start'),
),
],
),
];
}
Widget _buildBody(ThemeData theme) {
switch (_mode) {
case NoteEditorMode.preview:

View File

@ -63,16 +63,31 @@ void main() {
'sync.token': 'tok',
};
testWidgets('opens the guided editor with section guidance', (tester) async {
testWidgets('defaults to Raw, with Guided available via the entry wizard', (
tester,
) async {
await pumpCapture(tester);
// The guided stepper shows the design-spec sections and the title step's
// guidance, with no note persisted yet.
// Defaults to Raw: a single text field, no note persisted yet.
expect(find.byType(TextField), findsOneWidget);
expect(find.text('Guided'), findsOneWidget);
expect(find.text('0 saved'), 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.pump();
expect(find.text('Step 1 of 2'), findsOneWidget);
await tester.tap(find.text('Next'));
await tester.pump();
await tester.tap(find.text('Start'));
await tester.pump();
// Now in the bare guided stepper, showing the design-spec sections.
expect(find.textContaining('imperative'), findsOneWidget); // title helper
expect(find.text('what'), findsOneWidget); // a section step header
expect(find.text('done'), findsOneWidget);
expect(find.text('0 saved'), findsOneWidget);
});
testWidgets('saving the untouched template creates no note', (tester) async {
@ -115,8 +130,10 @@ void main() {
await tester.pump();
expect(await repo.listNotes(), hasLength(1));
// The editor reset to a fresh guided template (title guidance shown again).
expect(find.textContaining('imperative'), findsOneWidget);
// The editor reset to a fresh, empty Raw draft.
expect(find.byType(TextField), findsOneWidget);
final raw = tester.widget<TextField>(find.byType(TextField));
expect(raw.controller!.text, isEmpty);
});
testWidgets('tapping Sync while unconfigured prompts for a token', (

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:todo/data/note.dart';
import 'package:todo/ui/markdown_view.dart';
import 'package:todo/ui/note_detail_screen.dart';
import 'fake_note_repository.dart';
@ -33,14 +32,12 @@ void main() {
return repo;
}
testWidgets('opens in the rendered Markdown view with the title in the bar', (
tester,
) async {
testWidgets('opens in Raw with the title in the app bar', (tester) async {
await pumpDetail(tester, seedNote('# My note\n\n## what\n_why_\n\nbody'));
expect(find.byType(MarkdownView), findsOneWidget);
// Title appears both in the app bar and the rendered body.
expect(find.text('My note'), findsWidgets);
final raw = tester.widget<TextField>(find.byType(TextField));
expect(raw.controller!.text, contains('My note'));
expect(find.text('My note'), findsOneWidget); // app bar title
});
testWidgets('changing the priority dropdown persists the note', (
@ -84,6 +81,34 @@ void main() {
expect((await repo.listNotes()).single.text, contains('new body'));
});
testWidgets(
'clearing the body then tapping Guided runs the wizard and persists the chosen priority',
(tester) async {
final repo = await pumpDetail(tester, seedNote('# T\n\n## what\nold'));
await tester.enterText(find.byType(TextField), '');
await tester.pump();
await tester.tap(find.text('Guided'));
await tester.pump();
expect(find.text('Step 1 of 2'), findsOneWidget);
// Wizard hides the Status dropdown (onChromeVisibleChanged(false)).
expect(find.text('Status'), findsNothing);
await tester.tap(find.byType(DropdownButtonFormField<Priority>));
await tester.pumpAndSettle();
await tester.tap(find.text('High').last);
await tester.pumpAndSettle();
await tester.tap(find.text('Next'));
await tester.pump();
await tester.tap(find.text('Start'));
await tester.pump();
expect((await repo.listNotes()).single.priority, Priority.high);
},
);
testWidgets('the delete action removes the note and pops', (tester) async {
final repo = await pumpDetail(tester, seedNote('# Bye'));

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:todo/data/note.dart';
import 'package:todo/data/note_template.dart';
import 'package:todo/ui/markdown_view.dart';
import 'package:todo/ui/note_editor.dart';
@ -14,6 +15,9 @@ void main() {
String initialText = '',
NoteTemplate? initialTemplate,
NoteEditorMode initialMode = NoteEditorMode.guided,
Priority priority = Priority.defaultValue,
ValueChanged<Priority>? onPriorityChanged,
ValueChanged<bool>? onChromeVisibleChanged,
}) async {
final emitted = <String>[];
tester.view.physicalSize = const Size(1200, 2400);
@ -27,6 +31,9 @@ void main() {
initialText: initialText,
initialTemplate: initialTemplate,
initialMode: initialMode,
priority: priority,
onPriorityChanged: onPriorityChanged ?? (_) {},
onChromeVisibleChanged: onChromeVisibleChanged ?? (_) {},
onChanged: emitted.add,
),
),
@ -41,7 +48,7 @@ void main() {
) async {
final emitted = await pumpEditor(tester, initialTemplate: spec);
expect(find.text('Guided'), findsOneWidget);
expect(find.byTooltip('Exit guided'), findsOneWidget);
await tester.enterText(find.byType(TextField).first, 'Dark mode');
await tester.pump();
@ -73,8 +80,12 @@ void main() {
});
testWidgets('switching to View renders the note as Markdown', (tester) async {
await pumpEditor(tester, initialTemplate: spec);
await tester.enterText(find.byType(TextField).first, 'Render me');
await pumpEditor(
tester,
initialTemplate: spec,
initialMode: NoteEditorMode.raw,
);
await tester.enterText(find.byType(TextField).first, '# Render me');
await tester.pump();
await tester.tap(find.text('View'));
@ -84,14 +95,14 @@ void main() {
expect(find.text('Render me'), findsOneWidget); // rendered heading text
});
testWidgets('guided → Raw materialises the assembled text and edits emit', (
tester,
) async {
testWidgets(
'bare Guided back arrow returns to Raw, materialising the assembled text',
(tester) async {
final emitted = await pumpEditor(tester, initialTemplate: spec);
await tester.enterText(find.byType(TextField).first, 'T');
await tester.pump();
await tester.tap(find.text('Raw'));
await tester.tap(find.byTooltip('Exit guided'));
await tester.pump();
final raw = tester.widget<TextField>(find.byType(TextField));
@ -100,7 +111,8 @@ void main() {
await tester.enterText(find.byType(TextField), '# T\n\n## what\nedited');
await tester.pump();
expect(emitted.last, contains('edited'));
});
},
);
testWidgets('Raw → Guided is blocked with a snackbar when non-conforming', (
tester,
@ -120,25 +132,29 @@ void main() {
expect(find.byType(Stepper), findsNothing); // still raw
});
testWidgets('guided → Raw → Guided round-trips when text still conforms', (
tester,
) async {
testWidgets(
'guided → Raw → Guided round-trips (skipping the wizard) when text still conforms',
(tester) async {
// Open guided from a conforming note (pre-fills the section controllers
// without typing into collapsed steps), so the assembled body conforms.
final conforming = assemble(spec, {'title': 'Keep', 'what': 'a body'});
await pumpEditor(tester, initialTemplate: spec, initialText: conforming);
expect(find.byType(Stepper), findsOneWidget);
// guided raw makes the (still conforming) body the source
await tester.tap(find.text('Raw'));
// Bare guided -> raw via the back arrow makes the (still conforming)
// body the source
await tester.tap(find.byTooltip('Exit guided'));
await tester.pump();
expect(find.byType(Stepper), findsNothing);
// then raw guided re-parses the conforming body back into sections.
// 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.pump();
expect(find.byType(Stepper), findsOneWidget);
});
expect(find.text('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);
@ -155,16 +171,19 @@ void main() {
testWidgets('switching template via the dropdown reloads the source', (
tester,
) async {
await pumpEditor(tester, initialTemplate: spec);
expect(find.byType(Stepper), findsOneWidget);
await pumpEditor(
tester,
initialTemplate: spec,
initialMode: NoteEditorMode.raw,
);
expect(find.text('Guided'), findsOneWidget); // structured: Guided offered
await tester.tap(find.text('LLM design spec').first); // open the menu
await tester.pumpAndSettle();
await tester.tap(find.text('Blank').last);
await tester.pumpAndSettle();
// Freeform now: the stepper is gone and Guided is no longer offered.
expect(find.byType(Stepper), findsNothing);
// Freeform now: Guided is no longer offered.
expect(find.text('Guided'), findsNothing);
});
@ -175,7 +194,7 @@ void main() {
await pumpEditor(tester, initialText: conforming);
expect(find.byType(Stepper), findsOneWidget); // guided by default
expect(find.text('Guided'), findsOneWidget);
expect(find.byTooltip('Exit guided'), findsOneWidget);
});
testWidgets('detects a legacy note (no template given) as freeform raw', (
@ -202,6 +221,23 @@ void main() {
expect(find.text('Preview me'), findsOneWidget);
});
testWidgets('Preview → Raw materialises the still-guided source', (
tester,
) async {
final conforming = assemble(spec, {'title': 'Preview src', 'what': 'x'});
await pumpEditor(
tester,
initialText: conforming,
initialMode: NoteEditorMode.preview,
);
await tester.tap(find.text('Raw'));
await tester.pump();
final raw = tester.widget<TextField>(find.byType(TextField));
expect(raw.controller!.text, contains('Preview src'));
});
testWidgets('initialMode guided falls back to Raw for non-conforming text', (
tester,
) async {
@ -217,4 +253,138 @@ void main() {
final raw = tester.widget<TextField>(find.byType(TextField));
expect(raw.controller!.text, 'cannot be guided');
});
testWidgets(
'tapping Guided on an empty draft opens the priority+template wizard',
(tester) async {
await pumpEditor(
tester,
initialTemplate: spec,
initialMode: NoteEditorMode.raw,
);
await tester.tap(find.text('Guided'));
await tester.pump();
expect(find.text('Step 1 of 2'), findsOneWidget);
expect(find.byType(DropdownButtonFormField<Priority>), findsOneWidget);
expect(find.byType(Stepper), findsNothing);
},
);
testWidgets(
'the wizard Next/Back moves between priority and template steps',
(tester) async {
await pumpEditor(
tester,
initialTemplate: spec,
initialMode: NoteEditorMode.raw,
);
await tester.tap(find.text('Guided'));
await tester.pump();
await tester.tap(find.text('Next'));
await tester.pump();
expect(find.text('Step 2 of 2'), findsOneWidget);
expect(find.byType(DropdownButtonFormField<String>), findsOneWidget);
await tester.tap(find.text('Back'));
await tester.pump();
expect(find.text('Step 1 of 2'), findsOneWidget);
},
);
testWidgets('the wizard template step only offers structured templates', (
tester,
) async {
await pumpEditor(
tester,
initialTemplate: spec,
initialMode: NoteEditorMode.raw,
);
await tester.tap(find.text('Guided'));
await tester.pump();
await tester.tap(find.text('Next'));
await tester.pump();
await tester.tap(find.text('LLM design spec').first); // open the menu
await tester.pumpAndSettle();
expect(find.text('Blank'), findsNothing);
// 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);
});
testWidgets(
'wizard Start commits the chosen priority and enters the bare stepper',
(tester) async {
Priority? committed;
await pumpEditor(
tester,
initialTemplate: spec,
initialMode: NoteEditorMode.raw,
onPriorityChanged: (p) => committed = p,
);
await tester.tap(find.text('Guided'));
await tester.pump();
await tester.tap(find.byType(DropdownButtonFormField<Priority>));
await tester.pumpAndSettle();
await tester.tap(find.text('High').last);
await tester.pumpAndSettle();
await tester.tap(find.text('Next'));
await tester.pump();
await tester.tap(find.text('Start'));
await tester.pump();
expect(committed, Priority.high);
expect(find.byType(Stepper), findsOneWidget);
expect(find.byTooltip('Exit guided'), findsOneWidget);
},
);
testWidgets('wizard Cancel returns to Raw with the chrome restored', (
tester,
) async {
final chromeVisible = <bool>[];
await pumpEditor(
tester,
initialTemplate: spec,
initialMode: NoteEditorMode.raw,
onChromeVisibleChanged: chromeVisible.add,
);
await tester.tap(find.text('Guided'));
await tester.pump();
expect(chromeVisible.last, false);
await tester.tap(find.byTooltip('Cancel'));
await tester.pump();
expect(chromeVisible.last, true);
expect(find.byType(Stepper), findsNothing);
expect(find.text('Raw'), findsOneWidget); // chrome's mode selector is back
});
testWidgets('exiting bare Guided via the back arrow restores the chrome', (
tester,
) async {
final chromeVisible = <bool>[];
final conforming = assemble(spec, {'title': 'X', 'what': 'a body'});
await pumpEditor(
tester,
initialTemplate: spec,
initialText: conforming,
onChromeVisibleChanged: chromeVisible.add,
);
expect(find.byType(Stepper), findsOneWidget);
await tester.tap(find.byTooltip('Exit guided'));
await tester.pump();
expect(chromeVisible, contains(true));
expect(find.text('Raw'), findsOneWidget);
});
}