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
83 changes: 83 additions & 0 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: SonarCloud Analysis
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]
merge_group:

permissions:
contents: read

jobs:
sonarcloud:
name: SonarCloud
runs-on: ubuntu-latest

steps:
- name: Harden Runner
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
java-version: "17"
distribution: "temurin"
cache: gradle

- name: Setup Android SDK
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2

# Setup KVM for hardware acceleration
- name: Setup KVM
run: |
sudo apt-get update
sudo apt-get install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils
sudo adduser $USER kvm
sudo chown $USER /dev/kvm
sudo chmod 777 /dev/kvm

# Accept Android SDK licenses
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses || true

# Install required SDK components
- name: Install SDK components
run: |
sdkmanager "platform-tools" "platforms;android-33" "system-images;android-33;google_apis;x86_64"
sdkmanager --install "emulator"

- name: Run Instrumented Tests
uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # v2.34.0
with:
api-level: 33
target: google_apis
arch: x86_64
profile: pixel_6
force-avd-creation: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -accel on -no-snapshot
disable-animations: true
script: adb start-server && adb wait-for-device && until adb shell getprop sys.boot_completed 2>/dev/null | grep -q '^1$'; do echo "Waiting for boot completion..."; sleep 5; done && adb devices && ./gradlew jacocoAndroidTestReport && (adb emu kill || true) && sleep 5

- name: SonarCloud Scan
uses: SonarSource/[email protected]
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
projectBaseDir: .
args: >
-Dsonar.organization=formbricks
-Dsonar.projectKey=formbricks_android
-Dsonar.java.binaries=android/build/tmp/kotlin-classes/debug
-Dsonar.sources=android/src/main/java
-Dsonar.tests=android/src/androidTest/java
-Dsonar.coverage.jacoco.xmlReportPaths=android/build/reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml
-Dsonar.verbose=true
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,19 @@
.externalNativeBuild
.cxx
local.properties

# IDE
.vscode/
.settings/

# Coverage
/tools/
*.exec
*.ec
/coverage/
/reports/
generate-*-coverage.sh

# Generated files
**/build/
**/reports/
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies {
}
```

Enable DataBinding in your apps module build.gradle.kts:
Enable DataBinding in your app's module build.gradle.kts:

```kotlin
android {
Expand Down Expand Up @@ -63,6 +63,42 @@ Formbricks.logout()

We welcome issues and pull requests on our GitHub repository.

## Testing and Code Coverage

### Running Tests

To run the instrumented tests, make sure you have an Android emulator running or a physical device connected, then execute:

```bash
./gradlew connectedDebugAndroidTest
```

### Generating Coverage Reports

The SDK uses JaCoCo for code coverage reporting. To generate a coverage report for instrumented tests:

1. Make sure you have an Android emulator running or a physical device connected
2. Run the provided script:
```bash
./generate-instrumented-coverage.sh
```
This will:
- Run the instrumented tests
- Generate a JaCoCo coverage report
- Open the HTML report in your default browser

Alternatively, you can run the Gradle task directly:

```bash
./gradlew jacocoAndroidTestReport
```

The coverage report will be generated at:

```
android/build/reports/jacoco/jacocoAndroidTestReport/html/index.html
```

## License

This SDK is released under the MIT License.
79 changes: 69 additions & 10 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@ plugins {
id("org.jetbrains.dokka") version "1.9.10"
id("jacoco")
id("com.vanniktech.maven.publish") version "0.31.0"
id("org.sonarqube") version "4.4.1.3373"
}

// Import JaCoCo configuration
// apply(from = "../jacoco.gradle.kts")

version = "1.1.0"
val groupId = "com.formbricks"
val artifactId = "android"

// Configure JaCoCo version
jacoco {
toolVersion = "0.8.11"
}

android {
namespace = "com.formbricks.android"
compileSdk = 35
Expand All @@ -28,6 +37,7 @@ android {
buildTypes {
getByName("debug") {
enableAndroidTestCoverage = true
isTestCoverageEnabled = true // For backward compatibility
}
release {
isMinifyEnabled = true
Expand Down Expand Up @@ -62,15 +72,6 @@ android {
}
}

tasks.withType<Test>().configureEach {
extensions.configure<JacocoTaskExtension> {
isIncludeNoLocationClasses = true
excludes = listOf(
"jdk.internal.*",
)
}
}

dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.annotation)
Expand All @@ -91,7 +92,6 @@ dependencies {
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.databinding.common)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
Expand Down Expand Up @@ -126,4 +126,63 @@ mavenPublishing {
url = "https://github.com/formbricks/android"
}
}
}

// Add JaCoCo tasks
tasks.register<JacocoReport>("jacocoAndroidTestReport") {
dependsOn("connectedDebugAndroidTest")

reports {
xml.required.set(true)
html.required.set(true)
}

val fileFilter = listOf(
"**/R.class",
"**/R\$*.class",
"**/BuildConfig.*",
"**/Manifest*.*",
"**/*Test*.*",
"android/databinding/**/*.class",
"android/databinding/*Binding.*",
"android/BuildConfig.*",
"**/*\$*.*",
"**/Lambda\$*.class",
"**/Lambda.class",
"**/*Lambda.class",
"**/*Lambda*.class",
"**/*_MembersInjector.class",
"**/Dagger*Component.class",
"**/Dagger*Component\$*.class",
"**/*Module_*Factory.class"
)

val debugTree = fileTree(mapOf(
"dir" to layout.buildDirectory.dir("tmp/kotlin-classes/debug").get().asFile,
"excludes" to fileFilter
))

val mainSrc = "${project.projectDir}/src/main/java"

sourceDirectories.setFrom(files(mainSrc))
classDirectories.setFrom(files(debugTree))
executionData.setFrom(fileTree(mapOf(
"dir" to layout.buildDirectory.get().asFile,
"includes" to listOf(
"outputs/code_coverage/debugAndroidTest/connected/**/*.ec",
"outputs/code_coverage/debugAndroidTest/connected/**/*.exec"
)
)))
}

// Configure Sonar
sonar {
properties {
property("sonar.coverage.jacoco.xmlReportPaths",
layout.buildDirectory.file("reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml").get().asFile.path)
}
}

tasks.sonar {
dependsOn("jacocoAndroidTestReport")
}
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@
"triggers": [
{
"actionClass": {
"id": "cm6ow6hht000isf0k39hbmi5f",
"name": "Clicked the demo button"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.formbricks.android.api.FormbricksApi
import com.formbricks.android.helper.FormbricksConfig
import com.formbricks.android.logger.Logger
import com.formbricks.android.manager.SurveyManager
import com.formbricks.android.manager.UserManager
import org.junit.Assert.assertEquals
Expand Down Expand Up @@ -44,7 +45,7 @@ class FormbricksInstrumentedTest {
@Test
fun testFormbricks() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.formbricks.formbrickssdk.test", appContext.packageName)
assertEquals("com.formbricks.android.test", appContext.packageName)

// Everything should be in the default state
assertFalse(Formbricks.isInitialized)
Expand Down Expand Up @@ -90,7 +91,7 @@ class FormbricksInstrumentedTest {
(FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = true
assertFalse(SurveyManager.hasApiError)
SurveyManager.refreshEnvironmentIfNeeded(true)
waitForSeconds(1)
waitForSeconds(3) // Increased wait time to 3 seconds
assertTrue(SurveyManager.hasApiError)
(FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = false

Expand All @@ -113,9 +114,24 @@ class FormbricksInstrumentedTest {

// Track a known event, thus, the survey should be shown.
SurveyManager.isShowingSurvey = false

// Track the event but don't show the survey
val firstSurveyBeforeTrack = SurveyManager.filteredSurveys.firstOrNull()
assertNotNull("Should have a survey before tracking", firstSurveyBeforeTrack)
assertEquals("Should have the correct survey ID", surveyID, firstSurveyBeforeTrack?.id)

val actionClasses = SurveyManager.environmentDataHolder?.data?.data?.actionClasses ?: listOf()
val clickDemoButtonAction = actionClasses.firstOrNull { it.key == "click_demo_button" }
assertNotNull("Should have click_demo_button action class", clickDemoButtonAction)

val triggers = firstSurveyBeforeTrack?.triggers ?: listOf()
val matchingTrigger = triggers.firstOrNull { it.actionClass?.id == clickDemoButtonAction?.id }
assertNotNull("Survey should have matching trigger", matchingTrigger)

// Now track the event
Formbricks.track("click_demo_button")
waitForSeconds(1)
assertTrue(SurveyManager.isShowingSurvey)
assertTrue("Survey should be marked as showing", SurveyManager.isShowingSurvey)

// Validate display and response
SurveyManager.onNewDisplay(surveyID)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
package com.formbricks.android

import androidx.test.platform.app.InstrumentationRegistry
import com.formbricks.android.model.environment.EnvironmentDataHolder
import com.formbricks.android.model.environment.EnvironmentResponse
import com.formbricks.android.model.user.PostUserBody
import com.formbricks.android.model.user.UserResponse
import com.formbricks.android.network.FormbricksApiService
import com.google.gson.Gson
import com.formbricks.android.model.error.SDKError

class MockFormbricksApiService: FormbricksApiService() {
private val gson = Gson()
private val environmentJson = MockFormbricksApiService::class.java.getResource("/Environment.json")!!.readText()
private val userJson = MockFormbricksApiService::class.java.getResource("/User.json")!!.readText()
private val environment = gson.fromJson(environmentJson, EnvironmentResponse::class.java)
private val user = gson.fromJson(userJson, UserResponse::class.java)
private val environment: EnvironmentResponse
private val user: UserResponse
var isErrorResponseNeeded = false

init {
val context = InstrumentationRegistry.getInstrumentation().context
val environmentJson = context.assets.open("Environment.json").bufferedReader().readText()
val userJson = context.assets.open("User.json").bufferedReader().readText()

environment = gson.fromJson(environmentJson, EnvironmentResponse::class.java)
user = gson.fromJson(userJson, UserResponse::class.java)
}

override fun getEnvironmentStateObject(environmentId: String): Result<EnvironmentDataHolder> {
return if (isErrorResponseNeeded) {
Result.failure(RuntimeException())
Result.failure(SDKError.unableToRefreshEnvironment)
} else {
Result.success(EnvironmentDataHolder(environment.data, mapOf()))
}
}

override fun postUser(environmentId: String, body: PostUserBody): Result<UserResponse> {
return if (isErrorResponseNeeded) {
Result.failure(RuntimeException())
Result.failure(SDKError.unableToPostResponse)
} else {
Result.success(user)
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ object SurveyManager {
val actionClass = codeActionClasses.firstOrNull { it.key == action }
val firstSurveyWithActionClass = filteredSurveys.firstOrNull { survey ->
val triggers = survey.triggers ?: listOf()
triggers.firstOrNull { it.actionClass?.name.equals(actionClass?.name) } != null
triggers.firstOrNull { trigger ->
trigger.actionClass?.name == actionClass?.name
} != null
}

if (firstSurveyWithActionClass == null) {
Expand Down
Loading
Loading