Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ package com.openvino.notes.app

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.stopKoin
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [35])
class AppCompositionTest {
@After fun tearDown() {
stopKoin()
}

@Test fun `composition exposes public feature contracts`() {
val context = ApplicationProvider.getApplicationContext<Context>()
AppComposition(context).use { composition ->
Expand All @@ -24,4 +30,13 @@ class AppCompositionTest {
assertNotNull(composition.noteAssistant)
}
}

@Test fun `composition runs without network and google configuration`() {
val context = ApplicationProvider.getApplicationContext<Context>()
AppComposition(context).use { composition ->
assertNotNull(composition.identityService)
assertNotNull(composition.workerFactory)
assertNotNull(composition.syncScheduler)
}
}
}
3 changes: 3 additions & 0 deletions modules/openvino-notes/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ android.useAndroidX=true
android.nonTransitiveRClass=true
android.suppressUnsupportedCompileSdk=37.0


# Enabled parallel sync for Gradle 9.4+
org.gradle.tooling.parallel=true
2 changes: 2 additions & 0 deletions modules/openvino-notes/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-compose-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
androidx-compose-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,30 @@ class FakeNotesService(
initial: List<Note> = emptyList(),
) : NotesService {
private val notes = MutableStateFlow(initial)
var createOutcome: NoteMutationOutcome? = null
val currentNotes: List<Note> get() = notes.value

override fun observeNotes(): Flow<List<Note>> = notes

override suspend fun get(id: NoteId): Note? = notes.value.firstOrNull { it.id == id }

override suspend fun create(draft: NoteDraft): NoteMutationOutcome =
NoteMutationOutcome.Invalid("fake", "create outcome not configured")

override suspend fun create(draft: NoteDraft): NoteMutationOutcome {
val configured = createOutcome
if (configured != null) return configured
val created = Note(
id = NoteId("fake-note-${notes.value.size + 1}"),
accountKey = AccountKey("fake-account"),
title = draft.title,
contentItems = draft.contentItems,
attachments = draft.attachments,
folderId = draft.folderId,
tags = draft.tags,
isFavorite = draft.isFavorite,
createdAt = java.time.Instant.EPOCH,
updatedAt = java.time.Instant.EPOCH,
)
notes.value = notes.value + created
return NoteMutationOutcome.Saved(created)
}
override suspend fun update(command: UpdateNoteCommand): NoteMutationOutcome {
val current = get(command.id) ?: return NoteMutationOutcome.NotFound
val updated =
Expand Down Expand Up @@ -213,6 +229,35 @@ class FakeNotesService(
}
}

class FakeFolderService(initial: List<Folder> = emptyList()) : FolderService {
private val folders = MutableStateFlow(initial)
override fun observeFolders(): Flow<List<Folder>> = folders
override suspend fun getFolder(id: FolderId): Folder? = folders.value.firstOrNull { it.id == id }
override suspend fun createFolder(draft: FolderDraft): FolderMutationOutcome {
val created = Folder(
id = FolderId("fake-folder-${folders.value.size + 1}"),
accountKey = AccountKey("fake-account"),
name = draft.name,
createdAt = java.time.Instant.EPOCH,
updatedAt = java.time.Instant.EPOCH,
)
folders.value = folders.value + created
return FolderMutationOutcome.Saved(created)
}
override suspend fun renameFolder(id: FolderId, name: String): FolderMutationOutcome {
val current = getFolder(id) ?: return FolderMutationOutcome.NotFound
val updated = current.copy(name = name)
folders.value = folders.value.map { if (it.id == id) updated else it }
return FolderMutationOutcome.Saved(updated)
}
override suspend fun deleteFolder(id: FolderId): FolderMutationOutcome {
val existed = folders.value.any { it.id == id }
folders.value = folders.value.filterNot { it.id == id }
return if (existed) FolderMutationOutcome.Deleted else FolderMutationOutcome.NotFound
}
override suspend fun moveNote(noteId: NoteId, folderId: FolderId?): MoveNoteOutcome = MoveNoteOutcome.Invalid("not configured")
}

@Suppress("UNCHECKED_CAST")
private fun <T> FieldUpdate<T>.resolve(current: T): T =
when (this) {
Expand Down
6 changes: 6 additions & 0 deletions modules/openvino-notes/view/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
testOptions { unitTests.isIncludeAndroidResources = true }
}

dependencies {
Expand All @@ -32,11 +33,16 @@ dependencies {
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.test.manifest)
testImplementation(testFixtures(project(":notes:api")))
testImplementation(testFixtures(project(":identity:api")))
testImplementation(testFixtures(project(":sync:api")))
testImplementation(testFixtures(project(":assistant:api")))
testImplementation(testFixtures(project(":settings:api")))
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.androidx.test.core)
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.robolectric)
testImplementation(libs.androidx.compose.test.junit4)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (C) 2018-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

package com.openvino.notes.view.app

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onLast
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.openvino.notes.assistant.api.FakeNoteAssistant
import com.openvino.notes.identity.api.AuthenticationState
import com.openvino.notes.identity.api.FakeIdentityService
import com.openvino.notes.identity.api.UserSession
import com.openvino.notes.kernel.AccountKey
import com.openvino.notes.notes.api.FakeFolderService
import com.openvino.notes.notes.api.FakeNotesService
import com.openvino.notes.settings.api.FakeSettingsService
import com.openvino.notes.sync.api.FakeSyncService
import com.openvino.notes.view.assistant.AssistantViewModel
import com.openvino.notes.view.identity.signin.IdentityViewModel
import com.openvino.notes.view.notes.folders.FoldersViewModel
import com.openvino.notes.view.notes.list.NotesListViewModel
import com.openvino.notes.view.settings.SettingsViewModel
import com.openvino.notes.view.sync.SyncStatusViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [35])
class AppNavigationTest {
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun `navigation switches tabs and user action invokes service once`() {
val notesService = FakeNotesService()
val folderService = FakeFolderService()
val session = UserSession(AccountKey("user-1"), "Test User", "test@example.com")
val identityService = FakeIdentityService(AuthenticationState.SignedIn(session))
val syncService = FakeSyncService()
val settingsService = FakeSettingsService()
val assistantService = FakeNoteAssistant()

val notesVm = NotesListViewModel(notesService)
val foldersVm = FoldersViewModel(folderService)
val identityVm = IdentityViewModel(identityService)
val syncVm = SyncStatusViewModel(syncService)
val settingsVm = SettingsViewModel(settingsService)
val assistantVm = AssistantViewModel(assistantService)

composeTestRule.setContent {
OpenVinoNotesRoot(
notes = notesVm,
folders = foldersVm,
identity = identityVm,
sync = syncVm,
settings = settingsVm,
assistant = assistantVm,
)
}

// Verify initial screen (Notes)
composeTestRule.onNodeWithText("No notes yet.").assertIsDisplayed()

// User action: Click "New" to create welcome note
composeTestRule.onNodeWithText("New").performClick()

// Check service was invoked once (note created in FakeNotesService)
assertEquals(1, notesService.currentNotes.size)
assertEquals("Welcome", notesService.currentNotes.first().title)

// Navigation: Click "Folders" in bottom navigation
composeTestRule.onAllNodesWithText("Folders").onLast().performClick()
composeTestRule.onNodeWithText("No folders yet.").assertIsDisplayed()

// Navigation: Click "Sync" in bottom navigation
composeTestRule.onAllNodesWithText("Sync").onLast().performClick()
composeTestRule.onNodeWithText("Sync now").assertIsDisplayed()

// Navigation: Click "Settings" in bottom navigation
composeTestRule.onAllNodesWithText("Settings").onLast().performClick()
composeTestRule.onNodeWithText("Change theme").assertIsDisplayed()

// Navigation: Click "Assistant" in bottom navigation
composeTestRule.onAllNodesWithText("Assistant").onLast().performClick()
composeTestRule.onNodeWithText("Select an AI operation").assertIsDisplayed()
}

@Test
fun `signed out state renders sign in button and triggers sign in effect`() = runTest {
val identityService = FakeIdentityService(AuthenticationState.SignedOut)
val identityVm = IdentityViewModel(identityService)

composeTestRule.setContent {
OpenVinoNotesRoot(
notes = NotesListViewModel(FakeNotesService()),
folders = FoldersViewModel(FakeFolderService()),
identity = identityVm,
sync = SyncStatusViewModel(FakeSyncService()),
settings = SettingsViewModel(FakeSettingsService()),
assistant = AssistantViewModel(FakeNoteAssistant()),
)
}

composeTestRule.onNodeWithText("Sign in").assertIsDisplayed()
composeTestRule.onNodeWithText("Sign in").performClick()

assertEquals(AppUiEffect.LaunchGoogleSignIn, identityVm.effects.first())
}
}
Loading