From 63ba218cd1e64c085afcdb08ce431bdc08e518cd Mon Sep 17 00:00:00 2001 From: Matthew Tighe Date: Wed, 31 Jan 2024 10:38:51 -0800 Subject: [PATCH] Bug 1878185 - Add ChangeDetectionMiddleware --- .../components/ChangeDetectionMiddleware.kt | 40 +++++++ .../ChangeDetectionMiddlewareTest.kt | 102 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 app/src/main/java/org/mozilla/fenix/components/ChangeDetectionMiddleware.kt create mode 100644 app/src/test/java/org/mozilla/fenix/components/ChangeDetectionMiddlewareTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/components/ChangeDetectionMiddleware.kt b/app/src/main/java/org/mozilla/fenix/components/ChangeDetectionMiddleware.kt new file mode 100644 index 000000000..8aa1d1aa0 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/ChangeDetectionMiddleware.kt @@ -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.components + +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.lib.state.State + +/** + * A Middleware for detecting changes to a state property, and offering a callback that captures the action that changed + * the property, the property before the change, and the property after the change. + * + * For example, this can be useful for debugging: + * ``` + * val selectedTabChangedMiddleware: Middleware = ChangeDetectionMiddleware( + * val selector = { it.selectedTabId } + * val onChange = { actionThatCausedResult, preResult, postResult -> + * logger.debug("$actionThatCausedResult changed selectedTabId from $preResult to $postResult") + * } + * ``` + * + * @param selector A function to map from the State to the properties that are being inspected. + * @param onChange A callback to react to changes to the properties defined by [selector]. + */ +class ChangeDetectionMiddleware( + private val selector: (S) -> T, + private val onChange: (A, pre: T, post: T) -> Unit, +) : Middleware { + override fun invoke(context: MiddlewareContext, next: (A) -> Unit, action: A) { + val pre = selector(context.store.state) + next(action) + val post = selector(context.store.state) + if (pre != post) { + onChange(action, pre, post) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/components/ChangeDetectionMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/components/ChangeDetectionMiddlewareTest.kt new file mode 100644 index 000000000..c8471db5d --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/ChangeDetectionMiddlewareTest.kt @@ -0,0 +1,102 @@ +/* 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.components + +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.Reducer +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store +import mozilla.components.support.test.ext.joinBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class ChangeDetectionMiddlewareTest { + @Test + fun `GIVEN single state property change WHEN action changes that state THEN callback is invoked`() { + var capturedAction: TestAction? = null + var preCount = 0 + var postCount = 0 + val middleware: Middleware = ChangeDetectionMiddleware( + selector = { it.counter }, + onChange = { action, pre, post -> + capturedAction = action + preCount = pre + postCount = post + }, + ) + + val store = TestStore( + TestState(counter = preCount, enabled = false), + ::reducer, + listOf(middleware), + ) + + store.dispatch(TestAction.IncrementAction).joinBlocking() + assertTrue(capturedAction is TestAction.IncrementAction) + assertEquals(0, preCount) + assertEquals(1, postCount) + + store.dispatch(TestAction.DecrementAction).joinBlocking() + assertTrue(capturedAction is TestAction.DecrementAction) + assertEquals(1, preCount) + assertEquals(0, postCount) + } + + @Test + fun `GIVEN multiple state property change WHEN action changes any state THEN callback is invoked`() { + var capturedAction: TestAction? = null + var preState = listOf() + var postState = listOf() + val middleware: Middleware = ChangeDetectionMiddleware( + selector = { listOf(it.counter, it.enabled) }, + onChange = { action, pre, post -> + capturedAction = action + preState = pre + postState = post + }, + ) + + val store = TestStore( + TestState(counter = 0, enabled = false), + ::reducer, + listOf(middleware), + ) + + store.dispatch(TestAction.SetEnabled(true)).joinBlocking() + assertTrue(capturedAction is TestAction.SetEnabled) + assertEquals(false, preState[1]) + assertEquals(true, postState[1]) + + store.dispatch(TestAction.SetEnabled(false)).joinBlocking() + assertTrue(capturedAction is TestAction.SetEnabled) + assertEquals(true, preState[1]) + assertEquals(false, postState[1]) + } + + private class TestStore( + initialState: TestState, + reducer: Reducer, + middleware: List>, + ) : Store(initialState, reducer, middleware) + + private data class TestState( + val counter: Int, + val enabled: Boolean, + ) : State + + private sealed class TestAction : Action { + object IncrementAction : TestAction() + object DecrementAction : TestAction() + data class SetEnabled(val enabled: Boolean) : TestAction() + } + + private fun reducer(state: TestState, action: TestAction): TestState = when (action) { + is TestAction.IncrementAction -> state.copy(counter = state.counter + 1) + is TestAction.DecrementAction -> state.copy(counter = state.counter - 1) + is TestAction.SetEnabled -> state.copy(enabled = action.enabled) + } +}