diff --git a/.github/workflows/unit_testing.yml b/.github/workflows/unit_testing.yml new file mode 100644 index 0000000..13e9c59 --- /dev/null +++ b/.github/workflows/unit_testing.yml @@ -0,0 +1,47 @@ +name: Android CI + +on: + push: + branches: + - updated-version + paths-ignore: + - 'README.md' + - 'TEST_CASE.md' + pull_request: + paths-ignore: + - 'README.md' + - 'TEST_CASE.md' + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Grant execute permission for Gradle wrapper + run: chmod +x gradlew + + - name: Inject MAPS_API_KEY into local.properties + run: echo "MAPS_API_KEY=${{ secrets.MAPS_API_KEY }}" >> local.properties + + - name: Run unit tests + run: ./gradlew test --stacktrace --info + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-reports + path: app/build/reports/tests/testDebugUnitTest/ + + - name: Build with Gradle Wrapper + run: ./gradlew assembleDebug --stacktrace \ No newline at end of file diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml new file mode 100644 index 0000000..9401895 --- /dev/null +++ b/.idea/androidTestResultsUserPreferences.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..2ccb0e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,45 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..b589d56 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..acea94c --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c694954..33e6983 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,16 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..f8467b4 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index f9e3101..8b9cfd6 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,8 +1,14 @@ - - + diff --git a/README.md b/README.md index 8a75b3b..305d737 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,90 @@ -# StoryAppSub2 - Submission 2 for Intermediate Android Dicoding Course +# Story App 📖 + +![Header](assets/logo/feature_graphic.png) +![Unit Testing](https://github.com/waffiqaziz/story-app/actions/workflows/unit_testing.yml/badge.svg) + +**Story App** is an Android application developed for **educational purposes only**. It allows users +to create an account, log in, post stories with pictures from their gallery or camera, add +locations, share activities, and explore other people's stories. The app is designed for learning +and no commercial use. + +## Features 🌟 + +- **Create Account**: Sign up and join the app community. +- **Login**: Securely log in to your account. +- **Post a Story**: Share your stories with a title, description, and pictures from the gallery or + camera. +- **Share Location**: Add location to your posts so others can see you on the map. +- **Browse Other Stories**: Discover and engage with stories posted by other users. + +## Installation 🛠️ + +Follow these steps to install the app on your Android device or emulator: + +1. **Clone the repository**: + ```bash + git clone https://github.com/waffiqaziz/story-app.git + ``` +2. Open in Android Studio: Open the project in Android Studio. +3. Sync Gradle Files: Android Studio will automatically sync the necessary Gradle files. +4. Setup your `MAPS_API_KEY`, follow this + instruction [here](https://developers.google.com/maps/documentation/android-sdk/get-api-key). + After you got the key put in `local.properties` +5. Run the App: Select a physical device or emulator, and click the "Run" button in Android Studio. + +## Screenshots 📸 + +Splash Screen  +SignIn Page  +Register Page  +Main Page  +List Page  +Add Post Page  +Map Page  + +### Light Mode + +Splash Screen  +SignIn Page  +Register Page  +Main Page  +List Page  +Add Post Page  +Map Page  + +### Night Mode + +## Technologies Used 🛠️ + +- Platform: Android (Kotlin) +- UI Framework: Android XML for layout designs +- Database: Room database (Paging3 with RemoteMeditor) +- Location Services: Google Maps API (for location sharing) +- Networking : Retrofit +- Testing: Junit, mockito, espresso + +## Terms of Use 📜 + +Educational Use Only +This app is built solely for educational purposes, and by using it, users agree to the following +terms: + +1. Non-commercial Use: Story App is not to be used for any commercial activities. +2. Third-party API Use: The third-party API used in this app is provided from an external source ( + bootcamp), and users should acknowledge that it is for learning purposes only. +3. User Content: Users are responsible for the stories they post. No inappropriate, offensive, or + illegal content is allowed. +4. Privacy: Users should avoid sharing any personal, sensitive, or private information within the + app. +5. Non-liability: The app developer is not liable for any misuse of the API or user-generated + content shared through the app. + +## Contributing 🤝 + +Contributions to the Story App are welcome! Follow these steps if you’d like to contribute: + +1. Fork the repository. +2. Create a new branch `git checkout -b improve/new-improve` +3. Commit your changes `git commit -m 'Add new improve'` +4. Push the branch `git push origin improve/new-improve` +5. Open a pull request. \ No newline at end of file diff --git a/TEST_CASE.md b/TEST_CASE.md new file mode 100644 index 0000000..f84ecd5 --- /dev/null +++ b/TEST_CASE.md @@ -0,0 +1,156 @@ +# Test Scenarios + +## END TO END TEST @LargeTest + +### [ListStoryActivityEndToEndTest](./app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityEndToEndTest.kt) + +#### loadListStory() + +- **Assumption**: User is logged in. +- **Verify**: + - The List of Stories is visible. + - The ListStoryActivity opens successfully. + - RecyclerView scrolls properly. + - SwipeRefreshLayout is displayed. + - Button to add a story is visible. + - Button to view stories on Google Map is visible. + - Perform a click action on the first item in the RecyclerView. + +#### loadDetailStory() + +- **Assumption**: User is logged in. +- **Verify**: + - The List of Stories is visible. + - Perform a click action on the first item in the RecyclerView. + - Story description is displayed. + - Sender's name is displayed. + - Posting time of the story is displayed. + - [Further verification steps are needed.] + +#### loadStoryMap() + +- **Assumption**: User is logged in and has allowed the application to use location services. +- **Verify**: + - The List of Stories is visible. + - Perform a click action on the button to view stories on Google Map. + - Map is displayed. + +#### loadAddStory() + +- **Assumption**: User is logged in. +- **Verify**: + - The List of Stories is visible. + - Perform a click action on the button to post a story. + - Image preview area is displayed. + - Button to add location is visible. + - Button to open the camera is visible. + - Button to open the gallery is visible. + - Button to upload story is displayed. + +--- + +## INTEGRATION TEST @MediumTest + +### [ListStoryActivityTest](./app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityTest.kt) + +#### getStory_Success() + +*[Check for successful data retrieval of stories from the network]* + +- **Verify**: + - Stories are displayed. + - A story containing the keyword "Zekken" is displayed. + - Scrolling is functional. + - A story containing the keyword "ya" is displayed. + +#### getStory_Error() + +*[Check for error in data retrieval of stories from the network]* + +- **Verify**: + - Stories are displayed if available in the local database. + - TextView "Oops.. something went wrong. Check your connection." is displayed. + +--- + +## UNIT TEST + +### [StoryRepositoryTest](./app/src/test/java/com/dicoding/storyapp/data/repository/StoryRepositoryTest.kt) + +- When the `register()` function is called, it should not return a null value. +- When the `login()` function is called, it should not return a null value and should return user + details including name, user ID, and token. +- When the `getStoryMap()` function is called, it should not return a null value and should return + story data. +- When the `postStory()` function is called, it should not return a null value. +- When the `getPagingStories()` function is called, it should not return a null value and should + return `PagingData`. + +### [AddStoryViewModelTest](./app/src/test/java/com/dicoding/storyapp/ui/viewmodel/AddStoryViewModelTest.kt) + +- When successfully adding a story: + - `ResultResponse.Success` should be true. + - `expectedResponse` should equal `ResultResponse.Success(dummyResponse)`. + - `expectedResponse` and `actualResponse` should be the same. +- When failing to add a story: + - `ResultResponse.Error` should be false. + - `expectedResponse` should equal `ResultResponse.Error(dummyResponseError)`. + - `actualResponse` and `ResultResponse.Error` should be the same. + +### [DetailStoryViewModelTest](./app/src/test/java/com/dicoding/storyapp/ui/viewmodel/DetailStoryViewModelTest.kt) + +- When successfully displaying story data: + - `expectedStory` should equal `dummyStory`. + - `actualStory` should equal `itemStory`. + - `expectedStory` and `actualStory` should be the same. + +### [ListStoryViewModelTest](./app/src/test/java/com/dicoding/storyapp/ui/viewmodel/ListStoryViewModelTest.kt) + +- When successfully retrieving a list of stories: + - Ensure that the data is not empty. + - Ensure that the size of the actual data matches that of the dummy data. + +### [LoginViewModelTest](./app/src/test/java/com/dicoding/storyapp/ui/viewmodel/LoginViewModelTest.kt) + +- When successfully logging in: + - `ResultResponse.Success` should be true. + - Ensure that `actualResponse` is not empty. + - `actualResponse` should equal `ResultResponse.Success`. + - `dummyResult` should match `actualResponse`, meaning the data returns the same values for user + name, user ID, and token. +- When failing to log in: + - `ResultResponse.Error` should be false. + - Ensure that `actualResponse` is not empty. + - `actualResponse` should equal `ResultResponse.Error`, meaning it returns the same error data. + +### [MainViewModelTest](./app/src/test/java/com/dicoding/storyapp/ui/viewmodel/MainViewModelTest.kt) + +- When successfully retrieving user data from local storage: + - Ensure that the local data is not empty. + - Local data should match `dummyUserModel`. +- When successfully logging out: + - The logout process with `mainViewModel` should match `userPreference`. + +### [MapsViewModelTest](./app/src/test/java/com/dicoding/storyapp/ui/viewmodel/MapsViewModelTest.kt) + +- When successfully retrieving story map data: + - `ResultResponse.Success` should be true. + - Ensure that `actualStory` is not empty. + - `actualStory` should equal `ResultResponse.Success`. + - Ensure the size of the actual data (`actualStory`) matches `dummyMaps`. +- When failing to retrieve story map data: + - `ResultResponse.Error` should be false. + - Ensure that `actualStory` is not empty. + - `actualStory` should equal `ResultResponse.Error`. + +### [RegisterViewModelTest](./app/src/test/java/com/dicoding/storyapp/ui/viewmodel/RegisterViewModelTest.kt) + +- When successfully registering: + - `ResultResponse.Success` should be true. + - Ensure that `actualResponse` is not empty. + - `actualResponse` should equal `ResultResponse.Success`. + - Ensure that `dummyResponse` matches `actualResponse`. +- When failing to register: + - `ResultResponse.Error` should be false. + - Ensure that `actualResponse` is not empty. + - `actualResponse` should equal `ResultResponse.Error`. diff --git a/app/build.gradle b/app/build.gradle index 9edd5a6..6185417 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,12 +7,13 @@ plugins { } android { - compileSdk 32 + compileSdk 34 + namespace 'com.dicoding.storyapp' defaultConfig { applicationId "com.dicoding.storyapp" minSdk 23 - targetSdk 32 + targetSdk 34 versionCode 1 versionName "1.0" @@ -39,6 +40,7 @@ android { } buildFeatures { viewBinding true + buildConfig true } packagingOptions { resources { @@ -47,75 +49,89 @@ android { } testOptions { animationsDisabled = true + unitTests.returnDefaultValues(true) + unitTests.all { + // Enable detailed test logs in the console + testLogging { + events "passed", "skipped", "failed" + showExceptions true + showCauses true + showStackTraces true + exceptionFormat "full" // To show full stack trace + } + } } } dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' - + //desugaring - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.2" - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - implementation "androidx.activity:activity-ktx:1.4.0" - implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation "androidx.activity:activity-ktx:1.9.2" - implementation 'androidx.annotation:annotation:1.3.0' + // for custom view + implementation 'com.google.android.material:material:1.12.0' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' - implementation "androidx.datastore:datastore-preferences:1.0.0" + implementation 'androidx.annotation:annotation:1.8.2' + + //splashscreen API + implementation 'androidx.core:core-splashscreen:1.0.1' - implementation 'com.github.bumptech.glide:glide:4.13.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.6' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6' + implementation "androidx.datastore:datastore-preferences:1.1.1" + + implementation 'com.github.bumptech.glide:glide:4.16.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' //testing testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test:core-ktx:1.4.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test:runner:1.6.2' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test:core-ktx:1.6.1' - implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0' + implementation 'androidx.test.espresso:espresso-idling-resource:3.6.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1' androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2' - androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1' //special testing - testImplementation "androidx.arch.core:core-testing:2.1.0" // InstantTaskExecutorRule - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" //TestCoroutineDispatcher - - //splashscreen API - implementation 'androidx.core:core-splashscreen:1.0.0-beta02' + testImplementation "androidx.arch.core:core-testing:2.2.0" // InstantTaskExecutorRule + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" //TestCoroutineDispatcher //retrofit - implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:retrofit:2.11.0' implementation "com.squareup.retrofit2:converter-gson:2.9.0" implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" //cameraX - def camerax_version = "1.1.0-beta03" + def camerax_version = "1.3.4" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-view:${camerax_version}" //room & paging - implementation 'androidx.room:room-ktx:2.5.0-alpha01' - kapt 'androidx.room:room-compiler:2.5.0-alpha01' - implementation 'androidx.room:room-paging:2.5.0-alpha01' - implementation "androidx.paging:paging-runtime-ktx:3.1.1" + implementation 'androidx.room:room-ktx:2.6.1' + kapt 'androidx.room:room-compiler:2.6.1' + implementation 'androidx.room:room-paging:2.6.1' + implementation "androidx.paging:paging-runtime-ktx:3.3.2" //google maps - implementation 'com.google.android.gms:play-services-maps:18.0.2' - implementation "com.google.android.gms:play-services-location:19.0.1" + implementation 'com.google.android.gms:play-services-maps:19.0.0' + implementation "com.google.android.gms:play-services-location:21.3.0" //mockito - testImplementation 'org.mockito:mockito-core:3.12.4' - testImplementation 'org.mockito:mockito-inline:3.12.4' + testImplementation 'org.mockito:mockito-core:5.14.1' + testImplementation 'org.mockito:mockito-inline:5.0.0' //mock web server androidTestImplementation "com.squareup.okhttp3:mockwebserver:4.9.3" diff --git a/app/src/androidTest/java/com/dicoding/storyapp/JsonConverter.kt b/app/src/androidTest/java/com/dicoding/storyapp/JsonConverter.kt index 1cf723e..43083b0 100644 --- a/app/src/androidTest/java/com/dicoding/storyapp/JsonConverter.kt +++ b/app/src/androidTest/java/com/dicoding/storyapp/JsonConverter.kt @@ -6,18 +6,18 @@ import java.io.IOException import java.io.InputStreamReader object JsonConverter { - fun readStringFromFile(fileName: String): String { - try { - val applicationContext = ApplicationProvider.getApplicationContext() - val inputStream = applicationContext.assets.open(fileName) - val builder = StringBuilder() - val reader = InputStreamReader(inputStream, "UTF-8") - reader.readLines().forEach { - builder.append(it) - } - return builder.toString() - } catch (e: IOException) { - throw e - } + fun readStringFromFile(fileName: String): String { + try { + val applicationContext = ApplicationProvider.getApplicationContext() + val inputStream = applicationContext.assets.open(fileName) + val builder = StringBuilder() + val reader = InputStreamReader(inputStream, "UTF-8") + reader.readLines().forEach { + builder.append(it) + } + return builder.toString() + } catch (e: IOException) { + throw e } + } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityEndToEndTest.kt b/app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityEndToEndTest.kt index 5d7f456..ee61643 100644 --- a/app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityEndToEndTest.kt +++ b/app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityEndToEndTest.kt @@ -19,15 +19,28 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import com.dicoding.storyapp.MainActivity -import com.dicoding.storyapp.R +import com.dicoding.storyapp.R.id.btn_camera_x +import com.dicoding.storyapp.R.id.btn_gallery +import com.dicoding.storyapp.R.id.btn_upload +import com.dicoding.storyapp.R.id.detail_view +import com.dicoding.storyapp.R.id.et_description +import com.dicoding.storyapp.R.id.iv_add_story +import com.dicoding.storyapp.R.id.iv_show_map +import com.dicoding.storyapp.R.id.iv_story +import com.dicoding.storyapp.R.id.map_view +import com.dicoding.storyapp.R.id.rv_story +import com.dicoding.storyapp.R.id.swipe_refresh +import com.dicoding.storyapp.R.id.switchCompat +import com.dicoding.storyapp.R.id.tv_created_time +import com.dicoding.storyapp.R.id.tv_description +import com.dicoding.storyapp.R.id.tv_name import com.dicoding.storyapp.data.model.UserModel -import com.dicoding.storyapp.util.EspressoIdlingResource +import com.dicoding.storyapp.utils.EspressoIdlingResource import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith - @RunWith(AndroidJUnit4::class) @LargeTest class ListStoryActivityEndToEndTest { @@ -59,16 +72,16 @@ class ListStoryActivityEndToEndTest { scenario = launchActivity(intent) Intents.init() - onView(withId(R.id.rv_story)).check(matches(isDisplayed())) - onView(withId(R.id.rv_story)).perform( + onView(withId(rv_story)).check(matches(isDisplayed())) + onView(withId(rv_story)).perform( RecyclerViewActions.scrollToPosition( 10 ) ) - onView(withId(R.id.swipe_refresh)).check(matches(isDisplayed())) - onView(withId(R.id.iv_add_story)).check(matches(isDisplayed())) - onView(withId(R.id.iv_show_map)).check(matches(isDisplayed())) - onView(withId(R.id.rv_story)).perform( + onView(withId(swipe_refresh)).check(matches(isDisplayed())) + onView(withId(iv_add_story)).check(matches(isDisplayed())) + onView(withId(iv_show_map)).check(matches(isDisplayed())) + onView(withId(rv_story)).perform( RecyclerViewActions.actionOnItemAtPosition( 0, click() @@ -84,18 +97,18 @@ class ListStoryActivityEndToEndTest { scenario = launchActivity(intent) Intents.init() - onView(withId(R.id.rv_story)).perform( + onView(withId(rv_story)).perform( RecyclerViewActions.actionOnItemAtPosition( 0, click() ) ) intended(hasComponent(DetailStoryActivity::class.java.name)) - onView(withId(R.id.detail_view)).check(matches(isDisplayed())) - onView(withId(R.id.tv_description)).check(matches(isDisplayed())) - onView(withId(R.id.iv_story)).check(matches(isDisplayed())) - onView(withId(R.id.tv_name)).check(matches(isDisplayed())) - onView(withId(R.id.tv_created_time)).check(matches(isDisplayed())) + onView(withId(detail_view)).check(matches(isDisplayed())) + onView(withId(tv_description)).check(matches(isDisplayed())) + onView(withId(iv_story)).check(matches(isDisplayed())) + onView(withId(tv_name)).check(matches(isDisplayed())) + onView(withId(tv_created_time)).check(matches(isDisplayed())) Intents.release() } @@ -106,9 +119,9 @@ class ListStoryActivityEndToEndTest { scenario = launchActivity(intent) Intents.init() - onView(withId(R.id.iv_show_map)).perform(click()) + onView(withId(iv_show_map)).perform(click()) intended(hasComponent(MapsActivity::class.java.name)) - onView(withId(R.id.map_view)).check(matches(isDisplayed())) + onView(withId(map_view)).check(matches(isDisplayed())) Intents.release() } @@ -118,11 +131,11 @@ class ListStoryActivityEndToEndTest { intent.putExtra(ListStoryActivity.EXTRA_USER, user) scenario = launchActivity(intent) - onView(withId(R.id.iv_add_story)).perform(click()) - onView(withId(R.id.switchCompat)).check(matches(isDisplayed())) - onView(withId(R.id.btn_gallery)).check(matches(isDisplayed())) - onView(withId(R.id.btn_camera_x)).check(matches(isDisplayed())) - onView(withId(R.id.et_description)).check(matches(isDisplayed())) - onView(withId(R.id.btn_upload)).check(matches(isDisplayed())) + onView(withId(iv_add_story)).perform(click()) + onView(withId(switchCompat)).check(matches(isDisplayed())) + onView(withId(btn_gallery)).check(matches(isDisplayed())) + onView(withId(btn_camera_x)).check(matches(isDisplayed())) + onView(withId(et_description)).check(matches(isDisplayed())) + onView(withId(btn_upload)).check(matches(isDisplayed())) } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityTest.kt b/app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityTest.kt index 342ff84..431aec8 100644 --- a/app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityTest.kt +++ b/app/src/androidTest/java/com/dicoding/storyapp/ui/activity/ListStoryActivityTest.kt @@ -16,10 +16,10 @@ import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.dicoding.storyapp.JsonConverter -import com.dicoding.storyapp.R +import com.dicoding.storyapp.R.id.rv_story import com.dicoding.storyapp.data.model.UserModel import com.dicoding.storyapp.data.remote.retrofit.ApiConfig -import com.dicoding.storyapp.util.EspressoIdlingResource +import com.dicoding.storyapp.utils.EspressoIdlingResource import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.After @@ -66,15 +66,15 @@ class ListStoryActivityTest { .setBody(JsonConverter.readStringFromFile("success_response.json")) mockWebServer.enqueue(mockResponse) - onView(withId(R.id.rv_story)).check( + onView(withId(rv_story)).check( matches(isDisplayed()) ) onView(withText("Zekken")) .check(matches(isDisplayed())) - onView(withId(R.id.rv_story)).perform( + onView(withId(rv_story)).perform( ViewActions.swipeUp() ) - onView(withId(R.id.rv_story)) + onView(withId(rv_story)) .perform( RecyclerViewActions.scrollTo( hasDescendant(withText("ya")) @@ -92,10 +92,9 @@ class ListStoryActivityTest { .setResponseCode(500) // error mockWebServer.enqueue(mockResponse) - onView(withId(R.id.rv_story)) + onView(withId(rv_story)) .check(matches(isDisplayed())) onView(withText("Oops.. something went wrong. Check your connection")) .check(matches(isDisplayed())) } - } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55a01fc..95c3636 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -9,7 +8,9 @@ - + + + @@ -20,8 +21,8 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:usesCleartextTraffic="true" - android:theme="@style/Theme.StoryApp"> + android:theme="@style/Theme.StoryApp" + android:usesCleartextTraffic="false"> - - + + @@ -64,5 +70,13 @@ @font/bold - + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 1c5a17e..c87cba6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,6 +1,6 @@ - StoryApp + Story App Введите свой пароль Введите адрес электронной почты @@ -8,7 +8,7 @@ Этот логотип StoryApp Недействительный адрес электронной почты - Пароль должен быть >5 символов + Пароль должен быть длиной не менее 8 символов. Войти регистр @@ -25,7 +25,7 @@ Заполните все поле правильно Гало %1$s, - Создано в %1$s, + Создано в %1$s Dicoding 2022 Публикуйте и делитесь своей историей, делитесь своей деятельностью и знакомьтесь с историями других людей. Войдите в свою учетную запись @@ -39,6 +39,7 @@ Выйти успешно Информация + Предупреждение Камера Описание Галерея @@ -66,5 +67,7 @@ Упс! Что-то пошло не так Пожалуйста, разрешите свои службы определения местоположения Поделиться местоположением + Выход из системы завершит ваш сеанс. Вы уверены, что хотите продолжить? + Нет \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6f225d8..1dc1d18 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,7 +1,5 @@ - #FFBB86FC - #FF3700B3 #FF03DAC5 #FF018786 #FF000000 @@ -9,6 +7,7 @@ #ACACAC #FFFFFFFF #CD5C5C + #843838 #00639F #0091ea #64c1ff diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..bf906c1 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + + 24dp + 10dp + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml index beab31f..a6b3dae 100644 --- a/app/src/main/res/values/ic_launcher_background.xml +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,2 @@ - - #000000 - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_monochrome_icon_background.xml b/app/src/main/res/values/ic_launcher_monochrome_icon_background.xml new file mode 100644 index 0000000..7715126 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_monochrome_icon_background.xml @@ -0,0 +1,4 @@ + + + #E3CEAA + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ad391e..b4115bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - StoryApp + Story App Input your password Input your email @@ -7,7 +7,7 @@ This StoryApp logo Not a valid email - Password must be >5 characters + Password must be at least 8 characters long Sign In Register @@ -24,7 +24,7 @@ Fill All The Field With Correctly Hi %1$s, - Created at %1$s, + Created at %1$s Dicoding 2022 Post and share your story, share your activities, and get to know other people\'s stories Login your account @@ -36,6 +36,9 @@ Upload Success Swipe Up to Refresh Sign In Failed Log Out Success + Warning + Signing out will end your session. Are you sure you want to continue? + No Information Camera diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 5bd3120..5454e6d 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -10,14 +10,14 @@ @color/teal_700 @color/black - ?attr/colorPrimaryVariant + ?attr/colorPrimaryVariant + + @color/black true true @android:transition/fade @android:transition/fade - - @color/black - + + - @@ -63,4 +69,14 @@ 16sp @font/bold + + \ No newline at end of file diff --git a/app/src/test/java/com/dicoding/storyapp/LiveDataTestUtil.kt b/app/src/test/java/com/dicoding/storyapp/LiveDataTestUtil.kt index 35982df..ea2dcf7 100644 --- a/app/src/test/java/com/dicoding/storyapp/LiveDataTestUtil.kt +++ b/app/src/test/java/com/dicoding/storyapp/LiveDataTestUtil.kt @@ -9,33 +9,33 @@ import java.util.concurrent.TimeoutException @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun LiveData.getOrAwaitValue( - time: Long = 2, - timeUnit: TimeUnit = TimeUnit.SECONDS, - afterObserve: () -> Unit = {} + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS, + afterObserve: () -> Unit = {} ): T { - var data: T? = null - val latch = CountDownLatch(1) - val observer = object : Observer { - override fun onChanged(o: T?) { - data = o - latch.countDown() - this@getOrAwaitValue.removeObserver(this) - } + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(value: T) { + data = value + latch.countDown() + this@getOrAwaitValue.removeObserver(this) } - this.observeForever(observer) + } + this.observeForever(observer) - try { - afterObserve.invoke() + try { + afterObserve.invoke() - // Don't wait indefinitely if the LiveData is not set. - if (!latch.await(time, timeUnit)) { - throw TimeoutException("LiveData value was never set.") - } - - } finally { - this.removeObserver(observer) + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") } - @Suppress("UNCHECKED_CAST") - return data as T + } finally { + this.removeObserver(observer) + } + + @Suppress("UNCHECKED_CAST") + return data as T } \ No newline at end of file diff --git a/app/src/test/java/com/dicoding/storyapp/MainCoroutineRule.kt b/app/src/test/java/com/dicoding/storyapp/MainCoroutineRule.kt index 2586062..201fde3 100644 --- a/app/src/test/java/com/dicoding/storyapp/MainCoroutineRule.kt +++ b/app/src/test/java/com/dicoding/storyapp/MainCoroutineRule.kt @@ -11,12 +11,12 @@ import org.junit.runner.Description val dispatcher: TestDispatcher = UnconfinedTestDispatcher() ) : TestWatcher() { - override fun starting(description: Description?) { + override fun starting(description: Description) { super.starting(description) Dispatchers.setMain(dispatcher) } - override fun finished(description: Description?) { + override fun finished(description: Description) { super.finished(description) Dispatchers.resetMain() } diff --git a/app/src/test/java/com/dicoding/storyapp/data/repository/StoryRepositoryTest.kt b/app/src/test/java/com/dicoding/storyapp/data/repository/StoryRepositoryTest.kt index c3265c1..a645c4e 100644 --- a/app/src/test/java/com/dicoding/storyapp/data/repository/StoryRepositoryTest.kt +++ b/app/src/test/java/com/dicoding/storyapp/data/repository/StoryRepositoryTest.kt @@ -2,7 +2,6 @@ package com.dicoding.storyapp.data.repository import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.paging.AsyncPagingDataDiffer -import androidx.paging.ExperimentalPagingApi import androidx.recyclerview.widget.ListUpdateCallback import com.dicoding.storyapp.DataDummy import com.dicoding.storyapp.MainCoroutineRule @@ -77,12 +76,17 @@ class StoryRepositoryTest { @Test fun `when postStory() is called Should Not Null`() = runTest { val expectedResponse = DataDummy.generateDummyApiResponseSuccess() - val actualResponse = apiService.addStories("Token", dummyDescription, dummyMultipart, dummyLatitude, dummyLongitude) + val actualResponse = apiService.addStories( + "Token", + dummyDescription, + dummyMultipart, + dummyLatitude, + dummyLongitude + ) Assert.assertNotNull(actualResponse) Assert.assertEquals(expectedResponse, actualResponse) } - @OptIn(ExperimentalPagingApi::class) @Test fun `when getPagingStories() is called Should Not Null`() = runTest { val mainCoroutineRule = MainCoroutineRule() diff --git a/app/src/test/java/com/dicoding/storyapp/ui/viewmodel/MainViewModelTest.kt b/app/src/test/java/com/dicoding/storyapp/ui/viewmodel/MainViewModelTest.kt index f922043..85d295c 100644 --- a/app/src/test/java/com/dicoding/storyapp/ui/viewmodel/MainViewModelTest.kt +++ b/app/src/test/java/com/dicoding/storyapp/ui/viewmodel/MainViewModelTest.kt @@ -16,7 +16,6 @@ import org.mockito.Mockito import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnitRunner -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(MockitoJUnitRunner::class) class MainViewModelTest { @get:Rule diff --git a/app/src/test/java/com/dicoding/storyapp/utils/PagedTestDataSource.kt b/app/src/test/java/com/dicoding/storyapp/utils/PagedTestDataSource.kt index 58a32db..eaa471b 100644 --- a/app/src/test/java/com/dicoding/storyapp/utils/PagedTestDataSource.kt +++ b/app/src/test/java/com/dicoding/storyapp/utils/PagedTestDataSource.kt @@ -7,20 +7,20 @@ import androidx.paging.PagingState import com.dicoding.storyapp.data.remote.response.ListStoryItem class PagedTestDataSource : - PagingSource>>() { + PagingSource>>() { - companion object { - fun snapshot(items: List): PagingData { - return PagingData.from(items) - } + companion object { + fun snapshot(items: List): PagingData { + return PagingData.from(items) } + } - override fun getRefreshKey(state: PagingState>>): Int { - return 0 - } + override fun getRefreshKey(state: PagingState>>): Int { + return 0 + } - override suspend fun load(params: LoadParams): LoadResult>> { - return LoadResult.Page(emptyList(), 0, 1) - } + override suspend fun load(params: LoadParams): LoadResult>> { + return LoadResult.Page(emptyList(), 0, 1) + } } \ No newline at end of file diff --git a/assets/images/ss-dark-addpost.jpg b/assets/images/ss-dark-addpost.jpg new file mode 100644 index 0000000..cf25fb6 Binary files /dev/null and b/assets/images/ss-dark-addpost.jpg differ diff --git a/assets/images/ss-dark-detail.jpg b/assets/images/ss-dark-detail.jpg new file mode 100644 index 0000000..47fcbbd Binary files /dev/null and b/assets/images/ss-dark-detail.jpg differ diff --git a/assets/images/ss-dark-list.jpg b/assets/images/ss-dark-list.jpg new file mode 100644 index 0000000..0d4b1a6 Binary files /dev/null and b/assets/images/ss-dark-list.jpg differ diff --git a/assets/images/ss-dark-main.jpg b/assets/images/ss-dark-main.jpg new file mode 100644 index 0000000..de6718a Binary files /dev/null and b/assets/images/ss-dark-main.jpg differ diff --git a/assets/images/ss-dark-map.jpg b/assets/images/ss-dark-map.jpg new file mode 100644 index 0000000..3200bcf Binary files /dev/null and b/assets/images/ss-dark-map.jpg differ diff --git a/assets/images/ss-dark-register.jpg b/assets/images/ss-dark-register.jpg new file mode 100644 index 0000000..3e03208 Binary files /dev/null and b/assets/images/ss-dark-register.jpg differ diff --git a/assets/images/ss-dark-signin.jpg b/assets/images/ss-dark-signin.jpg new file mode 100644 index 0000000..8514518 Binary files /dev/null and b/assets/images/ss-dark-signin.jpg differ diff --git a/assets/images/ss-dark-splashscreen.jpg b/assets/images/ss-dark-splashscreen.jpg new file mode 100644 index 0000000..4646067 Binary files /dev/null and b/assets/images/ss-dark-splashscreen.jpg differ diff --git a/assets/images/ss-light-addpost.jpg b/assets/images/ss-light-addpost.jpg new file mode 100644 index 0000000..bf0381d Binary files /dev/null and b/assets/images/ss-light-addpost.jpg differ diff --git a/assets/images/ss-light-detail.jpg b/assets/images/ss-light-detail.jpg new file mode 100644 index 0000000..1bcf172 Binary files /dev/null and b/assets/images/ss-light-detail.jpg differ diff --git a/assets/images/ss-light-list.jpg b/assets/images/ss-light-list.jpg new file mode 100644 index 0000000..a8cd8f5 Binary files /dev/null and b/assets/images/ss-light-list.jpg differ diff --git a/assets/images/ss-light-main.jpg b/assets/images/ss-light-main.jpg new file mode 100644 index 0000000..3c27478 Binary files /dev/null and b/assets/images/ss-light-main.jpg differ diff --git a/assets/images/ss-light-map.jpg b/assets/images/ss-light-map.jpg new file mode 100644 index 0000000..4851630 Binary files /dev/null and b/assets/images/ss-light-map.jpg differ diff --git a/assets/images/ss-light-register.jpg b/assets/images/ss-light-register.jpg new file mode 100644 index 0000000..6c830b5 Binary files /dev/null and b/assets/images/ss-light-register.jpg differ diff --git a/assets/images/ss-light-signin.jpg b/assets/images/ss-light-signin.jpg new file mode 100644 index 0000000..6e7de52 Binary files /dev/null and b/assets/images/ss-light-signin.jpg differ diff --git a/assets/images/ss-light-splashscreen.jpg b/assets/images/ss-light-splashscreen.jpg new file mode 100644 index 0000000..b202b1b Binary files /dev/null and b/assets/images/ss-light-splashscreen.jpg differ diff --git a/assets/logo/feature_graphic.png b/assets/logo/feature_graphic.png new file mode 100644 index 0000000..52e7026 Binary files /dev/null and b/assets/logo/feature_graphic.png differ diff --git a/assets/logo/ic_logo.png b/assets/logo/ic_logo.png new file mode 100644 index 0000000..24c67ab Binary files /dev/null and b/assets/logo/ic_logo.png differ diff --git a/assets/logo/ic_logo.svg b/assets/logo/ic_logo.svg new file mode 100644 index 0000000..bcca5c4 --- /dev/null +++ b/assets/logo/ic_logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/logo/ic_logo_monochrome.png b/assets/logo/ic_logo_monochrome.png new file mode 100644 index 0000000..8e5f883 Binary files /dev/null and b/assets/logo/ic_logo_monochrome.png differ diff --git a/assets/logo/icon_512.webp b/assets/logo/icon_512.webp new file mode 100644 index 0000000..e6db838 Binary files /dev/null and b/assets/logo/icon_512.webp differ diff --git a/assets/logo/logo.cdr b/assets/logo/logo.cdr new file mode 100644 index 0000000..534872c Binary files /dev/null and b/assets/logo/logo.cdr differ diff --git a/build.gradle b/build.gradle index 0003d74..7c8954a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,20 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.1.3' apply false - id 'com.android.library' version '7.1.3' apply false - id 'org.jetbrains.kotlin.android' version '1.6.21' apply false + id 'com.android.application' version '8.2.0' apply false + id 'com.android.library' version '8.2.0' apply false + id 'org.jetbrains.kotlin.android' version '1.9.10' apply false id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' apply false } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir +} + +tasks.withType(Test).configureEach { + // Show more detailed output for tests + testLogging { + events "passed", "skipped", "failed" + showStandardStreams = true + exceptionFormat = 'full' + } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index cd0519b..2a7ec69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59f2d02..f7a5e31 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Apr 07 16:22:59 ICT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/gradle_tests_report.gradle b/gradle_tests_report.gradle new file mode 100644 index 0000000..c62d114 --- /dev/null +++ b/gradle_tests_report.gradle @@ -0,0 +1,71 @@ +import groovy.time.TimeCategory +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +rootProject { + ext.testsResults = [] // Container for tests summaries + + allprojects { project -> + tasks.withType(Test) { testTask -> + + testTask.testLogging { logging -> + events TestLogEvent.FAILED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_OUT, + TestLogEvent.STANDARD_ERROR + + exceptionFormat TestExceptionFormat.FULL + showExceptions true + showCauses true + showStackTraces true + } + + ignoreFailures = true // Always try to run all tests for all modules + + afterSuite { desc, result -> + + if (desc.parent) return // Only summarize results for whole modules + + String summary = "${testTask.project.name}:${testTask.name} results: ${result.resultType} " + + "(" + + "${result.testCount} tests, " + + "${result.successfulTestCount} successes, " + + "${result.failedTestCount} failures, " + + "${result.skippedTestCount} skipped" + + ") " + + "in ${TimeCategory.minus(new Date(result.endTime), new Date(result.startTime))}" + + "\n" + + "Report file: ${testTask.reports.html.entryPoint}" + + // Add reports in `testsResults`, keep failed suites at the end + if (result.resultType == TestResult.ResultType.SUCCESS) { + rootProject.testsResults.add(0, summary) + } else { + rootProject.testsResults += summary + } + } + } + } +} + +gradle.buildFinished { + def allResults = rootProject.ext.testsResults + + if (!allResults.isEmpty()) { + printResults allResults + } +} + +private static void printResults(allResults) { + def maxLength = allResults*.readLines().flatten().collect { it.length() }.max() + + println "┌${"${"─" * maxLength}"}┐" + + println allResults.collect { + it.readLines().collect { + "│" + it + " " * (maxLength - it.length()) + "│" + }.join("\n") + }.join("\n├${"${"─" * maxLength}"}┤\n") + + println "└${"${"─" * maxLength}"}┘" +} \ No newline at end of file diff --git a/skenario-pengujian.TXT b/skenario-pengujian.TXT deleted file mode 100644 index 298eaaf..0000000 --- a/skenario-pengujian.TXT +++ /dev/null @@ -1,125 +0,0 @@ -END TO END TEST @LargeTest - >>> ListStoryActivityEndToEndTest - >> loadListStory() - - Asumsi user telah login - - Melihat List Cerita - - Memastikan bahwa ListStoryActivity terbuka - - Memastikan bahwa RecyclerView berkerja dengan baik ketika di scroll - - Memastikan bahwa SwipeRefreshLayout tampil - - Memastikan bahwa tombol untuk tambah story/posting story tampil - - Memastikan bahwa tombol untuk melihat story pada Googgle map tampil - - Melakukan aksi klik pada item pertama di RecyclerView - - >> loadDetailStory() - - Asumsi user telah login - - Melihat List Cerita - - Melakukan aksi klik pada item pertama di RecyclerView - - Memastikan bahwa deskripsi story tampil - - Memastikan bahwa nama pengirim story tampil - - Memastikan bahwa waktu ketika story di posting tampil - - Memastikan bahwa - - >> loadStoryMap() - - Asumsi user telah login dan memperbolehkan aplikasi untuk menggunakan service lokasi - - Melihat List Cerita - - Melakukan aksi klik pada tombol untuk melihat story pada Googgle map tampil - - Memastikan bahwa map tampil - - >> loadAddStory() - - Asumsi user telah login - - Melihat List Cerita - - Melakukan aksi klik pada tombol untuk posting story - - Memastikan bahwa tempat untuk preview gambar tampil - - Memastikan bahwa tombol untuk menambahkan lokasi sekarang tampil - - Memastikan bahwa tombol untuk membuka kamera tampil - - Memastikan bahwa tombol untuk membuka galeri tampil - - Memastikan bahwa untuk mengunggah story tampil - - -INTEGRATION TEST @MediumTest - >>> ListStoryActivityTest - >> getStory_Success() [Pengecekan pengambilan data story dari network] - - Memastikan bahwa story tampil - - Memastikan bahwa story dengan kata "Zekken" tampil - - Memastikan bahwa dapat dilakukan scroll - - Memastikan bahwa story dengan kata "ya" tampil - - >> getStory_Error() [Pengecekan pengambilan data story dari network] - - Memastikan bahwa story tampil, jika ada dalam local database - - Memastikan bahwa TextView "Oops.. something went wrong. Check your connection" tampil - - -UNIT TEST - >>> StoryRepositoryTest - - Ketika fungsi register() dipanggil maka seharusnya tidak mengembalikan nilai null - - Ketika fungsi login() dipanggil maka seharusnya tidak mengembalikan nilai null dan mengembalikan nilai user berupa nama, user id, dan token - - Ketika fungsi getStoryMap() dipanggil maka seharusnya tidak mengembalikan nilai null dan mengembalikan data story - - Ketika fungsi postStory() dipanggil maka seharusnya tidak mengembalikan nilai null - - Ketika fungsi getPagingStories() dipanggil maka seharusnya tidak mengembalikan nilai null dan mengembalikan PagingData - - >>> AddStoryViewModelTest - - Ketika berhasil menambahkan story, - ResultResponse.Success bernilai true, - expectedResponse sama dengan ResultResponse.Success(dummyResponse), - expectedResponse dan actualResponse sama - - Ketika gagal menambahkan story, - ResultResponse.Error bernilai false, - expectedResponse sama dengan ResultResponse.Error(dummyResponseError), - actualResponse dan ResultResponse.Error sama - - >>> DetailStoryViewModelTest - - Ketika berhasil menampilkan data story, - expectedStory sama dengan dummyStory, - actualStory sama dengan itemStory, - expectedStory dan actualStory sama - - >>> ListStoryViewModelTest - - Ketika berhasil mendapatkan data list story, - memastikan bahwa data tidak kosong, - memastikan bahwa ukuran data asli dengan data dummy sama - - >>> LoginViewModelTest - - Ketika berhasil login, - ResultResponse.Success bernilai true, - Memastikan bahwa actualResponse tidak kosong, - actualRespon sama dengan ResultResponse.Success, - dummyResult sama dengan actualResponse, - yang berarti data mengembalikan nilai yang sama berupa data user yakni name, userId dan token - - Ketika gagal login, - ResultResponse.Error bernilai false, - Memastikan bahwa actualResponse tidak kosong, - actualResponse dan ResultResponse.Error sama, - yang berarti mengembalikan data yang sama berupa error - - >>> MainViewModelTest - - Ketika berhasil mendapatkan data user dari local (datastore), - Memastikan bahwa data local tidak kosong, - Data local dengan data dummyUserModel sama, - - Ketika berhasil melakukan logout, - proses logout dengan mainVideModel denan userPreference sama - - >>> MapsViewModelTest - - Ketika berhasil mendapatkan data story map, - ResultResponse.Success bernilai true, - Memastikan bahwa actualStory tidak kosong, - actualStory dan ResultResponse.Success sama, - memastikan bahwa ukuran data asli(actualStory) dengan data dummyMaps sama - - Ketika gagal mendapatkan data story map, - ResultResponse.Error bernilai false, - memastikan bahwa actualStory tidak kosong - actualStoy dan ResultResponse.Error sama - - >>> RegisterViewModelTest - - Ketika berhasil melakukan register - ResultResponse.Success bernilai true, - Memastikan bahwa actualResponse tidak kosong, - actualResponse dan ResultResponse.Success sama, - Memastikan bahwa data dummyRespon dan data actualResponse sama - - Ketika gagal melakukan register - ResultResponse.Error bernilai false, - Memastikan bahwa actualResponse tidak kosong, - actualResponse dan ResultResponse.Error - - - -