diff --git a/integration_test/fixtures/test_data.dart b/integration_test/fixtures/test_data.dart index 1ba3dbb0..1ee86271 100644 --- a/integration_test/fixtures/test_data.dart +++ b/integration_test/fixtures/test_data.dart @@ -33,29 +33,29 @@ class InvalidPassword { } // Valid test user for login -const String validEmail = 'bassel@rythmify.com'; -const String validPassword = 'Biso1234'; +const String validEmail = 'yoeweida@gmail.com'; +const String validPassword = 'Yomna1234'; // New user for registration final TestUser newUser = TestUser( - email: 'newuser@rythmify.com', + email: 'testingNewUser1@gmail.com', password: 'NewUser1234', - username: 'newuser', - month: 'March', - day: '3', - year: '2013', + username: 'testing_user', + month: 'June', + day: '30', + year: '1999', gender: 'Female', ); // Existing user final TestUser existingUser = TestUser( - email: 'bassel@rythmify.com', - password: 'Biso1234', - username: 'user-002', + email: 'yoeweida@gmail.com', + password: 'Yomna1234', + username: 'yoeweidalll', month: 'March', - day: '5', - year: '2013', - gender: 'Male', + day: '3', + year: '2006', + gender: 'Female', ); // User with restricted age (below 13) @@ -93,6 +93,16 @@ final List invalidPasswords = [ ), ]; +// Profile edit test data +const String profileEditNewName = 'User Test'; +const String profileEditUserName = 'yoeweida'; +const String profileEditFirstName = 'YOMNA'; +const String profileEditLastName = 'Mahmoud'; +const String profileEditNewCity = 'Yafa'; +const String profileEditNewCountry = 'EG'; +const String profileEditNewCountryName = 'Egypt'; +const String profileEditNewBio = 'Integration test bio'; + // Scroll test values class ScrollValues { static const String days = '31'; diff --git a/integration_test/pages/M10_Notifications/notification_page.dart b/integration_test/pages/M10_Notifications/notification_page.dart new file mode 100644 index 00000000..8775fca4 --- /dev/null +++ b/integration_test/pages/M10_Notifications/notification_page.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class NotificationsPage extends BasePage { + NotificationsPage(WidgetTester tester) : super(tester); + + // ── Navigation ───────────────────────────────────────────────────────────── + Future tapNotificationIcon() async => await tapByKey(homeNotificationsButton); + Future tapBack() async => await tapByKey(notificationsBackButton); + Future tapTrackPageBack() async => await tapByKey(behindTheTrackBackButton); + Future tapProfilePageBack() async => await tapByKey(publicProfileBackButton); + + // ── Scroll ───────────────────────────────────────────────────────────────── + Future scrollDown() async { + await tester.drag(find.byKey(const Key(notificationsListView)), const Offset(0, -400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future scrollUp() async { + await tester.drag(find.byKey(const Key(notificationsListView)), const Offset(0, 400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + // ── Filter ───────────────────────────────────────────────────────────────── + Future tapFilterIcon() async { + await tapByKey(notificationsFilterIcon); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + Future tapFilterComments() async { + await tapByKey(notificationsFilterComments); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + Future tapFilterLikes() async { + await tapByKey(notificationsFilterLikes); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + Future tapFilterFollowing() async { + await tapByKey(notificationsFilterFollowing); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + Future tapFilterReposts() async { + await tapByKey(notificationsFilterReposts); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + Future tapFilterReactions() async { + await tapByKey(notificationsFilterReactions); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + Future tapFilterShowAll() async { + await tapByKey(notificationsFilterShowAll); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + // ── Tap first notification in list ───────────────────────────────────────── + Future tapFirstNotification() async { + final finder = find.byKey(const Key(notificationTileInkwell)); + + if (finder.evaluate().isEmpty) { + debugPrint('tapFirstNotification: no notification tiles found, skipping'); + return; + } + debugPrint('tapFirstNotification: found ${finder.evaluate().length} tiles, tapping first'); + // ensure the first tile is scrolled into view before tapping + await tester.ensureVisible(finder.first); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + await tester.tap(finder.first, warnIfMissed: false); + await tester.pumpAndSettle(const Duration(seconds: 4)); + } + + Future waitForNotificationsToLoad() async { + // first wait for bottom sheet to fully dismiss (filter button reappears) + for (int i = 0; i < 10; i++) { + await tester.pump(const Duration(milliseconds: 300)); + final filterVisible = find + .byKey(const Key(notificationsFilterIcon)) + .evaluate() + .isNotEmpty; + debugPrint('waitForNotifications: filterVisible=$filterVisible'); + if (filterVisible) break; + } + + // then wait for tiles or empty state + for (int i = 0; i < 20; i++) { + await tester.pump(const Duration(milliseconds: 500)); + + final isLoading = find + .byKey(const Key(notificationsLoadingIndicator)) + .evaluate() + .isNotEmpty; + + final hasTiles = find + .byKey(const Key(notificationTileInkwell)) + .evaluate() + .isNotEmpty; + + final isEmpty = find + .text('Switch to showing all to see recent notifications') + .evaluate() + .isNotEmpty; + + debugPrint('waitForNotifications: loading=$isLoading tiles=$hasTiles empty=$isEmpty'); + + if (!isLoading && (hasTiles || isEmpty)) break; + } + + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + // ── Visibility checks ────────────────────────────────────────────────────── + bool isNotificationsScreenVisible() => isVisible(notificationsScreenKey); + bool isHomePageVisible() => isVisible(homeScaffold); + + // ── Filter result checks ─────────────────────────────────────────────────── + // After applying a filter the screen is valid if EITHER: + // - the list has results (notificationsListKey visible), OR + // - the empty-filter message is shown (no notifications of that type) + // Both mean the filter was applied successfully. + bool isFilterAppliedSuccessfully() => + isVisible(notificationsListKey) || + find.text('Switch to showing all to see recent notifications') + .evaluate().isNotEmpty; + + bool hasNotificationItems() { + final count = find.byKey(const Key(notificationTileInkwell)).evaluate().length; + debugPrint('hasNotificationItems: $count tiles found'); + return count > 0; + } + + bool isOnTrackPage() => isVisible(behindTheTrackBackButton); + bool isOnProfilePage() => isVisible(profileAvatarGesture); +} \ No newline at end of file diff --git a/integration_test/pages/M12_Library/library_page.dart b/integration_test/pages/M12_Library/library_page.dart new file mode 100644 index 00000000..39fd2c96 --- /dev/null +++ b/integration_test/pages/M12_Library/library_page.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class LibraryPage extends BasePage { + LibraryPage(WidgetTester tester) : super(tester); + + Future _tapByText(String text) async { + await tester.tap(find.text(text)); + await tester.pumpAndSettle(); + } + + // ── Navigation ───────────────────────────────────────────────────────────── + Future tapLibraryNavButton() async => await tapByKey(libraryNavButton); + + // ── Library main screen sections ─────────────────────────────────────────── + Future tapYourLikes() async => await tapByKey(libraryLikesItem); + Future tapAlbums() async => await tapByKey(libraryAlbumsItem); + Future tapFollowing() async => await tapByKey( libraryFollowingItem); + Future tapStations() async => await tapByKey(libraryStationsItem); + Future tapYourInsights() async => await tapByKey(libraryInsightsItem); + Future tapYourUploads() async => await tapByKey(libraryUploadsItem); + Future tapRecentlyPlayedSeeAll() async => await tapByKey(libraryRecentlyPlayedSeeAllButton); + + // ── Generic scroll helpers ───────────────────────────────────────────────── + Future scrollListDown(String scrollableKey) async { + await tester.drag(find.byKey(Key(scrollableKey)), const Offset(0, -400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future scrollListUp(String scrollableKey) async { + await tester.drag(find.byKey(Key(scrollableKey)), const Offset(0, 400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + // ── Your Likes ───────────────────────────────────────────────────────────── + Future tapLikesBack() async => await tapByKey(libraryLikesBackButton); + Future tapLikesPlay() async => await tapByKey(libraryLikesPlayButton); + Future tapLikesShuffle() async => await tapByKey(libraryLikesShuffleButton); + + Future typeInLikesSearch(String query) async { + await enterTextByKey(libraryLikesSearchBar, query); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future tapLikesFilterButton() async => await tapByKey(libraryLikesFilterButton); + Future tapLikeFilterOptionTitleAZ() async => await tapByKey(libraryLikesFilterTitleAZ); + Future tapLikeFilterOptionMoreRecent() async => await tapByKey(libraryLikesFilterMoreRecent); + Future tapLikeFilterOptionArtistAZ() async => await tapByKey(libraryLikesFilterArtistAZ); + + // Finds the first "more" inkwell in the likes list and taps it + Future tapFirstTrackThreeDots() async { + final moreFinder = find.byWidgetPredicate( + (widget) => + widget is InkWell && + widget.key != null && + widget.key.toString().contains('track_card_') && + widget.key.toString().contains('_more_inkwell'), + ); + await tester.tap(moreFinder.first); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future tapUnlikeOption() async => await tapByKey(trackLikeInkwell); + bool isLikesScreenVisible() => isVisible(libraryLikesScrollView); + bool isTrackVisible(String trackName) => find.text(trackName).evaluate().isNotEmpty; + + // ── Albums ───────────────────────────────────────────────────────────────── + Future tapAlbumsBack() async { + if (isVisible(playlistDetailBackButton)) { + await tapByKey(playlistDetailBackButton); + } else { + await tapByKey(libraryAlbumsBackButton); + } + } + + Future typeInAlbumsSearch(String query) async { + await enterTextByKey(libraryAlbumsSearchField, query); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future tapAlbumLike() async => await tapByKey(playlistDetailLikeButton); + Future tapAlbumShowMore() async => await tapByKey(playlistDetailMoreButton); + Future tapAlbumShuffle() async => await tapByKey(playlistDetailShuffleButton); + Future tapAlbumPlay() async => await tapByKey(playlistDetailPlayButton); + Future tapAlbumFilterButton() async => await tapByKey(libraryAlbumsFilterButton); + Future tapAlbumFilterOptionRecentlyAdded() async => await tapByKey(libraryAlbumsFilterRecentlyAdded); + Future tapAlbumFilterOptionFirstAdded() async => await tapByKey(libraryAlbumsFilterFirstAdded); + Future tapAlbumFilterOptionAlbumName() async => await tapByKey(libraryAlbumsFilterAlbumName); + + Future tapFirstAlbum() async { + final finder = find.byWidgetPredicate( + (widget) => + widget is InkWell && + widget.key != null && + widget.key.toString().contains('album_tile_') && + widget.key.toString().contains('_inkwell'), + ); + await tester.tap(finder.first); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + + bool isAlbumsScreenVisible() => isVisible(libraryAlbumsBackButton); + bool isAlbumDetailVisible() => isVisible(libraryAlbumDetailScreen); + bool isNoResultsMessageVisible(String query) => + find.text('No results for "$query"').evaluate().isNotEmpty; + + // ── Following ────────────────────────────────────────────────────────────── + Future tapFollowingBack() async => await tapByKey(libraryFollowingBackButton); + + Future tapFirstFollower() async { + final finder = find.byWidgetPredicate( + (widget) => + widget is ListTile && + widget.key != null && + widget.key.toString().contains('following_item_') && + widget.key.toString().contains('_list_tile'), + ); + await tester.tap(finder.first); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + bool isProfileScreenVisible() => isVisible(publicProfileBackButton); + Future tapProfileBack() async => await tapByKey(publicProfileBackButton); + + Future tapFirstFollowingButton() async { + final finder = find.byWidgetPredicate( + (widget) => + widget is OutlinedButton && + widget.key != null && + widget.key.toString().contains('following_item_') && + widget.key.toString().contains('_following_button'), + ); + await tester.tap(finder.first); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + Future tapCancelOnDialog() async => await tapByKey(libraryFollowingDialogCancel); + Future tapUnfollowOnDialog() async => await tapByKey(libraryFollowingDialogUnfollow); + + bool isFollowingScreenVisible() => isVisible(followingListView); + bool isUnfollowDialogVisible() => isVisible(libraryFollowingUnfollowDialog); + + // ── Stations ─────────────────────────────────────────────────────────────── + Future tapStationsBack() async => await tapByKey(libraryStationsBackButton); + Future tapStationDetailBack() async => await tapByKey(libraryStationDetailScreen); + Future tapStationFilterButton() async => await tapByKey(libraryStationsFilterButton); + Future tapStationFilterOptionFirstAdded() async => await tapByKey(libraryStationsFilterFirstAdded); + Future tapStationFilterOptionStationName() async => await tapByKey(libraryStationsFilterStationName); + Future tapStationFilterOptionRecentlyAdded() async => await tapByKey(libraryStationsFilterRecentlyAdded); + + Future typeInStationsSearch(String query) async { + await enterTextByKey(libraryStationsSearchField, query); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future tapFirstStation() async { + final finder = find.byWidgetPredicate( + (widget) => + widget is InkWell && + widget.key != null && + widget.key.toString().contains('station_tile_') && + widget.key.toString().contains('_inkwell'), + ); + await tester.tap(finder.first); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + Future tapStationPlay() async => await tapByKey(libraryStationsPlayButton); + + bool isStationsScreenVisible() => isVisible(libraryStationsBackButton); + bool isStationDetailVisible() => isVisible(libraryStationDetailScreen); + + // ── Your Insights ────────────────────────────────────────────────────────── + Future tapAllPlatformsTab() async => await _tapByText('All Platforms'); + + Future scrollInsightsDown() async { + await tester.drag(find.byKey(const Key(insightsScDataListView)), const Offset(0, -400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future scrollInsightsUp() async { + await tester.drag(find.byKey(const Key(insightsScDataListView)), const Offset(0, 400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future tapInsightsBack() async { + if (isVisible(libraryInsightsBackButton)) { + await tapByKey(libraryInsightsBackButton); + } else { + await tapByKey(libraryHistoryBackButton); + } + } + + bool isInsightsScreenVisible() => isVisible(insightsSummaryCard); + bool isInsightsStatsVisible() => + isVisible(insightsTotalPlaysStat) && + isVisible(insightsTotalListenersStat) && + isVisible(insightsTotalLikesStat); + + // ── Your Uploads ─────────────────────────────────────────────────────────── + Future tapUploadsBack() async => await tapByKey(uploadsBackButton); + Future tapFirstUploadPlay() async => await tapByKey(uploadsPlayButton); + Future tapYourUploadsPlay() async => await tapByKey(uploadsPlayButton); + Future tapYourUploadsShuffle() async => await tapByKey(uploadsShuffleButton); + Future tapUpdatedTrack() async => await tapByKey(trackUpdateTrackInkwell); + + Future typeInUploadsSearch(String query) async { + await enterTextByKey(uploadsSearchBar, query); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future tapFirstUploadThreeDots() async { + final finder = find.byWidgetPredicate( + (widget) => + widget is InkWell && + widget.key != null && + widget.key.toString().contains('track_card_') && + widget.key.toString().contains('_more_inkwell'), + ); + await tester.tap(finder.first); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + Future tapDeleteUploadOption() async { + await tapByKey(uploadsDeleteOption); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await _tapByText('Delete'); + } + + bool isUploadsScreenVisible() => isVisible(uploadsScrollView); + + // ── Recently Played ──────────────────────────────────────────────────────── + Future tapHistoryPlay() async => await tapByKey(historyPlayAllFab); + Future tapHistoryShuffle() async => tapByKey(historyShuffleIconButton); + Future tapDeleteHistoryIcon() async => await tapByKey(historyClearIconButton); + Future tapCancelClearDialog() async => await tapByKey(libraryRecentlyPlayedDialogCancel); + Future tapClearOnDialog() async => await tapByKey(libraryRecentlyPlayedDialogClear); + + Future tapHistoryBack() async => await tapByKey(libraryHistoryBackButton); + + bool isRecentlyPlayedScreenVisible() => isVisible(historyListView); + bool isNoListeningHistoryVisible() => + find.text('No listening history').evaluate().isNotEmpty; +} \ No newline at end of file diff --git a/integration_test/pages/M13_Settings/settings_page.dart b/integration_test/pages/M13_Settings/settings_page.dart new file mode 100644 index 00000000..d80dcc56 --- /dev/null +++ b/integration_test/pages/M13_Settings/settings_page.dart @@ -0,0 +1,115 @@ +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class SettingsPage extends BasePage { + SettingsPage(super.tester); + + // ── Visibility ──────────────────────────────────────────────────────────── + + bool isOnBoardingPage() => isVisible(onboardingLoginButton); + + // ── Main Settings screen ────────────────────────────────────────────────── + + /// The settings icon lives on the Library tab — tap it to open Settings. + Future tapSettingsIcon() async { + await tapByKey(librarySettingsButton); + } + + Future tapImportMyMusic() async { + await tapByKey(settingsOptionImportMyMusic); + } + + Future tapAccount() async { + await tapByKey(settingsOptionAccount); + } + + Future tapUpload() async { + await tapByKey(settingsOptionUpload); + } + + Future tapBasicSettings() async { + await tapByKey(settingsOptionBasicSettings); + } + + Future tapSocailSettings() async { + await tapByKey(settingsOptionSocialSettings); + } + + Future tapInbox() async { + await tapByKey(settingsOptionInbox); + } + + Future tapNotifications() async { + await tapByKey(settingsOptionNotifications); + } + + Future tapAddWidgets() async { + await tapByKey(settingsOptionAddWidgets); + } + + // ── Back navigation ─────────────────────────────────────────────────────── + // Using tester.pageBack() — triggers the Navigator back action, + // no dedicated key needed for sub-screen back buttons. + + Future gobackFromImportMyMusic() async { + await tester.pageBack(); + await tester.pump(const Duration(milliseconds: 500)); + } + + Future gobackFromUpload() async { + await tester.pageBack(); + await tester.pump(const Duration(milliseconds: 500)); + } + + Future gobackFromBasicSettings() async { + await tester.pageBack(); + await tester.pump(const Duration(milliseconds: 500)); + } + + Future gobackFromSocailSettings() async { + await tester.pageBack(); + await tester.pump(const Duration(milliseconds: 500)); + } + + Future gobackFromInbox() async { + await tester.pageBack(); + await tester.pump(const Duration(milliseconds: 500)); + } + + Future gobackFromNotifications() async { + await tester.pageBack(); + await tester.pump(const Duration(milliseconds: 500)); + } + + Future gobackFromAddWidgets() async { + await tester.pageBack(); + await tester.pump(const Duration(milliseconds: 500)); + } + + // ── Basic settings screen ───────────────────────────────────────────────── + + Future tapOnClearAppCache() async { + await tapByKey(settingsClearCacheTile); + } + + // ── Confirmation dialogs ────────────────────────────────────────────────── + // Clear-cache and sign-out dialogs use standard AlertDialog text buttons — + // no dedicated keys exist in the codebase. + + Future tapYes() async { + await tester.tap(find.text('YES')); + await tester.pump(const Duration(milliseconds: 500)); + } + + Future tapOK() async { + await tester.tap(find.text('OK')); + await tester.pump(const Duration(milliseconds: 500)); + } + + // ── Account screen ──────────────────────────────────────────────────────── + + Future tapSignOut() async { + await tapByKey(settingsSignOutButton); + } +} diff --git a/integration_test/pages/M14_Premium/premium_page.dart b/integration_test/pages/M14_Premium/premium_page.dart new file mode 100644 index 00000000..665115c2 --- /dev/null +++ b/integration_test/pages/M14_Premium/premium_page.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class PremiumPage extends BasePage { + PremiumPage(super.tester); + + Future _tapByText(String text) async { + await tester.tap(find.text(text)); + await tester.pumpAndSettle(); + } + + // ── Navigation ───────────────────────────────────────────────────────────── + Future tapUpgradeNavButton() async => await tapByKey(upgradeNavButton); + + // ── Landing screen ───────────────────────────────────────────────────────── + bool isButtonsVisible() => + find.text('Continue').evaluate().isNotEmpty && + find.text('See all plans').evaluate().isNotEmpty; + + Future tapContinueButton() async => await _tapByText('Continue'); + Future tapSeeAllPlansButton() async => await _tapByText('See all plans'); + + // ── Checkout screen ──────────────────────────────────────────────────────── + Future tapConfirmPaymentButton() async => await _tapByText('Confirm Payment'); + + // ── Success sheet ────────────────────────────────────────────────────────── + Future tapStartExploring() async => await _tapByText('Start exploring'); + + // ── All Plans screen (PageView — no key in app, use byType) ─────────────── + @override + Future scrollHorizontallyInSection(String sectionKey) async { + await tester.drag(find.byType(PageView).first, const Offset(-300, 0)); + await tester.pumpAndSettle(); + } + + // ── Scrolling (no key on SingleChildScrollView — use byType) ────────────── + @override + Future scrollUntilVisible({ + required String itemText, + required String scrollableKey, + }) async { + await tester.dragUntilVisible( + find.text(itemText), + find.byType(SingleChildScrollView).first, + const Offset(0, -50), + ); + await tester.pumpAndSettle(); + } + + // ── Manage subscription (upgrade_screen → /cancellation) ────────────────── + Future tapMangeSubscribtionButton() async => + await _tapByText('Manage subscription'); + + // ── Cancellation screen ──────────────────────────────────────────────────── + // Opens the cancel dialog then taps "Keep Premium" to dismiss it. + Future tapKeepsubscribtionButton() async { + await _tapByText('Cancel subscription'); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await _tapByText('Keep Premium'); + } + + // Opens the cancel dialog then confirms cancellation. + Future tapCancelSubscribtionButton() async { + await _tapByText('Cancel subscription'); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await _tapByText('Cancel anyway'); + } +} diff --git a/integration_test/pages/auth/login_page.dart b/integration_test/pages/M1_Authentication/login_page.dart similarity index 81% rename from integration_test/pages/auth/login_page.dart rename to integration_test/pages/M1_Authentication/login_page.dart index 123812e8..be6f2b69 100644 --- a/integration_test/pages/auth/login_page.dart +++ b/integration_test/pages/M1_Authentication/login_page.dart @@ -22,19 +22,7 @@ class LoginPage extends BasePage { Future tapLogin() async { await tapByKey(authLoginPasswordSignInElevatedButton); } - - Future tapGoogleSignIn() async { - await tapByKey(authSocialGoogleButton); - } - - Future tapAppleSignIn() async { - await tapByKey(authSocialAppleButton); - } - - Future tapFacebookSignIn() async { - await tapByKey(authSocialFacebookButton); - } - + Future tapPasswordBack() async { await tapByKey(authLoginPasswordBackButton); } diff --git a/integration_test/pages/M1_Authentication/register_page.dart b/integration_test/pages/M1_Authentication/register_page.dart new file mode 100644 index 00000000..1698093f --- /dev/null +++ b/integration_test/pages/M1_Authentication/register_page.dart @@ -0,0 +1,106 @@ +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class RegisterPage extends BasePage { + RegisterPage(WidgetTester tester) : super(tester); + + Future enterEmail(String email) async { + await enterTextByKey(authEmailTextField, email); + } + + Future tapContinue() async { + await tapByKey(authContinueButton); + } + + Future enterPassword(String password) async { + await enterTextByKey(authPasswordTextField, password); + } + + Future tapPasswordContinue() async { + await tapByKey(authContinueElevatedButton); + } + + Future enterUsername(String username) async { + await enterTextByKey(authRegisterUsernameTextField, username); + } + + // DropdownButton opens all items in widget tree — just tap by text + Future selectMonth(String month) async { + await tapByKey(authRegisterMonthDropdown); + await scrollUntilVisible(itemText: month, scrollableKey: authRegisterMonthDropdown); + await tester.tap(find.text(month).last); + await tester.pumpAndSettle(); + } + + Future selectDay(String day) async { + await tapByKey(authRegisterDayDropdown); + await scrollUntilVisible(itemText: day, scrollableKey: authRegisterDayDropdown); + await tester.tap(find.text(day).last); + await tester.pumpAndSettle(); + } + + Future selectYear(String year) async { + await tapByKey(authRegisterYearDropdown); + await scrollUntilVisible(itemText: year, scrollableKey: authRegisterYearDropdown); + await tester.tap(find.text(year).last); + await tester.pumpAndSettle(); + } + + Future selectGender(String gender) async { + await tapByKey(authRegisterGenderDropdown); + await tester.tap(find.text(gender).last); + await tester.pumpAndSettle(); + } + + Future tapFinalContinue() async { + await tapByKey(authRegisterFinalContinueButton); + } + + Future register( + String email, + String password, + String username, + String month, + String day, + String year, + String gender, + ) async { + await enterEmail(email); + await tapContinue(); + await tester.pumpAndSettle(); + await enterPassword(password); + await tapPasswordContinue(); + await tester.pumpAndSettle(); + await enterUsername(username); + await selectMonth(month); + await selectDay(day); + await selectYear(year); + await selectGender(gender); + await tapFinalContinue(); + } + + bool isOnLoginPage() => isVisible(authTitleText); + bool isOnHomePage() => isVisible(homeScaffold); + bool isOnValidEmailPage() => isVisible(authVerifyEmailDescription); + bool isEmailEmptyErrorVisible() => find.text('Please enter your email').evaluate().isNotEmpty; + bool isInvalidEmailErrorVisible() => find.text('Please enter a valid email').evaluate().isNotEmpty; + bool isPasswordEmptyErrorVisible() => find.text('Please enter a password').evaluate().isNotEmpty; + bool isUsernameEmptyErrorVisible() => find.text('Please enter a display name').evaluate().isNotEmpty; + bool isDateOfBirthEmptyErrorVisible() => find.text('Please select your date of birth').evaluate().isNotEmpty; + bool isGenderEmptyErrorVisible() => find.text('Please select your gender').evaluate().isNotEmpty; + bool isAgeRestrictionErrorVisible() => find.text('You must be at least 13 years old to register').evaluate().isNotEmpty; + bool isAlreadyExistsErrorVisible() => find.text('An account with this email already exists.').evaluate().isNotEmpty; + + bool isSpecificPasswordErrorVisible(String errorKey) { + const errorMessages = { + 'ERROR_PASSWORD_TOO_SHORT': 'Password must be at least 8 characters', + 'ERROR_PASSWORD_NO_UPPERCASE': 'Password must contain an uppercase letter', + 'ERROR_PASSWORD_NO_LOWERCASE': 'Password must contain a lowercase letter', + 'ERROR_PASSWORD_NO_NUMBER': 'Password must contain a number', + }; + final message = errorMessages[errorKey]; + if (message == null) return false; + return find.text(message).evaluate().isNotEmpty; + } +} \ No newline at end of file diff --git a/integration_test/pages/M2_Profile/profile_page.dart b/integration_test/pages/M2_Profile/profile_page.dart new file mode 100644 index 00000000..35088721 --- /dev/null +++ b/integration_test/pages/M2_Profile/profile_page.dart @@ -0,0 +1,133 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class ProfilePage extends BasePage { + ProfilePage(WidgetTester tester) : super(tester); + + Future goToLibraryTab() async { + await tester.tap(find.text('Library')); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future tapProfileAvatar() async { + await tapByKey(libraryProfileAvatarGesture); + } + + + bool isOnProfilePage() => isVisible(publicProfileBackButton); + + bool isProfileDataVisible() { + return isVisible(publicProfileEditGesture) && + isVisible(publicProfilePlayButton); + } + + bool isProfileNameVisible(String name) { + return find.text(name).evaluate().isNotEmpty; + } + + Future tapEdit() async { + await tapByKey(publicProfileEditGesture); + } + + bool isOnEditProfilePage() => isVisible(editProfileSaveButton); + + Future enterName(String name) async { + await enterTextByKey(editProfileNameTextField, name); + } + + Future enterUsername(String name) async { + await enterTextByKey(editProfileUsernameTextField , name); + } + + Future enterFirstname(String name) async { + await enterTextByKey(editProfileFirstNameTextField , name); + } + + Future enterLastname(String name) async { + await enterTextByKey(editProfileLastNameTextField , name); + } + + Future enterCity(String city) async { + await enterTextByKey(editProfileCityTextField, city); + } + + Future tapCountryField() async { + await tester.dragUntilVisible( + find.byKey(Key(editProfileCountryGesture)), + find.byKey(Key(editProfileScrollView)), + const Offset(0, -100), + ); + await tester.pump(); + await tester.tap(find.byKey(Key(editProfileCountryGesture))); + // Use explicit pumps instead of pumpAndSettle — continuous animations can + // prevent pumpAndSettle from settling, causing it to return before the + // modal bottom sheet is in the widget tree. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 300)); + await tester.pump(const Duration(milliseconds: 300)); + } + + Future selectCountry(String countryName) async { + await tester.dragUntilVisible( + find.text(countryName), + find.byKey(const Key(editProfileCountryListView)), + const Offset(0, -100), + maxIteration: 100, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text(countryName)); + await tester.pumpAndSettle(); + } + + Future tapBioField() async { + await tester.dragUntilVisible( + find.byKey(Key(editProfileBioGesture)), + find.byKey(Key(editProfileScrollView)), + const Offset(0, -100), + ); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(find.byKey(Key(editProfileBioGesture))); + await tester.pumpAndSettle(); + } + + Future enterBio(String bio) async { + await enterTextByKey(editProfileBioTextField, bio); + } + + Future tapBioDone() async { + await tapByKey(editProfileBioDoneButton); + } + + Future tapSave() async { + await tapByKey(editProfileSaveButton); + } + + Future tapBack() async { + await tapByKey(editProfileBackButton); + } + + bool isUnsavedChangesDialogVisible() { + return isVisible(profileUnsavedChangesDiscard) && + isVisible(profileUnsavedChangesContinue); + } + + Future tapContinueEditing() async { + await tapByKey(profileUnsavedChangesContinue); + } + + Future tapDiscardChanges() async { + await tapByKey(profileUnsavedChangesDiscard); + } + + Future scrollUpUntilVisible(String key) async { + await tester.dragUntilVisible( + find.byKey(Key(key)), + find.byKey(const Key(homeScrollView)), + const Offset(0, 500), + maxIteration: 50, + ); + await tester.pumpAndSettle(); +} +} diff --git a/integration_test/pages/M4_Track/Track_page.dart b/integration_test/pages/M4_Track/Track_page.dart new file mode 100644 index 00000000..0d275c7b --- /dev/null +++ b/integration_test/pages/M4_Track/Track_page.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class TrackPage extends BasePage { + TrackPage(super.tester); + + Future playFromHotForYou() async { + await tapByKeyNow(hotForYouPlayButton); + await tester.pump(const Duration(seconds: 3)); + } + + Future openMiniPlayer() async { + await tester.tap(find.byKey(const Key(playerMiniPlayerGesture))); + await tester.pump(const Duration(seconds: 10)); + } + + Future tapBehindThisTrack() async { + await tester.tap(find.byKey(const Key(behindTheTrackBehindTrackButton)).last); + await tester.pumpAndSettle(const Duration(seconds: 3)); + } + + + Future tapBack() async { + await tapByKeyNow(behindTheTrackBackButton); + await tester.pump(const Duration(seconds: 2)); + } + + bool isTrackInfoVisible() { + return isVisible(behindTheTrackBackButton) && + find.byType(Image).evaluate().isNotEmpty; + } + + bool isShowMoreVisible() => isVisible(behindTheTrackShowMore); + + bool isDescriptionSheetVisible() => find.text('Description').evaluate().isNotEmpty; + + bool isTagsVisible() => isVisible(behindTheTrackTagsListView); + + bool isFollowButtonVisible() => + find.text('Follow').evaluate().isNotEmpty || + find.text('Following').evaluate().isNotEmpty; + + bool isOnArtistPage() => isVisible(publicProfileBackButton); + + + Future tapPlayPause() async { + await tapByKeyNow(behindTheTrackPlayPause); + await tester.pump(const Duration(seconds: 1)); + } + + Future tapShowMore() async { + await tapByKeyNow(behindTheTrackShowMore); + await tester.pump(const Duration(seconds: 1)); + } + + Future closeDescriptionSheet() async { + await tapByKeyNow(coreBottomSheetCloseButton); + await tester.pump(const Duration(seconds: 1)); + } + + Future scrollTagsHorizontally() async { + await scrollHorizontallyInSection(behindTheTrackTagsListView); + } + + Future tapFollowButton() async { + final finder = find.text('Follow').evaluate().isNotEmpty + ? find.text('Follow') + : find.text('Following'); + await tester.ensureVisible(finder.first); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(finder.first); + await tester.pump(const Duration(seconds: 2)); + } + + Future tapLike() async { + final finder = find.byIcon(Icons.favorite_border).evaluate().isNotEmpty + ? find.byIcon(Icons.favorite_border) + : find.byIcon(Icons.favorite); + await tester.tap(finder.first); + await tester.pump(const Duration(seconds: 1)); + } + + Future tapRepost() async { + await tester.tap(find.byIcon(Icons.repeat).first); + await tester.pump(const Duration(seconds: 1)); + } + + Future tapComment() async { + await tester.tap(find.byIcon(Icons.chat_outlined).first); + await tester.pumpAndSettle(const Duration(seconds: 3)); + } + + Future returnToBehindTheTrack() async { + await tapByKeyNow(commentsBackButton); + await tester.pump(const Duration(seconds: 2)); + } + + Future tapMore() async { + await tester.tap(find.byIcon(Icons.more_vert).first); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future dragToCloseMoreOptions() async { + await tester.drag( + find.byType(DraggableScrollableSheet).first, + const Offset(0, 500), + ); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future scrollUp() async { + await tester.drag( + find.byType(SingleChildScrollView).first, + const Offset(0, 1000), + ); + await tester.pump(const Duration(seconds: 1)); + } + + Future tapArtist() async { + final artistFinder = find.byWidgetPredicate( + (widget) => + widget.key?.toString().contains('track_details_section_artist_inkwell_') == true, + ); + await tester.ensureVisible(artistFinder.first); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(artistFinder.first); + await tester.pump(const Duration(seconds: 2)); + } + + bool isFansLeaderboardVisible() => + find.byWidgetPredicate( + (widget) => widget.key?.toString().contains('behind_the_track_fans_leaderboard_') == true, + ).evaluate().isNotEmpty; + + Future scrollToFansLeaderboard() async { + final finder = find.byWidgetPredicate( + (widget) => widget.key?.toString().contains('behind_the_track_fans_leaderboard_') == true, + ); + for (int i = 0; i < 10; i++) { + if (finder.evaluate().isNotEmpty) break; + await tester.drag( + find.byType(SingleChildScrollView).first, + const Offset(0, -300), + ); + await tester.pump(const Duration(milliseconds: 500)); + } + } + + FuturebackToHome() async { + await tapByKeyNow(behindTheTrackBackButton); + await tester.pumpAndSettle(const Duration(seconds: 3)); + } + + Future tapTopSegment() async { + await tapByKey(fansLeaderboardOverallButton); + await tester.pump(const Duration(seconds: 1)); + } + + Future tapFirstSegment() async { + await tapByKey(fansLeaderboardSevenDaysButton); + await tester.pump(const Duration(seconds: 1)); + } +} diff --git a/integration_test/pages/M5_Player/Player_page.dart b/integration_test/pages/M5_Player/Player_page.dart new file mode 100644 index 00000000..47b566f8 --- /dev/null +++ b/integration_test/pages/M5_Player/Player_page.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class PlayerPage extends BasePage { + PlayerPage(super.tester); + + // ── Navigation ────────────────────────────────────────────────────────────── + + /// Taps the play button on the first Hot For You track card. + Future playFromHotForYou() async { + await tester.ensureVisible(find.byKey(const Key(hotForYouPlayButton))); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(find.byKey(const Key(hotForYouPlayButton))); + await tester.pump(const Duration(seconds: 2)); // allow mini player to render + } + + /// Taps the mini player bar to open the full player. + Future openFullPlayer() async { + await tester.tap(find.byKey(const Key(playerMiniPlayerGesture))); + await tester.pump(const Duration(seconds: 3)); + } + + /// Taps the collapse (arrow-down) button to close the full player. + /// Uses .first because the PageView pre-renders adjacent pages, each + /// with its own collapse button sharing the same key. + Future tapCollapseButton() async { + final finder = find.byKey(const Key(playerFullPageCollapse)); + await tester.ensureVisible(finder.first); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(finder.first); + await tester.pump(const Duration(seconds: 2)); + } + + /// Taps the follow/unfollow icon button inside the full player. + Future tapFollowButton() async { + await tester.tap(find.byIcon(Icons.person_add)); + await tester.pump(const Duration(seconds: 2)); + } + + /// Taps the full-screen GestureDetector to toggle play / pause. + Future tapPlayPause() async { + await tapByKeyNow(playerFullPageTogglePlay); + await tester.pump(const Duration(seconds: 1)); + } + + /// Taps the comment icon in the action bar. + Future tapLikeButton() async { + await tapByKeyNow(playerActionBarFavorite); + await tester.pump(const Duration(seconds: 2)); + } + + Future dragWaveformSmoothly({required double pixels}) async { + final waveformFinder = find.byType(CustomPaint).first; + + // Ensure the waveform is actually there before touching it + await tester.ensureVisible(waveformFinder); + + final Offset center = tester.getCenter(waveformFinder); + + // We touch slightly above the center to ensure we hit the bars + // if the 'base' of the custom paint isn't interactive. + final Offset touchPoint = center + const Offset(0, -10); + + final TestGesture gesture = await tester.startGesture(touchPoint); + + // Small increments allow the internal 'onHorizontalDragUpdate' to fire + final int steps = 10; + for (int i = 0; i < steps; i++) { + await gesture.moveBy(Offset(-pixels / steps, 0)); + await tester.pump(const Duration(milliseconds: 30)); + } + + await gesture.up(); + await tester.pump(const Duration(seconds: 1)); + } + + // ── State Checks ───────────────────────────────────────────────────────────── + + /// True when the mini player is rendered and shows a title + artist. + bool isMiniPlayerVisible() => + isVisible(playerMiniPlayerGesture) && + isVisible(playerMiniPlayerTitle) && + isVisible(playerMiniPlayerArtist); + + /// True when the track-info box (title/artist) and "Behind this track" + /// button are both rendered in the full player. + bool isTrackInfoVisible() => + isVisible(playerTrackInfoBoxDetails) && + isVisible(behindTheTrackBehindTrackButton); + + /// True when the collapse and follow buttons are rendered. + bool isCollapseVisible() => + isVisible(playerFullPageCollapse); + + bool isFollowVisible() => + find.byIcon(Icons.person_add).evaluate().isNotEmpty; + + /// True when all five action-bar items are rendered. + bool isActionBarVisible() => + isVisible(playerActionBarFavorite) && + isVisible(playerActionBarCommentIcon) && + isVisible(playerActionBarShareIcon) && + isVisible(playerActionBarPlaylistIcon) && + isVisible(playerActionBarMoreIcon); + + /// True when the full player page is rendered (collapse button present). + bool isFullPlayerOpen() => isVisible(playerFullPageCollapse); +} diff --git a/integration_test/pages/M6_Playlist/Playlist_page.dart b/integration_test/pages/M6_Playlist/Playlist_page.dart new file mode 100644 index 00000000..324bfb2b --- /dev/null +++ b/integration_test/pages/M6_Playlist/Playlist_page.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class PlaylistPage extends BasePage { + PlaylistPage(super.tester); + + // ── Navigation ───────────────────────────────────────────────────────────── + + Future _tapByText(String text) async { + await tester.tap(find.text(text)); + await tester.pumpAndSettle(); + } + + /// Taps the Library tab in the bottom navigation bar. + Future goToLibraryTab() async { + await tester.tap(find.text('Library')); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + /// Taps the Library tab to return to the main Library screen. + Future goBackToLibrary() async { + await tester.tap(find.text('Library')); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + /// Taps the Playlists menu item in the Library screen. + Future goToPlaylistsSection() async { + await tapByKey(libraryPlaylistsItem); + } + + /// Taps the Albums menu item in the Library screen. + Future goToAlbumsSection() async { + await tapByKey(libraryAlbumsItem); + } + + /// Taps the back button on the Playlists list screen. + Future goBackFromPlaylistsList() async { + await tapByKey(libraryPlaylistsBackButton); + } + + /// Taps the back button on the Playlist detail screen. + Future goBackFromPlaylistDetail() async { + await tapByKey(playlistDetailBackButton); + } + + /// Taps the playlist with [name] in the playlists list to open its detail screen. + Future tapPlaylistInList(String playlistName) async { + await tester.tap(find.text(playlistName)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + /// Taps the ⋮ more button next to the playlist with [name] in the playlists list. + Future tapMoreForPlaylist(String playlistName) async { + final tileRow = find.ancestor( + of: find.text(playlistName), + matching: find.byType(Row), + ).first; + await tester.tap( + find.descendant(of: tileRow, matching: find.byIcon(Icons.more_vert)), + ); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + // ── Playlist Filter ──────────────────────────────────────────────────────── + + /// Taps the filter button on the Playlists list screen. + Future tapPlaylistFilterButton() async { + await tapByKey(libraryPlaylistsFilterButton); + } + + Future tapPlaylistFilterOptionFirstAdded() async { + await tapByKey(libraryPlaylistsFilterFirstAdded); + } + + Future tapPlaylistFilterOptionRecentlyUpdated() async { + await tapByKey(libraryPlaylistsFilterRecentlyUpdated); + } + + Future tapPlaylistFilterOptionPlaylistName() async { + await tapByKey(libraryPlaylistsFilterPlaylistName); + } + + Future tapPlaylistFilterOptionLikedPlayists() async { + await tapByKey(libraryPlaylistsFilterLikedPlaylists); + } + + Future tapPlaylistFilterOptionOwnedPlayists() async { + await tapByKey(libraryPlaylistsFilterOwnedPlaylists); + } + + Future tapPlaylistFilterOptionAllPlayists() async { + await tapByKey(libraryPlaylistsFilterAllPlaylists); + } + + // ── Create Playlist ───────────────────────────────────────────────────────── + + /// Taps the "+" create button on the Playlists list screen. + Future tapCreateButton() async { + await tapByKey(libraryPlaylistsCreateButton); + } + + /// Types [name] into the playlist name field in the Create sheet. + Future fillPlaylistName(String name) async { + await enterTextByKey(createPlaylistNameField, name); + } + + /// Taps the Create button in the Create Playlist bottom sheet. + Future tapConfirmCreate() async { + await tapByKey(createPlaylistCreateButton); + } + + /// Taps the Cancel button in the Create Playlist bottom sheet. + Future tapCancelCreate() async { + await tapByKey(createPlaylistCancelButton); + } + + // ── Playlist Detail — Options sheet ──────────────────────────────────────── + + /// Taps the ⋮ more button on the Playlist detail screen. + Future tapMoreOptions() async { + await tapByKey(playlistDetailMoreButton); + } + + /// Taps the "Edit playlist" option in the options sheet. + Future tapOptionEdit() async { + await tapByKey(optionsEdit); + } + + /// Taps the "Make public / Make private" toggle option. + Future tapOptionTogglePrivacy() async { + await tapByKey(optionsTogglePrivacy); + } + + /// Taps the "Delete playlist" option in the options sheet. + /// Scrolls the options sheet until the Delete option is visible. + Future tapOptionDelete() async { + await tester.dragUntilVisible( + find.byKey(Key(optionsDelete)), + find.byKey(Key(playlistOptionsScrollView)), + const Offset(0, -100), + maxIteration: 20, + ); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(Key(optionsDelete))); + await tester.pumpAndSettle(); + } + + /// Taps Cancel in the delete confirmation dialog. + Future cancelDelete() async { + await tapByKey(deletePlaylistCancelButton); + } + + /// Taps Confirm/Delete in the delete confirmation dialog. + Future confirmDelete() async { + await tapByKey(deletePlaylistConfirmButton); + } + + // ── Edit Playlist Sheet ───────────────────────────────────────────────────── + + /// Replaces the playlist name field content with [name] in the Edit sheet. + Future fillEditName(String name) async { + await enterTextByKey(editPlaylistNameField, name); + } + + /// Replaces the description field content with [description] in the Edit sheet. + Future fillEditDescription(String description) async { + await enterTextByKey(editPlaylistDescriptionField, description); + } + + /// Taps the Public switch in the Edit sheet. + Future tapEditPublicSwitch() async { + await tapByKey(editPlaylistPublicSwitch); + } + + /// Taps the Save button in the Edit sheet. + Future tapSaveEdit() async { + await tapByKey(editPlaylistSaveButton); + } + FuturetapSave() async { + await _tapByText('Save'); + } + + /// Taps the Cancel button in the Edit sheet. + Future tapCancelEdit() async { + await tapByKey(editPlaylistCancelButton); + } + + /// Taps the remove button at [index] in the Edit sheet. + /// Keys use track IDs (UUIDs), so we find all remove buttons by key prefix and tap by position. + Future removeTrack(int index) async { + final removeFinder = find.byWidgetPredicate( + (widget) => + widget is IconButton && + widget.key != null && + widget.key.toString().contains('edit_track_remove_'), + ); + await tester.ensureVisible(removeFinder.at(index)); + await tester.pumpAndSettle(); + await tester.tap(removeFinder.at(index)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + // ── Suggestions (Playlist Detail) ────────────────────────────────────────── + + /// Taps the "Add track" button on the Playlist detail screen to open suggestions. + Future tapAddTrackButton() async { + await tapByKey(playlistDetailAddTrackButton); + } + + /// Taps the add button for the suggestion at [index] in the suggestions list. + /// Uses icon-based lookup because suggestion keys include the track ID, not the index. + Future tapSuggestionAddButtonByIndex(int index) async { + final buttons = find.byIcon(Icons.add_box_outlined); + await tester.ensureVisible(buttons.at(index)); + await tester.pumpAndSettle(); + await tester.tap(buttons.at(index)); + await tester.pumpAndSettle(); + } + + /// Scrolls to and taps the add button for the suggestion with [suggestionId]. + Future addSuggestion(String suggestionId) async { + final button = find.byKey(Key('add_suggestion_$suggestionId')); + await tester.ensureVisible(button); + await tester.pumpAndSettle(); + await tester.tap(button); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + /// Taps the Refresh button to reload the suggestion list. + Future tapRefreshSuggestions() async { + await tapByKey(refreshSuggestionsButton); + } + + // ── Playlist Detail — Action buttons ─────────────────────────────────────── + + /// Taps the Like button on the Playlist detail screen. + Future tapLikeButton() async { + await tapByKey(playlistDetailLikeButton); + } + + // ── Convert to Album ─────────────────────────────────────────────────────── + + /// Taps the "Convert to Album" option in the Edit sheet. + Future tapConvertToAlbum() async { + await tapByKey(editConvertToAlbum); + } + + /// Taps Cancel in the convert-to-album confirmation dialog. + Future cancelConvertToAlbum() async { + await tapByKey(convertToAlbumCancelButton); + } + + /// Taps Confirm in the convert-to-album confirmation dialog. + Future confirmConvertToAlbum() async { + await tapByKey(convertToAlbumConfirmButton); + } + + /// Pulls down to trigger a refresh on the current screen. + Future refreshPage() async { + await tester.drag(find.byType(Scrollable).first, const Offset(0, 300)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + } + + // ── State Checks ─────────────────────────────────────────────────────────── + + /// True when the Playlists list screen is active (back button present). + bool isOnPlaylistsListScreen() => isVisible(libraryPlaylistsBackButton); + + /// True when the Playlist detail screen is active (back button present). + bool isOnPlaylistDetailScreen() => isVisible(playlistDetailBackButton); + + /// True when a widget with exactly [name] exists in the tree. + bool isPlaylistNameVisible(String name) => + find.text(name).evaluate().isNotEmpty; + + /// True when a track title [title] is visible in the detail or edit sheet. + bool isTrackVisible(String title) => + find.text(title).evaluate().isNotEmpty; + + /// True when the Edit playlist sheet is open (Save button present). + bool isEditSheetOpen() => isVisible(editPlaylistSaveButton); + + /// True when a widget with exactly [name] exists — used for album name checks. + bool isAlbumNameVisible(String name) => + find.text(name).evaluate().isNotEmpty; + + /// Search for playlists with [query] and returns true if the "No results for [query]" message is shown. + Future typeInPlaylistSearch(String query) async { + await enterTextByKey(libraryPlaylistsSearchField, query); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future pullToRefresh(String pageKey) async { + await tester.drag(find.byKey(Key(pageKey)), const Offset(0, 300)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + } +} diff --git a/integration_test/pages/M7_Feed/feed_page.dart b/integration_test/pages/M7_Feed/feed_page.dart new file mode 100644 index 00000000..91de8f4c --- /dev/null +++ b/integration_test/pages/M7_Feed/feed_page.dart @@ -0,0 +1,18 @@ +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class FeedPage extends BasePage { + FeedPage(WidgetTester tester) : super(tester); + + Future tapFeedButton() async => await tapByKey(feedNavButton); + Future tapDiscoverButton() async => await tapByKey(feedTabDiscover ); + Future tapFollowingButton() async => await tapByKey(feedTabFollowing); + Future taplikeButton() async => await tapByKey(feedCardSideActionsLike); + Future tapCommentButton() async => await tapByKey(feedCardSideActionsComment); + Future closeComments() async => await tapByKey(commentsBackButton); + Future dragTrackCard() async => await tapByKey(feedCardBottomInfoPlayCircle); + bool playerTrack() => isVisible (playerTrackInfoBoxDetails); + bool isFeedPageVisible() => isVisible(feedCardCover); + Future tapDragtButton() async => await tapByKey(playerCollapseButton); +} diff --git a/integration_test/pages/M7_Feed/home_page.dart b/integration_test/pages/M7_Feed/home_page.dart new file mode 100644 index 00000000..dfc34cee --- /dev/null +++ b/integration_test/pages/M7_Feed/home_page.dart @@ -0,0 +1,153 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class HomePage extends BasePage { + HomePage(WidgetTester tester) : super(tester); + + // ── Visibility ── + bool isOnHomePage() => isVisible(homeScaffold); + + bool isHeaderVisible() => isVisible(homeAppBar); + + bool isAllNavTabsVisible() { + return isVisible(mainBottomNavigationBar); + } + + bool isAllHeaderElementsVisible() { + return isVisible(homeAppBar) && + isVisible(homeUploadTrackButton) && + isVisible(homeInboxButton) && + isVisible(homeNotificationsButton); + } + + bool isActivityCardVisible() => isVisible(hotForYouSection); + + bool isActionButtonVisible() => isVisible(hotForYouPlayButton); + + bool isHotForYouMetadataVisible() { + return isVisible(hotForYouTrackTitle) && + isVisible(hotForYouTrackArtist) && + isVisible(hotForYouLikeCount); + } + + bool isMixedForYouVisible() => isVisible(mixedForYouSection); + + bool isDiscoverWithStationsVisible() => isVisible(discoverWithStationsSection); + + bool isMoreOfWhatYouLikeVisible() => isVisible(moreOfWhatYouLikeSection); + + bool isTrendingByGenreVisible() => isVisible(trendingByGenreSection); + + bool isGenreTabBarVisible() => isVisible(genreTabBar); + + bool isReggaeGenreVisible() => find.text('Reggae').evaluate().isNotEmpty; + + bool isOnInboxPage() => isVisible(messagingComposeButton); + bool isOnNotificationsPage() => isVisible(notificationsScreenKey); + + // ── Navigation Taps ── + + Future tapUploadButton() async => await tapByKey(homeUploadTrackButton); + Future tapMessageButton() async => await tapByKey(homeInboxButton); + Future tapNotificationButton() async => await tapByKey(homeNotificationsButton); + // Audio may be playing after TC-HOME-006 — use tapByKeyNow to avoid pumpAndSettle blocking. + Future mixedForYouBackButton() async => await tapByKeyNow(mixedForYouBackbutton); + Future relatedTracksBackButton() async => await tapByKeyNow(relatedTracksBackbutton); + Future tapMixedForYou() async { + await tester.tap(find.descendant( + of: find.byKey(Key(mixedListView)), + matching: find.byType(GestureDetector), + ).first); + await tester.pumpAndSettle(const Duration(seconds: 3)); +} + +Future tapDiscoverWithStations() async { + await tester.tap(find.descendant( + of: find.byKey(Key(discoverListView)), + matching: find.byType(GestureDetector), + ).first); + await tester.pumpAndSettle(const Duration(seconds: 3)); +} + +Future tapMoreOfWhatYouLike() async { + await tester.tap(find.descendant( + of: find.byKey(Key(moreListView)), + matching: find.byType(GestureDetector), + ).first); + await tester.pumpAndSettle(const Duration(seconds: 3)); +} + + /// Taps the like button on a playlist / mix / station detail page. + /// Uses [tapByKeyNow] to avoid pumpAndSettle stalling on audio playback. + Future tapPlaylistDetailLikeButton() async => + await tapByKeyNow(playlistDetailLikeButton); + + /// Taps the like button on the Mix Detail page (Key: 'mix_detail_like_button'). + Future tapMixDetailLikeButton() async => + await tapByKeyNow(mixDetailLikeButton); + + /// Taps the like button on the Related Tracks page + /// (used by both /home/station/:id and /home/related-tracks/:id). + Future tapRelatedTracksLikeButton() async => + await tapByKeyNow(relatedTracksLikeButton); + + /// Tap the inbox button, verify navigation, then return to the home screen. + Future tapInboxAndReturn() async { + await tapByKey(homeInboxButton); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await tester.pageBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + /// Tap the notifications button, verify navigation, then return to the home screen. + Future tapNotificationsAndReturn() async { + await tapByKey(homeNotificationsButton); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await tester.pageBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + // ── Genre Tabs ── + + Future tapGenreTab(String genreName) async { + await tester.tap(find.text(genreName)); + await tester.pump(const Duration(milliseconds: 400)); + } + + // ── Scroll Helpers ── + + /// Scrolls the main feed down until the widget with [key] is visible. + /// Uses key-based lookup so section keys resolve correctly. + Future scrollDownUntilVisible(String key) async { + await tester.dragUntilVisible( + find.byKey(Key(key)), + find.byKey(const Key(homeScrollView)), + const Offset(0, -100), + maxIteration: 50, + ); + await tester.pump(const Duration(milliseconds: 500)); + } + /// Overrides BasePage to use pump instead of pumpAndSettle so horizontal + /// scrolls do not block when audio is playing in the background. + /// [dx] is the horizontal drag distance in logical pixels (negative = scroll left). + @override + Future scrollHorizontallyInSection(String sectionKey, [double dx = -300]) async { + await tester.drag(find.byKey(Key(sectionKey)), Offset(dx, 0)); + await tester.pump(const Duration(milliseconds: 500)); + } + + Future pauseIfPlaying() async { + final pauseFinder = find.byKey(const Key(playerMiniPlayerPlayPauseButton )); + if (pauseFinder.evaluate().isNotEmpty) { + await tester.tap(pauseFinder, warnIfMissed: false); + await tester.pump(const Duration(milliseconds: 500)); + } +} + +Future settle([Duration d = const Duration(seconds: 3)]) async { + await tester.pump(d); + await tester.pump(const Duration(milliseconds: 500)); +} +} diff --git a/integration_test/pages/M8_Search/search_page.dart b/integration_test/pages/M8_Search/search_page.dart new file mode 100644 index 00000000..5febccaa --- /dev/null +++ b/integration_test/pages/M8_Search/search_page.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class SearchPage extends BasePage { + SearchPage(WidgetTester tester) : super(tester); + + // ── Navigation ───────────────────────────────────────────────────────────── + Future tapSearchNavButton() async => await tapByKey(searchNavButton); + //Future tapBack() async => await tapByKey(searchBackButton); + + // ── Search bar ───────────────────────────────────────────────────────────── + Future tapSearchBar() async => await tapByKey(searchBarField); + + Future typeQuery(String query) async { + await enterTextByKey(searchBarField, query); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future clearSearch() async => await tapByKey(searchBarClearButton); + + Future submitSearch() async { + await tester.testTextInput.receiveAction(TextInputAction.search); + await tester.pumpAndSettle(const Duration(seconds: 3)); + } + + // ── Vibes section ────────────────────────────────────────────────────────── + Future scrollVibesDown() async { + await tester.drag(find.byKey(const Key(vibesList)), const Offset(0, -400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future scrollVibesUp() async { + await tester.drag(find.byKey(const Key(vibesList)), const Offset(0, 400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future tapVibeCard(String vibeName) async { + await tester.tap(find.text(vibeName)); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + // ── Vibe detail — inner tab bar (All / Trending / Playlists / Albums) ────── + Future tapAllVibeTab() async => await tapByKey(genreTabAll); + Future tapTrendingVibeTab() async => await tapByKey(genreTabTrending); + Future tapPlaylistsVibeTab() async => await tapByKey(genreTabPlaylists); + Future tapAlbumsVibeTab() async => await tapByKey(genreTabAlbums); + + Future scrollVibeDetailDown() async { + await tester.drag(find.byKey(const Key(genrePage)), const Offset(0, -600)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future scrollVibeDetailUp() async { + await tester.drag(find.byKey(const Key(genrePage)), const Offset(0, 600)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future swipeVibeDetailLeft() async { + await tester.drag(find.byKey(const Key(genrePage)), const Offset(-300, 0)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future swipeVibeDetailRight() async { + await tester.drag(find.byKey(const Key(genrePage)), const Offset(300, 0)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future GenreBackButton() async => await tapByKey(genreBackButton); + + // ── Search results — tab bar (All / Tracks / Profiles / Playlists / Albums) ─ + Future tapAllResultsTab() async => await tapByKey(searchTabAll); + Future tapTracksResultsTab() async => await tapByKey(searchTabTracks); + Future tapProfilesResultsTab() async => await tapByKey(searchTabProfiles); + Future tapPlaylistsResultsTab() async => await tapByKey(searchTabPlaylists); + Future tapAlbumsResultsTab() async => await tapByKey(searchTabAlbums); + + Future swipeResultsLeft() async { + await tester.drag(find.byKey(const Key(searchTabView)), const Offset(-300, 0)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future swipeResultsRight() async { + await tester.drag(find.byKey(const Key(searchTabView)), const Offset(300, 0)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future scrollResultsDown() async { + await tester.drag(find.byKey(const Key(searchTabView)), const Offset(0, -400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future scrollResultsUp() async { + await tester.drag(find.byKey(const Key(searchTabView)), const Offset(0, 400)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + Future pausePlayback() async { + final btn = find.byKey( + const Key('player_mini_progress_button_toggle_play_pause_gesturedetector'), + ); + if (btn.evaluate().isEmpty) return; + await tester.tap(btn, warnIfMissed: false); + await tester.pump(const Duration(seconds: 2)); + } + + Future tapFirstTrackInResults() async { + // _TracksSection in AllTab wraps each result in Padding(key: Key('all_tab_track_N')). + // Tapping the Padding propagates to the TrackCard InkWell inside it. + final firstTrack = find.byKey(const Key('all_tab_track_0')); + if (firstTrack.evaluate().isEmpty) { + throw StateError('No tracks found in search results All tab'); + } + await tester.ensureVisible(firstTrack); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(firstTrack, warnIfMissed: false); + await tester.pump(const Duration(seconds: 3)); + } + + // ── Visibility checks ────────────────────────────────────────────────────── + bool isSearchScreenVisible() => isVisible(searchScreen); + bool isSuggestionListVisible() => isVisible(searchSuggestionsList); + bool isNoResultsVisible() => find.text('No results found').evaluate().isNotEmpty; + + bool isVibePageScreenVisible() => isVisible(genrePage); + bool isResultsScreenVisible() => isVisible(searchTabView); + bool isMiniPlayerVisible() => isVisible(playerMiniPlayerGesture) || isVisible(coreMiniPlayerWidget); +} \ No newline at end of file diff --git a/integration_test/pages/M9_Comments/comments_page.dart b/integration_test/pages/M9_Comments/comments_page.dart new file mode 100644 index 00000000..ab6b5b80 --- /dev/null +++ b/integration_test/pages/M9_Comments/comments_page.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import '../../selectors/selectors.dart'; + +class CommentsPage extends BasePage { + CommentsPage(super.tester); + + // ── Navigation ───────────────────────────────────────────────────────────── + + /// Opens the Comments screen from the full-player action bar. + Future openFromPlayer() async { + await tapByKeyNow(playerActionBarCommentIcon); + await tester.pump(const Duration(seconds: 3)); + } + + /// Closes the Comments screen via the X button in the AppBar. + Future close() async { + await tester.tap(find.byIcon(Icons.close).first); + await tester.pump(const Duration(seconds: 1)); + } + + /// Re-opens the Comments screen after navigating away (e.g. "View profile"). + /// Taps the mini player → full player opens → taps the comment icon. + Future returnToCommentsScreen() async { + if (isCommentsScreenVisible()) return; + await tester.tap(find.byKey(const Key(playerMiniPlayerGesture))); + await tester.pump(const Duration(seconds: 3)); + await tapByKeyNow(playerActionBarCommentIcon); + await tester.pump(const Duration(seconds: 3)); + } + + // ── Floating Comment Bar (Full Player screen) ────────────────────────────── + + /// Taps one of the three preset emoji buttons (🔥 / 👏 / 🥺) in the + /// floating bar, waits for the send button to appear, then posts it. + Future sendEmojiFromFloatingBar(String emoji) async { + await tester.tap(find.text(emoji)); + await tester.pump(const Duration(milliseconds: 300)); + // After appending the emoji the send icon replaces the emoji row + await tester.tap(find.byIcon(Icons.send).first); + await tester.pump(const Duration(seconds: 1)); + } + + /// Types [text] into the floating comment bar's text field and posts it. + Future sendCommentFromFloatingBar(String text) async { + await tester.enterText(find.byType(TextField).first, text); + await tester.pump(const Duration(milliseconds: 200)); + await tester.tap(find.byIcon(Icons.send).first); + await tester.pump(const Duration(seconds: 1)); + } + + // ── Comments Screen ───────────────────────────────────────────────────────── + + /// Types [text] into the "Add a comment…" input and posts it. + Future sendComment(String text) async { + await tester.enterText(find.byType(TextField).first, text); + await tester.pump(const Duration(milliseconds: 200)); + await tester.tap(find.byIcon(Icons.send).first); + await tester.pump(const Duration(seconds: 2)); + } + + /// Taps the like (heart) button on the first comment in the list. + Future likeFirstComment() async { + await tester.tap(find.byIcon(Icons.favorite_border).first); + await tester.pump(const Duration(seconds: 1)); + } + + /// Taps the "Reply" gesture detector on the first visible comment, + /// types [replyText], and posts it. + Future replyToFirstComment(String replyText) async { + final replyFinder = find.byWidgetPredicate( + (w) => w.key?.toString().contains(commentCardReplyGesturePrefix) == true, + ); + await tester.tap(replyFinder.first); + await tester.pump(const Duration(milliseconds: 500)); + await tester.enterText(find.byType(TextField).first, replyText); + await tester.pump(const Duration(milliseconds: 200)); + await tester.tap(find.byIcon(Icons.send).first); + await tester.pump(const Duration(seconds: 2)); + } + + /// Taps "Show N replies" on the first comment that exposes a reply toggle. + Future tapShowReplies() async { + final button = find.textContaining('replies').first; + await tester.ensureVisible(button); + await tester.tap(button); + await tester.pump(const Duration(seconds: 2)); + } + + /// Taps "Show less" to collapse the currently expanded replies. + Future tapShowLess() async { + await tester.tap(find.text('Show less')); + await tester.pump(const Duration(seconds: 1)); + } + + /// Taps the track-timestamp chip (blue text matching mm:ss) on the first + /// comment that has one. The player is expected to seek to that position. + Future tapFirstTrackTimestamp() async { + final timestampFinder = find.byWidgetPredicate( + (widget) => + widget is Text && + widget.style?.color == Colors.blueAccent && + RegExp(r'^\d+:\d+$').hasMatch(widget.data ?? ''), + ); + if (timestampFinder.evaluate().isNotEmpty) { + await tester.tap(timestampFinder.first); + await tester.pump(const Duration(seconds: 1)); + } + } + + /// Taps the ⋮ (more_vert) icon on the last visible comment (which is + /// typically the innermost / most-recently-added reply when replies are + /// expanded). + Future tapMoreOnLastVisible() async { + final moreFinder = find.byWidgetPredicate( + (w) => w.key?.toString().contains(commentCardMoreInkwellPrefix) == true, + ); + await tester.tap(moreFinder.last); + await tester.pump(const Duration(seconds: 3)); + } + + /// Taps the ⋮ (more_vert) icon on the first comment in the list. + /// Scrolls to the top first so the comment is never off-screen after replies expand. + Future tapMoreOnFirstComment() async { + final moreFinder = find.byWidgetPredicate( + (w) => w.key?.toString().contains(commentCardMoreInkwellPrefix) == true, + ); + await tester.ensureVisible(moreFinder.first); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(moreFinder.first); + await tester.pump(const Duration(seconds: 3)); + } + + /// Taps an option inside the comment action bottom sheet by its label text. + Future tapBottomSheetOption(String label) async { + await tester.tap(find.text(label)); + await tester.pump(const Duration(seconds: 2)); + } + + /// Taps the "Play from X:XX" option in the comment action bottom sheet. + Future tapPlayFromOption() async { + await tapByKeyNow(commentActionPlayFromInkwell); + await tester.pump(const Duration(seconds: 2)); + } + + /// Taps the "View profile" option in the comment action bottom sheet. + Future tapViewProfileOption() async { + await tapByKeyNow(commentActionViewProfileInkwell); + await tester.pump(const Duration(seconds: 3)); + } + + /// Taps the "Copy" option in the comment action bottom sheet. + Future tapCopyOption() async { + await tapByKeyNow(commentActionCopyInkwell); + await tester.pump(const Duration(seconds: 1)); + } + + /// Taps the "Delete comment" option in the comment action bottom sheet. + Future tapDeleteCommentOption() async { + await tapByKeyNow(commentActionDeleteInkwell); + await tester.pump(const Duration(seconds: 3)); + } + + /// Dismisses a visible bottom sheet by tapping outside its bounds. + Future dismissBottomSheet() async { + await tester.tapAt(const Offset(200, 50)); + await tester.pump(const Duration(seconds: 1)); + } + + /// Taps the sort icon (tune) to open the sort options bottom sheet. + Future tapSortIcon() async { + await tester.tap(find.byIcon(Icons.tune)); + await tester.pump(const Duration(seconds: 1)); + } + + /// Selects a sort option. [option] must be one of: "Newest", "Oldest", + /// "Track Time". + Future selectSortOption(String option) async { + await tester.tap(find.text(option)); + await tester.pump(const Duration(seconds: 2)); + } + + /// Scrolls the comments list down then back up. + Future scrollCommentsList() async { + await tester.drag( + find.byType(CustomScrollView).first, + const Offset(0, -300), + ); + await tester.pump(const Duration(seconds: 1)); + await tester.drag( + find.byType(CustomScrollView).first, + const Offset(0, 300), + ); + await tester.pump(const Duration(seconds: 1)); + } + + // ── State Checks ─────────────────────────────────────────────────────────── + + /// True when the Comments screen AppBar title is in the tree. + bool isCommentsScreenVisible() => + find.textContaining('Comments').evaluate().isNotEmpty; + + /// True when a widget with exactly [text] is in the tree. + bool isCommentVisible(String text) => + find.text(text).evaluate().isNotEmpty; + + /// True when at least one "Show N replies" toggle is visible. + bool hasShowRepliesButton() => + find.textContaining('replies').evaluate().isNotEmpty; + + /// True when "Show less" is visible (replies are currently expanded). + bool isShowLessVisible() => + find.text('Show less').evaluate().isNotEmpty; + + /// True when the filled heart icon is visible (a comment has been liked). + bool isLikedHeartVisible() => + find.byIcon(Icons.favorite).evaluate().isNotEmpty; + + /// True when [label] text is currently shown in a bottom sheet row. + bool isBottomSheetOptionVisible(String label) => + find.text(label).evaluate().isNotEmpty; + + /// True when the "Play from X:XX" inkwell is visible in the bottom sheet. + bool isPlayFromOptionVisible() => + find.byKey(const Key(commentActionPlayFromInkwell)).evaluate().isNotEmpty; + + /// True when the "Delete comment" inkwell is visible in the bottom sheet. + bool isDeleteCommentOptionVisible() => + find.byKey(const Key(commentActionDeleteInkwell)).evaluate().isNotEmpty; + + /// True when the profile screen is visible after tapping "View profile". + bool isProfilePageVisible() => + find.byKey(const Key(publicProfileBackButton)).evaluate().isNotEmpty; +} diff --git a/integration_test/pages/User_one_main_flow/one_main_flow_page.dart b/integration_test/pages/User_one_main_flow/one_main_flow_page.dart new file mode 100644 index 00000000..1ac42e5f --- /dev/null +++ b/integration_test/pages/User_one_main_flow/one_main_flow_page.dart @@ -0,0 +1,93 @@ +import 'package:flutter_test/flutter_test.dart'; +import '../base_page.dart'; +import 'package:flutter/material.dart'; +import '../../selectors/selectors.dart'; + +class OneMainFlowPage extends BasePage { + OneMainFlowPage(super.tester); + Future _tapByText(String text) async { + await tester.tap(find.text(text)); + await tester.pumpAndSettle(); + } + // -- Comment Scenario -- + Future tapCloseCommentsSection() async => tapByKey(commentsBackButton); + + // -- Messaging Scenario -- + Future tapFriendChat(String name) async { + await tester.tap(find.byKey(Key(messagingInboxItemTile(name)))); + await tester.pumpAndSettle(); + } + + Future writeMessage(String query) async { + await enterTextByKey(messagingMessageInputField, query); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future sendMessage() async => await tapByKey(chatScreenSendButton); + + Future tapAddIcon() async => await tapByKey(chatScreenAddEmbedButton); + + Future tapLikeTrack() async => await _tapByText('أنا وأخي'); + + Future tapDoneButton() async => await tapByKey(doneButton); + + // -- Feed Scenario + Future swipeToNextTrack() async { + await tester.drag( + find.byKey(const Key(feedListDiscoverPageview)), + const Offset(0, -600), // swipe up → next track + ); + await tester.pumpAndSettle(); + } + + Future swipeToPreviousTrack() async { + await tester.drag( + find.byKey(const Key(feedListDiscoverPageview)), + const Offset(0, 600), // swipe down → previous track + ); + await tester.pumpAndSettle(); + } + + Future backToSearchResults() async => await tapByKey(playlistDetailBackButton); + + Future tapTrack(int index) async{ + await tester.tap(find.byKey(Key(trackTile(index)))); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + + Future tapTrackMoreOptions(int index) async { + final moreButtons = find.byWidgetPredicate( + (widget) => widget.key is ValueKey && + (widget.key as ValueKey).value.startsWith('track_card_') && + (widget.key as ValueKey).value.endsWith('_more_inkwell'), + ); + await tester.tap(moreButtons.at(index)); + await tester.pumpAndSettle(); + } + Future tapAddToPlaylist() async => await tapByKey(trackOptionAddToPlaylist); + Future tapNewPlaylist() async => await _tapByText('New playlist'); + + Future writePlaylistName(String name) async { + await tester.enterText( + find.byKey(const Key(addToPlaylistNewNameField)), + name, + ); + await tester.pumpAndSettle(); + } + Future tapCreate() async => await tapByKey(confirmAddToPlaylist); + Future tapDone() async => await tapByKey(doneAddToPlaylistButton); + + Future playPlaylist() async => await tapByKey(playlistDetailPlayButton); + + Future tapPlaylist(int index) async{ + await tester.tap(find.byKey(Key(playlisTile(index)))); + await tester.pumpAndSettle(const Duration(seconds: 2)); + } + Future backToLibraryPlaylistButton() async => await tapByKey(libraryPlaylistsBackButton); + + // -- Profile scenario -- + Future goBackFromProfile() async => await tapByKey(publicProfileBackButton); + + // -- Debug the Failure -- + Future pauseTheTrack() async => await tapByKey(playerMiniPlayerPlayPauseButton); +} \ No newline at end of file diff --git a/integration_test/pages/auth/register_page.dart b/integration_test/pages/auth/register_page.dart deleted file mode 100644 index c6f91f9a..00000000 --- a/integration_test/pages/auth/register_page.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/material.dart'; -import '../base_page.dart'; -import '../../selectors/selectors.dart'; - -class RegisterPage extends BasePage { - - RegisterPage(super.tester); - - // ─── Email Field ─── - Future enterEmail(String email) async { - await enterTextByKey(authEmailTextField, email); - } - - // ─── Continue Button ─── - Future tapContinue() async { - await tapByKey(authContinueButton); - } - - // ─── Password Field ─── - Future enterPassword(String password) async { - await enterTextByKey(authPasswordTextField, password); - } - - Future tapPasswordContinue() async { - await tapByKey(authContinueElevatedButton); - } - - // ─── Username Field ─── - Future enterUsername(String username) async { - await enterTextByKey(authRegisterUsernameTextField, username); - } - - // ─── Date Dropdowns ─── - Future selectMonth(String month) async { - await tapByKey(authRegisterMonthDropdown); - await tester.pumpAndSettle(); - await tester.tap(find.text(month)); - await tester.pumpAndSettle(); - } - - Future openDayDropdown() async { - await tapByKey(authRegisterDayDropdown); - await tester.pumpAndSettle(); - } - - Future scrollToDay(String day) async { - await tester.dragUntilVisible( - find.text(day), - find.byKey(const Key(authRegisterDayDropdown)), - const Offset(0, -100), - ); - await tester.pumpAndSettle(); - } - - Future selectDay(String day) async { - await openDayDropdown(); - await scrollToDay(day); - await tester.tap(find.text(day)); - await tester.pumpAndSettle(); - } - - Future openYearDropdown() async { - await tapByKey(authRegisterYearDropdown); - await tester.pumpAndSettle(); - } - - Future scrollToYear(String year) async { - await tester.dragUntilVisible( - find.text(year), - find.byKey(const Key(authRegisterYearDropdown)), - const Offset(0, -100), - ); - await tester.pumpAndSettle(); - } - - Future selectYear(String year) async { - await openYearDropdown(); - await scrollToYear(year); - await tester.tap(find.text(year)); - await tester.pumpAndSettle(); - } - - // ─── Gender Dropdown ─── - Future selectGender(String gender) async { - await tapByKey(authRegisterGenderDropdown); - await tester.pumpAndSettle(); - await tester.tap(find.text(gender)); - await tester.pumpAndSettle(); - } - - // ─── Final Continue Button ─── - Future tapFinalContinue() async { - await tapByKey(authRegisterFinalContinueButton); - } - - // ─── Complete Registration Flow ─── - Future register( - String email, - String password, - String username, - String month, - String day, - String year, - String gender, - ) async { - await enterEmail(email); - await tapContinue(); - await tester.pumpAndSettle(); - await enterPassword(password); - await tapPasswordContinue(); - await tester.pumpAndSettle(); - await enterUsername(username); - await selectMonth(month); - await selectDay(day); - await selectYear(year); - await selectGender(gender); - await tapFinalContinue(); - } - - // ─── Visibility Check Methods ─── - bool isOnLoginPage() { - return isVisible(authTitleText); - } - - bool isOnHomePage() { - return isVisible(homeScaffold); - } - - bool isEmailEmptyErrorVisible() { - return find.text('Please enter your email').evaluate().isNotEmpty; - } - - bool isInvalidEmailErrorVisible() { - return find.text('Please enter a valid email').evaluate().isNotEmpty; - } - - bool isPasswordEmptyErrorVisible() { - return find.text('Please enter your password').evaluate().isNotEmpty; - } - - bool isPasswordTooShortErrorVisible() { - return find.text('Password must be at least 8 characters').evaluate().isNotEmpty; - } - - bool isPasswordNoUppercaseErrorVisible() { - return find.text('Password must contain an uppercase letter').evaluate().isNotEmpty; - } - - bool isPasswordNoLowercaseErrorVisible() { - return find.text('Password must contain a lowercase letter').evaluate().isNotEmpty; - } - - bool isPasswordNoNumberErrorVisible() { - return find.text('Password must contain a number').evaluate().isNotEmpty; - } - - bool isUsernameEmptyErrorVisible() { - return find.text('Please enter a username').evaluate().isNotEmpty; - } - - bool isDateOfBirthEmptyErrorVisible() { - return find.text('Please enter your date of birth').evaluate().isNotEmpty; - } - - bool isGenderEmptyErrorVisible() { - return find.text('Please select a gender').evaluate().isNotEmpty; - } - - bool isAgeRestrictionErrorVisible() { - return find.text('You must be at least 13 years old').evaluate().isNotEmpty; - } - - bool isAlreadyExistsErrorVisible() { - return find.text('This email already exists').evaluate().isNotEmpty || - find.text('Email already exists').evaluate().isNotEmpty; - } - - bool isSpecificPasswordErrorVisible(String errorKey) { - // Map error keys to error messages - const errorMessages = { - 'ERROR_PASSWORD_TOO_SHORT': 'Password must be at least 8 characters', - 'ERROR_PASSWORD_NO_UPPERCASE': 'Password must contain an uppercase letter', - 'ERROR_PASSWORD_NO_LOWERCASE': 'Password must contain a lowercase letter', - 'ERROR_PASSWORD_NO_NUMBER': 'Password must contain a number', - }; - - final message = errorMessages[errorKey]; - if (message == null) return false; - - return find.text(message).evaluate().isNotEmpty; - } -} diff --git a/integration_test/pages/base_page.dart b/integration_test/pages/base_page.dart index 79942eef..f35f0cf4 100644 --- a/integration_test/pages/base_page.dart +++ b/integration_test/pages/base_page.dart @@ -9,15 +9,46 @@ class BasePage { BasePage(this.tester); Future tapByKey(String key) async { + await tester.ensureVisible(find.byKey(Key(key))); + await tester.pumpAndSettle(); await tester.tap(find.byKey(Key(key))); await tester.pumpAndSettle(); } + /// Same as [tapByKey] but uses fixed [pump] calls instead of [pumpAndSettle]. + /// Use this whenever audio is playing — the continuous frame updates from the + /// player prevent [pumpAndSettle] from ever settling. + Future tapByKeyNow(String key) async { + await tester.ensureVisible(find.byKey(Key(key))); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(find.byKey(Key(key))); + await tester.pump(const Duration(milliseconds: 300)); + } + Future enterTextByKey(String key, String text) async { await tester.enterText(find.byKey(Key(key)), text); await tester.pumpAndSettle(); } + Future scrollUntilVisible({ + required String itemText, + required String scrollableKey, + }) async { + await tester.dragUntilVisible( + find.text(itemText), + find.byKey(Key(scrollableKey)), + const Offset(0, -100), + maxIteration: 50, + ); + await tester.pumpAndSettle(); + } + + /// Drags the widget identified by [sectionKey] horizontally (left by 300px). + Future scrollHorizontallyInSection(String sectionKey) async { + await tester.drag(find.byKey(Key(sectionKey)), const Offset(-300, 0)); + await tester.pumpAndSettle(); + } + bool isVisible(String key) { return find.byKey(Key(key)).evaluate().isNotEmpty; } @@ -25,4 +56,9 @@ class BasePage { Future waitForSettle() async { await tester.pumpAndSettle(); } + + Future pullToRefresh(String pageKey) async { + await tester.drag(find.byKey(Key(pageKey)), const Offset(0, 300)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + } } \ No newline at end of file diff --git a/integration_test/selectors/selectors.dart b/integration_test/selectors/selectors.dart index caf52e4d..b52567b1 100644 --- a/integration_test/selectors/selectors.dart +++ b/integration_test/selectors/selectors.dart @@ -1,6 +1,3 @@ -// ============================================================ -// Centralized Selectors — ALL widget keys in one place -// ============================================================ // --- Core / Navigation --- const String mainBottomNavigationBar = 'main_bottom_navigation_bar'; @@ -10,13 +7,11 @@ const String mainFullPlayerPage = 'main_full_player_page'; const String coreBottomSheetCloseButton = 'core_bottom_sheet_close_icon_button'; const String coreMiniPlayerWidget = 'core_mini_player_widget'; -// --- Authentication --- +// ----- Authentication ----- +// --- Login/Sign In --- const String authTitleText = 'auth_title_text'; const String authEmailTextField = 'auth_email_text_field_container'; const String authContinueButton = 'auth_continue_button'; -const String authSocialGoogleButton = 'auth_social_google_button'; -const String authSocialFacebookButton = 'auth_social_facebook_button'; -const String authSocialAppleButton = 'auth_social_apple_button'; const String authNeedHelpButton = 'auth_need_help_button'; const String onboardingCreateAccountButton = 'onboarding_create_account_button'; const String onboardingLoginButton = 'onboarding_login_button'; @@ -33,6 +28,11 @@ const String authVerifyEmailTitle = 'authentication_verify_email_title const String authVerifyEmailDescription = 'authentication_verify_email_description_text'; const String authResendEmailButton = 'authentication_resend_email_elevated_button'; const String authDismissButton = 'authentication_dismiss_text_button'; +const String authTermsAndPrivacyText = 'auth_terms_and_privacy_text'; +const String authEmailDividerText = 'auth_email_divider_text'; +const String authHelpGestureDetector = 'authentication_help_gesture_detector'; +const String authLoginPasswordBackIconButton = 'authentication_login_password_back_icon_button'; +const String authForgotPasswordBackButton = 'authentication_forgot_password_back_icon_button'; // --- Registration / Sign Up --- const String authRegisterUsernameTextField = 'authentication_create_account_profile_display_name_auth_text_field'; @@ -43,15 +43,36 @@ const String authRegisterGenderDropdown = 'authentication_create_account_pro const String authRegisterFinalContinueButton = 'authentication_create_account_profile_continue_elevated_button'; const String authRegisterBackButton = 'authentication_create_account_profile_back_icon_button'; const String authPasswordBackButton = 'authentication_back_icon_button'; +const String registerBackButton = 'register_back_button'; +const String registerEmailField = 'register_email_field'; +const String registerPasswordField = 'register_password_field'; +const String registerConfirmPasswordField = 'register_confirm_password_field'; +const String registerGoogleEmailField = 'register_google_email_field'; +const String registerDisplayNameField = 'register_display_name_field'; +const String registerMonthDropdown = 'register_month_dropdown'; +const String registerDayDropdown = 'register_day_dropdown'; +const String registerYearDropdown = 'register_year_dropdown'; +const String registerGenderDropdown = 'register_gender_dropdown'; +const String registerSubmitButton = 'register_submit_button'; // --- Home / Feed --- const String homeScaffold = 'home_scaffold'; +const String feedNavButton = 'feed_nav_button'; +const String searchNavButton = 'search_nav_button'; +const String libraryNavButton = 'library_nav_button'; +const String upgradeNavButton = 'upgrade_nav_button'; const String homeAppBar = 'home_app_bar'; const String homeUploadTrackButton = 'home_upload_track_icon_button'; const String homeInboxButton = 'home_inbox_icon_button'; const String homeNotificationsButton = 'home_notifications_icon_button'; const String homeScrollView = 'home_scroll_view'; +// --- Feed Tab Bar --- +const String feedTabBarSizedBox = 'feed_tab_bar_sizedbox'; +const String feedTabBar = 'feed_tab_bar'; +const String feedTabDiscover = 'feed_tab_discover'; +const String feedTabFollowing = 'feed_tab_following'; + // --- Feed Widgets --- const String hotForYouSection = 'hot_for_you_section'; const String hotForYouErrorText = 'hot_for_you_error_text'; @@ -59,28 +80,55 @@ const String hotForYouPlayButton = 'hot_for_you_play_icon_button'; const String hotForYouTrackTitle = 'hot_for_you_track_title_text'; const String hotForYouTrackArtist = 'hot_for_you_track_artist_text'; const String hotForYouLikeCount = 'hot_for_you_like_count_text'; +const String hotForYouLoading = 'hot_for_you_loading'; const String mixedForYouSection = 'mixed_for_you_section'; const String mixedForYouErrorText = 'mixed_for_you_error_text'; const String mixedListView = 'mixed_list_view'; +const String mixedForYouBackbutton = 'mix_detail_back_button'; +const String relatedTracksBackbutton = 'related_tracks_back_button'; const String moreOfWhatYouLikeSection = 'more_of_what_you_like_section'; const String moreOfWhatYouLikeErrorText = 'more_of_what_you_like_error_text'; const String moreListView = 'more_list_view'; const String trendingByGenreSection = 'trending_by_genre_section'; const String genreTabBar = 'genre_tab_bar'; const String genreTabView = 'genre_tab_view'; +const String discoverWithStationsSection = 'discover_stations_section'; +const String discoverListView = 'discover_stations_list_view'; const String discoverWithStationsErrorText = 'discover_with_stations_error_text'; +const String frostedGlassRepaintBoundary = 'frosted_glass_repaint_boundary'; + +// --- Feed Card --- +const String feedCardStack = 'feed_card_stack'; +const String feedCardCover = 'feed_card_cover'; +const String feedCardSideActionsPositioned = 'feed_card_side_actions_positioned'; +const String feedCardSideActions = 'feed_card_side_actions'; +const String feedCardSideActionsColumn = 'feed_card_side_actions_column'; +const String feedCardSideActionsLike = 'feed_card_side_actions_like'; +const String feedCardSideActionsComment = 'feed_card_side_actions_comment'; +const String feedCardBottomPositioned = 'feed_card_bottom_positioned'; +const String feedCardBottomInfo = 'feed_card_bottom_info'; +const String feedCardBottomInfoTitle = 'feed_card_bottom_info_title'; +const String feedCardBottomInfoUsername = 'feed_card_bottom_info_username'; +const String feedCardBottomInfoPlayCircle = 'feed_card_bottom_info_play_circle'; +const String feedCardBottomInfoFollowButton = 'feed_card_bottom_info_follow_button'; +const String feedCardPlayCircleIcon = 'feed_card_play_circle_icon'; +const String feedListDiscoverPageview = 'feed_list_discover_pageview'; +const String feedListFollowingPageview = 'feed_list_following_pageview'; +const String feedListDiscoverError = 'feed_list_discover_error'; +const String feedListFollowingError = 'feed_list_following_error'; // --- Player --- -const String playerFullPageTogglePlay = 'player_full_page_toggle_play_pause_gesturedetector'; -const String playerFullPageCollapse = 'player_full_page_collapse_iconbutton'; -const String playerFullPageAddPerson = 'player_full_page_add_person_iconbutton'; +const String playerFullPageTogglePlay = 'player_waveform_gesture_detector'; +const String playerFullPageCollapse = 'player_collapse_button'; +const String playerFullPageAddPerson = 'player_follow_button'; const String playerFullPageNoTrackText = 'player_full_page_no_track_text'; const String playerMiniPlayerGesture = 'player_mini_player_gesture_detector'; const String playerMiniPlayerTitle = 'player_mini_player_title_text'; const String playerMiniPlayerArtist = 'player_mini_player_artist_text'; const String playerMiniPlayerFollowButton = 'player_mini_player_follow_icon_button'; const String playerMiniPlayerLikeButton = 'player_mini_player_like_icon_button'; -const String playerProgressBarSlider = 'player_progress_bar_slider'; +const String playerMiniPlayerPlayPauseButton = 'player_mini_progress_button_toggle_play_pause_gesturedetector'; +const String playerCollapseButton = 'player_collapse_button'; const String playerProgressBarDuration = 'player_progress_bar_duration_text'; const String playerActionBarFavorite = 'player_action_bar_favorite_icon'; const String playerActionBarLikeCount = 'player_action_bar_like_count_text'; @@ -91,6 +139,9 @@ const String playerActionBarPlaylistIcon = 'player_action_bar_playlist_icon'; const String playerActionBarMoreIcon = 'player_action_bar_more_icon'; const String playerTrackInfoBoxDetails = 'player_track_info_box_details_gesturedetector'; const String playerMiniProgressButtonIcon = 'player_mini_progress_button_icon'; +const String playerMiniProgressButtonIndicator = 'player_mini_progress_button_indicator'; +const String playerActionBarLikeCountErrorText = 'player_action_bar_like_count_error_text'; +const String playerActionBarCommentCountErrorText = 'player_action_bar_comment_count_error_text'; // --- Profile --- const String profileAvatarGesture = 'profile_avatar_gesture'; @@ -102,34 +153,173 @@ const String profileInfoBioText = 'profile_info_bio_text'; const String profileInfoFollowersStat = 'profile_info_followers_stat'; const String profileInfoFollowingStat = 'profile_info_following_stat'; const String profileInfoTracksStat = 'profile_info_tracks_stat'; -const String profileShareMessageButton = 'profile_share_message_button'; -const String profileShareCopyLinkButton = 'profile_share_copy_link_button'; -const String profileShareWhatsappButton = 'profile_share_whatsapp_button'; -const String profileShareQrCodeButton = 'profile_share_qr_code_button'; +const String profileShareViewInfoGesture = 'profile_share_view_info_gesture'; const String profileUnsavedChangesDiscard = 'profile_unsaved_changes_discard_button'; const String profileUnsavedChangesContinue = 'profile_unsaved_changes_continue_button'; +const String profileStatsFollowersButton = 'profile_stats_followers_button'; +const String profileStatsFollowingButton = 'profile_stats_following_button'; +const String profileUnsavedChangesTitleText = 'profile_unsaved_changes_title_text'; +const String profileUnsavedChangesDescText = 'profile_unsaved_changes_description_text'; +const String profileConnectionsBackButton = 'profile_connections_back_button'; // --- Edit Profile --- const String editProfileBackButton = 'edit_profile_back_button'; const String editProfileSaveButton = 'edit_profile_save_button'; const String editProfileNameTextField = 'edit_profile_name_textfield'; const String editProfileCityTextField = 'edit_profile_city_textfield'; +const String editProfileUsernameTextField = 'edit_profile_username_textfield'; +const String editProfileFirstNameTextField = 'edit_profile_first_name_textfield'; +const String editProfileLastNameTextField = 'edit_profile_last_name_textfield'; +const String profileConnectionsListView = 'profile_connections_list_view'; const String editProfileBioTextField = 'edit_profile_bio_textfield'; const String editProfileBioDoneButton = 'edit_profile_bio_done_button'; const String editProfilePickCoverGesture = 'edit_profile_pick_cover_gesture'; +const String editProfileCountryGesture = 'edit_profile_country_gesture'; +const String editProfileCountryListView = 'edit_profile_country_list_view'; +const String editProfileBioGesture = 'edit_profile_bio_gesture'; +const String editProfileScrollView = 'edit_profile_scroll_view'; // --- Public Profile --- +const String publicProfilePage = 'public_profile_avatar_fullscreen_gesture_detector'; const String publicProfileBackButton = 'public_profile_back_button'; const String publicProfileEditGesture = 'public_profile_edit_gesture'; const String publicProfileFollowGesture = 'public_profile_follow_gesture'; const String publicProfilePlayButton = 'public_profile_play_button'; const String publicProfileShuffleGesture = 'public_profile_shuffle_gesture'; const String publicProfileRetryButton = 'public_profile_retry_button'; +const String publicProfileCastButton = 'public_profile_cast_button'; +const String publicProfileMoreButton = 'public_profile_more_button'; +const String publicProfileCoverImage = 'public_profile_cover_image'; +const String publicProfileCompleteDataBanner = 'public_profile_complete_data_banner'; +const String publicProfileCompleteDataButton = 'public_profile_complete_data_button'; +const String publicProfileCoverEmptyContainer = 'public_profile_cover_empty_container'; +const String publicProfileCoverErrorContainer = 'public_profile_cover_error_container'; + +// --- Reposted / Uploaded Tracks --- +const String repostedTracksBackButton = 'reposted_tracks_back_button'; +const String repostedTracksCastButton = 'reposted_tracks_cast_button'; +const String uploadedTracksBackButton = 'uploaded_tracks_back_button'; +const String uploadedTracksCastButton = 'uploaded_tracks_cast_button'; // --- Library --- const String libraryGetProButton = 'library_get_pro_text_button'; const String librarySettingsButton = 'library_settings_icon_button'; const String libraryProfileAvatarGesture = 'library_profile_avatar_gesture_detector'; +const String libraryCastIconButton = 'library_cast_icon_button'; +const String libraryMainScrollView = 'library_main_scroll_view'; +const String libraryImportBannerCard = 'library_import_banner_card'; +const String libraryImportBannerCloseGesture = 'library_import_banner_close_gesture'; +const String libraryImportNowButton = 'library_import_now_button'; +const String libraryRecentlyPlayedSeeAllButton = 'library_recently_played_see_all_button'; +const String libraryRecentlyPlayedListView = 'library_recently_played_list_view'; + +// --- Library Menu Items --- +const String libraryLikesItem = 'library_likes_item'; +const String libraryPlaylistsItem = 'library_playlists_item'; +const String libraryAlbumsItem = 'library_albums_item'; +const String libraryFollowingItem = 'library_following_item'; +const String libraryStationsItem = 'library_stations_item'; +const String libraryInsightsItem = 'library_insights_item'; +const String libraryUploadsItem = 'library_uploads_item'; + +// ── Library — Likes ─────────────────────────────────────────────────────────── +const String libraryLikesBackButton = 'library_likes_back_button'; +const String libraryLikesSearchBar = 'library_likes_search_bar'; +const String libraryLikesFilterButton = 'library_likes_filter_button'; +const String libraryLikesFilterTitleAZ = 'library_likes_sort_title'; +const String libraryLikesFilterMoreRecent = 'library_likes_sort_recent'; +const String libraryLikesFilterArtistAZ = 'library_likes_sort_artist'; +const String libraryLikesCastButton = 'library_likes_cast_button'; +const String libraryLikesScrollView = 'library_likes_scroll_view'; +const String libraryLikesShuffleButton = 'library_likes_shuffle_button'; +const String libraryLikesPlayButton = 'library_likes_play_button'; + +// ── Library — Following ─────────────────────────────────────────────────────── +const String followingListView = 'following_list_view'; +const String followingRetryButton = 'following_retry_button'; +const String libraryFollowingBackButton = 'following_back_button'; +const String libraryFollowingUnfollowDialog = 'unfollow_dialog'; +const String libraryFollowingDialogCancel = 'unfollow_dialog_cancel_button'; +const String libraryFollowingDialogUnfollow = 'unfollow_dialog_unfollow_button'; + +// ── Library — Albums ────────────────────────────────────────────────────────── +const String libraryAlbumsBackButton = 'library_albums_back_button'; +const String libraryAlbumsSearchField = 'library_albums_search_field'; +const String libraryAlbumsScrollView = 'library_albums_scroll_view'; +const String libraryAlbumDetailScreen = 'playlist_detail_back_button'; +const String libraryAlbumsFilterButton = 'library_albums_filter_button'; +const String libraryAlbumsFilterRecentlyAdded = 'library_albums_filter_recently_added'; +const String libraryAlbumsFilterFirstAdded = 'library_albums_filter_first_added'; +const String libraryAlbumsFilterAlbumName = 'library_albums_filter_album_name'; +// Album detail uses playlist_detail_screen — reuse those keys +const String libraryAlbumsPlayButton = playlistDetailPlayButton; +const String libraryAlbumsShuffleButton = playlistDetailShuffleButton; + +// ── Library — Stations ──────────────────────────────────────────────────────── +const String libraryStationsBackButton = 'library_stations_back_button'; +const String libraryStationsSearchField = 'library_stations_search_field'; +const String libraryStationsScrollView = 'library_stations_scroll_view'; +const String libraryStationDetailScreen = 'related_tracks_back_button'; +const String libraryStationsFilterButton = 'library_stations_filter_button'; +const String libraryStationsFilterRecentlyAdded = 'library_stations_filter_recently_added'; +const String libraryStationsFilterFirstAdded = 'library_stations_filter_first_added'; +const String libraryStationsFilterStationName = 'library_stations_filter_station_name'; +const String libraryStationsPlayButton = 'related_tracks_play_button'; + +// ── Library — History ───────────────────────────────────────────────────────── +const String historyBackButton = 'history_back_button'; +const String libraryHistoryBackButton = historyBackButton; +const String libraryRecentlyPlayedDialogCancel = 'clear_history_dialog_cancel_button'; +const String libraryRecentlyPlayedDialogClear = 'clear_history_dialog_clear_button'; +const String historySearchBar = 'history_search_bar'; +const String historyFilterButton = 'history_filter_button'; +const String historyCastButton = 'history_cast_button'; +const String historyScrollView = 'history_scroll_view'; +const String historyClearIconButton = 'history_clear_icon_button'; +const String historyShuffleIconButton = 'history_shuffle_icon_button'; +const String historyPlayButton = 'history_play_button'; +const String historyPlayAllFab = historyPlayButton; +const String historyListView = historyScrollView; + +// ── Library — Insights ──────────────────────────────────────────────────────── +const String libraryInsightsBackButton = 'insights_back_button'; +const String insightsSummaryCard = 'insights_summary_card'; +const String insightsTotalPlaysStat = 'insights_total_plays_stat'; +const String insightsTotalListenersStat = 'insights_total_listeners_stat'; +const String insightsTotalLikesStat = 'insights_total_likes_stat'; +const String insightsAllPlatformsTab = 'insights_all_platforms_tab'; +const String insightsAllPlatformsTabScroll = 'insights_all_platforms_tab_scroll'; +const String insightsScDataListView = 'insights_sc_data_list_view'; +const String insightsShareIconButton = 'insights_share_icon_button'; +const String insightsCastIconButton = 'insights_cast_icon_button'; +const String insightsScRetryButton = 'insights_sc_retry_button'; +const String insightsScEmptyState = 'insights_sc_empty_state'; +const String insightsScHeadlineText = 'insights_sc_headline_text'; +const String insightsScBodyText = 'insights_sc_body_text'; +const String insightsScUploadButton = 'insights_sc_upload_button'; +const String insightsApHeadlineText = 'insights_ap_headline_text'; +const String insightsApBodyText = 'insights_ap_body_text'; +const String insightsApUpgradeButton = 'insights_ap_upgrade_button'; +const String insightsUpgradeProButton = 'insights_upgrade_pro_button'; + +// ── Library — Playlists ─────────────────────────────────────────────────────── +const String libraryPlaylistsBackButton = 'library_playlists_back_button'; +const String libraryPlaylistsSearchField = 'library_playlists_search_field'; +const String libraryPlaylistsImportButton = 'library_playlists_import_button'; +const String libraryPlaylistsCreateButton = 'library_playlists_create_button'; +const String libraryPlaylistsFilterButton = 'library_playlists_filter_button'; +const String libraryPlaylistsFilterRecentlyAdded = 'library_playlists_filter_recently_added'; +const String libraryPlaylistsFilterFirstAdded = 'library_playlists_filter_first_added'; +const String libraryPlaylistsFilterRecentlyUpdated = 'library_playlists_filter_recently_updated'; +const String libraryPlaylistsFilterPlaylistName = 'library_playlists_filter_playlist_name'; +const String libraryPlaylistsFilterAllPlaylists = 'library_playlists_filter_all_playlists'; +const String libraryPlaylistsFilterLikedPlaylists = 'library_playlists_filter_liked_playlists'; +const String libraryPlaylistsFilterOwnedPlaylists = 'library_playlists_filter_owned_playlists'; +const String libraryPlaylistsList = 'library_playlists_list'; + +// ── Library — Misc ──────────────────────────────────────────────────────────── +const String libraryUpgradeProButton = 'library_upgrade_pro_button'; +const String libraryImportBannerDismissible = 'library_import_banner_dismissible'; // --- Messaging --- const String inboxErrorText = 'inbox_error_text'; @@ -137,14 +327,28 @@ const String inboxComposeButton = 'inbox_compose_button'; const String messagingComposeButton = 'messaging_compose_floating_action_button'; const String messagingEmptyInboxMessage = 'messaging_empty_inbox_message_text'; const String messagingEmptyInboxInstruction = 'messaging_empty_inbox_instruction_text'; -const String messagingMessageInputField = 'messaging_message_input_text_field'; const String messagingSearchTextField = 'messaging_search_text_field'; const String messagingSearchClearButton = 'messaging_search_clear_icon_button'; const String messagingSearchMessageButton = 'messaging_search_message_elevated_button'; +const String messagingSearchMessageText = 'messaging_search_message_text'; +const String messagingEmptySearchUsersText = 'messaging_empty_Search_users_text'; +const String messagingUserAvatarClipOval = 'messaging_user_avatar_clip_oval'; +const String messagingNewMessageSearchBar = 'search_screen_new_message_search_bar'; const String chatScreenScaffold = 'chat_screen_scaffold'; const String chatParticipantNameText = 'chat_participant_name_text'; +const String chatScreenAppBar = 'chat_screen_app_bar'; +const String chatScreenBlockedUserWidget = 'chat_screen_blocked_user_widget'; +const String chatScreenBlockedByWidget = 'chat_screen_blocked_by_widget'; +const String chatScreenSelectedEmbedsPreview = 'chat_screen_selected_embeds_preview'; +const String likesPlaylistsScaffold = 'likes_playlists_screen_scaffold'; +const String likesPlaylistsCloseButton = 'likes_playlists_app_bar_close_button'; +const String likesPlaylistsTabBarView = 'likes_playlists_tab_bar_view'; +const String likesPlaylistsTracksList = 'likes_playlists_tracks_list'; +const String likesPlaylistsPlaylistsAlbumsList = 'likes_playlists_playlists_albums_list'; // --- Track Upload --- +const String trackUploadGenrePicker = 'track_upload_genre_picker_gesture_detector'; +const String trackUploadGenreListView = 'track_upload_genre_list_view'; const String trackUploadGalleryListTile = 'track_upload_gallery_listtile'; const String trackUploadCameraListTile = 'track_upload_camera_listtile'; const String trackUploadCoverImagePicker = 'track_upload_cover_image_picker_gesture_detector'; @@ -153,14 +357,235 @@ const String trackUploadStatusText = 'track_upload_status_text'; const String trackUploadProgressUploading = 'track_upload_progress_uploading_text'; const String trackUploadProgressSuccess = 'track_upload_progress_success_text'; const String trackUploadProgressError = 'track_upload_progress_error_text'; +const String trackUploadProgressOverlay = 'track_upload_progress_overlay_gesture_detector'; + +// --- Uploads Page --- +const String uploadsBackButton = 'uploads_back_button'; +const String uploadsSearchBar = 'uploads_search_bar'; +const String uploadsFilterButton = 'uploads_filter_button'; +const String uploadsCastButton = 'uploads_cast_button'; +const String uploadsScrollView = 'uploads_scroll_view'; +const String uploadsUploadCircleButton = 'uploads_upload_circle_button'; +const String uploadsShuffleButton = 'uploads_shuffle_button'; +const String uploadsPlayButton = 'uploads_play_button'; +const String uploadsDeleteOption = 'uploads_delete_option'; +const String uploadsAmplifyChip = 'uploads_amplify_chip'; +const String uploadsMinutesChip = 'uploads_minutes_chip'; +const String uploadsEmptyHeadlineText = 'uploads_empty_headline_text'; +const String uploadsEmptyBodyText = 'uploads_empty_body_text'; +const String uploadsEmptyUploadButton = 'uploads_empty_upload_button'; // --- Track / Behind the Track --- const String behindTheTrackBackButton = 'behind_the_track_back_icon_button'; +const String behindTheTrackCastButton = 'behind_the_track_cast_icon_button'; +const String behindTheTrackBehindTrackButton = 'player_track_info_box_behind_track_gesturedetector'; const String behindTheTrackPlayPause = 'behind_the_track_play_pause_gesture_detector'; +const String behindTheTrackShowMore = 'behind_the_track_show_more_description_gesture_detector'; +const String behindTheTrackTagsListView = 'behind_the_track_tags_list_view'; +const String behindTheTrackScrollView = 'behind_the_track_scroll_view'; const String behindTheTrackFollowButton = 'behind_the_track_follow_outlined_button'; +const String fansLeaderboardOverallButton = 'fans_leaderboard_overall_segment_gesture_detector'; +const String fansLeaderboardSevenDaysButton = 'fans_leaderboard_first_7_days_segment_gesture_detector'; +const String fansLeaderboardInfoIcon = 'fans_leaderboard_info_icon'; // --- Comments --- +const String commentsBackButton = 'comments_close_icon_button'; const String commentsCommentPromptText = 'comments_comment_prompt_text'; +const String commentsFireEmoji = 'comments_fire_emoji_text'; +const String commentsClapEmoji = 'comments_clap_emoji_text'; +const String commentsPleadingEmoji = 'comments_pleading_emoji_text'; + +// --- Comment Card (dynamic prefix — append comment.id at runtime) --- +// e.g. 'comment_card_more_42_inkwell', 'comment_card_like_42_icon_button' +const String commentCardMoreInkwellPrefix = 'comment_card_more_'; +const String commentCardLikeIconButtonPrefix = 'comment_card_like_'; +const String commentCardReplyGesturePrefix = 'comment_card_reply_'; +const String commentCardShowRepliesPrefix = 'comment_card_show_replies_'; -// --- Likes --- -const String likesBackButton = 'likes_back_button'; \ No newline at end of file +// --- Comment Action Bottom Sheet --- +const String commentActionPlayFromInkwell = 'comment_action_play_from_inkwell'; +const String commentActionViewProfileInkwell = 'comment_action_view_profile_inkwell'; +const String commentActionCopyInkwell = 'comment_action_copy_inkwell'; +const String commentActionDeleteInkwell = 'comment_action_delete_inkwell'; +const String commentActionReportInkwell = 'comment_action_report_inkwell'; +const String commentActionToggleBlockInkwell = 'comment_action_toggle_block_inkwell'; + +// ── Profile — Likes ─────────────────────────────────────────────────────────── +const String profileLikesBackButton = 'likes_back_button'; +const String profileLikesCastButton = 'likes_cast_button'; +const String profileLikesSearchTextField = 'likes_search_text_field'; +const String profileLikesAddIconButton = 'likes_add_icon_button'; +const String profileLikesShuffleIconButton = 'likes_shuffle_icon_button'; +const String profileLikesPlayAllFab = 'likes_play_all_fab'; +const String profileLikesListView = 'likes_list_view'; + +// ---- Playlist Detail Page ---- +const String playlistDetailBackButton = 'playlist_detail_back_button'; +const String albumBackButton = 'library_albums_back_button'; +const String playlistDetailLikeButton = 'playlist_detail_like_button'; +const String playlistDetailMoreButton = 'playlist_detail_more_button'; +const String playlistDetailShuffleButton = 'playlist_detail_shuffle_button'; +const String playlistDetailPlayButton = 'playlist_detail_play_button'; +const String playlistDetailAddTrackButton = 'playlist_detail_add_track_button'; +const String refreshSuggestionsButton = 'refresh_suggestions_button'; + +// ---- Mix Detail Page (/home/mix/:id → MixDetailScreen) ---- +const String mixDetailBackButton = 'mix_detail_back_button'; +const String mixDetailLikeButton = 'mix_detail_like_button'; + +// ---- Related Tracks Page (/home/station/:id and /home/related-tracks/:id → RelatedTracksScreen) ---- +const String relatedTracksLikeButton = 'related_tracks_like_button'; + +// --- Sheets / Dialogs --- +const String createPlaylistNameField = 'create_playlist_name_field'; +const String createPlaylistPublicSwitch = 'create_playlist_public_switch'; +const String createPlaylistCreateButton = 'create_playlist_create_button'; +const String createPlaylistCancelButton = 'create_playlist_cancel_button'; +const String editPlaylistCancelButton = 'edit_playlist_cancel_button'; +const String editPlaylistSaveButton = 'edit_playlist_save_button'; +const String editPlaylistCoverPicker = 'edit_playlist_cover_picker'; +const String editPlaylistNameField = 'edit_playlist_name_field'; +const String editPlaylistDescriptionField = 'edit_playlist_description_field'; +const String editPlaylistPublicSwitch = 'edit_playlist_public_switch'; +const String editConvertToAlbum = 'edit_convert_to_album'; +const String editConvertToPlaylist = 'edit_convert_to_playlist'; +const String convertToAlbumCancelButton = 'convert_cancel_album'; +const String convertToAlbumConfirmButton = 'convert_confirm_album'; +const String deletePlaylistCancelButton = 'delete_playlist_cancel_button'; +const String deletePlaylistConfirmButton = 'delete_playlist_confirm_button'; +const String optionsShareSms = 'options_share_sms'; +const String optionsShareQr = 'options_share_qr'; +const String optionsShareCopy = 'options_share_copy'; +const String optionsShareWhatsapp = 'options_share_whatsapp'; +const String optionsShareMore = 'options_share_more'; +const String playlistOptionsScrollView = 'playlist_options_scroll_view'; +const String optionsLike = 'options_like'; +const String optionsPlayNext = 'options_play_next'; +const String optionsPlayLast = 'options_play_last'; +const String optionsCopy = 'options_copy'; +const String optionsEdit = 'options_edit'; +const String optionsTogglePrivacy = 'options_toggle_privacy'; +const String optionsDelete = 'options_delete'; + +// --- Search --- +const String searchScreen = 'search_screen'; +const String searchResultsTabs = 'search_results_tabs'; +const String searchSuggestionsList = 'search_suggestions_list'; +const String vibesSection = 'vibes_section'; +const String vibesList = 'vibes_list'; +const String vibesGrid = 'vibes_masonry_grid'; +const String vibesGridContainer = 'vibes_grid'; +const String searchBarField = 'search_bar_field'; +const String searchBarClearButton = 'search_bar_clear_button'; +const String searchTabBar = 'search_tab_bar'; +const String searchTabAll = 'search_tab_all'; +const String searchTabTracks = 'search_tab_tracks'; +const String searchTabProfiles = 'search_tab_profiles'; +const String searchTabPlaylists = 'search_tab_playlists'; +const String searchTabAlbums = 'search_tab_albums'; +const String searchTabView = 'search_tab_view'; +const String searchResultAllTabList = 'all_tab_list'; +const String searchResultAllTabTracksSect = 'all_tab_tracks_section'; +const String searchResultTracksList = 'tracks_list'; +const String searchResultProfilesList = 'profiles_list'; +const String searchResultPlaylistsList = 'playlists_list'; +const String searchResultAlbumsList = 'albums_list'; +const String searchResultSuggestionsList = 'suggestions_list'; +const String searchResultTrendingList = 'trending_tracks_list'; + +// ── Vibes Genre Page ────────────────────────────────────────────────────────── +const String genrePage = 'genre_page'; +const String genreHeader = 'genre_header'; +const String genreHeaderImage = 'genre_header_image'; +const String genreBackButton = 'genre_back_button'; +const String genreTitle = 'genre_title'; +const String genreTabAll = 'genre_tab_all'; +const String genreTabTrending = 'genre_tab_trending'; +const String genreTabPlaylists = 'genre_tab_playlists'; +const String genreTabAlbums = 'genre_tab_albums'; +const String genreSeeAllPage = 'genre_see_all_page'; +const String genreTrendingSeeAll = 'genre_trending_see_all'; +const String genreTrendingTracks = 'genre_trending_tracks'; +const String genreTrendingList = 'genre_trending_list'; +const String genreTrendingError = 'genre_trending_error'; +const String genreTrendingEmpty = 'genre_trending_empty'; +const String genreTrendingContent = 'genre_trending_content'; +const String genrePlaylistsSeeAll = 'genre_playlists_see_all'; +const String genrePlaylistsGrid = 'genre_playlists_grid'; +const String genrePlaylistsError = 'genre_playlists_error'; +const String genrePlaylistsEmpty = 'genre_playlists_empty'; +const String genrePlaylistsContent = 'genre_playlists_content'; +const String genreAlbumsSeeAll = 'genre_albums_see_all'; +const String genreAlbumsGrid = 'genre_albums_grid'; +const String genreAlbumsError = 'genre_albums_error'; +const String genreAlbumsEmpty = 'genre_albums_empty'; +const String genreAlbumsContent = 'genre_albums_content'; +const String genreProfilesList = 'genre_profiles_list'; +const String genreAllList = 'genre_all_list'; +const String genreAllError = 'genre_all_error'; +const String genreAllTracksList = 'genre_all_tracks_list'; +const String genreAllTracksError = 'genre_all_tracks_error'; +const String genreAllTracksEmpty = 'genre_all_tracks_empty'; + +// ── Settings selectors ─────────────────────────────────────────────────── +// Settings screen — main options +const String settingsOptionImportMyMusic = 'option_Import_my_music'; +const String settingsOptionAccount = 'option_Account'; +const String settingsOptionUpload = 'option_Upload'; +const String settingsOptionBasicSettings = 'option_Basic_settings'; +const String settingsOptionSocialSettings = 'option_Social_settings'; +const String settingsOptionInbox = 'option_Inbox'; +const String settingsOptionNotifications = 'option_Notifications'; +const String settingsOptionAddWidgets = 'option_Add_widgets'; + +// Basic settings screen +const String settingsClearCacheTile = 'Clear_application_cache_tile'; + +// Sign out (appears in both account_screen and sign_out_button_widget) +const String settingsSignOutButton = 'settings_sign_out_button'; + +// --- Track Card Options --- +const String trackShareMessageGestureDetector = 'track_options_share_message_gesture_detector'; +const String trackShareCopyLinkGestureDetector = 'track_options_share_copy_link_gesture_detector'; +const String trackShareWhatsappGestureDetector = 'track_options_share_whatsapp_gesture_detector'; +const String trackShareSMSGestureDetector = 'track_options_share_sms_gesture_detector'; +const String trackUpdateTrackInkwell = 'track_options_action_update_track_inkwell'; +const String trackLikeInkwell = 'track_options_action_toggle_like_inkwell'; + +// ── Notifications feed screen ─ +const String notificationsScreenKey = 'notifications_screen'; +const String notificationsBackButton = 'notifications_back_button'; +const String notificationsListView = 'notifications_list'; +const String notificationsFilterIcon = 'notifications_filter_button'; +const String notificationsFilterComments = 'filter_option_comments'; +const String notificationsFilterLikes = 'filter_option_likes'; +const String notificationsFilterFollowing = 'filter_option_followings'; +const String notificationsFilterReposts = 'filter_option_reposts'; +const String notificationsFilterShowAll = 'filter_option_all'; +const String notificationsFilterReactions = 'filter_option_reactions'; +const String notificationTileInkwell = 'notification_tile_inkwell'; +const String notificationsShowAllButton = 'notifications_show_all_button'; +const String notificationsLoadingIndicator = 'notifications_loading_indicator'; +const String notificationsErrorText = 'notifications_error_text'; +const String notificationsEmptyAll = 'notifications_empty_all'; +const String notificationsListKey = 'notifications_list'; + +// ── M14 — Premium / Upgrade ────────────────────────────────────────────────── +const String premiumPlansSection = 'premium_plans_section'; +const String premiumManageSubscriptionButton = 'premium_manage_subscribtion_button'; +const String premiumCancelsubscriptionButton = 'premium_cancel_subscribtion_button'; + +// ── One Main Flow ────────────────────────────────────────────────── +String messagingInboxItemTile(String participantName) => 'messaging_inbox_item_${participantName}_tile'; +const String chatScreenMessageInput = 'chat_screen_message_input'; +const String messagingMessageInputField = 'messaging_message_input_text_field'; +const String chatScreenSendButton = 'chat_screen_send_button'; +const String chatScreenAddEmbedButton = 'chat_screen_add_embed_button'; +const String doneButton = 'likes_playlists_done_button'; +const String trackOptionAddToPlaylist = 'track_options_action_add_to_playlist_inkwell'; +const String playlistNameField = 'playlist_create_name_textfield'; +String playlisTile(int index) => 'playlist_tile_${index}'; +String trackTile(int index) => 'track_tile_${index}'; +const String addToPlaylistNewNameField = 'add_to_playlist_new_name_field'; +const String confirmAddToPlaylist = 'add_to_playlist_confirm_create_button'; +const String doneAddToPlaylistButton = 'add_to_playlist_done_button'; \ No newline at end of file diff --git a/integration_test/tests/M10_Notifications/notifications_test.dart b/integration_test/tests/M10_Notifications/notifications_test.dart new file mode 100644 index 00000000..cdbf7ce1 --- /dev/null +++ b/integration_test/tests/M10_Notifications/notifications_test.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M10_Notifications/notification_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M10 - Notifications - all scenarios', (tester) async { + app.main(); + await tester.pumpAndSettle(const Duration(seconds: 5)); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + + // ── Login ──────────────────────────────────────────────────────────────── + final loginPage = LoginPage(tester); + final notificationsPage = NotificationsPage(tester); + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + await tryTest('TC-NOTIF-001 | Tap notification icon to open notifications screen', () async { + await notificationsPage.tapNotificationIcon(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(notificationsPage.isNotificationsScreenVisible(), true); + }); + + await tryTest('TC-NOTIF-002 | Scroll smoothly between notifications', () async { + await notificationsPage.scrollDown(); + await notificationsPage.scrollUp(); + expect(notificationsPage.isNotificationsScreenVisible(), true); + }); + + await tryTest('TC-NOTIF-003 | Filter by Comments → only comment notifications', () async { + await notificationsPage.tapFilterIcon(); + await notificationsPage.tapFilterComments(); + await notificationsPage.waitForNotificationsToLoad(); + debugPrint('TC-NOTIF-003 tile count: ${find.byKey(const Key(notificationTileInkwell)).evaluate().length}'); + expect(notificationsPage.isFilterAppliedSuccessfully(), true, + reason: 'Filter should be applied — list or empty message visible'); + }); + + await tryTest('TC-NOTIF-004 | Tap a comment notification → track page opens', () async { + if (notificationsPage.hasNotificationItems()) { + await notificationsPage.tapFirstNotification(); + expect(notificationsPage.isOnTrackPage(), true, + reason: 'Tapping a comment notification should open the track page'); + await notificationsPage.tapTrackPageBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(notificationsPage.isNotificationsScreenVisible(), true); + } else { + debugPrint('TC-NOTIF-004: no comment notifications on test account, skipping'); + } + }); + + await tryTest('TC-NOTIF-005 | Filter by Likes → only like notifications', () async { + await notificationsPage.tapFilterIcon(); + await notificationsPage.tapFilterLikes(); + await notificationsPage.waitForNotificationsToLoad(); + debugPrint('TC-NOTIF-005 tile count: ${find.byKey(const Key(notificationTileInkwell)).evaluate().length}'); + expect(notificationsPage.isFilterAppliedSuccessfully(), true, + reason: 'Filter should be applied — list or empty message visible'); + }); + + await tryTest('TC-NOTIF-006 | Tap a like notification → track page opens', () async { + if (notificationsPage.hasNotificationItems()) { + await notificationsPage.tapFirstNotification(); + expect(notificationsPage.isOnTrackPage(), true, + reason: 'Tapping a like notification should open the track page'); + await notificationsPage.tapTrackPageBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(notificationsPage.isNotificationsScreenVisible(), true); + } else { + debugPrint('TC-NOTIF-006: no like notifications on test account, skipping'); + } + }); + + await tryTest('TC-NOTIF-007 | Filter by Following → only following notifications', () async { + await notificationsPage.tapFilterIcon(); + await notificationsPage.tapFilterFollowing(); + await notificationsPage.waitForNotificationsToLoad(); + debugPrint('TC-NOTIF-007 tile count: ${find.byKey(const Key(notificationTileInkwell)).evaluate().length}'); + expect(notificationsPage.isFilterAppliedSuccessfully(), true, + reason: 'Filter should be applied — list or empty message visible'); + }); + + await tryTest('TC-NOTIF-008 | Tap a following notification → profile page opens', () async { + if (notificationsPage.hasNotificationItems()) { + await notificationsPage.tapFirstNotification(); + expect(notificationsPage.isOnProfilePage(), true, + reason: 'Tapping a follow notification should open the user profile'); + await notificationsPage.tapProfilePageBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(notificationsPage.isNotificationsScreenVisible(), true); + } else { + debugPrint('TC-NOTIF-008: no following notifications on test account, skipping'); + } + }); + + await tryTest('TC-NOTIF-009 | Filter by Reposts → only repost notifications', () async { + await notificationsPage.tapFilterIcon(); + await notificationsPage.tapFilterReposts(); + await notificationsPage.waitForNotificationsToLoad(); + debugPrint('TC-NOTIF-009 tile count: ${find.byKey(const Key(notificationTileInkwell)).evaluate().length}'); + expect(notificationsPage.isFilterAppliedSuccessfully(), true, + reason: 'Filter should be applied — list or empty message visible'); + }); + + await tryTest('TC-NOTIF-010 | Tap a repost notification → track page opens', () async { + if (notificationsPage.hasNotificationItems()) { + await notificationsPage.tapFirstNotification(); + expect(notificationsPage.isOnTrackPage(), true, + reason: 'Tapping a repost notification should open the track page'); + await notificationsPage.tapTrackPageBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(notificationsPage.isNotificationsScreenVisible(), true); + } else { + debugPrint('TC-NOTIF-010: no repost notifications on test account, skipping'); + } + }); + + //Expect to fail as is hidden by nav bar + await tryTest('TC-NOTIF-011 | Filter by Reactions → only reaction notifications', () async { + await notificationsPage.tapFilterIcon(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await notificationsPage.tapFilterReactions(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(notificationsPage.isFilterAppliedSuccessfully(), true, + reason: 'Filter should be applied — list or empty message visible'); + }); + + await tryTest('TC-NOTIF-012 | Tap a reaction notification → track page opens', () async { + if (notificationsPage.hasNotificationItems()) { + await notificationsPage.tapFirstNotification(); + expect(notificationsPage.isOnTrackPage(), true, + reason: 'Tapping a reaction notification should open the track page'); + + await notificationsPage.tapBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(notificationsPage.isNotificationsScreenVisible(), true); + } else { + debugPrint('TC-NOTIF-012: no reaction notifications on test account, skipping'); + } + }); + + await tryTest('TC-NOTIF-013 | Filter → Show all → all notifications visible', () async { + await notificationsPage.tapFilterIcon(); + await notificationsPage.tapFilterShowAll(); + await notificationsPage.waitForNotificationsToLoad(); + expect(notificationsPage.isFilterAppliedSuccessfully(), true, + reason: 'All notifications should be shown after Show All'); + }); + + await tryTest('TC-NOTIF-014 | Back from notifications → home page', () async { + await notificationsPage.tapBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(notificationsPage.isHomePageVisible(), true, + reason: 'Back from notifications should return to the home page'); + }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + + }); +} \ No newline at end of file diff --git a/integration_test/tests/M12_Library/library_test.dart b/integration_test/tests/M12_Library/library_test.dart new file mode 100644 index 00000000..85a91b8d --- /dev/null +++ b/integration_test/tests/M12_Library/library_test.dart @@ -0,0 +1,420 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M12_Library/library_page.dart'; +import '../../pages/base_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M12 - Library - all scenarios', (tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + + await tester.pumpAndSettle(const Duration(seconds: 5)); + + // ── Login ──────────────────────────────────────────────────────────────── + final loginPage = LoginPage(tester); + final basePage = BasePage(tester); + final libraryPage = LibraryPage(tester); + + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + await tryTest('TC-LIBRARY-001 | Navigate to Library tab', () async { + await libraryPage.tapLibraryNavButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + + // YOUR LIKES SECTION + debugPrint('YOUR LIKES SECTION'); + + await tryTest('TC-LIBRARY-002 | Open Your Likes', () async { + await libraryPage.tapYourLikes(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isLikesScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY-003 | Filter likes (Most recent/ Title(A-Z)/ Artist(A-Z))', () async { + await libraryPage.tapLikesFilterButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapLikeFilterOptionTitleAZ(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + // Hidden under the nav bar --> cross issue + // await libraryPage.tapLikesFilterButton(); + // await tester.pumpAndSettle(const Duration(seconds: 1)); + // await libraryPage.tapLikeFilterOptionArtistAZ(); + // await tester.pumpAndSettle(const Duration(seconds: 2)); + + await libraryPage.tapLikesFilterButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapLikeFilterOptionMoreRecent(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-004 | Play button is tappable', () async { + await libraryPage.tapLikesPlay(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await libraryPage.tapLikesPlay(); + }); + + await tryTest('TC-LIBRARY-005 | Scroll likes list down and up', () async { + await libraryPage.scrollListDown(libraryLikesScrollView); + await libraryPage.scrollListUp(libraryLikesScrollView); + }); + + await tryTest('TC-LIBRARY-006 | Three-dots → Unlike a track', () async { + await libraryPage.tapFirstTrackThreeDots(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapUnlikeOption(); + await basePage.pullToRefresh(libraryLikesScrollView); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-007 | Search for liked track → visible', () async { + await libraryPage.typeInLikesSearch("أنا وأخي"); + expect(libraryPage.isTrackVisible("أنا وأخي"), true, + reason: 'Liked track should appear in search results'); + }); + + await tryTest('TC-LIBRARY-008 | Search for unliked track → not visible', () async { + await libraryPage.typeInLikesSearch("Unliked Track"); + }); + + await tryTest('TC-LIBRARY-009 | Back to Library', () async { + await libraryPage.tapLikesBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + + // ALBUMS SECTION + debugPrint('ALBUMS SECTION'); + + await tryTest('TC-LIBRARY-010 | Open Albums', () async { + await libraryPage.tapAlbums(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isAlbumsScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY-011 | Filter albums (RecentlyAdded - FirstAdded - AlbumName)', () async { + await libraryPage.tapAlbumFilterButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapAlbumFilterOptionRecentlyAdded(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await libraryPage.tapAlbumFilterButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapAlbumFilterOptionFirstAdded(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await libraryPage.tapAlbumFilterButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapAlbumFilterOptionAlbumName(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-012 | Open an album', () async { + await tester.pumpAndSettle(const Duration(seconds: 2)); + await libraryPage.tapFirstAlbum(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isAlbumDetailVisible(), true); + }); + + await tryTest('TC-LIBARARY-013 | Like & 3 dots are clickable', () async { + await libraryPage.tapAlbumLike(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await libraryPage.tapAlbumShowMore(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-014 | Play & shuffle the album', () async { + await libraryPage.tapAlbumPlay(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await libraryPage.tapAlbumShuffle(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-015 | Back to Albums list', () async { + await libraryPage.tapAlbumsBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isAlbumsScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY-016 | Search for existing album → visible', () async { + await libraryPage.typeInAlbumsSearch('Quran'); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isTrackVisible('Quran'), true, + reason: 'Saved album should appear in search'); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-017 | Search for non-existing album → no results', () async { + await tester.pumpAndSettle(const Duration(seconds: 2)); + await libraryPage.typeInAlbumsSearch('notSavedAlbumName'); + expect(libraryPage.isNoResultsMessageVisible('notSavedAlbumName'), true, + reason: '"No results for" message should appear'); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-018 | Back to Library', () async { + await libraryPage.tapAlbumsBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + + // FOLLOWING SECTION + debugPrint('FOLLOWING SECTION'); + + await tryTest('TC-LIBRARY-019 | Open Following', () async { + await libraryPage.tapFollowing(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isFollowingScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY- 020 | Tap follower --> profile opens', () async { + await libraryPage.tapFirstFollower(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isProfileScreenVisible(), true); + await libraryPage.tapProfileBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-021 | Scroll following list down and up', () async { + await libraryPage.scrollListDown(followingListView); + await libraryPage.scrollListUp(followingListView); + }); + + await tryTest('TC-LIBRARY-022 | Tap UnFollow button → dialog appears', () async { + await libraryPage.tapFirstFollowingButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(libraryPage.isUnfollowDialogVisible(), true); + }); + + await tryTest('TC-LIBRARY-023 | Tap Cancel → dialog dismisses', () async { + await libraryPage.tapCancelOnDialog(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(libraryPage.isUnfollowDialogVisible(), false, + reason: 'Dialog should dismiss on Cancel'); + }); + + await tryTest('TC-LIBRARY-024 | Tap Following again → tap Unfollow', () async { + await libraryPage.tapFirstFollowingButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(libraryPage.isUnfollowDialogVisible(), true); + await libraryPage.tapUnfollowOnDialog(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-025 | Back to Library', () async { + await libraryPage.tapFollowingBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + + // STATIONS SECTION + debugPrint('STATIONS SECTION'); + + await tryTest('TC-LIBRARY-026 | Open Stations', () async { + await libraryPage.tapStations(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isStationsScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY-027 | Filter Stations', () async { + await libraryPage.tapStationFilterButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapStationFilterOptionFirstAdded(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await libraryPage.tapStationFilterButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapStationFilterOptionStationName(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await libraryPage.tapStationFilterButton(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapStationFilterOptionRecentlyAdded(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-028 | Open Station & play the tracks', () async { + await tester.pumpAndSettle(const Duration(seconds: 2)); + await libraryPage.tapFirstStation(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isStationDetailVisible(), true); + await libraryPage.tapStationPlay(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-LIBRARY-029 | Back to Stations list', () async { + await libraryPage.tapStationDetailBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isStationsScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY-030 | Search Stations', () async { + await libraryPage.typeInStationsSearch("Cairokee Radio"); + expect(libraryPage.isTrackVisible("Cairokee Radio"), true, + reason: 'Saved station should appear in search'); + + await libraryPage.typeInStationsSearch("notSavedStationName"); + expect(libraryPage.isNoResultsMessageVisible("notSavedStationName"), true, + reason: '"No results for" message should appear'); + }); + + await tryTest('TC-LIBRARY-031 | Back to Library', () async { + await libraryPage.tapStationsBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + + // YOUR INSIGHTS SECTION + debugPrint('YOUR INSIGHTS SECTION'); + + await tryTest('TC-LIBRARY-032 | Open Your Insights', () async { + await libraryPage.tapYourInsights(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isInsightsScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY-033 | Plays / Listeners / Likes stats are visible', () async { + expect(libraryPage.isInsightsStatsVisible(), true, + reason: 'All 3 stat counters should be visible'); + }); + + await tryTest('TC-LIBRARY-034 | Scroll insights down and up', () async { + await libraryPage.scrollInsightsDown(); + await libraryPage.scrollInsightsUp(); + }); + + await tryTest('TC-LIBRARY-035 | All Platforms tab is tappable', () async { + await libraryPage.tapAllPlatformsTab(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + }); + + await tryTest('TC-LIBRARY-036 | Back to Library', () async { + await libraryPage.tapInsightsBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + + // YOUR UPLOADS SECTION + debugPrint('YOUR UPLOADS SECTION'); + + await tryTest('TC-LIBRARY-037 | Open Your Uploads', () async { + await libraryPage.tapYourUploads(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isUploadsScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY-038 | Play & shuffle buttons', () async { + await libraryPage.tapYourUploadsPlay(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await libraryPage.tapYourUploadsShuffle(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + // ─── TC-LIBRARY-038 | Three-dots → Delete uploaded track ───────────── + // await tryTest('TC-LIBRARY-039 | Three-dots → Delete uploaded track', () async { + // await libraryPage.tapFirstUploadThreeDots(); + // await tester.pumpAndSettle(const Duration(seconds: 1)); + // await libraryPage.tapUpdatedTrack(); + // await tester.pumpAndSettle(const Duration(seconds: 2)); + // await libraryPage.scrollUntilVisible(itemText: 'Delete Track', scrollableKey: uploadsScrollView); //Delete button + // await libraryPage.tapDeleteUploadOption(); + // await tester.pumpAndSettle(const Duration(seconds: 2)); + // }); + + await tryTest('TC-LIBRARY-040 | Scroll Your Uploads', () async { + await libraryPage.scrollListDown(uploadsScrollView); + await libraryPage.scrollListUp(uploadsScrollView); + }); + + await tryTest('TC-LIBRARY-041 | Search in Your Uploads', () async { + await libraryPage.typeInUploadsSearch("أنا وأخي"); + expect(libraryPage.isTrackVisible("أنا وأخي"), true, + reason: 'Uploaded track should appear in search'); + await libraryPage.typeInUploadsSearch("notUploadedTrackName"); + // expect(libraryPage.isTrackVisible("notUploadedTrackName"), false, + // reason: 'Non-uploaded track should not appear'); + }); + + await tryTest('TC-LIBRARY-042 | Back to Library', () async { + await libraryPage.tapUploadsBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + + // RECENTLY PLAYED SECTION + debugPrint('RECENTLY PLAYED SECTION'); + + await tryTest('TC-LIBRARY-043 | Tap See All → Recently Played screen', () async{ + await libraryPage.scrollUntilVisible(itemText: 'Recently played', scrollableKey: libraryMainScrollView); + await libraryPage.tapRecentlyPlayedSeeAll(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(libraryPage.isRecentlyPlayedScreenVisible(), true); + }); + + await tryTest('TC-LIBRARY-044 | Play & shuffle buttons', () async{ + await libraryPage.tapHistoryPlay(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await libraryPage.tapHistoryShuffle(); + }); + + await tryTest('TC-LIBRARY-045 | Scroll history list down and up', () async{ + await libraryPage.scrollListDown(historyListView); + await libraryPage.scrollListUp(historyListView); + }); + + + await tryTest('TC-LIBRARY-046 | Delete History', () async { + await libraryPage.tapDeleteHistoryIcon(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapCancelClearDialog(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(libraryPage.isRecentlyPlayedScreenVisible(), true, + reason: 'History list should remain after Cancel'); + + await libraryPage.tapDeleteHistoryIcon(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await libraryPage.tapClearOnDialog(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + // expect(libraryPage.isNoListeningHistoryVisible(), true, + // reason: '"No listening history" should appear after clearing'); + }); + + await tryTest('TC-LIBRARY-047 | Back to Library', () async { + await libraryPage.tapInsightsBack(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} \ No newline at end of file diff --git a/integration_test/tests/M13_Settings/settings_test.dart b/integration_test/tests/M13_Settings/settings_test.dart new file mode 100644 index 00000000..4fa9a485 --- /dev/null +++ b/integration_test/tests/M13_Settings/settings_test.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M12_Library/library_page.dart'; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M13_Settings/settings_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M13 - Settings - all scenarios', (tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + await tester.pumpAndSettle(const Duration(seconds: 5)); + + final loginPage = LoginPage(tester); + final libraryPage = LibraryPage(tester); + final settingsPage = SettingsPage(tester); + + // ── Login ────────────────────────────────────────────────────────────────── + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + + // ─── TC-SETTINGS-001 | Navigate Setting Page ───────────────────────── + await tryTest('TC-SETTINGS-001 | Navigate to settings page', () async { + await libraryPage.tapLibraryNavButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.tapSettingsIcon(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + await tryTest('TC-SETTINGS-002 | check all tab are tapple and direct to a specific page witout crashing', () async { + await settingsPage.tapImportMyMusic(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.gobackFromImportMyMusic(); + + // await settingsPage.tapUpload(); + // await tester.pumpAndSettle(const Duration(seconds: 3)); + // await settingsPage.gobackFromUpload(); + + await settingsPage.tapSocailSettings(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.gobackFromSocailSettings(); + + await settingsPage.tapInbox(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.gobackFromInbox(); + + await settingsPage.tapNotifications(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.gobackFromNotifications(); + + await settingsPage.tapAddWidgets(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.gobackFromAddWidgets(); + }); + + await tryTest('TC-SETTINGS-003 | Clear cache successfully', () async { + await settingsPage.tapBasicSettings(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.tapOnClearAppCache(); + await settingsPage.tapYes(); + await settingsPage.gobackFromBasicSettings(); + + }); + + await tryTest('TC-SETTINGS-004 | Clear cache successfully', () async { + await settingsPage.tapBasicSettings(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.tapOnClearAppCache(); + await settingsPage.tapYes(); + await settingsPage.gobackFromBasicSettings(); + }); + + await tryTest('TC-SETTINGS-005 | Sign Out successfully', () async { + await settingsPage.tapSignOut(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.tapOK(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect (settingsPage.isOnBoardingPage(), true); + }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} \ No newline at end of file diff --git a/integration_test/tests/M14_Premium/premium_test.dart b/integration_test/tests/M14_Premium/premium_test.dart new file mode 100644 index 00000000..826e3d8f --- /dev/null +++ b/integration_test/tests/M14_Premium/premium_test.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M14_Premium/premium_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M14 - Premium - all scenarios', (tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + await tester.pumpAndSettle(const Duration(seconds: 5)); + + final loginPage = LoginPage(tester); + final premiumPage = PremiumPage(tester); + + // ── Login ────────────────────────────────────────────────────────────────── + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + + await tryTest('TC-PREMIUM-001 | Navigate to Upgrade tab', () async { + await premiumPage.tapUpgradeNavButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + await tryTest('TC-PREMIUM-002 | Check Buttons Visibility (continue/see all plans)', () async { + expect(premiumPage.isButtonsVisible(), true); + }); + + await tryTest('TC-PREMIUM-003 | Continue button --> Confirm payment scenario', () async { + await premiumPage.tapContinueButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await premiumPage.tapConfirmPaymentButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await premiumPage.tapStartExploring(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + await tryTest('TC-PREMIUM-004 | See All Plans button', () async { + await premiumPage.tapSeeAllPlansButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await premiumPage.scrollHorizontallyInSection(premiumPlansSection); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await premiumPage.scrollUntilVisible(itemText: 'Manage subscription', scrollableKey: premiumManageSubscriptionButton); + await premiumPage.tapMangeSubscribtionButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await premiumPage.scrollUntilVisible(itemText: 'Cancel subscription', scrollableKey: premiumCancelsubscriptionButton); + await premiumPage.tapKeepsubscribtionButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await premiumPage.tapCancelSubscribtionButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} \ No newline at end of file diff --git a/integration_test/tests/M1_authentication/login_test.dart b/integration_test/tests/M1_authentication/login_test.dart index 3fa3edc5..e5835de4 100644 --- a/integration_test/tests/M1_authentication/login_test.dart +++ b/integration_test/tests/M1_authentication/login_test.dart @@ -2,135 +2,80 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:rythmify/main.dart' as app; -import '../../pages/auth/login_page.dart'; +import '../../pages/M1_Authentication/login_page.dart'; import '../../fixtures/test_data.dart'; import '../../selectors/selectors.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('M1 - Authentication: Login', () { - - // ─── Page Load Tests ──────────────────────────────────────────────────── - testWidgets('onboarding login button is visible and navigates to sign-in page', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final loginPage = LoginPage(tester); - expect(find.byKey(const Key(onboardingLoginButton)), findsOneWidget); - expect(find.text('Log in'), findsOneWidget); - - await tester.tap(find.byKey(const Key(onboardingLoginButton))); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - expect(loginPage.isOnLoginPage(), true); - expect(find.text('Sign in or create an account'), findsOneWidget); - }); - - testWidgets('login page loads correctly with all social auth buttons', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final loginPage = LoginPage(tester); - expect(loginPage.isOnLoginPage(), true); - expect(find.byKey(const Key(authSocialGoogleButton)), findsOneWidget); - expect(find.byKey(const Key(authSocialAppleButton)), findsOneWidget); - expect(find.byKey(const Key(authSocialFacebookButton)), findsOneWidget); - expect(find.byKey(const Key(authContinueButton)), findsOneWidget); - }); - + testWidgets('M1 - Authentication: Login - all scenarios', (tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + await tester.pumpAndSettle(const Duration(seconds: 5)); + final loginPage = LoginPage(tester); - // ─── Valid Credentials ────────────────────────────────────────────────── - testWidgets('should login successfully with valid credentials', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); + // ── Helper to reset to onboarding between scenarios ── + Future goBackToOnboarding() async { + final NavigatorState navigator = tester.state(find.byType(Navigator).last); + navigator.popUntil((route) => route.isFirst); + await tester.pumpAndSettle(const Duration(seconds: 3)); + } - final loginPage = LoginPage(tester); + + await tryTest('TC-AUTH-LOGIN-001 | error message when enter empty email', () async { await tester.tap(find.byKey(const Key(onboardingLoginButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); - await loginPage.login(validEmail, validPassword); - await tester.pumpAndSettle(const Duration(seconds: 5)); + await loginPage.tapContinue(); + await tester.pumpAndSettle(const Duration(seconds: 1)); - expect(loginPage.isOnHomePage(), true); + expect(find.text('Please enter your email'), findsOneWidget); + await goBackToOnboarding(); }); - - // ─── Invalid Credentials ──────────────────────────────────────────────── - // Test multiple invalid password scenarios for authentication errors - for (final invalidPassword in ['WrongPassword1!', 'completelywrong', '12345678']) { - testWidgets('should show error message with invalid password: $invalidPassword', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final loginPage = LoginPage(tester); - await tester.tap(find.byKey(const Key(onboardingLoginButton))); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - await loginPage.enterEmail(validEmail); - await loginPage.tapContinue(); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - await loginPage.enterPassword(invalidPassword); - await loginPage.tapLogin(); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - expect(find.text('Invalid email or password.'), findsOneWidget); - expect(loginPage.isOnPasswordPage(), true); - }); - } - - testWidgets('should show error with unregistered email', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final loginPage = LoginPage(tester); + await tryTest('TC-AUTH-LOGIN-002 | error message when enter empty password', () async { await tester.tap(find.byKey(const Key(onboardingLoginButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); - await loginPage.login('unregistered@rythmify.com', validPassword); + await loginPage.enterEmail(validEmail); + await loginPage.tapContinue(); await tester.pumpAndSettle(const Duration(seconds: 3)); - expect(find.text('Invalid email or password.'), findsOneWidget); - expect(loginPage.isOnPasswordPage(), true); - }); + await loginPage.tapLogin(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.text('Please enter your password'), findsOneWidget); - testWidgets('should show error with invalid email format', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); + await goBackToOnboarding(); + }); - final loginPage = LoginPage(tester); + await tryTest('TC-AUTH-LOGIN-003 | error message when enter invalid email format', () async { await tester.tap(find.byKey(const Key(onboardingLoginButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); await loginPage.enterEmail('invalid-email-format'); + await tester.testTextInput.receiveAction(TextInputAction.done); await loginPage.tapContinue(); await tester.pumpAndSettle(const Duration(seconds: 2)); - expect(find.text('Please enter a valid email'), findsOneWidget); - }); - - // ─── Empty Fields Validation ──────────────────────────────────────────── - testWidgets('should show error with empty email', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final loginPage = LoginPage(tester); - await tester.tap(find.byKey(const Key(onboardingLoginButton))); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - await loginPage.tapContinue(); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - expect(find.text('Please enter your email'), findsOneWidget); - }); - - testWidgets('should show error with empty password', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); + await goBackToOnboarding(); + }); - final loginPage = LoginPage(tester); + await tryTest('TC-AUTH-LOGIN-004 | error message when enter invalid password', () async { await tester.tap(find.byKey(const Key(onboardingLoginButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); @@ -138,135 +83,44 @@ void main() { await loginPage.tapContinue(); await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.enterPassword('1234'); await loginPage.tapLogin(); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - expect(find.text('Please enter your password'), findsOneWidget); - }); - - - // ─── Social Authentication (SKIPPED - Ready for Backend Integration) ──── - // testWidgets( - // 'should login successfully with Google authentication', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final loginPage = LoginPage(tester); - // await tester.tap(find.byKey(const Key(onboardingLoginButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Tap Google Sign-In button - // await loginPage.tapGoogleSignIn(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Verify home page is reached - // expect(loginPage.isOnHomePage(), true); - // }, - // ); - - // testWidgets( - // 'should login successfully with Facebook authentication', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final loginPage = LoginPage(tester); - // await tester.tap(find.byKey(const Key(onboardingLoginButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Tap Facebook Sign-In button - // await loginPage.tapFacebookSignIn(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Verify home page is reached - // expect(loginPage.isOnHomePage(), true); - // }, - // ); - - // testWidgets( - // 'should login successfully with Apple authentication', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final loginPage = LoginPage(tester); - // await tester.tap(find.byKey(const Key(onboardingLoginButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Tap Apple Sign-In button - // await loginPage.tapAppleSignIn(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Verify home page is reached - // expect(loginPage.isOnHomePage(), true); - // }, - // ); - - // testWidgets( - // 'should handle Google authentication with invalid credentials', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final loginPage = LoginPage(tester); - // await tester.tap(find.byKey(const Key(onboardingLoginButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Attempt Google Sign-In with invalid credentials - // await loginPage.tapGoogleSignIn(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); + await tester.pumpAndSettle(const Duration(seconds: 3)); - // // Should show error or return to login page - // expect( - // loginPage.isOnLoginPage() || find.text('Authentication failed').evaluate().isNotEmpty, - // true, - // ); - // }, - // ); + expect(find.text('Invalid email or password.'), findsOneWidget); + expect(loginPage.isOnPasswordPage(), true); - // testWidgets( - // 'should handle Facebook authentication with invalid credentials', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); + await goBackToOnboarding(); + }); + + await tryTest('TC-AUTH-LOGIN-005 | error message when login with unregister email', () async { + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); - // final loginPage = LoginPage(tester); - // await tester.tap(find.byKey(const Key(onboardingLoginButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login('unregistered@rythmify.com', validPassword); + await tester.pumpAndSettle(const Duration(seconds: 3)); - // // Attempt Facebook Sign-In with invalid credentials - // await loginPage.tapFacebookSignIn(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(find.text('Invalid email or password.'), findsOneWidget); + expect(loginPage.isOnPasswordPage(), true); - // // Should show error or return to login page - // expect( - // loginPage.isOnLoginPage() || find.text('Authentication failed').evaluate().isNotEmpty, - // true, - // ); - // }, - // ); + await goBackToOnboarding(); + }); - // testWidgets( - // 'should handle Apple authentication with invalid credentials', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); + await tryTest('TC-AUTH-LOGIN-006 | Login Successfully', () async { + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); - // final loginPage = LoginPage(tester); - // await tester.tap(find.byKey(const Key(onboardingLoginButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); - // // Attempt Apple Sign-In with invalid credentials - // await loginPage.tapAppleSignIn(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + }); - // // Should show error or return to login page - // expect( - // loginPage.isOnLoginPage() || find.text('Authentication failed').evaluate().isNotEmpty, - // true, - // ); - // }, - // ); + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } }); } \ No newline at end of file diff --git a/integration_test/tests/M1_authentication/register_test.dart b/integration_test/tests/M1_authentication/register_test.dart index 1772f316..a229d3de 100644 --- a/integration_test/tests/M1_authentication/register_test.dart +++ b/integration_test/tests/M1_authentication/register_test.dart @@ -2,116 +2,50 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:rythmify/main.dart' as app; -import '../../pages/auth/register_page.dart'; +import '../../pages/M1_Authentication/register_page.dart'; import '../../fixtures/test_data.dart'; import '../../selectors/selectors.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('Registration', () { - - // ─── Valid Registration ───────────────────────────────────────────────── - testWidgets('should register successfully with new valid credentials', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); + testWidgets('M1 - Authentication: Registration - all scenarios', (tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + await tester.pumpAndSettle(const Duration(seconds: 5)); + final registerPage = RegisterPage(tester); - // Navigate to register - await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); + Future goBackToOnboarding() async { + final NavigatorState navigator = tester.state(find.byType(Navigator).last); + navigator.popUntil((route) => route.isFirst); await tester.pumpAndSettle(const Duration(seconds: 3)); + } - // Complete registration - await registerPage.register( - newUser.email, - newUser.password, - newUser.username, - newUser.month, - newUser.day, - newUser.year, - newUser.gender, - ); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - // Verify home page is visible - expect(registerPage.isOnHomePage(), true); - }); - - testWidgets('should scroll and select the right values and register successfully', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register + await tryTest('TC-AUTH-REGISTER-001 | error message when enter empty email', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); - // Manual step-by-step registration with scrolling - await registerPage.enterEmail(newUser.email); await registerPage.tapContinue(); - await tester.pumpAndSettle(); - - await registerPage.enterPassword(newUser.password); - await registerPage.tapPasswordContinue(); - await tester.pumpAndSettle(); - - await registerPage.enterUsername(newUser.username); - await registerPage.selectMonth(newUser.month); - await registerPage.openDayDropdown(); - await registerPage.scrollToDay(ScrollValues.days); - expect(find.text(ScrollValues.days), findsOneWidget); - - await registerPage.selectDay(newUser.day); - - await registerPage.openYearDropdown(); - await registerPage.scrollToYear(ScrollValues.years); - expect(find.text(ScrollValues.years), findsOneWidget); - - await registerPage.selectYear(newUser.year); - await registerPage.selectGender(newUser.gender); - await registerPage.tapFinalContinue(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - expect(registerPage.isOnHomePage(), true); - }); - - - // ─── Existing Email ────────────────────────────────────────────────── - testWidgets('should show error when registering with existing email', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register - await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - await registerPage.register( - existingUser.email, - existingUser.password, - existingUser.username, - existingUser.month, - existingUser.day, - existingUser.year, - existingUser.gender, - ); await tester.pumpAndSettle(const Duration(seconds: 2)); - expect(registerPage.isAlreadyExistsErrorVisible(), true); + expect(registerPage.isEmailEmptyErrorVisible(), true); + await goBackToOnboarding(); }); - - // ─── Invalid Email Format ──────────────────────────────────────────── - testWidgets('should show error when registering with invalid email format', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register + await tryTest('TC-AUTH-REGISTER-002 | error message when enter invalid email format', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); @@ -120,66 +54,25 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 2)); expect(registerPage.isInvalidEmailErrorVisible(), true); + await goBackToOnboarding(); }); - - // ─── Invalid Password Format ───────────────────────────────────────── - for (final invalidPassword in invalidPasswords) { - testWidgets( - 'should show error when registering with invalid password: ${invalidPassword.reason}', - (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register - await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - await registerPage.enterEmail(newUser.email); - await registerPage.tapContinue(); - await tester.pumpAndSettle(); - - await registerPage.enterPassword(invalidPassword.password); - await registerPage.tapPasswordContinue(); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - expect( - registerPage.isSpecificPasswordErrorVisible(invalidPassword.errorKey), - true, - reason: 'Expected error for ${invalidPassword.reason}', - ); - }, - ); - } - - - // ─── Empty Fields Validation ──────────────────────────────────────── - testWidgets('should show error when registering with empty email', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register + await tryTest('TC-AUTH-REGISTER-003 | error message when enter empty password', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); - await registerPage.enterEmail(''); + await registerPage.enterEmail(newUser.email); await registerPage.tapContinue(); + await tester.pumpAndSettle(); + + await registerPage.tapPasswordContinue(); await tester.pumpAndSettle(const Duration(seconds: 2)); - expect(registerPage.isEmailEmptyErrorVisible(), true); + expect(registerPage.isPasswordEmptyErrorVisible(), true); + await goBackToOnboarding(); }); - testWidgets('should show error when registering with empty password', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register + await tryTest('TC-AUTH-REGISTER-004 | error message when enter invalid password format (too short/no UpperCase/no LowerCase/ <8 Char)', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); @@ -187,32 +80,39 @@ void main() { await registerPage.tapContinue(); await tester.pumpAndSettle(); - await registerPage.enterPassword(''); - await registerPage.tapPasswordContinue(); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - expect(registerPage.isPasswordEmptyErrorVisible(), true); + for (final invalidPassword in invalidPasswords) { + await registerPage.enterPassword(invalidPassword.password); + await registerPage.tapPasswordContinue(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect( + registerPage.isSpecificPasswordErrorVisible(invalidPassword.errorKey), + true, + reason: 'Expected error for ${invalidPassword.reason}', + ); + + // Clear the password field before trying the next one + await tester.enterText( + find.byKey(const Key(authPasswordTextField)), + '', + ); + await tester.pumpAndSettle(); + } + await goBackToOnboarding(); }); - testWidgets('should show error when registering with empty username', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - final registerPage = RegisterPage(tester); - - // Navigate to register + await tryTest('TC-AUTH-REGISTER-005 | error message when enter empty Display name', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); await registerPage.enterEmail(newUser.email); await registerPage.tapContinue(); await tester.pumpAndSettle(); - await registerPage.enterPassword(newUser.password); await registerPage.tapPasswordContinue(); await tester.pumpAndSettle(); - await registerPage.enterUsername(''); await registerPage.selectMonth(newUser.month); await registerPage.selectDay(newUser.day); await registerPage.selectYear(newUser.year); @@ -221,80 +121,61 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 2)); expect(registerPage.isUsernameEmptyErrorVisible(), true); - }); - - testWidgets('should show error when registering with empty month', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); + await goBackToOnboarding(); + }); - // Navigate to register + await tryTest('TC-AUTH-REGISTER-006 | error message when enter empty Month', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); await registerPage.enterEmail(newUser.email); await registerPage.tapContinue(); await tester.pumpAndSettle(); - await registerPage.enterPassword(newUser.password); await registerPage.tapPasswordContinue(); await tester.pumpAndSettle(); await registerPage.enterUsername(newUser.username); - // Skip month selection await registerPage.selectDay(newUser.day); await registerPage.selectYear(newUser.year); await registerPage.selectGender(newUser.gender); await registerPage.tapFinalContinue(); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(seconds: 2)); expect(registerPage.isDateOfBirthEmptyErrorVisible(), true); + await goBackToOnboarding(); }); - testWidgets('should show error when registering with empty day', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register + await tryTest('TC-AUTH-REGISTER-007 | error message when enter empty Day', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); await registerPage.enterEmail(newUser.email); await registerPage.tapContinue(); await tester.pumpAndSettle(); - await registerPage.enterPassword(newUser.password); await registerPage.tapPasswordContinue(); await tester.pumpAndSettle(); await registerPage.enterUsername(newUser.username); await registerPage.selectMonth(newUser.month); - // Skip day selection await registerPage.selectYear(newUser.year); await registerPage.selectGender(newUser.gender); await registerPage.tapFinalContinue(); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(seconds: 2)); expect(registerPage.isDateOfBirthEmptyErrorVisible(), true); + await goBackToOnboarding(); }); - testWidgets('should show error when registering with empty year', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register + + await tryTest('TC-AUTH-REGISTER-008 | error message when enter empty Year', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); await registerPage.enterEmail(newUser.email); await registerPage.tapContinue(); await tester.pumpAndSettle(); - await registerPage.enterPassword(newUser.password); await registerPage.tapPasswordContinue(); await tester.pumpAndSettle(); @@ -302,28 +183,21 @@ void main() { await registerPage.enterUsername(newUser.username); await registerPage.selectMonth(newUser.month); await registerPage.selectDay(newUser.day); - // Skip year selection await registerPage.selectGender(newUser.gender); await registerPage.tapFinalContinue(); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(seconds: 2)); expect(registerPage.isDateOfBirthEmptyErrorVisible(), true); + await goBackToOnboarding(); }); - testWidgets('should show error when registering with empty gender', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register + await tryTest('TC-AUTH-REGISTER-009 | error message when enter empty gender', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); await registerPage.enterEmail(newUser.email); await registerPage.tapContinue(); await tester.pumpAndSettle(); - await registerPage.enterPassword(newUser.password); await registerPage.tapPasswordContinue(); await tester.pumpAndSettle(); @@ -332,22 +206,14 @@ void main() { await registerPage.selectMonth(newUser.month); await registerPage.selectDay(newUser.day); await registerPage.selectYear(newUser.year); - // Skip gender selection await registerPage.tapFinalContinue(); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(seconds: 2)); expect(registerPage.isGenderEmptyErrorVisible(), true); + await goBackToOnboarding(); }); - - // ─── Age Restriction ─────────────────────────────────────────────────── - testWidgets('should show error when registering with age below 13', (tester) async { - app.main(); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - final registerPage = RegisterPage(tester); - - // Navigate to register + await tryTest('TC-AUTH-REGISTER-010 | error message if age < 13', () async { await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); await tester.pumpAndSettle(const Duration(seconds: 3)); @@ -363,143 +229,50 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 2)); expect(registerPage.isAgeRestrictionErrorVisible(), true); + await goBackToOnboarding(); + }); + + await tryTest('TC-AUTH-REGISTER-011 | error message when register with existing email', () async { + await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + await registerPage.register( + existingUser.email, + existingUser.password, + existingUser.username, + existingUser.month, + existingUser.day, + existingUser.year, + existingUser.gender, + ); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(registerPage.isAlreadyExistsErrorVisible(), true); + await goBackToOnboarding(); }); + await tryTest('TC-AUTH-REGISTER-012 | Register Successfully', () async { + await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); - // ─── Social Authentication (SKIPPED - Ready for Backend Integration) ──── - // testWidgets( - // 'should register successfully with Google authentication', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final registerPage = RegisterPage(tester); - - // // Navigate to sign-in page (social buttons are on sign-in page) - // await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Tap Google Sign-In button for registration - // await tester.tap(find.byKey(const Key(authSocialGoogleButton))); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Verify home page is reached (successful registration) - // expect(registerPage.isOnHomePage(), true); - // }, - // ); - - // testWidgets( - // 'should register successfully with Facebook authentication', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final registerPage = RegisterPage(tester); - - // // Navigate to sign-in page (social buttons are on sign-in page) - // await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Tap Facebook Sign-In button for registration - // await tester.tap(find.byKey(const Key(authSocialFacebookButton))); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Verify home page is reached (successful registration) - // expect(registerPage.isOnHomePage(), true); - // }, - // ); - - // testWidgets( - // 'should register successfully with Apple authentication', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final registerPage = RegisterPage(tester); - - // // Navigate to sign-in page (social buttons are on sign-in page) - // await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Tap Apple Sign-In button for registration - // await tester.tap(find.byKey(const Key(authSocialAppleButton))); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Verify home page is reached (successful registration) - // expect(registerPage.isOnHomePage(), true); - // }, - // ); - - // testWidgets( - // 'should handle Google authentication registration with invalid credentials', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final registerPage = RegisterPage(tester); - - // // Navigate to sign-in page - // await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Attempt Google Sign-In with invalid credentials - // await tester.tap(find.byKey(const Key(authSocialGoogleButton))); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Should show error or return to sign-in page - // expect( - // registerPage.isOnLoginPage() || find.text('Authentication failed').evaluate().isNotEmpty, - // true, - // ); - // }, - // ); - - // testWidgets( - // 'should handle Facebook authentication registration with invalid credentials', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final registerPage = RegisterPage(tester); - - // // Navigate to sign-in page - // await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Attempt Facebook Sign-In with invalid credentials - // await tester.tap(find.byKey(const Key(authSocialFacebookButton))); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Should show error or return to sign-in page - // expect( - // registerPage.isOnLoginPage() || find.text('Authentication failed').evaluate().isNotEmpty, - // true, - // ); - // }, - // ); - - // testWidgets( - // 'should handle Apple authentication registration with invalid credentials', - // (tester) async { - // app.main(); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // final registerPage = RegisterPage(tester); - - // // Navigate to sign-in page - // await tester.tap(find.byKey(const Key(onboardingCreateAccountButton))); - // await tester.pumpAndSettle(const Duration(seconds: 3)); - - // // Attempt Apple Sign-In with invalid credentials - // await tester.tap(find.byKey(const Key(authSocialAppleButton))); - // await tester.pumpAndSettle(const Duration(seconds: 5)); - - // // Should show error or return to sign-in page - // expect( - // registerPage.isOnLoginPage() || find.text('Authentication failed').evaluate().isNotEmpty, - // true, - // ); - // }, - // ); + await registerPage.register( + newUser.email, + newUser.password, + newUser.username, + newUser.month, + newUser.day, + newUser.year, + newUser.gender, + ); + await tester.pumpAndSettle(const Duration(seconds: 5)); + //expect(registerPage.isOnValidEmailPage(), true); + }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } }); -} +} \ No newline at end of file diff --git a/integration_test/tests/M2_Profile/profile_test.dart b/integration_test/tests/M2_Profile/profile_test.dart new file mode 100644 index 00000000..3066240c --- /dev/null +++ b/integration_test/tests/M2_Profile/profile_test.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M2_Profile/profile_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M2 - Profile - all scenarios', (tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + await tester.pumpAndSettle(const Duration(seconds: 5)); + + // ── Login first ── + final loginPage = LoginPage(tester); + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + expect(loginPage.isOnHomePage(), true); + + final profilePage = ProfilePage(tester); + + await tryTest('TC-PROFILE-001 | Navigate to profile page successfully', () async { + await profilePage.goToLibraryTab(); + await profilePage.tapProfileAvatar(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(profilePage.isOnProfilePage(), true); + expect(profilePage.isProfileDataVisible(), true); + }); + + await tryTest('TC-PROFILE-002 | Edit profile data successfully', () async { + await profilePage.tapEdit(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(profilePage.isOnEditProfilePage(), true); + + await profilePage.enterName(profileEditNewName); + await profilePage.enterUsername(profileEditUserName); + await profilePage.enterCity(profileEditNewCity); + await profilePage.enterFirstname(profileEditFirstName); + await profilePage.enterLastname(profileEditLastName); + + // await profilePage.tapCountryField(); + // await tester.pump(const Duration(milliseconds: 500)); + // await profilePage.selectCountry(profileEditNewCountryName); + // await tester.tap(find.byType(Scaffold)); + // await tester.pumpAndSettle(); + + // await profilePage.tapBioField(); + // await tester.pumpAndSettle(); + // await profilePage.enterBio(profileEditNewBio); + // await profilePage.tapBioDone(); + + await profilePage.tapSave(); + await tester.pumpAndSettle(const Duration(seconds: 4)); + + expect(profilePage.isOnProfilePage(), true); + await profilePage.scrollUpUntilVisible(publicProfileBackButton); + await tester.pumpAndSettle(const Duration(seconds: 4)); + expect(profilePage.isProfileNameVisible(profileEditNewName), true); + }); + + await tryTest('TC-PROFILE-003 | Continue Editing Scenario', () async { + await profilePage.tapEdit(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await profilePage.enterName('Continue editing test'); + + await profilePage.tapBack(); + await tester.pumpAndSettle(); + expect(profilePage.isUnsavedChangesDialogVisible(), true); + + await profilePage.tapContinueEditing(); + await tester.pumpAndSettle(); + await profilePage.enterName(profileEditNewName); + await profilePage.tapSave(); + await tester.pumpAndSettle(const Duration(seconds: 4)); + + expect(profilePage.isOnProfilePage(), true); + expect(profilePage.isProfileNameVisible(profileEditNewName), true); + }); + + await tryTest('TC-PROFILE-004 | Discard Changes Scenario', () async { + await profilePage.tapEdit(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await profilePage.enterName('Discard changes test'); + + await profilePage.tapBack(); + await tester.pumpAndSettle(); + expect(profilePage.isUnsavedChangesDialogVisible(), true); + + await profilePage.tapDiscardChanges(); + await tester.pumpAndSettle(); + expect(profilePage.isOnProfilePage(), true); + expect(profilePage.isProfileNameVisible(profileEditNewName), true); + }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} diff --git a/integration_test/tests/M4_Track/Track_test.dart b/integration_test/tests/M4_Track/Track_test.dart new file mode 100644 index 00000000..64ba79c0 --- /dev/null +++ b/integration_test/tests/M4_Track/Track_test.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M4_Track/Track_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M3 - TRACK - all scenarios',(tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + await tester.pumpAndSettle(const Duration(seconds: 5)); + + final loginPage = LoginPage(tester); + final trackPage = TrackPage(tester); + + // ── Login ────────────────────────────────────────────────────────────── + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + await tryTest('TC-TRACK-001 | Play a track from the Hot For You section', () async { + await trackPage.playFromHotForYou(); + }); + + await tryTest('TC-TRACK-002 | Open the full player via the mini player bar', () async { + await trackPage.openMiniPlayer(); + await tester.pump(const Duration(seconds: 3)); + }); + + await tryTest('TC-TRACK-003 | Navigate to Behind the Track', () async { + await trackPage.tapBehindThisTrack(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + await tryTest('TC-TRACK-004 | check visibility (cover art + title + artist)', () async { + expect(trackPage.isTrackInfoVisible(), true); + }); + + await tryTest('TC-TRACK-005 | Action bar buttons are clickable (like/repost/comments/3-dots + play)', () async { + await trackPage.tapLike(); + await trackPage.tapRepost(); + await trackPage.tapComment(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await trackPage.returnToBehindTheTrack(); + await trackPage.tapMore(); + await trackPage.dragToCloseMoreOptions(); + await tester.pumpAndSettle(Duration(seconds: 2)); + }); + + await tryTest('TC-TRACK-006 | Stop and play the track from the player button', () async { + await trackPage.tapPlayPause(); // pause + await tester.pump(const Duration(seconds: 1)); + await trackPage.tapPlayPause(); // play + await tester.pump(const Duration(seconds: 1)); + }); + + await tryTest('TC-TRACK-007 | "Show more" opens description sheet', () async { + if (trackPage.isShowMoreVisible()) { + await trackPage.tapShowMore(); + expect(trackPage.isDescriptionSheetVisible(), true); + await trackPage.closeDescriptionSheet(); + } + }); + + await tryTest('TC-TRACK-008 | Tags row is visible and horizontally scrollable', () async { + if (trackPage.isTagsVisible()) { + await trackPage.scrollTagsHorizontally(); + } + }); + + await tryTest('TC-TRACK-009 | scroll successfully in behind the track page',()async{ + await trackPage.scrollToFansLeaderboard(); + expect(trackPage.isFansLeaderboardVisible(), true); + + }); + + await tryTest('TC-TRACK-010 | Fans leaderboard clickable',()async{ + await trackPage.tapTopSegment(); + await trackPage.tapFirstSegment(); + }); + + await tryTest('TC-TRACK-011 | Follow button is clickable & click Artist direct to artist profile page', () async { + await trackPage.scrollUp(); + await trackPage.tapFollowButton(); + await tester.pump(const Duration(seconds: 1)); + await trackPage.tapArtist(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(trackPage.isOnArtistPage(), true); + }); + + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} diff --git a/integration_test/tests/M5_Player/Player_test.dart b/integration_test/tests/M5_Player/Player_test.dart new file mode 100644 index 00000000..4944f80b --- /dev/null +++ b/integration_test/tests/M5_Player/Player_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M5_Player/Player_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M5 - PLAYER - All scenarios', (tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + await tester.pumpAndSettle(const Duration(seconds: 5)); + + final loginPage = LoginPage(tester); + final playerPage = PlayerPage(tester); + + // ── Login ────────────────────────────────────────────────────────────────── + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + + await tryTest ('Play a track & open player page', () async { + await playerPage.playFromHotForYou(); + expect(playerPage.isMiniPlayerVisible(), true); + await playerPage.openFullPlayer(); + await tester.pump(const Duration(seconds: 3)); + }); + + await tryTest('Check visibility', () async { + expect(playerPage.isTrackInfoVisible(), true); + expect(playerPage.isCollapseVisible(), true); + expect(playerPage.isFollowVisible(), true); + expect(playerPage.isActionBarVisible(), true); + }); + + await tryTest('Check Follow & collapse & like button tappble', () async { + await playerPage.tapFollowButton(); + await tester.pump(const Duration(seconds: 3)); + await playerPage.tapCollapseButton(); + await tester.pump(const Duration(seconds: 3)); + await playerPage.openFullPlayer(); + await tester.pump(const Duration(seconds: 3)); + await playerPage.tapLikeButton(); + await tester.pump(const Duration(seconds: 3)); + }); + + await tryTest('Play & Pause track without crashing', () async { + await playerPage.tapPlayPause(); // pause + await tester.pump(const Duration(seconds: 1)); + await playerPage.tapPlayPause(); // resume + await tester.pump(const Duration(seconds: 1)); + }); + + // ── TC-PLAYER-006 | Drag WaveForm Smoothly ─── + // await tryTest('Drag WaveForm Smoothly', () async { + // await tester.pump(const Duration(seconds: 2)); + // // The waveform timestamp is drawn on canvas (not a Text widget), + // // so we verify the drag completes without throwing. + // await playerPage.dragWaveformSmoothly(pixels: 120); + // await tester.pump(const Duration(seconds: 1)); + // }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} diff --git a/integration_test/tests/M6_Playlist/Playlist_test.dart b/integration_test/tests/M6_Playlist/Playlist_test.dart new file mode 100644 index 00000000..2ea32d47 --- /dev/null +++ b/integration_test/tests/M6_Playlist/Playlist_test.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M6_Playlist/Playlist_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M6 - PLAYLIST | all scenarios', (tester) async { + app.main(); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + await tester.pumpAndSettle(const Duration(seconds: 5)); + + final loginPage = LoginPage(tester); + final playlistPage = PlaylistPage(tester); + + // ── Login ──────────────────────────────────────────────────────────────── + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + await tryTest('TC-PLAYLIST-001 | Navigate Library → Playlists', () async { + await playlistPage.goToLibraryTab(); + await playlistPage.goToPlaylistsSection(); + expect(playlistPage.isOnPlaylistsListScreen(), true, + reason: 'Should land on the Playlists list screen'); + }); + + await tryTest('TC-PLAYLIST-002 | Filter playlists', () async { + await playlistPage.tapPlaylistFilterButton(); + await playlistPage.tapPlaylistFilterOptionFirstAdded(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await playlistPage.tapPlaylistFilterButton(); + await playlistPage.tapPlaylistFilterOptionRecentlyUpdated(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await playlistPage.tapPlaylistFilterButton(); + await playlistPage.tapPlaylistFilterOptionPlaylistName(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await playlistPage.tapPlaylistFilterButton(); + await playlistPage.tapPlaylistFilterOptionRecentlyUpdated(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await playlistPage.tapPlaylistFilterButton(); + await playlistPage.tapPlaylistFilterOptionLikedPlayists(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await playlistPage.tapPlaylistFilterButton(); + await playlistPage.tapPlaylistFilterOptionOwnedPlayists(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await playlistPage.tapPlaylistFilterButton(); + await playlistPage.tapPlaylistFilterOptionAllPlayists(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + }); + + await tryTest('TC-PLAYLIST-003 | Create a new playlist', () async { + await playlistPage.tapCreateButton(); + await playlistPage.fillPlaylistName('Integration Test Playlist'); + await playlistPage.tapConfirmCreate(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + // expect(playlistPage.isOnPlaylistDetailScreen(), true, + // reason: 'Should navigate to the new playlist detail screen'); + // expect(playlistPage.isPlaylistNameVisible('Integration Test Playlist'), true, + // reason: 'New playlist name should appear in the header'); + }); + + await tryTest('TC-PLAYLIST-004 | Add a track from suggestions', () async { + await playlistPage.tapAddTrackButton(); + await playlistPage.tapSuggestionAddButtonByIndex(0); + await tester.pumpAndSettle(const Duration(seconds: 4)); + await playlistPage.tapSuggestionAddButtonByIndex(1); + await tester.pumpAndSettle(const Duration(seconds: 4)); + }); + + // ── Navigate back to the Playlists list ─────────────────────────────── + await tryTest('Navigate back to the Playlists list', () async { + await playlistPage.goBackFromPlaylistDetail(); + expect(playlistPage.isOnPlaylistsListScreen(), true, + reason: 'Should be back on the Playlists list'); + }); + + await tryTest('TC-PLAYLIST-005 | Edit — tap ⋮ on the playlist row in the list', () async { + await playlistPage.tapMoreForPlaylist('Integration Test Playlist'); + await playlistPage.tapOptionEdit(); + expect(playlistPage.isEditSheetOpen(), true, + reason: 'Edit sheet should open after tapping Edit'); + + await playlistPage.fillEditName('Edited Integration Playlist'); + await playlistPage.fillEditDescription('Integration test description'); + await playlistPage.tapEditPublicSwitch(); + //await playlistPage.removeTrack(0); + await playlistPage.tapSaveEdit(); + + await tester.pumpAndSettle(const Duration(seconds: 2)); + await playlistPage.pullToRefresh(libraryPlaylistsList); + }); + + await tryTest('Verify edits are reflected on the Playlists list', () async { + expect(playlistPage.isOnPlaylistsListScreen(), true, + reason: 'Should be on the Playlists list after saving'); + expect(playlistPage.isPlaylistNameVisible('Edited Integration Playlist'), true, + reason: 'Updated playlist name should appear in the list'); + + // // ── Navigate to the edited playlist detail to verify track removal ──── + // await playlistPage.tapPlaylistInList('Edited Integration Playlist'); + // expect(playlistPage.isOnPlaylistDetailScreen(), true, + // reason: 'Should open the playlist detail screen'); + // // need to be handled as the deleted track'name is not static and changes with every test run, so we check for the absence of the track name instead of a specific name + // // expect(playlistPage.isTrackVisible('Birds Of A Feather (Remix)'), false, + // // reason: 'Removed track should no longer appear in the detail'); + }); + + await tryTest('TC-PLAYLIST-006 | Convert playlist to Album successfully',() async{ + await playlistPage.pullToRefresh(libraryPlaylistsList);; + await tester.pumpAndSettle(const Duration(seconds: 3)); + await playlistPage.tapMoreForPlaylist('Edited Integration Playlist'); + await playlistPage.tapOptionEdit(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(playlistPage.isEditSheetOpen(), true, + reason: 'Edit sheet should open after tapping Edit'); + + await playlistPage.tapConvertToAlbum(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await playlistPage.cancelConvertToAlbum(); + + await playlistPage.tapConvertToAlbum(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await playlistPage.confirmConvertToAlbum(); + + await playlistPage.pullToRefresh(libraryPlaylistsList); + await playlistPage.goBackToLibrary(); + await playlistPage.goToAlbumsSection(); + expect (playlistPage.isAlbumNameVisible('Edited Integration Playlist'), true, + reason: 'Converted album should appear in the Albums section'); + }); + + await tryTest('TC-PLAYLIST-007 | Delete the playlist from its detail screen', () async { + await playlistPage.goBackToLibrary(); + await playlistPage.goToPlaylistsSection(); + await playlistPage.tapPlaylistInList('Testing playlist'); + await playlistPage.tapMoreOptions(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await playlistPage.tapOptionDelete(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await playlistPage.cancelDelete(); + await playlistPage.tapOptionDelete(); + await playlistPage.confirmDelete(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + await playlistPage.pullToRefresh(libraryPlaylistsList); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('TC-PLAYLIST-008 | Search for playlist track → visible', () async { + await playlistPage.typeInPlaylistSearch("spacetoon"); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(playlistPage.isTrackVisible("spacetoon"), true, + reason: 'Playlist should appear in search results'); + }); + + await tryTest('TC-PLAYLIST-009 | Search for non existing playlist → not visible', () async { + await playlistPage.typeInPlaylistSearch("non existing playlist"); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} diff --git a/integration_test/tests/M7_Feed/feed_test.dart b/integration_test/tests/M7_Feed/feed_test.dart new file mode 100644 index 00000000..e952d42b --- /dev/null +++ b/integration_test/tests/M7_Feed/feed_test.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M7_Feed/home_page.dart'; +import '../../pages/M7_Feed/feed_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets('M7 - Home Page - all scenarios', (tester) async { + final originalOnError = FlutterError.onError; + // FlutterError.onError = (FlutterErrorDetails details) { + // debugPrint('[Test] Suppressed: ${details.exception}'); + // }; + FlutterError.onError = (FlutterErrorDetails details) { + final String exception = details.exceptionAsString(); + if (exception.contains('Socket disconnected') || + exception.contains('io client disconnect')) { + debugPrint('[Test] Suppressed socket teardown: $exception'); + return; + } + debugPrint('[Test] Suppressed: $exception'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + app.main(); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + // ── Login first ── + final loginPage = LoginPage(tester); + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + expect(loginPage.isOnHomePage(), true); + + final homePage = HomePage(tester); + + await tryTest('TC-HOME-001 | Home tab is visible', () async { + expect(homePage.isHeaderVisible(), true); + }); + + await tryTest('TC-HOME-002 | Bottom nav bar is visible', () async { + expect(homePage.isAllNavTabsVisible(), true); + }); + + await tryTest('TC-HOME-003 | All header elements are visible', () async { + expect(homePage.isAllHeaderElementsVisible(), true); + }); + + await tryTest('TC-HOME-004 | Tap message button & notifications button', () async { + await homePage.tapMessageButton(); + await tester.pump(const Duration(seconds: 3)); + expect(homePage.isOnInboxPage(), true); + await tester.pageBack(); + await tester.pump(const Duration(seconds: 2)); + + await homePage.tapNotificationButton(); + await tester.pump(const Duration(seconds: 3)); + expect(homePage.isOnNotificationsPage(), true); + await tester.pageBack(); + await tester.pump(const Duration(seconds: 2)); + }); + + await tryTest('TC-HOME-005 | Trending by genre visible — tap genres one by one', () async { + await homePage.scrollDownUntilVisible(trendingByGenreSection); + expect(homePage.isTrendingByGenreVisible(), true); + for (final genre in ['Hip-Hop', 'Arabic Pop', 'Arabic Trap', 'R&B', 'Arabic Rock', 'Pop', 'Alternative Rock', 'Indie Rock']) { + await homePage.tapGenreTab(genre); + } + for (final genre in ['Alternative Rock', 'Pop', 'Arabic Rock', 'R&B', 'Arabic Trap', 'Arabic Pop', 'Hip-Hop', 'Indie Rock']) { + await homePage.tapGenreTab(genre); + } + expect(homePage.isTrendingByGenreVisible(), true); + }); + + await tryTest('TC-HOME-006 | User can scroll horizontally in the genre section', () async { + await homePage.scrollHorizontallyInSection(genreTabBar); + }); + + await tryTest('TC-HOME-007 | Hot For You section visible — play and stop track', () async { + await homePage.scrollDownUntilVisible(hotForYouSection); + expect(homePage.isActivityCardVisible(), true); + await homePage.tapByKeyNow(hotForYouPlayButton); + await tester.pump(const Duration(seconds: 1)); + await homePage.tapByKeyNow(hotForYouPlayButton); + await tester.pump(const Duration(seconds: 1)); + expect(homePage.isActionButtonVisible(), true); + await homePage.pauseIfPlaying(); + }); + + await tryTest('TC-HOME-008 | Mixed For You — scroll, open playlist, like, return', () async { + await homePage.scrollDownUntilVisible(mixedForYouSection); + expect(homePage.isMixedForYouVisible(), true); + await tester.pump(const Duration(seconds: 1)); + // Scroll only 100px so items remain at the list-view center for the tap. + await homePage.scrollHorizontallyInSection(mixedListView, -100); + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.byKey(const Key(mixedListView)), findsOneWidget, + reason: 'Mixed For You list must be loaded with data'); + await tester.tap(find.byKey(const Key(mixedListView)), warnIfMissed: false); + await homePage.settle(); + + await homePage.tapMixDetailLikeButton(); + await tester.pump(const Duration(milliseconds: 500)); + await homePage.pauseIfPlaying(); + await homePage.mixedForYouBackButton(); + await homePage.settle(); + }); + + await tryTest('TC-HOME-009 | Discover With Stations — scroll, open playlist, like, return', () async { + await homePage.scrollDownUntilVisible(discoverWithStationsSection); + expect(homePage.isDiscoverWithStationsVisible(), true); + await tester.pump(const Duration(seconds: 1)); + // Scroll only 100px so the first card stays at center for the tap. + await homePage.scrollHorizontallyInSection(discoverListView, -100); + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.byKey(const Key(discoverListView)), findsOneWidget, + reason: 'Discover With Stations list must be loaded with data'); + await tester.tap(find.byKey(const Key(discoverListView)), warnIfMissed: false); + await homePage.settle(); + + // Station route opens RelatedTracksScreen — use its like/back keys. + await homePage.tapRelatedTracksLikeButton(); + await tester.pump(const Duration(milliseconds: 500)); + await homePage.pauseIfPlaying(); + await homePage.relatedTracksBackButton(); + await homePage.settle(); + }); + + await tryTest('TC-HOME-010 | More of What You Like — scroll, open playlist, like, return', () async { + await homePage.scrollDownUntilVisible(moreOfWhatYouLikeSection); + expect(homePage.isMoreOfWhatYouLikeVisible(), true); + await tester.pump(const Duration(seconds: 1)); + // Scroll only 100px so the first card stays at center for the tap. + await homePage.scrollHorizontallyInSection(moreListView, -100); + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.byKey(const Key(moreListView)), findsOneWidget, + reason: 'More of What You Like list must be loaded with data'); + await tester.tap(find.byKey(const Key(moreListView)), warnIfMissed: false); + await homePage.settle(); + + // More of What You Like opens RelatedTracksScreen — use its like/back keys. + await homePage.tapRelatedTracksLikeButton(); + await tester.pump(const Duration(milliseconds: 500)); + await homePage.pauseIfPlaying(); + await homePage.relatedTracksBackButton(); + await homePage.settle(); + }); + + await homePage.pauseIfPlaying(); + + // ══ Feed Page test cases ═════════════════════════════════════════════ + final feedPage = FeedPage(tester); + await tester.pump(const Duration(seconds: 2)); + debugPrint('M7 - Feed Page - all scenarios'); + + await tryTest('TC-FEED-001 | Navigate to feed page successfully', () async { + await feedPage.tapFeedButton(); + await tester.pump(const Duration(seconds: 3)); + }); + + await tryTest('TC-FEED-002 | Tap following & discover buttons', () async { + await feedPage.tapFollowingButton(); + await tester.pump(const Duration(seconds: 3)); + await feedPage.tapDiscoverButton(); + await tester.pump(const Duration(seconds: 3)); + await feedPage.tapFollowingButton(); + await tester.pump(const Duration(seconds: 3)); + }); + + await tryTest('TC-FEED-003 | Tap like button', () async { + await feedPage.taplikeButton(); + await tester.pump(const Duration(milliseconds: 500)); + }); + + await tryTest('TC-FEED-004 | Tap comment button and navigate back', () async { + await feedPage.tapCommentButton(); + await tester.pump(const Duration(seconds: 2)); + await feedPage.closeComments(); + await tester.pump(const Duration(seconds: 1)); + }); + + await tryTest('TC-FEED-005 | Open full player via track card and collapse back', () async { + await feedPage.dragTrackCard(); + await tester.pump(const Duration(seconds: 3)); + expect(feedPage.playerTrack(), true); + await homePage.pauseIfPlaying(); + await feedPage.tapDragtButton(); + await tester.pump(const Duration(seconds: 2)); + }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} diff --git a/integration_test/tests/M8_Search/search_test.dart b/integration_test/tests/M8_Search/search_test.dart new file mode 100644 index 00000000..cc7fd0fe --- /dev/null +++ b/integration_test/tests/M8_Search/search_test.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M8_Search/search_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M8 - Search Page - all scenarios', (tester) async { + app.main(); + await tester.pumpAndSettle(const Duration(seconds: 5)); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + + // ── Login ──────────────────────────────────────────────────────────────── + final loginPage = LoginPage(tester); + final searchPage = SearchPage(tester); + + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + await tryTest('TC-SEARCH-001 | Navigate to Search tab', () async { + await searchPage.tapSearchNavButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(searchPage.isSearchScreenVisible(), true); + }); + + await tryTest('TC-SEARCH-002 | Scroll vibes section down then up', () async { + await searchPage.scrollVibesDown(); + await searchPage.scrollVibesUp(); + }); + + await tryTest('TC-SEARCH-003 | Tap a Vibe card → land on vibe detail screen', () async { + await searchPage.tapVibeCard('Alternative Rock'); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(searchPage.isVibePageScreenVisible(), true); + }); + + await tryTest('TC-SEARCH-004/005 | Tap all vibe detail tabs and scroll', () async { + await searchPage.tapTrendingVibeTab(); + await searchPage.scrollVibeDetailDown(); + await searchPage.scrollVibeDetailUp(); + + await searchPage.tapPlaylistsVibeTab(); + await searchPage.scrollVibeDetailDown(); + await searchPage.scrollVibeDetailUp(); + + await searchPage.tapAlbumsVibeTab(); + await searchPage.scrollVibeDetailDown(); + await searchPage.scrollVibeDetailUp(); + + await searchPage.tapAllVibeTab(); + await searchPage.scrollVibeDetailDown(); + await searchPage.scrollVibeDetailUp(); + + expect(searchPage.isVibePageScreenVisible(), true); + }); + + await tryTest('TC-SEARCH-006 | Back from vibe detail to main search screen', () async { + await searchPage.GenreBackButton(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(searchPage.isSearchScreenVisible(), true); + }); + + await tryTest('TC-SEARCH-007 | Suggestion list appears when typing in search bar', () async { + await searchPage.tapSearchBar(); + await searchPage.typeQuery('A'); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(searchPage.isSuggestionListVisible(), true, + reason: 'Typing "A" should surface a suggestion list'); + }); + + await tryTest('TC-SEARCH-008 | No results message appears for unlikely query', () async { + await searchPage.typeQuery('zhsyg'); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(searchPage.isNoResultsVisible(), true, + reason: 'Typing "zhsyg" should show "No results found" in suggestions'); + }); + + await tryTest('TC-SEARCH-009 | Search "Yo" and navigate to results screen', () async { + await searchPage.typeQuery('Yo'); + await searchPage.submitSearch(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(searchPage.isResultsScreenVisible(), true, + reason: 'Searching "Yo" should navigate to the results screen'); + }); + + await tryTest('TC-SEARCH-010 | Scroll results down and up', () async { + await searchPage.scrollResultsDown(); + await searchPage.scrollResultsUp(); + expect(searchPage.isResultsScreenVisible(), true); + }); + + await tryTest('TC-SEARCH-011 | Play a track from results and show mini player', () async { + await searchPage.tapFirstTrackInResults(); + // pump instead of pumpAndSettle — audio playback prevents the frame loop from settling + await tester.pump(const Duration(seconds: 2)); + expect(searchPage.isMiniPlayerVisible(), true, + reason: 'Mini player should appear after playing a track'); + }); + + await tryTest('TC-SEARCH-012/013 | Switch result tabs', () async { + await searchPage.tapTracksResultsTab(); + await searchPage.tapProfilesResultsTab(); + await searchPage.tapPlaylistsResultsTab(); + await searchPage.tapAlbumsResultsTab(); + await searchPage.scrollHorizontallyInSection(searchTabBar); + await searchPage.tapAllResultsTab(); + + // ─── TC-SEARCH-013 | Switch result tabs by swiping horizontally ────── + await searchPage.swipeResultsRight(); // Albums → Playlists + await searchPage.swipeResultsLeft(); // Playlists → Albums + await searchPage.swipeResultsLeft(); // All → Tracks + await searchPage.swipeResultsLeft(); // Tracks → Profiles + await searchPage.swipeResultsLeft(); // Profiles → Playlists + expect(searchPage.isResultsScreenVisible(), true); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + await tryTest('TC-SEARCH-014 | Clear search and return to main search screen', () async { + await searchPage.clearSearch(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(searchPage.isSearchScreenVisible(), true, + reason: 'Clearing search should return to the vibes screen'); + }); + + await searchPage.pausePlayback(); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} \ No newline at end of file diff --git a/integration_test/tests/M9_Comments/Comments_test.dart b/integration_test/tests/M9_Comments/Comments_test.dart new file mode 100644 index 00000000..7e860df4 --- /dev/null +++ b/integration_test/tests/M9_Comments/Comments_test.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M5_Player/Player_page.dart'; +import '../../pages/M9_Comments/comments_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('M9 - COMMENTS - All scenarios', (tester) async { + app.main(); + await tester.pumpAndSettle(const Duration(seconds: 5)); + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + + final loginPage = LoginPage(tester); + final playerPage = PlayerPage(tester); + final commentsPage = CommentsPage(tester); + + // ── Login ──────────────────────────────────────────────────────────── + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + + await tryTest('TC-COMMENTS-000 | Play a track and open full player immediately', () async { + await playerPage.playFromHotForYou(); + for (int i = 0; i < 15; i++) { + await tester.pump(const Duration(milliseconds: 200)); + if (playerPage.isMiniPlayerVisible()) break; + } + expect(playerPage.isMiniPlayerVisible(), true); + await playerPage.openFullPlayer(); + await tester.pump(const Duration(seconds: 3)); + }); + + await tryTest('TC-COMMENTS-001/002 | Send emojis and comment from floating comment bar', () async { + await commentsPage.sendEmojiFromFloatingBar('🔥'); + await commentsPage.sendEmojiFromFloatingBar('👏'); + await commentsPage.sendEmojiFromFloatingBar('🥺'); + await commentsPage.sendCommentFromFloatingBar('test comment'); + }); + + await tryTest('TC-COMMENTS-003 | Open Comments screen from player', () async { + await commentsPage.openFromPlayer(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(commentsPage.isCommentsScreenVisible(), true); + }); + + await tryTest('TC-COMMENTS-004 | All 4 comments visible', () async { + expect(commentsPage.isCommentVisible('🔥'), true); + expect(commentsPage.isCommentVisible('👏'), true); + expect(commentsPage.isCommentVisible('🥺'), true); + expect(commentsPage.isCommentVisible('test comment'), true); + }); + + await tryTest('TC-COMMENTS-005 | Like the first comment and verify UI update', () async { + await commentsPage.likeFirstComment(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(commentsPage.isLikedHeartVisible(), true, + reason: 'First comment should show a filled heart after liking'); + }); + + await tryTest('TC-COMMENTS-006 | Reply works successfully', () async { + await commentsPage.replyToFirstComment('reply test'); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(commentsPage.isCommentVisible('reply test'), true); + }); + + await tryTest('TC-COMMENTS-007 | Show less / Show replies toggle works', () async { + await tester.pumpAndSettle(const Duration(seconds: 1)); + if (commentsPage.isShowLessVisible()) { + await commentsPage.tapShowLess(); + expect(commentsPage.isShowLessVisible(), false, + reason: 'Replies should be collapsed after tapping "Show less"'); + + await commentsPage.tapShowReplies(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(commentsPage.isShowLessVisible(), true, + reason: 'Replies should be expanded after tapping "Show N replies"'); + } + }); + + await tryTest('TC-COMMENTS-008 | Tap "Play from" in comment ⋮ menu to seek the player', () async { + await commentsPage.scrollCommentsList(); + await commentsPage.tapMoreOnFirstComment(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(commentsPage.isPlayFromOptionVisible(), true, + reason: '"Play from X:XX" option must appear in the action sheet'); + await commentsPage.tapPlayFromOption(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(commentsPage.isCommentsScreenVisible(), true, + reason: 'Comments screen must stay open after seeking'); + }); + + await tryTest('TC-COMMENTS-009 | Comment ⋮ menu: Copy is tappable and View profile navigates correctly', () async { + await commentsPage.tapMoreOnFirstComment(); + expect(commentsPage.isBottomSheetOptionVisible('View profile'), true, + reason: '"View profile" must appear in the action sheet'); + await commentsPage.tapViewProfileOption(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(commentsPage.isProfilePageVisible(), true, + reason: 'Should navigate to profile after tapping "View profile"'); + await commentsPage.returnToCommentsScreen(); + expect(commentsPage.isCommentsScreenVisible(), true, + reason: 'Comments screen must be visible after returning from profile'); + }); + + await tryTest('TC-COMMENTS-010 | Comment ⋮ menu: Copy is tappable', () async { + await commentsPage.scrollCommentsList(); + await commentsPage.tapMoreOnFirstComment(); + expect(commentsPage.isBottomSheetOptionVisible('Copy'), true, + reason: '"Copy" must appear in the action sheet'); + await commentsPage.tapCopyOption(); + expect(commentsPage.isCommentsScreenVisible(), true, + reason: 'Comments screen must be visible after copying'); + }); + + // // ── TC-COMMENTS-011 | Delete the reply ─────────────────────────────── + // await tryTest('TC-COMMENTS-013 | Delete the reply and verify it is removed', () async { + // // Expand replies so the reply card is visible + // if (!commentsPage.isShowLessVisible()) { + // await commentsPage.tapShowReplies(); + // } + // // The reply is the last ⋮ in the expanded list + // await commentsPage.tapMoreOnLastVisible(); + // expect(commentsPage.isDeleteCommentOptionVisible(), true, + // reason: '"Delete comment" must appear for own reply'); + // await commentsPage.tapDeleteCommentOption(); + // expect(commentsPage.isCommentVisible('reply test'), false, + // reason: '"reply test" should no longer appear after deletion'); + // }); + + await tryTest('TC-COMMENTS-012 | Delete "test comment" and verify it is removed', () async { + await commentsPage.scrollCommentsList(); + await commentsPage.tapMoreOnFirstComment(); + expect(commentsPage.isDeleteCommentOptionVisible(), true, + reason: '"Delete comment" must appear for own comment'); + await commentsPage.tapDeleteCommentOption(); + expect(commentsPage.isCommentVisible('test comment'), false, + reason: '"test comment" should no longer appear after deletion'); + }); + + await tryTest('TC-COMMENTS-013 | Scroll the comments list', () async { + await commentsPage.scrollCommentsList(); + expect(commentsPage.isCommentsScreenVisible(), true, + reason: 'Screen must remain stable after scrolling'); + }); + + + await tryTest('TC-COMMENTS-014/015/016 | Sort comments by Newest, Oldest, Track Time', () async { + await commentsPage.tapSortIcon(); + await commentsPage.selectSortOption('Oldest'); + expect(commentsPage.isCommentsScreenVisible(), true); + + await commentsPage.tapSortIcon(); + await commentsPage.selectSortOption('Track Time'); + expect(commentsPage.isCommentsScreenVisible(), true); + + await commentsPage.tapSortIcon(); + await commentsPage.selectSortOption('Newest'); + expect(commentsPage.isCommentsScreenVisible(), true); + }); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }, + ); +} diff --git a/integration_test/tests/User_one_main_flow/one_main_flow_test.dart b/integration_test/tests/User_one_main_flow/one_main_flow_test.dart new file mode 100644 index 00000000..6092e907 --- /dev/null +++ b/integration_test/tests/User_one_main_flow/one_main_flow_test.dart @@ -0,0 +1,205 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:rythmify/main.dart' as app; +import '../../pages/M1_Authentication/login_page.dart'; +import '../../pages/M2_Profile/profile_page.dart'; +import '../../pages/M4_Track/Track_page.dart'; +import '../../pages/M5_Player/Player_page.dart'; +import '../../pages/M6_Playlist/Playlist_page.dart'; +import '../../pages/M7_Feed/feed_page.dart'; +import '../../pages/M7_Feed/home_page.dart'; +import '../../pages/M8_Search/search_page.dart'; +import '../../pages/M9_Comments/comments_page.dart'; +import '../../pages/M10_Notifications/notification_page.dart'; +import '../../pages/M13_Settings/settings_page.dart'; +import '../../pages/User_one_main_flow/one_main_flow_page.dart'; +import '../../pages/base_page.dart'; +import '../../fixtures/test_data.dart'; +import '../../selectors/selectors.dart'; + + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets('USER - ONE MAIN FLOW', (tester) async { + final originalOnError = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + debugPrint('[Test] Suppressed: ${details.exception}'); + }; + final List failures = []; + Future tryTest(String name, Future Function() body) async { + try { + await body(); + debugPrint('[PASS] $name'); + } catch (e) { + failures.add('❌ $name\n → $e'); + debugPrint('[FAIL] $name: $e'); + } + } + app.main(); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + final mainFlowPage = OneMainFlowPage(tester); + final loginPage = LoginPage(tester); + final profilePage = ProfilePage(tester); + final trackPage = TrackPage(tester); + final playerPage = PlayerPage(tester); + final playlistPage = PlaylistPage(tester); + final homePage = HomePage(tester); + final feedPage = FeedPage(tester); + final searchPage = SearchPage(tester); + final commentsPage = CommentsPage(tester); + final notificationsPage = NotificationsPage(tester); + final settingsPage = SettingsPage(tester); + final basePage = BasePage(tester); + + await tryTest('User log in to RYTHMIFY', () async { + await tester.tap(find.byKey(const Key(onboardingLoginButton))); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await loginPage.login(validEmail, validPassword); + await tester.pumpAndSettle(const Duration(seconds: 5)); + expect(loginPage.isOnHomePage(), true); + }); + + await tryTest('User play a track "Hot for you', () async { + await trackPage.playFromHotForYou(); + //await playerPage.openFullPlayer(); + //await tester.pump(const Duration (seconds: 3)); + }); + + // await tryTest('User like the track & write a comment', () async { + // await playerPage.tapLikeButton(); + // await tester.pump(const Duration (seconds: 2)); + // await commentsPage.sendCommentFromFloatingBar('testing comment'); + // await commentsPage.openFromPlayer(); + // await tester.pump(const Duration (seconds: 3)); + // expect(commentsPage.isCommentVisible('testing comment'), true); + // await mainFlowPage.tapCloseCommentsSection(); + // //await playerPage.tapPlayPause(); + // await tester.pump(const Duration (seconds: 2)); + // await playerPage.tapCollapseButton(); + // await tester.pump(const Duration (seconds: 2)); + // }); + + await tryTest('User wants to message a friend',() async { + await homePage.tapMessageButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await mainFlowPage.tapFriendChat('Yomna'); + await tester.pump(const Duration(seconds: 3)); + await mainFlowPage.writeMessage('Hello!! I found out an amazing song, listen to it!!'); + await tester.pump(const Duration(seconds: 3)); + await mainFlowPage.sendMessage(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await mainFlowPage.tapAddIcon(); + await tester.pump(const Duration(seconds: 3)); + await mainFlowPage.tapLikeTrack(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await mainFlowPage.tapDoneButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await mainFlowPage.sendMessage(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await tester.pageBack(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await tester.pageBack(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + await tryTest('User wants to check likes on his tracks', () async { + await homePage.tapNotificationButton(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await notificationsPage.tapFilterIcon(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await notificationsPage.tapFilterLikes(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await notificationsPage.waitForNotificationsToLoad(); + expect(notificationsPage.isFilterAppliedSuccessfully(), true, + reason: 'Filter should be applied — list or empty message visible'); + await tester.pageBack(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + await tryTest('User see what is new from feed page', () async { + await feedPage.tapFeedButton(); + await tester.pump(const Duration(seconds: 3)); + await mainFlowPage.swipeToNextTrack(); + // await tester.pump(const Duration(milliseconds: 500)); + await mainFlowPage.swipeToNextTrack(); + await tester.pump(const Duration(milliseconds: 500)); + await mainFlowPage.swipeToPreviousTrack(); + await tester.pump(const Duration(milliseconds: 500)); + }); + + await tryTest('User want to search a playlist & listen to it', () async { + await searchPage.tapSearchNavButton(); + await searchPage.typeQuery('my playlist'); + await searchPage.submitSearch(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(searchPage.isResultsScreenVisible(), true, + reason: 'Searching "my playlist" should navigate to the results screen'); + await searchPage.tapPlaylistsResultsTab(); + await mainFlowPage.tapPlaylist(0); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await mainFlowPage.playPlaylist(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await mainFlowPage.backToSearchResults(); + await searchPage.clearSearch(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); + + await tryTest('User want to create a new playlist with a track he founds', () async { + await searchPage.typeQuery('ياللي صاحي الليل'); + await searchPage.submitSearch(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect(searchPage.isResultsScreenVisible(), true, + reason: 'Searching "ياللي صاحي الليل" should navigate to the results screen'); + await searchPage.tapTracksResultsTab(); + await mainFlowPage.tapTrack(0); + await mainFlowPage.tapTrackMoreOptions(0); + await mainFlowPage.tapAddToPlaylist(); + await mainFlowPage.tapNewPlaylist(); + await mainFlowPage.writePlaylistName('Yarab teshta8al ana t3bt'); + await mainFlowPage.tapCreate(); + await mainFlowPage.tapDone(); + await searchPage.clearSearch(); + await tester.pumpAndSettle(const Duration(seconds : 5)); + await playlistPage.goToLibraryTab(); + await playlistPage.goToPlaylistsSection(); + await playlistPage.goBackToLibrary(); + }); + + await tryTest('User want edits his profile name', () async { + //await profilePage.goToLibraryTab(); + await profilePage.tapProfileAvatar(); + await profilePage.tapEdit(); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(profilePage.isOnEditProfilePage(), true); + await profilePage.enterName(profileEditNewName); + await profilePage.tapSave(); + await tester.pumpAndSettle(const Duration(seconds: 4)); + //await basePage.scrollUntilVisible(itemText: 'User test', scrollableKey: publicProfilePage); + await mainFlowPage.goBackFromProfile(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + }); + + await mainFlowPage.pauseTheTrack(); + + await tryTest('User finish all what he want and will sign out', () async { + //await profilePage.goToLibraryTab(); + await settingsPage.tapSettingsIcon(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + await settingsPage.tapSignOut(); + await settingsPage.tapOK(); + await tester.pumpAndSettle(const Duration(seconds: 3)); + expect (settingsPage.isOnBoardingPage(), true); + }); + + debugPrint('FEEDBACK: user: fantastic user experinece I will use it all the time!!!'); + + FlutterError.onError = originalOnError; + if (failures.isNotEmpty) { + final summary = failures.join('\n'); + debugPrint('\n══ TEST SUMMARY ══\n$summary'); + fail('${failures.length} test(s) failed:\n$summary'); + } + }); +} \ No newline at end of file diff --git a/lib/core/presentation/widgets/bottom_navigation.dart b/lib/core/presentation/widgets/bottom_navigation.dart index 1dc78cec..6546be45 100644 --- a/lib/core/presentation/widgets/bottom_navigation.dart +++ b/lib/core/presentation/widgets/bottom_navigation.dart @@ -71,24 +71,28 @@ class BottomNavigation extends StatelessWidget { ), BottomNavigationBarItem( + key: const Key('feed_nav_button'), icon: _inactiveIcon('feed_outlined'), activeIcon: _activeIcon('feed'), label: 'Feed', ), BottomNavigationBarItem( + key: const Key('search_nav_button'), icon: _inactiveIcon('search_outlined'), activeIcon: _activeIcon('search'), label: 'Search', ), BottomNavigationBarItem( + key: const Key('library_nav_button'), icon: _inactiveIcon('library_outlined'), activeIcon: _activeIcon('library'), label: 'Library', ), BottomNavigationBarItem( + key: const Key('upgrade_nav_button'), icon: _inactiveIcon('logo'), activeIcon: _activeIcon('logo'), label: 'Upgrade', diff --git a/lib/features/feed/presentation/widgets/discover_with_stations.dart b/lib/features/feed/presentation/widgets/discover_with_stations.dart index 9c251893..7cc0773b 100644 --- a/lib/features/feed/presentation/widgets/discover_with_stations.dart +++ b/lib/features/feed/presentation/widgets/discover_with_stations.dart @@ -55,6 +55,7 @@ class DiscoverWithStationsSection extends ConsumerWidget { return SizedBox( height: 200, child: ListView.separated( + key: const Key('discover_stations_list_view'), scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: items.length, diff --git a/lib/features/library/presentation/pages/following_page.dart b/lib/features/library/presentation/pages/following_page.dart index 0d7a9540..b87cc074 100644 --- a/lib/features/library/presentation/pages/following_page.dart +++ b/lib/features/library/presentation/pages/following_page.dart @@ -52,7 +52,15 @@ class _FollowingPageState extends ConsumerState { return Scaffold( backgroundColor: AppTheme.background, - appBar: AppBar(title: const Text('Following'), centerTitle: false), + appBar: AppBar( + title: const Text('Following'), + centerTitle: false, + leading: IconButton( + key: const Key('following_back_button'), + icon: const Icon(Icons.chevron_left), + onPressed: () => context.pop(), + ), + ), body: RefreshIndicator( color: AppTheme.primaryBrand, onRefresh: () => @@ -155,6 +163,7 @@ class _UserTile extends ConsumerWidget { final confirm = await showDialog( context: context, builder: (dialogContext) => AlertDialog( + key: const Key('unfollow_dialog'), backgroundColor: AppTheme.surface, title: const Text('Unfollow?', style: TextStyle(color: Colors.white)), content: Text( @@ -163,10 +172,12 @@ class _UserTile extends ConsumerWidget { ), actions: [ TextButton( + key: const Key('unfollow_dialog_cancel_button'), onPressed: () => Navigator.pop(dialogContext, false), child: Text('Cancel', style: AppTheme.labelLarge), ), TextButton( + key: const Key('unfollow_dialog_unfollow_button'), onPressed: () => Navigator.pop(dialogContext, true), child: Text( 'Unfollow', diff --git a/lib/features/library/presentation/pages/history_page.dart b/lib/features/library/presentation/pages/history_page.dart index f638ae0f..0b2f0e62 100644 --- a/lib/features/library/presentation/pages/history_page.dart +++ b/lib/features/library/presentation/pages/history_page.dart @@ -76,6 +76,7 @@ class _HistoryPageState extends ConsumerState { ), actions: [ TextButton( + key: const Key('clear_history_dialog_cancel_button'), onPressed: () { if (dialogContext.mounted) { Navigator.of(dialogContext).pop(false); @@ -84,6 +85,7 @@ class _HistoryPageState extends ConsumerState { child: Text('Cancel', style: AppTheme.labelLarge), ), TextButton( + key: const Key('clear_history_dialog_clear_button'), onPressed: () { if (dialogContext.mounted) { Navigator.of(dialogContext).pop(true); diff --git a/lib/features/library/presentation/pages/insights_page.dart b/lib/features/library/presentation/pages/insights_page.dart index 427893ee..c6e8e70e 100644 --- a/lib/features/library/presentation/pages/insights_page.dart +++ b/lib/features/library/presentation/pages/insights_page.dart @@ -45,6 +45,11 @@ class _InsightsPageState extends ConsumerState appBar: AppBar( title: const Text('Your insights'), centerTitle: false, + leading: IconButton( + key: const Key('insights_back_button'), + icon: const Icon(Icons.chevron_left), + onPressed: () => context.pop(), + ), actions: [ TextButton( key: const Key('insights_upgrade_pro_button'), diff --git a/lib/features/notifications/presentation/pages/notifications_screen.dart b/lib/features/notifications/presentation/pages/notifications_screen.dart index 8eee5102..dcb60635 100644 --- a/lib/features/notifications/presentation/pages/notifications_screen.dart +++ b/lib/features/notifications/presentation/pages/notifications_screen.dart @@ -53,7 +53,9 @@ class _NotificationScreenState extends ConsumerState { final state = ref.watch(notificationsProvider); return Scaffold( + key: const Key('notifications_screen'), appBar: AppBar( + leading: const BackButton(key: Key('notifications_back_button')), title: const Text('Notifications'), centerTitle: false, actions: [ diff --git a/lib/features/playlist/presentation/screens/library_albums_screen.dart b/lib/features/playlist/presentation/screens/library_albums_screen.dart index 72213df3..091d4ec9 100644 --- a/lib/features/playlist/presentation/screens/library_albums_screen.dart +++ b/lib/features/playlist/presentation/screens/library_albums_screen.dart @@ -221,15 +221,18 @@ class _LibraryAlbumsScreenState extends ConsumerState { ), ), ), - IconButton( - key: _filterIconKey, - icon: Icon( - Icons.tune, - color: _isFiltered - ? AppTheme.primaryBrand - : AppTheme.textSecondary, + KeyedSubtree( + key: const Key('library_albums_filter_button'), + child: IconButton( + key: _filterIconKey, + icon: Icon( + Icons.tune, + color: _isFiltered + ? AppTheme.primaryBrand + : AppTheme.textSecondary, + ), + onPressed: _toggleOverlay, ), - onPressed: _toggleOverlay, ), ], ), @@ -351,16 +354,19 @@ class _AlbumFilterDropdown extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ _DropdownItem( + key: const Key('library_albums_filter_recently_added'), label: 'Recently added', checked: sort == _AlbumSort.recentlyAdded, onTap: () => onSortChanged(_AlbumSort.recentlyAdded), ), _DropdownItem( + key: const Key('library_albums_filter_first_added'), label: 'First added', checked: sort == _AlbumSort.firstAdded, onTap: () => onSortChanged(_AlbumSort.firstAdded), ), _DropdownItem( + key: const Key('library_albums_filter_album_name'), label: 'Album name', checked: sort == _AlbumSort.albumName, onTap: () => onSortChanged(_AlbumSort.albumName), @@ -400,6 +406,7 @@ class _AlbumFilterDropdown extends StatelessWidget { class _DropdownItem extends StatelessWidget { const _DropdownItem({ + super.key, required this.label, required this.checked, required this.onTap, @@ -463,6 +470,7 @@ class _AlbumTile extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( + key: Key('album_tile_${album.id}_inkwell'), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), diff --git a/lib/features/playlist/presentation/screens/library_playlists_screen.dart b/lib/features/playlist/presentation/screens/library_playlists_screen.dart index a41cfa3b..85c52877 100644 --- a/lib/features/playlist/presentation/screens/library_playlists_screen.dart +++ b/lib/features/playlist/presentation/screens/library_playlists_screen.dart @@ -399,17 +399,20 @@ class _LibraryPlaylistsScreenState extends ConsumerState ), ), ), - IconButton( - key: _filterIconKey, - icon: Icon( - Icons.tune, - color: - (_sort != _SortOption.recentlyAdded || - _filter != _FilterOption.all) - ? AppTheme.primaryBrand - : AppTheme.textSecondary, + KeyedSubtree( + key: const Key('library_playlists_filter_button'), + child: IconButton( + key: _filterIconKey, + icon: Icon( + Icons.tune, + color: + (_sort != _SortOption.recentlyAdded || + _filter != _FilterOption.all) + ? AppTheme.primaryBrand + : AppTheme.textSecondary, + ), + onPressed: _toggleOverlay, ), - onPressed: _toggleOverlay, ), ], ), @@ -633,37 +636,44 @@ class _FilterDropdown extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ _DropdownItem( + key: const Key('library_playlists_filter_recently_added'), label: 'Recently added', checked: sort == _SortOption.recentlyAdded, onTap: () => onSortChanged(_SortOption.recentlyAdded), ), _DropdownItem( + key: const Key('library_playlists_filter_first_added'), label: 'First added', checked: sort == _SortOption.firstAdded, onTap: () => onSortChanged(_SortOption.firstAdded), ), _DropdownItem( + key: const Key('library_playlists_filter_recently_updated'), label: 'Recently updated', checked: sort == _SortOption.recentlyUpdated, onTap: () => onSortChanged(_SortOption.recentlyUpdated), ), _DropdownItem( + key: const Key('library_playlists_filter_playlist_name'), label: 'Playlist name', checked: sort == _SortOption.playlistName, onTap: () => onSortChanged(_SortOption.playlistName), ), const Divider(height: 1, thickness: 1, color: Color(0xFF3A3A3A)), _DropdownItem( + key: const Key('library_playlists_filter_all_playlists'), label: 'All playlists', checked: filter == _FilterOption.all, onTap: () => onFilterChanged(_FilterOption.all), ), _DropdownItem( + key: const Key('library_playlists_filter_liked_playlists'), label: 'Liked playlists', checked: filter == _FilterOption.liked, onTap: () => onFilterChanged(_FilterOption.liked), ), _DropdownItem( + key: const Key('library_playlists_filter_owned_playlists'), label: 'Owned playlists', checked: filter == _FilterOption.owned, onTap: () => onFilterChanged(_FilterOption.owned), @@ -677,6 +687,7 @@ class _FilterDropdown extends StatelessWidget { class _DropdownItem extends StatelessWidget { const _DropdownItem({ + super.key, required this.label, required this.checked, required this.onTap, diff --git a/lib/features/playlist/presentation/screens/library_stations_screen.dart b/lib/features/playlist/presentation/screens/library_stations_screen.dart index 18837b60..44f8ad1a 100644 --- a/lib/features/playlist/presentation/screens/library_stations_screen.dart +++ b/lib/features/playlist/presentation/screens/library_stations_screen.dart @@ -221,15 +221,18 @@ class _LibraryStationsScreenState extends ConsumerState { ), ), ), - IconButton( - key: _filterIconKey, - icon: Icon( - Icons.tune, - color: _isFiltered - ? AppTheme.primaryBrand - : AppTheme.textSecondary, + KeyedSubtree( + key: const Key('library_stations_filter_button'), + child: IconButton( + key: _filterIconKey, + icon: Icon( + Icons.tune, + color: _isFiltered + ? AppTheme.primaryBrand + : AppTheme.textSecondary, + ), + onPressed: _toggleOverlay, ), - onPressed: _toggleOverlay, ), ], ), @@ -341,16 +344,19 @@ class _StationFilterDropdown extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ _DropdownItem( + key: const Key('library_stations_filter_recently_added'), label: 'Recently added', checked: sort == _StationSort.recentlyAdded, onTap: () => onSortChanged(_StationSort.recentlyAdded), ), _DropdownItem( + key: const Key('library_stations_filter_first_added'), label: 'First added', checked: sort == _StationSort.firstAdded, onTap: () => onSortChanged(_StationSort.firstAdded), ), _DropdownItem( + key: const Key('library_stations_filter_station_name'), label: 'Station name', checked: sort == _StationSort.stationName, onTap: () => onSortChanged(_StationSort.stationName), @@ -365,6 +371,7 @@ class _StationFilterDropdown extends StatelessWidget { // ── Shared dropdown item ────────────────────────────────────────────────────── class _DropdownItem extends StatelessWidget { const _DropdownItem({ + super.key, required this.label, required this.checked, required this.onTap, @@ -428,6 +435,7 @@ class _StationTile extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( + key: Key('station_tile_${station.artistId}_inkwell'), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), diff --git a/lib/features/playlist/presentation/screens/playlist_detail_screen.dart b/lib/features/playlist/presentation/screens/playlist_detail_screen.dart index 18fb9858..4b559270 100644 --- a/lib/features/playlist/presentation/screens/playlist_detail_screen.dart +++ b/lib/features/playlist/presentation/screens/playlist_detail_screen.dart @@ -342,6 +342,7 @@ class _PlaylistDetailScreenState extends ConsumerState { // Suggestions — ONLY owned regular playlists if (showSuggestionsSection) ...[ Padding( + key: const Key('playlist_detail_add_track_button'), padding: const EdgeInsets.fromLTRB(16, 20, 16, 12), child: Text( 'Suggestions for your new playlist', diff --git a/lib/features/playlist/presentation/widgets/add_to_playlist_sheet.dart b/lib/features/playlist/presentation/widgets/add_to_playlist_sheet.dart index a3f0b180..f77b76c5 100644 --- a/lib/features/playlist/presentation/widgets/add_to_playlist_sheet.dart +++ b/lib/features/playlist/presentation/widgets/add_to_playlist_sheet.dart @@ -9,24 +9,6 @@ /// - Allows creating a new playlist directly from the sheet /// - Adds the selected track to all chosen playlists /// - Shows loading and success/error feedback -/// -/// Main Components: -/// - showAddToPlaylistSheet(): -/// Opens the modal bottom sheet. -/// -/// - _AddToPlaylistSheet: -/// Main stateful widget handling playlist loading, searching, -/// selection, and saving. -/// -/// - _PlaylistPickerRow: -/// Reusable row widget displaying playlist info with a selectable -/// checkbox. -/// -/// Dependencies: -/// - Riverpod providers -/// - Playlist datasource/provider -/// - PlaylistEntity model -/// - Shared playlist UI widgets library; import 'package:flutter/material.dart'; @@ -38,7 +20,6 @@ import '../../../../core/theme/app_theme.dart'; import 'playlist_shared_widgets.dart'; /// Shows the "Add to playlist" picker bottom sheet. -/// Call this after closing the track options sheet. Future showAddToPlaylistSheet( BuildContext context, { required String trackId, @@ -72,7 +53,6 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { @override void initState() { super.initState(); - // Use addPostFrameCallback to avoid modifying providers during build WidgetsBinding.instance.addPostFrameCallback((_) => _loadPlaylists()); } @@ -86,7 +66,6 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { if (!mounted) return; setState(() => _loading = true); try { - // Fetch directly from the datasource — avoids touching shared provider state final ds = ref.read(playlistDatasourceProvider); final playlists = await ds.fetchMyPlaylists(filter: 'created'); if (mounted) { @@ -147,12 +126,14 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { showDialog( context: context, builder: (ctx) => AlertDialog( + key: const Key('add_to_playlist_create_dialog'), backgroundColor: const Color(0xFF1E1E1E), title: const Text( 'New playlist', style: TextStyle(color: Colors.white), ), content: TextField( + key: const Key('add_to_playlist_new_name_field'), controller: controller, autofocus: true, style: const TextStyle(color: Colors.white), @@ -169,6 +150,7 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { ), actions: [ TextButton( + key: const Key('add_to_playlist_cancel_create_button'), onPressed: () => Navigator.of(ctx).pop(), child: const Text( 'Cancel', @@ -176,6 +158,7 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { ), ), TextButton( + key: const Key('add_to_playlist_confirm_create_button'), onPressed: () { final name = controller.text.trim(); Navigator.of(ctx).pop(); @@ -238,6 +221,7 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { final filtered = _filtered; return Container( + key: const Key('add_to_playlist_sheet'), height: MediaQuery.of(context).size.height * 0.88, decoration: const BoxDecoration( color: Color(0xFF1C1C1C), @@ -245,10 +229,9 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { ), child: Column( children: [ - // ── Handle ──────────────────────────────────────────── const BottomSheetHandle(), - // ── Search row ──────────────────────────────────────── + // Search row Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: Row( @@ -261,6 +244,7 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { borderRadius: BorderRadius.circular(20), ), child: TextField( + key: const Key('add_to_playlist_search_field'), controller: _searchController, onChanged: (v) => setState(() => _query = v), style: const TextStyle(color: Colors.white, fontSize: 14), @@ -290,8 +274,9 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { ), ), - // ── New playlist row ────────────────────────────────── + // New playlist row InkWell( + key: const Key('add_to_playlist_create_new_row'), onTap: _showCreateDialog, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), @@ -319,11 +304,12 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { const Divider(color: Colors.white12, height: 1), - // ── Playlist list ───────────────────────────────────── + // Playlist list Expanded( child: _loading ? const Center( child: CircularProgressIndicator( + key: Key('add_to_playlist_loading_indicator'), color: AppTheme.primaryBrand, strokeWidth: 2, ), @@ -339,12 +325,14 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { ), ) : ListView.builder( + key: const Key('add_to_playlist_listview'), padding: const EdgeInsets.only(bottom: 16), itemCount: filtered.length, itemBuilder: (context, index) { final playlist = filtered[index]; final isSelected = _selectedIds.contains(playlist.id); return _PlaylistPickerRow( + key: Key('playlist_item_${playlist.id}'), playlist: playlist, isSelected: isSelected, onTap: () => setState(() { @@ -357,7 +345,7 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { ), ), - // ── Fixed Done button ───────────────────────────────── + // Fixed Done button Container( color: const Color(0xFF1C1C1C), padding: EdgeInsets.fromLTRB(24, 12, 24, bottomInset + 20), @@ -366,6 +354,7 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { width: 160, height: 48, child: ElevatedButton( + key: const Key('add_to_playlist_done_button'), onPressed: _saving ? null : _onDone, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, @@ -381,6 +370,7 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { width: 20, height: 20, child: CircularProgressIndicator( + key: Key('add_to_playlist_saving_indicator'), color: Colors.black, strokeWidth: 2, ), @@ -402,9 +392,9 @@ class _AddToPlaylistSheetState extends ConsumerState<_AddToPlaylistSheet> { } } -// ── Playlist row with checkbox ──────────────────────────────────────────────── class _PlaylistPickerRow extends StatelessWidget { const _PlaylistPickerRow({ + super.key, required this.playlist, required this.isSelected, required this.onTap, @@ -451,6 +441,7 @@ class _PlaylistPickerRow extends StatelessWidget { ), // Animated checkbox AnimatedContainer( + key: Key('playlist_checkbox_${playlist.id}'), duration: const Duration(milliseconds: 150), width: 24, height: 24, diff --git a/lib/features/playlist/presentation/widgets/playlist_options_sheet.dart b/lib/features/playlist/presentation/widgets/playlist_options_sheet.dart index 9933f262..e20a18c3 100644 --- a/lib/features/playlist/presentation/widgets/playlist_options_sheet.dart +++ b/lib/features/playlist/presentation/widgets/playlist_options_sheet.dart @@ -93,6 +93,7 @@ class PlaylistOptionsSheet extends ConsumerWidget { borderRadius: BorderRadius.vertical(top: Radius.circular(14)), ), child: SingleChildScrollView( + key: const Key('playlist_options_scroll_view'), controller: scrollController, child: Column( mainAxisSize: MainAxisSize.min, @@ -320,6 +321,7 @@ class PlaylistOptionsSheet extends ConsumerWidget { ), actions: [ TextButton( + key: const Key('delete_playlist_cancel_button'), onPressed: () => Navigator.of(dialogContext).pop(), child: const Text( 'Cancel', @@ -327,6 +329,7 @@ class PlaylistOptionsSheet extends ConsumerWidget { ), ), TextButton( + key: const Key('delete_playlist_confirm_button'), onPressed: () { Navigator.of(dialogContext).pop(); Navigator.of(sheetContext).pop(); diff --git a/lib/features/profile/presentation/pages/edit_profile_page.dart b/lib/features/profile/presentation/pages/edit_profile_page.dart index a486435a..c4c1d79a 100644 --- a/lib/features/profile/presentation/pages/edit_profile_page.dart +++ b/lib/features/profile/presentation/pages/edit_profile_page.dart @@ -306,6 +306,7 @@ class _EditProfilePageState extends ConsumerState { borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (_) => ListView( + key: const Key('edit_profile_country_list_view'), children: _countries.entries .map( (entry) => ListTile( @@ -472,6 +473,7 @@ class _EditProfilePageState extends ConsumerState { ), ) : SingleChildScrollView( + key: const Key('edit_profile_scroll_view'), padding: const EdgeInsets.only(bottom: 174), child: Column( children: [ diff --git a/lib/features/track/presentation/widgets/track_details_section.dart b/lib/features/track/presentation/widgets/track_details_section.dart index 8af52ff4..5300aba6 100644 --- a/lib/features/track/presentation/widgets/track_details_section.dart +++ b/lib/features/track/presentation/widgets/track_details_section.dart @@ -147,6 +147,7 @@ class TrackDetailsSection extends ConsumerWidget { SizedBox( height: 32, child: ListView.separated( + key: const Key('behind_the_track_tags_list_view'), padding: const EdgeInsets.symmetric(horizontal: 20), scrollDirection: Axis.horizontal, itemCount: track.tags.length, diff --git a/lib/features/track_upload/presentation/screens/upload_track_screen.dart b/lib/features/track_upload/presentation/screens/upload_track_screen.dart index 6578ceb1..c9c2c18c 100644 --- a/lib/features/track_upload/presentation/screens/upload_track_screen.dart +++ b/lib/features/track_upload/presentation/screens/upload_track_screen.dart @@ -528,6 +528,7 @@ class _TrackInfoTabState extends ConsumerState<_TrackInfoTab> { width: double.infinity, height: 52, child: OutlinedButton( + key: const Key('uploads_delete_option'), style: OutlinedButton.styleFrom( side: const BorderSide(color: Colors.redAccent), shape: RoundedRectangleBorder( @@ -1236,6 +1237,7 @@ class _GenrePicker extends StatelessWidget { borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (_) => ListView.builder( + key: const Key('track_upload_genre_list_view'), itemCount: genres.length, itemBuilder: (_, i) { final genre = genres[i]; @@ -1264,6 +1266,7 @@ class _GenrePicker extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( + key: const Key('track_upload_genre_picker_gesture_detector'), onTap: () => _show(context), child: Container( padding: const EdgeInsets.symmetric(vertical: 12), diff --git a/pubspec.lock b/pubspec.lock index 9750de17..c83ad051 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1163,10 +1163,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -1824,26 +1824,26 @@ packages: dependency: transitive description: name: test - sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" + sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" url: "https://pub.dev" source: hosted - version: "1.29.0" + version: "1.30.0" test_api: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.10" test_core: dependency: transitive description: name: test_core - sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" + sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" url: "https://pub.dev" source: hosted - version: "0.6.15" + version: "0.6.16" timezone: dependency: transitive description: