/* 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.library.history.state import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.advanceUntilIdle import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.action.HistoryMetadataAction import mozilla.components.browser.state.action.RecentlyClosedAction import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.libstate.ext.waitUntilIdle import mozilla.components.support.test.middleware.CaptureActionsMiddleware import mozilla.components.support.test.mock import mozilla.components.support.test.rule.MainCoroutineRule import mozilla.components.support.test.rule.runTestOnMain import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mockito.Mockito.anyLong import org.mockito.Mockito.verify import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider import org.mozilla.fenix.library.history.History import org.mozilla.fenix.library.history.HistoryFragmentAction import org.mozilla.fenix.library.history.HistoryFragmentState import org.mozilla.fenix.library.history.HistoryFragmentStore import org.mozilla.fenix.library.history.HistoryItemTimeGroup import org.mozilla.fenix.library.history.RemoveTimeFrame import org.mozilla.fenix.library.history.toPendingDeletionHistory class HistoryStorageMiddlewareTest { @get:Rule val coroutinesTestRule = MainCoroutineRule() private val appStore = mock() private lateinit var captureBrowserActions: CaptureActionsMiddleware private lateinit var browserStore: BrowserStore private val provider = mock() private lateinit var storage: PlacesHistoryStorage @Before fun setup() { storage = mock() captureBrowserActions = CaptureActionsMiddleware() browserStore = BrowserStore(middleware = listOf(captureBrowserActions)) } @Test fun `WHEN items are deleted THEN action to add to pending deletion is dispatched and snackbar is shown`() = runTestOnMain { var snackbarCalled = false val history = setOf(History.Regular(0, "title", "url", 0, HistoryItemTimeGroup.timeGroupForTimestamp(0))) val middleware = HistoryStorageMiddleware( appStore = appStore, browserStore = browserStore, historyProvider = provider, historyStorage = storage, undoDeleteSnackbar = { _, _, _ -> snackbarCalled = true }, onTimeFrameDeleted = { }, scope = this, ) val store = HistoryFragmentStore( initialState = HistoryFragmentState.initial, middleware = listOf(middleware), ) store.dispatch(HistoryFragmentAction.DeleteItems(history)).joinBlocking() store.waitUntilIdle() assertTrue(snackbarCalled) verify(appStore).dispatch(AppAction.AddPendingDeletionSet(history.toPendingDeletionHistory())) } @Test fun `WHEN items are deleted THEN undo dispatches action to remove them from pending deletion state`() = runTestOnMain { var snackbarCalled = false val history = setOf(History.Regular(0, "title", "url", 0, HistoryItemTimeGroup.timeGroupForTimestamp(0))) val middleware = HistoryStorageMiddleware( appStore = appStore, browserStore = browserStore, historyProvider = provider, historyStorage = storage, undoDeleteSnackbar = { _, undo, _ -> runBlocking { undo(history) } snackbarCalled = true }, onTimeFrameDeleted = { }, scope = this, ) val store = HistoryFragmentStore( initialState = HistoryFragmentState.initial, middleware = listOf(middleware), ) store.dispatch(HistoryFragmentAction.DeleteItems(history)).joinBlocking() store.waitUntilIdle() assertTrue(snackbarCalled) verify(appStore).dispatch(AppAction.AddPendingDeletionSet(history.toPendingDeletionHistory())) verify(appStore).dispatch(AppAction.UndoPendingDeletionSet(history.toPendingDeletionHistory())) } @Test fun `WHEN regular items are deleted and not undone THEN items are removed from storage`() = runTestOnMain { var snackbarCalled = false val history = setOf(History.Regular(0, "title", "url", 0, HistoryItemTimeGroup.timeGroupForTimestamp(0))) val middleware = HistoryStorageMiddleware( appStore = appStore, browserStore = browserStore, historyProvider = provider, historyStorage = storage, undoDeleteSnackbar = { _, _, delete -> runBlocking { delete(history) } snackbarCalled = true }, onTimeFrameDeleted = { }, scope = this, ) val store = HistoryFragmentStore( initialState = HistoryFragmentState.initial, middleware = listOf(middleware), ) store.dispatch(HistoryFragmentAction.DeleteItems(history)).joinBlocking() store.waitUntilIdle() assertTrue(snackbarCalled) verify(storage).deleteVisitsFor(history.first().url) } @Test fun `WHEN group items are deleted and not undone THEN items are removed from provider and browser store updated`() = runTestOnMain { var snackbarCalled = false val history = setOf(History.Group(0, "title", 0, HistoryItemTimeGroup.timeGroupForTimestamp(0), listOf())) val middleware = HistoryStorageMiddleware( appStore = appStore, browserStore = browserStore, historyProvider = provider, historyStorage = storage, undoDeleteSnackbar = { _, _, delete -> runBlocking { delete(history) } snackbarCalled = true }, onTimeFrameDeleted = { }, scope = this, ) val store = HistoryFragmentStore( initialState = HistoryFragmentState.initial, middleware = listOf(middleware), ) store.dispatch(HistoryFragmentAction.DeleteItems(history)).joinBlocking() store.waitUntilIdle() browserStore.waitUntilIdle() assertTrue(snackbarCalled) captureBrowserActions.assertFirstAction(HistoryMetadataAction.DisbandSearchGroupAction::class) verify(provider).deleteMetadataSearchGroup(history.first()) } @Test fun `WHEN a null time frame is deleted THEN browser store is informed, storage deletes everything, and callback is invoked`() = runTestOnMain { var callbackInvoked = false val middleware = HistoryStorageMiddleware( appStore = appStore, browserStore = browserStore, historyProvider = provider, historyStorage = storage, undoDeleteSnackbar = mock(), onTimeFrameDeleted = { callbackInvoked = true }, scope = this, ) val store = HistoryFragmentStore( initialState = HistoryFragmentState.initial, middleware = listOf(middleware), ) store.dispatch(HistoryFragmentAction.DeleteTimeRange(null)).joinBlocking() store.waitUntilIdle() browserStore.waitUntilIdle() assertTrue(callbackInvoked) assertEquals(HistoryFragmentState.Mode.Normal, store.state.mode) captureBrowserActions.assertFirstAction(RecentlyClosedAction.RemoveAllClosedTabAction::class) captureBrowserActions.assertLastAction(EngineAction.PurgeHistoryAction::class) {} verify(storage).deleteEverything() } @Ignore("Intermittent failure; see Bug 1848436.") @Test fun `WHEN a specified time frame is deleted THEN browser store is informed, storage deletes time frame, and callback is invoked`() = runTestOnMain { var callbackInvoked = false val middleware = HistoryStorageMiddleware( appStore = appStore, browserStore = browserStore, historyProvider = provider, historyStorage = storage, undoDeleteSnackbar = mock(), onTimeFrameDeleted = { callbackInvoked = true }, scope = this, ) val store = HistoryFragmentStore( initialState = HistoryFragmentState.initial, middleware = listOf(middleware), ) store.dispatch(HistoryFragmentAction.DeleteTimeRange(RemoveTimeFrame.LastHour)).joinBlocking() store.waitUntilIdle() browserStore.waitUntilIdle() this.advanceUntilIdle() assertTrue(callbackInvoked) assertFalse(store.state.isDeletingItems) captureBrowserActions.assertFirstAction(RecentlyClosedAction.RemoveAllClosedTabAction::class) captureBrowserActions.assertLastAction(EngineAction.PurgeHistoryAction::class) {} verify(storage).deleteVisitsBetween(anyLong(), anyLong()) } }