diff --git a/dart_test.yaml b/dart_test.yaml index 4be9e3f3..f0e6c820 100644 --- a/dart_test.yaml +++ b/dart_test.yaml @@ -2,16 +2,33 @@ tags: # Tests which we want to run on desktop desktop: + timeout: none # Tests which we want to run on Android android: + timeout: none # Tests consuming quiet a lot of time slow: + timeout: none # Minimal tests # quick verification that the framework is working minimal: + timeout: none # OATH tests oath: + timeout: none + + # OTP tests + otp: + timeout: none + + # PIV tests + piv: + timeout: none + + # Management tests + management: + timeout: none \ No newline at end of file diff --git a/integration_test/management_test.dart b/integration_test/management_test.dart index b3fbeb91..ee5667d3 100644 --- a/integration_test/management_test.dart +++ b/integration_test/management_test.dart @@ -14,6 +14,7 @@ * limitations under the License. */ +@Tags(['desktop', 'management']) import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/integration_test/otp_test.dart b/integration_test/otp_test.dart index 0b9b8d84..0c0dc013 100644 --- a/integration_test/otp_test.dart +++ b/integration_test/otp_test.dart @@ -14,12 +14,14 @@ * limitations under the License. */ -@Tags(['android', 'desktop', 'oath']) +@Tags(['desktop', 'otp']) import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:yubico_authenticator/app/views/keys.dart'; import 'package:yubico_authenticator/otp/keys.dart'; +import 'package:yubico_authenticator/otp/models.dart'; +import 'utils/otp_test_util.dart'; import 'utils/test_util.dart'; void main() { @@ -27,21 +29,12 @@ void main() { binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; group('OTP UI tests', () { - appTest('OTP menu items exist', (WidgetTester tester) async { - await tester.tap(find.byKey(otpAppDrawer)); - await tester.shortWait(); - - await tester.tap(find.byKey(configureYubiOtp).hitTestable()); - await tester.shortWait(); - }); - appTest('Yubico OTP slot 1', (WidgetTester tester) async { await tester.tap(find.byKey(otpAppDrawer).hitTestable()); await tester.shortWait(); //verify "Slot 1 is empty" - - // we are missing the right click on top of the correct slot + await tester.openSlotMenu(SlotId.one); await tester.tap(find.byKey(configureYubiOtp).hitTestable()); await tester.shortWait(); @@ -65,7 +58,7 @@ void main() { // verify "Slot 1 is configured" - // we are missing the right click on top of the correct slot + await tester.openSlotMenu(SlotId.one); await tester.tap(find.byKey(configureChalResp).hitTestable()); await tester.shortWait(); @@ -85,9 +78,9 @@ void main() { // verify "Slot 2 is empty" - // we are missing the right click on top of the correct slot + await tester.openSlotMenu(SlotId.two); - await tester.tap(find.byKey(configureYubiOtp).hitTestable()); + await tester.tap(find.byKey(configureStatic).hitTestable()); await tester.shortWait(); // this generates and saves static password @@ -105,9 +98,9 @@ void main() { // verify "Slot 2 is configured" - // we are missing the right click on top of the correct slot + await tester.openSlotMenu(SlotId.two); - await tester.tap(find.byKey(configureYubiOtp).hitTestable()); + await tester.tap(find.byKey(configureHotp).hitTestable()); await tester.shortWait(); // this writes and saves oath secret @@ -127,17 +120,14 @@ void main() { // verify "Slot 2 is configured" // taps swap - await tester.tap(find.byKey(actionsIconButtonKey).hitTestable()); - await tester.shortWait(); - await tester.tap(find.byKey(swapSlots).hitTestable()); - await tester.shortWait(); - await tester.tap(find.byKey(swap).hitTestable()); + await tester.tapSwapSlotsButton(); + await tester.tap(find.byKey(swapButton).hitTestable()); await tester.shortWait(); // verify "Slot 1 is configured" // verify "Slot 2 is configured" - }); + appTest('Delete Credentials', (WidgetTester tester) async { await tester.tap(find.byKey(otpAppDrawer).hitTestable()); await tester.shortWait(); @@ -145,15 +135,24 @@ void main() { // verify "Slot 1 is configured" // verify "Slot 2 is configured" - // we need to right click on slot 1 + await tester.openSlotMenu(SlotId.one); await tester.tap(find.byKey(deleteAction).hitTestable()); - await tester.shortWait(); + await tester.longWait(); await tester.tap(find.byKey(deleteButton).hitTestable()); - await tester.shortWait(); + + // wait for any toasts to be gone + await tester.pump(const Duration(seconds: 3)); + var closeFinder = find.byKey(closeButton); + if (closeFinder.evaluate().isNotEmpty) { + // close the view + await tester.tap(closeFinder); + await tester.shortWait(); + } // we need to right click on slot 2 + await tester.openSlotMenu(SlotId.two); await tester.tap(find.byKey(deleteAction).hitTestable()); - await tester.shortWait(); + await tester.longWait(); await tester.tap(find.byKey(deleteButton).hitTestable()); await tester.shortWait(); diff --git a/integration_test/piv_test.dart b/integration_test/piv_test.dart index 0d249ff2..fa986a85 100644 --- a/integration_test/piv_test.dart +++ b/integration_test/piv_test.dart @@ -14,6 +14,7 @@ * limitations under the License. */ +@Tags(['desktop', 'piv']) import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/integration_test/utils/otp_test_util.dart b/integration_test/utils/otp_test_util.dart new file mode 100644 index 00000000..c2a64005 --- /dev/null +++ b/integration_test/utils/otp_test_util.dart @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 Yubico. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:yubico_authenticator/app/views/keys.dart'; +import 'package:yubico_authenticator/otp/keys.dart'; +import 'package:yubico_authenticator/otp/models.dart'; + +import 'test_util.dart'; + +extension OathFunctions on WidgetTester { + /// Opens the menu of specific OTP Slot, either by tapping the button or + /// by tapping the list item + Future openSlotMenu(SlotId slotId) async { + final menuButtonFinder = find.byKey(getOpenMenuButtonKey(slotId)); + if (menuButtonFinder.evaluate().isNotEmpty) { + await tap(menuButtonFinder); + } else { + await tap(find.byKey(getAppListItemKey(slotId))); + } + await longWait(); + } + + /// tap the Swap Slots button - either first open the action menu, + /// or try to find the button on visible screen + Future tapSwapSlotsButton() async { + final actionButtonFinder = find.byKey(actionsIconButtonKey); + if (actionButtonFinder.evaluate().isNotEmpty) { + await tap(actionButtonFinder); + await shortWait(); + } + + await tap(find.byKey(swapSlots).hitTestable()); + await longWait(); + } +} diff --git a/lib/otp/keys.dart b/lib/otp/keys.dart index 92d937e5..8686bfe1 100644 --- a/lib/otp/keys.dart +++ b/lib/otp/keys.dart @@ -16,6 +16,8 @@ import 'package:flutter/material.dart'; +import 'models.dart'; + const _prefix = 'otp.keys'; const _keyAction = '$_prefix.actions'; const _slotAction = '$_prefix.slot.actions'; @@ -34,7 +36,7 @@ const deleteAction = Key('$_slotAction.delete'); const saveButton = Key('$_prefix.save'); const deleteButton = Key('$_prefix.delete'); -const swap = Key('$_prefix.swap'); +const swapButton = Key('$_prefix.swap'); const secretField = Key('$_prefix.secret'); const publicIdField = Key('$_prefix.public_id'); @@ -42,4 +44,10 @@ const privateIdField = Key('$_prefix.private_id'); const useSerial = Key('$_prefix.use_serial'); const generatePrivateId = Key('$_prefix.generate_private_id'); -const generateSecretKey = Key('$_prefix.generate_secret_key'); \ No newline at end of file +const generateSecretKey = Key('$_prefix.generate_secret_key'); + +Key getOpenMenuButtonKey(SlotId slotId) => + Key('$_prefix.open_slot_menu_slot_${slotId.name}'); + +Key getAppListItemKey(SlotId slotId) => + Key('$_prefix.app_list_item_slot_${slotId.name}'); diff --git a/lib/otp/views/otp_screen.dart b/lib/otp/views/otp_screen.dart index a813e03c..5577a4c3 100644 --- a/lib/otp/views/otp_screen.dart +++ b/lib/otp/views/otp_screen.dart @@ -32,6 +32,7 @@ import '../../core/state.dart'; import '../../management/models.dart'; import '../../widgets/list_title.dart'; import '../features.dart' as features; +import '../keys.dart'; import '../models.dart'; import '../state.dart'; import 'actions.dart'; @@ -197,6 +198,7 @@ class _SlotListItem extends ConsumerWidget { final hasFeature = ref.watch(featureProvider); return AppListItem( + key: getAppListItemKey(slot), otpSlot, selected: selected, leading: CircleAvatar( @@ -209,6 +211,7 @@ class _SlotListItem extends ConsumerWidget { trailing: expanded ? null : OutlinedButton( + key: getOpenMenuButtonKey(slot), onPressed: Actions.handler(context, OpenIntent(otpSlot)), child: const Icon(Icons.more_horiz), ), diff --git a/lib/otp/views/swap_slots_dialog.dart b/lib/otp/views/swap_slots_dialog.dart index 5ca728e2..6bc4559c 100644 --- a/lib/otp/views/swap_slots_dialog.dart +++ b/lib/otp/views/swap_slots_dialog.dart @@ -36,7 +36,7 @@ class SwapSlotsDialog extends ConsumerWidget { title: Text(l10n.s_swap_slots), actions: [ TextButton( - key: swap, + key: swapButton, onPressed: () async { await ref.read(otpStateProvider(devicePath).notifier).swapSlots(); await ref.read(withContextProvider)((context) async {