mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-07-04 12:03:10 +02:00
Rework guided mode from Stepper to full-screen per-step view
Replace the vertical Stepper widget with a full-screen single-step view: progress bar, label, helper text, and a tall TextField that expands to fill the available height. Navigation is Next/Back buttons only; the final step's Next becomes Done which exits to Raw. Removes _stepKeys (scroll-into-view keys) and the async _goToStep (Future.delayed + Scrollable.ensureVisible) since neither is needed without a collapsing Stepper. _goToStep is now synchronous. Nav buttons sit below an Expanded area so they stay above the soft keyboard when resizeToAvoidBottomInset resizes the Scaffold. Tests: replace find.byType(Stepper) with find.byType(LinearProgressIndicator), rewrite the navigation test, add three new tests (progress counter, last-step Done, Done exits to Raw). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Cb7oE5Xsc8zSBHHtT6qwa
This commit is contained in:
parent
29f94e76a5
commit
5ca289ca81
@ -9,7 +9,7 @@ enum NoteEditorMode {
|
|||||||
/// Read-only rendered Markdown (headings, guidance, bullets).
|
/// Read-only rendered Markdown (headings, guidance, bullets).
|
||||||
preview,
|
preview,
|
||||||
|
|
||||||
/// Inline [Stepper], one step per template section.
|
/// Full-screen per-step view, one step per template section.
|
||||||
guided,
|
guided,
|
||||||
|
|
||||||
/// A single text field showing the assembled Markdown verbatim.
|
/// A single text field showing the assembled Markdown verbatim.
|
||||||
@ -25,8 +25,8 @@ enum NoteEditorMode {
|
|||||||
///
|
///
|
||||||
/// Modes (see [NoteEditorMode]):
|
/// Modes (see [NoteEditorMode]):
|
||||||
/// * **Preview** — the note rendered as Markdown, read-only.
|
/// * **Preview** — the note rendered as Markdown, read-only.
|
||||||
/// * **Guided** — an inline [Stepper], one step per template section, each
|
/// * **Guided** — a full-screen per-step view, one step per template
|
||||||
/// with guidance on what to write and why the LLM needs it.
|
/// section, with guidance on what to write and why the LLM needs it.
|
||||||
/// * **Raw** — a single text field showing the assembled text verbatim.
|
/// * **Raw** — a single text field showing the assembled text verbatim.
|
||||||
///
|
///
|
||||||
/// Non-conforming or freeform text never enters the guided stepper (we never
|
/// Non-conforming or freeform text never enters the guided stepper (we never
|
||||||
@ -36,7 +36,7 @@ enum NoteEditorMode {
|
|||||||
/// Entering Guided on an empty draft first runs a two-step wizard (priority,
|
/// Entering Guided on an empty draft first runs a two-step wizard (priority,
|
||||||
/// then template) via [onPriorityChanged], since those choices only make
|
/// then template) via [onPriorityChanged], since those choices only make
|
||||||
/// sense once, before there's anything to guide. Guided itself — wizard or
|
/// sense once, before there's anything to guide. Guided itself — wizard or
|
||||||
/// bare stepper — hides the template/mode chrome entirely (just a back arrow
|
/// bare step page — hides the template/mode chrome entirely (just a back arrow
|
||||||
/// to return to Raw); [onChromeVisibleChanged] tells the parent screen to
|
/// to return to Raw); [onChromeVisibleChanged] tells the parent screen to
|
||||||
/// hide its own priority/status row in sync.
|
/// hide its own priority/status row in sync.
|
||||||
class NoteEditor extends StatefulWidget {
|
class NoteEditor extends StatefulWidget {
|
||||||
@ -113,12 +113,6 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
/// One controller per structured section (keyed by section key).
|
/// One controller per structured section (keyed by section key).
|
||||||
final Map<String, TextEditingController> _section = {};
|
final Map<String, TextEditingController> _section = {};
|
||||||
|
|
||||||
/// One key per structured section, used to scroll the newly-active step's
|
|
||||||
/// content into view after a step change — the Stepper reflows height
|
|
||||||
/// (other steps collapse/expand) without moving the scroll offset, so
|
|
||||||
/// without this the active step's input can end up hidden off-screen.
|
|
||||||
final Map<String, GlobalKey> _stepKeys = {};
|
|
||||||
|
|
||||||
/// Single field used for the freeform [NoteTemplate.blank] body and for raw
|
/// Single field used for the freeform [NoteTemplate.blank] body and for raw
|
||||||
/// mode of a structured template.
|
/// mode of a structured template.
|
||||||
final TextEditingController _body = TextEditingController();
|
final TextEditingController _body = TextEditingController();
|
||||||
@ -187,36 +181,17 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
return desired;
|
return desired;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensures a controller and scroll-into-view key exist for every section
|
/// Ensures a controller exists for every section of [template].
|
||||||
/// of [template].
|
|
||||||
void _ensureControllers(NoteTemplate template) {
|
void _ensureControllers(NoteTemplate template) {
|
||||||
for (final s in template.sections) {
|
for (final s in template.sections) {
|
||||||
_section.putIfAbsent(s.key, () => TextEditingController());
|
_section.putIfAbsent(s.key, () => TextEditingController());
|
||||||
_stepKeys.putIfAbsent(s.key, () => GlobalKey());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Moves to step [index] and scrolls that step's content into view once the
|
void _goToStep(int index) {
|
||||||
/// Stepper's own expand/collapse transition has settled. The Stepper
|
setState(() {
|
||||||
/// animates each step's content height over [kThemeAnimationDuration], so
|
_currentStep = index.clamp(0, _template.sections.length - 1);
|
||||||
/// scrolling on the next post-frame callback (before that animation
|
});
|
||||||
/// finishes) measures a layout that's still mid-collapse — the scroll lands
|
|
||||||
/// on a stale position and the active step ends up hidden under the fixed
|
|
||||||
/// app bar above. Waiting for the animation to finish before measuring
|
|
||||||
/// fixes that.
|
|
||||||
Future<void> _goToStep(int index) async {
|
|
||||||
setState(() => _currentStep = index);
|
|
||||||
await Future<void>.delayed(kThemeAnimationDuration);
|
|
||||||
if (!mounted) return;
|
|
||||||
final key = _stepKeys[_template.sections[index].key];
|
|
||||||
final stepContext = key?.currentContext;
|
|
||||||
if (stepContext == null || !stepContext.mounted) return;
|
|
||||||
await Scrollable.ensureVisible(
|
|
||||||
stepContext,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
alignment: 0.0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fillSections(Map<String, String> values) {
|
void _fillSections(Map<String, String> values) {
|
||||||
@ -545,7 +520,7 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
case NoteEditorMode.raw:
|
case NoteEditorMode.raw:
|
||||||
return _buildRaw(theme);
|
return _buildRaw(theme);
|
||||||
case NoteEditorMode.guided:
|
case NoteEditorMode.guided:
|
||||||
return _buildStepper(theme);
|
return _buildStepPage(theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,98 +541,92 @@ class _NoteEditorState extends State<NoteEditor> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStepper(ThemeData theme) {
|
Widget _buildStepPage(ThemeData theme) {
|
||||||
_ensureControllers(_template);
|
_ensureControllers(_template);
|
||||||
final sections = _template.sections;
|
final sections = _template.sections;
|
||||||
return SingleChildScrollView(
|
final idx = _currentStep.clamp(0, sections.length - 1);
|
||||||
child: Stepper(
|
final section = sections[idx];
|
||||||
currentStep: _currentStep.clamp(0, sections.length - 1),
|
final total = sections.length;
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
final controller = _section[section.key]!;
|
||||||
onStepTapped: _goToStep,
|
|
||||||
onStepContinue: _currentStep < sections.length - 1
|
return Column(
|
||||||
? () => _goToStep(_currentStep + 1)
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
: null,
|
children: [
|
||||||
onStepCancel: _currentStep > 0
|
// Progress bar + step counter
|
||||||
? () => _goToStep(_currentStep - 1)
|
Padding(
|
||||||
: null,
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||||
controlsBuilder: (context, details) {
|
child: Row(
|
||||||
return Padding(
|
children: [
|
||||||
padding: const EdgeInsets.only(top: 12),
|
Text('${idx + 1} / $total', style: theme.textTheme.labelMedium),
|
||||||
child: Row(
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: LinearProgressIndicator(value: (idx + 1) / total),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Section label + helper + text field
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (details.onStepContinue != null)
|
const SizedBox(height: 8),
|
||||||
FilledButton(
|
Text(
|
||||||
onPressed: details.onStepContinue,
|
section.isTitle ? 'title' : section.label,
|
||||||
child: const Text('Next'),
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
section.helper,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
if (details.onStepCancel != null) ...[
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 12),
|
||||||
TextButton(
|
TextField(
|
||||||
onPressed: details.onStepCancel,
|
controller: controller,
|
||||||
child: const Text('Back'),
|
autofocus: widget.autofocus && idx == 0,
|
||||||
|
maxLines: section.inline ? 1 : null,
|
||||||
|
minLines: section.inline ? 1 : 6,
|
||||||
|
keyboardType: TextInputType.multiline,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: section.hint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
),
|
),
|
||||||
],
|
onChanged: (_) {
|
||||||
|
setState(() {});
|
||||||
|
_emit();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
steps: [
|
|
||||||
for (var i = 0; i < sections.length; i++)
|
|
||||||
_stepFor(theme, sections[i], i),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Step _stepFor(ThemeData theme, TemplateSection section, int index) {
|
|
||||||
final controller = _section[section.key]!;
|
|
||||||
final hasValue = controller.text.trim().isNotEmpty;
|
|
||||||
return Step(
|
|
||||||
// Keyed on the title (not content): ensureVisible aligns this widget's
|
|
||||||
// top to the viewport's top, so keying the title — which Stepper
|
|
||||||
// renders *above* content as the tappable heading — is what keeps the
|
|
||||||
// step's own heading on-screen. Keying content alone scrolled the
|
|
||||||
// heading off the top of the viewport.
|
|
||||||
title: KeyedSubtree(
|
|
||||||
key: _stepKeys[section.key],
|
|
||||||
child: Text(section.isTitle ? 'title' : section.label),
|
|
||||||
),
|
|
||||||
state: hasValue
|
|
||||||
? StepState.complete
|
|
||||||
: (index == _currentStep ? StepState.editing : StepState.indexed),
|
|
||||||
isActive: index <= _currentStep,
|
|
||||||
content: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
section.helper,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
autofocus: widget.autofocus && index == 0,
|
|
||||||
maxLines: section.inline ? 1 : null,
|
|
||||||
minLines: section.inline ? 1 : 3,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: section.hint,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
isDense: true,
|
|
||||||
),
|
|
||||||
// Rebuild so the step's completion tick reflects the value.
|
|
||||||
onChanged: (_) {
|
|
||||||
setState(() {});
|
|
||||||
_emit();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
// Navigation buttons — below Expanded so they stay above the keyboard
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (idx > 0)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _goToStep(idx - 1),
|
||||||
|
child: const Text('Back'),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (idx < total - 1)
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => _goToStep(idx + 1),
|
||||||
|
child: const Text('Next'),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
FilledButton(onPressed: _exitGuided, child: const Text('Done')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:http/testing.dart';
|
import 'package:http/testing.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:todo/data/note.dart';
|
import 'package:todo/data/note.dart';
|
||||||
|
import 'package:todo/data/note_template.dart';
|
||||||
import 'package:todo/sync/local_backup.dart';
|
import 'package:todo/sync/local_backup.dart';
|
||||||
import 'package:todo/sync/notes_markdown.dart';
|
import 'package:todo/sync/notes_markdown.dart';
|
||||||
import 'package:todo/ui/capture_screen.dart';
|
import 'package:todo/ui/capture_screen.dart';
|
||||||
@ -84,10 +85,13 @@ void main() {
|
|||||||
await tester.tap(find.text('Start'));
|
await tester.tap(find.text('Start'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
// Now in the bare guided stepper, showing the design-spec sections.
|
// Now on step 1 of the full-screen step page.
|
||||||
|
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.text('1 / ${NoteTemplate.llmDesignSpec.sections.length}'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
expect(find.textContaining('imperative'), findsOneWidget); // title helper
|
expect(find.textContaining('imperative'), findsOneWidget); // title helper
|
||||||
expect(find.text('what'), findsOneWidget); // a section step header
|
|
||||||
expect(find.text('done'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('saving the untouched template creates no note', (tester) async {
|
testWidgets('saving the untouched template creates no note', (tester) async {
|
||||||
|
|||||||
@ -55,28 +55,22 @@ void main() {
|
|||||||
expect(emitted.last, startsWith('# Dark mode'));
|
expect(emitted.last, startsWith('# Dark mode'));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('stepper Next/Back and tapping a step header navigate', (
|
testWidgets('step page Next/Back navigate and progress counter updates', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
await pumpEditor(tester, initialTemplate: spec);
|
await pumpEditor(tester, initialTemplate: spec);
|
||||||
|
|
||||||
// A vertical Stepper keeps every step's controls in the tree (collapsed),
|
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
||||||
// so the buttons resolve to many identical widgets — they all drive the
|
expect(find.text('1 / ${spec.sections.length}'), findsOneWidget);
|
||||||
// same shared continue/cancel callbacks, so tap the first.
|
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
await tester.tap(find.text('Next'));
|
||||||
// Only the current step's controls are hit-testable (others are collapsed
|
await tester.pump();
|
||||||
// to zero height), so .hitTestable() resolves the single visible button.
|
expect(find.text('2 / ${spec.sections.length}'), findsOneWidget);
|
||||||
// Settle the expand/collapse animation so the next step's controls lay out.
|
|
||||||
await tester.tap(find.text('Next').hitTestable()); // advance past the title
|
await tester.tap(find.text('Back'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pump();
|
||||||
await tester.tap(find.text('Back').hitTestable()); // and back
|
expect(find.text('1 / ${spec.sections.length}'), findsOneWidget);
|
||||||
await tester.pumpAndSettle();
|
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
||||||
// Jump directly to a step by tapping its header. _goToStep delays past
|
|
||||||
// the Stepper's own collapse/expand animation before scrolling, so drain
|
|
||||||
// that timer rather than a bare pump — see CLAUDE.md's Timer pitfall.
|
|
||||||
await tester.tap(find.text('done'));
|
|
||||||
await tester.pump(const Duration(milliseconds: 250));
|
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('switching to View renders the note as Markdown', (tester) async {
|
testWidgets('switching to View renders the note as Markdown', (tester) async {
|
||||||
@ -129,7 +123,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.textContaining("doesn't match the template"), findsOneWidget);
|
expect(find.textContaining("doesn't match the template"), findsOneWidget);
|
||||||
expect(find.byType(Stepper), findsNothing); // still raw
|
expect(find.byType(LinearProgressIndicator), findsNothing); // still raw
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
@ -139,19 +133,19 @@ void main() {
|
|||||||
// without typing into collapsed steps), so the assembled body conforms.
|
// without typing into collapsed steps), so the assembled body conforms.
|
||||||
final conforming = assemble(spec, {'title': 'Keep', 'what': 'a body'});
|
final conforming = assemble(spec, {'title': 'Keep', 'what': 'a body'});
|
||||||
await pumpEditor(tester, initialTemplate: spec, initialText: conforming);
|
await pumpEditor(tester, initialTemplate: spec, initialText: conforming);
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
||||||
|
|
||||||
// Bare guided -> raw via the back arrow makes the (still conforming)
|
// Bare guided -> raw via the back arrow makes the (still conforming)
|
||||||
// body the source…
|
// body the source…
|
||||||
await tester.tap(find.byTooltip('Exit guided'));
|
await tester.tap(find.byTooltip('Exit guided'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.byType(Stepper), findsNothing);
|
expect(find.byType(LinearProgressIndicator), findsNothing);
|
||||||
|
|
||||||
// …then raw -> guided re-parses the conforming body straight back into
|
// …then raw -> guided re-parses the conforming body straight back into
|
||||||
// the bare stepper — no wizard, since the content isn't empty.
|
// the bare stepper — no wizard, since the content isn't empty.
|
||||||
await tester.tap(find.text('Guided'));
|
await tester.tap(find.text('Guided'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
||||||
expect(find.text('Step 1 of 2'), findsNothing); // wizard was skipped
|
expect(find.text('Step 1 of 2'), findsNothing); // wizard was skipped
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -193,7 +187,10 @@ void main() {
|
|||||||
final conforming = assemble(spec, {'title': 'Detected', 'what': 'x'});
|
final conforming = assemble(spec, {'title': 'Detected', 'what': 'x'});
|
||||||
await pumpEditor(tester, initialText: conforming);
|
await pumpEditor(tester, initialText: conforming);
|
||||||
|
|
||||||
expect(find.byType(Stepper), findsOneWidget); // guided by default
|
expect(
|
||||||
|
find.byType(LinearProgressIndicator),
|
||||||
|
findsOneWidget,
|
||||||
|
); // guided by default
|
||||||
expect(find.byTooltip('Exit guided'), findsOneWidget);
|
expect(find.byTooltip('Exit guided'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -203,7 +200,7 @@ void main() {
|
|||||||
await pumpEditor(tester, initialText: 'old\n\nwhat — legacy');
|
await pumpEditor(tester, initialText: 'old\n\nwhat — legacy');
|
||||||
|
|
||||||
// Non-conforming → blank/raw, no guided stepper offered.
|
// Non-conforming → blank/raw, no guided stepper offered.
|
||||||
expect(find.byType(Stepper), findsNothing);
|
expect(find.byType(LinearProgressIndicator), findsNothing);
|
||||||
expect(find.text('Guided'), findsNothing);
|
expect(find.text('Guided'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -249,7 +246,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Guided was requested but the text does not conform → opened in Raw.
|
// Guided was requested but the text does not conform → opened in Raw.
|
||||||
expect(find.byType(Stepper), findsNothing);
|
expect(find.byType(LinearProgressIndicator), findsNothing);
|
||||||
final raw = tester.widget<TextField>(find.byType(TextField));
|
final raw = tester.widget<TextField>(find.byType(TextField));
|
||||||
expect(raw.controller!.text, 'cannot be guided');
|
expect(raw.controller!.text, 'cannot be guided');
|
||||||
});
|
});
|
||||||
@ -268,7 +265,7 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('Step 1 of 2'), findsOneWidget);
|
expect(find.text('Step 1 of 2'), findsOneWidget);
|
||||||
expect(find.byType(DropdownButtonFormField<Priority>), findsOneWidget);
|
expect(find.byType(DropdownButtonFormField<Priority>), findsOneWidget);
|
||||||
expect(find.byType(Stepper), findsNothing);
|
expect(find.byType(LinearProgressIndicator), findsNothing);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -341,7 +338,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(committed, Priority.high);
|
expect(committed, Priority.high);
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
||||||
expect(find.byTooltip('Exit guided'), findsOneWidget);
|
expect(find.byTooltip('Exit guided'), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -364,7 +361,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(chromeVisible.last, true);
|
expect(chromeVisible.last, true);
|
||||||
expect(find.byType(Stepper), findsNothing);
|
expect(find.byType(LinearProgressIndicator), findsNothing);
|
||||||
expect(find.text('Raw'), findsOneWidget); // chrome's mode selector is back
|
expect(find.text('Raw'), findsOneWidget); // chrome's mode selector is back
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -379,7 +376,7 @@ void main() {
|
|||||||
initialText: conforming,
|
initialText: conforming,
|
||||||
onChromeVisibleChanged: chromeVisible.add,
|
onChromeVisibleChanged: chromeVisible.add,
|
||||||
);
|
);
|
||||||
expect(find.byType(Stepper), findsOneWidget);
|
expect(find.byType(LinearProgressIndicator), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.byTooltip('Exit guided'));
|
await tester.tap(find.byTooltip('Exit guided'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -387,4 +384,40 @@ void main() {
|
|||||||
expect(chromeVisible, contains(true));
|
expect(chromeVisible, contains(true));
|
||||||
expect(find.text('Raw'), findsOneWidget);
|
expect(find.text('Raw'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('last step shows Done instead of Next', (tester) async {
|
||||||
|
await pumpEditor(tester, initialTemplate: spec);
|
||||||
|
|
||||||
|
// Navigate to the final step.
|
||||||
|
for (var i = 0; i < spec.sections.length - 1; i++) {
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(find.text('Done'), findsOneWidget);
|
||||||
|
expect(find.text('Next'), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.text('${spec.sections.length} / ${spec.sections.length}'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Done on last step exits Guided and materialises text into Raw', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await pumpEditor(tester, initialTemplate: spec);
|
||||||
|
await tester.enterText(find.byType(TextField).first, 'My title');
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
for (var i = 0; i < spec.sections.length - 1; i++) {
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
await tester.tap(find.text('Done'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.byType(LinearProgressIndicator), findsNothing);
|
||||||
|
final raw = tester.widget<TextField>(find.byType(TextField));
|
||||||
|
expect(raw.controller!.text, startsWith('# My title'));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user