From 5fe6cea0732036ab0a549df2267b06860c8834b0 Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Wed, 5 Feb 2025 09:38:00 +0100 Subject: [PATCH 1/7] test: add android caching, multi sdk testing --- .github/workflows/code-integration.yml | 33 ++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-integration.yml b/.github/workflows/code-integration.yml index 12b0a3d4..d27d59f8 100644 --- a/.github/workflows/code-integration.yml +++ b/.github/workflows/code-integration.yml @@ -17,6 +17,10 @@ jobs: integration_tests_android: name: 🤖 Android Tests runs-on: ubuntu-latest + strategy: + matrix: + api-level: [ 23, 35 ] + target: [ default, google_apis ] timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -35,9 +39,34 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - uses: reactivecircus/android-emulator-runner@v2 + - name: Gradle cache + uses: gradle/actions/setup-gradle@v4 + - name: AVD cache + uses: actions/cache@v4 + id: avd-cache with: - api-level: 29 + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ matrix.api-level }} + - name: create AVD and generate snapshot for caching + if: steps.avd-cache.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + force-avd-creation: false + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: echo "Generated AVD snapshot for caching." + - name: Run integration test + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: x86_64 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true script: flutter test integration_test working-directory: flutter_secure_storage/example From e99932a49184b2c427561d6beaaf89fa13b0edd6 Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Wed, 5 Feb 2025 09:43:13 +0100 Subject: [PATCH 2/7] test: update vm creation --- .github/workflows/code-integration.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code-integration.yml b/.github/workflows/code-integration.yml index d27d59f8..b6bbb006 100644 --- a/.github/workflows/code-integration.yml +++ b/.github/workflows/code-integration.yml @@ -49,11 +49,13 @@ jobs: ~/.android/avd/* ~/.android/adb* key: avd-${{ matrix.api-level }} - - name: create AVD and generate snapshot for caching + - name: Create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} + target: ${{ matrix.target }} + arch: x86_64 force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: false From 1d79d4ace20adb2450141bf115dd49424ca419f2 Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Wed, 5 Feb 2025 10:14:15 +0100 Subject: [PATCH 3/7] test: add unit tests for platform interface --- .../test/flutter_secure_storage_mock.dart | 56 ++++++ .../test/flutter_secure_storage_test.dart | 172 ++++++++++++++++-- 2 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 flutter_secure_storage/test/flutter_secure_storage_mock.dart diff --git a/flutter_secure_storage/test/flutter_secure_storage_mock.dart b/flutter_secure_storage/test/flutter_secure_storage_mock.dart new file mode 100644 index 00000000..05a3c034 --- /dev/null +++ b/flutter_secure_storage/test/flutter_secure_storage_mock.dart @@ -0,0 +1,56 @@ +import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockFlutterSecureStoragePlatform extends Mock + with MockPlatformInterfaceMixin + implements FlutterSecureStoragePlatform {} + +class ImplementsFlutterSecureStoragePlatform extends Mock + implements FlutterSecureStoragePlatform {} + +class ExtendsFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform { + @override + Future containsKey({ + required String key, + required Map options, + }) => + Future.value(true); + + @override + Future delete({ + required String key, + required Map options, + }) => + Future.value(); + + @override + Future deleteAll({required Map options}) => + Future.value(); + + @override + Future read({ + required String key, + required Map options, + }) => + Future.value(); + + @override + Future> readAll({required Map options}) => + Future.value({}); + + @override + Future write({ + required String key, + required String value, + required Map options, + }) => + Future.value(); + + // @override + // Future isCupertinoProtectedDataAvailable() => Future.value(true); + // + // @override + // Stream get onCupertinoProtectedDataAvailabilityChanged => + // Stream.value(true); +} diff --git a/flutter_secure_storage/test/flutter_secure_storage_test.dart b/flutter_secure_storage/test/flutter_secure_storage_test.dart index 3357ce7f..b1cce438 100644 --- a/flutter_secure_storage/test/flutter_secure_storage_test.dart +++ b/flutter_secure_storage/test/flutter_secure_storage_test.dart @@ -1,25 +1,169 @@ +import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -// ✅ Correct Mock Class Implementation -class MockFlutterSecureStoragePlatform extends Mock - with MockPlatformInterfaceMixin - implements FlutterSecureStoragePlatform {} +import 'flutter_secure_storage_mock.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late FlutterSecureStorage storage; late MockFlutterSecureStoragePlatform mockPlatform; + const channel = MethodChannel('plugins.it_nomads.com/flutter_secure_storage'); + final methodStorage = MethodChannelFlutterSecureStorage(); + final log = []; + + Future? handler(MethodCall methodCall) async { + log.add(methodCall); + if (methodCall.method == 'containsKey') { + return true; + } else if (methodCall.method == 'isProtectedDataAvailable') { + return true; + } + return null; + } + setUp(() { mockPlatform = MockFlutterSecureStoragePlatform(); FlutterSecureStoragePlatform.instance = mockPlatform; storage = const FlutterSecureStorage(); + + // Ensure method channel mock is set up for the tests + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, handler); + + log.clear(); // Clear logs before each test + }); + + tearDown(() { + log.clear(); // Clear logs after each test + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); // Remove the mock handler + }); + + group('Method Channel Interaction Tests for FlutterSecureStorage', () { + test('read', () async { + const key = 'test_key'; + const options = {}; + await methodStorage.read(key: key, options: options); + + expect( + log, + [ + isMethodCall( + 'read', + arguments: { + 'key': key, + 'options': options, + }, + ), + ], + ); + }); + + test('write', () async { + const key = 'test_key'; + const options = {}; + await methodStorage.write(key: key, value: 'test', options: options); + + expect( + log, + [ + isMethodCall( + 'write', + arguments: { + 'key': key, + 'value': 'test', + 'options': options, + }, + ), + ], + ); + }); + + test('containsKey', () async { + const key = 'test_key'; + const options = {}; + await methodStorage.write(key: key, value: 'test', options: options); + + final result = + await methodStorage.containsKey(key: key, options: options); + + expect(result, true); + }); + + test('delete', () async { + const key = 'test_key'; + const options = {}; + await methodStorage.write(key: key, value: 'test', options: options); + await methodStorage.delete(key: key, options: options); + + expect( + log, + [ + isMethodCall( + 'write', + arguments: { + 'key': key, + 'value': 'test', + 'options': options, + }, + ), + isMethodCall( + 'delete', + arguments: { + 'key': key, + 'options': options, + }, + ), + ], + ); + }); + + test('deleteAll', () async { + const options = {}; + await methodStorage.deleteAll(options: options); + + expect( + log, + [ + isMethodCall( + 'deleteAll', + arguments: { + 'options': options, + }, + ), + ], + ); + }); + }); + + group('Platform-Specific Interface Tests', () { + test('Cannot be implemented with `implements`', () { + expect( + () { + FlutterSecureStoragePlatform.instance = + ImplementsFlutterSecureStoragePlatform(); + }, + throwsA(isInstanceOf()), + ); + }); + + test('Can be mocked with `implements`', () { + final mock = MockFlutterSecureStoragePlatform(); + FlutterSecureStoragePlatform.instance = mock; + }); + + test('Can be extended', () { + FlutterSecureStoragePlatform.instance = + ExtendsFlutterSecureStoragePlatform(); + }); }); - group('FlutterSecureStorage Tests', () { + group('FlutterSecureStorage Methods Invocation Tests', () { const testKey = 'testKey'; const testValue = 'testValue'; @@ -118,7 +262,7 @@ void main() { }); }); - group('AndroidOptions Tests', () { + group('AndroidOptions Configuration Tests', () { test('Default AndroidOptions should have correct default values', () { const options = AndroidOptions.defaultOptions; @@ -201,7 +345,7 @@ void main() { }); }); - group('WebOptions Tests', () { + group('WebOptions Configuration Tests', () { test('Default WebOptions should have correct default values', () { const options = WebOptions.defaultOptions; @@ -261,7 +405,7 @@ void main() { }); }); - group('WindowsOptions Tests', () { + group('WindowsOptions Configuration Tests', () { test('Default WindowsOptions should have correct default values', () { const options = WindowsOptions.defaultOptions; @@ -308,7 +452,7 @@ void main() { }); }); - group('IOSOptions Tests', () { + group('iOSOptions Configuration Tests', () { test('Default IOSOptions should have correct default values', () { const options = IOSOptions.defaultOptions; @@ -368,8 +512,8 @@ void main() { }); }); - group('MacOsOptions Tests', () { - test('Default MacOsOptions should have correct default values', () { + group('macOSOptions Configuration Tests', () { + test('Default macOSOptions should have correct default values', () { // Ignore for test // ignore: use_named_constants const options = MacOsOptions(); @@ -382,7 +526,7 @@ void main() { }); }); - test('MacOsOptions with custom values', () { + test('macOSOptions with custom values', () { const options = MacOsOptions( accountName: 'macAccount', groupId: 'group.mac.example', @@ -400,7 +544,7 @@ void main() { }); }); - test('MacOsOptions defaultOptions matches default constructor', () { + test('macOSOptions defaultOptions matches default constructor', () { const defaultOptions = MacOsOptions.defaultOptions; // Ignore for test // ignore: use_named_constants From 205b3b1d1a5460c1dc0f8fec865da27a28f26801 Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Wed, 5 Feb 2025 10:26:40 +0100 Subject: [PATCH 4/7] test: remove platform 23 --- .github/workflows/code-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-integration.yml b/.github/workflows/code-integration.yml index b6bbb006..dd67556f 100644 --- a/.github/workflows/code-integration.yml +++ b/.github/workflows/code-integration.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - api-level: [ 23, 35 ] + api-level: [ 35 ] target: [ default, google_apis ] timeout-minutes: 30 steps: From e144a00167ac727c62328b2d2ed905a8ce52fb0f Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Wed, 5 Feb 2025 10:29:34 +0100 Subject: [PATCH 5/7] Revert "test: add unit tests for platform interface" This reverts commit 1d79d4ace20adb2450141bf115dd49424ca419f2. --- .../test/flutter_secure_storage_mock.dart | 56 ------ .../test/flutter_secure_storage_test.dart | 172 ++---------------- 2 files changed, 14 insertions(+), 214 deletions(-) delete mode 100644 flutter_secure_storage/test/flutter_secure_storage_mock.dart diff --git a/flutter_secure_storage/test/flutter_secure_storage_mock.dart b/flutter_secure_storage/test/flutter_secure_storage_mock.dart deleted file mode 100644 index 05a3c034..00000000 --- a/flutter_secure_storage/test/flutter_secure_storage_mock.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockFlutterSecureStoragePlatform extends Mock - with MockPlatformInterfaceMixin - implements FlutterSecureStoragePlatform {} - -class ImplementsFlutterSecureStoragePlatform extends Mock - implements FlutterSecureStoragePlatform {} - -class ExtendsFlutterSecureStoragePlatform extends FlutterSecureStoragePlatform { - @override - Future containsKey({ - required String key, - required Map options, - }) => - Future.value(true); - - @override - Future delete({ - required String key, - required Map options, - }) => - Future.value(); - - @override - Future deleteAll({required Map options}) => - Future.value(); - - @override - Future read({ - required String key, - required Map options, - }) => - Future.value(); - - @override - Future> readAll({required Map options}) => - Future.value({}); - - @override - Future write({ - required String key, - required String value, - required Map options, - }) => - Future.value(); - - // @override - // Future isCupertinoProtectedDataAvailable() => Future.value(true); - // - // @override - // Stream get onCupertinoProtectedDataAvailabilityChanged => - // Stream.value(true); -} diff --git a/flutter_secure_storage/test/flutter_secure_storage_test.dart b/flutter_secure_storage/test/flutter_secure_storage_test.dart index b1cce438..3357ce7f 100644 --- a/flutter_secure_storage/test/flutter_secure_storage_test.dart +++ b/flutter_secure_storage/test/flutter_secure_storage_test.dart @@ -1,169 +1,25 @@ -import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage_platform_interface/flutter_secure_storage_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'flutter_secure_storage_mock.dart'; +// ✅ Correct Mock Class Implementation +class MockFlutterSecureStoragePlatform extends Mock + with MockPlatformInterfaceMixin + implements FlutterSecureStoragePlatform {} void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - late FlutterSecureStorage storage; late MockFlutterSecureStoragePlatform mockPlatform; - const channel = MethodChannel('plugins.it_nomads.com/flutter_secure_storage'); - final methodStorage = MethodChannelFlutterSecureStorage(); - final log = []; - - Future? handler(MethodCall methodCall) async { - log.add(methodCall); - if (methodCall.method == 'containsKey') { - return true; - } else if (methodCall.method == 'isProtectedDataAvailable') { - return true; - } - return null; - } - setUp(() { mockPlatform = MockFlutterSecureStoragePlatform(); FlutterSecureStoragePlatform.instance = mockPlatform; storage = const FlutterSecureStorage(); - - // Ensure method channel mock is set up for the tests - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, handler); - - log.clear(); // Clear logs before each test - }); - - tearDown(() { - log.clear(); // Clear logs after each test - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, null); // Remove the mock handler - }); - - group('Method Channel Interaction Tests for FlutterSecureStorage', () { - test('read', () async { - const key = 'test_key'; - const options = {}; - await methodStorage.read(key: key, options: options); - - expect( - log, - [ - isMethodCall( - 'read', - arguments: { - 'key': key, - 'options': options, - }, - ), - ], - ); - }); - - test('write', () async { - const key = 'test_key'; - const options = {}; - await methodStorage.write(key: key, value: 'test', options: options); - - expect( - log, - [ - isMethodCall( - 'write', - arguments: { - 'key': key, - 'value': 'test', - 'options': options, - }, - ), - ], - ); - }); - - test('containsKey', () async { - const key = 'test_key'; - const options = {}; - await methodStorage.write(key: key, value: 'test', options: options); - - final result = - await methodStorage.containsKey(key: key, options: options); - - expect(result, true); - }); - - test('delete', () async { - const key = 'test_key'; - const options = {}; - await methodStorage.write(key: key, value: 'test', options: options); - await methodStorage.delete(key: key, options: options); - - expect( - log, - [ - isMethodCall( - 'write', - arguments: { - 'key': key, - 'value': 'test', - 'options': options, - }, - ), - isMethodCall( - 'delete', - arguments: { - 'key': key, - 'options': options, - }, - ), - ], - ); - }); - - test('deleteAll', () async { - const options = {}; - await methodStorage.deleteAll(options: options); - - expect( - log, - [ - isMethodCall( - 'deleteAll', - arguments: { - 'options': options, - }, - ), - ], - ); - }); - }); - - group('Platform-Specific Interface Tests', () { - test('Cannot be implemented with `implements`', () { - expect( - () { - FlutterSecureStoragePlatform.instance = - ImplementsFlutterSecureStoragePlatform(); - }, - throwsA(isInstanceOf()), - ); - }); - - test('Can be mocked with `implements`', () { - final mock = MockFlutterSecureStoragePlatform(); - FlutterSecureStoragePlatform.instance = mock; - }); - - test('Can be extended', () { - FlutterSecureStoragePlatform.instance = - ExtendsFlutterSecureStoragePlatform(); - }); }); - group('FlutterSecureStorage Methods Invocation Tests', () { + group('FlutterSecureStorage Tests', () { const testKey = 'testKey'; const testValue = 'testValue'; @@ -262,7 +118,7 @@ void main() { }); }); - group('AndroidOptions Configuration Tests', () { + group('AndroidOptions Tests', () { test('Default AndroidOptions should have correct default values', () { const options = AndroidOptions.defaultOptions; @@ -345,7 +201,7 @@ void main() { }); }); - group('WebOptions Configuration Tests', () { + group('WebOptions Tests', () { test('Default WebOptions should have correct default values', () { const options = WebOptions.defaultOptions; @@ -405,7 +261,7 @@ void main() { }); }); - group('WindowsOptions Configuration Tests', () { + group('WindowsOptions Tests', () { test('Default WindowsOptions should have correct default values', () { const options = WindowsOptions.defaultOptions; @@ -452,7 +308,7 @@ void main() { }); }); - group('iOSOptions Configuration Tests', () { + group('IOSOptions Tests', () { test('Default IOSOptions should have correct default values', () { const options = IOSOptions.defaultOptions; @@ -512,8 +368,8 @@ void main() { }); }); - group('macOSOptions Configuration Tests', () { - test('Default macOSOptions should have correct default values', () { + group('MacOsOptions Tests', () { + test('Default MacOsOptions should have correct default values', () { // Ignore for test // ignore: use_named_constants const options = MacOsOptions(); @@ -526,7 +382,7 @@ void main() { }); }); - test('macOSOptions with custom values', () { + test('MacOsOptions with custom values', () { const options = MacOsOptions( accountName: 'macAccount', groupId: 'group.mac.example', @@ -544,7 +400,7 @@ void main() { }); }); - test('macOSOptions defaultOptions matches default constructor', () { + test('MacOsOptions defaultOptions matches default constructor', () { const defaultOptions = MacOsOptions.defaultOptions; // Ignore for test // ignore: use_named_constants From 53555046775a9bc3093f43fdd5b465be631f51e9 Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Wed, 5 Feb 2025 11:45:16 +0100 Subject: [PATCH 6/7] test: add build for ios, introduce small delay for vm to catchup --- .github/workflows/code-integration.yml | 3 +++ .../example/integration_test/app_test.dart | 16 +++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/code-integration.yml b/.github/workflows/code-integration.yml index dd67556f..eec94755 100644 --- a/.github/workflows/code-integration.yml +++ b/.github/workflows/code-integration.yml @@ -90,5 +90,8 @@ jobs: os: iOS os_version: ">=18.1" model: "iPhone 15" + - run: flutter pub get + - run: flutter build ios --simulator --target=integration_test/app_test.dart + working-directory: flutter_secure_storage/example - run: flutter test integration_test working-directory: flutter_secure_storage/example \ No newline at end of file diff --git a/flutter_secure_storage/example/integration_test/app_test.dart b/flutter_secure_storage/example/integration_test/app_test.dart index a5ae8c72..bc98ff68 100644 --- a/flutter_secure_storage/example/integration_test/app_test.dart +++ b/flutter_secure_storage/example/integration_test/app_test.dart @@ -78,9 +78,11 @@ void main() { }); } +Duration duration = const Duration(milliseconds: 300); + Future _setupHomePage(WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: HomePage())); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(duration); final pageObject = HomePageObject(tester); await pageObject.deleteAll(); return pageObject; @@ -112,7 +114,7 @@ class HomePageObject { final textField = find.byKey(const Key('value_field')); expect(textField, findsOneWidget, reason: 'Value text field not found'); await tester.enterText(textField, newValue); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(duration); await _tap(find.byKey(const Key('save'))); } @@ -142,11 +144,11 @@ class HomePageObject { final textField = find.byKey(const Key('key_field')); expect(textField, findsOneWidget); await tester.enterText(textField, keyWidget.data!); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(duration); // Confirm the action await tester.tap(find.text('OK')); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(duration); // Verify the SnackBar message final expectedText = 'Contains Key: $expectedResult'; @@ -168,11 +170,11 @@ class HomePageObject { final textField = find.byKey(const Key('key_field')); expect(textField, findsOneWidget); await tester.enterText(textField, keyWidget.data!); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(duration); // Confirm the action await tester.tap(find.text('OK')); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(duration); // Verify the SnackBar message expect(find.text('value: $expectedValue'), findsOneWidget); @@ -213,6 +215,6 @@ class HomePageObject { reason: 'Widget not found for tapping: $finder', ); await tester.tap(finder); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(duration); } } From 13a583a62e8eecc672ec31447ed4ac77662fe642 Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Wed, 5 Feb 2025 12:04:20 +0100 Subject: [PATCH 7/7] test: change android runner --- .github/workflows/code-integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-integration.yml b/.github/workflows/code-integration.yml index eec94755..6979c1d5 100644 --- a/.github/workflows/code-integration.yml +++ b/.github/workflows/code-integration.yml @@ -19,8 +19,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - api-level: [ 35 ] - target: [ default, google_apis ] + api-level: [ 23, 35 ] + target: [ default ] timeout-minutes: 30 steps: - uses: actions/checkout@v4