diff --git a/packages/material_ui/lib/src/_about_io.dart b/packages/material_ui/lib/src/_about_io.dart new file mode 100644 index 00000000000..bda9d23d9f0 --- /dev/null +++ b/packages/material_ui/lib/src/_about_io.dart @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' show Platform; + +/// The file name of the currently running executable. +/// +/// Used as the fallback application name by the about dialog widgets when +/// no `Title` ancestor is available. +String get executableName => Platform.resolvedExecutable.split(Platform.pathSeparator).last; diff --git a/packages/material_ui/lib/src/_about_web.dart b/packages/material_ui/lib/src/_about_web.dart new file mode 100644 index 00000000000..6da44e22387 --- /dev/null +++ b/packages/material_ui/lib/src/_about_web.dart @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The file name of the currently running executable. +/// +/// There is no executable on the web, so this is always the empty string. +String get executableName => ''; diff --git a/packages/material_ui/lib/src/about.dart b/packages/material_ui/lib/src/about.dart index be76f7f4ccb..28d2df8a4d3 100644 --- a/packages/material_ui/lib/src/about.dart +++ b/packages/material_ui/lib/src/about.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/// @docImport 'dart:io'; +/// /// @docImport 'package:cupertino_ui/cupertino_ui.dart'; /// /// @docImport 'drawer.dart'; @@ -9,13 +11,13 @@ library; import 'dart:developer' show Flow, Timeline; -import 'dart:io' show Platform; import 'package:cupertino_ui/cupertino_ui.dart' show CupertinoDialogAction; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/widgets.dart' hide Flow; +import '_about_io.dart' if (dart.library.js_interop) '_about_web.dart' as about; import 'app_bar.dart'; import 'back_button.dart'; import 'card.dart'; @@ -1201,7 +1203,7 @@ String _defaultApplicationName(BuildContext context) { // can provide an explicit applicationName to the widgets defined in this // file, instead of relying on the default. final Title? ancestorTitle = context.findAncestorWidgetOfExactType(); - return ancestorTitle?.title ?? Platform.resolvedExecutable.split(Platform.pathSeparator).last; + return ancestorTitle?.title ?? about.executableName; } String _defaultApplicationVersion(BuildContext context) { diff --git a/packages/material_ui/pending_changelogs/change_2026_09_07_about_dart_io.yaml b/packages/material_ui/pending_changelogs/change_2026_09_07_about_dart_io.yaml new file mode 100644 index 00000000000..86ac579ed0c --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_09_07_about_dart_io.yaml @@ -0,0 +1,3 @@ +changelog: | + - Removes the unconditional `dart:io` import from `AboutDialog`, so that `material_ui` is correctly detected as supporting the web platform and `showAboutDialog` no longer throws on the web when no application name is available. +version: patch diff --git a/packages/material_ui/test/about_test.dart b/packages/material_ui/test/about_test.dart index c300f76978f..0390e7b8e1e 100644 --- a/packages/material_ui/test/about_test.dart +++ b/packages/material_ui/test/about_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io' show Platform; import 'dart:ui'; import 'package:cupertino_ui/cupertino_ui.dart'; @@ -16,6 +17,47 @@ void main() { LicenseRegistry.reset(); }); + Widget buildWithoutTitle(Widget child) { + return MediaQuery( + data: const MediaQueryData(), + child: Localizations( + locale: const Locale('en', 'US'), + delegates: const <LocalizationsDelegate<dynamic>>[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Directionality( + textDirection: TextDirection.ltr, + child: Theme(data: ThemeData(), child: child), + ), + ), + ); + } + + testWidgets( + 'AboutDialog defaults the application name to the executable name without a Title ancestor', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/191887. + await tester.pumpWidget(buildWithoutTitle(const AboutDialog())); + + final String expectedName = Platform.resolvedExecutable.split(Platform.pathSeparator).last; + expect(find.text(expectedName), findsOneWidget); + }, + skip: kIsWeb, // [intended] There is no executable name on the web. + ); + + testWidgets( + 'AboutDialog does not throw on the web without a Title ancestor', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/191887. + await tester.pumpWidget(buildWithoutTitle(const AboutDialog())); + + expect(tester.takeException(), isNull); + expect(find.byType(AboutDialog), findsOneWidget); + }, + skip: !kIsWeb, // [intended] Exercises the web-specific fallback. + ); + testWidgets('Material3 has sentence case labels', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp(