Revert "For #24220 and #24223: Connect GleanPlumb messages with the new tab ui card. Co-authored-by: Jonathan Almeida <jalmeida@mozilla.com>"
This reverts commit f953c5ec
upstream-sync
parent
febff55459
commit
cc9e91809b
@ -1,34 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.mozilla.fenix.utils.BrowsersCache
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom attributes that the messaging framework will use to evaluate if message is eligible
|
|
||||||
* to be shown.
|
|
||||||
*/
|
|
||||||
object CustomAttributeProvider {
|
|
||||||
private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a [JSONObject] that contains all the custom attributes, evaluated when the function
|
|
||||||
* was called.
|
|
||||||
*/
|
|
||||||
fun getCustomAttributes(context: Context): JSONObject {
|
|
||||||
val now = Calendar.getInstance()
|
|
||||||
return JSONObject(
|
|
||||||
mapOf(
|
|
||||||
"is_default_browser" to BrowsersCache.all(context).isDefaultBrowser,
|
|
||||||
"date_string" to formatter.format(now.time)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import org.mozilla.fenix.BuildConfig
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
import org.mozilla.fenix.components.AppStore
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles default interactions with the ui of GleanPlumb messages.
|
|
||||||
*/
|
|
||||||
class DefaultMessageController(
|
|
||||||
private val appStore: AppStore,
|
|
||||||
private val messagingStorage: NimbusMessagingStorage,
|
|
||||||
private val homeActivity: HomeActivity
|
|
||||||
) : MessageController {
|
|
||||||
|
|
||||||
override fun onMessagePressed(message: Message) {
|
|
||||||
// Report telemetry event
|
|
||||||
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
|
|
||||||
val action = messagingStorage.getMessageAction(message)
|
|
||||||
handleAction(action)
|
|
||||||
appStore.dispatch(MessageClicked(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessageDismissed(message: Message) {
|
|
||||||
// Report telemetry event
|
|
||||||
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
|
|
||||||
appStore.dispatch(MessageDismissed(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessageDisplayed(message: Message) {
|
|
||||||
// Report telemetry event
|
|
||||||
// This will be covered on https://github.com/mozilla-mobile/fenix/issues/24224
|
|
||||||
appStore.dispatch(MessageDisplayed(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun handleAction(action: String): Intent {
|
|
||||||
val partialAction = if (action.startsWith("http", ignoreCase = true)) {
|
|
||||||
"://open?url=${Uri.encode(action)}"
|
|
||||||
} else {
|
|
||||||
action
|
|
||||||
}
|
|
||||||
val intent =
|
|
||||||
Intent(Intent.ACTION_VIEW, "${BuildConfig.DEEP_LINK_SCHEME}$partialAction".toUri())
|
|
||||||
homeActivity.processIntent(intent)
|
|
||||||
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
/* Dummy implementation until we provide full implementation.
|
|
||||||
* This will covered on https://github.com/mozilla-mobile/fenix/issues/24222
|
|
||||||
* */
|
|
||||||
class KeyPairMessageMetadataStorage : MessageMetadataStorage {
|
|
||||||
override fun getMetadata(): List<Message.Metadata> {
|
|
||||||
return listOf(
|
|
||||||
Message.Metadata(
|
|
||||||
id = "eu-tracking-protection-for-ireland",
|
|
||||||
displayCount = 0,
|
|
||||||
pressed = false,
|
|
||||||
dismissed = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addMetadata(metadata: Message.Metadata): Message.Metadata {
|
|
||||||
return metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("EmptyFunctionBlock")
|
|
||||||
override fun updateMetadata(metadata: Message.Metadata) {
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
import org.mozilla.fenix.nimbus.MessageData
|
|
||||||
import org.mozilla.fenix.nimbus.StyleData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that holds a representation of GleanPlum message from Nimbus.
|
|
||||||
*
|
|
||||||
* @param id identifies a message as unique.
|
|
||||||
* @param data Data information provided from Nimbus.
|
|
||||||
* @param action A strings that represents which action should be performed
|
|
||||||
* after a message is clicked.
|
|
||||||
* @param style Indicates how a message should be styled.
|
|
||||||
* @param triggers A list of strings corresponding to targeting expressions. The message
|
|
||||||
* will be shown if all expressions `true`.
|
|
||||||
* @param metadata Metadata that help to identify if a message should shown.
|
|
||||||
*/
|
|
||||||
data class Message(
|
|
||||||
val id: String,
|
|
||||||
val data: MessageData,
|
|
||||||
val action: String,
|
|
||||||
val style: StyleData,
|
|
||||||
val triggers: List<String>,
|
|
||||||
val metadata: Metadata
|
|
||||||
) {
|
|
||||||
val maxDisplayCount: Int
|
|
||||||
get() = style.maxDisplayCount
|
|
||||||
|
|
||||||
val priority: Int
|
|
||||||
get() = style.priority
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that holds metadata that help to identify if a message should shown.
|
|
||||||
*
|
|
||||||
* @param id identifies a message as unique.
|
|
||||||
* @param displayCount Indicates how many times a message is displayed.
|
|
||||||
* @param pressed Indicates if a message has been clicked.
|
|
||||||
* @param dismissed Indicates if a message has been closed.
|
|
||||||
*/
|
|
||||||
data class Metadata(
|
|
||||||
val id: String,
|
|
||||||
val displayCount: Int = 0,
|
|
||||||
val pressed: Boolean = false,
|
|
||||||
val dismissed: Boolean = false
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,25 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls all the interactions with a [Message].
|
|
||||||
*/
|
|
||||||
interface MessageController {
|
|
||||||
/**
|
|
||||||
* Indicates the provided [message] was pressed by a user.
|
|
||||||
*/
|
|
||||||
fun onMessagePressed(message: Message)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates the provided [message] was dismissed by a user.
|
|
||||||
*/
|
|
||||||
fun onMessageDismissed(message: Message)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates the provided [message] was displayed to a user.
|
|
||||||
*/
|
|
||||||
fun onMessageDisplayed(message: Message)
|
|
||||||
}
|
|
@ -1,23 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
interface MessageMetadataStorage {
|
|
||||||
/**
|
|
||||||
* Provide all the message metadata saved in the storage.
|
|
||||||
*/
|
|
||||||
fun getMetadata(): List<Message.Metadata>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a [metadata] add the message metadata on the storage.
|
|
||||||
* @return the added message on the [MessageMetadataStorage]
|
|
||||||
*/
|
|
||||||
fun addMetadata(metadata: Message.Metadata): Message.Metadata
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a [metadata] update the message metadata on the storage.
|
|
||||||
*/
|
|
||||||
fun updateMetadata(metadata: Message.Metadata)
|
|
||||||
}
|
|
@ -1,24 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
|
||||||
import org.mozilla.fenix.FeatureFlags
|
|
||||||
import org.mozilla.fenix.components.AppStore
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A message observer that updates the provided.
|
|
||||||
*/
|
|
||||||
class MessagingFeature(val store: AppStore) : LifecycleAwareFeature {
|
|
||||||
|
|
||||||
override fun start() {
|
|
||||||
if (FeatureFlags.messagingFeature) {
|
|
||||||
store.dispatch(MessagingAction.Evaluate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stop() = Unit
|
|
||||||
}
|
|
@ -1,16 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represent all the state related to the Messaging framework.
|
|
||||||
* @param messages Indicates all the available messages.
|
|
||||||
* @param messageToShow Indicates the message that should be shown to users,
|
|
||||||
* if it is null means there is not message that is eligible to be shown to users.
|
|
||||||
*/
|
|
||||||
data class MessagingState(
|
|
||||||
val messages: List<Message> = emptyList(),
|
|
||||||
val messageToShow: Message? = null
|
|
||||||
)
|
|
@ -1,174 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import mozilla.components.support.base.log.logger.Logger
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.mozilla.experiments.nimbus.GleanPlumbInterface
|
|
||||||
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
|
|
||||||
import org.mozilla.experiments.nimbus.internal.FeatureHolder
|
|
||||||
import org.mozilla.experiments.nimbus.internal.NimbusException
|
|
||||||
|
|
||||||
import org.mozilla.fenix.nimbus.Messaging
|
|
||||||
import org.mozilla.fenix.nimbus.StyleData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides messages from [messagingFeature] and combine with the metadata store on [metadataStorage].
|
|
||||||
*/
|
|
||||||
class NimbusMessagingStorage(
|
|
||||||
private val context: Context,
|
|
||||||
private val metadataStorage: MessageMetadataStorage,
|
|
||||||
private val gleanPlumb: GleanPlumbInterface,
|
|
||||||
private val messagingFeature: FeatureHolder<Messaging>
|
|
||||||
) {
|
|
||||||
private val logger = Logger("MessagingStorage")
|
|
||||||
private val nimbusFeature = messagingFeature.value()
|
|
||||||
private val customAttributes: JSONObject
|
|
||||||
get() = JSONObject()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of available messages descending sorted by their priority.
|
|
||||||
*/
|
|
||||||
fun getMessages(): List<Message> {
|
|
||||||
val nimbusTriggers = nimbusFeature.triggers
|
|
||||||
val nimbusStyles = nimbusFeature.styles
|
|
||||||
val nimbusActions = nimbusFeature.actions
|
|
||||||
|
|
||||||
val nimbusMessages = nimbusFeature.messages
|
|
||||||
val defaultStyle = StyleData(context)
|
|
||||||
val storageMetadata = metadataStorage.getMetadata().associateBy {
|
|
||||||
it.id
|
|
||||||
}
|
|
||||||
|
|
||||||
return nimbusMessages.mapNotNull { (key, value) ->
|
|
||||||
val action = sanitizeAction(value.action, nimbusActions) ?: return@mapNotNull null
|
|
||||||
Message(
|
|
||||||
id = key,
|
|
||||||
data = value,
|
|
||||||
action = action,
|
|
||||||
style = nimbusStyles[value.style] ?: defaultStyle,
|
|
||||||
metadata = storageMetadata[key] ?: addMetadata(key),
|
|
||||||
triggers = sanitizeTriggers(value.trigger, nimbusTriggers) ?: return@mapNotNull null
|
|
||||||
)
|
|
||||||
}.filter {
|
|
||||||
it.data.maxDisplayCount >= it.metadata.displayCount &&
|
|
||||||
!it.metadata.dismissed &&
|
|
||||||
!it.metadata.pressed
|
|
||||||
}.sortedByDescending {
|
|
||||||
it.style.priority
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next higher priority message which all their triggers are true.
|
|
||||||
*/
|
|
||||||
fun getNextMessage(availableMessages: List<Message>): Message? {
|
|
||||||
val helper = gleanPlumb.createMessageHelper(customAttributes)
|
|
||||||
var message = availableMessages.firstOrNull {
|
|
||||||
isMessageEligible(it, helper)
|
|
||||||
} ?: return null
|
|
||||||
|
|
||||||
if (isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) {
|
|
||||||
messagingFeature.recordExposure()
|
|
||||||
|
|
||||||
if (message.data.isControl) {
|
|
||||||
message = availableMessages.firstOrNull {
|
|
||||||
!it.data.isControl && isMessageEligible(it, helper)
|
|
||||||
} ?: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a valid action for the provided [message].
|
|
||||||
*/
|
|
||||||
fun getMessageAction(message: Message): String {
|
|
||||||
val helper = gleanPlumb.createMessageHelper(customAttributes)
|
|
||||||
val uuid = helper.getUuid(message.action)
|
|
||||||
|
|
||||||
return helper.stringFormat(message.action, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updated the provided [metadata] in the storage.
|
|
||||||
*/
|
|
||||||
fun updateMetadata(metadata: Message.Metadata) {
|
|
||||||
metadataStorage.updateMetadata(metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun sanitizeAction(
|
|
||||||
unsafeAction: String,
|
|
||||||
nimbusActions: Map<String, String>
|
|
||||||
): String? {
|
|
||||||
return if (unsafeAction.startsWith("http")) {
|
|
||||||
unsafeAction
|
|
||||||
} else {
|
|
||||||
val safeAction = nimbusActions[unsafeAction]
|
|
||||||
if (safeAction.isNullOrBlank() || safeAction.isEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
safeAction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun sanitizeTriggers(
|
|
||||||
unsafeTriggers: List<String>,
|
|
||||||
nimbusTriggers: Map<String, String>
|
|
||||||
): List<String>? {
|
|
||||||
return unsafeTriggers.map {
|
|
||||||
val safeTrigger = nimbusTriggers[it]
|
|
||||||
if (safeTrigger.isNullOrBlank() || safeTrigger.isEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
safeTrigger
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean {
|
|
||||||
return when {
|
|
||||||
expression.isNullOrBlank() -> {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
expression.endsWith("-") -> {
|
|
||||||
message.id.startsWith(expression)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
message.id == expression
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun isMessageEligible(
|
|
||||||
message: Message,
|
|
||||||
helper: GleanPlumbMessageHelper
|
|
||||||
): Boolean {
|
|
||||||
return message.triggers.all { condition ->
|
|
||||||
try {
|
|
||||||
helper.evalJexl(condition)
|
|
||||||
} catch (e: NimbusException.EvaluationException) {
|
|
||||||
// Report to glean as malformed message
|
|
||||||
// Will be addressed on https://github.com/mozilla-mobile/fenix/issues/24224
|
|
||||||
logger.info("Unable to evaluate $condition")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addMetadata(id: String): Message.Metadata {
|
|
||||||
// This will be improve on https://github.com/mozilla-mobile/fenix/issues/24222
|
|
||||||
return metadataStorage.addMetadata(
|
|
||||||
Message.Metadata(
|
|
||||||
id = id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,139 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb.state
|
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import mozilla.components.lib.state.Middleware
|
|
||||||
import mozilla.components.lib.state.MiddlewareContext
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Evaluate
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
|
|
||||||
import org.mozilla.fenix.components.appstate.AppState
|
|
||||||
import org.mozilla.fenix.gleanplumb.Message
|
|
||||||
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
|
|
||||||
|
|
||||||
typealias AppStoreMiddlewareContext = MiddlewareContext<AppState, AppAction>
|
|
||||||
|
|
||||||
class MessagingMiddleware(
|
|
||||||
private val messagingStorage: NimbusMessagingStorage
|
|
||||||
) : Middleware<AppState, AppAction> {
|
|
||||||
|
|
||||||
override fun invoke(
|
|
||||||
context: AppStoreMiddlewareContext,
|
|
||||||
next: (AppAction) -> Unit,
|
|
||||||
action: AppAction
|
|
||||||
) {
|
|
||||||
when (action) {
|
|
||||||
is Restore -> {
|
|
||||||
val messages = messagingStorage.getMessages()
|
|
||||||
|
|
||||||
context.dispatch(UpdateMessages(messages))
|
|
||||||
}
|
|
||||||
|
|
||||||
is Evaluate -> {
|
|
||||||
val message = messagingStorage.getNextMessage(context.state.messaging.messages)
|
|
||||||
if (message != null) {
|
|
||||||
context.dispatch(UpdateMessageToShow(message))
|
|
||||||
} else {
|
|
||||||
context.dispatch(ConsumeMessageToShow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is MessageClicked -> onMessageClicked(action.message, context)
|
|
||||||
|
|
||||||
is MessageDismissed -> onMessageDismissed(context, action.message)
|
|
||||||
|
|
||||||
is MessageDisplayed -> onMessagedDisplayed(action.message, context)
|
|
||||||
}
|
|
||||||
next(action)
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun onMessagedDisplayed(
|
|
||||||
oldMessage: Message,
|
|
||||||
context: AppStoreMiddlewareContext
|
|
||||||
) {
|
|
||||||
val newMetadata = oldMessage.metadata.copy(
|
|
||||||
displayCount = oldMessage.metadata.displayCount + 1
|
|
||||||
)
|
|
||||||
val newMessage = oldMessage.copy(
|
|
||||||
metadata = newMetadata
|
|
||||||
)
|
|
||||||
val newMessages = if (newMetadata.displayCount < oldMessage.maxDisplayCount) {
|
|
||||||
updateMessage(context, oldMessage, newMessage)
|
|
||||||
} else {
|
|
||||||
consumeMessageToShowIfNeeded(context, oldMessage)
|
|
||||||
removeMessage(context, oldMessage)
|
|
||||||
}
|
|
||||||
context.dispatch(UpdateMessages(newMessages))
|
|
||||||
messagingStorage.updateMetadata(newMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun onMessageDismissed(
|
|
||||||
context: AppStoreMiddlewareContext,
|
|
||||||
message: Message
|
|
||||||
) {
|
|
||||||
val newMessages = removeMessage(context, message)
|
|
||||||
val updatedMetadata = message.metadata.copy(dismissed = true)
|
|
||||||
|
|
||||||
messagingStorage.updateMetadata(updatedMetadata)
|
|
||||||
context.dispatch(UpdateMessages(newMessages))
|
|
||||||
consumeMessageToShowIfNeeded(context, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun onMessageClicked(
|
|
||||||
message: Message,
|
|
||||||
context: AppStoreMiddlewareContext
|
|
||||||
) {
|
|
||||||
// Update Nimbus storage.
|
|
||||||
val updatedMetadata = message.metadata.copy(pressed = true)
|
|
||||||
messagingStorage.updateMetadata(updatedMetadata)
|
|
||||||
|
|
||||||
// Update app state.
|
|
||||||
val newMessages = removeMessage(context, message)
|
|
||||||
context.dispatch(UpdateMessages(newMessages))
|
|
||||||
consumeMessageToShowIfNeeded(context, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun consumeMessageToShowIfNeeded(
|
|
||||||
context: AppStoreMiddlewareContext,
|
|
||||||
message: Message
|
|
||||||
) {
|
|
||||||
if (context.state.messaging.messageToShow?.id == message.id) {
|
|
||||||
context.dispatch(ConsumeMessageToShow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun removeMessage(
|
|
||||||
context: AppStoreMiddlewareContext,
|
|
||||||
message: Message
|
|
||||||
): List<Message> {
|
|
||||||
return context.state.messaging.messages.filter { it.id != message.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun updateMessage(
|
|
||||||
context: AppStoreMiddlewareContext,
|
|
||||||
oldMessage: Message,
|
|
||||||
updatedMessage: Message
|
|
||||||
): List<Message> {
|
|
||||||
val actualMessageToShow = context.state.messaging.messageToShow
|
|
||||||
|
|
||||||
if (actualMessageToShow?.id == oldMessage.id) {
|
|
||||||
context.dispatch(UpdateMessageToShow(updatedMessage))
|
|
||||||
}
|
|
||||||
return removeMessage(context, oldMessage) + updatedMessage
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb.state
|
|
||||||
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
|
|
||||||
import org.mozilla.fenix.components.appstate.AppState
|
|
||||||
import org.mozilla.fenix.gleanplumb.MessagingState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reducer for [MessagingState].
|
|
||||||
*/
|
|
||||||
internal object MessagingReducer {
|
|
||||||
fun reduce(state: AppState, action: AppAction.MessagingAction): AppState = when (action) {
|
|
||||||
is UpdateMessageToShow -> {
|
|
||||||
state.copy(
|
|
||||||
messaging = state.messaging.copy(
|
|
||||||
messageToShow = action.message
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is UpdateMessages -> {
|
|
||||||
state.copy(
|
|
||||||
messaging = state.messaging.copy(
|
|
||||||
messages = action.messages
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is ConsumeMessageToShow -> {
|
|
||||||
state.copy(
|
|
||||||
messaging = state.messaging.copy(
|
|
||||||
messageToShow = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> state
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,40 @@
|
|||||||
|
/* 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.home.sessioncontrol.viewholders.onboarding
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.databinding.ExperimentDefaultBrowserBinding
|
||||||
|
import org.mozilla.fenix.ext.increaseTapArea
|
||||||
|
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View holder for the default browser card.
|
||||||
|
*/
|
||||||
|
class ExperimentDefaultBrowserCardViewHolder(
|
||||||
|
view: View,
|
||||||
|
private val interactor: SessionControlInteractor
|
||||||
|
) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
val binding = ExperimentDefaultBrowserBinding.bind(view)
|
||||||
|
binding.setDefaultBrowser.setOnClickListener {
|
||||||
|
interactor.onSetDefaultBrowserClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.close.apply {
|
||||||
|
increaseTapArea(CLOSE_BUTTON_EXTRA_DPS)
|
||||||
|
setOnClickListener {
|
||||||
|
interactor.onCloseExperimentCardClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal const val LAYOUT_ID = R.layout.experiment_default_browser
|
||||||
|
private const val CLOSE_BUTTON_EXTRA_DPS = 38
|
||||||
|
}
|
||||||
|
}
|
@ -1,57 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.databinding.NimbusMessageCardBinding
|
|
||||||
import org.mozilla.fenix.ext.increaseTapArea
|
|
||||||
import org.mozilla.fenix.gleanplumb.Message
|
|
||||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
|
|
||||||
|
|
||||||
class MessageCardViewHolder(
|
|
||||||
view: View,
|
|
||||||
private val interactor: SessionControlInteractor
|
|
||||||
) : RecyclerView.ViewHolder(view) {
|
|
||||||
|
|
||||||
fun bind(message: Message) {
|
|
||||||
val binding = NimbusMessageCardBinding.bind(itemView)
|
|
||||||
|
|
||||||
if (message.data.title.isNullOrBlank()) {
|
|
||||||
binding.titleText.isVisible = false
|
|
||||||
} else {
|
|
||||||
binding.titleText.text = message.data.title
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.descriptionText.text = message.data.text
|
|
||||||
|
|
||||||
if (message.data.buttonLabel.isNullOrBlank()) {
|
|
||||||
binding.messageButton.isVisible = false
|
|
||||||
binding.experimentCard.setOnClickListener {
|
|
||||||
interactor.onMessageClicked(message)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.messageButton.text = message.data.buttonLabel
|
|
||||||
binding.messageButton.setOnClickListener {
|
|
||||||
interactor.onMessageClicked(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.close.apply {
|
|
||||||
increaseTapArea(CLOSE_BUTTON_EXTRA_DPS)
|
|
||||||
setOnClickListener {
|
|
||||||
interactor.onMessageClosedClicked(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
interactor.onMessageDisplayed(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
internal const val LAYOUT_ID = R.layout.nimbus_message_card
|
|
||||||
private const val CLOSE_BUTTON_EXTRA_DPS = 38
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.spyk
|
|
||||||
import io.mockk.verify
|
|
||||||
import mozilla.components.support.test.robolectric.testContext
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mozilla.fenix.BuildConfig
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
import org.mozilla.fenix.components.AppStore
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
|
|
||||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
|
||||||
import org.mozilla.fenix.nimbus.MessageData
|
|
||||||
|
|
||||||
@RunWith(FenixRobolectricTestRunner::class)
|
|
||||||
class DefaultMessageControllerTest {
|
|
||||||
|
|
||||||
private val activity: HomeActivity = mockk(relaxed = true)
|
|
||||||
private val storageNimbus: NimbusMessagingStorage = mockk(relaxed = true)
|
|
||||||
private lateinit var controller: DefaultMessageController
|
|
||||||
private val store: AppStore = mockk(relaxed = true)
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
controller = DefaultMessageController(
|
|
||||||
messagingStorage = storageNimbus,
|
|
||||||
appStore = store,
|
|
||||||
homeActivity = activity
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN calling onMessagePressed THEN update the store and handle the action`() {
|
|
||||||
val customController = spyk(controller)
|
|
||||||
every { customController.handleAction(any()) } returns mockk()
|
|
||||||
|
|
||||||
val message = mockMessage()
|
|
||||||
|
|
||||||
customController.onMessagePressed(message)
|
|
||||||
|
|
||||||
verify { customController.handleAction(any()) }
|
|
||||||
verify { store.dispatch(MessageClicked(message)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN an URL WHEN calling handleAction THEN process the intent with an open uri`() {
|
|
||||||
val intent = controller.handleAction("http://mozilla.org")
|
|
||||||
|
|
||||||
verify { activity.processIntent(any()) }
|
|
||||||
|
|
||||||
val encodedUrl = Uri.encode("http://mozilla.org")
|
|
||||||
assertEquals(
|
|
||||||
"${BuildConfig.DEEP_LINK_SCHEME}://open?url=$encodedUrl",
|
|
||||||
intent.data.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN an deeplink WHEN calling handleAction THEN process the intent with an deeplink uri`() {
|
|
||||||
val intent = controller.handleAction("://settings_privacy")
|
|
||||||
|
|
||||||
verify { activity.processIntent(any()) }
|
|
||||||
|
|
||||||
assertEquals("${BuildConfig.DEEP_LINK_SCHEME}://settings_privacy", intent.data.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN calling onMessageDismissed THEN report to the messageManager`() {
|
|
||||||
val message = mockMessage()
|
|
||||||
|
|
||||||
controller.onMessageDismissed(message)
|
|
||||||
|
|
||||||
verify { store.dispatch(AppAction.MessagingAction.MessageDismissed(message)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() {
|
|
||||||
val message = mockMessage()
|
|
||||||
|
|
||||||
controller.onMessageDisplayed(message)
|
|
||||||
|
|
||||||
verify { store.dispatch(MessageDisplayed(message)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mockMessage() = Message(
|
|
||||||
id = "id",
|
|
||||||
data = MessageData(_context = testContext),
|
|
||||||
style = mockk(),
|
|
||||||
action = "action",
|
|
||||||
triggers = emptyList(),
|
|
||||||
metadata = Message.Metadata(
|
|
||||||
id = "id",
|
|
||||||
displayCount = 0,
|
|
||||||
pressed = false,
|
|
||||||
dismissed = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,46 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockkObject
|
|
||||||
import io.mockk.spyk
|
|
||||||
import io.mockk.unmockkObject
|
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
|
||||||
import mozilla.components.support.test.mock
|
|
||||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.Config
|
|
||||||
import org.mozilla.fenix.FeatureFlags
|
|
||||||
import org.mozilla.fenix.components.AppStore
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
|
||||||
|
|
||||||
class MessagingFeatureTest {
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
@get:Rule
|
|
||||||
val coroutinesTestRule = MainCoroutineRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN start is called THEN evaluate messages`() {
|
|
||||||
val store: AppStore = spyk(AppStore())
|
|
||||||
val binding = MessagingFeature(store)
|
|
||||||
|
|
||||||
mockkObject(FeatureFlags)
|
|
||||||
every { FeatureFlags.messagingFeature } returns true
|
|
||||||
|
|
||||||
binding.start()
|
|
||||||
|
|
||||||
store.dispatch(UpdateMessageToShow(mock()))
|
|
||||||
store.waitUntilIdle()
|
|
||||||
|
|
||||||
verify { store.dispatch(MessagingAction.Evaluate) }
|
|
||||||
|
|
||||||
unmockkObject(Config)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,487 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb
|
|
||||||
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.spyk
|
|
||||||
import io.mockk.verify
|
|
||||||
import mozilla.components.support.test.mock
|
|
||||||
import mozilla.components.support.test.robolectric.testContext
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mozilla.experiments.nimbus.GleanPlumbInterface
|
|
||||||
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
|
|
||||||
import org.mozilla.experiments.nimbus.internal.FeatureHolder
|
|
||||||
import org.mozilla.experiments.nimbus.internal.NimbusException
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
|
||||||
import org.mozilla.fenix.nimbus.MessageData
|
|
||||||
import org.mozilla.fenix.nimbus.Messaging
|
|
||||||
import org.mozilla.fenix.nimbus.StyleData
|
|
||||||
|
|
||||||
@RunWith(FenixRobolectricTestRunner::class)
|
|
||||||
class NimbusMessagingStorageTest {
|
|
||||||
|
|
||||||
private val activity: HomeActivity = mockk(relaxed = true)
|
|
||||||
private val storageNimbus: NimbusMessagingStorage = mockk(relaxed = true)
|
|
||||||
private lateinit var storage: NimbusMessagingStorage
|
|
||||||
private lateinit var metadataStorage: MessageMetadataStorage
|
|
||||||
private lateinit var gleanPlumb: GleanPlumbInterface
|
|
||||||
private lateinit var messagingFeature: FeatureHolder<Messaging>
|
|
||||||
private lateinit var messaging: Messaging
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
gleanPlumb = mockk(relaxed = true)
|
|
||||||
metadataStorage = mockk(relaxed = true)
|
|
||||||
|
|
||||||
messagingFeature = createMessagingFeature()
|
|
||||||
|
|
||||||
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
|
|
||||||
|
|
||||||
storage = NimbusMessagingStorage(
|
|
||||||
testContext,
|
|
||||||
metadataStorage,
|
|
||||||
gleanPlumb,
|
|
||||||
messagingFeature
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN calling getMessages THEN provide a list of available messages`() {
|
|
||||||
val message = storage.getMessages().first()
|
|
||||||
|
|
||||||
assertEquals("message-1", message.id)
|
|
||||||
assertEquals("message-1", message.metadata.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN calling getMessages THEN provide a list of sorted messages by priority`() {
|
|
||||||
val messages = mapOf(
|
|
||||||
"low-message" to createMessageData(style = "low-priority"),
|
|
||||||
"high-message" to createMessageData(style = "high-priority"),
|
|
||||||
"medium-message" to createMessageData(style = "medium-priority"),
|
|
||||||
)
|
|
||||||
val styles = mapOf(
|
|
||||||
"high-priority" to createStyle(priority = 100),
|
|
||||||
"medium-priority" to createStyle(priority = 50),
|
|
||||||
"low-priority" to createStyle(priority = 1)
|
|
||||||
)
|
|
||||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
|
||||||
val messagingFeature = createMessagingFeature(
|
|
||||||
styles = styles,
|
|
||||||
messages = messages
|
|
||||||
)
|
|
||||||
|
|
||||||
every { metadataStorage.getMetadata() } returns listOf(Message.Metadata(id = "message-1"))
|
|
||||||
|
|
||||||
val storage = NimbusMessagingStorage(
|
|
||||||
testContext,
|
|
||||||
metadataStorage,
|
|
||||||
gleanPlumb,
|
|
||||||
messagingFeature
|
|
||||||
)
|
|
||||||
|
|
||||||
val results = storage.getMessages()
|
|
||||||
|
|
||||||
assertEquals("high-message", results[0].id)
|
|
||||||
assertEquals("medium-message", results[1].id)
|
|
||||||
assertEquals("low-message", results[2].id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN pressed message WHEN calling getMessages THEN filter out the pressed message`() {
|
|
||||||
val metadataList = listOf(
|
|
||||||
Message.Metadata(id = "pressed-message", pressed = true),
|
|
||||||
Message.Metadata(id = "normal-message", pressed = false)
|
|
||||||
)
|
|
||||||
val messages = mapOf(
|
|
||||||
"pressed-message" to createMessageData(style = "high-priority"),
|
|
||||||
"normal-message" to createMessageData(style = "high-priority"),
|
|
||||||
)
|
|
||||||
val styles = mapOf(
|
|
||||||
"high-priority" to createStyle(priority = 100),
|
|
||||||
)
|
|
||||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
|
||||||
val messagingFeature = createMessagingFeature(
|
|
||||||
styles = styles,
|
|
||||||
messages = messages
|
|
||||||
)
|
|
||||||
|
|
||||||
every { metadataStorage.getMetadata() } returns metadataList
|
|
||||||
|
|
||||||
val storage = NimbusMessagingStorage(
|
|
||||||
testContext,
|
|
||||||
metadataStorage,
|
|
||||||
gleanPlumb,
|
|
||||||
messagingFeature
|
|
||||||
)
|
|
||||||
|
|
||||||
val results = storage.getMessages()
|
|
||||||
|
|
||||||
assertEquals(1, results.size)
|
|
||||||
assertEquals("normal-message", results[0].id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN dismissed message WHEN calling getMessages THEN filter out the dismissed message`() {
|
|
||||||
val metadataList = listOf(
|
|
||||||
Message.Metadata(id = "dismissed-message", dismissed = true),
|
|
||||||
Message.Metadata(id = "normal-message", dismissed = false)
|
|
||||||
)
|
|
||||||
val messages = mapOf(
|
|
||||||
"dismissed-message" to createMessageData(style = "high-priority"),
|
|
||||||
"normal-message" to createMessageData(style = "high-priority"),
|
|
||||||
)
|
|
||||||
val styles = mapOf(
|
|
||||||
"high-priority" to createStyle(priority = 100),
|
|
||||||
)
|
|
||||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
|
||||||
val messagingFeature = createMessagingFeature(
|
|
||||||
styles = styles,
|
|
||||||
messages = messages
|
|
||||||
)
|
|
||||||
|
|
||||||
every { metadataStorage.getMetadata() } returns metadataList
|
|
||||||
|
|
||||||
val storage = NimbusMessagingStorage(
|
|
||||||
testContext,
|
|
||||||
metadataStorage,
|
|
||||||
gleanPlumb,
|
|
||||||
messagingFeature
|
|
||||||
)
|
|
||||||
|
|
||||||
val results = storage.getMessages()
|
|
||||||
|
|
||||||
assertEquals(1, results.size)
|
|
||||||
assertEquals("normal-message", results[0].id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a message that the maxDisplayCount WHEN calling getMessages THEN filter out the message`() {
|
|
||||||
val metadataList = listOf(
|
|
||||||
Message.Metadata(id = "shown-many-times-message", displayCount = 10),
|
|
||||||
Message.Metadata(id = "normal-message", displayCount = 0)
|
|
||||||
)
|
|
||||||
val messages = mapOf(
|
|
||||||
"shown-many-times-message" to createMessageData(
|
|
||||||
style = "high-priority",
|
|
||||||
maxDisplayCount = 2
|
|
||||||
),
|
|
||||||
"normal-message" to createMessageData(style = "high-priority"),
|
|
||||||
)
|
|
||||||
val styles = mapOf(
|
|
||||||
"high-priority" to createStyle(priority = 100),
|
|
||||||
)
|
|
||||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
|
||||||
val messagingFeature = createMessagingFeature(
|
|
||||||
styles = styles,
|
|
||||||
messages = messages
|
|
||||||
)
|
|
||||||
|
|
||||||
every { metadataStorage.getMetadata() } returns metadataList
|
|
||||||
|
|
||||||
val storage = NimbusMessagingStorage(
|
|
||||||
testContext,
|
|
||||||
metadataStorage,
|
|
||||||
gleanPlumb,
|
|
||||||
messagingFeature
|
|
||||||
)
|
|
||||||
|
|
||||||
val results = storage.getMessages()
|
|
||||||
|
|
||||||
assertEquals(1, results.size)
|
|
||||||
assertEquals("normal-message", results[0].id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a malformed message WHEN calling getMessages THEN provide a list of messages ignoring the malformed one`() {
|
|
||||||
val messages = storage.getMessages()
|
|
||||||
val firstMessage = messages.first()
|
|
||||||
|
|
||||||
assertEquals("message-1", firstMessage.id)
|
|
||||||
assertEquals("message-1", firstMessage.metadata.id)
|
|
||||||
assertTrue(messages.size == 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a malformed action WHEN calling sanitizeAction THEN return null`() {
|
|
||||||
val actionsMap = mapOf("action-1" to "action-1-url")
|
|
||||||
|
|
||||||
val notFoundAction = storage.sanitizeAction("no-found-action", actionsMap)
|
|
||||||
val emptyAction = storage.sanitizeAction("", actionsMap)
|
|
||||||
val blankAction = storage.sanitizeAction(" ", actionsMap)
|
|
||||||
|
|
||||||
assertNull(notFoundAction)
|
|
||||||
assertNull(emptyAction)
|
|
||||||
assertNull(blankAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() {
|
|
||||||
|
|
||||||
storage.updateMetadata(mockk())
|
|
||||||
|
|
||||||
verify { metadataStorage.updateMetadata(any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a valid action WHEN calling sanitizeAction THEN return the action`() {
|
|
||||||
val actionsMap = mapOf("action-1" to "action-1-url")
|
|
||||||
|
|
||||||
val validAction = storage.sanitizeAction("action-1", actionsMap)
|
|
||||||
|
|
||||||
assertEquals("action-1-url", validAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a trigger action WHEN calling sanitizeTriggers THEN return null`() {
|
|
||||||
val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
|
|
||||||
|
|
||||||
val notFoundTrigger = storage.sanitizeTriggers(listOf("no-found-trigger"), triggersMap)
|
|
||||||
val emptyTrigger = storage.sanitizeTriggers(listOf(""), triggersMap)
|
|
||||||
val blankTrigger = storage.sanitizeTriggers(listOf(" "), triggersMap)
|
|
||||||
|
|
||||||
assertNull(notFoundTrigger)
|
|
||||||
assertNull(emptyTrigger)
|
|
||||||
assertNull(blankTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a valid trigger WHEN calling sanitizeAction THEN return the trigger`() {
|
|
||||||
val triggersMap = mapOf("trigger-1" to "trigger-1-expression")
|
|
||||||
|
|
||||||
val validTrigger = storage.sanitizeTriggers(listOf("trigger-1"), triggersMap)
|
|
||||||
|
|
||||||
assertEquals(listOf("trigger-1-expression"), validTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() {
|
|
||||||
val message = Message(
|
|
||||||
"id", mockk(),
|
|
||||||
action = "action",
|
|
||||||
mock(),
|
|
||||||
emptyList(),
|
|
||||||
Message.Metadata("id")
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = storage.isMessageUnderExperiment(message, null)
|
|
||||||
|
|
||||||
assertFalse(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() {
|
|
||||||
val message = Message(
|
|
||||||
"end-", mockk(),
|
|
||||||
action = "action",
|
|
||||||
mock(),
|
|
||||||
emptyList(),
|
|
||||||
Message.Metadata("end-")
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = storage.isMessageUnderExperiment(message, "end-")
|
|
||||||
|
|
||||||
assertTrue(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() {
|
|
||||||
val message = Message(
|
|
||||||
"same-id", mockk(),
|
|
||||||
action = "action",
|
|
||||||
mock(),
|
|
||||||
emptyList(),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = storage.isMessageUnderExperiment(message, "same-id")
|
|
||||||
|
|
||||||
assertTrue(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() {
|
|
||||||
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
|
|
||||||
val message = Message(
|
|
||||||
"same-id", mockk(),
|
|
||||||
action = "action",
|
|
||||||
mock(),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
every { helper.evalJexl(any()) } returns true
|
|
||||||
|
|
||||||
val result = storage.isMessageEligible(message, helper)
|
|
||||||
|
|
||||||
assertTrue(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() {
|
|
||||||
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
|
|
||||||
val message = Message(
|
|
||||||
"same-id", mockk(),
|
|
||||||
action = "action",
|
|
||||||
mock(),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
every { helper.evalJexl(any()) } throws NimbusException.EvaluationException("")
|
|
||||||
|
|
||||||
val result = storage.isMessageEligible(message, helper)
|
|
||||||
|
|
||||||
assertFalse(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() {
|
|
||||||
val spiedStorage = spyk(storage)
|
|
||||||
val message = Message(
|
|
||||||
"same-id", mockk(),
|
|
||||||
action = "action",
|
|
||||||
mock(),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
every { spiedStorage.isMessageEligible(any(), any()) } returns false
|
|
||||||
|
|
||||||
val result = spiedStorage.getNextMessage(listOf(message))
|
|
||||||
|
|
||||||
assertNull(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() {
|
|
||||||
val spiedStorage = spyk(storage)
|
|
||||||
val message = Message(
|
|
||||||
"same-id", mockk(),
|
|
||||||
action = "action",
|
|
||||||
mock(),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
every { spiedStorage.isMessageEligible(any(), any()) } returns true
|
|
||||||
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns false
|
|
||||||
|
|
||||||
val result = spiedStorage.getNextMessage(listOf(message))
|
|
||||||
|
|
||||||
assertEquals(message.id, result!!.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a message under experiment WHEN calling getNextMessage THEN call recordExposure`() {
|
|
||||||
val spiedStorage = spyk(storage)
|
|
||||||
val messageData: MessageData = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messageData.isControl } returns false
|
|
||||||
|
|
||||||
val message = Message(
|
|
||||||
"same-id",
|
|
||||||
messageData,
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
every { spiedStorage.isMessageEligible(any(), any()) } returns true
|
|
||||||
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true
|
|
||||||
|
|
||||||
val result = spiedStorage.getNextMessage(listOf(message))
|
|
||||||
|
|
||||||
verify { messagingFeature.recordExposure() }
|
|
||||||
assertEquals(message.id, result!!.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a control message WHEN calling getNextMessage THEN return the next eligible message`() {
|
|
||||||
val spiedStorage = spyk(storage)
|
|
||||||
val messageData: MessageData = mockk(relaxed = true)
|
|
||||||
val controlMessageData: MessageData = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messageData.isControl } returns false
|
|
||||||
every { controlMessageData.isControl } returns true
|
|
||||||
|
|
||||||
val message = Message(
|
|
||||||
"id",
|
|
||||||
messageData,
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
val controlMessage = Message(
|
|
||||||
"control-id",
|
|
||||||
controlMessageData,
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
every { spiedStorage.isMessageEligible(any(), any()) } returns true
|
|
||||||
every { spiedStorage.isMessageUnderExperiment(any(), any()) } returns true
|
|
||||||
|
|
||||||
val result = spiedStorage.getNextMessage(listOf(controlMessage, message))
|
|
||||||
|
|
||||||
verify { messagingFeature.recordExposure() }
|
|
||||||
assertEquals(message.id, result!!.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMessageData(
|
|
||||||
action: String = "action-1",
|
|
||||||
style: String = "style-1",
|
|
||||||
triggers: List<String> = listOf("trigger-1"),
|
|
||||||
maxDisplayCount: Int = 5
|
|
||||||
): MessageData {
|
|
||||||
val messageData1: MessageData = mockk(relaxed = true)
|
|
||||||
every { messageData1.action } returns action
|
|
||||||
every { messageData1.style } returns style
|
|
||||||
every { messageData1.trigger } returns triggers
|
|
||||||
every { messageData1.maxDisplayCount } returns maxDisplayCount
|
|
||||||
return messageData1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMessagingFeature(
|
|
||||||
triggers: Map<String, String> = mapOf("trigger-1" to "trigger-1-expression"),
|
|
||||||
styles: Map<String, StyleData> = mapOf("style-1" to createStyle()),
|
|
||||||
actions: Map<String, String> = mapOf("action-1" to "action-1-url"),
|
|
||||||
messages: Map<String, MessageData> = mapOf(
|
|
||||||
"message-1" to createMessageData(),
|
|
||||||
"malformed" to mockk(relaxed = true)
|
|
||||||
),
|
|
||||||
): FeatureHolder<Messaging> {
|
|
||||||
val messagingFeature: FeatureHolder<Messaging> = mockk(relaxed = true)
|
|
||||||
|
|
||||||
messaging = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messaging.triggers } returns triggers
|
|
||||||
every { messaging.styles } returns styles
|
|
||||||
every { messaging.actions } returns actions
|
|
||||||
every { messaging.messages } returns messages
|
|
||||||
|
|
||||||
every { messagingFeature.value() } returns messaging
|
|
||||||
return messagingFeature
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createStyle(priority: Int = 1): StyleData {
|
|
||||||
val style1: StyleData = mockk(relaxed = true)
|
|
||||||
every { style1.priority } returns priority
|
|
||||||
return style1
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,327 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb.state
|
|
||||||
|
|
||||||
import io.mockk.Runs
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.spyk
|
|
||||||
import io.mockk.verify
|
|
||||||
import mozilla.components.lib.state.MiddlewareContext
|
|
||||||
import mozilla.components.service.glean.testing.GleanTestRule
|
|
||||||
import mozilla.components.support.test.robolectric.testContext
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mozilla.fenix.components.AppStore
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Evaluate
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDisplayed
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
|
|
||||||
import org.mozilla.fenix.components.appstate.AppState
|
|
||||||
import org.mozilla.fenix.gleanplumb.Message
|
|
||||||
import org.mozilla.fenix.gleanplumb.MessagingState
|
|
||||||
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
|
|
||||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
|
||||||
import org.mozilla.fenix.nimbus.MessageData
|
|
||||||
|
|
||||||
@RunWith(FenixRobolectricTestRunner::class)
|
|
||||||
class MessagingMiddlewareTest {
|
|
||||||
|
|
||||||
private lateinit var store: AppStore
|
|
||||||
private lateinit var middleware: MessagingMiddleware
|
|
||||||
private lateinit var messagingStorage: NimbusMessagingStorage
|
|
||||||
private lateinit var middlewareContext: MiddlewareContext<AppState, AppAction>
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val gleanTestRule = GleanTestRule(testContext)
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
messagingStorage = mockk(relaxed = true)
|
|
||||||
middlewareContext = mockk(relaxed = true)
|
|
||||||
middleware = MessagingMiddleware(
|
|
||||||
messagingStorage
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN Restore THEN get messages from the storage and UpdateMessages`() {
|
|
||||||
val messages: List<Message> = emptyList()
|
|
||||||
|
|
||||||
every { messagingStorage.getMessages() } returns messages
|
|
||||||
|
|
||||||
middleware.invoke(middlewareContext, {}, Restore)
|
|
||||||
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessages(messages)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN Restore THEN getNextMessage from the storage and UpdateMessageToShow`() {
|
|
||||||
val message: Message = mockk(relaxed = true)
|
|
||||||
val appState: AppState = mockk(relaxed = true)
|
|
||||||
val messagingState: MessagingState = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messagingState.messages } returns emptyList()
|
|
||||||
every { appState.messaging } returns messagingState
|
|
||||||
every { middlewareContext.state } returns appState
|
|
||||||
every { messagingStorage.getNextMessage(any()) } returns message
|
|
||||||
|
|
||||||
middleware.invoke(middlewareContext, {}, Evaluate)
|
|
||||||
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessageToShow(message)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN MessageClicked THEN update storage`() {
|
|
||||||
val message = Message(
|
|
||||||
"control-id",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
val appState: AppState = mockk(relaxed = true)
|
|
||||||
val messagingState: MessagingState = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messagingState.messages } returns emptyList()
|
|
||||||
every { appState.messaging } returns messagingState
|
|
||||||
every { middlewareContext.state } returns appState
|
|
||||||
|
|
||||||
middleware.invoke(middlewareContext, {}, MessageClicked(message))
|
|
||||||
|
|
||||||
verify { messagingStorage.updateMetadata(message.metadata.copy(pressed = true)) }
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN MessageDismissed THEN update storage`() {
|
|
||||||
val message = Message(
|
|
||||||
"control-id",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
val appState: AppState = mockk(relaxed = true)
|
|
||||||
val messagingState: MessagingState = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messagingState.messages } returns emptyList()
|
|
||||||
every { appState.messaging } returns messagingState
|
|
||||||
every { middlewareContext.state } returns appState
|
|
||||||
|
|
||||||
middleware.invoke(
|
|
||||||
middlewareContext, {},
|
|
||||||
MessageDismissed(message)
|
|
||||||
)
|
|
||||||
|
|
||||||
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN MessageDisplayed THEN update storage`() {
|
|
||||||
val message = Message(
|
|
||||||
"control-id",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
val appState: AppState = mockk(relaxed = true)
|
|
||||||
val messagingState: MessagingState = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messagingState.messages } returns emptyList()
|
|
||||||
every { appState.messaging } returns messagingState
|
|
||||||
every { middlewareContext.state } returns appState
|
|
||||||
|
|
||||||
middleware.invoke(
|
|
||||||
middlewareContext, {},
|
|
||||||
MessageDisplayed(message)
|
|
||||||
)
|
|
||||||
|
|
||||||
verify { messagingStorage.updateMetadata(message.metadata.copy(displayCount = 1)) }
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN onMessageDismissed THEN updateMetadata,removeMessage , UpdateMessages and removeMessageToShowIfNeeded`() {
|
|
||||||
val message = Message(
|
|
||||||
"control-id",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
|
|
||||||
val spiedMiddleware = spyk(middleware)
|
|
||||||
|
|
||||||
every { spiedMiddleware.removeMessage(middlewareContext, message) } returns emptyList()
|
|
||||||
every { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, message) } just Runs
|
|
||||||
|
|
||||||
spiedMiddleware.onMessageDismissed(middlewareContext, message)
|
|
||||||
|
|
||||||
verify { messagingStorage.updateMetadata(message.metadata.copy(dismissed = true)) }
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
|
||||||
verify { spiedMiddleware.removeMessage(middlewareContext, message) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN removeMessage THEN remove the message`() {
|
|
||||||
val message = Message(
|
|
||||||
"control-id",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
val messages = listOf(message)
|
|
||||||
val appState: AppState = mockk(relaxed = true)
|
|
||||||
val messagingState: MessagingState = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messagingState.messages } returns messages
|
|
||||||
every { appState.messaging } returns messagingState
|
|
||||||
every { middlewareContext.state } returns appState
|
|
||||||
|
|
||||||
val results = middleware.removeMessage(middlewareContext, message)
|
|
||||||
|
|
||||||
assertTrue(results.isEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN consumeMessageToShowIfNeeded THEN consume the message`() {
|
|
||||||
val message = Message(
|
|
||||||
"control-id",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id")
|
|
||||||
)
|
|
||||||
val appState: AppState = mockk(relaxed = true)
|
|
||||||
val messagingState: MessagingState = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messagingState.messageToShow } returns message
|
|
||||||
every { appState.messaging } returns messagingState
|
|
||||||
every { middlewareContext.state } returns appState
|
|
||||||
|
|
||||||
middleware.consumeMessageToShowIfNeeded(middlewareContext, message)
|
|
||||||
|
|
||||||
verify { middlewareContext.dispatch(ConsumeMessageToShow) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN updateMessage THEN update available messages`() {
|
|
||||||
val oldMessage = Message(
|
|
||||||
"oldMessage",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id", pressed = false)
|
|
||||||
)
|
|
||||||
|
|
||||||
val updatedMessage = Message(
|
|
||||||
"oldMessage",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id", pressed = true)
|
|
||||||
)
|
|
||||||
|
|
||||||
val spiedMiddleware = spyk(middleware)
|
|
||||||
|
|
||||||
val appState: AppState = mockk(relaxed = true)
|
|
||||||
val messagingState: MessagingState = mockk(relaxed = true)
|
|
||||||
|
|
||||||
every { messagingState.messageToShow } returns oldMessage
|
|
||||||
every { appState.messaging } returns messagingState
|
|
||||||
every { middlewareContext.state } returns appState
|
|
||||||
every { spiedMiddleware.removeMessage(middlewareContext, oldMessage) } returns emptyList()
|
|
||||||
|
|
||||||
val results = spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage)
|
|
||||||
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessageToShow(updatedMessage)) }
|
|
||||||
verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) }
|
|
||||||
|
|
||||||
assertTrue(results.size == 1)
|
|
||||||
assertTrue(results.first().metadata.pressed)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a message with that not surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN update the available messages and the updateMetadata`() {
|
|
||||||
val oldMessageData: MessageData = mockk(relaxed = true)
|
|
||||||
val oldMessage = Message(
|
|
||||||
"oldMessage",
|
|
||||||
oldMessageData,
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id", displayCount = 0)
|
|
||||||
)
|
|
||||||
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
|
|
||||||
val spiedMiddleware = spyk(middleware)
|
|
||||||
|
|
||||||
every { oldMessageData.maxDisplayCount } returns 2
|
|
||||||
every {
|
|
||||||
spiedMiddleware.updateMessage(
|
|
||||||
middlewareContext,
|
|
||||||
oldMessage,
|
|
||||||
updatedMessage
|
|
||||||
)
|
|
||||||
} returns emptyList()
|
|
||||||
|
|
||||||
spiedMiddleware.onMessagedDisplayed(oldMessage, middlewareContext)
|
|
||||||
|
|
||||||
verify { spiedMiddleware.updateMessage(middlewareContext, oldMessage, updatedMessage) }
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
|
||||||
verify { messagingStorage.updateMetadata(updatedMessage.metadata) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a message with that surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN remove the message and consume it`() {
|
|
||||||
val oldMessageData: MessageData = mockk(relaxed = true)
|
|
||||||
val oldMessage = Message(
|
|
||||||
"oldMessage",
|
|
||||||
oldMessageData,
|
|
||||||
action = "action",
|
|
||||||
mockk(relaxed = true),
|
|
||||||
listOf("trigger"),
|
|
||||||
Message.Metadata("same-id", displayCount = 0)
|
|
||||||
)
|
|
||||||
val updatedMessage = oldMessage.copy(metadata = oldMessage.metadata.copy(displayCount = 1))
|
|
||||||
val spiedMiddleware = spyk(middleware)
|
|
||||||
|
|
||||||
every { oldMessageData.maxDisplayCount } returns 1
|
|
||||||
every {
|
|
||||||
spiedMiddleware.consumeMessageToShowIfNeeded(
|
|
||||||
middlewareContext,
|
|
||||||
oldMessage
|
|
||||||
)
|
|
||||||
} just Runs
|
|
||||||
every { spiedMiddleware.removeMessage(middlewareContext, oldMessage) } returns emptyList()
|
|
||||||
|
|
||||||
spiedMiddleware.onMessagedDisplayed(oldMessage, middlewareContext)
|
|
||||||
|
|
||||||
verify { spiedMiddleware.consumeMessageToShowIfNeeded(middlewareContext, oldMessage) }
|
|
||||||
verify { spiedMiddleware.removeMessage(middlewareContext, oldMessage) }
|
|
||||||
verify { middlewareContext.dispatch(UpdateMessages(emptyList())) }
|
|
||||||
verify { messagingStorage.updateMetadata(updatedMessage.metadata) }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,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/. */
|
|
||||||
|
|
||||||
package org.mozilla.fenix.gleanplumb.state
|
|
||||||
|
|
||||||
import io.mockk.mockk
|
|
||||||
import org.junit.Assert.assertFalse
|
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMessageToShow
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
|
|
||||||
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
|
|
||||||
import org.mozilla.fenix.components.appstate.AppState
|
|
||||||
import org.mozilla.fenix.components.appstate.AppStoreReducer
|
|
||||||
import org.mozilla.fenix.gleanplumb.MessagingState
|
|
||||||
|
|
||||||
class MessagingReducerTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a new value for messageToShow WHEN UpdateMessageToShow is called THEN update the current value`() {
|
|
||||||
val initialState = AppState(
|
|
||||||
messaging = MessagingState(
|
|
||||||
messageToShow = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
var updatedState = MessagingReducer.reduce(
|
|
||||||
initialState,
|
|
||||||
UpdateMessageToShow(mockk())
|
|
||||||
)
|
|
||||||
|
|
||||||
assertNotNull(updatedState.messaging.messageToShow)
|
|
||||||
|
|
||||||
updatedState = AppStoreReducer.reduce(updatedState, ConsumeMessageToShow)
|
|
||||||
|
|
||||||
assertNull(updatedState.messaging.messageToShow)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN a new value for messages WHEN UpdateMessages is called THEN update the current value`() {
|
|
||||||
val initialState = AppState(
|
|
||||||
messaging = MessagingState(
|
|
||||||
messages = emptyList()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
var updatedState = MessagingReducer.reduce(
|
|
||||||
initialState,
|
|
||||||
UpdateMessages(listOf(mockk()))
|
|
||||||
)
|
|
||||||
|
|
||||||
assertFalse(updatedState.messaging.messages.isEmpty())
|
|
||||||
|
|
||||||
updatedState = AppStoreReducer.reduce(updatedState, UpdateMessages(emptyList()))
|
|
||||||
|
|
||||||
assertTrue(updatedState.messaging.messages.isEmpty())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue