[file_selector] Throw for unsupported type groups (#6120)

diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md
index 8a4f217..0adf1ab 100644
--- a/packages/file_selector/file_selector_macos/CHANGELOG.md
+++ b/packages/file_selector/file_selector_macos/CHANGELOG.md
@@ -1,5 +1,8 @@
-## NEXT
+## 0.9.0
 
+* **BREAKING CHANGE**: Methods that take `XTypeGroup`s now throw an
+  `ArgumentError` if any group is not a wildcard (all filter types null or
+  empty), but doesn't include any of the filter types supported by macOS.
 * Ignores deprecation warnings for upcoming styleFrom button API changes.
 
 ## 0.8.2+2
diff --git a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart
index e50c296..74ce283 100644
--- a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart
+++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart
@@ -105,10 +105,19 @@
     };
     for (final XTypeGroup typeGroup in typeGroups) {
       // If any group allows everything, no filtering should be done.
+      if (typeGroup.allowsAny) {
+        return null;
+      }
+      // Reject a filter that isn't an allow-any, but doesn't set any
+      // macOS-supported filter categories.
       if ((typeGroup.extensions?.isEmpty ?? true) &&
           (typeGroup.macUTIs?.isEmpty ?? true) &&
           (typeGroup.mimeTypes?.isEmpty ?? true)) {
-        return null;
+        throw ArgumentError('Provided type group $typeGroup does not allow '
+            'all files, but does not set any of the macOS-supported filter '
+            'categories. At least one of "extensions", "macUTIs", or '
+            '"mimeTypes" must be non-empty for macOS if anything is '
+            'non-empty.');
       }
       allowedTypes[extensionKey]!.addAll(typeGroup.extensions ?? <String>[]);
       allowedTypes[mimeTypeKey]!.addAll(typeGroup.mimeTypes ?? <String>[]);
diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml
index 8c6d1f7..fc9ca4d 100644
--- a/packages/file_selector/file_selector_macos/pubspec.yaml
+++ b/packages/file_selector/file_selector_macos/pubspec.yaml
@@ -2,7 +2,7 @@
 description: macOS implementation of the file_selector plugin.
 repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_macos
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.8.2+2
+version: 0.9.0
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -18,7 +18,7 @@
 
 dependencies:
   cross_file: ^0.3.1
-  file_selector_platform_interface: ^2.0.4
+  file_selector_platform_interface: ^2.1.0
   flutter:
     sdk: flutter
   flutter_test:
diff --git a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart
index 1c1b9c1..40ab585 100644
--- a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart
+++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart
@@ -62,6 +62,7 @@
         ],
       );
     });
+
     test('passes initialDirectory correctly', () async {
       await plugin.openFile(initialDirectory: '/example/directory');
 
@@ -77,6 +78,7 @@
         ],
       );
     });
+
     test('passes confirmButtonText correctly', () async {
       await plugin.openFile(confirmButtonText: 'Open File');
 
@@ -92,7 +94,28 @@
         ],
       );
     });
+
+    test('throws for a type group that does not support macOS', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'images',
+        webWildCards: <String>['images/*'],
+      );
+
+      await expectLater(
+          plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('allows a wildcard group', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'text',
+      );
+
+      await expectLater(
+          plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]), completes);
+    });
   });
+
   group('openFiles', () {
     test('passes the accepted type groups correctly', () async {
       final XTypeGroup group = XTypeGroup(
@@ -127,6 +150,7 @@
         ],
       );
     });
+
     test('passes initialDirectory correctly', () async {
       await plugin.openFiles(initialDirectory: '/example/directory');
 
@@ -142,6 +166,7 @@
         ],
       );
     });
+
     test('passes confirmButtonText correctly', () async {
       await plugin.openFiles(confirmButtonText: 'Open File');
 
@@ -157,6 +182,26 @@
         ],
       );
     });
+
+    test('throws for a type group that does not support macOS', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'images',
+        webWildCards: <String>['images/*'],
+      );
+
+      await expectLater(
+          plugin.openFiles(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('allows a wildcard group', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'text',
+      );
+
+      await expectLater(
+          plugin.openFiles(acceptedTypeGroups: <XTypeGroup>[group]), completes);
+    });
   });
 
   group('getSavePath', () {
@@ -194,6 +239,7 @@
         ],
       );
     });
+
     test('passes initialDirectory correctly', () async {
       await plugin.getSavePath(initialDirectory: '/example/directory');
 
@@ -209,6 +255,7 @@
         ],
       );
     });
+
     test('passes confirmButtonText correctly', () async {
       await plugin.getSavePath(confirmButtonText: 'Open File');
 
@@ -224,33 +271,56 @@
         ],
       );
     });
-    group('getDirectoryPath', () {
-      test('passes initialDirectory correctly', () async {
-        await plugin.getDirectoryPath(initialDirectory: '/example/directory');
 
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
-              'initialDirectory': '/example/directory',
-              'confirmButtonText': null,
-            }),
-          ],
-        );
-      });
-      test('passes confirmButtonText correctly', () async {
-        await plugin.getDirectoryPath(confirmButtonText: 'Open File');
+    test('throws for a type group that does not support macOS', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'images',
+        webWildCards: <String>['images/*'],
+      );
 
-        expect(
-          log,
-          <Matcher>[
-            isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
-              'initialDirectory': null,
-              'confirmButtonText': 'Open File',
-            }),
-          ],
-        );
-      });
+      await expectLater(
+          plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('allows a wildcard group', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'text',
+      );
+
+      await expectLater(
+          plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[group]),
+          completes);
+    });
+  });
+
+  group('getDirectoryPath', () {
+    test('passes initialDirectory correctly', () async {
+      await plugin.getDirectoryPath(initialDirectory: '/example/directory');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+            'initialDirectory': '/example/directory',
+            'confirmButtonText': null,
+          }),
+        ],
+      );
+    });
+
+    test('passes confirmButtonText correctly', () async {
+      await plugin.getDirectoryPath(confirmButtonText: 'Open File');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+            'initialDirectory': null,
+            'confirmButtonText': 'Open File',
+          }),
+        ],
+      );
     });
   });
 
diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md
index 3963601..8a8a53a 100644
--- a/packages/file_selector/file_selector_web/CHANGELOG.md
+++ b/packages/file_selector/file_selector_web/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.9.0
+
+* **BREAKING CHANGE**: Methods that take `XTypeGroup`s now throw an
+  `ArgumentError` if any group is not a wildcard (all filter types null or
+  empty), but doesn't include any of the filter types supported by web.
+
 ## 0.8.1+5
 
 * Minor fixes for new analysis options.
diff --git a/packages/file_selector/file_selector_web/lib/src/utils.dart b/packages/file_selector/file_selector_web/lib/src/utils.dart
index fe8d1f4..7a7aa7a 100644
--- a/packages/file_selector/file_selector_web/lib/src/utils.dart
+++ b/packages/file_selector/file_selector_web/lib/src/utils.dart
@@ -11,7 +11,11 @@
   }
   final List<String> allTypes = <String>[];
   for (final XTypeGroup group in acceptedTypes) {
-    _assertTypeGroupIsValid(group);
+    // If any group allows everything, no filtering should be done.
+    if (group.allowsAny) {
+      return '';
+    }
+    _validateTypeGroup(group);
     if (group.extensions != null) {
       allTypes.addAll(group.extensions!.map(_normalizeExtension));
     }
@@ -25,13 +29,17 @@
   return allTypes.join(',');
 }
 
-/// Make sure that at least one of its fields is populated.
-void _assertTypeGroupIsValid(XTypeGroup group) {
-  assert(
-      !((group.extensions == null || group.extensions!.isEmpty) &&
-          (group.mimeTypes == null || group.mimeTypes!.isEmpty) &&
-          (group.webWildCards == null || group.webWildCards!.isEmpty)),
-      'At least one of extensions / mimeTypes / webWildCards is required for web.');
+/// Make sure that at least one of the supported fields is populated.
+void _validateTypeGroup(XTypeGroup group) {
+  if ((group.extensions?.isEmpty ?? true) &&
+      (group.mimeTypes?.isEmpty ?? true) &&
+      (group.webWildCards?.isEmpty ?? true)) {
+    throw ArgumentError('Provided type group $group does not allow '
+        'all files, but does not set any of the web-supported filter '
+        'categories. At least one of "extensions", "mimeTypes", or '
+        '"webWildCards" must be non-empty for web if anything is '
+        'non-empty.');
+  }
 }
 
 /// Append a dot at the beggining if it is not there png -> .png
diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml
index c685cca..a764cae 100644
--- a/packages/file_selector/file_selector_web/pubspec.yaml
+++ b/packages/file_selector/file_selector_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Web platform implementation of file_selector
 repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.8.1+5
+version: 0.9.0
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -17,7 +17,7 @@
         fileName: file_selector_web.dart
 
 dependencies:
-  file_selector_platform_interface: ^2.0.0
+  file_selector_platform_interface: ^2.1.0
   flutter:
     sdk: flutter
   flutter_web_plugins:
diff --git a/packages/file_selector/file_selector_web/test/utils_test.dart b/packages/file_selector/file_selector_web/test/utils_test.dart
index 9bddfd2..3281c4c 100644
--- a/packages/file_selector/file_selector_web/test/utils_test.dart
+++ b/packages/file_selector/file_selector_web/test/utils_test.dart
@@ -53,6 +53,13 @@
         final String accepts = acceptedTypesToString(acceptedTypes);
         expect(accepts, 'image/*,audio/*,video/*');
       });
+
+      test('throws for a type group that does not support web', () {
+        final List<XTypeGroup> acceptedTypes = <XTypeGroup>[
+          XTypeGroup(label: 'text', macUTIs: <String>['public.text']),
+        ];
+        expect(() => acceptedTypesToString(acceptedTypes), throwsArgumentError);
+      });
     });
   });
 }
diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md
index f5354b3..3f15de7 100644
--- a/packages/file_selector/file_selector_windows/CHANGELOG.md
+++ b/packages/file_selector/file_selector_windows/CHANGELOG.md
@@ -1,5 +1,8 @@
-## NEXT
+## 0.9.0
 
+* **BREAKING CHANGE**: Methods that take `XTypeGroup`s now throw an
+  `ArgumentError` if any group is not a wildcard (all filter types null or
+  empty), but doesn't include any of the filter types supported by Windows.
 * Ignores deprecation warnings for upcoming styleFrom button API changes.
 
 ## 0.8.2+2
diff --git a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart
index b91a223..6b1bc89 100644
--- a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart
+++ b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart
@@ -26,6 +26,7 @@
     String? initialDirectory,
     String? confirmButtonText,
   }) async {
+    _validateTypeGroups(acceptedTypeGroups);
     final List<String>? path = await _channel.invokeListMethod<String>(
       'openFile',
       <String, dynamic>{
@@ -46,6 +47,7 @@
     String? initialDirectory,
     String? confirmButtonText,
   }) async {
+    _validateTypeGroups(acceptedTypeGroups);
     final List<String>? pathList = await _channel.invokeListMethod<String>(
       'openFile',
       <String, dynamic>{
@@ -67,6 +69,7 @@
     String? suggestedName,
     String? confirmButtonText,
   }) async {
+    _validateTypeGroups(acceptedTypeGroups);
     return _channel.invokeMethod<String>(
       'getSavePath',
       <String, dynamic>{
@@ -93,4 +96,20 @@
       },
     );
   }
+
+  /// Throws an [ArgumentError] if any of the provided type groups are not valid
+  /// for Windows.
+  void _validateTypeGroups(List<XTypeGroup>? groups) {
+    if (groups == null) {
+      return;
+    }
+    for (final XTypeGroup group in groups) {
+      if (!group.allowsAny && (group.extensions?.isEmpty ?? true)) {
+        throw ArgumentError('Provided type group $group does not allow '
+            'all files, but does not set any of the Windows-supported filter '
+            'categories. "extensions" must be non-empty for Windows if '
+            'anything is non-empty.');
+      }
+    }
+  }
 }
diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml
index 7c933b2..13487fe 100644
--- a/packages/file_selector/file_selector_windows/pubspec.yaml
+++ b/packages/file_selector/file_selector_windows/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Windows implementation of the file_selector plugin.
 repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_windows
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.8.2+2
+version: 0.9.0
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -18,7 +18,7 @@
 
 dependencies:
   cross_file: ^0.3.1
-  file_selector_platform_interface: ^2.0.4
+  file_selector_platform_interface: ^2.1.0
   flutter:
     sdk: flutter
   flutter_test:
diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart
index 72604dd..48e13c2 100644
--- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart
+++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart
@@ -10,7 +10,7 @@
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
 
-  group('$FileSelectorWindows()', () {
+  group('FileSelectorWindows()', () {
     final FileSelectorWindows plugin = FileSelectorWindows();
 
     final List<MethodCall> log = <MethodCall>[];
@@ -63,6 +63,7 @@
           ],
         );
       });
+
       test('passes initialDirectory correctly', () async {
         await plugin.openFile(initialDirectory: '/example/directory');
 
@@ -78,6 +79,7 @@
           ],
         );
       });
+
       test('passes confirmButtonText correctly', () async {
         await plugin.openFile(confirmButtonText: 'Open File');
 
@@ -93,7 +95,29 @@
           ],
         );
       });
+
+      test('throws for a type group that does not support Windows', () async {
+        final XTypeGroup group = XTypeGroup(
+          label: 'text',
+          mimeTypes: <String>['text/plain'],
+        );
+
+        await expectLater(
+            plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+            throwsArgumentError);
+      });
+
+      test('allows a wildcard group', () async {
+        final XTypeGroup group = XTypeGroup(
+          label: 'text',
+        );
+
+        await expectLater(
+            plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+            completes);
+      });
     });
+
     group('#openFiles', () {
       test('passes the accepted type groups correctly', () async {
         final XTypeGroup group = XTypeGroup(
@@ -128,6 +152,7 @@
           ],
         );
       });
+
       test('passes initialDirectory correctly', () async {
         await plugin.openFiles(initialDirectory: '/example/directory');
 
@@ -143,6 +168,7 @@
           ],
         );
       });
+
       test('passes confirmButtonText correctly', () async {
         await plugin.openFiles(confirmButtonText: 'Open File');
 
@@ -158,6 +184,27 @@
           ],
         );
       });
+
+      test('throws for a type group that does not support Windows', () async {
+        final XTypeGroup group = XTypeGroup(
+          label: 'text',
+          mimeTypes: <String>['text/plain'],
+        );
+
+        await expectLater(
+            plugin.openFiles(acceptedTypeGroups: <XTypeGroup>[group]),
+            throwsArgumentError);
+      });
+
+      test('allows a wildcard group', () async {
+        final XTypeGroup group = XTypeGroup(
+          label: 'text',
+        );
+
+        await expectLater(
+            plugin.openFiles(acceptedTypeGroups: <XTypeGroup>[group]),
+            completes);
+      });
     });
 
     group('#getSavePath', () {
@@ -194,6 +241,7 @@
           ],
         );
       });
+
       test('passes initialDirectory correctly', () async {
         await plugin.getSavePath(initialDirectory: '/example/directory');
 
@@ -209,6 +257,7 @@
           ],
         );
       });
+
       test('passes confirmButtonText correctly', () async {
         await plugin.getSavePath(confirmButtonText: 'Open File');
 
@@ -224,33 +273,56 @@
           ],
         );
       });
-      group('#getDirectoryPath', () {
-        test('passes initialDirectory correctly', () async {
-          await plugin.getDirectoryPath(initialDirectory: '/example/directory');
 
-          expect(
-            log,
-            <Matcher>[
-              isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
-                'initialDirectory': '/example/directory',
-                'confirmButtonText': null,
-              }),
-            ],
-          );
-        });
-        test('passes confirmButtonText correctly', () async {
-          await plugin.getDirectoryPath(confirmButtonText: 'Open File');
+      test('throws for a type group that does not support Windows', () async {
+        final XTypeGroup group = XTypeGroup(
+          label: 'text',
+          mimeTypes: <String>['text/plain'],
+        );
 
-          expect(
-            log,
-            <Matcher>[
-              isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
-                'initialDirectory': null,
-                'confirmButtonText': 'Open File',
-              }),
-            ],
-          );
-        });
+        await expectLater(
+            plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[group]),
+            throwsArgumentError);
+      });
+
+      test('allows a wildcard group', () async {
+        final XTypeGroup group = XTypeGroup(
+          label: 'text',
+        );
+
+        await expectLater(
+            plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[group]),
+            completes);
+      });
+    });
+
+    group('#getDirectoryPath', () {
+      test('passes initialDirectory correctly', () async {
+        await plugin.getDirectoryPath(initialDirectory: '/example/directory');
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+              'initialDirectory': '/example/directory',
+              'confirmButtonText': null,
+            }),
+          ],
+        );
+      });
+
+      test('passes confirmButtonText correctly', () async {
+        await plugin.getDirectoryPath(confirmButtonText: 'Open File');
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+              'initialDirectory': null,
+              'confirmButtonText': 'Open File',
+            }),
+          ],
+        );
       });
     });
   });