From 263b0048cbab7d03726da2a8287b591c0727542c Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Sun, 29 Mar 2026 17:25:03 +0300 Subject: [PATCH 1/3] fix: export DialogShell, fix button layout, add footerBuilder, fix body spacing (#7) - Export MagicStarterDialogShell publicly from barrel - Replace footer: Widget? with footerBuilder: Widget Function(BuildContext) so external callers can Navigator.pop with the dialog's own context - Fix ConfirmDialog and PasswordConfirmDialog buttons from full-width (flex-1) to compact right-aligned (justify-end gap-2 wrap) - Fix DialogShell body gap by replacing SingleChildScrollView with ListView(shrinkWrap: true) for shrink-to-content behavior --- .claude/rules/widgets.md | 3 +- CHANGELOG.md | 8 ++ CLAUDE.md | 2 +- lib/magic_starter.dart | 2 + .../widgets/magic_starter_confirm_dialog.dart | 30 +++----- .../widgets/magic_starter_dialog_shell.dart | 56 ++++++++++---- ...magic_starter_password_confirm_dialog.dart | 30 +++----- .../magic_starter_confirm_dialog_test.dart | 38 ++++++++++ .../magic_starter_dialog_shell_test.dart | 76 +++++++++++++++++-- ..._starter_password_confirm_dialog_test.dart | 56 ++++++++++++++ 10 files changed, 241 insertions(+), 60 deletions(-) diff --git a/.claude/rules/widgets.md b/.claude/rules/widgets.md index 84fc050..86492dd 100644 --- a/.claude/rules/widgets.md +++ b/.claude/rules/widgets.md @@ -20,6 +20,7 @@ path: "lib/src/ui/widgets/**/*.dart" - Confirm dialog: `MagicStarterConfirmDialog` with `static Future show(BuildContext context, {required String title, String? description, String? confirmLabel, String? cancelLabel, ConfirmDialogVariant variant, Future Function()? onConfirm})`; variant enum `ConfirmDialogVariant.primary` (default), `.danger`, `.warning` — controls confirm button styling - Confirm dialog variant usage: `ConfirmDialogVariant.danger` for destructive actions (delete team, revoke session), `.warning` for caution (leave team), `.primary` for neutral confirmations - Modal theme consumption: all dialogs (ConfirmDialog, PasswordConfirmDialog, TwoFactorModal) read tokens from `MagicStarter.manager.modalTheme` at build time — never hardcode dialog classNames; use theme fields (titleClassName, primaryButtonClassName, dangerButtonClassName, etc.) -- Dialog shell: `MagicStarterDialogShell` is internal-only (NOT exported from barrel) — sticky header/footer with scrollable body; uses Material Dialog shell + Wind UI content; all exported dialogs compose on top of it +- Dialog shell: `MagicStarterDialogShell` — exported from barrel; sticky header/footer with scrollable body (`ListView(shrinkWrap: true)`); uses Material `Dialog` shell + Wind UI content; accepts `footerBuilder: Widget Function(BuildContext dialogContext)?` so callers can safely call `Navigator.pop(dialogContext)` with the dialog's own context +- Dialog button layout: all dialog footers use compact right-aligned buttons with `justify-end gap-2 wrap` — never `flex-1` full-width buttons; `wrap` is required alongside `justify-end` to prevent overflow in constrained containers (Wind renders as `Wrap(alignment: WrapAlignment.end)`) - Wind UI exclusively — no Material widgets except `Icons.*` for icon references and `Dialog` shell in `MagicStarterDialogShell` - Dark mode: always pair light/dark classes: `bg-white dark:bg-gray-800` diff --git a/CHANGELOG.md b/CHANGELOG.md index b82ae96..1345e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Changed +- **MagicStarterDialogShell**: Now exported publicly from the barrel (`package:magic_starter/magic_starter.dart`) — consumer apps can compose custom dialogs on top of it +- **MagicStarterDialogShell**: `footer` parameter replaced with `footerBuilder` (`Widget Function(BuildContext dialogContext)?`) — provides the dialog's own `BuildContext` so callers can call `Navigator.pop(dialogContext)` without needing an outer context + +### Fixed +- **MagicStarterConfirmDialog** and **MagicStarterPasswordConfirmDialog**: Buttons are now compact and right-aligned (`justify-end gap-2 wrap`) — previously rendered as full-width (`flex-1`) buttons that stretched across the footer +- **MagicStarterDialogShell**: Body no longer creates a gap between scrollable content and the footer when content is shorter than the available height — switched from `SingleChildScrollView` to `ListView(shrinkWrap: true)` + ## [0.0.1-alpha.4] - 2026-03-29 ### ✨ New Features diff --git a/CLAUDE.md b/CLAUDE.md index 7d85aa8..6253a95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,7 +109,7 @@ Every feature, fix, or refactor must go through the red-green-refactor cycle: ## Skills & Extensions - `fluttersdk:magic-framework` — Magic Framework patterns: facades, service providers, IoC, Eloquent ORM, controllers, routing. Use for ANY code touching Magic APIs. -- `fluttersdk:magic-starter-widgets` — Reusable standalone widgets exported from `package:magic_starter/magic_starter.dart`: `MagicStarterCard` (with `CardVariant` enum: surface/inset/elevated), `MagicStarterPageHeader` (title, subtitle, leading, actions), `MagicStarterConfirmDialog` (with `ConfirmDialogVariant` enum: primary/danger/warning), `MagicStarterPasswordConfirmDialog`, `MagicStarterTwoFactorModal`. All accept plain callbacks — no internal controller coupling required. All modals read `MagicStarterModalTheme` tokens at build time. +- `fluttersdk:magic-starter-widgets` — Reusable standalone widgets exported from `package:magic_starter/magic_starter.dart`: `MagicStarterCard` (with `CardVariant` enum: surface/inset/elevated), `MagicStarterPageHeader` (title, subtitle, leading, actions), `MagicStarterConfirmDialog` (with `ConfirmDialogVariant` enum: primary/danger/warning), `MagicStarterPasswordConfirmDialog`, `MagicStarterTwoFactorModal`, `MagicStarterDialogShell` (sticky header/footer + scrollable body shell; accepts `footerBuilder: Widget Function(BuildContext dialogContext)?` so callers can `Navigator.pop(dialogContext)` safely). All accept plain callbacks — no internal controller coupling required. All modals read `MagicStarterModalTheme` tokens at build time. ## CI diff --git a/lib/magic_starter.dart b/lib/magic_starter.dart index 5df5dfc..cdf5e96 100644 --- a/lib/magic_starter.dart +++ b/lib/magic_starter.dart @@ -44,4 +44,6 @@ export 'src/ui/widgets/magic_starter_confirm_dialog.dart'; export 'src/ui/widgets/magic_starter_two_factor_modal.dart'; export 'src/ui/widgets/magic_starter_timezone_select.dart'; export 'src/ui/widgets/magic_starter_page_header.dart'; +export 'src/ui/widgets/magic_starter_dialog_shell.dart'; +export 'src/ui/widgets/hide_bottom_nav.dart'; export 'src/ui/views/teams/magic_starter_team_invitation_accept_view.dart'; diff --git a/lib/src/ui/widgets/magic_starter_confirm_dialog.dart b/lib/src/ui/widgets/magic_starter_confirm_dialog.dart index d3ffe20..74c0bac 100644 --- a/lib/src/ui/widgets/magic_starter_confirm_dialog.dart +++ b/lib/src/ui/widgets/magic_starter_confirm_dialog.dart @@ -127,27 +127,21 @@ class _MagicStarterConfirmDialogState extends State { title: widget.title, description: widget.description, body: const SizedBox.shrink(), - footer: WDiv( - className: 'flex flex-row gap-2 w-full', + footerBuilder: (_) => WDiv( + className: 'flex flex-row justify-end gap-2 wrap', children: [ - WDiv( - className: 'flex-1', - child: WAnchor( - onTap: _isLoading ? null : _onCancel, - child: WDiv( - className: theme.secondaryButtonClassName, - child: WText(cancelLabel), - ), + WAnchor( + onTap: _isLoading ? null : _onCancel, + child: WDiv( + className: theme.secondaryButtonClassName, + child: WText(cancelLabel), ), ), - WDiv( - className: 'flex-1', - child: WButton( - onTap: _isLoading ? null : _onConfirm, - isLoading: _isLoading, - className: 'w-full ${_resolveConfirmClassName()}', - child: WText(confirmLabel), - ), + WButton( + onTap: _isLoading ? null : _onConfirm, + isLoading: _isLoading, + className: _resolveConfirmClassName(), + child: WText(confirmLabel), ), ], ), diff --git a/lib/src/ui/widgets/magic_starter_dialog_shell.dart b/lib/src/ui/widgets/magic_starter_dialog_shell.dart index 16a9805..af0297b 100644 --- a/lib/src/ui/widgets/magic_starter_dialog_shell.dart +++ b/lib/src/ui/widgets/magic_starter_dialog_shell.dart @@ -3,20 +3,42 @@ import 'package:magic/magic.dart'; import '../../facades/magic_starter.dart'; -/// Internal dialog shell — NOT exported from barrel. -/// Provides consistent Dialog chrome with sticky header/footer and scrollable body. +/// Reusable dialog shell providing consistent chrome for all Magic Starter +/// dialogs — sticky header, scrollable body, and optional sticky footer. +/// +/// All visual tokens (container, header, title, description, body, footer +/// classNames, and `maxWidth`) are read from +/// `MagicStarter.manager.modalTheme` at build time. Set a custom theme via +/// `MagicStarter.useModalTheme()` before the first dialog is shown. +/// +/// **Parameters:** +/// - [title] — optional heading rendered in the sticky header section. +/// - [description] — optional sub-heading rendered below [title]. +/// - [body] — required content widget rendered in the scrollable body area. +/// - [footerBuilder] — optional builder for the sticky footer; receives the +/// dialog's own [BuildContext] so callers can access inherited widgets +/// (e.g. navigator) scoped to the dialog tree. +/// +/// **Layout caveat:** the body is wrapped in a `ListView(shrinkWrap: true)`, +/// which collapses to content height. If [body] itself contains a nested +/// `ListView`, give it an explicit height constraint to avoid unbounded layout +/// errors. class MagicStarterDialogShell extends StatelessWidget { final String? title; final String? description; final Widget body; - final Widget? footer; + + /// Builder for the sticky footer section. Receives the dialog's own + /// [BuildContext] so callers can access inherited widgets (e.g. theme, + /// navigator) scoped to the dialog tree. + final Widget Function(BuildContext dialogContext)? footerBuilder; const MagicStarterDialogShell({ super.key, this.title, this.description, required this.body, - this.footer, + this.footerBuilder, }); @override @@ -55,18 +77,24 @@ class MagicStarterDialogShell extends StatelessWidget { ], ), Flexible( - child: SingleChildScrollView( - child: WDiv( - className: theme.bodyClassName, - child: body, - ), + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.zero, + children: [ + WDiv( + className: theme.bodyClassName, + child: body, + ), + ], ), ), - if (footer != null) - WDiv( - key: const Key('magic_starter_dialog_shell_footer'), - className: theme.footerClassName, - child: footer, + if (footerBuilder != null) + Builder( + builder: (dialogContext) => WDiv( + key: const Key('magic_starter_dialog_shell_footer'), + className: theme.footerClassName, + child: footerBuilder!(dialogContext), + ), ), ], ), diff --git a/lib/src/ui/widgets/magic_starter_password_confirm_dialog.dart b/lib/src/ui/widgets/magic_starter_password_confirm_dialog.dart index 6957a08..c6f5544 100644 --- a/lib/src/ui/widgets/magic_starter_password_confirm_dialog.dart +++ b/lib/src/ui/widgets/magic_starter_password_confirm_dialog.dart @@ -171,28 +171,20 @@ class _MagicStarterPasswordConfirmDialogState // Footer WDiv( className: - '${theme.footerClassName} flex flex-row gap-2 w-full', + '${theme.footerClassName} flex flex-row justify-end gap-2 wrap', children: [ - WDiv( - className: 'flex-1', - child: WAnchor( - onTap: _isLoading ? null : _onCancel, - child: WDiv( - className: - '${theme.secondaryButtonClassName} text-center', - child: WText(trans('common.cancel')), - ), + WAnchor( + onTap: _isLoading ? null : _onCancel, + child: WDiv( + className: theme.secondaryButtonClassName, + child: WText(trans('common.cancel')), ), ), - WDiv( - className: 'flex-1', - child: WButton( - onTap: _isLoading ? null : _onConfirm, - isLoading: _isLoading, - className: - 'w-full ${theme.primaryButtonClassName} text-center', - child: WText(trans('common.confirm')), - ), + WButton( + onTap: _isLoading ? null : _onConfirm, + isLoading: _isLoading, + className: theme.primaryButtonClassName, + child: WText(trans('common.confirm')), ), ], ), diff --git a/test/ui/widgets/magic_starter_confirm_dialog_test.dart b/test/ui/widgets/magic_starter_confirm_dialog_test.dart index 32d24c2..d3d990f 100644 --- a/test/ui/widgets/magic_starter_confirm_dialog_test.dart +++ b/test/ui/widgets/magic_starter_confirm_dialog_test.dart @@ -321,6 +321,44 @@ void main() { }, ); + // --------------------------------------------------------------------------- + // Button layout + // --------------------------------------------------------------------------- + + testWidgets( + 'footer buttons are compact and right-aligned — no flex-1 wrappers', + (WidgetTester tester) async { + tester.view.physicalSize = const Size(1200, 800); + tester.view.devicePixelRatio = 1.0; + addTearDown(() => tester.view.resetPhysicalSize()); + addTearDown(() => tester.view.resetDevicePixelRatio()); + + await tester.pumpWidget( + wrap( + const MagicStarterConfirmDialog(title: 'Confirm?'), + ), + ); + + // The footer row must be a Wrap widget (Wind `justify-end wrap` renders + // as Wrap with WrapAlignment.end) — not an expanded Row with flex-1 children. + expect(find.byType(Wrap), findsAtLeastNWidgets(1)); + + // Both buttons must be direct children of the Wrap — no intermediate + // flex-1 WDiv wrappers between the Wrap and the button widgets. + final wrapWidget = tester.widget( + find + .ancestor( + of: find.text('common.cancel'), + matching: find.byType(Wrap), + ) + .first, + ); + + // Wrap alignment must be end (right-aligned). + expect(wrapWidget.alignment, WrapAlignment.end); + }, + ); + // --------------------------------------------------------------------------- // Theme integration // --------------------------------------------------------------------------- diff --git a/test/ui/widgets/magic_starter_dialog_shell_test.dart b/test/ui/widgets/magic_starter_dialog_shell_test.dart index a9d3c96..f585928 100644 --- a/test/ui/widgets/magic_starter_dialog_shell_test.dart +++ b/test/ui/widgets/magic_starter_dialog_shell_test.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:magic/magic.dart'; import 'package:magic_starter/magic_starter.dart'; -import 'package:magic_starter/src/ui/widgets/magic_starter_dialog_shell.dart'; void main() { setUp(() async { @@ -90,23 +89,25 @@ void main() { expect(find.text('unique body content'), findsOneWidget); }); - testWidgets('renders footer widget when provided', (tester) async { + testWidgets('renders footer widget when footerBuilder is provided', + (tester) async { tester.view.physicalSize = const Size(1200, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(wrap( - const MagicStarterDialogShell( - body: Text('body content'), - footer: Text('footer content'), + MagicStarterDialogShell( + body: const Text('body content'), + footerBuilder: (_) => const Text('footer content'), ), )); expect(find.text('footer content'), findsOneWidget); }); - testWidgets('footer section absent when footer is null', (tester) async { + testWidgets('footer section absent when footerBuilder is null', + (tester) async { tester.view.physicalSize = const Size(1200, 800); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); @@ -118,13 +119,74 @@ void main() { ), )); - // The footer placeholder key must not be present when footer is null. + // The footer placeholder key must not be present when footerBuilder is null. expect( find.byKey(const Key('magic_starter_dialog_shell_footer')), findsNothing, ); }); + testWidgets('footerBuilder callback receives a valid BuildContext', + (tester) async { + tester.view.physicalSize = const Size(1200, 800); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + BuildContext? capturedContext; + + await tester.pumpWidget(wrap( + MagicStarterDialogShell( + body: const Text('body content'), + footerBuilder: (dialogContext) { + capturedContext = dialogContext; + return const Text('footer with context'); + }, + ), + )); + + expect(find.text('footer with context'), findsOneWidget); + expect(capturedContext, isNotNull); + // Verify the context is a valid mounted context by reading media query. + expect(MediaQuery.maybeOf(capturedContext!), isNotNull); + }); + + testWidgets('body uses ListView so it shrinks to content height', + (tester) async { + tester.view.physicalSize = const Size(1200, 800); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(wrap( + MagicStarterDialogShell( + body: const Text('short body'), + footerBuilder: (_) => const Text('footer below body'), + ), + )); + + // Body must not fill all available height when content is short — ListView + // with shrinkWrap: true collapses to content height. Verify the footer is + // visible immediately below the body without an expanding gap. + expect(find.text('short body'), findsOneWidget); + expect(find.text('footer below body'), findsOneWidget); + + // Neither widget should require scrolling — both visible in one frame. + expect(find.text('short body'), findsOneWidget); + expect(find.text('footer below body'), findsOneWidget); + + // Confirm no SingleChildScrollView is a descendant of the dialog shell + // (the body is now wrapped by ListView, not SingleChildScrollView). + final shellFinder = find.byType(MagicStarterDialogShell); + expect( + find.descendant( + of: shellFinder, + matching: find.byType(SingleChildScrollView), + ), + findsNothing, + ); + }); + testWidgets( 'reads containerClassName from MagicStarter.manager.modalTheme', (tester) async { diff --git a/test/ui/widgets/magic_starter_password_confirm_dialog_test.dart b/test/ui/widgets/magic_starter_password_confirm_dialog_test.dart index 4cf900d..f6e2f3d 100644 --- a/test/ui/widgets/magic_starter_password_confirm_dialog_test.dart +++ b/test/ui/widgets/magic_starter_password_confirm_dialog_test.dart @@ -158,6 +158,62 @@ void main() { expect(result, isTrue); }); + group('compact right-aligned button layout', () { + testWidgets('footer has no flex-1 wrapper divs', (tester) async { + tester.view.physicalSize = const Size(1200, 800); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(wrap(const MagicStarterPasswordConfirmDialog())); + + final flex1Divs = find.byWidgetPredicate( + (widget) => + widget is WDiv && + widget.className != null && + widget.className!.contains('flex-1'), + ); + expect(flex1Divs, findsNothing); + }); + + testWidgets('footer container uses justify-end for right-alignment', + (tester) async { + tester.view.physicalSize = const Size(1200, 800); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(wrap(const MagicStarterPasswordConfirmDialog())); + + expect( + find.byWidgetPredicate( + (widget) => + widget is WDiv && + widget.className != null && + widget.className!.contains('justify-end'), + ), + findsOneWidget, + ); + }); + + testWidgets('confirm WButton has no w-full className', (tester) async { + tester.view.physicalSize = const Size(1200, 800); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + await tester.pumpWidget(wrap(const MagicStarterPasswordConfirmDialog())); + + final wButtons = find.byWidgetPredicate( + (widget) => + widget is WButton && + widget.className != null && + widget.className!.contains('w-full'), + ); + expect(wButtons, findsNothing); + }); + }); + group('modal theme integration', () { testWidgets('uses custom containerClassName from modal theme', (WidgetTester tester) async { From 0f40bbdfc16aa424e42e5355d7f855ef8fe49d7b Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Sun, 29 Mar 2026 17:28:57 +0300 Subject: [PATCH 2/3] fix: remove untracked hide_bottom_nav export from barrel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The export line for hide_bottom_nav.dart was in the working tree before this PR and got included accidentally — the file does not exist in the repository yet. --- lib/magic_starter.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/magic_starter.dart b/lib/magic_starter.dart index cdf5e96..ad4c96b 100644 --- a/lib/magic_starter.dart +++ b/lib/magic_starter.dart @@ -45,5 +45,4 @@ export 'src/ui/widgets/magic_starter_two_factor_modal.dart'; export 'src/ui/widgets/magic_starter_timezone_select.dart'; export 'src/ui/widgets/magic_starter_page_header.dart'; export 'src/ui/widgets/magic_starter_dialog_shell.dart'; -export 'src/ui/widgets/hide_bottom_nav.dart'; export 'src/ui/views/teams/magic_starter_team_invitation_accept_view.dart'; From 96bc4da83f8eeb4d90fa6d6f75f4023685795ff0 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Sun, 29 Mar 2026 17:31:51 +0300 Subject: [PATCH 3/3] fix: address Copilot review + bump version to 0.0.1-alpha.5 - Scope flex-1/w-full assertions to footer container (not entire subtree) - Add geometry assertion for body-footer gap regression test - Add flex-1 and w-full absence checks in confirm dialog test - Bump version to 0.0.1-alpha.5 for release --- CHANGELOG.md | 2 +- CLAUDE.md | 2 +- pubspec.yaml | 2 +- .../magic_starter_confirm_dialog_test.dart | 44 +++++++++++++------ .../magic_starter_dialog_shell_test.dart | 16 ++++--- ..._starter_password_confirm_dialog_test.dart | 20 ++++++--- 6 files changed, 58 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1345e22..6d59857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## [Unreleased] +## [0.0.1-alpha.5] - 2026-03-29 ### Changed - **MagicStarterDialogShell**: Now exported publicly from the barrel (`package:magic_starter/magic_starter.dart`) — consumer apps can compose custom dialogs on top of it diff --git a/CLAUDE.md b/CLAUDE.md index 6253a95..b95d25a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Flutter starter kit for the Magic Framework. Pre-built Auth, Profile, Teams & Notifications — 13 opt-in features, every screen overridable via Wind UI. -**Version:** 0.0.1-alpha.4 · **Dart:** >=3.6.0 · **Flutter:** >=3.27.0 +**Version:** 0.0.1-alpha.5 · **Dart:** >=3.6.0 · **Flutter:** >=3.27.0 ## Commands diff --git a/pubspec.yaml b/pubspec.yaml index d0554e2..d49fa97 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: magic_starter description: Starter kit for Magic Framework. Auth, Profile, Teams, Notifications — 13 opt-in features with overridable views. -version: 0.0.1-alpha.4 +version: 0.0.1-alpha.5 homepage: https://magic.fluttersdk.com/starter documentation: https://magic.fluttersdk.com/packages/starter/getting-started/installation repository: https://github.com/fluttersdk/magic_starter diff --git a/test/ui/widgets/magic_starter_confirm_dialog_test.dart b/test/ui/widgets/magic_starter_confirm_dialog_test.dart index d3d990f..8aeb129 100644 --- a/test/ui/widgets/magic_starter_confirm_dialog_test.dart +++ b/test/ui/widgets/magic_starter_confirm_dialog_test.dart @@ -339,23 +339,39 @@ void main() { ), ); - // The footer row must be a Wrap widget (Wind `justify-end wrap` renders - // as Wrap with WrapAlignment.end) — not an expanded Row with flex-1 children. - expect(find.byType(Wrap), findsAtLeastNWidgets(1)); - - // Both buttons must be direct children of the Wrap — no intermediate - // flex-1 WDiv wrappers between the Wrap and the button widgets. - final wrapWidget = tester.widget( - find - .ancestor( - of: find.text('common.cancel'), - matching: find.byType(Wrap), - ) - .first, - ); + // Locate the specific footer Wrap that contains the cancel button. + final footerWrapFinder = find + .ancestor( + of: find.text('common.cancel'), + matching: find.byType(Wrap), + ) + .first; + + final wrapWidget = tester.widget(footerWrapFinder); // Wrap alignment must be end (right-aligned). expect(wrapWidget.alignment, WrapAlignment.end); + + // Within the footer container, there must be no flex-1 WDiv wrappers. + final flex1WrapperFinder = find.descendant( + of: footerWrapFinder, + matching: find.byWidgetPredicate( + (widget) => + widget is WDiv && (widget.className?.contains('flex-1') ?? false), + ), + ); + expect(flex1WrapperFinder, findsNothing); + + // And no WButton in the footer should be forced to full width. + final fullWidthButtonFinder = find.descendant( + of: footerWrapFinder, + matching: find.byWidgetPredicate( + (widget) => + widget is WButton && + (widget.className?.contains('w-full') ?? false), + ), + ); + expect(fullWidthButtonFinder, findsNothing); }, ); diff --git a/test/ui/widgets/magic_starter_dialog_shell_test.dart b/test/ui/widgets/magic_starter_dialog_shell_test.dart index f585928..78be874 100644 --- a/test/ui/widgets/magic_starter_dialog_shell_test.dart +++ b/test/ui/widgets/magic_starter_dialog_shell_test.dart @@ -165,15 +165,19 @@ void main() { ), )); - // Body must not fill all available height when content is short — ListView - // with shrinkWrap: true collapses to content height. Verify the footer is - // visible immediately below the body without an expanding gap. + // Both widgets must be rendered. expect(find.text('short body'), findsOneWidget); expect(find.text('footer below body'), findsOneWidget); - // Neither widget should require scrolling — both visible in one frame. - expect(find.text('short body'), findsOneWidget); - expect(find.text('footer below body'), findsOneWidget); + // Geometry assertion: footer must sit directly below the body content + // without an expanding gap. The distance between body bottom and footer + // top should be small (only theme padding, not leftover Flexible space). + final bodyBottom = tester.getBottomLeft(find.text('short body')).dy; + final footerTop = tester.getTopLeft(find.text('footer below body')).dy; + final gap = footerTop - bodyBottom; + + // Gap should be modest (theme padding) — not hundreds of pixels. + expect(gap, lessThan(80)); // Confirm no SingleChildScrollView is a descendant of the dialog shell // (the body is now wrapped by ListView, not SingleChildScrollView). diff --git a/test/ui/widgets/magic_starter_password_confirm_dialog_test.dart b/test/ui/widgets/magic_starter_password_confirm_dialog_test.dart index f6e2f3d..90b009f 100644 --- a/test/ui/widgets/magic_starter_password_confirm_dialog_test.dart +++ b/test/ui/widgets/magic_starter_password_confirm_dialog_test.dart @@ -167,11 +167,21 @@ void main() { await tester.pumpWidget(wrap(const MagicStarterPasswordConfirmDialog())); - final flex1Divs = find.byWidgetPredicate( - (widget) => - widget is WDiv && - widget.className != null && - widget.className!.contains('flex-1'), + // Locate the footer container (the Wrap rendered by Wind's justify-end). + final footerWrapFinder = find + .ancestor( + of: find.text('common.cancel'), + matching: find.byType(Wrap), + ) + .first; + + // No flex-1 WDiv wrappers inside the footer. + final flex1Divs = find.descendant( + of: footerWrapFinder, + matching: find.byWidgetPredicate( + (widget) => + widget is WDiv && (widget.className?.contains('flex-1') ?? false), + ), ); expect(flex1Divs, findsNothing); });