Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

- [ ] πŸ› Bug fix (non-breaking change which fixes an issue)
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] πŸ’₯ Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] πŸ“ Documentation update
- [ ] ♻️ Code refactoring (no functional changes)
- [ ] 🎨 UI/UX improvement
Expand All @@ -20,7 +19,6 @@ Fixes #(issue number)

## Changes Made
<!-- List the specific changes made in this PR -->

-
-
-
Expand All @@ -47,17 +45,5 @@ Fixes #(issue number)
- Android Version:
- App Version:

## Checklist
<!-- Mark completed items with an 'x' -->

- [ ] My code follows the project's code style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings or errors
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published

## Additional Notes
<!-- Add any additional context or notes for reviewers -->
55 changes: 2 additions & 53 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ on:
push:
branches:
- develop
- main
- master
pull_request:
branches:
- develop
- main
- master

jobs:
build:
Expand Down Expand Up @@ -80,57 +80,6 @@ jobs:
app/build/reports/lint-results*.xml
retention-days: 7

# Uncomment this job when you have instrumented tests ready
# instrumented-tests:
# name: Instrumented Tests
# runs-on: ubuntu-latest
# timeout-minutes: 45
# strategy:
# matrix:
# api-level: [31, 34]
# target: [google_apis]
#
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
#
# - name: Set up JDK 17
# uses: actions/setup-java@v4
# with:
# distribution: 'temurin'
# java-version: '17'
#
# - name: Setup Gradle
# uses: gradle/actions/setup-gradle@v3
#
# - name: Grant execute permission for gradlew
# run: chmod +x gradlew
#
# - name: Enable KVM group perms
# run: |
# echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
# sudo udevadm control --reload-rules
# sudo udevadm trigger --name-match=kvm
#
# - name: Run instrumented tests
# uses: reactivecircus/android-emulator-runner@v2
# with:
# api-level: ${{ matrix.api-level }}
# target: ${{ matrix.target }}
# arch: x86_64
# profile: Nexus 6
# script: ./gradlew connectedDebugAndroidTest --stacktrace
#
# - name: Upload instrumented test results
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: instrumented-test-results-api-${{ matrix.api-level }}
# path: |
# app/build/reports/androidTests/
# app/build/outputs/androidTest-results/
# retention-days: 7

code-quality:
name: Code Quality Checks
runs-on: ubuntu-latest
Expand Down
13 changes: 4 additions & 9 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 30 additions & 13 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.detekt)
alias(libs.plugins.ksp)
id("org.jetbrains.kotlin.kapt")
id("com.google.dagger.hilt.android")
}
Expand All @@ -16,8 +17,8 @@ android {
applicationId = "com.serranoie.app.media.sorter"
minSdk = 31
targetSdk = 36
versionCode = 1014
versionName = "1.0.14"
versionCode = 102
versionName = "1.0.2"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -48,10 +49,17 @@ android {
buildFeatures {
compose = true
}

testOptions {
unitTests {
isReturnDefaultValues = true
}
}
}

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
Expand All @@ -64,7 +72,15 @@ dependencies {
implementation(libs.androidx.material3)
implementation(libs.material3)
implementation(libs.androidx.work.runtime.ktx)
// Unit Testing
testImplementation(libs.junit)
testImplementation("io.mockk:mockk:1.13.14")
testImplementation("io.mockk:mockk-android:1.13.14")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("app.cash.turbine:turbine:1.2.0")

// UI Testing
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand All @@ -90,23 +106,24 @@ dependencies {
// Wavy Slider for video progress
implementation(libs.wavy.slider)

implementation("com.google.dagger:hilt-android:2.57.1")
kapt("com.google.dagger:hilt-android-compiler:2.57.1")
implementation(libs.hilt.android)
kapt(libs.hilt.android.compiler)
implementation(libs.androidx.hilt.navigation.compose)

// Hilt Work integration
implementation("androidx.hilt:hilt-work:1.2.0")
kapt("androidx.hilt:hilt-compiler:1.2.0")
kapt(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.work)

// Update checking dependencies
implementation("com.google.android.play:app-update:2.1.0")
implementation("com.google.android.play:app-update-ktx:2.1.0")
implementation("androidx.work:work-runtime-ktx:2.10.0")
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
implementation(libs.app.update)
implementation(libs.app.update.ktx)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.retrofit)
implementation(libs.retrofit.gson)
implementation(libs.okhttp)
implementation(libs.okhttp.logging)

implementation("androidx.compose.animation:animation:1.6.7")
}

// Detekt configuration
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.serranoie.app.media.sorter.presentation.review

import android.app.PendingIntent
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.serranoie.app.media.sorter.data.MediaFile
import com.serranoie.app.media.sorter.domain.Result
import com.serranoie.app.media.sorter.domain.repository.MediaRepository
import com.serranoie.app.media.sorter.presentation.model.MediaFileUi
import com.serranoie.app.media.sorter.presentation.ui.theme.SorterTheme
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.time.LocalDate

@RunWith(AndroidJUnit4::class)
class ReviewScreenTest {

@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()

private val fakeRepository = object : MediaRepository {
override suspend fun fetchMediaFiles(): Result<List<MediaFile>> = Result.Success(emptyList())
override suspend fun getMediaByFolder(): Result<Map<String, List<MediaFile>>> = Result.Success(emptyMap())
override suspend fun getMediaGroupedByDate(): Result<Map<LocalDate, List<MediaFile>>> = Result.Success(emptyMap())
override suspend fun getMediaGroupedByDateFiltered(): Result<Map<LocalDate, List<MediaFile>>> = Result.Success(emptyMap())
override suspend fun deleteMedia(uri: Uri): Result<Boolean> = Result.Success(true)
override suspend fun deleteMultipleMedia(uris: List<Uri>): Result<Int> = Result.Success(uris.size)
override fun createDeletionRequest(uris: List<Uri>, useTrash: Boolean): PendingIntent? = null
override fun clearCache() {}
override suspend fun markAsViewed(mediaId: Long) {}
override suspend fun isViewed(mediaId: Long): Boolean = false
override suspend fun clearViewedHistory() {}
override suspend fun getViewedCount(): Int = 0
}

private fun dummyFile(id: String = "1") = MediaFileUi(
id = id,
fileName = "File_$id.jpg",
fileInfo = "1.0 MB β€’ Today",
mediaType = "image",
date = "Today",
fileSize = "1.0 MB",
dimensions = "100x100",
dateCreated = "Today",
lastAccessed = "Today",
modified = "Today",
path = "/"
)

@Test
fun emptyState_is_shown_when_no_deleted_files() {
composeRule.setContent {
SorterTheme {
ReviewScreen(
deletedFiles = emptyList(),
repository = fakeRepository,
useTrash = false
)
}
}

composeRule.onNodeWithTag("ReviewScreen").assertExists()
composeRule.onNodeWithTag("ReviewEmptyState").assertIsDisplayed()
composeRule.onNodeWithTag("ReviewDeleteAllFab").assertDoesNotExist()
}

@Test
fun deleteAll_flow_shows_dialog_cancel_dismisses_and_confirm_calls_callback() {
var deleteAllCalls = 0

composeRule.setContent {
SorterTheme {
ReviewScreen(
deletedFiles = listOf(dummyFile("1"), dummyFile("2")),
repository = fakeRepository,
useTrash = false,
onDeleteAll = { deleteAllCalls++ }
)
}
}

composeRule.onNodeWithTag("ReviewGrid").assertIsDisplayed()
composeRule.onNodeWithTag("ReviewDeleteAllFab").performClick()
composeRule.onNodeWithTag("ReviewDeleteAllDialog").assertExists()

composeRule.onNodeWithTag("ReviewDeleteAllCancel").performClick()
composeRule.onNodeWithTag("ReviewDeleteAllDialog").assertDoesNotExist()

composeRule.onNodeWithTag("ReviewDeleteAllFab").performClick()
composeRule.onNodeWithTag("ReviewDeleteAllConfirm").performClick()

composeRule.waitUntil(timeoutMillis = 5_000) { deleteAllCalls == 1 }
}

@Test
fun doubleTap_on_grid_item_opens_fullscreen_and_close_dismisses() {
val file = dummyFile("42")

composeRule.setContent {
SorterTheme {
ReviewScreen(
deletedFiles = listOf(file),
repository = fakeRepository,
useTrash = false
)
}
}

composeRule.onNodeWithTag("ReviewGridItem_${file.id}")
.performTouchInput { doubleClick() }

composeRule.onNodeWithTag("ReviewFullscreenViewer").assertExists()
composeRule.onNodeWithTag("ReviewFullscreenClose").performClick()
composeRule.onNodeWithTag("ReviewFullscreenViewer").assertDoesNotExist()
}
}
Loading
Loading