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
18 changes: 12 additions & 6 deletions mobile/android/fenix/app/longfox/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

plugins {
alias(libs.plugins.kotlin.compose)
}
Expand All @@ -26,6 +20,11 @@ android {
minSdk config.minSdkVersion
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}

dependencies {
Expand All @@ -44,7 +43,14 @@ dependencies {
implementation libs.androidx.datastore.preferences
implementation libs.google.material
testImplementation libs.junit4
testImplementation platform(libs.androidx.compose.bom)
testImplementation libs.androidx.compose.ui.test
testImplementation libs.androidx.test.core
testImplementation libs.androidx.test.junit
testImplementation libs.kotlinx.coroutines.test
testImplementation libs.robolectric
androidTestImplementation libs.androidx.test.junit
androidTestImplementation libs.androidx.test.espresso.core
debugImplementation libs.androidx.compose.ui.tooling
debugImplementation libs.androidx.compose.ui.test.manifest
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ fun LongFoxGameScreen() {
}
val restartGame = { gameState = GameState(numCells = numCells, size = Size(canvasSizePx, canvasSizePx)) }
SideEffect {
@Suppress("AssignedValueIsNeverRead")
if (gameState.shouldCelebrateScore && !celebrationShown) celebrationSeed = gameState.score
celebrationShown = gameState.shouldCelebrateScore
}
Expand All @@ -85,9 +86,11 @@ fun LongFoxGameScreen() {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val longFoxDataStore = remember(context) { LongFoxDataStore(context) }
val hiscore by longFoxDataStore.hiscoreFlow()
.collectAsState(initial = null, coroutineScope.coroutineContext)
val soundOn by longFoxDataStore.soundOnFlow()
.collectAsState(initial = false, coroutineScope.coroutineContext)
val soundEffectsPlayer = remember(soundOn) { SoundEffectsPlayer(context, soundOn) }
.collectAsState(initial = null, coroutineScope.coroutineContext)
val soundEffectsPlayer = remember(soundOn) { SoundEffectsPlayer(context, soundOn == true) }

DisposableEffect(soundEffectsPlayer) {
onDispose { soundEffectsPlayer.release() }
Expand Down Expand Up @@ -148,9 +151,12 @@ fun LongFoxGameScreen() {
) {
if (gameState.isGameOver) {
NewGameScreen(
longFoxDataStore = longFoxDataStore,
initialGameState = gameState,
hiscore = hiscore,
soundOn = soundOn,
onToggleSoundOn = { coroutineScope.launch { longFoxDataStore.toggleSoundOn() } },
startGame = restartGame,
shareHiscore = { coroutineScope.launch { longFoxDataStore.shareHiscore(it) }}
)
} else {
GameCanvas(gameState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@

package org.mozilla.fenix.longfox

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
Expand All @@ -23,49 +21,44 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.mozilla.fenix.longfox.GameState.Companion.CELL_SIZE_DP
import org.mozilla.fenix.longfox.GameState.Companion.GAME_INTERVAL_TIME_MS

/**
* A little intro screen to launch the game and provide high score and sound on/off switch.
* @param initialGameState the current game state.
* @param longFoxDataStore a data store to save game default preferences.
* @param hiscore the persisted high score, or `null` while the data store is still loading.
* @param soundOn the persisted sound setting, or `null` while the data store is still loading.
* @param onToggleSoundOn invoked when the user taps the sound toggle.
* @param startGame a callback to start the game.
* @param shareHiscore a callback to share the hiscore with other apps.
*/
@Composable
fun NewGameScreen(
initialGameState: GameState,
longFoxDataStore: LongFoxDataStore,
hiscore: Int?,
soundOn: Boolean?,
onToggleSoundOn: () -> Unit,
startGame: () -> Unit,
shareHiscore: (Int) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()

val hiscore by longFoxDataStore.hiscoreFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
val soundOn by longFoxDataStore.soundOnFlow()
.collectAsState(initial = false, coroutineScope.coroutineContext)
var gameState by remember(initialGameState.numCells) {
mutableStateOf(
initialGameState.copy(
Expand Down Expand Up @@ -126,16 +119,17 @@ fun NewGameScreen(
)
TextButton(
modifier = Modifier
.alpha(if (hiscore > 0) 1f else 0f)
.padding(top = 36.dp, bottom = 36.dp)
.background(Color.Transparent),
onClick = { if (hiscore > 0) longFoxDataStore.shareHiscore(hiscore) },
.alpha(if (hiscore == null) 0f else 1f),
onClick = { if (hiscore != null) shareHiscore(hiscore) },
) {
Text(
fontSize = 22.sp,
modifier = Modifier
.alpha(if (hiscore == null) 0f else 1f),
fontSize = 18.sp,
fontFamily = LongFoxText.zx,
color = Color.Cyan,
text = stringResource(R.string.hiscore, hiscore)
text = stringResource(R.string.hiscore, hiscore ?: 0)
)
Spacer(Modifier.width(12.dp))
Icon(
Expand All @@ -146,13 +140,14 @@ fun NewGameScreen(
}
Text(
modifier = Modifier
.border(width = 2.dp, color = if (soundOn) Color.White else Color.Gray)
.clickable { onToggleSoundOn() }
.border(width = 2.dp, color = if (soundOn == true) Color.White else Color.Gray)
.padding(8.dp)
.clickable { coroutineScope.launch { longFoxDataStore.toggleSoundOn() } },
.alpha(if (soundOn == null) 0f else 1f),
fontFamily = LongFoxText.zx,
fontSize = 16.sp,
color = if (soundOn) Color.White else Color.Gray,
text = if (soundOn) stringResource(R.string.sound_on) else stringResource(R.string.sound_off)
color = if (soundOn == true) Color.White else Color.Gray,
text = if (soundOn == true) stringResource(R.string.sound_on) else stringResource(R.string.sound_off)
)
}
}
Expand All @@ -169,10 +164,10 @@ fun NewGameScreenPreview() {
size = Size(canvasSizePx, canvasSizePx),
isGameOver = true
),
longFoxDataStore = LongFoxDataStore(
context = LocalContext.current,
initialHiscore = 5
),
hiscore = 0,
soundOn = false,
onToggleSoundOn = {},
startGame = {},
shareHiscore = {},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.mozilla.fenix.longfox

import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
* Tests for the new game screen rendering.
*/
@RunWith(AndroidJUnit4::class)
class NewGameScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

private val context = ApplicationProvider.getApplicationContext<Context>()
private val soundOnText = context.getString(R.string.sound_on)
private val soundOffText = context.getString(R.string.sound_off)
private fun hiscoreText(value: Int? = null) = context.getString(R.string.hiscore, value)

private val initialGameState =
GameState(numCells = 20, size = Size(400f, 400f), isGameOver = true)

@Test
fun `if you don't have a hiscore yet, no hiscore is shown`() {
composeTestRule.setContent {
NewGameScreen(
initialGameState = initialGameState,
hiscore = null,
soundOn = false,
onToggleSoundOn = {},
startGame = {},
shareHiscore = {},
)
}

composeTestRule.onNodeWithText(hiscoreText()).assertIsNotDisplayed()
}

@Test
fun `if you do have a hiscore, it is shown`() {
composeTestRule.setContent {
NewGameScreen(
initialGameState = initialGameState,
hiscore = 42,
soundOn = false,
onToggleSoundOn = {},
startGame = {},
shareHiscore = {},
)
}

composeTestRule.onNodeWithText(hiscoreText(42)).assertIsDisplayed()
}

@Test
fun `when sound is switched on, sound on label is shown`() {
composeTestRule.setContent {
NewGameScreen(
initialGameState = initialGameState,
hiscore = 0,
soundOn = true,
onToggleSoundOn = {},
startGame = {},
shareHiscore = {},
)
}
composeTestRule.onNodeWithText(soundOnText).assertIsDisplayed()
}

@Test
fun `when sound is switched off, sound off label is shown`() {
composeTestRule.setContent {
NewGameScreen(
initialGameState = initialGameState,
hiscore = 0,
soundOn = false,
onToggleSoundOn = {},
startGame = {},
shareHiscore = {},
)
}
composeTestRule.onNodeWithText(soundOffText).assertIsDisplayed()
}

@Test
fun `clicking sound off button toggles sound on`() {
var toggleCount = 0
composeTestRule.setContent {
NewGameScreen(
initialGameState = initialGameState,
hiscore = 0,
soundOn = false,
onToggleSoundOn = { toggleCount++ },
startGame = {},
shareHiscore = {},
)
}
composeTestRule.onNodeWithText(soundOffText).performClick()

assertEquals(1, toggleCount)
}

@Test
fun `clicking sound off button changes text to sound on`() {
composeTestRule.setContent {
var soundOn by remember { mutableStateOf(false) }
NewGameScreen(
initialGameState = initialGameState,
hiscore = 0,
soundOn = soundOn,
onToggleSoundOn = { soundOn = !soundOn },
startGame = {},
shareHiscore = {},
)
}
composeTestRule.onNodeWithText(soundOffText).performClick()

composeTestRule.onNodeWithText(soundOnText).assertIsDisplayed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#

sdk=35