Add defaults to FML, add control behaviour
This commit is contained in:
parent
480ab3dca7
commit
2b36ca75bf
|
@ -1,81 +0,0 @@
|
|||
{
|
||||
"default-browser-message": {
|
||||
"description": "A small feature allowing experiments on the placement of a default browser message.",
|
||||
"hasExposure": true,
|
||||
"exposureDescription": "",
|
||||
"variables": {
|
||||
"message-location": {
|
||||
"type": "string",
|
||||
"description": "Where is the message to be put."
|
||||
}
|
||||
}
|
||||
},
|
||||
"homescreen": {
|
||||
"description": "The homescreen that the user goes to when they press home or new tab.",
|
||||
"hasExposure": true,
|
||||
"exposureDescription": "",
|
||||
"variables": {
|
||||
"sections-enabled": {
|
||||
"type": "json",
|
||||
"description": "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default."
|
||||
}
|
||||
}
|
||||
},
|
||||
"messaging": {
|
||||
"description": "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n",
|
||||
"hasExposure": true,
|
||||
"exposureDescription": "",
|
||||
"variables": {
|
||||
"actions": {
|
||||
"type": "json",
|
||||
"description": "A growable map of action URLs."
|
||||
},
|
||||
"message-under-experiment": {
|
||||
"type": "string",
|
||||
"description": "Id or prefix of the message under experiment."
|
||||
},
|
||||
"messages": {
|
||||
"type": "json",
|
||||
"description": "A growable collection of messages"
|
||||
},
|
||||
"styles": {
|
||||
"type": "json",
|
||||
"description": "A map of styles to configure message appearance.\n"
|
||||
},
|
||||
"triggers": {
|
||||
"type": "json",
|
||||
"description": "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nimbus-validation": {
|
||||
"description": "A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.",
|
||||
"hasExposure": true,
|
||||
"exposureDescription": "",
|
||||
"variables": {
|
||||
"settings-icon": {
|
||||
"type": "string",
|
||||
"description": "The drawable displayed in the app menu for Settings"
|
||||
},
|
||||
"settings-punctuation": {
|
||||
"type": "string",
|
||||
"description": "The emoji displayed in the Settings screen title."
|
||||
},
|
||||
"settings-title": {
|
||||
"type": "string",
|
||||
"description": "The title of displayed in the Settings screen and app menu."
|
||||
}
|
||||
}
|
||||
},
|
||||
"search-term-groups": {
|
||||
"description": "A feature allowing the grouping of URLs around the search term that it came from.",
|
||||
"hasExposure": true,
|
||||
"exposureDescription": "",
|
||||
"variables": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "If true, the feature shows up on the homescreen and on the new tab screen."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
default-browser-message:
|
||||
description: A small feature allowing experiments on the placement of a default browser message.
|
||||
hasExposure: true
|
||||
exposureDescription: ""
|
||||
variables:
|
||||
message-location:
|
||||
type: string
|
||||
description: Where is the message to be put.
|
||||
homescreen:
|
||||
description: The homescreen that the user goes to when they press home or new tab.
|
||||
hasExposure: true
|
||||
exposureDescription: ""
|
||||
variables:
|
||||
sections-enabled:
|
||||
type: json
|
||||
description: "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default."
|
||||
messaging:
|
||||
description: "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n"
|
||||
hasExposure: true
|
||||
exposureDescription: ""
|
||||
variables:
|
||||
actions:
|
||||
type: json
|
||||
description: A growable map of action URLs.
|
||||
messages:
|
||||
type: json
|
||||
description: A growable collection of messages
|
||||
styles:
|
||||
type: json
|
||||
description: "A map of styles to configure message appearance.\n"
|
||||
triggers:
|
||||
type: json
|
||||
description: "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n"
|
||||
nimbus-validation:
|
||||
description: A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.
|
||||
hasExposure: true
|
||||
exposureDescription: ""
|
||||
variables:
|
||||
settings-icon:
|
||||
type: string
|
||||
description: The drawable displayed in the app menu for Settings
|
||||
settings-punctuation:
|
||||
type: string
|
||||
description: The emoji displayed in the Settings screen title.
|
||||
settings-title:
|
||||
type: string
|
||||
description: The title of displayed in the Settings screen and app menu.
|
||||
search-term-groups:
|
||||
description: A feature allowing the grouping of URLs around the search term that it came from.
|
||||
hasExposure: true
|
||||
exposureDescription: ""
|
||||
variables:
|
||||
enabled:
|
||||
type: boolean
|
||||
description: "If true, the feature shows up on the homescreen and on the new tab screen."
|
|
@ -26,6 +26,7 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService
|
|||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.experiments.createNimbus
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.gleanplumb.CustomAttributeProvider
|
||||
import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage
|
||||
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
|
||||
import org.mozilla.fenix.nimbus.FxNimbus
|
||||
|
@ -137,6 +138,7 @@ class Analytics(
|
|||
metrics.track(Event.Messaging.MessageMalformed(it))
|
||||
},
|
||||
messagingFeature = FxNimbus.features.messaging,
|
||||
attributeProvider = CustomAttributeProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/* 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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ class DefaultMessageController(
|
|||
}
|
||||
|
||||
override fun onMessageDisplayed(message: Message) {
|
||||
if (message.data.maxDisplayCount <= message.metadata.displayCount + 1) {
|
||||
if (message.maxDisplayCount <= message.metadata.displayCount + 1) {
|
||||
metrics.track(Event.Messaging.MessageExpired(message.id))
|
||||
}
|
||||
metrics.track(Event.Messaging.MessageShown(message.id))
|
||||
|
|
|
@ -27,6 +27,12 @@ data class Message(
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -12,6 +12,7 @@ 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.ControlMessageBehavior
|
||||
import org.mozilla.fenix.nimbus.Messaging
|
||||
import org.mozilla.fenix.nimbus.StyleData
|
||||
|
||||
|
@ -23,12 +24,13 @@ class NimbusMessagingStorage(
|
|||
private val metadataStorage: MessageMetadataStorage,
|
||||
private val reportMalformedMessage: (String) -> Unit,
|
||||
private val gleanPlumb: GleanPlumbInterface,
|
||||
private val messagingFeature: FeatureHolder<Messaging>
|
||||
private val messagingFeature: FeatureHolder<Messaging>,
|
||||
private val attributeProvider: CustomAttributeProvider? = null
|
||||
) {
|
||||
private val logger = Logger("MessagingStorage")
|
||||
private val nimbusFeature = messagingFeature.value()
|
||||
private val customAttributes: JSONObject
|
||||
get() = JSONObject()
|
||||
get() = attributeProvider?.getCustomAttributes(context) ?: JSONObject()
|
||||
|
||||
/**
|
||||
* Returns a list of available messages descending sorted by their priority.
|
||||
|
@ -54,7 +56,7 @@ class NimbusMessagingStorage(
|
|||
?: return@mapNotNull null
|
||||
)
|
||||
}.filter {
|
||||
it.data.maxDisplayCount >= it.metadata.displayCount &&
|
||||
it.maxDisplayCount >= it.metadata.displayCount &&
|
||||
!it.metadata.dismissed &&
|
||||
!it.metadata.pressed
|
||||
}.sortedByDescending {
|
||||
|
@ -66,21 +68,33 @@ class NimbusMessagingStorage(
|
|||
* Returns the next higher priority message which all their triggers are true.
|
||||
*/
|
||||
fun getNextMessage(availableMessages: List<Message>): Message? {
|
||||
val jexlCache = HashMap<String, Boolean>()
|
||||
val helper = gleanPlumb.createMessageHelper(customAttributes)
|
||||
var message = availableMessages.firstOrNull {
|
||||
isMessageEligible(it, helper)
|
||||
val message = availableMessages.firstOrNull {
|
||||
isMessageEligible(it, helper, jexlCache)
|
||||
} ?: return null
|
||||
|
||||
if (isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) {
|
||||
messagingFeature.recordExposure()
|
||||
// Check this isn't an experimental message. If not, we can go ahead and return it.
|
||||
if (!isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) {
|
||||
return message
|
||||
}
|
||||
// If the message is under experiment, then we need to record the exposure
|
||||
messagingFeature.recordExposure()
|
||||
|
||||
if (message.data.isControl) {
|
||||
message = availableMessages.firstOrNull {
|
||||
!it.data.isControl && isMessageEligible(it, helper)
|
||||
} ?: return null
|
||||
// If this is an experimental message, but not a placebo, then just return the message.
|
||||
return if (!message.data.isControl) {
|
||||
message
|
||||
} else {
|
||||
// This is a control, so we need to either return the next message (there may not be one)
|
||||
// or not display anything.
|
||||
when (getOnControlBehavior()) {
|
||||
ControlMessageBehavior.SHOW_NEXT_MESSAGE -> availableMessages.firstOrNull {
|
||||
// There should only be one control message, and we've just detected it.
|
||||
!it.data.isControl && isMessageEligible(it, helper, jexlCache)
|
||||
}
|
||||
ControlMessageBehavior.SHOW_NONE -> null
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,7 +150,7 @@ class NimbusMessagingStorage(
|
|||
|
||||
@VisibleForTesting
|
||||
internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean {
|
||||
return when {
|
||||
return message.data.isControl || when {
|
||||
expression.isNullOrBlank() -> {
|
||||
false
|
||||
}
|
||||
|
@ -152,19 +166,26 @@ class NimbusMessagingStorage(
|
|||
@VisibleForTesting
|
||||
internal fun isMessageEligible(
|
||||
message: Message,
|
||||
helper: GleanPlumbMessageHelper
|
||||
helper: GleanPlumbMessageHelper,
|
||||
jexlCache: MutableMap<String, Boolean> = mutableMapOf()
|
||||
): Boolean {
|
||||
return message.triggers.all { condition ->
|
||||
try {
|
||||
helper.evalJexl(condition)
|
||||
} catch (e: NimbusException.EvaluationException) {
|
||||
reportMalformedMessage(message.id)
|
||||
logger.info("Unable to evaluate $condition")
|
||||
false
|
||||
}
|
||||
jexlCache[condition]
|
||||
?: try {
|
||||
helper.evalJexl(condition).also { result ->
|
||||
jexlCache[condition] = result
|
||||
}
|
||||
} catch (e: NimbusException.EvaluationException) {
|
||||
reportMalformedMessage(message.id)
|
||||
logger.info("Unable to evaluate $condition")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun getOnControlBehavior(): ControlMessageBehavior = nimbusFeature.onControl
|
||||
|
||||
private suspend fun addMetadata(id: String): Message.Metadata {
|
||||
return metadataStorage.addMetadata(
|
||||
Message.Metadata(
|
||||
|
|
|
@ -73,7 +73,7 @@ class MessagingMiddleware(
|
|||
val newMessage = oldMessage.copy(
|
||||
metadata = newMetadata
|
||||
)
|
||||
val newMessages = if (newMetadata.displayCount < oldMessage.data.maxDisplayCount) {
|
||||
val newMessages = if (newMetadata.displayCount < oldMessage.maxDisplayCount) {
|
||||
updateMessage(context, oldMessage, newMessage)
|
||||
} else {
|
||||
consumeMessageToShowIfNeeded(context, oldMessage)
|
||||
|
|
|
@ -93,7 +93,7 @@ class DefaultMessageControllerTest {
|
|||
|
||||
@Test
|
||||
fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() {
|
||||
val data = MessageData(_context = testContext, maxDisplayCount = 1)
|
||||
val data = MessageData(_context = testContext)
|
||||
val message = mockMessage(data)
|
||||
|
||||
controller.onMessageDisplayed(message)
|
||||
|
@ -106,7 +106,7 @@ class DefaultMessageControllerTest {
|
|||
private fun mockMessage(data: MessageData = MessageData(_context = testContext)) = Message(
|
||||
id = "id",
|
||||
data = data,
|
||||
style = mockk(),
|
||||
style = mockk(relaxed = true),
|
||||
action = "action",
|
||||
triggers = emptyList(),
|
||||
metadata = Message.Metadata(
|
||||
|
|
|
@ -9,9 +9,7 @@ import io.mockk.every
|
|||
import io.mockk.mockk
|
||||
import io.mockk.spyk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import mozilla.components.support.test.mock
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
|
@ -26,6 +24,7 @@ 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.ControlMessageBehavior.SHOW_NEXT_MESSAGE
|
||||
import org.mozilla.fenix.nimbus.MessageData
|
||||
import org.mozilla.fenix.nimbus.Messaging
|
||||
import org.mozilla.fenix.nimbus.StyleData
|
||||
|
@ -40,7 +39,6 @@ class NimbusMessagingStorageTest {
|
|||
private lateinit var gleanPlumb: GleanPlumbInterface
|
||||
private lateinit var messagingFeature: FeatureHolder<Messaging>
|
||||
private lateinit var messaging: Messaging
|
||||
private val coroutineScope = TestCoroutineScope()
|
||||
private var malformedWasReported = false
|
||||
private val reportMalformedMessage: (String) -> Unit = {
|
||||
malformedWasReported = true
|
||||
|
@ -195,13 +193,12 @@ class NimbusMessagingStorageTest {
|
|||
)
|
||||
val messages = mapOf(
|
||||
"shown-many-times-message" to createMessageData(
|
||||
style = "high-priority",
|
||||
maxDisplayCount = 2
|
||||
style = "high-priority"
|
||||
),
|
||||
"normal-message" to createMessageData(style = "high-priority"),
|
||||
)
|
||||
val styles = mapOf(
|
||||
"high-priority" to createStyle(priority = 100),
|
||||
"high-priority" to createStyle(priority = 100, maxDisplayCount = 2),
|
||||
)
|
||||
val metadataStorage: MessageMetadataStorage = mockk(relaxed = true)
|
||||
val messagingFeature = createMessagingFeature(
|
||||
|
@ -253,7 +250,7 @@ class NimbusMessagingStorageTest {
|
|||
@Test
|
||||
fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() = runBlockingTest {
|
||||
|
||||
storage.updateMetadata(mockk())
|
||||
storage.updateMetadata(mockk(relaxed = true))
|
||||
|
||||
coEvery { metadataStorage.updateMetadata(any()) }
|
||||
}
|
||||
|
@ -294,9 +291,10 @@ class NimbusMessagingStorageTest {
|
|||
@Test
|
||||
fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() {
|
||||
val message = Message(
|
||||
"id", mockk(),
|
||||
"id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mock(),
|
||||
mockk(relaxed = true),
|
||||
emptyList(),
|
||||
Message.Metadata("id")
|
||||
)
|
||||
|
@ -309,9 +307,10 @@ class NimbusMessagingStorageTest {
|
|||
@Test
|
||||
fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() {
|
||||
val message = Message(
|
||||
"end-", mockk(),
|
||||
"end-",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mock(),
|
||||
mockk(relaxed = true),
|
||||
emptyList(),
|
||||
Message.Metadata("end-")
|
||||
)
|
||||
|
@ -324,9 +323,10 @@ class NimbusMessagingStorageTest {
|
|||
@Test
|
||||
fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() {
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
"same-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mock(),
|
||||
mockk(relaxed = true),
|
||||
emptyList(),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
@ -340,9 +340,10 @@ class NimbusMessagingStorageTest {
|
|||
fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() {
|
||||
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
"same-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mock(),
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
@ -358,9 +359,10 @@ class NimbusMessagingStorageTest {
|
|||
fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() {
|
||||
val helper: GleanPlumbMessageHelper = mockk(relaxed = true)
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
"same-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mock(),
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
@ -376,9 +378,10 @@ class NimbusMessagingStorageTest {
|
|||
fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() {
|
||||
val spiedStorage = spyk(storage)
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
"same-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mock(),
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
@ -394,9 +397,10 @@ class NimbusMessagingStorageTest {
|
|||
fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() {
|
||||
val spiedStorage = spyk(storage)
|
||||
val message = Message(
|
||||
"same-id", mockk(),
|
||||
"same-id",
|
||||
mockk(relaxed = true),
|
||||
action = "action",
|
||||
mock(),
|
||||
mockk(relaxed = true),
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id")
|
||||
)
|
||||
|
@ -441,6 +445,7 @@ class NimbusMessagingStorageTest {
|
|||
val controlMessageData: MessageData = mockk(relaxed = true)
|
||||
|
||||
every { messageData.isControl } returns false
|
||||
every { spiedStorage.getOnControlBehavior() } returns SHOW_NEXT_MESSAGE
|
||||
every { controlMessageData.isControl } returns true
|
||||
|
||||
val message = Message(
|
||||
|
@ -473,14 +478,12 @@ class NimbusMessagingStorageTest {
|
|||
private fun createMessageData(
|
||||
action: String = "action-1",
|
||||
style: String = "style-1",
|
||||
triggers: List<String> = listOf("trigger-1"),
|
||||
maxDisplayCount: Int = 5
|
||||
triggers: List<String> = listOf("trigger-1")
|
||||
): 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
|
||||
}
|
||||
|
||||
|
@ -506,9 +509,10 @@ class NimbusMessagingStorageTest {
|
|||
return messagingFeature
|
||||
}
|
||||
|
||||
private fun createStyle(priority: Int = 1): StyleData {
|
||||
private fun createStyle(priority: Int = 1, maxDisplayCount: Int = 5): StyleData {
|
||||
val style1: StyleData = mockk(relaxed = true)
|
||||
every { style1.priority } returns priority
|
||||
every { style1.maxDisplayCount } returns maxDisplayCount
|
||||
return style1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.mozilla.fenix.gleanplumb.MessagingState
|
|||
import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.nimbus.MessageData
|
||||
import org.mozilla.fenix.nimbus.StyleData
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class MessagingMiddlewareTest {
|
||||
|
@ -276,12 +277,13 @@ class MessagingMiddlewareTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN a message with that not surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN update the available messages and the updateMetadata`() {
|
||||
val style: StyleData = mockk(relaxed = true)
|
||||
val oldMessageData: MessageData = mockk(relaxed = true)
|
||||
val oldMessage = Message(
|
||||
"oldMessage",
|
||||
oldMessageData,
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
style,
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id", displayCount = 0)
|
||||
)
|
||||
|
@ -289,7 +291,7 @@ class MessagingMiddlewareTest {
|
|||
val spiedMiddleware = spyk(middleware)
|
||||
|
||||
every { spiedMiddleware.now() } returns 0
|
||||
every { oldMessageData.maxDisplayCount } returns 2
|
||||
every { style.maxDisplayCount } returns 2
|
||||
every {
|
||||
spiedMiddleware.updateMessage(
|
||||
middlewareContext,
|
||||
|
@ -307,12 +309,13 @@ class MessagingMiddlewareTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN a message with that surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN remove the message and consume it`() {
|
||||
val style: StyleData = mockk(relaxed = true)
|
||||
val oldMessageData: MessageData = mockk(relaxed = true)
|
||||
val oldMessage = Message(
|
||||
"oldMessage",
|
||||
oldMessageData,
|
||||
action = "action",
|
||||
mockk(relaxed = true),
|
||||
style,
|
||||
listOf("trigger"),
|
||||
Message.Metadata("same-id", displayCount = 0)
|
||||
)
|
||||
|
@ -320,7 +323,7 @@ class MessagingMiddlewareTest {
|
|||
val spiedMiddleware = spyk(middleware)
|
||||
|
||||
every { spiedMiddleware.now() } returns 0
|
||||
every { oldMessageData.maxDisplayCount } returns 1
|
||||
every { style.maxDisplayCount } returns 1
|
||||
every {
|
||||
spiedMiddleware.consumeMessageToShowIfNeeded(
|
||||
middlewareContext,
|
||||
|
|
173
nimbus.fml.yaml
173
nimbus.fml.yaml
|
@ -94,51 +94,77 @@ features:
|
|||
expressions. Each entry maps to a
|
||||
valid JEXL expression.
|
||||
type: Map<String, String>
|
||||
default:
|
||||
english-speaking: "'en' in locale"
|
||||
ALWAYS: "true"
|
||||
NEW_USER: "days_since_install < 7"
|
||||
|
||||
default: {}
|
||||
styles:
|
||||
description: >
|
||||
A map of styles to configure message
|
||||
appearance.
|
||||
type: Map<String, StyleData>
|
||||
default:
|
||||
urgent:
|
||||
background-color: red
|
||||
text-color: white
|
||||
button-background: bright-blue
|
||||
button-text-color: white
|
||||
priority: 70
|
||||
warning:
|
||||
background-color: cyan,
|
||||
text-color: black,
|
||||
button-background: bright-blue
|
||||
button-text-color: white
|
||||
priority: 55
|
||||
default:
|
||||
background-color: blue
|
||||
text-color: white
|
||||
button-background: bright-blue
|
||||
button-text-color: white
|
||||
priority: 50
|
||||
excited:
|
||||
background-color: blue
|
||||
text-color: white
|
||||
button-background: bright-blue
|
||||
button-text-color: white
|
||||
priority: 60
|
||||
default: {}
|
||||
|
||||
actions:
|
||||
type: Map<String, String>
|
||||
description: A growable map of action URLs.
|
||||
default:
|
||||
OPEN_SYNC_SETTINGS: firefox://settings/sync
|
||||
OPEN_POCKET_SETTINGS: https:///getpocket.com/settings?fxa={fxa-token}
|
||||
OPEN_SETTINGS: ://settings
|
||||
|
||||
default: {}
|
||||
on-control:
|
||||
type: ControlMessageBehavior
|
||||
description: What should be displayed when a control message is selected.
|
||||
default: show-next-message
|
||||
defaults:
|
||||
- value:
|
||||
triggers:
|
||||
USER_RECENTLY_INSTALLED: days_since_install < 7
|
||||
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
|
||||
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
|
||||
USER_EN_SPEAKER: "'en' in locale"
|
||||
USER_DE_SPEAKER: "'de' in locale"
|
||||
USER_FR_SPEAKER: "'fr' in locale"
|
||||
DEVICE_ANDROID: os == 'Android'
|
||||
DEVICE_IOS: os == 'iOS'
|
||||
ALWAYS: "true"
|
||||
NEVER: "false"
|
||||
actions:
|
||||
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
|
||||
INSTALL_SEARCH_WIDGET: ://install_search_widget
|
||||
MAKE_DEFAULT_BROWSER: ://make_default_browser
|
||||
VIEW_BOOKMARKS: ://urls_bookmarks
|
||||
VIEW_COLLECTIONS: ://home_collections
|
||||
VIEW_HISTORY: ://urls_history
|
||||
VIEW_HOMESCREEN: ://home
|
||||
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
|
||||
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
|
||||
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
|
||||
OPEN_SETTINGS_LOGINS: ://settings_logins
|
||||
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
|
||||
OPEN_SETTINGS_PRIVACY: ://settings_privacy
|
||||
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
|
||||
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
|
||||
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
|
||||
OPEN_SETTINGS: ://settings
|
||||
TURN_ON_SYNC: ://turn_on_sync
|
||||
styles:
|
||||
DEFAULT:
|
||||
priority: 50
|
||||
max-display-count: 5
|
||||
PERSISTENT:
|
||||
priority: 50
|
||||
max-display-count: 20
|
||||
WARNING:
|
||||
priority: 60
|
||||
max-display-count: 10
|
||||
URGENT:
|
||||
priority: 100
|
||||
max-display-count: 10
|
||||
|
||||
- channel: developer
|
||||
value:
|
||||
styles:
|
||||
DEFAULT:
|
||||
priority: 50
|
||||
max-display-count: 100
|
||||
EXPIRES_QUICKLY:
|
||||
priority: 100
|
||||
max-display-count: 1
|
||||
- channel: developer
|
||||
value: {
|
||||
"messages": {
|
||||
|
@ -147,13 +173,10 @@ features:
|
|||
"text": "Love Firefox? Fill in our survey!",
|
||||
"action": "https://surveyprovider.com/survey-id/{uuid}",
|
||||
"trigger": [ "ALWAYS" ],
|
||||
"max-display-count": 5,
|
||||
"style": "warning",
|
||||
"style": "DEFAULT",
|
||||
"button-label": "Go to the survey"
|
||||
}
|
||||
},
|
||||
|
||||
"message-under-experiment": "my-viewpoint-survey"
|
||||
}
|
||||
}
|
||||
- channel: developer
|
||||
value: {
|
||||
|
@ -161,12 +184,9 @@ features:
|
|||
"private-tabs-auto-close": {
|
||||
"action": "OPEN_SETTINGS",
|
||||
"text": "Sharing your phone? Autoclosing private tabs is for you!",
|
||||
"style": "warning",
|
||||
"trigger": [
|
||||
"NEW_USER",
|
||||
"first-private-tabs-opened"
|
||||
],
|
||||
"max-display-count": 5
|
||||
"USER_RECENTLY_INSTALLED"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -175,23 +195,15 @@ features:
|
|||
- channel: developer
|
||||
value: {
|
||||
"triggers": {
|
||||
"ireland": "'IE' in locale"
|
||||
"USER_IE_COUNTRY": "'IE' in locale"
|
||||
},
|
||||
|
||||
"styles": {
|
||||
"irish-green": {
|
||||
"background-color": "green",
|
||||
"text-color": "dark-green",
|
||||
"button-background": "foo",
|
||||
"button-text-color": "very-green",
|
||||
"priority": 50
|
||||
}
|
||||
},
|
||||
|
||||
"actions": {
|
||||
"OPEN_SETTINGS": "://settings"
|
||||
},
|
||||
|
||||
"messages": {
|
||||
"eu-tracking-protection-for-ireland": {
|
||||
"action": "OPEN_SETTINGS",
|
||||
|
@ -199,9 +211,8 @@ features:
|
|||
"style": "irish-green",
|
||||
"trigger": [
|
||||
"NEW_USER",
|
||||
"ireland"
|
||||
],
|
||||
"max-display-count": 5
|
||||
"USER_IE_COUNTRY"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -220,7 +231,7 @@ types:
|
|||
and call to action.
|
||||
fields:
|
||||
action:
|
||||
type: String
|
||||
type: Text
|
||||
description: >
|
||||
A URL of a page or a deeplink.
|
||||
This may have substitution variables in.
|
||||
|
@ -239,7 +250,6 @@ types:
|
|||
type: Boolean
|
||||
description: "Indicates if this message is the control message, if true shouldn't be displayed"
|
||||
default: false
|
||||
|
||||
button-label:
|
||||
type: Option<Text>
|
||||
description: >
|
||||
|
@ -251,20 +261,7 @@ types:
|
|||
description: >
|
||||
The style as described in a
|
||||
`StyleData` from the styles table.
|
||||
default: "default"
|
||||
max-display-count:
|
||||
type: Int
|
||||
description: >
|
||||
The number of sessions the user is
|
||||
shown the message before the message expires.
|
||||
If the user is able to dismiss the message,
|
||||
this is the number of times they dismiss
|
||||
it before the message expires.
|
||||
A count of -1 means that the message will
|
||||
never expire.
|
||||
default: 5
|
||||
# These triggers aren't part of the MVP,
|
||||
# so may be excluded.
|
||||
default: DEFAULT
|
||||
trigger:
|
||||
type: List<String>
|
||||
description: >
|
||||
|
@ -277,31 +274,27 @@ types:
|
|||
A group of properities (predominantly visual) to
|
||||
describe the style of the message.
|
||||
fields:
|
||||
# How the string is transformed into a color is unspecified
|
||||
background-color:
|
||||
type: String
|
||||
description: The color of the background.
|
||||
default: "blue"
|
||||
text-color:
|
||||
type: String
|
||||
description: The color of the background.
|
||||
default: "white"
|
||||
button-background:
|
||||
type: String
|
||||
description: The color of the button background.
|
||||
default: "bright-blue"
|
||||
button-text-color:
|
||||
type: String
|
||||
description: The color of the button text.
|
||||
default: "white"
|
||||
priority:
|
||||
type: Int
|
||||
description: >
|
||||
The importance of this message.
|
||||
0 is not very important, 100 is very important.
|
||||
default: 50
|
||||
max-display-count:
|
||||
type: Int
|
||||
description: >
|
||||
How many sessions will this message be shown to the user
|
||||
before it is expired.
|
||||
default: 5
|
||||
|
||||
enums:
|
||||
ControlMessageBehavior:
|
||||
description: An enum to influence what should be displayed when a control message is selected.
|
||||
variants:
|
||||
show-next-message:
|
||||
description: The next eligible message should be shown.
|
||||
show-none:
|
||||
description: The surface should show no message.
|
||||
HomeScreenSection:
|
||||
description: The identifiers for the sections of the homescreen.
|
||||
variants:
|
||||
|
|
Loading…
Reference in New Issue