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
27 changes: 27 additions & 0 deletions mobile/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ android {

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "SENTRY_DSN", "\"${localProps.getProperty("SENTRY_DSN", "")}\"")
buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", "\"${localProps.getProperty("GOOGLE_WEB_CLIENT_ID", "")}\"")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

buildTypes {
Expand All @@ -39,6 +40,23 @@ android {
)
}
}

// Fail fast: GOOGLE_WEB_CLIENT_ID must be set in local.properties for release builds.
// (The resource default_web_client_id from google-services.json is preferred at runtime,
// but BuildConfig is the fallback and must not be empty in production.)
tasks.configureEach {
if (name.contains("Release", ignoreCase = true) &&
(name.startsWith("assemble") || name.startsWith("bundle"))
) {
doFirst {
val webClientId = localProps.getProperty("GOOGLE_WEB_CLIENT_ID", "")
check(webClientId.isNotEmpty()) {
"GOOGLE_WEB_CLIENT_ID must be set in local.properties for release builds. " +
"Add: GOOGLE_WEB_CLIENT_ID=<your-oauth2-web-client-id>"
}
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
Expand All @@ -61,10 +79,19 @@ dependencies {
implementation(libs.sentry.android)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging.ktx)
implementation(libs.firebase.auth.ktx)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.play.services)
implementation(libs.googleid)
implementation(libs.datastore.preferences)
implementation(libs.coil.compose)
implementation(libs.okhttp)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.okhttp.mockwebserver)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.company.template.auth

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.company.template.ui.theme.TemplateTheme
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LoginScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

private var capturedEmail = ""
private var capturedPassword = ""

private fun setContent(
loginForm: LoginFormState = LoginFormState(),
uiState: AuthUiState = AuthUiState.Idle,
onEmailChange: (String) -> Unit = { capturedEmail = it },
onPasswordChange: (String) -> Unit = { capturedPassword = it },
onSignIn: () -> Unit = {},
onSignInWithGoogle: () -> Unit = {},
onNavigateToRegister: () -> Unit = {},
) {
composeTestRule.setContent {
TemplateTheme {
LoginScreen(
loginForm = loginForm,
uiState = uiState,
onEmailChange = onEmailChange,
onPasswordChange = onPasswordChange,
onSignIn = onSignIn,
onSignInWithGoogle = onSignInWithGoogle,
onNavigateToRegister = onNavigateToRegister,
)
}
}
}

@Test
fun loginScreen_displaysSignInHeading() {
setContent()
// The heading "Sign In" is displayed (there may also be a button with the same label)
composeTestRule.onNodeWithText("Sign In", useUnmergedTree = false).assertIsDisplayed()
}

@Test
fun loginScreen_displaysGoogleSignInButton() {
setContent()
composeTestRule.onNodeWithText("Continue with Google").assertIsDisplayed()
}

@Test
fun loginScreen_googleButton_invokesCallback() {
var called = false
setContent(onSignInWithGoogle = { called = true })
composeTestRule.onNodeWithText("Continue with Google").performClick()
assertTrue(called)
}

@Test
fun loginScreen_displaysErrorMessage() {
setContent(uiState = AuthUiState.Error("Invalid credentials"))
composeTestRule.onNodeWithText("Invalid credentials").assertIsDisplayed()
}

@Test
fun loginScreen_displaysRegisterLink() {
setContent()
composeTestRule.onNodeWithText("Register").assertIsDisplayed()
}

@Test
fun loginScreen_clickRegister_invokesCallback() {
var navigateCalled = false
setContent(onNavigateToRegister = { navigateCalled = true })
composeTestRule.onNodeWithText("Register").performClick()
assertTrue(navigateCalled)
}

@Test
fun loginScreen_clickSignIn_invokesCallback() {
var signInCalled = false
setContent(
loginForm = LoginFormState(email = "test@example.com", password = "secret"),
onSignIn = { signInCalled = true },
)
// Use the test tag to target the button specifically, not the heading
composeTestRule.onNodeWithTag(LoginTestTags.SIGN_IN_BUTTON).performClick()
assertTrue(signInCalled)
}

@Test
fun loginScreen_typingEmail_updatesCallback() {
var updated = ""
setContent(onEmailChange = { updated = it })
composeTestRule.onNodeWithText("Email").performTextInput("test@example.com")
assertTrue(updated.isNotEmpty())
}

@Test
fun loginScreen_typingPassword_updatesCallback() {
var updated = ""
setContent(onPasswordChange = { updated = it })
composeTestRule.onNodeWithText("Password").performTextInput("secret123")
assertTrue(updated.isNotEmpty())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.company.template.auth

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.company.template.ui.theme.TemplateTheme
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class RegisterScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

private fun setContent(
registerForm: RegisterFormState = RegisterFormState(),
uiState: AuthUiState = AuthUiState.Idle,
onNameChange: (String) -> Unit = {},
onEmailChange: (String) -> Unit = {},
onPasswordChange: (String) -> Unit = {},
onConfirmPasswordChange: (String) -> Unit = {},
onRegister: () -> Unit = {},
onNavigateToLogin: () -> Unit = {},
) {
composeTestRule.setContent {
TemplateTheme {
RegisterScreen(
registerForm = registerForm,
uiState = uiState,
onNameChange = onNameChange,
onEmailChange = onEmailChange,
onPasswordChange = onPasswordChange,
onConfirmPasswordChange = onConfirmPasswordChange,
onRegister = onRegister,
onNavigateToLogin = onNavigateToLogin,
)
}
}
}

@Test
fun registerScreen_displaysCreateAccountHeading() {
setContent()
// Heading text (there may also be a button with the same label; use useUnmergedTree if needed)
composeTestRule.onNodeWithText("Create Account", useUnmergedTree = false).assertIsDisplayed()
}

@Test
fun registerScreen_displaysNameField() {
setContent()
composeTestRule.onNodeWithText("Full Name").assertIsDisplayed()
}

@Test
fun registerScreen_displaysErrorMessage() {
setContent(uiState = AuthUiState.Error("Email already in use"))
composeTestRule.onNodeWithText("Email already in use").assertIsDisplayed()
}

@Test
fun registerScreen_displaysSignInLink() {
setContent()
composeTestRule.onNodeWithText("Sign In").assertIsDisplayed()
}

@Test
fun registerScreen_clickSignIn_invokesCallback() {
var navigateCalled = false
setContent(onNavigateToLogin = { navigateCalled = true })
composeTestRule.onNodeWithText("Sign In").performClick()
assertTrue(navigateCalled)
}

@Test
fun registerScreen_clickCreateAccount_invokesCallback() {
var registerCalled = false
setContent(
registerForm = RegisterFormState(
name = "Alice",
email = "alice@example.com",
password = "pass",
confirmPassword = "pass",
),
onRegister = { registerCalled = true },
)
// Use the test tag to target the button specifically, not the heading
composeTestRule.onNodeWithTag(RegisterTestTags.CREATE_ACCOUNT_BUTTON).performClick()
assertTrue(registerCalled)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.company.template.home

import androidx.compose.ui.test.assertDoesNotExist
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.company.template.ui.theme.TemplateTheme
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class HomeScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun homeScreen_displaysWelcomeHeading() {
composeTestRule.setContent {
TemplateTheme {
HomeScreen(displayName = "Alice", onSignOut = {})
}
}
composeTestRule.onNodeWithText("Welcome back!").assertIsDisplayed()
}

@Test
fun homeScreen_displaysDisplayName() {
composeTestRule.setContent {
TemplateTheme {
HomeScreen(displayName = "Alice", onSignOut = {})
}
}
composeTestRule.onNodeWithText("Alice").assertIsDisplayed()
}

@Test
fun homeScreen_displaysSignOutButton() {
composeTestRule.setContent {
TemplateTheme {
HomeScreen(displayName = "Alice", onSignOut = {})
}
}
composeTestRule.onNodeWithText("Sign Out").assertIsDisplayed()
}

@Test
fun homeScreen_clickSignOut_invokesCallback() {
var signOutCalled = false
composeTestRule.setContent {
TemplateTheme {
HomeScreen(displayName = "Alice", onSignOut = { signOutCalled = true })
}
}
composeTestRule.onNodeWithText("Sign Out").performClick()
assertTrue(signOutCalled)
}

@Test
fun homeScreen_emptyDisplayName_doesNotShowNameText() {
composeTestRule.setContent {
TemplateTheme {
HomeScreen(displayName = "", onSignOut = {})
}
}
composeTestRule.onNodeWithText("Welcome back!").assertIsDisplayed()
// An empty string should not produce a visible name node
composeTestRule.onNodeWithText("").assertDoesNotExist()
}
Comment thread
GRACENOBLE marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.company.template.onboarding

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.company.template.ui.theme.TemplateTheme
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class OnboardingScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun onboardingScreen_displaysWelcomeHeading() {
composeTestRule.setContent {
TemplateTheme {
OnboardingScreen(onGetStarted = {})
}
}
composeTestRule.onNodeWithText("Welcome").assertIsDisplayed()
}

@Test
fun onboardingScreen_displaysGetStartedButton() {
composeTestRule.setContent {
TemplateTheme {
OnboardingScreen(onGetStarted = {})
}
}
composeTestRule.onNodeWithText("Get Started").assertIsDisplayed()
}

@Test
fun onboardingScreen_clickGetStarted_invokesCallback() {
var callbackInvoked = false
composeTestRule.setContent {
TemplateTheme {
OnboardingScreen(onGetStarted = { callbackInvoked = true })
}
}
composeTestRule.onNodeWithText("Get Started").performClick()
assertTrue(callbackInvoked)
}
}
Loading
Loading