diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/CodeViewModel.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/CodeViewModel.java index 4994f7a1..3260eb6f 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/CodeViewModel.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/CodeViewModel.java @@ -8,15 +8,22 @@ import com.d4rk.androidtutorials.java.ui.screens.android.repository.LessonRepository; public class CodeViewModel extends ViewModel { - private final MutableLiveData lesson = new MutableLiveData<>(); - private final LessonRepository repository = new LessonRepository(); - private final GetLessonUseCase getLessonUseCase = new GetLessonUseCase(repository); + private final MutableLiveData lesson = new MutableLiveData<>(); + private final GetLessonUseCase getLessonUseCase; + + public CodeViewModel() { + this(new GetLessonUseCase(new LessonRepository())); + } + + CodeViewModel(GetLessonUseCase getLessonUseCase) { + this.getLessonUseCase = getLessonUseCase; + } public void setLessonName(String lessonName) { lesson.setValue(getLessonUseCase.invoke(lessonName)); } - public LiveData getLesson() { + public LiveData getLesson() { return lesson; } } diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/android/CodeViewModelTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/android/CodeViewModelTest.java new file mode 100644 index 00000000..6f909973 --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/android/CodeViewModelTest.java @@ -0,0 +1,57 @@ +package com.d4rk.androidtutorials.java.ui.screens.android; + +import static org.junit.Assert.assertEquals; + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule; + +import com.d4rk.androidtutorials.java.data.repository.LessonRepository; +import com.d4rk.androidtutorials.java.domain.android.GetLessonUseCase; + +import org.junit.Rule; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +public class CodeViewModelTest { + + @Rule + public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule(); + + @Test + public void setLessonName_updatesLiveData() { + FakeLessonRepository repository = new FakeLessonRepository(); + LessonRepository.Lesson expected = new LessonRepository.Lesson(1, 2, 3); + repository.addLesson("lesson", expected); + CodeViewModel viewModel = new CodeViewModel(new GetLessonUseCase(repository)); + + viewModel.setLessonName("lesson"); + + assertEquals(expected, viewModel.getLesson().getValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void setLessonName_unknownLessonThrows() { + FakeLessonRepository repository = new FakeLessonRepository(); + CodeViewModel viewModel = new CodeViewModel(new GetLessonUseCase(repository)); + + viewModel.setLessonName("unknown"); + } + + private static final class FakeLessonRepository implements LessonRepository { + private final Map lessons = new HashMap<>(); + + void addLesson(String name, Lesson lesson) { + lessons.put(name, lesson); + } + + @Override + public Lesson getLesson(String lessonName) { + Lesson lesson = lessons.get(lessonName); + if (lesson == null) { + throw new IllegalArgumentException("Unknown lesson: " + lessonName); + } + return lesson; + } + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/help/HelpViewModelTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/help/HelpViewModelTest.java new file mode 100644 index 00000000..2631af9e --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/help/HelpViewModelTest.java @@ -0,0 +1,63 @@ +package com.d4rk.androidtutorials.java.ui.screens.help; + +import static org.junit.Assert.assertSame; + +import android.app.Activity; + +import com.d4rk.androidtutorials.java.data.repository.HelpRepository; +import com.d4rk.androidtutorials.java.domain.help.LaunchReviewFlowUseCase; +import com.d4rk.androidtutorials.java.domain.help.RequestReviewFlowUseCase; +import com.google.android.play.core.review.ReviewInfo; + +import org.junit.Test; +import org.mockito.Mockito; + +public class HelpViewModelTest { + + @Test + public void requestReviewFlowDelegatesToRepository() { + FakeHelpRepository repository = new FakeHelpRepository(); + HelpViewModel viewModel = new HelpViewModel( + new RequestReviewFlowUseCase(repository), + new LaunchReviewFlowUseCase(repository) + ); + HelpRepository.OnReviewInfoListener listener = Mockito.mock(HelpRepository.OnReviewInfoListener.class); + + viewModel.requestReviewFlow(listener); + + assertSame(listener, repository.lastListener); + } + + @Test + public void launchReviewFlowDelegatesToRepository() { + FakeHelpRepository repository = new FakeHelpRepository(); + HelpViewModel viewModel = new HelpViewModel( + new RequestReviewFlowUseCase(repository), + new LaunchReviewFlowUseCase(repository) + ); + Activity activity = Mockito.mock(Activity.class); + ReviewInfo info = Mockito.mock(ReviewInfo.class); + + viewModel.launchReviewFlow(activity, info); + + assertSame(activity, repository.lastActivity); + assertSame(info, repository.lastReviewInfo); + } + + private static final class FakeHelpRepository implements HelpRepository { + private HelpRepository.OnReviewInfoListener lastListener; + private Activity lastActivity; + private ReviewInfo lastReviewInfo; + + @Override + public void requestReviewFlow(OnReviewInfoListener listener) { + lastListener = listener; + } + + @Override + public void launchReviewFlow(Activity activity, ReviewInfo reviewInfo) { + lastActivity = activity; + lastReviewInfo = reviewInfo; + } + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeViewModelTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeViewModelTest.java index 7673d9da..d55d4292 100644 --- a/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeViewModelTest.java +++ b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeViewModelTest.java @@ -4,6 +4,9 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import android.content.Intent; +import android.net.Uri; + import androidx.arch.core.executor.testing.InstantTaskExecutorRule; import com.d4rk.androidtutorials.java.data.model.PromotedApp; @@ -16,7 +19,10 @@ import org.junit.Rule; import org.junit.Test; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class HomeViewModelTest { @@ -28,12 +34,7 @@ public class HomeViewModelTest { public void uiStateUpdatesWithData() { List promoted = List.of(new PromotedApp("App", "pkg", "icon")); FakeHomeRepository repo = new FakeHomeRepository(promoted); - HomeViewModel viewModel = new HomeViewModel( - new GetDailyTipUseCase(repo), - new GetPromotedAppsUseCase(repo), - new GetPlayStoreUrlUseCase(repo), - new GetAppPlayStoreUrlUseCase(repo) - ); + HomeViewModel viewModel = createViewModel(repo); viewModel.setAnnouncements("Title", "Subtitle"); HomeUiState state = viewModel.getUiState().getValue(); @@ -47,12 +48,7 @@ public void uiStateUpdatesWithData() { @Test public void uiStateHandlesEmptyPromotedApps() { FakeHomeRepository repo = new FakeHomeRepository(List.of()); - HomeViewModel viewModel = new HomeViewModel( - new GetDailyTipUseCase(repo), - new GetPromotedAppsUseCase(repo), - new GetPlayStoreUrlUseCase(repo), - new GetAppPlayStoreUrlUseCase(repo) - ); + HomeViewModel viewModel = createViewModel(repo); viewModel.setAnnouncements("Title", "Subtitle"); HomeUiState state = viewModel.getUiState().getValue(); @@ -60,26 +56,110 @@ public void uiStateHandlesEmptyPromotedApps() { assertTrue(state.promotedApps().isEmpty()); } - record FakeHomeRepository(List apps) implements HomeRepository { + @Test + public void promotedAppsLimitedToAtMostFour() { + List promoted = List.of( + new PromotedApp("App1", "pkg1", "icon1"), + new PromotedApp("App2", "pkg2", "icon2"), + new PromotedApp("App3", "pkg3", "icon3"), + new PromotedApp("App4", "pkg4", "icon4"), + new PromotedApp("App5", "pkg5", "icon5") + ); + FakeHomeRepository repo = new FakeHomeRepository(promoted); + HomeViewModel viewModel = createViewModel(repo); + + HomeUiState state = viewModel.getUiState().getValue(); + assertNotNull(state); + assertTrue(state.promotedApps().size() <= 4); + } + + @Test + public void getOpenPlayStoreIntent_buildsViewIntent() { + FakeHomeRepository repo = new FakeHomeRepository(List.of()); + String expectedUrl = "https://play.google.com/store/apps/details?id=com.example"; + repo.setPlayStoreUrl(expectedUrl); + HomeViewModel viewModel = createViewModel(repo); + + Intent intent = viewModel.getOpenPlayStoreIntent(); + + assertEquals(Intent.ACTION_VIEW, intent.getAction()); + assertEquals(Uri.parse(expectedUrl), intent.getData()); + } + + @Test + public void getPromotedAppIntent_buildsViewIntent() { + FakeHomeRepository repo = new FakeHomeRepository(List.of()); + String packageName = "pkg"; + String expectedUrl = "https://play.google.com/store/apps/details?id=" + packageName; + repo.setAppUrl(packageName, expectedUrl); + HomeViewModel viewModel = createViewModel(repo); + + Intent intent = viewModel.getPromotedAppIntent(packageName); + + assertEquals(Intent.ACTION_VIEW, intent.getAction()); + assertEquals(Uri.parse(expectedUrl), intent.getData()); + } + + @Test + public void getLearnMoreIntent_targetsAndroidDevelopers() { + FakeHomeRepository repo = new FakeHomeRepository(List.of()); + HomeViewModel viewModel = createViewModel(repo); + + Intent intent = viewModel.getLearnMoreIntent(); + + assertEquals(Intent.ACTION_VIEW, intent.getAction()); + assertEquals(Uri.parse("https://developer.android.com"), intent.getData()); + } + + private HomeViewModel createViewModel(FakeHomeRepository repo) { + return new HomeViewModel( + new GetDailyTipUseCase(repo), + new GetPromotedAppsUseCase(repo), + new GetPlayStoreUrlUseCase(repo), + new GetAppPlayStoreUrlUseCase(repo) + ); + } + + private static final class FakeHomeRepository implements HomeRepository { + private final List promotedApps; + private String dailyTip = "tip"; + private String playStoreUrl = "https://play.google.com/store/apps/details?id=default"; + private final Map appUrls = new HashMap<>(); + + FakeHomeRepository(List promotedApps) { + this.promotedApps = new ArrayList<>(promotedApps); + } + + void setDailyTip(String dailyTip) { + this.dailyTip = dailyTip; + } + + void setPlayStoreUrl(String playStoreUrl) { + this.playStoreUrl = playStoreUrl; + } + + void setAppUrl(String packageName, String url) { + appUrls.put(packageName, url); + } @Override public String dailyTip() { - return "tip"; + return dailyTip; } @Override public String getPlayStoreUrl() { - return ""; + return playStoreUrl; } @Override public String getAppPlayStoreUrl(String packageName) { - return ""; + return appUrls.getOrDefault(packageName, ""); } @Override public void fetchPromotedApps(PromotedAppsCallback callback) { - callback.onResult(apps); + callback.onResult(promotedApps); } } } diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/settings/SettingsViewModelTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/settings/SettingsViewModelTest.java new file mode 100644 index 00000000..92298b01 --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/settings/SettingsViewModelTest.java @@ -0,0 +1,156 @@ +package com.d4rk.androidtutorials.java.ui.screens.settings; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import android.content.SharedPreferences; + +import com.d4rk.androidtutorials.java.data.repository.SettingsRepository; +import com.d4rk.androidtutorials.java.domain.settings.ApplyConsentUseCase; +import com.d4rk.androidtutorials.java.domain.settings.GetDarkModeUseCase; +import com.d4rk.androidtutorials.java.domain.settings.OnPreferenceChangedUseCase; +import com.d4rk.androidtutorials.java.domain.settings.RegisterPreferenceChangeListenerUseCase; +import com.d4rk.androidtutorials.java.domain.settings.SetConsentAcceptedUseCase; +import com.d4rk.androidtutorials.java.domain.settings.UnregisterPreferenceChangeListenerUseCase; + +import org.junit.Test; + +public class SettingsViewModelTest { + + @Test + public void onPreferenceChangedDelegatesWhenKeyProvided() { + FakeSettingsRepository repository = new FakeSettingsRepository(); + repository.themeResult = true; + SettingsViewModel viewModel = createViewModel(repository); + + boolean result = viewModel.onPreferenceChanged("theme"); + + assertTrue(result); + assertEquals("theme", repository.lastChangedKey); + assertTrue(repository.applyThemeCalled); + } + + @Test + public void onPreferenceChangedReturnsFalseWhenKeyNull() { + FakeSettingsRepository repository = new FakeSettingsRepository(); + SettingsViewModel viewModel = createViewModel(repository); + + boolean result = viewModel.onPreferenceChanged(null); + + assertFalse(result); + assertEquals(0, repository.changeCount); + } + + @Test + public void applyConsentDelegatesToRepository() { + FakeSettingsRepository repository = new FakeSettingsRepository(); + SettingsViewModel viewModel = createViewModel(repository); + + viewModel.applyConsent(); + + assertTrue(repository.applyConsentCalled); + } + + @Test + public void registerPreferenceChangeListenerDelegates() { + FakeSettingsRepository repository = new FakeSettingsRepository(); + SettingsViewModel viewModel = createViewModel(repository); + SharedPreferences.OnSharedPreferenceChangeListener listener = (prefs, key) -> { }; + + viewModel.registerPreferenceChangeListener(listener); + + assertSame(listener, repository.registeredListener); + } + + @Test + public void unregisterPreferenceChangeListenerDelegates() { + FakeSettingsRepository repository = new FakeSettingsRepository(); + SettingsViewModel viewModel = createViewModel(repository); + SharedPreferences.OnSharedPreferenceChangeListener listener = (prefs, key) -> { }; + + viewModel.unregisterPreferenceChangeListener(listener); + + assertSame(listener, repository.unregisteredListener); + } + + @Test + public void getDarkModeReturnsRepositoryValue() { + FakeSettingsRepository repository = new FakeSettingsRepository(); + repository.darkMode = "dark"; + SettingsViewModel viewModel = createViewModel(repository); + + assertEquals("dark", viewModel.getDarkMode()); + } + + @Test + public void setConsentAcceptedDelegates() { + FakeSettingsRepository repository = new FakeSettingsRepository(); + SettingsViewModel viewModel = createViewModel(repository); + + viewModel.setConsentAccepted(true); + + assertTrue(repository.consentAccepted != null && repository.consentAccepted); + } + + private SettingsViewModel createViewModel(FakeSettingsRepository repository) { + return new SettingsViewModel( + new OnPreferenceChangedUseCase(repository), + new RegisterPreferenceChangeListenerUseCase(repository), + new UnregisterPreferenceChangeListenerUseCase(repository), + new GetDarkModeUseCase(repository), + new SetConsentAcceptedUseCase(repository), + new ApplyConsentUseCase(repository) + ); + } + + private static final class FakeSettingsRepository implements SettingsRepository { + private String lastChangedKey; + private int changeCount; + private boolean applyThemeCalled; + private boolean themeResult; + private boolean applyConsentCalled; + private SharedPreferences.OnSharedPreferenceChangeListener registeredListener; + private SharedPreferences.OnSharedPreferenceChangeListener unregisteredListener; + private String darkMode = "system"; + private Boolean consentAccepted; + + @Override + public void handlePreferenceChange(String key) { + changeCount++; + lastChangedKey = key; + } + + @Override + public boolean applyTheme() { + applyThemeCalled = true; + return themeResult; + } + + @Override + public void applyConsent() { + applyConsentCalled = true; + } + + @Override + public void registerPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + registeredListener = listener; + } + + @Override + public void unregisterPreferenceChangeListener(SharedPreferences.OnSharedPreferenceChangeListener listener) { + unregisteredListener = listener; + } + + @Override + public String getDarkMode() { + return darkMode; + } + + @Override + public void setConsentAccepted(boolean accepted) { + consentAccepted = accepted; + } + } +} diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/support/SupportViewModelTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/support/SupportViewModelTest.java new file mode 100644 index 00000000..19231d48 --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/support/SupportViewModelTest.java @@ -0,0 +1,118 @@ +package com.d4rk.androidtutorials.java.ui.screens.support; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.d4rk.androidtutorials.java.data.repository.SupportRepository; +import com.d4rk.androidtutorials.java.domain.support.InitBillingClientUseCase; +import com.d4rk.androidtutorials.java.domain.support.InitMobileAdsUseCase; +import com.d4rk.androidtutorials.java.domain.support.InitiatePurchaseUseCase; +import com.d4rk.androidtutorials.java.domain.support.QueryProductDetailsUseCase; +import com.google.android.gms.ads.AdRequest; + +import org.junit.Test; + +import java.util.List; + +public class SupportViewModelTest { + + @Test + public void initBillingClientDelegatesToRepository() { + FakeSupportRepository repository = new FakeSupportRepository(); + SupportViewModel viewModel = createViewModel(repository); + Runnable onConnected = () -> { }; + + viewModel.initBillingClient(onConnected); + + assertSame(onConnected, repository.lastOnConnected); + } + + @Test + public void queryProductDetailsDelegatesToRepository() { + FakeSupportRepository repository = new FakeSupportRepository(); + SupportViewModel viewModel = createViewModel(repository); + List productIds = List.of("id1", "id2"); + SupportRepository.OnProductDetailsListener listener = productDetails -> { }; + + viewModel.queryProductDetails(productIds, listener); + + assertEquals(productIds, repository.lastProductIds); + assertSame(listener, repository.lastProductDetailsListener); + } + + @Test + public void initiatePurchaseReturnsLauncherFromRepository() { + FakeSupportRepository repository = new FakeSupportRepository(); + SupportViewModel viewModel = createViewModel(repository); + repository.setBillingFlowLauncher(activity -> { }); + + SupportRepository.BillingFlowLauncher launcher = viewModel.initiatePurchase("donation"); + + assertEquals("donation", repository.lastProductId); + assertSame(repository.billingFlowLauncher, launcher); + } + + @Test + public void initMobileAdsReturnsRepositoryRequest() { + FakeSupportRepository repository = new FakeSupportRepository(); + SupportViewModel viewModel = createViewModel(repository); + AdRequest adRequest = new AdRequest.Builder().build(); + repository.setAdRequest(adRequest); + + AdRequest result = viewModel.initMobileAds(); + + assertTrue(repository.initMobileAdsCalled); + assertSame(adRequest, result); + } + + private SupportViewModel createViewModel(FakeSupportRepository repository) { + return new SupportViewModel( + new InitBillingClientUseCase(repository), + new QueryProductDetailsUseCase(repository), + new InitiatePurchaseUseCase(repository), + new InitMobileAdsUseCase(repository) + ); + } + + private static final class FakeSupportRepository implements SupportRepository { + private Runnable lastOnConnected; + private List lastProductIds; + private OnProductDetailsListener lastProductDetailsListener; + private String lastProductId; + private BillingFlowLauncher billingFlowLauncher = activity -> { }; + private AdRequest adRequest = new AdRequest.Builder().build(); + private boolean initMobileAdsCalled; + + void setBillingFlowLauncher(BillingFlowLauncher billingFlowLauncher) { + this.billingFlowLauncher = billingFlowLauncher; + } + + void setAdRequest(AdRequest adRequest) { + this.adRequest = adRequest; + } + + @Override + public void initBillingClient(Runnable onConnected) { + lastOnConnected = onConnected; + } + + @Override + public void queryProductDetails(List productIds, OnProductDetailsListener listener) { + lastProductIds = productIds; + lastProductDetailsListener = listener; + } + + @Override + public BillingFlowLauncher initiatePurchase(String productId) { + lastProductId = productId; + return billingFlowLauncher; + } + + @Override + public AdRequest initMobileAds() { + initMobileAdsCalled = true; + return adRequest; + } + } +}